# 游戏开发精粹1 ## 通用编程技术 ### 数据驱动 逻辑部分定义游戏引擎的核心原则和算法,数据部分则提供其内容和行为的具体细节。 游戏数据应该从文件载入,而不应该内嵌在代码中。 1. 创建一个能按需解析文本文件的系统(能在不修改代码的情况下,进行各种数据尝试) 2. 不要硬编码常量,把常量放进文本中,这样进行改变时就不用重新编译代码 3. 假定任何东西都可能改变,通过配置文件的神奇作用可以对所有元素进行改变 4. 将控制流写成脚本 5. 避免重复数据,**绝对不要复制代码**,同理真实数据也是 6. 开发工具来生成数据:游戏编辑器、关卡编辑器、脚本编辑器等 > 你的游戏应该有能力应对规则、角色、种族、武器、关卡、控制方案以及对象的改变进行处理 > 没有这种灵活性,改变的代价很大,并且每一个改变都需要有程序员参与——这完全是资源浪费 ### 面向对象编程 1. 代码风格,比如**匈牙利标注法**,比如`私有整型变量`可以使用`m_iSomeVar`等 | 类型 | 描述 | | --- | --- | | I | Interger | | F | Float | | D | Double(float) | | L | Long(nteger) | | C | Character | | B | Boolean | | Dw | Double wond | | W | Wond | | by 或 byte | Byte | | Sz | C-style(null-terminated) string | | 常用扩展名 | 描述 | | --- | --- | | Str | C++字符串对象 | | H | 句柄(自定义类型) | | V | 向量(自定义类) | | Rgb | RGB三元组(自定义结构或类型) | | P | 指针 | | R | 索引 | | U | 非符号 | | a或ary | 数组 | | 范围 | 描述 | | --- | --- | | m_ | 成员变量 | | g_ | 全局变量 | | s_ | 静态变量 | > 常用的标注类型(对象通常不加任何前缀) > 不要再代码规范上做的太过,如果写代码要查规范严重影响效率 > 类名的设计也应该易于阅读和维护,用功能作为类名前缀也是很有用的 2. 类设计 ```cpp class Sample{ public: Sample() {Clear();} ~Sample() {Destroy();} void Clear(); // 清空所有内部成员变量 bool Create(); // 动态创建对象,返回true/false表示创建成功或失败 void Update(); void Destroy(); // 销毁对象 } ``` 动态分配内存的开销很大,所以只要可能都尽量避免 无论表示什么对象,`Create()`和`Destroy()`成员函数真正完成创建和销毁对象的工作 > `Create()`方法也可以返回错误代码类型,通常为符号整形 > `Destroy()`方法我们希望既有自动清理的便利,又有按需创建和删除的灵活性,所以`Destroy()`函数能安全地多次调用,或者没有调用`Create()`的情况下也能安全调用 3. 类层次结构设计 通过继承来复用类,是面向对象编程的一大要素 扩展类之间的合作只要是两种方法:继承和分层 分层:从一个类派生出另一个类 分层:一个对象作为成员包含于另一个对象,也称组合 4. 设计模式 - 单例模式:当大量的类或模块需要访问一个全局对象时,使用单例模式 - Facade界面模式(外观模式):尽量避免子系统内部类对外的暴露 - State(状态)模式:简单的使用对象来表示逻辑状态是一种更为优雅的面向对象解决方案 - 状态得到了更好的封装 - 状态可以在他们的基类中逻辑的共享代码,并且新的状态可以通过继承轻易地从现有状态中派生出来 - 减少了在离散的状态间进行剪切、粘贴代码所带来的典型问题 - 还可以用于AI系统,甚至可以用于在一个游戏中表示不同的游戏模式类型 - Factory(工程)模式:用于组织对象的创建,他的一种形式定义为一种方法,允许抽象接口类指定何时创建具体的派生实现类。把对象分配聚集到一个位置的好处是尤其值得注意的 - 动态内存分配很昂贵,所以需要对分配进行仔细监控,在一个中央区域分配所有的对象是的分配监控变得更为容易 - 通常全体对象的公共初始化或创建方法应该在一个类层次结构中。如果把对象的分配放在一个中央区域,就使得基于对象的操作变得更加容易 - 工厂具有可扩展性,它允许新的对象从现有工厂中派生出来。通过传递新的类ID,就可以在运行时扩展新的的类而不用改变现有的基代码 ### 使用模板元编程的快速数学方法 [浅谈元编程](https://zhuanlan.zhihu.com/p/87917516) 1. 斐波那契额 ```cpp #include using namespace std; template< unsigned N > struct Fib{ enum{ // 递归定义 不常见但合法 Val = Fib::Val + Fib::Val }; }; // 基于特殊情况的模板特化 template<> struct Fib< 0 > {enum{Val = 0};}; template<> struct Fib< 1 > {enum{Val = 1};}; #define FibT( n ) Fib< n >::Val int main() { std::cout << FibT(4) << std::endl; return 0; } ``` > 模板函数并不是真正的函数——他是叫做Val的枚举整形,在**编译期**递归生成 > 模板参数N用于指定函数的输入,以简化编辑。在默认情况下结构数据是公用的 > 要终止递归,需要正确地处理结束条件。对于斐波那契数来说,终止条件就是0和1 2. 阶乘 ```cpp template< unsigned N > struct Fact{ enum { Val = N * Fact< N - 1 >::Val }; } template<> struct Fact<1>{ enum {Val = 1}; }; #define FactT( n ) Fact< n >::Val ``` 3. 三角函数 通过泰勒公式可以直接用上面类似的方法展开 4. 模板与标准C++ 并非所有编译程序都能完全与C++标准兼容,尤其是在它们处理模板的时候更是如此 模板是如此灵活而且强大,以至于为了让他们能够正确运行,编译程序的开发者需要付出大量辛苦的劳动 ### 资源和内存管理 游戏通常有多种类型的数据库,每种数据库常常将各种不同的情况硬编码,以保证速度。 游戏数据库的例子有:文件系统、纹理管理器、字体管理器、游戏角色管理器等。在此之上有大量基于不同游戏风格和内容的专用数据库 建立在所有游戏中的一个资源数据库就是基本对象内存管理器。程序员调用new创建对象、不用时delete将资源返还给系统,但这种做法不适用共享资源 游戏中的不同系统:开发控制台和GUI中的文本控制都要使用字体对象,但不能让每个系统都创建自己的本地字体对象拷贝,因为这样做的运行速度很慢并且会消耗大量内存。这个时候使用一个单例的字体管理器`FontManager`对象,使用它获得字体指针、在系统空闲时加载、将其高速缓存直到不再需要位置、全局范围有效,负责系统中的所有字体对象。但对于何时创建、如何释放等问题单纯的单例模式并不能给出完美的解决方案 资源管理器的工作是按需创建资源,将它提供给需要使用的对象,并最终将其删除 - 如果使用指针 - 不安全 - 指针可能变成空指针,系统的某部分可能让资源管理器删除某资源,而这会让正在使用这资源的所有指针无效 - 底层的数据组织方式不能改变,任何对缓冲区的再分配都会使得所有相应的指针无效 > 因此用指针的方式提供资源,对安全、灵活的资源管理器来说并不是个好主意 可以不必编写高智能、过于复杂的智能指针,而是加一层抽象,使用`句柄`,把重任抓交给管理器类 > Win32文件系统中`CreateFile()`调用返回的HANDLE类型就是句柄 > 句柄很适合单CPU寄存器,因为它能以集合方式搞笑存储,并且能作为参数传递给函数,易于检查有效性,而且不是直接指向资源所以改变底层的数据组织方式而不会产生无效句柄,句柄还可以直接存储 ```cpp #include #include #include //...[平台相关的句柄类型] typedef LPDIRECTDRAWSURFACR7 OsHandle; struct tagTexture {}; typedef Handle HTexture; class TextureMgr { //材质对象数据 struct Texture { typedef std::vector HandleVec; std::string m_Name; //重构造时使用 unsigned int m_Width; //宽度 unsigned int m_Height; //高度 HandleVec m_Handles; //surface句柄 OsHandle GetOsHandle(unsigned int mip) const { assert(mip < m_Handles.size()); return { m_Handles[mip]}; } bool Load (const std::string& name); void Unload(void); }; typedef HandleMgr HTextureMgr; //数据库以名字为索引 //大小写不敏感的字符串比较谓词 struct istring_less { bool operator() (const std::string& l, const std::string& r) const { return ( ::stricmp(1.c_str(), r.c_str()) < 0 ); } }; typedef std::map NameIndex; typedef std::pair NameIndexInsertRc; //私有数据 HTextureMgr m_Texture; NameIndex m_NameIndex; public: //生命期 TextureMgr(void) { /*...*/ } ~TextureMgr(void); //材质管理 HTexture GetTexture(const char* name); void DeleteTexture(HTexture htex); //材质查询 const std::string& GetName(HTexture htex) const{ return m_Texture.Dereference(htex)->m_Name; } int GetWidth(HTexture htex) const{ return m_Texture.Dereference(htex)->m_Width; } int GetHeight(HTexture htex) const{ return m_Texture.Dereference(htex)->m_Height; } OsHandle GetTexture(HTexture htex, unsigned int mip = 0) const{ return m_Texture.Dereference(htex)->GetOsHandle(mip); } }; TextureMgr::~TextureMgr(void) { //在析构前释放索引剩余材质 NameIndex::iterator i, begin = m_NameIndex.begin(), end = m_NameIndex.end(); for(i = begin; i != end; ++i) { m_Textures.Dereference(i->second)->Unload(); } } HTexture TextureMgr::GetTexture(const char* name) { //插入和查找 NameIndexInsertRc rc = m_NameIndex.insert(std::make_pair(name, HTexture())); if(rc.second) { //这是一个新的插入操作 Texture* tex = m_Textures.Acquire(rc.first->second); if(!tex->Load(rc.first->first)) { DeleteTexture(rc.first->second); rc.first->second=HTexture(); } } return rc.first->second; } void TextureMgr::DeleteTexture(HTexture htex) { Texture* tex = m_Textures.Dereference(htex); if(tex != 0) { //使用索引删除 m_NameIndex.erase(m_NameIndex.find(tex->m_Name)); //从数据库删除 tex->Unload(); m_Texture.Release(htex); } } bool TextureMgr::Texture::Load(const std::string& name) { m_Name = name; //...[从文件系统载入材质,失败时返回false] return true; } void TextureMgr::Texture::UnLoad(void) { m_Name.erase(); //...[释放surfaces] m_Handles.clear(); } ``` ### 快速数据载入技巧 ### 基于帧的内存分配 ### 简单快速的位数组 ### 在线游戏的网络协议 ### 最大限度的利用Assert ### Stats:事实统计和游戏内调试 ### 实时的游戏内建刨析