Category Archives: Game Programming

iOS开发与OpenGL ES相关问题整理(1)

在极其有限的工作日的晚上和周末进行着iOS上game programming的研究,进展非常缓慢,不过还是有必要将过程中的一些问题随时记录下来。 OpenGL ES崩溃在函数’glMatrixMode’ 这很有可能是因为在OpenGL ES2的context中使用OpenGL ES1的函数,ES1是固定函数渲染管线 (fixed function pipeline),而ES2是可编程的渲染管线 (programmable pipeline),ES2不再支持这些ES1的固定渲染管线的函数, 比如’glMatrixMode’。所以当ES2遇到这些不支持的ES1函数时,你的程序会收到一个’EXC_BAD_ACCESS’消息并且崩溃。通过gdb查看callstack,显示最后一个函数是gliUnimplemented: 如果一定要使用ES1函数, 你只能以ES1来初始化你的GL context: 函数’pathForResource’返回nil 需要将资源文件添加到项目的’Groups & Files’中 OpenGL ES崩溃在绘制函数’glDrawArrays’ 这可能是因为你启动了GL的某个渲染状态(比如vertex,color,texture coordinate等等,这里有所有可能状态的清单),但是却没有在绘制函数(比如’glDrawArray’)进行之前,设置该状态所对应的数据指针。 比如说: 对于上面这两句代码,如果你设置了使用color数组,但是却没有设置color数组指针,在真正的绘制时你的程序就会因为找不到对应的指针而崩溃。同样的, 如果你调用了‘glEnableClientState(GL_TEXTURE_COORD_ARRAY), 则你也需要通过glTexCoordPointer()设置纹理坐标的数组指针,如此类推。 怎么样通过OpenGL ES以像素为单位来绘制,而不是屏幕比例? 在iPhone上通过OpenGL ES进行2D绘制时,默认的原点位置在屏幕中央,并且屏幕的坐标范围依次是从-1到1,不管是横轴还是纵轴。如下图所示,GL中的默认的单位1分别代表着屏幕长度和宽度的一半。 图片来自book: ‘Learning iOS Game Programming_A Hands-On Guide to Building Your First iPhone Game’ 如果希望将原点移动到屏幕左下角,并且绘制时希望以像素为单位,只需要通过glOrthof函数将投影矩阵设置成一个垂直投影矩阵即可,代码如下:

Game Framework的两种实现方式

一年多前,曾经写过一篇关于Game Engine Framework的文章,当时基本上是为了巩固并加深对framework的理解。最近又做了一些关于framework的工作,对于framework的实现方式又有了些新的认识。虽然我现在做的已经完全不是game了,不过方式对于game也同样适用。 这篇文章主要希望通过一些示例性的C++代码介绍game framework的两种实现方式。首先,我还是搬出一年多前的那篇文章里的game流程图,以下的一些代码也主要基于这张图实现。对于图的细节在这里不再赘述,可以再去翻看之前的那篇文章。 1.通过继承 这是一种最传统的方式了,之前我一直使用这种方式。基本上是提供一个基类,基类封装并决定了整个程序控制流,同时基于该控制流,基类提供了一系列的接口(在C++里是虚函数),以供继承类override,并实现定制化的行为。就像下面的这个Game类,run函数决定了整个程序的执行逻辑,同时initialize,update,render等非纯虚函数则提供了接口以供用户定制并实现具体Game想要的行为。 在上面的代码中,我把实现全部写在了类的定义头文件中,在这里只是为了省事,在现实中你最好还是分开在.h和.cpp文件中。而且在这里,Game类还可以做的更多,我这里只是为了说明实现方式,所以依照前面的流程图而尽量让它简单。另外,不要纠结这段代码中的Event,Mouse,Keyboard等几个类和popEvent方法,它们只是我为了将故事说得更圆满一点而假象出来的几个类,我想你应该能猜到它们是用来干嘛的。 基于此framework,一个具体的游戏只需要重写这些虚函数就可以了,像下面的这些代码。 2.通过Delegation模式 前一种方法有一个缺点,就是将程序的控制流和具体行为紧耦合在了一起,而且还必须使用继承,不易于扩展。现代软件设计的一些方法告诉我们,要尽量使用接口,且尽量使用组合而非继承。Delegation模式就可以帮我们达到这一目的。 何为Delegation模式,wiki上的解释一语中的: Delegation is the simple yet powerful concept of handing a task over to another part of the program. Delegation将一些task委托给程序的另外一部分来处理,以达到了行为使用者和具体行为的松耦合。 以下是通过Delegation模式重新实现的framework。 基于此framework,当需要具体实现一个游戏的时候,只需要实现GameDelegation接口即可,然后将Game类的GameDelegation设置为你所实现的具体的ConcreteGameDelegation类,代码如下。 最近我正好做了一些iOS上开发的研究,发现Delegation在iOS框架中被普遍使用。比如,iOS中的每个应用程序对应的是一个UIApplication类,为每一个UIApplication,开发人员必须要实现一个特定的UIApplicationDelegate,并将它指定给当前的应用程序(在main函数中通过UIApplicationMain函数指定,或者是在nib文件中绑定)。在这个UIApplicationDelegate类中,开发人员就需要重写诸如didFinishLaunchingWithOptions,applicationWillTerminate这样的方法,就类似与上面game framework中的initialize,destroy等方法。当然iOS框架要复杂很多,它还用到其它一系列的设计模式,有空研究些这样设计是非常有趣的。 图片来自于这篇苹果官方关于iOS中delegation的介绍:http://developer.apple.com/library/ios/#documentation/General/Conceptual/DevPedia-CocoaCore/Delegation.html

