Sviluppare un gioco in Python: isometria, Seconda parte.

Introduzione

In questa parte vedremo le migliorie che si possono apportare all’engine isometrico, per non perdere prestazioni e per rendere più malleabile la creazione del gioco.

Per il momento ho eliminato il giocatore, ma ho lasciato la possibilità di selezionare e deselezionare le piastrelle (tasto sinistro del mouse) ed ora è anche possibile creare un oggetto (in questo caso un albero) tramite il tasto destro del mouse, su una data piastrella. Il terreno viene caricato seguendo una stringa che indica il tipo di tile che dovrà essere visualizzata. Sarà anche possibile scorrere la mappa con le frecce direzionali, visto che ora è più grande del campo visivo messo a disposizione dalla finestra.

Tutte queste piccole cose, oltre a fornirci una solida base per le prossime puntate, sono di vitale importanza per capire l’isometria e i suoi principi. Resta il fatto che le soluzioni proposte sono molto generali e che quindi possono essere applicate a qualsiasi progetto abbiate in mente. Non è stato facile trovare delle applicazioni utili per i più disparati casi, ma penso di esserci riuscito, se non altro concettualmente; c’è da sottolineare anche il fatto che si possono prendere delle scelte inerenti solamente al gioco da voi sviluppato: per esempio scegliere delle piastrelle quadrate od esagonali può portare a delle semplificazioni notevoli, ma dipende sempre se il nostro gioco si sviluppa con un assonometria di 45° o semplicemente in verticale.

Per ricoprire una casistica più vasta ho quindi utilizzato tecniche più generali e pochi trucchi (soltanto quelli necessari a far alzare il frame rate).

Codice

