Sviluppare un gioco in Python: parallax scrolling.

Introduzione

Alcune tipologie di videogiochi utilizzano una tecnica chiamata parallax scrolling per dare al giocatore la sensazione di percorrere (attraversare) un mondo persistente, che può anche non avere fine (ma quest’ultimo aspetto non interessa il parallasse, è solo un effetto che si utilizza spesso in concomitanza). A volte questa tecnica viene solo utilizzata negli sfondi di videogiochi a scorrimento (non solo come sfondo) o side-scrolling game, per aggiungere uteriore realismo. Anche giochi belt scrolling possono averla integrata, visto che segue gli stessi principi, ma con regole diverse per i movimenti del giocatore (che si può muovere anche verso l’alto o verso il basso ma come se fosse dentro una scatola).

Un gioco a scorrimento è di facile implementazione (come abbiamo visto per esempio negli articoli dell’isometria), basta tenere conto di come muovere la telecamera ed il giocatore rispetto al mondo che lo circonda. In base a queste scelte è in oltre possibile dare un tocco di realismo se si cerca di far muovere le cose che sono più lontane dal giocatore più lentamente, dando una certa profondità all’immagine. Questo è detto parallax scrolling, ovvero gli oggetti si muovono ad una certa velocità in base al livello (di profondità) in cui si trovano (praticamente quanto sono lontani dal giocatore).

Naturalmente è solo un’impressione, in realtà siamo sempre in due dimensioni e quindi non si raggiungeranno mai gli stessi risultati del 3D (come valeva per l’isometria), ma in compenso vedremo come creare questa tipologia di videogiochi, graficamente più appaganti.

L’impostazione che diamo al motore che gestisce questo effetto ricorda molto quello per l’isometria. I vari livelli di profondità verranno gestiti molto semplicemente in questo esempio, organizzando quindi il render in base alla posizione dell’oggetto sullo schermo. In un engine completo si dovrebbero trattare questi come veri e propri livelli o, se parte del backgroud del gioco, in una sezione separata.

Nel nostro esempio il giocatore sembrerà percorrere un paesaggio (in realtà è completamente fermo) che non finisce mai in entrambe le direzioni percorribili (cioè verso destra e verso sinistra). Per rendere l’idea si osservi la seguente immagine:

Il background è ripetuto tre volte ed ha una certa linea che delimita la fine di quel settore. La telecamera prenderà al massimo due di essi, visto che è grande quanto un singolo settore e quindi si dovrà aggiornare opportunamente il background per non farlo terminare. La soluzione proposta è molto semplice e verrà illustrata di seguito:

#!/usr/bin/python
# -*- coding: utf-8 -*-

import pygame
from giocatore import giocatore

class oggetto():
    """
    Classe che rappresenta un oggetto del panorama
    """
    def __init__(self,imm,vel,screen,xy = (0,0)):
        """
        Inizializza l'oggetto
        """
        self.imm = imm
        self.xy = xy
        self.vel = vel
        self.screen = screen

    def muovi(self,var):
        """
        Muove l'oggetto a seconda della propria velocità
        """
        var = var*self.vel
        x,y = self.xy
        x = (x+var)
        self.xy = (x,y)

    def render(self):
        """
        Renderizza l'oggetto
        """
        self.screen.blit(self.imm,self.xy)

