MIDI音乐是将音乐结合到游戏的最佳方式之一,本章就将介绍究竟如何使用相对较少的代码播放MIDI音乐。
本章内容包括:
- 如何使用MCI播放MIDI音乐
- 如何向游戏添加MIDI音乐
接上文 游戏编程入门(10):播放数字声音效果
使用MCI播放MIDI音乐
在游戏中播放MIDI音乐的设备是MIDI合成器,它的工作是获得音乐的音符并大声播放出来。MIDI 合成器不是可以插到计算机上的物理设备,它是内置在声卡上的。几乎当前的每一块声卡都包括了MIDI合成器,因此播放 MIDI音乐应该没有任何问题。
使用MCI 播放 MIDI音乐 的很大一部分工作是建立与MIDI合成设备的联系。例如,在发出一个命令(如播放一首MIDI歌曲)之前必须首先打开设备。
打开MIDI设备
设备是使用一个唯一的设备标识符引用的,因此,在打开一个MIDI设备以播放MIDI音乐时,需要跟踪最初打开设备时返回的设备ID。这个ID是在这之后与设备通信的票据。
要想使用MCI 执行任何MIDI任务,必须熟悉mciSendCommand( )函数,它向一个MIDI设备发送命令字符串。这个函数在Win32 API中的描述如下所示:
MCIERROR mciSendCommand(MCIDEVICEID IDDevice, UNIT uiMsg, DWORD dwCommand,DWORD_PTR pdwParam);
mciSendCommand( ) 函数用来向MIDI设备发送命令消息。这个函数的参数根据所发送的消息类型不同而变化,因为我们根据每一种消息来分析它们。
在发送一个播放MIDI歌曲的消息之前,必须首先使用mciSendCommand( ) 函数发送一个打开消息,以便打开MIDI设备。
打开消息名为MCI_OPEN,它需要使用一个名为MCI_OPEN_PARMS的特殊数据结构,其定义如下所示。
typedef struct{
DWORD_PTR dwCallback;
MCIDEVICEID wDeviceID;
LPCSTR lpstrDeviceType;
LPCSTR lpstrElementName;
LPCSTR lpstrAlias;
} MCI_OPEN_PARMS;
在这个结构中,我们感兴趣的成员只有中间3个:wDeviceID,lpstrDeviceType和lpstrElementName。wDeviceID成员存储了MIDI编曲器的设备ID,在这里将用来检索这个设备ID。换句话说,在打开设备时,将使用设备ID填写wDeviceID成员。
在打开设备时必须指定另外两个字段。必须将lpstrDeviceType字段设置为字符串 ”sequencer” ,指出希望打开MIDI编曲器。
lpstrElementName字段必须包含希望播放的MIDI文件的名称。
这揭示了MCI的一个有趣之处,在打开一个设备时就必须指定一个多媒体对象。换句话说,不能仅仅打开一个设备,然后再决定想要播放哪一个多媒体文件。
下面这个例子,展示了打开MIDI编曲器设备的代码。
UNIT uiMIDIPlayerID;
MCI_OPEN_PARMS mciOpenParms;
mciOpenParms.lpstrDeviceType=”sequencer”;
mciOpenParms.lpstrElementName=”Music.mid”;
If(mciSendCommand(NULL,MCI_OPEN,MCI_OPEN_TYPE |MCI_OPEN_ELEMENT,(DWORD)&mciOpenParms)==0)
//获得MIDI播放器的ID
uiMIDIPlayerID=mciOpenParms.wDeviceID;
这段代码初始化MCI_OPEN_PARMS这个结构的两个重要字段,lpstrDeviceType和lpstrElementName,然后将整个结构作为最后一个参数传递给mciSendCommand函数。因为不知道设备的ID,所以将mciSendCommand( ) 函数的第一个参数设置为0,第二个参数MCI_OPEN是发送给MIDI编曲器的消息。最后,第三个参数标识了MCI_OPEN_PARMS数据结构中要在消息中使用的字段。
如果一切正常,那么mciSendCommand函数将返回0,指出成功打开了MIDI编曲器。这时MCI_OPEN_PARMS数据结构的wDeviceID就包含了设备ID。
提示:读者可能已经注意到了,播放MIDI歌曲的MCI 方法涉及打开和播放MIDI文件,而不是使用MIDI歌曲作为资源。不幸的是,对于这个问题没有很好的解决方法,因此不能像使用位图和波形声音那样将 .MID音乐文件编译到可执行游戏文件中。这意味着在发布游戏时,必须随游戏提供单独的 .MID文件。
播放MIDI歌曲
为一首特定的MIDI歌曲打开了MIDI编曲器之后,就准备好发出一个播放命令并开始播放歌曲了。这是通过同一个mciSendCommand( ) 函数完成的,不过这一次要提供一个不同的命令。
在这种情况下,命令是MCI_PLAY,所需的参数比MCI_OPEN命令的参数要稍微简单一些。不过,MCI_PALY命令涉及另一个数据结构,即MCI_PLAY_PARMS,但是在从头到尾地播放一首歌曲时,可以不必初始化这个数据结构。
无论何时想要更精确地控制播放MIDI歌曲的方式,例如为歌曲选择一个不同的起点和终点,则需要使用MCI_PLAY_PARMS数据结构。
MCI_OPEN 命令不需要设备ID,与此不同的是,MCI_PLAY命令需要一个设备ID才能成功播放MIDI歌曲。
下面是播放一首歌曲的代码,它基于上一小节中为歌曲Music.md打开MIDI编曲器的代码。
MCI_PLAY_PARMS mciPlayParms;
mciSendCommand(uiMIDPlayerID,MCI_PALY,0,(DWORD_PTR)&mciPlayParms);
暂停MIDI歌曲
有时,需要在播放MIDI歌曲时暂停播放。例如,在停用主游戏窗口时,不希望就继续播放音乐。因此,需要一种方法来暂停MIDI歌曲。
这是使用MCI_PAUSE命令实现的。下面是使用MCI_PAUSE命令暂停播放一首MIDI歌曲的例子。
mciSendCommand(uiMIDIPlayerID, MCI_PAUSE,0,NULL);
关闭MIDI设备
MCI_CLOSE命令可以通过mciSendCommand( )函数关闭MIDI设备。下面是一个例子。
mciSendCommand(uiMIDIPlayerID, MCI_CLOSE,0,NULL);
向游戏引擎添加MIDI音乐支持
要向游戏引擎添加MIDI 支持,需要做的第一件事情就是记录MIDI 编曲设备ID。使用一个新的成员变量就很容易做到这一点,这个变量如下所示。
UNIT m_uiMIDIPlayerID;
uiMIDIPlayerID成员变量包含MIDI编曲器的设备ID。只要这个ID不为0,就知道打开了设备,准备好播放音乐。如果这个成员变量设置为0,则关闭了设备。这意味着需要在GameEngine( )构造函数中初始化uiMIDIPlayerID成员变量,如下所示。
m_uiMIDIPlayerID=0;
这是为了支持MIDI音乐对GameEngine( )构造函数中的唯一一处修改。
针对MIDI音乐,对游戏引擎最重要的要求就是建立一个完成MIDI音乐任务的接口。下面是在游戏引擎中完成重要的MIDI音乐播放任务的3个新方法。
void PlayMIDISong(LPTSTR szMIDFileName= TEXT(“ “),BOOL bRestart =TRUE);
void PauseMIDISong();
void CloseMIDIPlayer();
这些方法的用途很明显,它们用来播放MIDI歌曲、暂停MIDI歌曲以及关闭MIDI播放器(编曲器)。而打开MIDI播放器这项任务实际上是在PlayMIDISong()方法中处理的。
PlayMIDISong()方法有一个字符串参数,它是要播放的MIDI文件的文件名。这个文件名用作打开MIDI播放器的基础。MIDI文件名有一个默认值可能显得有点奇怪,这意味着不需要提供文件名。调用PlayMIDISong()方法而不带文件名只有在这种情况下才有效:已经开始播放了一首歌曲,并且现在暂停了播放,在这种情况下它将继续播放歌曲。
允许向PlayMIDISong()方法传递空文件名的目的是允许重新启动或者继续已经开始播放的歌曲。在这种情况下,方法的第2个参数bRestart用来确定如何播放歌曲。如果只是暂停了游戏中的音乐,那么应该继续播放。如果是开始一个新游戏并从头开始播放音乐,那么应该重新启动播放。
PlayMIDISong()方法负责打开MIDI播放器(如果还没有打开它)。这个方法首先检查是否还没有打开播发器,如果确实没有,则发出一个打开命令,以便打开MIDI编曲设备。MIDI编曲器的设备ID存储在m_uiMIDIPlayer变量中。如果bRestart参数为TRUE,那么PlayMIDISong()方法将重新启动歌曲。然后通过MCI发出一个播放命令,以播放MIDI歌曲。如果播放命令失败,则关闭MIDI编曲设备。
在某些时候,可能希望在开始播放MIDI歌曲之后暂停播放。使用PauseMIDISong( )方法就可以做到这点,它仅仅向MIDI播放器发出一个暂停命令(条件是设备ID没有设置为0)。
CloseMIDIPlayer()方法用来关闭MIDI设备并清空设备ID。这个方法首先进行检查,确保设备确实是打开的(通过检查设备ID是否为0)。如果设备是打开的,那么CloseMIDIPlayer( )方法将发出一个关闭命令,然后清空设备ID成员变量,从而关闭设备。
开发Henway 2(小鸡过马路2)示例程序
为Henway添加MIDI音乐,还使用了波形声音,从而将声音效果结合到游戏中。
注意:若出现编译错误,请在项目设置->连接->对象/库模块中 加入 msimg32.lib winmm.lib
Henway 2(小鸡过马路2)目录结构和效果图
Henway 2(小鸡过马路2)目录结构:
Henway 2(小鸡过马路2)效果图:
编写程序代码
具有声音功能的Henway2游戏的代码从GameStart( )函数开始,现在这个函数在最后包括了一行代码。用来播放存储在文件Music.md中的MIDI 歌曲。
g_pGame->PlayMIDISong(TEXT(“Music.mid”));
这行代码向游戏引擎中的PlayMIDISong( )方法传递MIDI 歌曲文件的名称。
正如GameStart( )函数通过开始播放MIDI 歌曲来打开MIDI 播放器一样,GameEnd( )函数负责通过关闭MIDI 播放器来进行清理。下面一行代码出现在GameEnd( )函数的开始位置。
g_pGame->CloseMIDIPlayer();
GameEnd( )函数调用CloseMIDIPlayer( )方法来关闭MIDI 播放器,执行必要的MIDI音乐清理工作。
GameActivate( ) 和GameDeactivate( )
如果停用了游戏窗口,那么继续播放MIDI歌曲就不太合适。为了防止继续播放,要在发生窗口停用时暂停MIDI歌曲,并在窗口重新激活时再次。
// 激活游戏
void GameActivate(HWND hWindow)
{
// 继续背景音乐
g_pGame->PlayMIDISong(TEXT(""), FALSE);
}
// 停用游戏
void GameDeactivate(HWND hWindow)
{
// 暂停背景音乐
g_pGame->PauseMIDISong();
}
GameActivate( )函数负责继续播放暂停的MIDI歌曲,方法是调用PlayMIDISong( )方法并指定FALSE作为第2个参数。FALSE是指不要反饶歌曲,从而具有从之前暂停的位置继续播放的效果。GameDeactivate( )函数执行相反的任务,它调用PauseMIDISong( )来暂停MIDI歌曲。
GameCycle( )
GameCycle( )函数没有任何与MIDI有关的代码。但是它包含了一些新的声音效果代码。更具体的说,现在GameCycle( )函数会在随机的时间间隔播放汽车喇叭声,向这个游戏增添了一些真实感。
//游戏循环
void GameCycle()
{
if (!g_bGameOver)
{
// 随机播放一个随机的汽车声音
if (rand() % 100 == 0)
if (rand() % 2 == 0)
PlaySound((LPCSTR)IDW_CARHORN1, g_hInstance, SND_ASYNC | SND_RESOURCE);
else
PlaySound((LPCSTR)IDW_CARHORN2, g_hInstance, SND_ASYNC | SND_RESOURCE);
// 更新子画面
g_pGame->UpdateSprites();
// 获得用来重新绘制游戏的设备环境
HWND hWindow = g_pGame->GetWindow();
HDC hDC = GetDC(hWindow);
// 在屏幕外设备环境上绘制游戏
GamePaint(g_hOffscreenDC);
// 将屏幕外位图位块传送到游戏屏幕上
BitBlt(hDC, 0, 0, g_pGame->GetWidth(), g_pGame->GetHeight(),
g_hOffscreenDC, 0, 0, SRCCOPY);
// 清理
ReleaseDC(hWindow, hDC);
}
}
MouseButtonDown( )
MouseButtonDown( )函数在开始一个新游戏的同时重新播放MIDI歌曲。
// 按下鼠标
void MouseButtonDown(int x, int y, BOOL bLeft)
{
// 如有必要,开始一个新游戏
if (g_bGameOver)
{
// 重新启动背景音乐
g_pGame->PlayMIDISong(); //不带任何参数将重新播放当前歌曲
// 初始化游戏变量
g_iNumLives = 3;
g_iScore = 0;
g_bGameOver = FALSE;
}
}
SpriteCollision( )
SpriteCollision( )函数在小鸡被汽车压过、游戏结束时播放声音效果。
// 碰撞函数
BOOL SpriteCollision(Sprite* pSpriteHitter, Sprite* pSpriteHittee)
{
// 检查是否撞上了小鸡
if (pSpriteHittee == g_pChickenSprite)
{
// 使小鸡移回开始位置
g_pChickenSprite->SetPosition(4, 175);
// 查看游戏是否结束
if (--g_iNumLives > 0)
// 播放小鸡被撞上的声音
PlaySound((LPCSTR)IDW_SQUISH, g_hInstance, SND_ASYNC | SND_RESOURCE);
else
{
// 播放游戏终止的声音
PlaySound((LPCSTR)IDW_GAMEOVER, g_hInstance, SND_ASYNC | SND_RESOURCE);
// 显示游戏结束消息
TCHAR szText[64];
wsprintf(szText, "Game Over! You scored %d points.", g_iScore);
MessageBox(g_pGame->GetWindow(), szText, TEXT("Henway 2"), MB_OK);
g_bGameOver = TRUE;
// 暂停背景音乐
g_pGame->PauseMIDISong();
}
return FALSE;
}
return TRUE;
}
MoveChicken( )
MoveChicken( )函数在小鸡安全穿过高速公路的任何时候都播放一个庆祝声音。
void MoveChicken(int iXDistance, int iYDistance)
{
// 播放小鸡声音
PlaySound((LPCSTR)IDW_BOK, g_hInstance, SND_ASYNC | SND_NOSTOP | SND_RESOURCE);
// 使小鸡移动到新位置
g_pChickenSprite->OffsetPosition(iXDistance, iYDistance);
// 查看小鸡是否真实通过
if (g_pChickenSprite->GetPosition().left > 400)
{
// 播放小鸡安全通过的声音
PlaySound((LPCSTR)IDW_CELEBRATE, g_hInstance, SND_ASYNC | SND_RESOURCE);
// 使小鸡移回到开始位置并增加得分
g_pChickenSprite->SetPosition(4, 175);
g_iScore += 150;
}
}
源代码下载