六、声音

Cocos2d-x 框架附带了一个名为CocosDenshion的音频引擎,它是从用于 iPhone 的 Cocos2d 继承而来的。这个引擎封装了播放音效和背景音乐的所有复杂性。现在,Cocos2d-x 有了另一个从头开始构建的音频引擎,目的是提供比CocosDenshion库更大的灵活性。请注意,没有计划从 Cocos2d-x 框架中删除CocosDenshion音频引擎,现在 Cocos2d-x 中普遍存在冗余组件,以便程序员可以选择更适合他们需求的组件。

本章将涵盖以下主题:

  • 播放背景音乐和音效
  • 修改音频属性
  • 离开游戏时处理音频
  • 新的音频引擎

播放背景音乐和音效

为了通过使用CocosDenshion音频引擎将背景音乐添加到我们的游戏中,第一步是将以下文件包含添加到我们的HelloWorldScene.cpp实现文件中:

#include "SimpleAudioEngine.h"

在这个头文件中,在私有成员段中,我们还将添加我们新的initAudio方法的声明,我们将使用它来启动我们的背景音乐,并预加载每次炸弹撞上我们的player精灵时将播放的音频效果:

void initAudio();

现在在HelloWorld.cpp实现文件中,我们将使用CocosDenshion命名空间,这样我们就不必在每次访问音频引擎单例实例时都隐式引用这个命名空间:

using namespace CocosDenshion;

现在在同一个实现文件中,我们将编写 initAudio方法的主体,正如我们之前提到的,它将开始播放循环音乐背景。我们提供了这一点以及本章的源代码,并且我们将预载每次我们的玩家失败时将要播放的音频效果。playBackgroundMusic方法的第二个参数是一个布尔值,它决定了我们是否希望我们的背景音乐永远重复。

void HelloWorld::initAudio()
{
   SimpleAudioEngine::getInstance()->playBackgroundMusic("music.mp3",    true);
   SimpleAudioEngine::getInstance()->preloadEffect("uh.wav");   
}

让我们在我们的Resources目录中创建一个名为sounds的文件夹,这样我们就可以有组织地在那里添加我们所有的声音文件。完成此操作后,我们需要在searchPaths std::vector实例化后将以下行添加到我们的AppDelegate.cpp实现文件中,用于将sounds目录添加到搜索路径中,以便音频引擎可以找到我们的文件:

searchPaths.push_back("sounds");

我们鼓励您整理您的Resources文件夹,为声音创建一个文件夹,并为音频和音乐创建子文件夹,这样我们就不会在根目录中拥有所有内容。

让我们来看看onCollision方法,每次两个物理体碰撞时都会调用这个方法。如果玩家雪碧的物理体出现崩溃,那么我们将停止背景音乐,播放uh.wav音效,然后通过添加以下几行代码切换到游戏结束场景:

SimpleAudioEngine::getInstance()->stopBackgroundMusic();
SimpleAudioEngine::getInstance()->playEffect("uh.wav");

最后,我们将在HelloWorld.cpp实现文件中的init方法末尾添加对我们的initAudio方法的调用:

initAudio();

修改音频属性

您可以通过调用setBackgroundMusicVolume方法和setEffectsVolume方法来轻松修改背景音乐和音效的基本音频属性。两者都接收一个float值作为参数,其中0.0表示静音,1.0表示最大音量,如下代码清单所示:

SimpleAudioEngine::getInstance()->setBackgroundMusicVolume(0.5f);
SimpleAudioEngine::getInstance()->setEffectsVolume(1.0f);

离开游戏时处理音频

当游戏活动不再活跃时,背景音乐和音效不会自动停止,应该通过从AppDelegate类的applicationDidEnterBackgound方法中删除以下评论块来手动停止:

// if you use SimpleAudioEngine, it must be pause
 SimpleAudioEngine::getInstance()->pauseBackgroundMusic();

为了使这一新的代码行工作,我们需要添加与我们已经添加到HelloWorld.cpp实现文件中的代码行相同的代码行,以便使用CocosDenshion命名空间:

using namespace CocosDenshion;

当用户切换到另一个应用程序时,您的游戏将停止所有当前声音。用户一回到我们的游戏,我们就需要恢复音乐。我们可以用与之前相同的方式来实现,但是现在,我们将从AppDelegate类的applicationWillEnterForeground方法中移除以下注释块:

 // if you use SimpleAudioEngine, it must resume here