1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3 
4import pygame
5from sys import exit
6from pygame.locals import *
7 
8s_terreno = """ 2 2 2 2 2 2 2 2 2 2
92 2 2 2 2 2 2 2 2 2
102 1 1 1 1 1 1 1 1 2
112 1 1 1 1 1 1 1 1 2
122 1 1 1 1 1 1 1 1 2
132 1 1 1 1 1 1 1 1 2
142 1 1 1 1 1 1 1 1 2
152 1 1 1 1 1 1 1 1 2
162 1 1 1 1 1 1 1 1 2
172 1 1 1 1 1 1 1 1 2
182 1 1 1 1 1 1 1 1 2
192 1 1 1 1 1 1 1 1 2
202 1 1 1 1 1 1 1 1 2
212 1 1 1 1 1 1 1 1 2
222 2 2 2 2 2 2 2 2 2
232 2 2 2 2 2 2 2 2 2"""
24 
25class oggetto():
26    """
27    Classe che rappresenta un oggetto sulla piastrella
28    """
29    def __init__(self,imm,xy,screen):
30        """
31        Inizializzazione dell'oggetto
32        """
33        self.aggiorna = True
34        self.imm = imm
35        self.xy = xy
36        self.rect = pygame.Rect(self.xy,imm.get_size())
37        self.screen = screen
38 
39    def render(self):
40        """
41        Rendering dell'oggetto
42        """
43        self.screen.fill((0,0,0),self.rect, BLEND_MAX)
44        self.screen.blit(self.imm,self.xy)
45        self.aggiorna = False
46 
47class pointocollide():
48    """
49    Classe che rappresenta il punto di collisione del puntatore del mouse
50    """
51    def __init__(self):
52        """
53        Inizializza la mashera
54        """
55        self.mask = pygame.mask.from_surface(pygame.Surface((1,1)))
56        self.mask.fill()
57        self.rect = pygame.Rect((0,0),(150,105))
58 
59class tile():
60    """
61    Classe che rappresenta la piastrella sullo schermo
62    """
63    def __init__(self,img,cor,xy,screen):
64        """
65        Inizializzazione della piastrella.
66        """
67        self.aggiorna = True
68        self.selected = False
69        self.collide = False
70        self.walkable = True
71        img = pygame.transform.rotozoom(img,45.,1)
72        img = pygame.transform.scale(img,(100,70))
73        cor = pygame.transform.rotozoom(cor,45.,1)
74        cor = pygame.transform.scale(cor,(100,70))
75        self.cor = cor
76        self.img = img
77        self.screen = screen
78        self.xy = xy
79        self.center = (self.xy[0]+50,self.xy[1]+35)
80        self.rect = pygame.Rect(xy,(100,70))
81        self.mask = pygame.mask.from_surface(self.img)
82 
83    def collide_mask(self,punto):
84        """
85        Ritorna true se il punto collide con la piastrella
86        """
87        x,y = self.xy
88        z,t = punto
89        if self.mask.overlap( mousepoint.mask , (z-x,t-y)):
90            self.collide = True
91            self.aggiorna = True
92            return self.collide
93        else:
94            self.collide = False
95            self.aggiorna = True
96            return self.collide
97 
98    def render(self):
99        """
100        Rendering della piastrella
101        """
102        self.screen.fill((0,0,0),self.rect, BLEND_MAX)
103        self.screen.blit(self.img,self.xy)
104        if self.selected or self.collide:
105            self.screen.blit(self.cor,self.xy)
106        pygame.draw.rect(self.screen,(255,255,0),(self.center,(2,2)),5)
107        self.aggiorna = False
108 
109class iso_engine():
110    """
111    Semplice engine per gestire l'isometria
112    """
113    def __init__(self,screen,terr,acq,cor,tipi):
114        """
115        Inizializza il motore isometrico
116        """
117        self.aggiorna = True
118        self.tiles_list = []
119        self.level = []
120        self.tipi = tipi
121        self.anim = 25
122        self.screen = screen
123        self.list_visibili_tiles = []
124        self.list_visibili_level = []
125        self.rect_visibili = pygame.Rect((0,0),(640,480))
126        self.coordinate_rect = (-1,-1)
127        self.prev_collide = []
128 
129        temp = s_terreno.split('\n')
130        for x in range(0,len(temp)):
131            temp[x] = temp[x].split()
132 
133        x_d = 0
134        y_d = 0
135        for x in range(0,len(temp)):
136            self.tiles_list.append([])
137            self.level.append([])
138            current = temp[x]
139            for y in range(0,len(current)):
140                if current[y] is '1':
141                    self.tiles_list[x].append(tile(terr,cor,(x_d+100*y,y_d),screen))
142                elif current[y] is '2':
143                    self.tiles_list[x].append(tile(acq,cor,(x_d+100*y,y_d),screen))
144                    self.tiles_list[x][y].walkable = False
145                self.level[x].append(0)
146            y_d+=35
147            x_d+=50
148 
149    def aggiorna_visibili(self,cgame):
150        """
151        Aggiorna la lista delle piastrelle visibili
152        """
153        if self.coordinate_rect != cgame:
154            x,y = self.coordinate_rect
155            z,t = cgame
156            self.rect_visibili.move_ip(x-z,y-t)
157            self.coordinate_rect = cgame
158 
159        del self.list_visibili_level[:]
160        del self.list_visibili_tiles[:]
161 
162        for x in range(0,len(self.tiles_list)):
163            for y in range(0,len(self.tiles_list[x])):
164                if self.tiles_list[x][y].rect.colliderect(self.rect_visibili):
165                        self.list_visibili_tiles.append(self.tiles_list[x][y])
166                        self.list_visibili_level.append(self.level[x][y])
167 
168    def add_oggetto(self,pun,type):
169        """
170        Aggiunge un oggetto in base al tipo passato nella
171        piastrella selezionata
172        """
173        for x in range(0,len(self.tiles_list)):
174            for y in range(0,len(self.tiles_list[x])):
175                if self.tiles_list[x][y].collide_mask(pun):
176                    if self.tiles_list[x][y].walkable:
177                        self.tiles_list[x][y].aggiorna = True
178                        self.tiles_list[x][y].walkable = False
179                        z,t = self.tipi[type].get_size()
180                        i = self.tiles_list[x][y].center[0] - z/2
181                        p = self.tiles_list[x][y].center[1] - t+18
182                        self.level[x][y] = oggetto(self.tipi[type],(i,p),self.screen)
183                        self.aggiorna = True
184 
185    def selection(self,pun):
186        """
187        Seleziona e deseleziona una piastrella
188        """
189        for x in self.prev_collide:
190            if x.collide_mask(pun):
191                x.aggiorna = True
192                if x.selected:
193                    x.selected = False
194                else:
195                    x.selected = True
196 
197    def collide(self,pun):
198        """
199        Aggiorna le tiles per visualizzare la selezione del mouse
200        """
201        if len(self.prev_collide)>0:
202            for x in self.prev_collide:
203                x.aggiorna = True
204 
205        del self.prev_collide[:]
206 
207        for x in self.list_visibili_tiles:
208            if x.rect.colliderect(mousepoint.rect):
209                x.aggiorna = True
210                self.prev_collide.append(x)
211 
212        for x in self.prev_collide:
213            if x.collide_mask(pun):
214                x.aggiorna = True
215 
216    def aggiorna_blitter(self):
217        """
218        Aggiorna lo schermo in base alle piastrelle che
219        devono essere aggiornate
220        """
221        var = orologio.get_time()
222        #print var # For debug
223        if self.aggiorna and self.anim >= 50:
224            #print "\n aggiornato \n" # For debug
225            for x in self.list_visibili_tiles:
226                #print x.aggiorna, # For debug
227                if x.aggiorna:
228                    x.render()
229            for x in self.list_visibili_level:
230                if x != 0:
231                    x.render()
232            self.aggiorna = False
233            self.anim = 0
234        else:
235            self.anim+= var
236 
237    def first_render(self):
238        """
239        Primo rendering
240        """
241        for x in range(0,len(self.tiles_list)):
242            for y in range(0,len(self.tiles_list[x])):
243                self.tiles_list[x][y].render()
244        for x in range(0,len(self.level)):
245            for y in range(0,len(self.level[x])):
246                if self.level[x][y] != 0:
247                    self.level[x][y].render()
248 
249def run():
250    pygame.init()
251    pygame.key.set_repeat(25,10)
252    screen = pygame.display.set_mode((640,480), DOUBLEBUF | HWSURFACE, 32)
253    sup_terr = pygame.Surface((1750,600), DOUBLEBUF | HWSURFACE, screen)
254    pygame.display.set_caption("Isometria")
255 
256    global orologio
257    orologio = pygame.time.Clock()
258 
259    global mousepoint
260    mousepoint = pointocollide()
261 
262    t = "terreno.png"
263    terreno = pygame.image.load(t).convert_alpha()
264    cc = "cornicer.png"
265    cornicer = pygame.image.load(cc).convert_alpha()
266    a = "albero_plus.png"
267    albero = pygame.image.load(a).convert_alpha()
268    s = "sfera.png"
269    sfera = pygame.image.load(s).convert_alpha()
270    ac = "acqua.png"
271    acqua = pygame.image.load(ac).convert_alpha()
272 
273    game = (0,0)
274    mousepoint.rect.center = game
275    puntatore = (0,0)
276    ENGINE = iso_engine(sup_terr,terreno,acqua,cornicer,[albero,sfera])
277    ENGINE.first_render()
278    ENGINE.aggiorna_visibili(game)
279 
280    while True:
281        for event in pygame.event.get():
282            if event.type == QUIT:
283                exit()
284 
285            if event.type == MOUSEBUTTONDOWN:
286 
287                pulsanti_mouse = pygame.mouse.get_pressed()
288 
289                if pulsanti_mouse[0]==1:
290                    x,y = pygame.mouse.get_pos()
291                    z,t = game
292                    puntatore = (x-z,y-t)
293                    ENGINE.selection(puntatore)
294 
295                elif pulsanti_mouse[2]==1:
296                    x,y = pygame.mouse.get_pos()
297                    z,t = game
298                    puntatore = (x-z,y-t)
299                    ENGINE.add_oggetto(puntatore,0)
300                    ENGINE.aggiorna_visibili(game)
301                    ENGINE.aggiorna = True
302 
303            if event.type == KEYDOWN:
304                tasti_premuti = pygame.key.get_pressed()
305 
306                if tasti_premuti[K_ESCAPE]:
307                    exit()
308                elif tasti_premuti[K_LEFT]:
309                    ENGINE.aggiorna_visibili(game)
310                    ENGINE.aggiorna = True
311                    x,y = game
312                    x+= 10
313                    game = (x,y)
314                elif tasti_premuti[K_RIGHT]:
315                    ENGINE.aggiorna_visibili(game)
316                    ENGINE.aggiorna = True
317                    x,y = game
318                    x-= 10
319                    game = (x,y)
320                elif tasti_premuti[K_UP]:
321                    ENGINE.aggiorna_visibili(game)
322                    ENGINE.aggiorna = True
323                    x,y = game
324                    y+= 10
325                    game = (x,y)
326                elif tasti_premuti[K_DOWN]:
327                    ENGINE.aggiorna_visibili(game)
328                    ENGINE.aggiorna = True
329                    x,y = game
330                    y-= 10
331                    game = (x,y)
332 
333            if event.type == MOUSEMOTION:
334                x,y = pygame.mouse.get_pos()
335                z,t = game
336                puntatore = (x-z,y-t)
337                mousepoint.rect.center = puntatore
338                ENGINE.collide(puntatore)
339                ENGINE.aggiorna = True
340 
341        tempo_p = orologio.tick(60)
342        screen.fill((150,150,150))
343        ENGINE.aggiorna_blitter()
344        screen.blit(sup_terr,game)
345        pygame.display.set_caption("Isometria FPS = "+str(orologio.get_fps()))
346        pygame.display.flip()
347 
348if __name__ == '__main__':
349    exit(run())

