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

# -*- coding: utf-8 -*-

from direct.showbase.ShowBase import ShowBase
from direct.showbase import Audio3DManager
from direct.showbase.DirectObject import DirectObject
from pandac.PandaModules import *
from direct.gui.DirectGui import *
from panda3d.core import Vec3
from sys import exit

class MyApp(ShowBase,DirectObject):
    def __init__(self):
        ShowBase.__init__(self)

        self.velocita = 0
        self.gas = 0
        self.maxVel = 100
        self.accel = 25
        self.gestGira = 25

        self.keyMap = {"w" : False,
                       "s" : False,
                       "a" : False,
                       "d" : False,
                       "mouse1" : False,
                       "mouse3" : False}

        self.accept("w", self.tastoPremuto,["w", True])
        self.accept("a", self.tastoPremuto,["a", True])
        self.accept("s", self.tastoPremuto,["s", True])
        self.accept("d", self.tastoPremuto,["d", True])

        self.accept("w-up", self.tastoPremuto,["w", False])
        self.accept("a-up", self.tastoPremuto,["a", False])
        self.accept("s-up", self.tastoPremuto,["s", False])
        self.accept("d-up", self.tastoPremuto,["d", False])

        self.accept("mouse1", self.tastoPremuto, ["mouse1", True])
        self.accept("mouse3", self.tastoPremuto, ["mouse3", True])
        self.accept("mouse1-up", self.tastoPremuto, ["mouse1", False])
        self.accept("mouse3-up", self.tastoPremuto, ["mouse3", False])

        self.accept("escape", exit)

        self.oggetto0 = self.loader.loadModel("models/cubo_giallo.egg")
        self.oggetto0.reparentTo(self.render)
        self.oggetto0.setPos(-3,7,3)
        self.oggetto0.setScale(0.15,0.15,0.15)

        self.oggetto0_0 = self.loader.loadModel("models/cubo_giallo.egg")
        self.oggetto0_0.reparentTo(self.render)
        self.oggetto0_0.setPos(self.oggetto0,-5,10,5)
        self.oggetto0_0.setScale(0.15,0.15,0.15)

        self.oggetto0_00 = self.loader.loadModel("models/cubo_rosso.egg")
        self.oggetto0_00.reparentTo(self.render)
        self.oggetto0_00.setPos(-5,10,5)
        self.oggetto0_00.setScale(0.15,0.15,0.15)

        self.oggetto1 = self.loader.loadModel("models/cubo_rosso.egg")
        self.oggetto1.reparentTo(self.render)
        self.oggetto1.setPos(3,7,3)
        self.oggetto1.setScale(0.15,0.15,0.15)
        self.oggetto1_1 = self.loader.loadModel("models/cubo_rosso.egg")
        self.oggetto1_1.reparentTo(self.oggetto1)
        self.oggetto1_1.setPos(-5,10,5)
        self.oggetto1_1.setScale(1,1,1)

        self.oggetto2 = self.loader.loadModel("models/carousel_base.egg.pz")
        self.oggetto2.reparentTo(self.render)
        self.oggetto2.setPos(0,7,2)
        tex = self.loader.loadTexture("models/test_texture.png")
        self.oggetto2.setTexture(tex,1)

        self.audio3d = Audio3DManager.Audio3DManager(base.sfxManagerList[0], self.oggetto2)
        mySound = self.audio3d.loadSfx('audio/29715__glaneur-de-sons__heart-beat.ogg')
        self.audio3d.attachSoundToObject(mySound, self.oggetto0)
        self.audio3d.setSoundVelocityAuto(mySound)
        mySound.setLoop(True)
        mySound.play()
        mySound.setVolume(1)

        self.basePlane = self.loader.loadModel("models/base.egg")
        self.basePlane.reparentTo(self.render)
        self.basePlane.setPos(self.oggetto2,(0,0,0))
        self.basePlane.setScale(5,5,5)

        self.setBackgroundColor(0,0,255)
        self.disableMouse()
        self.camera.reparentTo(self.oggetto2)
        self.camera.setY(self.camera, -5)
        self.camera.setZ(self.camera, 5)
        self.camera.setP(self.camera,-5)

        self.setFrameRateMeter(True)
        self.taskMgr.add(self.muoviCarosello,"Muovi Carosello")
        self.taskMgr.add(self.debugTask, "Task Manager Print")

        self.musicMgr = self.musicManager
        self.music = self.musicMgr.getSound("audio/554__bebeto__ambient-loop.mp3")
        self.music.setLoop(True)
        self.music.setPlayRate(1.5)
        self.music.setVolume(1)
        self.button = DirectButton(text = "Play", scale=0.1, command=self.setText, pos = Vec3(-1,0,-0.8),pad = (1.1,1.1))

    #Music
    def setText(self):
        if (self.music.status() == 2):
            self.music.stop()
            self.button['text'] = "Play"
        else :
            self.music.play()
            self.button['text'] = "Stop"

    def tastoPremuto(self,key,value):
        self.keyMap[key] = value

    def cameraZoom(self, dir, dt):
        if(dir == "in"):
            self.camera.setY(self.camera, 10 * dt)
        else:
            self.camera.setY(self.camera, -10 * dt)

    def muoviCarosello(self,task):
        dt = globalClock.getDt()
        if( dt > .20):
            return task.cont
        if (self.keyMap["w"] is True):
            self.gestioneGas("up",dt)
        elif (self.keyMap["s"] is True):
            self.gestioneGas("down",dt)

        if (self.keyMap["a"] is True):
            self.gira("l", dt)
        elif (self.keyMap["d"] is True):
            self.gira("r", dt)

        if(self.keyMap["mouse1"] == True):
            self.cameraZoom("in", dt)
        elif(self.keyMap["mouse3"] == True):
            self.cameraZoom("out", dt)

        if(self.mouseWatcherNode.hasMouse() == True):
            mpos = self.mouseWatcherNode.getMouse()
            self.camera.setP(mpos.getY() * 30)
            self.camera.setH(mpos.getX() * -30)

        self.controlloVelocita(dt)
        self.move(dt)

        return task.cont

    def move(self, dt):
        mps = self.velocita * 1000 / 3600
        self.oggetto2.setY(self.oggetto2, mps * dt)

    def debugTask(self,task):
        print(self.taskMgr)
        return task.cont

    def gestioneGas(self, dir, dt):
        if(dir == "up"):
            self.gas += .25 * dt
            if(self.gas > 1 ): self.gas = 1
        else:
            self.gas -= .25 * dt
            if(self.gas < -1 ): self.gas = -1

    def controlloVelocita(self, dt):
        tempVel = (self.maxVel * self.gas)
        if(self.velocita < tempVel):
            if((self.velocita + (self.accel * dt)) > tempVel):
                self.velocita = tempVel
            else:
                self.velocita += (self.accel * dt)
        elif(self.velocita > tempVel):
            if((self.velocita - (self.accel * dt)) < tempVel):
                self.velocita = tempVel
            else:
                self.velocita -= (self.accel * dt)

    def gira(self, dir, dt):
        tempGiro = self.gestGira * (3 -(self.velocita / self.maxVel))
        if(dir == "r"):
            tempGiro = -tempGiro
        self.oggetto2.setH(self.oggetto2, tempGiro * dt)

app = MyApp()
app.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