Runtime Game Engine Architecture

上上周末闲得无聊的时候画的:游戏引擎的大概架构图,图片源于Jason Gregory的《Game Engine Architecture》的第29页,这本书也基本上是围绕着这张图对每一个模块深入展开。这张图献给自己和其他游戏引擎架构研究的爱好者,应该打印一张贴到家里的剪贴板上。这段时间对我来说是一段困难的时期,不清楚未来是否还会在game的道路上走下去,但愿这不是一个终结。 runtime game engine architecture, from ‘Game Engine Architecture‘ P.29

关于游戏引擎多线程的一些整理和思考

在最近的项目中,由于时间紧任务重,我果断放弃了引擎原来有点畸形的多线程设计,退回到单线程。最后事实证明,这是一个明智的决定,不但效率没有降低,同时那些永无止境的多线程bug也一并被消除,系统稳定了很多,最终项目得以在deadline之前有一个安全的交付。 1.是否多线程 去掉多线程的主要原因是,原来的多线程设计本身就是一个未经过深思熟虑的设计,它几乎是在项目开发的中后期突然插入的。整个引擎的最初设计没有考虑多线程,现在突然引入了多线程,而又没有经过精心的设计,那结果就是各种多线程bug扑面而来,让开发人员焦头烂额。事实是,如果需要多线程,那最好应该从项目设计架构的第一天开始就仔细考虑。 直观上感觉,好像使用多线程就会带来性能的提升。其实不然,如果没有经过精心的设计,随便引入多线程,只会适得其反。不但性能没有得到提升,而且引入多线程后带来的庞大开发代价和潜在的多线程bug,会让你和团队崩溃到吐血,典型的吃力不讨好的事情。相比于单线程,引入多线程的设计需要带来2到3倍的开发和测试代价,在这么一个时间紧急的项目上,在项目的中后期突然引入多线程真是个大错特错的决策,应该果断拿掉。在这篇博客文章“一种3D引擎的多线程设计方案”里,作者就提到,“多线程的引入使引擎变得更加复杂,不良的设计带来的性能提升非常有限,甚至在单核环境下还会出现明显的性能下降”。 从这篇Unreal引擎工程师Tim Sweeney的访谈文章中,我找到了在我的情况中应该拒绝多线程的另一个重要原因。 Tim提到: For multithreading optimizations, we’re focusing on physics, animation updates, the renderer’s scene traversal loop, sound updates, and content streaming. We are not attempting to multithread systems that are highly sequential and object-oriented, such as the gameplay. This is also why it’s especially important to focus multithreading efforts on [...]

Lua相关问题整理(4) – 让Lua的eval函数支持赋值语句