Analisi

s_terreno = Questa stringa rapprensenta il terreno piastrellato in base al suo contenuto. Per questo esempio avremo che il 2 rappresenza il mare e 1 la terra. Sarà quindi molto semplice creare una qualsiasi mappa secondo le vostre necessità e tipi di celle, naturalmente con qualche piccola modifica.

class oggetto() = L’oggetto sulla piastrella è rappresentato da questa classe. Dell’oggetto memorizziamo poche informazioni, ma può essere ampliato secondo le necessità.

class pointocollide() = Per gestire le collisioni con il puntatore del mouse ci appoggeremo a questa classe. Come potete vedere, il puntatore avrà sia la sua parte mask che quella rect.

class tile() = Abbiamo già visto questa classe nella precedente puntata. Gli unici cambiamenti degni di nota sono la variabile booleana walkable, per determinare se la piastrella è percorribile o meno, ed il rendering, che è stato aggiornato per disegnare sullo schermo solo la parte coperta dalla piastrella. Questo era già possibile da prima, visto che le piastrelle non cambiano di dimensione ed utilizzano il canale alpha, ma l’istruzione fill precedente cancellava tutto il contenuto all’interno del rect dell’immagine, mentre ora si cancellano solo i pixel interessati grazie a blend_max. Questo flag (ce ne sono anche altri) può essere utilizzato anche in blit per dare alcuni effetti visivi, come per esempio lo schiarimento di un’immagine. Infine memorizziamo anche il centro della piastrella.

