Sviluppare un gioco in Python: Panda3D – audio

Introduzione

Il nostro tour attraverso questo splendido game engine, sta per volgere al termine. Con questo articolo vedremo come inserire effetti audio all’interno della nostra scena in Panda3d, senza troppe pretese.

Aggiungeremo una musica di sottofondo che si può ascoltare premendo una pulsante (GUI) ed un effetto audio 3d. Panda3d offre molte funzionalità sotto questo punti di vista ed ha anche delle alternative per quel che riguarda le librerie che si vuole utilizzare per riprodurre gli effetti audio.

Audio

Panda3d permette di utilizzare una delle seguenti librerie audio:

  • OpenAL
  • FMOD
  • Miles

Per l’ultima libreria elencata, è l’unica che non è presente con i file binari di Panda3d, ma è possibile integrarla compilando i sorgenti ed usando il sistema ppremake (non la prenderemo in considerazione in questo articolo, la cito solo per completezza).

Le altre due librerie vi dovrebbero suonare più familiari. Entrambe sono integrate con panda e sono selezionabili andando a modificare i file di configurazione.

Dovete andare quindi alla ricerca del seguente file:

Config.prc

Che si trova nella cartella etc all’interno di panda (es: ../Panda3d-1.7.2/etc). Successivamente cercate la seguente riga:

audio-library-name xxxxxxxx

Al posto delle ‘x’, mettete la libreria che volete utilizzare:

  • p3fmod_audio – per FMOD
  • p3openal_audio – per OpenAL

Fatto questo siete pronti per utilizzare i vostri sound effect in Panda3d. Vi ricordo solo che, per chi avesse compilato da sorgente Panda3d, deve aver tenuto conto di possedere i sorgenti (o le librerie) delle suddette audio libraries.

Gli effetti audio in Panda3d sono divisi in due categorie:

  1. Sound Effects
  2. Music

Per caricare i nostri effetti audio, abbiamo bisogno della classe Loader o di un Manager che ne fa uso. Oltre alle normali opzioni di Stop, Play, panning, setVolume, setLoop, playRate e la riproduzione ad un certo tempo del file audio, ogni AudioSound ha tre costanti per definire il suo status, controllabili tramite il metodo status() :

  1. AudioSound.BAD : c’è qualche problema nella riproduzione del suono (in interi equivale a 0).
  2. AudioSound.READY : è pronto per essere eseguito ma non sta eseguendo (1).
  3. AudioSound.PLAYING : il suono è in esecuzione (2).

Infine dovete sapere che ci possono essere solo un numero predeterminato di sound effects in cache per ogni AudioManager (cioè il gestore di suoni), di base 16, ma potete cambiare questo numero cercando la linea audio-cache-limit nel file config.prc del quale abbiamo già parlato prima.

Ora siamo pronti per vedere il codice ed analizzare l’esempio.

Codice

