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.