Gra 2D, część 5: Odtwarzamy dźwięk

03.02.2010 - Łukasz Milewski
Trudność
Możemy łatwo zwiększyć atrakcyjność naszej gry. Wystarczy wpłynąć na wyobraźnię gracza, tak aby sam zaczął widzieć to, czego oczekuje. Jednym z najprostszych sposobów oddziaływania na wyobraźnię jest dźwięk i muzyka. Stosując odpowiedni podkład muzyczny łatwo jest wpływać na nastrój gracza - sprawiać aby był szczęśliwy i odprężony, albo żeby się wystraszył (jeżeli gra w horror). Oprawa dźwiękowa jest jednym z najważniejszych elementów gier. Dlatego w tym artykule zobaczymy jak można dodać ją do naszej produkcji.

Poprzedni artykuł - Hall of fame Następny artykuł - Wykrywanie kolizji i obsługa jednostek

Plan działania

Zaczniemy od przywrócenia gry do stanu, w którym możemy biegać graczem po mapie. Następnie zaprogramujemy klasę Sound, która będzie odpowiedzialna za odtwarzanie muzyki i efektów dźwiękowych. Na koniec dodamy muzykę do rozgrywki oraz dźwięk w momencie, gdy gracz będzie skakał.

Skopiuj pliki 05_game.mp3 oraz jump.wav do katalogu data. Skopuj tam również również plik sounds.txt.

kod początkowy

Linkowanie

Aby uruchomić kod z tego artykułu, potrzebujemy biblioteki SDL_mixer. Musimy zainstalować tę bibliotekę oraz lekko zmodyfikować plik SConstruct, aby uwzględniał ją przy linkowaniu. Linijkę:

1
env.MergeFlags("-lSDL -lGL -lGLU");

zamieniamy na:

1
env.MergeFlags("-lSDL -lGL -lGLU -lSDL_mixer");

Interfejs klasy Sound

Naszym celem jest zaprogramowanie klasy do dźwięku i muzyki. Ostatecznie chcemy mieć możliwość odtworzenia efektu dźwiękowego znając tylko jego nazwę.

Potrzebujemy nowy nagłówek "SDL/SDL_mixer.h". Chcemy móc załadować dźwięki i ich konfigurację z dysku oraz mieć możliwość odtworzenia muzyki czy odgłosu. Te funkcje spełniają metody, odpowiednio: LoadSounds, PlayMusic, PlaySfx, LoadMusic i LoadSfx posłużą do wczytania obiektu danych muzyki i danych odgłosu (tak! to będą różne typy danych). W mapach m_sfx i m_music zapamiętamy odwzorowanie nazwy w obiekt dźwięku.

Pokaż/ukryj kod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#ifndef __SOUND_H__
#define __SOUND_H__
 
#include <string>
#include <map>
 
#include <boost/shared_ptr.hpp>
 
#include <SDL/SDL_mixer.h>
 
class Sound {
public:
    explicit Sound();
    void LoadSounds();
    
    void PlayMusic(const std::string& name);
    void PlaySfx(const std::string& name);
 
private:
    void LoadMusic(const std::string& name, const std::string& filename);
    void LoadSfx(const std::string& name, const std::string& filename);
 
private:
    std::map<std::string, Mix_Chunk*> m_sfx;
    std::map<std::string, Mix_Music*> m_music;
};
 
typedef boost::shared_ptr<Sound> SoundPtr;
 
#endif /* __SOUND_H__ */
  

Start!

Inicjalizacja

Na początek potrzebna jest nam lekka modyfikacja pliku App.cpp. Linijkę:

1
SDL_Init(SDL_INIT_VIDEO);

zamieniamy na:

1
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);

W ten sposób inicjalizujemy dźwięk w bibliotece SDL. Musimy jeszcze zaimplementować inicjalizację w konstruktorze Sound::Sound (plik Sound.cpp)

1
2
3
4
5
6
7
8
9
10
11
12
13
Sound::Sound() {
    int audio_rate = 44100;
    Uint16 audio_format = AUDIO_S16SYS;
    int audio_channels = 2;
    int audio_buffers = 4096;
 
    if(Mix_OpenAudio(audio_rate, audio_format, 
                    audio_channels, audio_buffers) != 0) {
        std::cout << "Unable to initialize audio: " 
                  << Mix_GetError() << "\n";
        exit(1);
    }
}

Przekazujemy parametry do funkcji Mix_OpenAudio. Ważny jest pierwszy (audio_rate). Określa on częstotliwość próbkowania. Im większy, tym lepsza jakość i większe zużycie CPU (tak - SDL_mixer to dźwięk programowy, a nie sprzętowy). Jeżeli zamiast 44100 wpiszemy 22050 to prawdopodobnie będzie słychać szum.

Wczytanie danych z dysku