上一篇文章提到了在Lua中实现类似于JavaScript中的eval函数,遗憾是该eval函数不支持赋值语句,原因是Lua的赋值运算符是不支持返回值。所以如果要让该eval函数也支持赋值语句,就需要一个额外的工作,让它鉴别一个语句是不是赋值语句,如果是,则return的是被赋值后变量的值。为此,我写了一个isAssignmentExpression函数,比较粗糙,不过够用了,基本思想是检测语句中第一个出现的”=“操作符,且该”=“不能在一对引号当中。

Lua相关问题整理(3)

在注册给Lua的C函数中为Lua提供默认参数 使用luaL_optstring, luaL_optnumber, luaL_optinteger等Lua API,如下示例,函数有一个默认字符串参数,默认值为””,这样在Lua中调用whatever的时候,whatever()或者whatever( “whatever”)均可。(Oh…whatever…随便…都行…) 建立Lua字符串到C enum的映射 使用luaL_checkoption这个Lua API,它可以把从Lua传来的string转换为相应的C string array中的index,从而可以建立Lua字符串和C enum的映射。以下是个简单的示例: 首先enum PlayerType定义了一组角色类型,来自人人都爱的三国杀:-)。 接着PlayerTypeList定义了一个字符串数组,给Lua使用。注意需要保证enum和字符串数组的对应,比如PlayerTypeList[PLAYER_TYPE_KING]是“KING”,同时,PlayerTypeList必须以NULL结尾。 在定义给Lua的函数testPlayerType中,就可以用luaL_checkoption将Lua传来的字符串参数转换为相应enum的值。luaL_checkoption还支持默认参数,比如在上面例子中,将第三个参数设为“INSURGENT”,如果Lua中没有提供任何参数,则PlayerType就为与“INSURGENT”相对应的PLAYER_TYPE_INSURGENT。 以下是一组测试及结果: 在Lua中实现eval函数 众所周知,JavaScript中有一个著名的eval函数,它用于把一个字符串当作一段JS代码去执行,在Lua中没有提供类似的函数,但稍微包装下Lua的库函数loadstring即可实现,以下是代码。 这样已经可以了,不过相比于JS的eval函数,功能稍微差一些,因为它不支持赋值语句,这是Lua语言天然的原因,因为Lua的赋值运算符没有返回值,在其他语言中常见传递赋值的“i=j=1”(先赋值j=1,然后将(j=1)的返回值j赋值给i),在Lua中是不允许的。所以当eval执行的是赋值运算(比如i=1)的时候,return i=1就会出错。 下面是一些测试例子: 实现luaL_checkbool 不知道为什么Lua的API没有提供luaL_checkbook函数,不过很容易实现:

Lua相关问题整理(1)

(Under Linux)cannot find ‘dlsym’ ‘dlopen’ ‘dlerror’ ‘dlclose’ 需要同时链接”dl“库 (Under Linux)编译lua报错luaconf.h:275:31: error: readline/readline.h: No such file or directory 需要下载并安装GNU Readline Library PANIC: unprotected error in call to Lua API (unable to get ModuleFileName) 1: 不推荐的解决方式:将Project Properties->Configuration Properties->General下的Character Set从unicode改成multi-set; 2: 彻底的解决方式,参考此链接:http://lua-users.org/lists/lua-l/2006-06/msg00427.html 如何将Lua文本文件转化为Lua块文件(chunk file) 调用LuaAPI – lua_dump 关于lua_dump: about lua_dump: Dumps a function as a binary chunk. Receives [...]

开始看《Game Engine Architecture》

国庆前就在淘宝上痛下血本拍下了《Game Engine Architecture》,本来想在国庆长假内好好消化这本书,但快递公司居然也休了7天的长假,自从10月1日临晨后,物流信息就停止了更新。结果就是,回老家过了一个漫长而慵懒的十一假期。 从去年就开始关注这本书,据说好评如潮。我最近一年多一直干的事情就是开发game engine,但我对目前开发的很多架构不太满意,也有很多疑惑,虽然觉得有些地方很不完善,却又没法找到好的方法解决,所以希望能得到一些比较系统比较权威的帮助以作参考。于是买下了这本书,据说在国外是作为本科生的教材,所以讲得比较基础,不过比较全面、系统。 书很厚,800多页,每晚回去看一些,争取两个月内能看完。 这本书在google book上有预览,不过不是很清楚。