SimpleAudioEngine::getInstance()->resumeBackgroundMusic();

全新的音频引擎

实验阶段的 Cocos2d-x 3.4 为了增加更多的功能和灵活性,从头开始构建了一个新的音频引擎。Cocos2d-x 的新音频引擎现在可用于安卓、iOS、苹果操作系统和 win-32 平台。它能够在安卓平台上同时再现多达 24 种声音;该数字可能会因平台而异。

如果您运行与 Cocos2d-x 框架捆绑在一起的测试,那么您可以测试这两个音频引擎。在运行时,它们可能听起来没有任何明显的区别,但它们在内部非常不同。

CocosDenshion引擎不同,这款新引擎的音效和背景音乐没有区别。因此,框架只有一个setVolume方法,而CocosDenshion有两个方法— setBackgroundMusicVolumesetEffectsVolume。在本节的后面,我们将向您展示如何调整每个播放音频的音量,而不管它是否是背景音乐的音效。

让我们在名为initAudioNewEngineHelloWorldScene.h头文件中添加一个新的方法声明,顾名思义,它将初始化我们游戏的音频功能,但现在它将使用相同的新音频引擎。

我们需要在我们的HelloWorldScene.h文件中包含新的引擎头,如下所示:

#include "audio/include/AudioEngine.h"

让我们在我们的HelloWorld.cpp实现文件中包含下面一行代码,这样我们就可以直接调用AudioEngine类,而不需要每次使用它时都引用它的名称空间:

using namespace cocos2d::experimental;

现在,让我们在实现文件中为initAudioNewEngine方法编写如下代码:

void HelloWorld::initAudioNewEngine()
{   
   if(AudioEngine::lazyInit())
   {
      auto musicId = AudioEngine::play2d("music.mp3");
      AudioEngine::setVolume(musicId, 0.25f);
      CCLOG("Audio initialized successfully");

   }else
   {
      log("Error while initializing new audio engine");
   }   
}

与使用单一实例的CocosDenshion不同,新的音频引擎将其所有方法声明为静态的。

从前面的代码清单中我们可以看到,在调用play2d方法之前,我们先调用lazyInit方法。尽管play2d在内部调用lazyInit方法,但我们想尽快知道我们的安卓系统是否能够重现音频并采取行动。请注意,当play2d方法返回AudioEngine::INVALID_AUDIO_ID值时,您还需要找出音频初始化是否有问题。

每次我们通过调用play2d方法播放任何给定的声音时,它都会返回一个唯一的增量audioID从零开始的索引,我们会将其存储起来,这样我们就可以在每次想要对其执行特定操作时引用该特定音频实例,例如改变音量、移动到特定位置、停止、暂停或恢复。

新音频引擎的一个缺点是,它支持的音频格式数量仍然有限。目前不支持.wav文件。所以为了播放uh.wav的声音,我们将把它转换成 mp3,然后我们将通过调用play2donCollision方法中播放它,如下所示:

AudioEngine::stopAll();
AudioEngine::play2d("uh.mp3");

我们在本章提供的代码资源档案中包含了新的uh.mp3音频文件。

对于我们的游戏,我们将两者都实现;传统的CocosDenshion引擎,它是最成熟的音频引擎,为我们提供了我们需要的基本功能,比如播放音效和背景音乐;和新引擎中相同的音频功能。

新音频引擎中包含的新功能

play2d方法是重载的,因此我们可以指定是否要循环声音、初始音量以及我们想要应用于它的音频配置文件。AudioProfile类是 Cocos2d-x 框架的一部分,它只有三个属性:name,不能为空;maxInstances,将定义同时播放多少声音;和minDelay,这是一个double数据类型,将指定声音之间的最小延迟是多少。

新音频引擎的另一个特点是能够通过调用setCurrentTime方法并在秒内传递audioID方法和自定义位置(由float表示)从自定义位置播放音频。

在新的音频引擎中,您可以指定希望在给定的音频实例完成播放时调用一个函数。这可以通过调用setFinishCallback方法来实现。

每次播放音频时,都会将其缓存,这样就不必再从文件系统中读取它。如果我们想释放一些资源,那么我们可以调用uncacheAll方法来移除音频引擎内部用于回放音频的所有缓冲区,或者您可以通过调用uncache方法并指定要移除的文件系统的文件路径来从缓存中移除任何特定的音频。

本节的目的是让你知道另一个处于实验阶段的音频引擎,如果你想在你的游戏中添加任何音频功能,而CocosDenshion没有,那么你应该检查另一个音频引擎,看看它是否有你想要的。