Zajmijmy się wczytaniem opisu dźwięków z pliku. Nasz plik (data/sound.txt) wygląda tak:

1
2
music game data/game.mp3
sfx jump data/jump.wav

Pierwsza kolumna to typ dźwięku (muzyka lub sfx.). Druga to nazwa dźwięku (po tej nazwie będziemy odwoływali się do dźwięku w programie). Ostatnia kolumna to ścieżka do pliku z dźwiękiem, jaki należy załadować.

Kod jest bardzo prosty. Wczytujemy kolejne linijki. W każdej z nich wczytujemy najpierw typ i nazwę, a następnie nazwę pliku. Z nazwy pliku usuwamy spacje. Ostatnim krokiem jest sprawdzenie, jakiego typu jest nasz dźwięk i wczytanie go przy pomocy metody LoadMusic lub LoadSfx.

Pokaż/ukryj kod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void Sound::LoadSounds() {
    std::ifstream settings("data/sounds.txt");
    
    std::string type;
    std::string name;
    std::string filename;
    while (settings) {
        settings >> type >> name;
        std::getline(settings, filename);
        filename.erase(std::remove(filename.begin(), filename.end(), ' '), filename.end());
        if (type == "music") {
            LoadMusic(name, filename);
        }
        else if (type == "sfx") {
            LoadSfx(name, filename);
        }
        else {
            std::cout << "Unknown sound type: '" << type << "'\n";
        }
    }
}
  

Metody LoadMusic i LoadSfx są bardzo podobne. Aby załadować plik z muzyką, wywołujemy metodę Mix_LoadMUS. Aby załadować plik z SFXem, wykorzystujemy Mix_LoadWAV. Te metody zwracają obiekty odpowiednich dźwięków, które następnie kojarzymy ze sobą w mapach m_sfx i m_music.

Pokaż/ukryj kod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void Sound::LoadMusic(const std::string& name, const std::string& filename) {
    Mix_Music* m = Mix_LoadMUS(filename.c_str());
    if (m) {
        m_music.insert(std::make_pair(name, m));
    }
    else {
        std::cout << "Can't load music file " << filename << "\n";
    }
}
 
void Sound::LoadSfx(const std::string& name, const std::string& filename) {
    Mix_Chunk* c = Mix_LoadWAV(filename.c_str());
    if (c) {
        m_sfx.insert(std::make_pair(name, c));
    }
    else {
        std::cout << "Can't load sfx file " << filename << "\n";
    }
}
  

Odtworzenie wczytanych dźwięków

Pozostaje tylko odtwarzanie dźwięku. Tutaj ponownie metody dla odgłosów i muzyki się nie różnią. Sprawdzamy, czy dźwięk o podanej nazwie był wczytany. Jeżeli nie, wypisujemy odpowiedni komunikat. Jeżeli tak, odtwarzamy go, wykorzystując odpowiednią funkcję SDL_mixera (Mix_PlayMusic lub Mix_PlayChannel).

Pokaż/ukryj kod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void Sound::PlayMusic(const std::string& name) {
    if (m_music.find(name) == m_music.end()) {
        std::cout << "Unknown music '" << name << "'\n";
    }
    else {
        Mix_PlayMusic(m_music[name], -1); // -1 oznacza zapętlenie
    }
}
 
void Sound::PlaySfx(const std::string& name) {
    if (m_sfx.find(name) == m_sfx.end()) {
        std::cout << "Unknown sfx '" << name << "'\n";
    }
    else {
        if (Mix_PlayChannel(-1, m_sfx[name], 0) == -1) { 
            std::cout <<"Unable to play sfx: '" << name << "'\n";
        }
    }    
}
  

Dodajemy dźwięki do gry

OK. Mamy już wszystko, co potrzebne. Teraz zmieniamy klasę App.cpp (plik App.cpp). Na początek przywróćmy chodzącego po mapie gracza (podczas poprzedniej lekcji usunęliśmy go). W tym celu zmieniamy metodę Run. Linijki:

Pokaż/ukryj kod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    // pętla główna
    ScoreSubmit ss(222);
    size_t last_ticks = SDL_GetTicks();
    while (!ss.IsDone()) {
        ss.ProcessEvents();
 
        // time update
        size_t ticks = SDL_GetTicks();
        double delta_time = (ticks - last_ticks) / 1000.0;
        last_ticks = ticks;
 
        // update & render
        if (delta_time > 0) {
            ss.Update(delta_time);
        }
        ss.Draw();
    }
  

zamieniamy na:

Pokaż/ukryj kod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    // pętla główna
    size_t last_ticks = SDL_GetTicks();
    while (!is_done) {
        ProcessEvents();
 
        // time update
        size_t ticks = SDL_GetTicks();
        double delta_time = (ticks - last_ticks) / 1000.0;
        last_ticks = ticks;
 
        // update & render
        if (delta_time > 0) {
            Update(delta_time);
        }
        Draw();
    }
  

