Introduzione
Nella prossima serie di articoli analizzeremo delle tematiche che completano il nostro tour nella realizzazione di un gioco in 2D (per quanto riguarda il codice vero e proprio, altri aspetti, come l’organizzazione del lavoro, saranno trattati più avanti).
Per prima cosa vorrei parlare dei videogiochi isometrici: in questo articolo vedremo brevemente come impostare questo effetto.
L’isometria infatti è una tecnica che permette di visualizzare un mondo tridimensionale da un angolo di 45 gradi (prendete per esempio Freeciv). L’unica limitazione è che non c’è prospettiva e quindi gli oggetti più lontani non diventano pù piccoli ed è anche per questo che si renderizza solo una parte dell’ambiente alla volta. Limitare la visuale infatti garantisce un realismo (non paragonabile al 3D) di gran lunga più appagante del semplice gioco in due dimensioni, ma sia ben chiaro che rimane sempre 2D (il che comporta qualche facilitazione).
Quello di cui abbiamo bisogno quindi è una gestione ottimale della profondità, che determina la visualizzazione degli oggetti sullo schermo. Inoltre tratteremo anche una tassellazione di base, ovvero renderemo il backgroud di gioco piastrellato e sensibile al puntatore/giocatore (che nel nostro caso sarà una sfera azzura mossa dal mouse, come potete vedere dall’immagine).
Codice
#!/usr/bin/python # -*- coding: utf-8 -*- import pygame,sys from pygame.locals import* pygame.init() screen = pygame.display.set_mode((640,480), DOUBLEBUF | HWSURFACE, 32) pygame.display.set_caption("Isometria") q = "quadrato.png" t = "terreno.png" cc = "cornicer.png" c = "cornice.png" a = "albero.png" aa = "ombraalbero.png" s = "sfera.png" quadrato = pygame.image.load(q).convert_alpha() terreno = pygame.image.load(t).convert_alpha() cornicer = pygame.image.load(cc).convert_alpha() cornice = pygame.image.load(c).convert_alpha() albero = pygame.image.load(a).convert_alpha() ombra = pygame.image.load(aa).convert_alpha() sfera = pygame.image.load(s).convert_alpha() orologio = pygame.time.Clock() pygame.mouse.set_visible(False) class giocatore(): def __init__(self,xy,img,screen): """ Inizializza il giocatore """ self.img = img self.coordinate = (xy[0]-24,xy[1]-24) self.screen = screen self.depht = self.coordinate[1]+48 self.rect = pygame.Rect((xy[0]-10,xy[1]+30),(28,18)) def move(self,xy): """ Aggiorna le coordinate del giocatore """ self.coordinate = (xy[0]-24,xy[1]-24) self.depht = self.coordinate[1]+48 def render(self): """ Renderizza il giocatore """ self.screen.blit(self.img,self.coordinate) class mouse(): def __init__(self,xy): """ Inizializza il puntatore """ self.img = pygame.Surface((1,1)) self.coordinate = xy self.mask = pygame.mask.from_surface(self.img) self.mask.fill() class tile(): def __init__(self,img,cor,ogg,om,xy,screen): """ Inizializza la piastrella """ img = pygame.transform.rotozoom(img,45.,1) img = pygame.transform.scale(img,(100,70)) cor = pygame.transform.rotozoom(cor,45.,1) cor = pygame.transform.scale(cor,(100,70)) self.img = img self.om = om self.cor = cor self.ogg = ogg self.coordinate = xy self.cooalbero = (xy[0]+27,xy[1]-10) self.dephtalbero = self.cooalbero[1]+55 self.screen = screen self.mask = pygame.mask.from_surface(self.img) self.collide = False def _collide(self,to): """ Gestisce le collisioni tra puntatore e piastrella """ if self.mask.overlap(to.mask, (to.coordinate[0]-self.coordinate[0],to.coordinate[1]-self.coordinate[1])): self.collide = True else: self.collide = False def render_ogg(self): """ Renderizza l'oggetto della piastrella """ self.screen.blit(self.ogg, self.cooalbero) def render_om(self): """ Renderizza l'obra dell'oggetto della piastrella """ self.screen.blit(self.om, self.cooalbero) def render_g(self): """ Renderizza lo sfondo della piastrella e la cornice """ self.screen.blit(self.img,self.coordinate) if self.collide == True: self.screen.blit(self.cor, self.coordinate) p = mouse((0,0)) g = giocatore((0,0),sfera,screen) lista = [] deltax = 100 deltay = 35 plusx = 0 plusy = 0 for y in range(0,13): for x in range(0,6): if y < 6: lista.append(tile(quadrato,cornice,albero,ombra,(x*deltax+plusx,deltay*y),screen)) else: lista.append(tile(terreno,cornicer,albero,ombra,(x*deltax+plusx,deltay*y),screen)) if y%2 == 0: plusx = 50 else : plusx = 0 while True: pos = (-1,-1) for event in pygame.event.get(): if event.type == QUIT: sys.exit() if event.type == MOUSEMOTION: pos = pygame.mouse.get_pos() p.coordinate = pos g.move(pos) tempo_p = orologio.tick(60) screen.fill((0,0,0)) for x in lista: x.render_g() for x in lista: x.render_om() g.render() for x in lista: x._collide(p) if x.collide == False: x.render_ogg() continue elif x.dephtalbero < g.depht: x.render_ogg() g.render() else: g.render() x.render_ogg() pygame.display.set_caption("Isometria FPS = "+str(orologio.get_fps())) pygame.display.flip()
Analisi
- Fino alla linea 30 non ci sono novità, importiamo pygame e tutto il necessario per l’esempio
- La classe giocatore definisce la nostra sfera blu che si muoverà nel bosco. In particolare bisogna notare la presenza del parametro depht, che corrisponde alla profondità dell’oggetto sullo schermo calcolando la base dell’immagine utilizzata (infatti sommiamo 48 perché l’immagine della sfera è alta 48 pixel; non dimenticate che l’asse delle ordinate è rivolto verso il basso). La profondità viene aggiornata con lo spostamento del giocatore, sempre in base all’immagine utilizzata. Da notare che teniamo conto solo delle coordinate sull’asse y.
- La classe mouse ci servirà per gestire le collisioni con le piastrelle dello sfondo. Infatti è composta solo da 1 pixel (per semplificare) che viene utilizzato dalla maschera. Quest’ultima è necessaria per le collisioni punto punto tra superfici; in questo caso sarà utilizzata per le collisioni tra il puntatore del mouse e la piastrella. Il puntatore non è visibile, ma corrisponderà al centro della nostra sfera.
- La classe tile corrisponde alla nostra piastrella. Quest’ultima memorizza la sua immagine (che sarà ruotata e schiacciata per dare l’effetto isometrico), l’immagine della cornice (quando viene selezionata), le proprie coordinate, l’oggetto che si trova su di essa e le sue coordinate, lo schermo dove deve renderizzare ed una variabile che memorizza se l’oggetto collide o no. Il metodo _collide() controlla se l’oggetto passato (ovvero il puntatore) collide con la maschera della piastrella. Le maschere non hanno coordinate spaziali ma sono “generate” tutte nell’angolo in alto a sinistra dello schermo, ovvero a partire dalle coordinate (0,0); ecco perché sottraggo alle coordinate del puntatore quelle della posizione della pistrella, così le coordinate saranno adatte a controllare se il pixel del puntatore si trova all’interno della maschera della piastrella. Le varie funzioni di render, renderizzano rispettivamente l’oggetto che si trova sulla piastrella (in questo caso un albero), l’ombra dell’oggetto e lo sfondo (cioè la piastrella stessa).
- Prima di entrare nel loop di gioco, prepariamo la tassellazione del terreno (metà terreno sarà rosso, mentre l’altra metà con una texture simile ad un terreno erboso, già utilizzata in precedenti esempi), il puntatore ed il giocatore. Non fate molto caso alla funzione utilizzata per generare il terreno, perché serve esclusivamente per questo esempio.
- Nel loop dei controlli aggiorniamo la posizione del giocatore e del puntatore.
- Nel loop principale possiamo vedere che si renderizza il gioco in questa sequenza: terreno, ombre degli oggetti, giocatore, albero con giocatore avanti o viceversa.
- Infine aggiorniamo lo schermo e controlliamo i frame per secondo che verrano visualizzati sul titolo della finestra.
Per prima cosa devo precisare che il giocatore viene renderizzato dopo le ombre poiché deve comparire sopra di esse nell’eventualità che il puntatore vada fuori dalla tasselazione.
La cosa più importante da notare è che il puntatore (quindi anche il giocatore, nel nostro caso), se non è presente in una specifica casella, quest’ultima renderizza solamente l’oggetto che ha sopra di essa; se invece il puntatore collide con la maschera della piastrella, allora si visualizza l’albero e la sfera in base alle loro ordinate, per capire chi deve andare prima o dopo.
L’effetto risultante sarà un primo approccio a questa tecnica non banale ma neanche di difficile utilizzo.
Conclusioni
Come avete ben capito, per gestire la “profondità” di gioco, c’è bisogno di “qualcuno” che decida come renderizzare le immagini sullo schermo. Inoltre in questo tipo di videogame, abbiamo bisogno anche di un gestore del backgroud per le eventuali operazioni che si andranno a fare su di esso, come gli spostamenti degli oggetti e/o dei personaggi, selezioni ecc…
Questo problema è di fondamentale importanza e va affrontato prima di qualsiasi altra cosa. Pensate infatti che scelte prese su questo “gestore” (per ora definiamolo così) andranno ad influire tutta la meccanica di gioco.
Con questo esempio ho lasciato aperte molte porte per non limitare la vostra fantasia, poiché non esistono solo piastrelle quadrate, ma il metodo che abbiamo visto in precedenza per le collisioni con il puntatore (per individuare il tassello selezionato), funziona bene lo stesso (se qualcuno ha un’idea più veloce, che funzioni con qualsiasi tipo di piastrella come quella che ho scritto, lo prego di scrivermi).
Detto questo non mi resta che darvi appuntamento al prossimo articolo e mi scuso per il ritardo.
Sorgenti:
- Isometria 0 : http://dl.dropbox.com/u/16546001/AD/Isometria%200.rar