1# -*- coding: utf-8 -*-
2 
3from direct.showbase.ShowBase import ShowBase
4from direct.showbase import Audio3DManager
5from direct.showbase.DirectObject import DirectObject
6from pandac.PandaModules import *
7from direct.gui.DirectGui import *
8from panda3d.core import Vec3
9from sys import exit
10 
11class MyApp(ShowBase,DirectObject):
12    def __init__(self):
13        ShowBase.__init__(self)
14 
15        self.velocita = 0
16        self.gas = 0
17        self.maxVel = 100
18        self.accel = 25
19        self.gestGira = 25
20 
21        self.keyMap = {"w" : False,
22                       "s" : False,
23                       "a" : False,
24                       "d" : False,
25                       "mouse1" : False,
26                       "mouse3" : False}
27 
28        self.accept("w", self.tastoPremuto,["w", True])
29        self.accept("a", self.tastoPremuto,["a", True])
30        self.accept("s", self.tastoPremuto,["s", True])
31        self.accept("d", self.tastoPremuto,["d", True])
32 
33        self.accept("w-up", self.tastoPremuto,["w", False])
34        self.accept("a-up", self.tastoPremuto,["a", False])
35        self.accept("s-up", self.tastoPremuto,["s", False])
36        self.accept("d-up", self.tastoPremuto,["d", False])
37 
38        self.accept("mouse1", self.tastoPremuto, ["mouse1", True])
39        self.accept("mouse3", self.tastoPremuto, ["mouse3", True])
40        self.accept("mouse1-up", self.tastoPremuto, ["mouse1", False])
41        self.accept("mouse3-up", self.tastoPremuto, ["mouse3", False])
42 
43        self.accept("escape", exit)
44 
45        self.oggetto0 = self.loader.loadModel("models/cubo_giallo.egg")
46        self.oggetto0.reparentTo(self.render)
47        self.oggetto0.setPos(-3,7,3)
48        self.oggetto0.setScale(0.15,0.15,0.15)
49 
50        self.oggetto0_0 = self.loader.loadModel("models/cubo_giallo.egg")
51        self.oggetto0_0.reparentTo(self.render)
52        self.oggetto0_0.setPos(self.oggetto0,-5,10,5)
53        self.oggetto0_0.setScale(0.15,0.15,0.15)
54 
55        self.oggetto0_00 = self.loader.loadModel("models/cubo_rosso.egg")
56        self.oggetto0_00.reparentTo(self.render)
57        self.oggetto0_00.setPos(-5,10,5)
58        self.oggetto0_00.setScale(0.15,0.15,0.15)
59 
60        self.oggetto1 = self.loader.loadModel("models/cubo_rosso.egg")
61        self.oggetto1.reparentTo(self.render)
62        self.oggetto1.setPos(3,7,3)
63        self.oggetto1.setScale(0.15,0.15,0.15)
64        self.oggetto1_1 = self.loader.loadModel("models/cubo_rosso.egg")
65        self.oggetto1_1.reparentTo(self.oggetto1)
66        self.oggetto1_1.setPos(-5,10,5)
67        self.oggetto1_1.setScale(1,1,1)
68 
69        self.oggetto2 = self.loader.loadModel("models/carousel_base.egg.pz")
70        self.oggetto2.reparentTo(self.render)
71        self.oggetto2.setPos(0,7,2)
72        tex = self.loader.loadTexture("models/test_texture.png")
73        self.oggetto2.setTexture(tex,1)
74 
75        self.audio3d = Audio3DManager.Audio3DManager(base.sfxManagerList[0], self.oggetto2)
76        mySound = self.audio3d.loadSfx('audio/29715__glaneur-de-sons__heart-beat.ogg')
77        self.audio3d.attachSoundToObject(mySound, self.oggetto0)
78        self.audio3d.setSoundVelocityAuto(mySound)
79        mySound.setLoop(True)
80        mySound.play()
81        mySound.setVolume(1)
82 
83        self.basePlane = self.loader.loadModel("models/base.egg")
84        self.basePlane.reparentTo(self.render)
85        self.basePlane.setPos(self.oggetto2,(0,0,0))
86        self.basePlane.setScale(5,5,5)
87 
88        self.setBackgroundColor(0,0,255)
89        self.disableMouse()
90        self.camera.reparentTo(self.oggetto2)
91        self.camera.setY(self.camera, -5)
92        self.camera.setZ(self.camera, 5)
93        self.camera.setP(self.camera,-5)
94 
95        self.setFrameRateMeter(True)
96        self.taskMgr.add(self.muoviCarosello,"Muovi Carosello")
97        self.taskMgr.add(self.debugTask, "Task Manager Print")
98 
99        self.musicMgr = self.musicManager
100        self.music = self.musicMgr.getSound("audio/554__bebeto__ambient-loop.mp3")
101        self.music.setLoop(True)
102        self.music.setPlayRate(1.5)
103        self.music.setVolume(1)
104        self.button = DirectButton(text = "Play", scale=0.1, command=self.setText, pos = Vec3(-1,0,-0.8),pad = (1.1,1.1))
105 
106    #Music
107    def setText(self):
108        if (self.music.status() == 2):
109            self.music.stop()
110            self.button['text'] = "Play"
111        else :
112            self.music.play()
113            self.button['text'] = "Stop"
114 
115    def tastoPremuto(self,key,value):
116        self.keyMap[key] = value
117 
118    def cameraZoom(self, dir, dt):
119        if(dir == "in"):
120            self.camera.setY(self.camera, 10 * dt)
121        else:
122            self.camera.setY(self.camera, -10 * dt)
123 
124    def muoviCarosello(self,task):
125        dt = globalClock.getDt()
126        if( dt > .20):
127            return task.cont
128        if (self.keyMap["w"] is True):
129            self.gestioneGas("up",dt)
130        elif (self.keyMap["s"] is True):
131            self.gestioneGas("down",dt)
132 
133        if (self.keyMap["a"] is True):
134            self.gira("l", dt)
135        elif (self.keyMap["d"] is True):
136            self.gira("r", dt)
137 
138        if(self.keyMap["mouse1"] == True):
139            self.cameraZoom("in", dt)
140        elif(self.keyMap["mouse3"] == True):
141            self.cameraZoom("out", dt)
142 
143        if(self.mouseWatcherNode.hasMouse() == True):
144            mpos = self.mouseWatcherNode.getMouse()
145            self.camera.setP(mpos.getY() * 30)
146            self.camera.setH(mpos.getX() * -30)
147 
148        self.controlloVelocita(dt)
149        self.move(dt)
150 
151        return task.cont
152 
153    def move(self, dt):
154        mps = self.velocita * 1000 / 3600
155        self.oggetto2.setY(self.oggetto2, mps * dt)
156 
157    def debugTask(self,task):
158        print(self.taskMgr)
159        return task.cont
160 
161    def gestioneGas(self, dir, dt):
162        if(dir == "up"):
163            self.gas += .25 * dt
164            if(self.gas > 1 ): self.gas = 1
165        else:
166            self.gas -= .25 * dt
167            if(self.gas < -1 ): self.gas = -1
168 
169    def controlloVelocita(self, dt):
170        tempVel = (self.maxVel * self.gas)
171        if(self.velocita < tempVel):
172            if((self.velocita + (self.accel * dt)) > tempVel):
173                self.velocita = tempVel
174            else:
175                self.velocita += (self.accel * dt)
176        elif(self.velocita > tempVel):
177            if((self.velocita - (self.accel * dt)) < tempVel):
178                self.velocita = tempVel
179            else:
180                self.velocita -= (self.accel * dt)
181 
182    def gira(self, dir, dt):
183        tempGiro = self.gestGira * (3 -(self.velocita / self.maxVel))
184        if(dir == "r"):
185            tempGiro = -tempGiro
186        self.oggetto2.setH(self.oggetto2, tempGiro * dt)
187 
188app = MyApp()
189app.run()

