Skip to content

Latest commit

 

History

History
698 lines (577 loc) · 29.6 KB

File metadata and controls

698 lines (577 loc) · 29.6 KB

十二、声音 FX

目前网络上的声音状态有点混乱,已经有一段时间了。很长一段时间以来,根据您使用的浏览器,加载 MP3 和 OGG 文件都存在问题。最近,浏览器在阻止自动播放的声音以防止烦人的音频垃圾方面出现了问题。Chrome 中的这项功能在我们的游戏中播放音频时,有时似乎会产生问题。我注意到,如果 Chrome 最初不播放音频,那么如果你重新加载页面,它通常会播放。我在火狐上没有这个问题。

You will need to include several images and audio files in your build to make this project work. Make sure that you include the /Chapter12/sprites/ folder as well as the /Chapter12/audio/ folder from the project's GitHub. If you haven't yet downloaded the GitHub project, you can get it online at https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly.

Emscripten 对音频播放的支持没有我希望的那么好。在留言板上,Emscripten 的支持者很快指责网络上的音频状态,而不是 Emscripten 本身,这种评价是有一定道理的。Emscripten 的 FAQ 声称 Emscripten 支持使用 SDL1 音频、SDL2 音频和 OpenAL,但是,根据我的经验,我发现使用非常有限的一组 SDL2 音频可以提供最好的结果。我将尽量少用 SDL2 音频,使用音频队列,而不是混合音效。您可能希望扩展或修改我在这里所做的工作。从理论上讲,OpenAL 应该与 Emscripten 合作,尽管我在这方面运气不太好。另外,您可能希望查看SDL_MixAudio()和SDL_AudioStream(【https://wiki.libsdl.org/Tutorials/AudioStream】)来改进游戏中的音频系统,但请注意,网络上流式和混合音频的性能和支持可能还没有为黄金时段做好准备。

我们将在本章中讨论以下主题:

  • 哪里可以获得音效
  • 带有 Emscripten 的简单音频
  • 给我们的游戏增加声音
  • 编译和运行

哪里可以获得音效

有很多很棒的地方可以在线获得音乐和音效。我用 SFXR(http://www.drpetter.se/project_sfxr.html)生成了我们在本章中使用的音效,这是一个用来生成老式 8 位音效的工具,听起来像你在 NES 游戏中听到的东西。这种音效可能不合你的口味。OpenGameArt.org 还有大量的音效(https://opengameart.org/art-search-advanced?keys=&field _ art _ type _ tid % 5B % 5D = 13&sort _ by = count&sort _ order = desc)和音乐(https://opengameart.org/art-search-advanced?keys=&field _ art _ type _ tid % 5B % 5D = 12&sort _ by = count&sort _ order = DESC)以及各种开放的许可证,因此请确保您在该网站上查看任何声音或艺术的许可证

带有 Emscripten 的简单音频

在我们给我们的主游戏添加音效之前,我将向您展示如何在audio.c文件中制作音频播放器,以演示如何使用 SDL 音频在 WebAssembly 应用中播放音效。这个应用将采用五种音效,我们将在我们的游戏中使用,并允许用户按数字键 1 到 5 来播放所有选择的音效。我将首先向您展示分成两个部分的代码,然后我将向您介绍每件事的作用。以下是audio.c中除main功能外的所有代码:

#include <SDL2/SDL.h>
#include <emscripten.h>
#include <stdio.h>
#include <stdbool.h>

#define ENEMY_LASER "/audio/enemy-laser.wav"
#define PLAYER_LASER "/audio/player-laser.wav"
#define LARGE_EXPLOSION "/audio/large-explosion.wav"
#define SMALL_EXPLOSION "/audio/small-explosion.wav"
#define HIT "/audio/hit.wav"

SDL_AudioDeviceID device_id;
SDL_Window *window;
SDL_Renderer *renderer;
SDL_Event event;

struct audio_clip {
    char file_name[100];
    SDL_AudioSpec spec;
    Uint32 len;
    Uint8 *buf;
} enemy_laser_snd, player_laser_snd, small_explosion_snd, large_explosion_snd, hit_snd;

void play_audio( struct audio_clip* clip ) {
    int success = SDL_QueueAudio(device_id, clip->buf, clip->len);
    if( success < 0 ) {
        printf("SDL_QueueAudio %s failed: %s\n", clip->file_name, 
        SDL_GetError());
    }
}

void init_audio( char* file_name, struct audio_clip* clip ) {
    strcpy( clip->file_name, file_name );

    if( SDL_LoadWAV(file_name, &(clip->spec), &(clip->buf), &(clip->len)) 
    == NULL ) {
        printf("Failed to load wave file: %s\n", SDL_GetError());
    }
}

void input_loop() {
    if( SDL_PollEvent( &event ) ){
        if( event.type == SDL_KEYUP ) {
            switch( event.key.keysym.sym ){
                case SDLK_1:
                    printf("one key release\n");
                    play_audio(&enemy_laser_snd);
                    break;
                case SDLK_2:
                    printf("two key release\n");
                    play_audio(&player_laser_snd);
                    break;
                case SDLK_3:
                    printf("three key release\n");
                    play_audio(&small_explosion_snd);
                    break;
                case SDLK_4:
                    printf("four key release\n");
                    play_audio(&large_explosion_snd);
                    break;
                case SDLK_5:
                    printf("five key release\n");
                    play_audio(&hit_snd);
                    break;
                default:
                    printf("unknown key release\n");
                    break;
            }
        }
    }
}

audio.c文件的末尾,我们有我们的main功能:

int main() {
    if((SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO)==-1)) {
        printf("Could not initialize SDL: %s.\n", SDL_GetError());
        return 0;
    }

    SDL_CreateWindowAndRenderer( 320, 200, 0, &window, &renderer );

    init_audio( ENEMY_LASER, &enemy_laser_snd );
    init_audio( PLAYER_LASER, &player_laser_snd );
    init_audio( SMALL_EXPLOSION, &small_explosion_snd );
    init_audio( LARGE_EXPLOSION, &large_explosion_snd );
    init_audio( HIT, &hit_snd );

    device_id = SDL_OpenAudioDevice(NULL, 0, &(enemy_laser_snd.spec), 
                                    NULL, 0);

    if (device_id == 0) {
        printf("Failed to open audio: %s\n", SDL_GetError());
    }

    SDL_PauseAudioDevice(device_id, 0);

    emscripten_set_main_loop(input_loop, 0, 0);

    return 1;
}

现在您已经看到了整个 audio.c 文件,让我们看一下它的所有部分。在这个文件的顶部,我们有我们的#include#define宏:

#include <SDL2/SDL.h>
#include <emscripten.h>
#include <stdio.h>
#include <stdbool.h>

#define ENEMY_LASER "/audio/enemy-laser.wav"
#define PLAYER_LASER "/audio/player-laser.wav"
#define LARGE_EXPLOSION "/audio/large-explosion.wav"
#define SMALL_EXPLOSION "/audio/small-explosion.wav"
#define HIT "/audio/hit.wav"

之后,我们就有了 SDL 特有的全局变量。我们的音频输出需要一个SDL_AudioDeviceIDSDL_WindowSDL_RendererSDL_Event在前面的大部分章节中已经使用过,现在应该很熟悉了:

SDL_AudioDeviceID device_id;
SDL_Window *window;
SDL_Renderer *renderer;
SDL_Event event;

我们正在开发一个 C 程序,而不是 C++ 程序,所以我们将使用一个结构来保存我们的音频数据,而不是一个类。我们将创建一个名为audio_clip的 C 结构,它将保存我们将在应用中播放的音频的所有信息。这些信息包括一个保存文件名的字符串。它包含一个保存音频规范的SDL_AudioSpec对象。它还包含音频片段的长度和指向 8 位数据缓冲区的指针,该缓冲区保存音频片段的波形数据。在audio_clip结构被定义之后,该结构的五个实例被创建,我们稍后将能够使用它们来播放这些声音:

struct audio_clip {
    char file_name[100];
    SDL_AudioSpec spec;
    Uint32 len;
    Uint8 *buf;
} enemy_laser_snd, player_laser_snd, small_explosion_snd, large_explosion_snd, hit_snd;

在我们定义audio_clip结构之后,我们需要创建一个函数来播放该结构中的音频。这个函数调用SDL_QueueAudio传入全局device_id,一个指向波形缓冲区的指针,以及片段的长度。device_id是音频设备(声卡)的参考。clip->buf变量是一个指向缓冲区的指针,该缓冲区包含我们将要加载的.wav文件的波形数据。clip->len变量包含片段播放的时间长度:

void play_audio( struct audio_clip* clip ) {
    int success = SDL_QueueAudio(device_id, clip->buf, clip->len);
    if( success < 0 ) {
        printf("SDL_QueueAudio %s failed: %s\n", clip->file_name, 
        SDL_GetError());
    }
}

我们需要的下一个函数是初始化我们的audio_clip的函数,这样我们就可以把它传递给play_audio函数。该功能设置我们的audio_clip的文件名,并加载一个波形文件设置我们的audio_clip中的specbuflen值。如果对SDL_LoadWAV的调用失败,我们会打印出一条错误消息:

void init_audio( char* file_name, struct audio_clip* clip ) {
    strcpy( clip->file_name, file_name );

    if( SDL_LoadWAV(file_name, &(clip->spec), &(clip->buf), &(clip-
        >len)) 
    == NULL ) {
        printf("Failed to load wave file: %s\n", SDL_GetError());
    }
}

现在input_loop应该很熟悉了。该函数调用SDL_PollEvent并使用它返回的事件来检查键盘键的释放。它检查哪个键被释放。如果该键是从 1 到 5 的数字键之一,则使用 switch 语句调用play_audio功能,传递特定的audio_clip。我们使用键释放而不是键按压的原因是为了防止用户按住键时重复按键。我们可以很容易地防止这种情况发生,但是我正在努力使这个应用的代码尽可能短。这里是input_loop代码:

void input_loop() {
    if( SDL_PollEvent( &event ) ){
        if( event.type == SDL_KEYUP ) {
            switch( event.key.keysym.sym ){
                case SDLK_1:
                    printf("one key release\n");
                    play_audio(&enemy_laser_snd);
                    break;
                case SDLK_2:
                    printf("two key release\n");
                    play_audio(&player_laser_snd);
                    break;
                case SDLK_3:
                    printf("three key release\n");
                    play_audio(&small_explosion_snd);
                    break;
                case SDLK_4:
                    printf("four key release\n");
                    play_audio(&large_explosion_snd);
                    break;
                case SDLK_5:
                    printf("five key release\n");
                    play_audio(&hit_snd);
                    break;
                default:
                    printf("unknown key release\n");
                    break;
            }
        }
    }
}

像往常一样,main函数为我们的应用完成所有初始化。除了我们在以前的应用中执行的初始化之外,我们还需要一个新的音频初始化。这就是新版本的main功能的样子:

int main() {
    if((SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO)==-1)) {
        printf("Could not initialize SDL: %s.\n", SDL_GetError());
        return 0;
    }
    SDL_CreateWindowAndRenderer( 320, 200, 0, &window, &renderer );
    init_audio( ENEMY_LASER, &enemy_laser_snd );
    init_audio( PLAYER_LASER, &player_laser_snd );
    init_audio( SMALL_EXPLOSION, &small_explosion_snd );
    init_audio( LARGE_EXPLOSION, &large_explosion_snd );
    init_audio( HIT, &hit_snd );

    device_id = SDL_OpenAudioDevice(NULL, 0, &(enemy_laser_snd.spec), NULL, 
    0);

    if (device_id == 0) {
        printf("Failed to open audio: %s\n", SDL_GetError());
    }
    SDL_PauseAudioDevice(device_id, 0);
    emscripten_set_main_loop(input_loop, 0, 0);
    return 1;
}

