Del creador de "Back To Kowloon"

dijous, 28 d’octubre del 2010

Plantant herba

Podeu descarregar el paquet corresponent a aquest post d'aqui -> tar.gz rar
(Recordeu que el codi estarà disponible en aquesta web)

Si, plantarem herba. Herba virtual, però. I de la que no es fuma ¬_¬

I això com pot ser, eh?

Doncs sent. Avui pintarem gespa a la finestreta del joc. Aprendrem a tractar imatges per mostrar-les en pantalla. I farem una parida amb el text, per entendre una mica com funciona Rect.

El codi s'ha dividit en tres fitxers (i més que en vindran) per una qüestió de sentit comú. Hi ha gent que potser creu que és millor ficar tot en un únic arxiu. Jo no ho penso.

Així doncs, tenim tres fitxers de codi: game.py, scene.py i text_panel.py. El primer és el nucli del programa (el que teníem fins ara, en gran part), el segon representa l'escenari del joc i el tercer representa un panel de text. No calia descriure-ho, oi? Gràcies.

En acabar, si seguiu els passos, tindreu això (o similar):

OH MY FUCKIN' GOOOOD!!!! O_O
Ostres, quina passada!!!! Explica ja el que ens interessa, pesat!

Primerament tenim scene.py. Pensem primer en què representa: un objecte de tipus scene representa l'escenari on es mourà el personatge. Tots els que hem jugat a algun RPG clàssic en 2D tenim una imatge clara del que representa. Els que no, podeu mirar la captura anterior...és la part verda. I com implementem això?

Doncs bé, potser hi ha dos cents mètodes millors que aquest, però jo no els conec. Així que imagineu que la vostra finestra està dividida en caselles iguals, formant una graella. La "casella" que jo faig servir té una mida de 32x32 pixels. Per tant, imagineu una graella de 20x15 sobre la finestra. Així:

Tranquils, no cal que imagineu
Tenint això, sembla fàcil. Només és qüestió de pintar una a una les caselles que ens interessi. Si es fan servir caselles d'altres mides (per exemple 128x128), la graella serà diferent.  Però el mètode és el mateix. Fins i tot fent servir tiles (així es diuen en anglès) rectangulars o d'altres formes, el mètode és el mateix o semblant (però es pot complicar bastant si sortim dels quatre costats).  

Perfecte, com fem això?

Observeu el codi següent, pertanyent a scene.py.
class scene:
  
    def __init__(self):
        self.tile = pygame.image.load("../data/tiles/grass.bmp").convert_alpha()
           
    def paint_scene(self,screen):   
        for x in range (1, 19):
            for y in range (1, 10):
                screen.blit(self.tile, (x*32, y*32))
Què tenim aquí? Doncs bé, tenim dos mètodes. El primer s'executa en crear un objecte de tipus scene. El que fa és carregar una imatge al atribut tile. De moment, pintarem tota la graella amb la mateixa imatge per simplificar. En el futur veurem com manegar un tileset. Un tileset és el resultat d'aplegar un munt de caselles diferents com la que fem servir avui (tile) en una mateixa imatge. Llavors es carrega aquesta imatge única a la memòria i després es van seleccionat parts (de 32x32 en el nostre cas) segons convingui. Per a aquest propòsit, pygame permet definir múltiples sub-surfaces d'un objecte Surface, que representen una part d'aquesta superfície (Surface) i n'hereten la informació. Però això no ve al cas ara.

Cal no oblidar convert_alpha(). El canal alpha d'una imatge permet definir el grau de transparència d'aquesta. Com que cada format d'imatge (bmp, png, jpg, ...) el guarda de forma diferent (si es que el guarda), cal aplicar convert_alpha() per tal de mostrar les transparències correctament. De moment, no en tenim...però més endavant veurem que ens resulta molt útil, per exemple, per dibuixar al personatge sobre l'escenari.

D'acord. Així doncs, en crear un objecte scene, se li adjudica automàticament una imatge.

Però, per què declares l'atribut com a self.tile i no com a tile?

Bona pregunta. Proveu a declarar sense posar la paraula clau self.

Bé, anem per feina. Què fa el segon mètode? Retorneu a la imatge de la graella. Teníem una graella de 20x15, oi? Molt bé. Si la volguéssim pintar tota sencera, podríem anar pintant una per una totes les quinze caselles de la primera fila. Després les de la segona fila, la tercera, etc...fins al final de la columna 20.

Feu la prova. Per pintar una única casella, el mètode paint_scene() només ha de fer el següent:

     def paint_scene(self,screen):   
         screen.blit(self.tile, (0*32,0*32))
Si volem pintar qualsevol altre casella, només cal canviar els 0 pel nombre de fila/columna corresponent. Feu proves.

I si volem pintar tota una fila o tota una columna? Doncs cal fer un bucle que passi casella per casella. Així:
    def paint_scene(self,screen):   
        for x in range (0, 20):
                screen.blit(self.tile, (x*32, y*32))
Tenim un bucle for que funciona mentre s'acompleix la condició 0<=x<=20 i que, per tant, pinta una per una totes les caselles de la fila. Per pintar columnes només cal posar y enlloc de x al bucle for. Però el que a nosaltres ens interessa és que les pinti totes, no? Llavors cal que després de pintar tota la primera fila, pinti la segona. O també, pintar la primera columna sencera, i passar a la segona. Justament això és el que fa el segon mètode. Pinta columna per columna fins omplir tota la graella amb un bucle dins d'un altre bucle. Els que feu recorreguts de matrius, ja sabeu de què parlo eh?