Analisi

L’esempio è quello precedente, quindi non dovreste trovare nessuna difficoltà nel leggere il codice ed individuare le parti aggiunte.

Innanzi tutto dovete porre la vostra attenzione sugli import alle righe 4,7 ed 8, che sono fondamentali per gli effetti che vogliamo inserire.

Di seguito, il primo ritocco viene fatto alla riga 75, dove creiamo un gestore per l’audio 3d. Successivamente carichiamo il nostro effetto audio e lo associamo a oggetto0 (cioè il primo cubo giallo inserito nella scena). Impostiamo il loop per la riproduzione e la avviamo con il volume al massimo. La velocità per l’effetto doppler è regolata in automatico per quanto riguarda l’emittente (mySound), come possiamo vedere alla riga 78. E’ comunque possibile impostarla manualmente, anche per il ricevitore (listener).

Fatto questo non ci resta che inserire una musica si sottofondo e lo facciamo alla riga 99, inizializzando un manager per la musica. Una volta caricata la canzone, impostiamo il tempo di esecuzione ad 1.5 ed il volume al massimo.

La funzione che permette di ascoltare la musica è setText, collagata la pulsante button (command = self.setText). Questa ci permette di iniziare la riproduzione musicale quando premiamo sul pulsante e di fermare la riproduzione ripremendo su di esso (se effettivamente la riproduzione è in corso senza errori : self.music.status() == 2). La funzione ha questo nome perché cambia il testo visualizzato dal pulsante dopo che è stato premuto.

Conclusioni

Ancora una volta con pochissime righe abbiamo aggiunto un tocco di “colore” alla nostra scena, dotandola di rumori e musica.

La possibilità di utilizzare diverse librerie senza preoccuparsi di implementare un audio manager, ci permette di concentrarci solo sulla resa ottimale dei nostri effetti sonori.

Il caricamento dei file è praticamente banale e questo facilita ancor più la portabilità dei nostri contenuti, non strettamente legati ad un particolare formato. In sostanza tutti i tipi di file audio più conosciuti sono supportati, quindi se di norma fate utilizzo di mp3, ogg o wav, non dovreste avere nessun tipo di problema.

Molti effetti sono già inclusi nel motore, anche se abbiamo visto solo l’audio “tridimensionale“. Nel manuale trovate qualche effetto in più se fate utilizzo della libreria FMOD, più dotata di OpenAL sotto questo punto di vista.

Nel prossimo articolo concluderemo il tour dando un primo stampo di come sono gestite le collisioni in Panda3d.

Di seguito rilascio i sorgenti della demo:

http://tinyurl.com/8x2llxc

Press ESC to close