新的音频引擎可以在苹果操作系统、iOS 和 win-32 平台上同时播放多达 32 种声音,但在安卓系统上最多只能同时播放 24 种声音。

在我们的游戏中增加一个静音按钮

在结束这一章之前,我们将在我们的游戏中添加一个静音按钮,这样我们就可以在一次触摸中简单地将我们的音效和背景音乐音量设置为零。

为了实现这一点,我们打算给HelloWorld类增加两个方法;一个用于初始化按钮,另一个用于实际静音所有声音。

为了实现这一点,我们将在私有部分下的HelloWorldScene.h头文件中添加以下几行:

int _musicId;
cocos2d::MenuItemImage* _muteItem;
cocos2d::MenuItemImage* _unmuteItem;
void initMuteButton();
void muteCallback(cocos2d::Ref* pSender);

现在,我们将把initMuteButton实现代码添加到HelloWorldScene.cpp文件中,如下所示:

void HelloWorld::initMuteButton()
{
   _muteItem = MenuItemImage::create("mute.png", "mute.png", CC_CALLBACK_1(HelloWorld::muteCallback, this));    

   _muteItem->setPosition(Vec2(_visibleSize.width - _muteItem-  >getContentSize().width/2 ,
   _visibleSize.height - _muteItem->getContentSize(). height / 2));
   _unmuteItem = MenuItemImage::create("unmute.png", "unmute.png", CC_CALLBACK_1(HelloWorld::muteCallback, this));    

   _unmuteItem->setPosition(Vec2(_visibleSize.width - _unmuteItem- >getContentSize().width/2 , _visibleSize.height - _unmuteItem->getContentSize().height /2));
   _unmuteItem -> setVisible(false);

   auto menu = Menu::create(_muteItem, _unmuteItem , nullptr);
   menu->setPosition(Vec2::ZERO);
   this->addChild(menu, 1);
}

正如你所看到的,我们刚刚创建了一个新的菜单,我们在其中添加了两个按钮,一个用于静音我们的游戏,另一个用于取消静音。我们将这些都存储在成员变量中,这样我们就可以通过muteCallback方法访问它们,我们在下面的代码清单中声明了这一点:

void HelloWorld::muteCallback(cocos2d::Ref* pSender)
{   
   if(_muteItem -> isVisible())
   {

      //CocosDenshion
      //SimpleAudioEngine::getInstance()->setBackgroundMusicVolume(0);
      AudioEngine::setVolume(_musicId, 0);
   }else
   {   
      //SimpleAudioEngine::getInstance()->setBackgroundMusicVolume(1);
      AudioEngine::setVolume(_musicId, 1);
   }

   _muteItem->setVisible(!_muteItem->isVisible());
   _unmuteItem->setVisible(!_muteItem->isVisible());
}

在这里,我们基本上只是问_muteItem菜单项是否可见。如果可见,则使用新的音频引擎CocosDenshion将音量设置为零,否则将音量设置为最大值,即 1。无论是哪种情况,请更改静音和取消静音菜单项中的实际可见性值。

我们可以在下面的截图中看到最终结果:

Adding a mute button to our game

把所有东西放在一起

这就是我们的AppDelegate.cpp实现文件中的applicationDidFinishLaunching方法在我们在resources路径中添加了包含sounds文件夹的行之后的样子:

bool AppDelegate::applicationDidFinishLaunching() {
    auto director = Director::getInstance();
    // OpenGL initialization done by cocos project creation script
    auto glview = director->getOpenGLView();
    if(!glview) {
    glview = GLViewImpl::create("Happy Bunny");
    glview->setFrameSize(480, 800);
    director->setOpenGLView(glview);
   }

   Size screenSize = glview->getFrameSize();
   Size designSize(768, 1280);
   std::vector<std::string> searchPaths;   
 searchPaths.push_back("sounds");

   if (screenSize.height > 800){
      //High Resolution
      searchPaths.push_back("images/high");
      director->setContentScaleFactor(1280.0f / designSize.height);
   }
   else if (screenSize.height > 600){
      //Mid resolution
      searchPaths.push_back("images/mid");
      director->setContentScaleFactor(800.0f / designSize.height);
   }
   else{
      //Low resolution
      searchPaths.push_back("images/low");
      director->setContentScaleFactor(320.0f / designSize.height);
   }
   FileUtils::getInstance()->setSearchPaths(searchPaths);
   glview->setDesignResolutionSize(designSize.width, designSize. height, ResolutionPolicy::EXACT_FIT);
   auto scene = HelloWorld::createScene();
   director->runWithScene(scene);
   return true;
}