class iso_engine() = La gestione del rendering isometrico è gestita completamente da questa classe che illustrerò in dettaglio.

  • self.aggiorna indica se lo schermo deve essere aggiornato  o meno.
  • self.tiles_list memorizza tutte le piastrelle che interessano il videogioco.
  • self.level memorizza gli oggetti che sono presenti sulle piastrelle.
  • self.tipi indica i vari tipi di oggetti che si possono inserire.
  • self.anim indica il tempo di refresh delle animazioni.
  • self.screen è lo schermo dove si andrà a renderizzare.
  • self.list_visibili_tiles contiene tutte le piastrelle visibili in quel momento.
  • self.list_visibili_level contiene tutti gli oggetti visibili in quel momento.
  • self.rect_visibili è un oggetto rect che rappresenta la visuale del giocatore. Tramite esso sarà stabilito quali pistrelle ed oggetti sono visibili.
  • self.coordinate_rect sono le coordinate della “telecamera” creata con rect_visibili.
  • self.prev_collide contiene la lista degli elementi da aggiornare al contatto con il puntatore.

Fatto il parsing della stringa, avremo creato il nostro campo con celle attraversabili e non e tutte avranno un certo indice che corrisponderà alla loro posizione allinterno della lista delle piastrelle. Per gli oggetti vale lo stesso principio, con la differenza che all’inizio non mettiamo nulla (quindi 0), ma si potrebbe facilmente creare una seconda stringa che indica la posizione degli oggetti ed il loro tipo per aggiornarla sin dall’inizio del gioco, ma per ora abbiamo previsto solo la possibilità di aggiungere un oggetto ad una piastrella che non ne ha.

  • def aggiorna_visibili() = questa funzione aggiorna la lista degli oggetti e delle celle visibili in base alla “telecamera del giocatore”. Se la telecamera viene spostata si devono prima calcolare le nuove coordinate e poi aggiornare le liste. Questa operazione viene effettuata solo se c’è uno spostamento o si aggiunge un oggetto sul terreno ed utilizzando solo i rect, non spreca molte risorse e ci semplifica molto il lavoro successivo. Da notare che gli oggetti e le piastrelle sono legati sempre dallo stesso indice, altrimenti sarebbe impossibile riferirsi all’oggetto giusto su una data casella.
  • def add_oggetto() = questa funzione aggiunge un oggetto sulla cella puntata dal mouse. L’oggetto viene aggiunto nella lista level, quindi quella che contiene tutti gli oggetti.
  • def selection() = utilizzata per selezionare o deselezionare una piastrella. Utilizza la lista delle piastrelle di collide, visto che sono le uniche in prossimità del mouse.
  • def collide() = gestisce le collisioni tra il puntatore e le piastrelle. Da notare che prima di calcolare le collisioni punto per punto, si cerca di ridurre la zona di lavoro vedendo in che parte dello schermo si trova il puntatore; il tutto grazie a mousepoint.rect. In questo modo abbiamo solo le celle che dobbiamo aggiornare e non sprecheremo ulteriori controlli più pesanti. Infatti controllare le collisioni con mask è molto più dispendioso del farlo con i rect, per questo si preferisce “cadere” prima vicino la zona interessata per poi scegliere tra pochi quello giusto.
  • def aggiorna_blitter() = la funzione di aggiornamento dello schermo è stata già prevista di una sincronia per le animazioni, anche se non ne gestiamo nessuna. In tal caso, l’importante sarà determinare quali celle ed oggetti sono da aggiornare e spuntare i relativi flag di aggiornamento con una funzione apposita.
  • def first_render() = utilizzata per il primo rendering del gioco, poi basterà soltanto aggiornare i cambiamenti.