让管理lua_State的类指针安全

最近因为在公司的项目中接手了Lua脚本模块的开发,所以研究了很多关于Lua脚本的东西,秉着“多看多想多记”的原则,我时刻敦促自己要及时记录下遇到的一些问题和想法。 在Lua中,几乎所有的东西都是围绕着lua_State转,所以,一般我们都会写一个类来封装管理它,比如: 这很好,不过它不是指针安全的。试想,如果一个LuaObject对象被复制,结果将会怎样。 上面这段代码将会导致运行时crash,因为luaObject1和luaObject事实上指向了同一块lua_State,这样当luaObject1和luaObject2被析构时,lua_State会被两次lua_close,这不crash才怪呢! 事实是,当一个类包含了一个指针时,我们就需要开始变得格外谨慎,除了在构造函数和析构函数中要处理指针的初始化和清理外,我们还需要考虑深拷贝(deep copy),浅拷贝(shallow copy))的问题。如果使用编译器默认生成的拷贝构造函数,它只会浅拷贝指针,而指针所指向的内存区域不会被拷贝。就像上面一样,两个LuaObject实则共享了一个lua_State。 那该如何处理让管理LuaObject类的指针安全呢,深拷贝?厄,首先我也没有深究深度拷贝lua_State具体该如何完成,不过我猜想这可是一个复杂而重型的操作,仅仅为了带来指针安全而选择此可不是一个明智的选择。 如果你是开发LuaObject类的程序员,也许你会对使用LuaObject的程序员(或许有时使用者就是你自己)说:“你不要对它进行拷贝操作不就OK了”,但这是一种严重不负责任的行为,因为这种皮球会越踢越远的。比如,程序员A使用了你的LuaObject类,他写了一个包含LuaObject的指针的类,同样A也不考虑拷贝指针安全问题,然后A又将它的类传递给了B,B封装了A的类指针,然后传递给了C,接着如此重复,C再给D,D再给E,最后E在对类进行拷贝操作时,程序crash掉了,因为在某个最底层,他们共享了lua_State。不过因为这时候已经嵌套了这么多层,E程序员或许根本不知道也不关心什么是LuaObject(他只跟D的类打交道)。这时,要想追踪到BUG之源——万恶的不考虑指针安全的LuaObject,已经非常困难了,God bless you all. 事实是,如果你不想让你的类被复制,你就应该明确而显示的禁止它。其实lua_State的这个问题非常类似于Effective C++书中的第14个条款中所提到的问题:在资源管理类中小心copying行为。对于这个问题,书中提供了两种解决方案。第一种就是禁止复制。第二种则是对底层资源的“引用计数法”(reference count)。 第一种禁止复制很简单。(关于Uncopyable) 第二种则是使用智能指针来封装lua_State,我们可以使用boost的shared_ptr来封装lua_State,即shared_ptr< lua_State > m_L。如何对m_L进行初始化是另一个需要注意的问题,如果使用shared_ptr的常见初始化形式:m_L = shared_ptr< lua_State >( luaL_newstate() ),这样是不对的,因为在这种形式下,当lua_State的计数变为0时,shared_ptr会去调用lua_State的的析构函数,这显然是错误的,对lua_State的释放动作是lua_close而不是删除。事实上,这样编译器也无法通过,如果这么写,会报出“use of undefined type ‘lua_State’”的错误,提示lua_State是一个非完成的类型(incomplete type)。 我们应该为shared_ptr的初始化传入一个删除器(deleter)。很显然,lua_State的deleter是lua_close()函数,这样最终的代码如下。 这样再回到前面拷贝的那个例子,luaObject1和luaObject2共同引用了一个lua_State,但因为使用了shared_ptr,所以只有在lua_State的引用次数变为0时,它的deleter(这里是lua_close)才会被调用,安全了! 在我的项目中,因为没有使用boost库,也没有提供任何智能指针,所以使用禁止复制来保证安全。