下面的代码清单显示了我们的HelloWorldScene.h头文件在本章中所做的更改之后的样子:

#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__

#include "cocos2d.h"
#include "PauseScene.h"
#include "GameOverScene.h"

class HelloWorld : public cocos2d::Layer
{
public:
    static cocos2d::Scene* createScene();
    virtual bool init();
    CREATE_FUNC(HelloWorld);
private:
   cocos2d::Director *_director;
   cocos2d::Size _visibleSize;   
   cocos2d::Sprite* _sprBomb;
   cocos2d::Sprite* _sprPlayer;   
   cocos2d::MenuItemImage* _muteItem;
   cocos2d::MenuItemImage* _unmuteItem;   
   int _score;
   int _musicId;
   void initPhysics();
   void pauseCallback(cocos2d::Ref* pSender);
   void muteCallback(cocos2d::Ref* pSender);
   bool onCollision(cocos2d::PhysicsContact& contact);
   void setPhysicsBody(cocos2d::Sprite* sprite);
   void initTouch();
   void movePlayerByTouch(cocos2d::Touch* touch, cocos2d::Event* event);
   void movePlayerIfPossible(float newX);
   void movePlayerByAccelerometer(cocos2d::Acceleration* acceleration, cocos2d::Event* event);
   void initAccelerometer();
   void initBackButtonListener();
   void onKeyPressed(cocos2d::EventKeyboard::KeyCode keyCode, cocos2d::Event* event);
   void updateScore(float dt);
   void addBombs(float dt);   
   void initAudio();
   void initAudioNewEngine();
   void initMuteButton();
};

#endif // __HELLOWORLD_SCENE_H__

最后,添加音频管理代码后,我们的HelloWorldScene.cpp实现文件是这样的:

#include "HelloWorldScene.h"#include "SimpleAudioEngine.h"
#include "audio/include/AudioEngine.h"
#include "../cocos2d/cocos/platform/android/jni/Java_org_cocos2dx_lib_Cocos2dxHelper.h"

USING_NS_CC;
using namespace CocosDenshion;
using namespace cocos2d::experimental;

Scene* HelloWorld::createScene()
{
  //no changes here
}

// physics code …

// event handling code …

在下面的方法中,我们将使用新的音频引擎初始化音频。请注意,我们将在_musicId整数成员变量中存储对应于音频实例背景音乐的 ID:

void HelloWorld::initAudioNewEngine()
{   
   if(AudioEngine::lazyInit())
   {      
      _musicId = AudioEngine::play2d("music.mp3");
      AudioEngine::setVolume(_musicId, 1);      
      AudioEngine::setLoop(_musicId,true);      
      CCLOG("Audio initialized successfully");
   }else
   {
      CCLOG("Error while initializing new audio engine");
   }   
}

这里,我们执行的初始化工作与之前的方法相同,但现在我们使用CocosDenshion音频引擎来执行:

void HelloWorld::initAudio()
{
   SimpleAudioEngine::getInstance()->playBackgroundMusic("music. mp3",true);   
   SimpleAudioEngine::getInstance()->preloadEffect("uh.wav");   
   SimpleAudioEngine::getInstance()->setBackgroundMusicVolume(1.0f);
}

在下面的方法中,我们创建了一个简单的菜单来显示游戏静音和取消静音的选项。在这里,我们将静音和非静音子画面存储在它们对应的成员变量中,以便我们稍后可以在muteCallback方法中访问它们,以操纵它们的visibility属性:

void HelloWorld::initMuteButton()
{
   _sprMute = Sprite::create("mute.png");
   _sprUnmute = Sprite::create("unmute.png");
   _muteItem = MenuItemImage::create("mute.png", "mute.png", CC_CALLBACK_1(HelloWorld::muteCallback, this));    
   _muteItem->setPosition(Vec2(_visibleSize.width - _muteItem- >getContentSize().width/2 ,
   _visibleSize.height - _muteItem->getContentSize().height / 2));
   _unmuteItem = MenuItemImage::create("unmute.png", "unmute.png", CC_CALLBACK_1(HelloWorld::muteCallback, this));    
   _unmuteItem->setPosition(Vec2(_visibleSize.width - _unmuteItem->getContentSize().width/2 ,
   _visibleSize.height - _unmuteItem->getContentSize(). height /2));
   _unmuteItem -> setVisible(false);
   auto menu = Menu::create(_muteItem, _unmuteItem , nullptr);
    menu->setPosition(Vec2::ZERO);
    this->addChild(menu, 1);

}

每次按下静音或取消静音菜单项都会调用以下方法,在这个方法中,我们只需将音量设置为零,并根据触摸的选项显示静音或取消静音按钮:

void HelloWorld::muteCallback(cocos2d::Ref* pSender)
{   
   if(_muteItem -> isVisible())
   {
      //CocosDenshion
      //SimpleAudioEngine::getInstance()->setBackgroundMusicVolume(0);
      AudioEngine::setVolume(_musicId, 0);   

   }else
   {   
      //SimpleAudioEngine::getInstance()->setBackgroundMusicVolume(1);
      AudioEngine::setVolume(_musicId, 1);
   }

   _muteItem->setVisible(!_muteItem->isVisible());
   _unmuteItem->setVisible(!_muteItem->isVisible());
}

我们对init方法所做的唯一修改是在最后添加对initMuteButton();方法的调用:

bool HelloWorld::init()
{
    if ( !Layer::init() )
    {
        return false;
    }
   _score = 0;
   _director = Director::getInstance();
   _visibleSize = _director->getVisibleSize();
   auto origin = _director->getVisibleOrigin();
   auto closeItem = MenuItemImage::create("pause.png", "pause_pressed.png", CC_CALLBACK_1(HelloWorld::pauseCallback, this));
   closeItem->setPosition(Vec2(_visibleSize.width - closeItem->getContentSize().width/2, closeItem->getContentSize().height/2));
   auto menu = Menu::create(closeItem, nullptr);
   menu->setPosition(Vec2::ZERO);
   this->addChild(menu, 1);
   _sprBomb = Sprite::create("bomb.png");   
   _sprBomb->setPosition(_visibleSize.width / 2, _visibleSize.height +_sprBomb->getContentSize().height/2);
   this->addChild(_sprBomb,1);
   auto bg = Sprite::create("background.png");
   bg->setAnchorPoint(Vec2());
   bg->setPosition(0,0);
   this->addChild(bg, -1);
   _sprPlayer = Sprite::create("player.png");   
   _sprPlayer->setPosition(_visibleSize.width / 2, _visibleSize.height* 0.23);
   setPhysicsBody(_sprPlayer);
   this->addChild(_sprPlayer, 0);
   //Animations
   Vector<SpriteFrame*> frames;
   Size playerSize = _sprPlayer->getContentSize();
   frames.pushBack(SpriteFrame::create("player.png", Rect(0, 0, playerSize.width, playerSize.height)));
   frames.pushBack(SpriteFrame::create("player2.png", Rect(0, 0, playerSize.width, playerSize.height)));
   auto animation = Animation::createWithSpriteFrames(frames,0.2f);
   auto animate = Animate::create(animation);
   _sprPlayer->runAction(RepeatForever::create(animate));      
   setPhysicsBody(_sprBomb);   
   initPhysics();   
   _sprBomb->getPhysicsBody()->setVelocity(Vect(0,-100));   
   initTouch();
   initAccelerometer();   
   #if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
   setKeepScreenOnJni(true);
   #endif
   initBackButtonListener();
   schedule(CC_SCHEDULE_SELECTOR(HelloWorld::updateScore), 3.0f);
   schedule(CC_SCHEDULE_SELECTOR(HelloWorld::addBombs), 8.0f);
   initAudioNewEngine();
 initMuteButton();
   return true;
}

正如可以看到的,尽管我们使用了新的音频引擎来播放声音,但我们已经显示了使用传统CocosDenshion音频引擎所需的所有代码。要启用CocosDenshion实现,您只需要调用HelloWorld.cpp文件的init方法底部的initAudio方法,而不是调用initAudioNewEngine方法,最后,您需要删除onCollision方法中CocosDenshion实现代码的注释斜线,并注释新的音频引擎回放代码。

总结

在这一章中,我们通过使用与 Cocos2d-x 框架捆绑在一起的两个音频引擎,以非常简单的方式为我们的游戏添加了背景音乐和音效。

在下一章中,我们将介绍如何在我们的游戏中添加粒子系统,以便在每次炸弹击中我们的player精灵时模拟更真实的爆炸。