游戏中的声音资源打包
董波
介绍
在以前的游戏当中,几乎所有的游戏都有将资源打包,无论是图形资源、声音文件等等。现在,越来越多的游戏的声音资源都公开了,通常背景音乐等采用ogg,音效采用wav;但是图形资源绝大部分游戏还是都是打包了的。有的有加密,有的没有。
在学习游戏编程的初期我就对这个东西很感兴趣,但是又没有人教,到网上找资料也少得可怜,所以就只好自己摸索了。记得去年也做过类似的工作,但是当时做的感觉很不好,采取了一些投机取巧的方法,虽然看起来是打包了,但是实际上只是一个躯壳而已。
随着时间的推移,自己懂得比以前稍微多了一点点,就想做点什么,即使现在还是“菜青虫”水平,“菜鸟”都算不上,呵呵。马上要毕业了,论文也早Over了,所以时间挺多的,因此写点这样的东西,希望现在的“曾经的我”能够从这个文章里边得到一点灵感。
这篇文章将介绍一种最简单的声音文件打包并播放的方法。最简单意味着它没有加密没有考虑太多的特殊情况,只实现了最简单的wav打包和播放,流媒体没考虑在内,因为它们的处理毕竟还是有很大的不同的。
注意
本文中所提到的程序以及附件都使用Visual Studio 2008 编写,项目文件也只能用VS2008打开。另外依赖于boost.filesystem、boost.shared_ptr、boost.unoredered_set,说白了如果您想查看效果的话必须安装boost,boost的安装教程可以参考我写得这个:http://ishare.iask.sina.com.cn/f/4875657.html 。另外声音播放我使用的Fmod引擎,所以您需要到http://www.fmod.org/ 去下载最新的fmod引擎sdk安装并配置好VS。我相信VS2005和VS2008在配置正确的情况下应该都能编译通过的 。
文件打包
文件打包有很多方式的,我采取了一种比较简单的方式,我将打包后的结果分为数据文件和索引文件两部分,这样的好处是什么呢?至少我们追加文件的时候很方便也很快。加入我们采用文件头的形式,即索引在数据文件的头部,那么当我们要添加的新文件的时候,则意味着所有文件的重写,因为操作系统不会提供一个执行“插入”操作的API。这样最直接的好处就是游戏工具执行“保存”动作的时候的快速性。
另外,文件打包还可以选择是否加密,是否压缩等等。加密这块我不是很了解,我去年做过一个加了密的,但是后来反思的时候发现并没有什么意义,与其说文件打包是为了保护资源的话,还不如说是为了管理的方便和效率。因为我觉得在一个电脑高手面前,加不加密并没有什么意义,比如说传奇式的”mycrack”大大。压缩个人以为还是有点意义的,至少可以少占不少硬盘,当然,这是需要付出代价的,运行时必须解压。Ogre3D中就支持zip打包,应该也是使用zlib来做的,使用zlib来做这个事情是很容易的,呵呵。它的官方网站是:http://www.zlib.net/
因此我们可以有这样的一个类来做这个简单的操作:
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(
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
{
return m_spImp->getNodes();
}
protected:
boost::shared_ptr<detail::CFilePackImp> m_spImp;
};
}
最终对CFilePack的调用将被转发到detail::CFilePackImp中。提供多种字符串的重载,最终将使用string_shim转换到char调用detail::CfilePackImp的对应的函数:
namespace db
{
namespace detail
{
class Node
{
public:
enum
{
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 _stricmp( 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::vector<Node> node_vec;
public:
public:
CFilePackImp(
const string_type& strDataFileName,
const string_type& strIndexFileName );
~CFilePackImp();
public:
bool addFile( const string_type& strFileName );
// bool deleteFile( const string_type& strFileName );
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;
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.addFile( "jaguar.wav" );
pack.addFile( L"swish.wav" );
// pack.load();
return 0;
}
实现细节就不说了,大家可以参考附件。
声音加载与播放
现在我们要做的就是从索引文件中读取出文件信息,然后去数据文件中找我们感兴趣的数据。在上面的代码中我们可以看到,每一个索引节点都由固定长度的文件名字符串、在数据文件中的偏移量和数据的长度组成,现在在管理中就应该先获取这些信息,由于索引文件通常都很小,所以我们在程序运行期间将这些信息保存在内存中,根据是否有boost分别选择散列表unordered_set或者红黑树实现的set来保存。
由于在整个系统中通常都只有一个这样的声音文件包,因此我们实现一个简单的单件管理类,这是它的接口类:
#pragma once
#include <string>
#include "ISound.hpp"
namespace db
{
class ISoundManager
{
public:
typedef std::string string_type;
public:
virtual bool initialize(
const 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"
#include "Singleton.hpp"
#include "..\\pack_base\filepack.hpp"
#include <fmod.hpp>
#ifdef __DB_WORK_WITH_BOOST__
#include <boost/unordered/unordered_set.hpp>
#define __SOUND_MANAGER_CONTAINER_IMP__ boost::unordered_set
struct ihash : public std::unary_function< const db::detail::Node&, size_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<uLen; ++i )
{
boost::hash_combine( uSeed, toupper( data.szName[i]) );
}
return uSeed;
}
};
#else
#include <set>
#define __SOUND_MANAGER_CONTAINER_IMP__ std::set
#endif // #ifdef __DB_WORK_WITH_BOOST__
namespace db
{
class CFmodSoundManager : public ISoundManager, public db::Singleton<CFmodSoundManager>
{
public:
typedef ISoundManager::string_type string_type;
typedef db::CFilePack::node_type node_type;
#ifdef __DB_WORK_WITH_BOOST__
typedef __SOUND_MANAGER_CONTAINER_IMP__<node_type, ihash> container_type;
#else
typedef __SOUND_MANAGER_CONTAINER_IMP__<node_type> container_type;
#endif
friend class db::Singleton<CFmodSoundManager>;
protected:
CFmodSoundManager();
virtual ~CFmodSoundManager();
public:
bool initialize(
const string_type& strDataFile,
const string_type& strIndexFile
);
ISound_SP createSound( const string_type& strFile );
void release();
protected:
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;
};
}
这是一个单件。接下来需要实现一个Sound的接口,因为Sound有普通的wav这样的声音文件,也有像mp3这样的流媒体。因此需要区别对待的,比如说wav的处理思路通常都是直接加载到内存中而mp3等是不会的。我们这里只实现wav的。接口类:
#pragma once
#include <string>
#include <boost/smart_ptr/shared_ptr.hpp>
namespace db
{
class ISound
{
public:
typedef std::string string_type;
public:
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;
};
typedef boost::shared_ptr<ISound> ISound_SP;
}
然后实现我们的内存wav,这和普通的声音有所不同,因为我们需要直接从内存中播放而不是通过一个wav格式的文件的文件名或者路径来构造,因此其实现如下:
#include "ISound.hpp"
#include <fmod.hpp>
#if defined(_MSC_VER )
#pragma comment( lib, "fmodex_vc.lib" )
#endif
namespace db
{
class CFmodSoundManager;
class 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:
bool load();
void unLoad();
void play();
void changePauseState();
void release();
const string_type& getName() const;
protected:
bool create( void* pBuf, unsigned uBytesCount );
protected:
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 != fopen_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 )
{
try
{
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 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( "加载失败!" );
}
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的智能指针。然后我们就可以使用这个对象进行声音的播放了。下面是主函数的实现:
// Test
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#include "FmodManager.hpp"
#include <iostream>
#include <conio.h>
#include <Windows.h>
using namespace std;
using namespace db;
void printDebug()
{
_CrtDumpMemoryLeaks(); // 它比Manager的析构先调用,因此内存泄露的提示是错误的。
}
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 = 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;
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->play();
break;
case 'q':
bExit = true;
break;
default:
break;
}
}
Sleep( 10 );
} while (!bExit);
system("pause");
return 0;
}
总结
由于时间关系这里只列出了部分代码,详细的工程我会放在附件中。另外就是Bug问题,也是因为时间关系,我没做大量详细的测试,但是我觉得重要的思想而不是具体的操作,只要明白了思想要改也是很容易的。另外这个东东也只是我自己的一些简单的思想,工程中是怎么样的其实我也不知道,呵呵,因为我还没有看过别人是怎么做的;时间仓促也没有做什么详细的构思和设计,敬请原谅,O(∩_∩)O哈哈~
欢迎各位朋友对文中的任何点提出您的意见和问题。
我的QQ是84638372,Email:dbdongbo@vip.qq.com。
2009/6/10于东北大学秦皇岛分校。
6/11补充
今天对对这个Demo做了一些修改,这些中最主要的修改是对流媒体的支持。在FMOD中普通的Sound需要拷贝到内存中,当我们调用FMOD::System::createSound之后,内存中的数据已经被FMOD系统拷贝到了它自己提供的缓冲区中,所以当创建完成之后我们就必须释放我们所提供的缓冲区,详细可以参加CFmodSoundManager的createSound方法。但是对于mp3这样的比较大一些的文件来说则不应该使用这种方式了,因为流媒体的优势就在于不需要一次性的全部拷贝到内存中,需要的时候再读取。
在以前我也不知道应该怎么实现这种方式,现在知道了这样一个东西:内存映射文件。有了它的帮助我们就可以很容易的实现流媒体打包了。下面我给出的解决方案是对于ogg的,实际上mp3也可以的,这并没有什么大的不同。
现在我们需要从ISound接口派生出CMusic类,这个类用于处理流媒体。
#include "ISound.hpp"
#include <Windows.h>
#include <fmod.hpp>
#if defined(_MSC_VER )
#pragma comment( lib, "fmodex_vc.lib" )
#endif
namespace db
{
class CFmodSoundManager;
class CMusic : public db::ISound
{
public:
public:
typedef db::ISound base_class;
typedef base_class::string_type string_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 changePauseState();
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* m_pChannel;
HANDLE m_hFileMap;
LPVOID m_pVoid;
};
}
其中下面的Handle便是用于文件映射的,另外CFmodSoundManager也需要做一些小的修改,主要是提供流媒体支持:
namespace db
{
class CFmodSoundManager : public ISoundManager, public db::Singleton<CFmodSoundManager>
{
public:
typedef ISoundManager::string_type string_type;
typedef db::CFilePack::node_type node_type;
#ifdef __DB_WORK_WITH_BOOST__
typedef __SOUND_MANAGER_CONTAINER_IMP__<node_type, ihash> container_type;
#else
typedef __SOUND_MANAGER_CONTAINER_IMP__<node_type> container_type;
#endif
friend class db::Singleton<CFmodSoundManager>;
protected:
CFmodSoundManager();
virtual ~CFmodSoundManager();
public:
bool initialize(
const string_type& strDataFile,
const string_type& strIndexFile
);
ISound_SP createSound( const string_type& 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 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()
{
m_hFile = ::CreateFile(
string_shim<TCHAR>(m_strDataFile).toStr(),
GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if( INVALID_HANDLE_VALUE == m_hFile )
{
return 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& strDataFile, 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.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 std::exception("没这个文件");
}
CMusic* pMusic = new CMusic( m_pSystem, m_hFile, m_hFileMap, pos->szName );
if( NULL == pMusic )
{
throw std::exception("内存分配失败!");
}
if( !pMusic->create( pos->uOffset, pos->uSize ) )
{
delete pMusic;
throw std::exception("创建失败咯");
}
return ISound_SP( pMusic, boost::mem_fn( &ISound::release ) ); // 这样就算是pSound由DLL创建也不会出错了
}
catch( std::exception& /*e*/ )
{
return ISound_SP();
}
}
转到CMusic的create函数:
bool CMusic::create( unsigned uOffset, unsigned uSize )
{
m_pVoid = ::MapViewOfFile( m_hFileMap, PAGE_READWRITE, 0, 0/*uOffset - uPass*/ /*uOffset*/, 0 );
if( !m_pVoid )
{
return false;
}
FMOD_CREATESOUNDEXINFO exinfo;
memset( &exinfo, 0, sizeof(exinfo) );
exinfo.cbsize = sizeof(FMOD_CREATESOUNDEXINFO);
exinfo.length = uSize;
FMOD_RESULT result = m_pSystem->createStream(
(const char*)(m_pVoid) + uOffset,
FMOD_HARDWARE|FMOD_OPENMEMORY|FMOD_LOOP_NORMAL,
&exinfo,
&m_pSound
);
if( FMOD_OK != result )
{
return false;
}
return true;
}
这样就可以实现ogg的加载与播放了,main:
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 = CFmodSoundManager::GetSingletonPtr();
ISound_SP spMusic1 = pManager->createStream( "butterfly.ogg" );
if( !spMusic1 )
{
cout<<"butterfly.ogg加载失败!"<<endl;
return -1;
}
ISound_SP spMusic2 = pManager->createStream( "shaonianxing.ogg" );
if( !spMusic2 )
{
cout<<"shaonianxing.ogg加载失败!"<<endl;
return -1;
}
printf_s( "请按播放&暂停butterfly.ogg\n请按播放&暂停shaonianxing.ogg\n请按q退出\n");
spMusic1->play();
spMusic2->play();
spMusic2->changePauseState();
bool bExit = false;
do
{
if (_kbhit())
{
int key = _getch();
switch( key )
{
case '1':
spMusic1->changePauseState();
break;
case '2':
spMusic2->changePauseState();
break;
case 'q':
bExit = true;
break;
default:
break;
}
}
Sleep( 20 );
} while (!bExit);
system("pause");
return 0;
}
它的特点就是在程序中增加音乐的话也不会导致内存占用的明显上升。这样我们这个程序就可以支持流媒体和普通声音两种模式的处理了!
董波
2009/6/11
于 东北大学秦皇岛分校
我将更多的东西放在了我的QQ空间:http://84638372.qzone.qq.com/
和CPPBLOG: http://www.cppblog.com/db123/
Demo下载地址:http://ishare.iask.sina.com.cn/f/5263833.html