Teraz zmieniamy klasę Engine (plik Engine.h).

Pokaż/ukryj kod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#ifndef ENGINE_H_
#define ENGINE_H_
 
#include <string>
#include <GL/gl.h>
 
#include "SpriteConfig.h"
#include "Renderer.h"
 
class Engine {
public:
    static Engine& Get() {
        static Engine engine;
        return engine;
    }
 
    void Load() {
        m_sprite_config.reset(new SpriteConfig::SpriteConfig());
        m_renderer.reset(new Renderer::Renderer());
    }
 
    SpriteConfigPtr SpriteConfig() {
        return m_sprite_config;
    }
 
    RendererPtr Renderer() {
        return m_renderer;
    }
 
private:
    SpriteConfigPtr m_sprite_config;
    RendererPtr m_renderer;
 
};
 
#endif /* ENGINE_H_ */
  

Dodajemy wskaźnik na klasę dźwięku tak samo, jak np. na klasę Renderer. Odpowiedni kod po zmianach wygląda następująco:

Pokaż/ukryj kod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#ifndef ENGINE_H_
#define ENGINE_H_
 
#include <string>
#include <GL/gl.h>
 
#include "SpriteConfig.h"
#include "Renderer.h"
#include "Sound.h"
 
class Engine {
public:
    static Engine& Get() {
        static Engine engine;
        return engine;
    }
 
    void Load() {
        m_sprite_config.reset(new SpriteConfig::SpriteConfig());
        m_renderer.reset(new Renderer::Renderer());
        m_sound.reset(new Sound::Sound());
    }
 
    SpriteConfigPtr SpriteConfig() {
        return m_sprite_config;
    }
 
    RendererPtr Renderer() {
        return m_renderer;
    }
 
    SoundPtr Sound() {
        return m_sound;
    }
 
private:
    SpriteConfigPtr m_sprite_config;
    RendererPtr m_renderer;
    SoundPtr m_sound;
 
};
 
#endif /* ENGINE_H_ */
  

Wracamy do App.cpp i przed komentarzem "pętla główna" dodajemy:

1
2
    Engine::Get().Sound()->LoadSounds();
    Engine::Get().Sound()->PlayMusic("game");

Zmieńmy jeszcze metodę skoku gracza (Player::Jump w pliku Player.cpp) z:

1
2
3
4
5
6
7
8
9
void Player::Jump(double y_velocity) {
    // wykonaj skok o ile jest taka możliwość
    if (m_jump_allowed) {
        m_jump_allowed = false;
        m_is_on_ground = false;
        m_vy = y_velocity;            // początkowa prędkość
        m_ay = DefaultYAcceleration;  // przyspieszenie
    }
}

na:

1
2
3
4
5
6
7
8
9
10
void Player::Jump(double y_velocity) {
    // wykonaj skok o ile jest taka możliwość
    if (m_jump_allowed) {
        m_jump_allowed = false;
        m_is_on_ground = false;
        m_vy = y_velocity;            // początkowa prędkość
        m_ay = DefaultYAcceleration;  // przyspieszenie
        Engine::Get().Sound()->PlaySfx("jump");
    }
}

Jak widać, odgrywanie dźwięków jest bardzo proste! Nauczyliśmy się wczytywać własne pliki z danymi oraz odtwarzać dźwięk i muzykę. W wyniku naszej pracy powstał nowy kod źródłowy, który jest dostępny tutaj.

Masz pytanie, uwagę? Zauważyłeś błąd? Powiedz o tym na forum.

Zadania

  • Przywróć widok klawiatury ekranowej z poprzedniego artykułu (ScoreSubmit). Dodaj dźwięk klawiszy naciskanych wtedy, gdy są wpisywane litery.
  • Spróbujmy zrobić odgłos kroków. W tym celu potrzebujemy odtworzyć zapętlony dźwięk, gdy gracz zaczyna się poruszać i wyciszyć go, gdy się zatrzymuje. Za zapętlenie odpowiada ostatni argument funkcji Mix_PlayChannel. Znajdź w internecie odpowiedni dźwięk i napisz metodę Sound::PlaySfxLooped.
  • Zmień metody PlaySfx, aby zwracały id kanału (to, co zwraca funkcja Mix_PlayChannel). Gdy gracz zaczyna iść, wywołaj metodę Sound::PlaySfxLooped("move"). Zapamiętaj zwrócone id. Gdy gracz się zatrzyma, wywołaj metodę Sound::StopSfx(zapamiętane_id); metodę Sound::StopSfx zaimplementuj, wykorzystując Mix_HaltChannel i podając jako argument id kanału.
5
Twoja ocena: Brak Ocena: 5 (1 ocena)

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com