Podeu pintar la graella com us doni la gana. Jo he decidit donar al meu joc un aspecte retro "posant" un marc al voltant de l'escenari. Més endavant potser ho canviaré i utilitzaré les 20 columnes deixant només les files buides al damunt i a sota. Això si no em dona per posar més resolució.

Si us hi fixeu, aquest mètode utilitza self.tile enlloc de tile un altre cop. La paraula clau self equival al this de Java i al self de C. Si no declarem tile com a self.tile, llavors paint_scene() no el veu i no hi pot accedir.

Perfecte, diria que ho he entès. Què més tens?

Que pesats que sou, segur que no heu entès una merda ¬_¬ Mireu l'aspecte del nou main().

def main():
    screen = pygame.display.set_mode((640,480))
    clock=pygame.time.Clock()

    scn = scene()
    tpanel = text_panel()
    active=True
   
    while active:
        clock.tick()
        pygame.display.set_caption("Classic RPG v0.01"+ " FPS: "+ str(clock.get_fps()))
       
        scn.paint_scene(screen)
       
        for event in pygame.event.get():
            if event.type==pygame.QUIT:
                active=False       
            elif event.type == KEYDOWN and event.unicode == 'q':
                tpanel.hide(screen)
            
        pygame.display.update()
        pygame.time.wait(15)
Hi ha unes quantes diferències, eh? Primerament s'ha inclós un atribut clock per mesurar els FPS (fixeu-vos en l'afegit a set_caption()). Només cal cridar a tick() un cop per a cada iteració del bucle del programa. La crida a wait() està posada per "simular" que el programa fa coses. Si la treieu, veureu que els FPS es disparen n_n 

Si, és una pijada. Ningú us hi obliga a posar-ho, però és útil per veure quin marge es té en quant a rendiment. Més endavant ho entendreu, petits padawans.

A més d'això s'han creat els objectes scene() i text_panel() corresponents i dintre del bucle es crida a paint_scene() per tal de pintar la gespa (o el que hagueu posat). I més endavant...
            elif event.type == KEYDOWN and event.unicode == 'q':
                tpanel.hide(screen)

Què coi és això?

Una altra pijada. Quan el programa detecta que hem premut la tecla Q, crida a hide(). És una funció de text_panel que mostra i oculta el panell de text. Ens serà útil més endavant per amagar el text quan el personatge s'estigui movent i tal. O quan tinguem més d'un panell i ens interessi anar passant  d'un a altre. I, a més, podreu ensenyar a les amigues que la vostra merda de programa fa coses. Al cap i a la fi, això és el que importa -_-'

Sempre m'he preguntat com s'ho devien manegar per fer aquestes coses.

Si? Doncs, amb màgia :D 
def hide(self, screen):
        if self.active:
            screen.fill((0,0,0),self.textRect)
            self.textRect.move_ip(0,300)
            self.draw_text(screen)
            self.active = False
        else:
            self.textRect.move_ip(0,-300)
            self.draw_text(screen)
            self.active = True
Us adoneu del que fa? Quan el panell està marcat com a actiu, pinta de negre a sobre i mou textRect (ara us ho explico, tranquils) fora de la part visible de la finestra. I quan detecta que el panell està inactiu, el que fa es tornar a posar el rectangle textRect a la seva posició original. Cada crida a display.update() s'encarrega de mostrar els canvis per pantalla en detectar que hi ha hagut canvis en algunes zones. Recordeu que display.flip(), a diferència de update(), actualitzaria la finestra sencera (amb la consegüent pèrdua de rendiment).

És ben ridícul, oi? Doncs us foteu, se m'ha acudit a mi primer n_n

Si aneu una mica més amunt en el codi...
        font=pygame.font.Font("../data/fonts/Final Fantasy I - NES.ttf",46)
        self.text=font.render("Em penso que encara no m'has dit el teu nom...", 1, (255,255,255))
        self.textRect=self.text.get_rect()
        self.textRect.x,self.textRect.y=32,650
Si us fixeu, he ficat el text en un objecte Rect fent servir get_rect(). Per què he fet això? Molt senzill, abans havíem "incrustat" el text a la finestra. Ficant-lo en un rectangle, el tenim "per sobre" de la finestra. És una capa independent que podem moure i editar sense modificar el fons. Això ens permet, justament, amagar-lo fora de la finestra amb hide().

I si, he fet servir la font del FF1 original. Sóc així de freak. I les tiles són del RPG Maker original, per si us cou per dins el dubte. Teniu un parell de links (és a dir, dos) al final del post amb webs sobre el tema. Els que he fet servir jo són gratuits i lliures, per que no són originals (son imitacions).

Què guai, no? Jugueu amb les ilusions dels gamers ¬_¬

Si us pensàveu que els programadors es trencaven el cap fent coses gairebé impossibles...doncs potser el vostre problema és que esteu seguint al programador equivocat.

Ja està bé per avui. Hauria estat bé afegir algun nino i fer-lo moure per la pantalla. Però com veureu, això si que representa una feinada. Posar el nino és ben fàcil, però després de moure'l, cal actualitzar la pantalla repintant les parts per on passa.  Es mereix un post sencer. Jo encara no ho se fer, i vosaltres?

Només em queda remarcar que m'heu fet perdre una tarda d'estudi. Ja us val ¬_¬


Wu Ying Ren 死

Enllaços d'interès: