1、游戏中的声音资源打包 董波 介绍 在以前的游戏当中,几乎所有的游戏都有将资源打包,无论是图形资源、声音文件等等。现在,越来越多的游戏的声音资源都公开了,通常背景音乐等采用ogg,音效采用wav;但是图形资源绝大部分游戏还是都是打包了的。有的有加密,有的没有。 在学习游戏编程的初期我就对这个东西很感兴趣,但是又没有人教,到网上找资料也少得可怜,所以就只好自己摸索了。记得去年也做过类似的工作,但是当时做的感觉很不好,采取了一些投机取巧的方法,虽然看起来是打包了,但是实际上只是一个躯壳而已。 随着时间的推移,自己懂得比以前稍微多了一点点,就想做点什么,即使现在还是“菜青虫”水平,“菜鸟”都
2、算不上,呵呵。马上要毕业了,论文也早Over了,所以时间挺多的,因此写点这样的东西,希望现在的“曾经的我”能够从这个文章里边得到一点灵感。 这篇文章将介绍一种最简单的声音文件打包并播放的方法。最简单意味着它没有加密没有考虑太多的特殊情况,只实现了最简单的wav打包和播放,流媒体没考虑在内,因为它们的处理毕竟还是有很大的不同的。 注意 本文中所提到的程序以及附件都使用Visual Studio 2008 编写,项目文件也只能用VS2008打开。另外依赖于boost.filesystem、boost.shared_ptr、boost.unoredered_set,说白了如果您想查看效果的
3、话必须安装boost,boost的安装教程可以参考我写得这个: 。另外声音播放我使用的Fmod引擎,所以您需要到http://www.fmod.org/ 去下载最新的fmod引擎sdk安装并配置好VS。我相信VS2005和VS2008在配置正确的情况下应该都能编译通过的 。 文件打包 文件打包有很多方式的,我采取了一种比较简单的方式,我将打包后的结果分为数据文件和索引文件两部分,这样的好处是什么呢?至少我们追加文件的时候很方便也很快。加入我们采用文件头的形式,即索引在数据文件的头部,那么当我们要添加的新文件的时候,则意味着所有文件的重写,因为操作系统不会提供一个执行“插入”操作的API
4、这样最直接的好处就是游戏工具执行“保存”动作的时候的快速性。 另外,文件打包还可以选择是否加密,是否压缩等等。加密这块我不是很了解,我去年做过一个加了密的,但是后来反思的时候发现并没有什么意义,与其说文件打包是为了保护资源的话,还不如说是为了管理的方便和效率。因为我觉得在一个电脑高手面前,加不加密并没有什么意义,比如说传奇式的”mycrack”大大。压缩个人以为还是有点意义的,至少可以少占不少硬盘,当然,这是需要付出代价的,运行时必须解压。Ogre3D中就支持zip打包,应该也是使用zlib来做的,使用zlib来做这个事情是很容易的,呵呵。它的官方网站是: 因此我们可以有这样的一个类来做
5、这个简单的操作: namespace db { class CFilePack { public: typedef detail::Node node_type; typedef detail::CFilePackImp::node_vec node_vec; public: CFilePack( const std::wstring& strDataFileName, const std::wstring& strIndexFileName ); CFilePack(
6、const std::string& strDataFileName, const std::string& strIndexFileName ); ~CFilePack(); public: bool addFile( const std::string& strFileName ); bool addFile( const std::wstring& strFileName ); bool load(); bool save() const; const node_vec& getNodes() const
7、{
return m_spImp->getNodes();
}
protected:
boost::shared_ptr
8、 { SIZE = 20 }; char szName[SIZE]; unsigned uOffset; unsigned uSize; public: bool operator == ( const Node& node ) const { return _stricmp( szName, node.szName ) == 0; } bool operator == ( const char* strName ) const { return _stricm
9、p( szName, strName ) == 0; } bool operator < ( const Node& node ) const { return _stricmp( szName, node.szName )<0; } }; class CFilePackImp { public: typedef std::string string_type; typedef unsigned offset_type; typedef std::ve
10、ctor
11、 public: bool load(); bool save() const; inline const node_vec& getNodes() const { return m_vecNodes; } protected: string_type m_strDataFileName; // 数据文件名 string_type m_strIndexFileName; // 索引文件名 node_vec m_vecNodes;
12、 static const size_t uMaxSaveBytesOnTime = 1024 * 1024; // 1M }; } } 有了这两个类我们就可以这样来生成打包后的数据文件和索引文件,当然下面的Demo代码忽略了错误检测等等: #include "filepack.hpp" int main() { // 下面我们使用这个类把三首歌曲打包成一个数据文件和一个索引 db::CFilePack pack( "music.data", "music.inx" ); pack.addFile( "drumloop.wav" ); pack.
13、addFile( "jaguar.wav" ); pack.addFile( L"swish.wav" ); // pack.load(); return 0; } 实现细节就不说了,大家可以参考附件。 声音加载与播放 现在我们要做的就是从索引文件中读取出文件信息,然后去数据文件中找我们感兴趣的数据。在上面的代码中我们可以看到,每一个索引节点都由固定长度的文件名字符串、在数据文件中的偏移量和数据的长度组成,现在在管理中就应该先获取这些信息,由于索引文件通常都很小,所以我们在程序运行期间将这些信息保存在内存中,根据是否有boost分别选择散列表unordere
14、d_set或者红黑树实现的set来保存。
由于在整个系统中通常都只有一个这样的声音文件包,因此我们实现一个简单的单件管理类,这是它的接口类:
#pragma once
#include
15、st string_type& strDataFile, const string_type& strIndexFile ) = 0; virtual ISound_SP createSound( const string_type& strFile ) = 0; virtual void release()=0; }; } 从它我们实现一个Fmod的管理类: #include "..\\pack_base\config.hpp" #include "ISoundManager.hpp" #in
16、clude "Singleton.hpp"
#include "..\\pack_base\filepack.hpp"
#include
17、t >
{
size_t operator()( const db::detail::Node& data ) const
{
size_t uSeed = 0;
size_t uLen = strlen( data.szName );
for( size_t i=0; i
18、SOUND_MANAGER_CONTAINER_IMP__ std::set
#endif // #ifdef __DB_WORK_WITH_BOOST__
namespace db
{
class CFmodSoundManager : public ISoundManager, public db::Singleton
19、 node_type;
#ifdef __DB_WORK_WITH_BOOST__
typedef __SOUND_MANAGER_CONTAINER_IMP__
20、anager(); virtual ~CFmodSoundManager(); public: bool initialize( const string_type& strDataFile, const string_type& strIndexFile ); ISound_SP createSound( const string_type& strFile ); void release(); protected:
21、bool initFmodSystem(); bool initData(); bool loadtomemory( void* pBuf, size_t uByteCounts, size_t uoffset ); protected: string_type m_strDataFile; string_type m_strIndexFile; container_type m_nodes; FMOD::System* m_pSystem; }; }
22、 这是一个单件。接下来需要实现一个Sound的接口,因为Sound有普通的wav这样的声音文件,也有像mp3这样的流媒体。因此需要区别对待的,比如说wav的处理思路通常都是直接加载到内存中而mp3等是不会的。我们这里只实现wav的。接口类:
#pragma once
#include
23、 virtual bool load() = 0; virtual void unLoad() = 0; virtual void play() = 0; virtual void changePauseState() = 0; virtual const string_type& getName() const = 0; virtual void release() = 0; };
24、typedef boost::shared_ptr
25、 CMemSound : public db::ISound { public: typedef db::ISound base_class; typedef base_class::string_type string_type; friend class CFmodSoundManager; public: CMemSound( FMOD::System* pSystem, const string_type& strFile ); virtual ~CMemSound(); public:
26、 bool load(); void unLoad(); void play(); void changePauseState(); void release(); const string_type& getName() const; protected: bool create( void* pBuf, unsigned uBytesCount ); protected:
27、 string_type m_strName; FMOD::System* m_pSystem; FMOD::Sound* m_pSound; FMOD::Channel* m_pChannel; }; } 现在再看管理单件的实现,其中需要提的就这两个函数: bool CFmodSoundManager::loadtomemory( void* pBuf, size_t uByteCounts, size_t uoffset ) { FILE* pData = NULL; if( 0 != fo
28、pen_s( &pData, m_strDataFile.c_str(), "rb") ) { return false; } fseek( pData, uoffset, SEEK_SET ); fread_s( pBuf, uByteCounts, uByteCounts, 1, pData ); fclose(pData); return true; } ISound_SP CFmodSoundManager::createSound( const string_type& strFile ) { tr
29、y { CFilePack::node_type node; #ifdef __DB_USE_SAFE_CRT_FUNC__ strcpy_s( node.szName, strFile.c_str() ); #else strcpy( node.szName, strFile.c_str() ); #endif container_type::const_iterator pos = m_nodes.find( node ); if( pos == m_nodes.end() ) { // 没找到哈 throw
30、std::exception("没这个文件"); } CMemSound* pSound = new CMemSound( m_pSystem, pos->szName ); char* pBuf = new char[pos->uSize]; if( !this->loadtomemory( pBuf, pos->uSize, pos->uOffset ) ) { delete pSound; delete[] pBuf; throw std::exception( "加载失败!" ); }
31、 pSound->create( pBuf, pos->uSize ); delete[] pBuf; return ISound_SP( pSound, boost::mem_fn( &ISound::release ) ); // 这样就算是pSound由DLL创建也不会出错了 } catch( std::exception& /*e*/ ) { return ISound_SP(); } } 它从散列或者二叉树中查找我们需要的文件的信息,如果找到了则打开数据文件将那一块文件加载到内存中,然后创建内存Sound的智能
32、指针。然后我们就可以使用这个对象进行声音的播放了。下面是主函数的实现:
// Test
#define _CRTDBG_MAP_ALLOC
#include
33、因此内存泄露的提示是错误的。 } int main() { _CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF ); atexit( printDebug ); if( !CFmodSoundManager::GetSingletonPtr()->initialize( "music.data", "music.inx" ) ) { cout<< "系统初始化失败!"<< endl; return -1; } CFmodSoundManager* pManager =
34、CFmodSoundManager::GetSingletonPtr(); ISound_SP spSound1 = pManager->createSound( "jaguar.wav" ); if( !spSound1 ) { cout<<"jaguar.wav加载失败!"<< endl; return -1; } ISound_SP spSound2 = pManager->createSound( "swish.wav" ); if( !spSound2 ) { cout<<"swish.wav加载失败!"<< endl;
35、 return -1; } printf_s( "请按播放jaguar.wav\n请按播放swish.wav\n请按q退出\n"); spSound1->play(); spSound2->play(); bool bExit = false; do { if (_kbhit()) { int key = _getch(); switch( key ) { case '1': spSound1->play(); break; case '2': spSound2->p
36、lay(); break; case 'q': bExit = true; break; default: break; } } Sleep( 10 ); } while (!bExit); system("pause"); return 0; } 总结 由于时间关系这里只列出了部分代码,详细的工程我会放在附件中。另外就是Bug问题,也是因为时间关系,我没做大量详细的测试,但是我觉得重要的思想而不是具体的操作,只要明白了思想要改也是很容易的。另外这个东东也只是我自己的一些简单的思想
37、工程中是怎么样的其实我也不知道,呵呵,因为我还没有看过别人是怎么做的;时间仓促也没有做什么详细的构思和设计,敬请原谅,O(∩_∩)O哈哈~ 欢迎各位朋友对文中的任何点提出您的意见和问题。 我的QQ是84638372,Email:dbdongbo@。 2009/6/10于东北大学秦皇岛分校。 6/11补充 今天对对这个Demo做了一些修改,这些中最主要的修改是对流媒体的支持。在FMOD中普通的Sound需要拷贝到内存中,当我们调用FMOD::System::createSound之后,内存中的数据已经被FMOD系统拷贝到了它自己提供的缓冲区中,所以当创建完成之后我们就必须释放我们
38、所提供的缓冲区,详细可以参加CFmodSoundManager的createSound方法。但是对于mp3这样的比较大一些的文件来说则不应该使用这种方式了,因为流媒体的优势就在于不需要一次性的全部拷贝到内存中,需要的时候再读取。
在以前我也不知道应该怎么实现这种方式,现在知道了这样一个东西:内存映射文件。有了它的帮助我们就可以很容易的实现流媒体打包了。下面我给出的解决方案是对于ogg的,实际上mp3也可以的,这并没有什么大的不同。
现在我们需要从ISound接口派生出CMusic类,这个类用于处理流媒体。
#include "ISound.hpp"
#include 39、h>
#include 40、ng_type;
friend class CFmodSoundManager;
public:
CMusic( FMOD::System* pSystem, HANDLE hFile, HANDLE hMap, const string_type& strFile );
virtual ~CMusic();
public:
bool load();
void unLoad();
void play();
void changeP 41、auseState();
void release();
const string_type& getName() const;
protected:
bool create( unsigned uOffset, unsigned uSize );
protected:
string_type m_strName;
FMOD::System* m_pSystem;
FMOD::Sound* m_pSound;
FMOD::Channel* 42、 m_pChannel;
HANDLE m_hFileMap;
LPVOID m_pVoid;
};
}
其中下面的Handle便是用于文件映射的,另外CFmodSoundManager也需要做一些小的修改,主要是提供流媒体支持:
namespace db
{
class CFmodSoundManager : public ISoundManager, public db::Singleton 43、anager::string_type string_type;
typedef db::CFilePack::node_type node_type;
#ifdef __DB_WORK_WITH_BOOST__
typedef __SOUND_MANAGER_CONTAINER_IMP__ 44、riend class db::Singleton 45、 strFile );
ISound_SP createStream( const string_type& strFile );
void release();
protected:
bool initFmodSystem();
bool initData();
bool initMap();
bool loadtomemory( void* pBuf, size_t uByteCounts, size_t 46、uoffset );
protected:
string_type m_strDataFile;
string_type m_strIndexFile;
container_type m_nodes;
FMOD::System* m_pSystem;
HANDLE m_hFile;
HANDLE m_hFileMap;
};
}
现在还需要初始化文件映射的函数:
bool CFmodSoundManager::initMap()
47、 {
m_hFile = ::CreateFile(
string_shim 48、false;
}
m_hFileMap = ::CreateFileMapping( m_hFile, NULL, PAGE_READWRITE, 0, (DWORD)boost::filesystem::file_size(m_strDataFile), NULL );
if( NULL == m_hFileMap )
{
return false;
}
return true;
}
然后将其放入初始化函数中:
bool CFmodSoundManager::initialize( const string_type& strDa 49、taFile, const string_type& strIndexFile )
{
m_strDataFile = strDataFile;
m_strIndexFile = strIndexFile;
if( !this->initFmodSystem() )
{
return false;
}
if( !this->initData() )
{
return false;
}
if( !this->initMap() )
{
return false;
}
// todo :
return true;
}
然后创建流媒体对象,这里是CMusic对象:
ISound_SP CFmodSoundManager::createStream( const string_type& strFile )
{
try
{
CFilePack::node_type node;
#ifdef __DB_USE_SAFE_CRT_FUNC__
strcpy_s( node.s