class parallax_engine():
    """
    Classe che gestisce la visuale in parallasse
    """
    def __init__(self,screen,orologio):
        """
        Inizializza l'engine
        """
        self.screen = screen

        self.sfondo = [(oggetto(pygame.image.load("sfondo.png").convert_alpha(),0,self.screen,(0,0)))]
        self.terreno = [(oggetto(pygame.image.load("_1.png").convert_alpha(),350,self.screen,(-640,0))),
                        (oggetto(pygame.image.load("_1.png").convert_alpha(),350,self.screen,(0,0))),
                        (oggetto(pygame.image.load("_1.png").convert_alpha(),350,self.screen,(640,0)))]

        self.mont = [(oggetto(pygame.image.load("_2.png").convert_alpha(),150,self.screen,(-640,0))),
                     (oggetto(pygame.image.load("_2.png").convert_alpha(),150,self.screen,(0,0))),
                     (oggetto(pygame.image.load("_2.png").convert_alpha(),150,self.screen,(640,0)))]

        self.nuvole = [(oggetto(pygame.image.load("clouds.png").convert_alpha(),25,self.screen,(-640,0))),
                       (oggetto(pygame.image.load("clouds.png").convert_alpha(),25,self.screen,(0,0))),
                       (oggetto(pygame.image.load("clouds.png").convert_alpha(),25,self.screen,(640,0)))]

        self.sole = oggetto(pygame.image.load("sun.png").convert_alpha(),10,self.screen,(0,0))

        self.player = giocatore(pygame.image.load("sfera.png").convert_alpha(),(300,360),350,self.screen)

        self.orologio = orologio

    def muovi_giocatore(self,direction):
        """
        Aggiorna la posizione del mondo rispetto al giocatore
        """
        if direction == "left":
            var = (self.orologio.get_time()/1000.)
            #self.player.muovi(-var)
            for x in self.terreno:
                x.muovi(var)
            for x in self.mont:
                x.muovi(var)
            for x in self.nuvole:
                x.muovi(var)
            self.sole.muovi(var)

        elif direction == "right":
            var = (self.orologio.get_time()/1000.)
            #self.player.muovi(var)
            for x in self.terreno:
                x.muovi(-var)
            for x in self.mont:
                x.muovi(-var)
            for x in self.nuvole:
                x.muovi(-var)
            self.sole.muovi(-var)

    def aggiorna(self):
        """
        Aggiorna il panorama quando si arriva al limite
        visualizzabile e si deve aggiungere un altro pezzo
        """
        if self.terreno[0].xy[0] > 0:
            self.terreno = [oggetto(self.terreno[0].imm,350,self.screen,(self.terreno[0].xy[0]-640,0)),
                            self.terreno[0],self.terreno[1]]
        elif self.terreno[-1].xy[0] < 0:
            self.terreno = [self.terreno[-2],self.terreno[-1],
                            oggetto(self.terreno[0].imm,350,self.screen,(self.terreno[-1].xy[0]+640,0))]
        if self.mont[0].xy[0] > 0:
            self.mont = [oggetto(self.mont[0].imm,150,self.screen,(self.mont[0].xy[0]-640,0)),
                            self.mont[0],self.mont[1]]
        elif self.mont[-1].xy[0] < 0:
            self.mont = [self.mont[-2],self.mont[-1],
                            oggetto(self.mont[0].imm,150,self.screen,(self.mont[-1].xy[0]+640,0))]
        if self.nuvole[0].xy[0] > 0:
            self.nuvole = [oggetto(self.nuvole[0].imm,25,self.screen,(self.nuvole[0].xy[0]-640,0)),
                            self.nuvole[0],self.nuvole[1]]
        elif self.nuvole[-1].xy[0] < 0:
            self.nuvole = [self.nuvole[-2],self.nuvole[-1],
                            oggetto(self.nuvole[0].imm,25,self.screen,(self.nuvole[-1].xy[0]+640,0))]

    def render(self):
        """
        Renderizza il panorama ed il giocatore
        """
        self.aggiorna()

        for x in self.sfondo:
            x.render()
        self.sole.render()
        for x in self.nuvole:
            x.render()
        for x in self.mont:
            x.render()
        for x in self.terreno:
            x.render()

        self.player.render()

Analisi

  • La classe oggetto rappresenta uno degli oggetti del panorama (o sfondi, visto che sono grandi quanto la finestra di gioco). Quindi comprenderà il terreno sotto il giocatore, le montagne, il sole, le nuvole e lo sfondo vero e proprio. Di questi oggetti ci basta memorizzare l’immagine e le coordinate. Implementiamo inoltre una funzione per lo spostamento dell’oggetto che varia in base alla propria velocità, che sarà scelta in base al livello in cui l’oggetto si trova (quindi quanto è distante dal giocatore).
  • Come potete vedere dal codice del parallax_engine, i vari livelli sono memorizzati come liste e contengono di base 3 copie dello stesso oggetto messe una in fila (visivamente parlando, infatti cambiano sono le coordinate delle ascisse), come nella precedente figura. La funzione muovi giocatore in realtà aggiornerà la posizione di questi oggetti dello sfondo rispetto al giocatore. Il rendering verrà sempre gestito in base al livello di profondità dell’immagine, semplicemente renderizzando prima gli oggetti che si trovano più lontani dal giocatore. La funzione chiave è aggiorna() : se teniamo presente la figura di prima, questa controlla se lo sfondo numero 1 od il 3 non sia completamente ripreso dalla telecamera (cioè la finestra principale di gioco). In questo caso sarà necessario aggiungere prima (o dopo) un nuovo oggetto dello stesso tipo per non terminare con un taglio lo sfondo e dare quindi un senso di continuità. Il tutto è gestito ricostruendo interamente la lista con gli elementi correnti e con il nuovo elemento, che dovrà avere le opportune coordinate per sembrare tutt’uno con il resto. L’unico elemento distaccato da questo meccanismo è il sole, così da poter capire meglio come ci stiamo muovendo.

Non riporto il main di gioco perché gestirà semplicemente i due input disponibili, cioè la freccia sinistra e quella destra e richiamerà la funzione render dell’engine per visualizzare tutto sullo schermo.

Conclusioni

Con questo semplice esempio abbiamo visto come creare un gioco con scorrimento di parallasse. Questa tecnica, se combinata al movimento della telecamera sul mondo di gioco, può dare maggiore profondità al vostro videogame, oltre che arricchirlo con un piacevole effetto ottico. Inoltre vi lascia completamente liberi di decidere su come implementare il mondo su cui il giocatore effettuerà le sue interazioni, visto che questo è “solo” un’effetto per il background.

Potete anche pensare ad uno scorrimento verticale, oppure sia verticale che orizzontale, ma il principio è sempre lo stesso, basta adattarlo al vostro videogame ed alle vostre esigenze.

Spero che vi torni utile e vi prego di non badare troppo al mio scarso talento nel disegnare.

Download: http://tinyurl.com/69xgfor

Press ESC to close