我们改变的第一件事是我们对SDL_Init的呼唤。我们需要添加一个标志,告诉 SDL 初始化音频子系统。我们通过将|SLD_INIT_AUDIO添加到我们传入的参数中来实现这一点,该参数对带有SDL_INIT_AUDIO标志的参数执行按位运算。在SDL_Init的新版本之后,我们将创建窗口和渲染器,在这一点上我们已经做了很多次了。

init_audio调用都是新的,并初始化我们的audio_clip结构:

init_audio( ENEMY_LASER, &enemy_laser_snd );
init_audio( PLAYER_LASER, &player_laser_snd );
init_audio( SMALL_EXPLOSION, &small_explosion_snd );
init_audio( LARGE_EXPLOSION, &large_explosion_snd );
init_audio( HIT, &hit_snd );

接下来,我们需要调用SDL_OpenAudioDevice并检索一个设备 ID。打开音频设备需要一个默认规范,它会通知音频设备您想要播放的声音剪辑的质量。确保你选择的声音文件的质量水平是你想在游戏中玩什么的好例子。在我们的代码中,我们选择了enemy_laser_snd。我们还需要称呼SDL_PauseAudioDevice。无论何时创建新的音频设备,默认情况下都会暂停。调用SDL_PauseAudioDevice并传入0作为第二个参数会打开我们刚刚创建的音频设备。一开始我觉得这有点混乱,但请记住,下面对SDL_PauseAudioDevice的调用实际上是对音频剪辑的解包:

device_id = SDL_OpenAudioDevice(NULL, 0, &(enemy_laser_snd.spec), NULL, 0);

if (device_id == 0) {
    printf("Failed to open audio: %s\n", SDL_GetError());
}

SDL_PauseAudioDevice(device_id, 0);

在返回之前,我们要做的最后一件事是将我们的循环设置为我们之前创建的input_loop函数:

emscripten_set_main_loop(input_loop, 0, 0);

既然我们已经有了代码,我们应该编译并测试我们的audio.c文件:

emcc audio.c --preload-file audio -s USE_SDL=2 -o audio.html

我们需要预加载音频文件夹,以便能够访问虚拟文件系统中的.wav文件。然后,将audio.html加载到网络浏览器中,用 emrun 或其他网络服务器提供文件。在 Chrome 中加载应用时,可能会遇到一些小困难。新版本的 Chrome 增加了防止未请求音频播放的检查,以防止一些恼人的垃圾邮件。有时候,这个检查有点太敏感了,这可以阻止我们游戏中的音频运行。如果发生这种情况,请尝试在 Chrome 浏览器中重新加载页面。有时,这可以解决问题。防止这种情况发生的另一种方法是切换到火狐。

给我们的游戏增加声音

现在我们已经了解了如何让 SDL 音频在网络上工作,我们可以开始为我们的游戏添加音效了。我们不会在游戏中使用混音器,所以一次只会播放一种音效。正因为如此,我们需要将一些声音归类为优先音效。如果触发了优先音效,声音队列将被清除,该音效将运行。我们还想防止我们的声音队列变得太长,所以如果声音队列中有两个以上的项目,我们将清除它。不要害怕!当我们到达代码的那个部分时,我将重复所有这些。

更新 game.hpp

我们首先需要改变的是我们的game.hpp文件。我们需要添加一个新的Audio类,以及其他新的代码来支持我们游戏中的音频。在game.hpp文件的顶部附近,我们将添加一系列#define宏来定义我们的音效.wav文件的位置:

#define ENEMY_LASER (char*)"/audio/enemy-laser.wav"
#define PLAYER_LASER (char*)"/audio/player-laser.wav"
#define LARGE_EXPLOSION (char*)"/audio/large-explosion.wav"
#define SMALL_EXPLOSION (char*)"/audio/small-explosion.wav"
#define HIT (char*)"/audio/hit.wav"

在类声明列表的顶部,我们应该添加一个名为Audio的类的新声明:

class Audio;
class Ship;
class Particle;
class Emitter;
class Collider;
class Asteroid;
class Star;
class PlayerShip;
class EnemyShip;
class Projectile;
class ProjectilePool;
class FiniteStateMachine;
class Camera;
class RenderManager;
class Locator;

然后我们将定义新的Audio类,它将非常类似于我们在audio.c文件中使用的audio_clip结构。这个类将有一个文件名、一个规范、一个长度(在运行时)和一个缓冲区。它还将有一个优先级标志,当设置时,它将优先于当前音频队列中的所有其他内容。最后,我们将在这个类中有两个函数;一个初始化声音的构造函数,一个实际播放声音的Play函数。这就是类定义的样子:

class Audio {
    public:
        char FileName[100];
        SDL_AudioSpec spec;
        Uint32 len;
        Uint8 *buf;
        bool priority = false;

        Audio( char* file_name, bool priority_value );
        void Play();
};

最后,我们需要定义一些与全局变量相关的外部音频。这些全局变量将是出现在我们的main.cpp文件中的变量的引用。这些大部分是Audio类的实例,将在我们的游戏中用来播放音频文件。最后一个变量是对我们音频设备的引用:

extern Audio* enemy_laser_snd;
extern Audio* player_laser_snd;
extern Audio* small_explosion_snd;
extern Audio* large_explosion_snd;
extern Audio* hit_snd;
extern SDL_AudioDeviceID device_id;

正在更新 main.cpp

我们在main.cpp文件中需要做的第一件事是定义音频相关的全局变量,我们在game.hpp文件的末尾将其定义为外部变量:

SDL_AudioDeviceID device_id;

Audio* enemy_laser_snd;
Audio* player_laser_snd;
Audio* small_explosion_snd;
Audio* large_explosion_snd;
Audio* hit_snd;

这些音效大多与我们游戏中发生碰撞时发生的爆炸有关。正因为如此,我们将在整个collisions功能中增加调用来播放这些音效。这就是我们新版本的collisions功能的样子:

void collisions() {
 Asteroid* asteroid;
 std::vector<Asteroid*>::iterator ita;
    if( player->m_CurrentFrame == 0 && player->CompoundHitTest( star ) ) {
        player->m_CurrentFrame = 1;
        player->m_NextFrameTime = ms_per_frame;
        player->m_Explode->Run(); // added
        large_explosion_snd->Play();
    }
    if( enemy->m_CurrentFrame == 0 && enemy->CompoundHitTest( star ) ) {
        enemy->m_CurrentFrame = 1;
        enemy->m_NextFrameTime = ms_per_frame;
        enemy->m_Explode->Run(); // added
        large_explosion_snd->Play();
    }
    Projectile* projectile;
    std::vector<Projectile*>::iterator it;
    for(it=projectile_pool->m_ProjectileList.begin(); 
        it!=projectile_pool->m_ProjectileList.end(); 
        it++){
        projectile = *it;
        if( projectile->m_CurrentFrame == 0 && projectile->m_Active ) {
            for( ita = asteroid_list.begin(); ita != 
                asteroid_list.end(); 
                 ita++ ) {
                asteroid = *ita;
                if( asteroid->m_Active ) {
                    if( asteroid->HitTest( projectile ) ) {
                        projectile->m_CurrentFrame = 1;
                        projectile->m_NextFrameTime = ms_per_frame;
                        small_explosion_snd->Play();
                    }
                }
            }
            if( projectile->HitTest( star ) ){
                projectile->m_CurrentFrame = 1;
                projectile->m_NextFrameTime = ms_per_frame;
                small_explosion_snd->Play();
            }
            else if( player->m_CurrentFrame == 0 && ( projectile-
                     >HitTest( player ) ||
                      player->CompoundHitTest( projectile ) ) ) {
                if( player->m_Shield->m_Active == false ) {
                    player->m_CurrentFrame = 1;
                    player->m_NextFrameTime = ms_per_frame;
                    player->m_Explode->Run();
                    large_explosion_snd->Play();
                }
                else { hit_snd->Play(); }
                projectile->m_CurrentFrame = 1;
                projectile->m_NextFrameTime = ms_per_frame;
            }
            else if( enemy->m_CurrentFrame == 0 && ( projectile-
                     >HitTest( enemy ) ||
                      enemy->CompoundHitTest( projectile ) ) ) {
                if( enemy->m_Shield->m_Active == false ) {
                    enemy->m_CurrentFrame = 1;
                    enemy->m_NextFrameTime = ms_per_frame;
                    enemy->m_Explode->Run();
                    large_explosion_snd->Play();
                }
                else { hit_snd->Play(); }
                projectile->m_CurrentFrame = 1;
                projectile->m_NextFrameTime = ms_per_frame;
            }
        }
    }
    for( ita = asteroid_list.begin(); ita != asteroid_list.end(); 
         ita++ ) {
        asteroid = *ita;
        if( asteroid->m_Active ) {
            if( asteroid->HitTest( star ) ) {
                asteroid->Explode();
                small_explosion_snd->Play();
            }
        }
        else { continue; }
        if( player->m_CurrentFrame == 0 && asteroid->m_Active &&
            ( asteroid->HitTest( player ) || player->CompoundHitTest( 
            asteroid ) ) ) {
            if( player->m_Shield->m_Active == false ) {
                player->m_CurrentFrame = 1;
                player->m_NextFrameTime = ms_per_frame;
                player->m_Explode->Run();
                large_explosion_snd->Play();
            }
            else {
                asteroid->Explode();
                small_explosion_snd->Play();
            }
        }
        if( enemy->m_CurrentFrame == 0 && asteroid->m_Active &&
            ( asteroid->HitTest( enemy ) || enemy->CompoundHitTest( 
              asteroid ) ) ) {
            if( enemy->m_Shield->m_Active == false ) {
                enemy->m_CurrentFrame = 1;
                enemy->m_NextFrameTime = ms_per_frame;
                enemy->m_Explode->Run();
                large_explosion_snd->Play();
            }
            else {
                asteroid->Explode();
                small_explosion_snd->Play();
            }
        }
    }
}

声音现在会在几次爆炸和碰撞后播放;例如,在玩家爆炸后:

player->m_Explode->Run(); 
large_explosion_snd->Play();

敌舰爆炸时也会发出声音:

enemy->m_Explode->Run();
large_explosion_snd->Play();

小行星爆炸后,我们会想要同样的效果:

asteroid->Explode();
small_explosion_snd->Play();

如果敌人盾牌被击中,我们想播放hit声音:

if( enemy->m_Shield->m_Active == false ) {
    enemy->m_CurrentFrame = 1;
    enemy->m_NextFrameTime = ms_per_frame;
    enemy->m_Explode->Run();
    large_explosion_snd->Play();
}
else {
    hit_snd->Play();
}

同样,如果玩家的护盾被击中,我们将再次想要播放hit声音:

if( player->m_Shield->m_Active == false ) {
    player->m_CurrentFrame = 1;
    player->m_NextFrameTime = ms_per_frame;

    player->m_Explode->Run();
    large_explosion_snd->Play();
}
else {
    hit_snd->Play();
}

最后,我们需要改变main功能来初始化我们的音频。以下是整个main功能代码:

int main() {
    SDL_Init( SDL_INIT_VIDEO | SDL_INIT_AUDIO );
    int return_val = SDL_CreateWindowAndRenderer( CANVAS_WIDTH, 
    CANVAS_HEIGHT, 0, &window, &renderer );

    if( return_val != 0 ) {
        printf("Error creating renderer %d: %s\n", return_val, 
        IMG_GetError() );
        return 0;
    }

    SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );
    SDL_RenderClear( renderer );
    last_frame_time = last_time = SDL_GetTicks();

    player = new PlayerShip();
    enemy = new EnemyShip();
    star = new Star();
    camera = new Camera(CANVAS_WIDTH, CANVAS_HEIGHT);
    render_manager = new RenderManager();
    locator = new Locator();
    enemy_laser_snd = new Audio(ENEMY_LASER, false);
 player_laser_snd = new Audio(PLAYER_LASER, false);
 small_explosion_snd = new Audio(SMALL_EXPLOSION, true);
 large_explosion_snd = new Audio(LARGE_EXPLOSION, true);
 hit_snd = new Audio(HIT, false);
 device_id = SDL_OpenAudioDevice(NULL, 0, &(enemy_laser_snd->spec), 
    NULL, 0);

 if (device_id == 0) {
 printf("Failed to open audio: %s\n", SDL_GetError());
 }
    int asteroid_x = 0;
    int asteroid_y = 0;
    int angle = 0;

    // SCREEN 1
    for( int i_y = 0; i_y < 8; i_y++ ) {
        asteroid_y += 100;
        asteroid_y += rand() % 400;
        asteroid_x = 0;
        for( int i_x = 0; i_x < 12; i_x++ ) {
            asteroid_x += 66;
            asteroid_x += rand() % 400;
            int y_save = asteroid_y;
            asteroid_y += rand() % 400 - 200;
            angle = rand() % 359;
            asteroid_list.push_back(
                new Asteroid( asteroid_x, asteroid_y,
                get_random_float(0.5, 1.0),
                DEG_TO_RAD(angle) ) );
            asteroid_y = y_save;
        }
    }
    projectile_pool = new ProjectilePool();
    emscripten_set_main_loop(game_loop, 0, 0);
    return 1;
}

我们需要对main函数进行的第一个更改是对SDL_Init调用进行更改,以包括音频子系统的初始化:

SDL_Init( SDL_INIT_VIDEO | SDL_INIT_AUDIO );

我们需要做的另一个改变是增加新的Audio对象和对SDL_OpenAudioDevice的调用:

enemy_laser_snd = new Audio(ENEMY_LASER, false);
player_laser_snd = new Audio(PLAYER_LASER, false);
small_explosion_snd = new Audio(SMALL_EXPLOSION, true);
large_explosion_snd = new Audio(LARGE_EXPLOSION, true);
hit_snd = new Audio(HIT, false);

device_id = SDL_OpenAudioDevice(NULL, 0, &(enemy_laser_snd->spec), 
NULL, 0);

if (device_id == 0) {
    printf("Failed to open audio: %s\n", SDL_GetError());
}

更新 ship.cpp

ship.cpp文件有一处小改动。当飞船发射炮弹时,我们正在增加一个播放声音的呼叫。这发生在Ship::Shoot()功能中。您会注意到对player_laser_snd->Play()的呼叫发生在对projectile->Launch的呼叫之后:

void Ship::Shoot() {
     Projectile* projectile;
     if( current_time - m_LastLaunchTime >= c_MinLaunchTime ) {
         m_LastLaunchTime = current_time;
         projectile = projectile_pool->GetFreeProjectile();
         if( projectile != NULL ) {
             projectile->Launch( m_Position, m_Direction );
             player_laser_snd->Play();
         }
     }
 }

新的 audio.cpp 文件

我们正在添加一个新的audio.cpp文件来实现Audio类构造函数和AudioPlay函数。以下是audio.cpp文件的全文:

#include "game.hpp"

Audio::Audio( char* file_name, bool priority_value ) {
    strcpy( FileName, file_name );
    priority = priority_value;

    if( SDL_LoadWAV(FileName, &spec, &buf, &len) == NULL ) {
        printf("Failed to load wave file: %s\n", SDL_GetError());
    }
}

void Audio::Play() {
    if( priority || SDL_GetQueuedAudioSize(device_id) > 2 ) {
        SDL_ClearQueuedAudio(device_id);
    }

    int success = SDL_QueueAudio(device_id, buf, len);
    if( success < 0 ) {
        printf("SDL_QueueAudio %s failed: %s\n", FileName, SDL_GetError());
    }
}

这个文件中的第一个函数是Audio类的构造函数。该函数将FileName属性设置为传递的值,并设置priority值。它还从传入的文件名加载波形文件,并使用SDL_LoadWAV文件设置specbuflen属性。

功能首先查看这是不是高优先级音频,或者音频队列的大小是否大于两个声音。如果出现这两种情况,我们会清除音频队列:

if( priority || SDL_GetQueuedAudioSize(device_id) > 2 ) {
    SDL_ClearQueuedAudio(device_id);
}

我们这样做是因为我们不想混合音频。我们正在按顺序播放音频。如果我们有一个优先的音频剪辑,我们希望清除队列,以便音频立即播放。如果队列太长,我们也想这样做。然后我们会呼叫SDL_QueueAudio尽快排队播放这个声音:

int success = SDL_QueueAudio(device_id, buf, len);
if( success < 0 ) {
 printf("SDL_QueueAudio %s failed: %s\n", FileName, SDL_GetError());
}

现在,我们应该准备好编译和运行我们的代码了。

编译和运行

现在,我们已经对代码进行了所有必要的更改,我们可以使用 Emscripten 编译并运行新代码:

em++ asteroid.cpp audio.cpp camera.cpp collider.cpp emitter.cpp enemy_ship.cpp finite_state_machine.cpp locator.cpp main.cpp particle.cpp player_ship.cpp projectile_pool.cpp projectile.cpp range.cpp render_manager.cpp shield.cpp ship.cpp star.cpp vector.cpp -o sound_fx.html --preload-file audio --preload-file sprites -std=c++ 17 -s USE_WEBGL2=1 -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=["png"] -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=["png"] 

没有添加新的标志来允许我们使用 SDL 音频库。然而,我们需要添加一个新的--preload-file audio标志来将新的audio目录加载到我们的虚拟文件系统中。一旦你编译了游戏的新版本,你就可以使用 emrun 运行它了(假设你在编译的时候包含了必要的 emrun 标志)。如果您愿意,您可以选择不同的 web 服务器来提供这些文件。

摘要

我们已经讨论了网络上音频的当前(混乱的)状态,并查看了 Emscripten 可用的音频库。我提到了几个可以获得免费音效的地方。我们使用 C 和 Emscripten 创建了一个简单的音频应用,允许我们播放一系列音频文件。然后我们在游戏中加入了音效,包括爆炸和激光声。我们在main()函数中修改了我们的初始化代码来初始化 SDL 音频子系统。我们增加了一个新的Shoot功能,供我们的宇宙飞船发射炮弹时使用。我们还创建了一个新的Audio类来帮助我们播放音频文件。

在下一章中,我们将学习如何在游戏中加入一些物理元素。