def run() = nella funzione di main ci sono alcune cose da puntualizzare: per esempio il gioco vero e proprio ora si trova sulla superfice sup_terr, mentre lo schermo del gioco è screen. Quindi se spostiamo la mappa di gioco aggiorniamo solo la posizione di sup_terr rispetto a screen, senza aver bisogno di spostare fisicamente le piastrelle. Naturalmente si dovrà tenere conto di questo memorizzando l’attuale posizione di sup_terr tramite la variabile game.

Conclusioni

Per non perdere prestazioni, in sostanza, si è cercato di ridurre il quantitativo di oggetti su cui si deve lavorare. Utilizzando prima dei controlli più veloci per scremare i risultati si guadagna moltissimo ed il tutto è anche semplice da maneggiare. Spero che il meccanismo di funzionamento sia abbastanza chiaro poiché, come ho già detto, queste soluzioni possono essere apportate in un qualsiasi progetto perché molto generali e non si utilizzano funzioni specifiche per la renderizzazionde del gioco.

Dovete porre particolare attenzione anche all’ordine di renderizzazione degli oggetti e al loro spazio occupato sullo schermo sia come immagini che come oggetti rect, altrimenti non riuscirete a discricarvi in questo scenario. Prossimamente vedremo come far muovere il giocatore in questo ambiente in “falso 3D”, di seguito lascio il link per l’esempio di questa parte:

Isometria 1 : http://tinyurl.com/3kmwmey

Press ESC to close