游戏邦在:
杂志专栏:
gamerboom.com订阅到鲜果订阅到抓虾google reader订阅到有道订阅到QQ邮箱订阅到帮看

iPhone游戏引擎编写之脚本处理和流动传输(2)

发布时间:2012-02-27 11:32:47 Tags:,,

作者:Simon Yeung

脚本处理

在引擎中添加脚本支持能够带来诸多好处,如无需耗费大量时间重新编译引擎源代码就可以进行游戏玩法代码的编写,还能够在游戏代码和引擎代码间划定清晰的界线。我选择运用Lua语言(游戏邦注:版本编号是5.1.4),因为它易于整合且所占空间较小。我并不擅于使用Lua语言,所以我想在下文中阐述如何将Lua和C/C++捆绑起来。(点击此处阅读文章第一第三、第四部分

从Lua处调用C函数

首先,你需要通过lua_open()创建lua_State*(游戏邦注:如果你需要将内存分配器与Lua衔接起来的话,可以使用lua_newState()),所有Lua运算都在其中完成。

lua_State* luaState= lua_open();

Lua和C可以通过虚拟栈交换数据。Lua和C都可以向虚拟栈推送或获取数据。比如,我们已经注册了一个供Lua使用且带有函数标记的C函数:

function drawText(str, screenPosX, screenPosY);

随后在Lua端,当执行下列Lua脚本时:

textWidth= drawText(“Ready Go~”,  240, 160);

3个值将被推送至Lua Stack:

lua Stack 1 from altdevblogaday.com

lua Stack 1 from altdevblogaday.com

通过进行从存储栈底部开始(从1开始)的绝对指数运用或增至存储栈顶部(从-1开始)的相对指数运算,我们便可以得到C中的存储栈数值。

随后,我们在C函数内检索存储栈中的数值,Lua使用下列代码调用该C函数:

int drawText(lua_State* luaState)
{
float screenPosY = (float) lua_tonumber(luaState, -1);  // get the value 160
float screenPosX = (float) lua_tonumber(luaState, -2);   // get the value 240
const char* str = lua_tostring(luaState, -3);                     // get the value “Ready Go~”
printf(“Text ‘%s’ draw at (%f, %f)\n”, str, screenPosX, screenPosY);
int textWidth= strlen(str);
lua_pushnumber(luaState, textWidth);                            // return a value to Lua
return 1;                                                                           // number of values return to Lua
}

当C函数停止时,存储栈将如下所示:

lua Stack 2 from altdevblogaday.com

lua Stack 2 from altdevblogaday.com

Lua获得来自C函数的返回数值后,C函数的参数和返回数值将跳出存储栈。

但是,在Lua执行drawText()前,记得运用下列代码将其注册至lua_State*:

lua_register(luaState, “drawText”, drawText);

从C处调用Lua函数

我们还可以从C处调用Lua函数。比如,我们已经在Lua脚本中编写出函数,目的是初始化引擎配置:

function initEngineConfig(date)
print(‘initialize engine on ‘ .. date);
end

由于Lua属于无类型语言,因此它将函数也视为变量。所以我们需通过下列C代码将Lua变量“initEngineConfig”推送至存储栈中:

lua_getglobal(luaState, “initEngineConfig“);   // get the function to the stack
lua_pushstring(luaState, “18th Aug, 2011″);   // push the argument of the function
lua_call(luaState, 1, 0);                          // execute the function

如果发生错误,你还可以使用lua_pcall()来代替lua_call()以获得更多的排除故障信息。

上述C代码等同于在Lua中调用函数:

initEngineConfig(“18th Aug, 2011″);

我们还可以使用类似的技术来执行C中的对象方法。比如,我们可以在Lua中调用对象方法:

gameObjectA:update(timeSlice);

我们也能够在C中进行此操作。在Lua中,冒号句法只是编写声明的短结构:

gameObjectA.update(gameObjectA, timeSlice);

所以,我们需将“gameObjectA”的“更新”函数推送至带有2个自变量的Lua存储栈中:

lua_getglobal(luaState, “gameObjectA“); // for getting the ‘update’ function of ‘gameObjectA’
lua_getfield(luaState, -1, “update“);     // get the ‘update’ function of ‘gameObjectA’
lua_insert(luaState, -2);                           // swap the order of “gameObjectA” and “update”
// so that “gameObjectA” becomes an argument
lua_pushnumber(luaState, 1.0f/30.0f); // push the timeSlice argument on the stack.
lua_call(luaState, 2, 0);                      // execute the functions.

从根本上说,这就是Lua与C的互动方式,但你还需要知道如何以用户数据或轻用户数据的形式呈现C结构。你还需了解LUA_REGISTRY_INDEX,以在C中创建变量,无需担心变量名称的冲突问题。在把握这些内容后,你或许可以尝试通过捆绑库来生成捆绑。但是,我希望这些捆绑方法能够对那些想要自行捆绑Lua和C的人有所帮助。

流动传输

介绍

我的游戏是款开放世界游戏。玩家可以自由探索游戏世界。游戏不可能在开始时就加载所有游戏物体,所以我的引擎应当能够在玩家玩游戏时流动呈现游戏物体。

加载

要顺畅呈现游戏物体,我必须将整个游戏世界分割为多个正方形格子:

partition World from altdevblogaday.com

partition World from altdevblogaday.com

我将根据玩家的位置加载游戏世界的格子。我将每个空间方格像下图那样分为9个区域:

region from altdevblogaday.com

region from altdevblogaday.com

每个格子中都包含有3种类型的区域(游戏邦注:分别标注为A、B或C)。当玩家位于A区域时,游戏只会加载玩家所处的方格图层。当玩家位于B区域时,游戏会加载临近的1个方格图层:

load 2 tiles  from altdevblogaday.com

load 2 tiles from altdevblogaday.com

当玩家位于C区域时,游戏就会加载临近的3个方格图层:

load 4 tiles from altdevblogaday.com

load 4 tiles from altdevblogaday.com

所以,内存中的风格数量极限值为4。

卸载

要把握内存方格数量不超过最大值,需卸载看不见的方格图层。针对卸载,我将每个格子分为4个区域:

region Unload from altdevblogaday.com

region Unload from altdevblogaday.com

假设当玩家位于0区域时,下图中的X、Y和Z风格都将被卸载:

unload Tile from altdevblogaday.com

unload Tile from altdevblogaday.com

根据上述规则,我只要确保X、Y和Z方格在需要加载新方格图层前被卸载。如果出现意料之外的情况,我会暂停游戏直到它们被卸载。

内存和线程

现在我们已经知道,游戏最多只可能存在4个方格。除主线程deep物理和脚本所使用的共用内存分配器外,我还有4个线性分配器能够进行空间方格的输送,所有方格都在此限制范围内。内存分配样式与线程模型在游戏中的运作情况有关。游戏中有两个线程:主线程和流线程。主线程负责游戏逻辑、物理和播放的更新,流线程用来加载资源和减压质感等内容。当玩家更新主线程位置时,就标志着流线程要加载所需的方格图层。经过系列图像之后,流线程将以完成加载的信息模式反馈给主线程。两个线程间的传输将进行双倍缓冲以实现加锁时间最小化,同时确保线性分配器只会被用在流线程中,进而避免使用任何互斥量。但是,当图像对象和Lua对象等需在主线程中创建时,事情就会变得复杂很多。比如,流线程在完成纹理的解压缩操作后,应当通知主线程创建openGL纹理。

优势

在流动传输中使用线性分配器可以避免出现内存碎片。将游戏世界分割成方格图层能够将内存管理变得更简单,因为每个方格所使用的内存量大致相同。

劣势

要卸载空间方格,通过重置线性分配器就可以轻松释放在CPU端生成的资源。但是,事情并不像我最初想象的那么简单。比如,当我卸载方格中的实体对象时,在重置分配器前我需要先将它们完全从碰撞世界中移除。而且,对于图像对象,在重置分配器前我需要释放该方格图层中的所有openGL对象,否则,GPU端就会发生泄漏现象。而且我还需要释放方格中的脚本对象,这样那些脚本就可以集中于Lua。因此,这几乎同挨个“删除”方格中的所有游戏对象相同,我们无法简单地通过重置线性分配器来释放所有资源。除此之外,运用另一自定义分配器在子弹物理模式中创建对象(而非通过当前衔接的btAlignedAllocSetCustom()模式分配器)绝非易事。因为子弹并非出于将对象分配给另一个内存分配器而设计。我需要修改子弹源代码,方能使之生效。

结论

在制作了流系统后,我对应当精心计划的线程间通信及需要在特定线程上创建的对象有了更清晰的认识。而且,我觉得运用针对流对象的线性分配器来区分内存区域并非睿智选择,因为当这些对象需要被移出游戏世界时,它们只能挨个进行删除。修改子弹物理,使之能与这种内存模型相适应需要耗费大量的工作,这也会导致子弹物理库的维护和更新变得更为困难。(本文为游戏邦/gamerboom.com编译,拒绝任何不保留版权的转载,如需转载请联系:游戏邦

Writing an iPhone Game Engine

Simon Yeung

Adding script support to an engine has many benefits such as writing game play code without recompile the engine source code which may takes a long time. It also can draw a much clear boundary between the game code and the engine code. I chose to use Lua (ver. 5.1.4) as it is easy to embed and its size is small. As I am no expert on Lua, I would like to write about what I have learnt on how to bind Lua and C/C++.

Calling C function from Lua

First, you need to create a lua_State*, where all Lua operations are done within it, by lua_open() (or you may use lua_newState() if you need to hook up your memory allocator to Lua.)

Lua and C can exchange data through a virtual stack. Both Lua and C can push and pop data from or to the stack. Say, we have already register a C function for Lua to use with function signature:

then in Lua side, when the following Lua script is executed:

3 values will be pushed to the Lua Stack:

We can get the values from the stack in C using an absolute index counting from the bottom of the stack (start from 1) or a relative index to the top of the stack (start from -1).

Then we can retrieve the values in stack inside the C function called by Lua with the following code:

when the C function exit, the stack will look like:

After Lua get the return value from the C function, the parameter and the return value of the C function will be popped out of the stack.

But before Lua can execute the drawText(), remember to register it to the lua_State* by:

Calling Lua functions from C

We can also call Lua functions from C. For example, we have declare a function in Lua script for initializing the engine configurations:

As Lua is a typeless language, it also treats functions as variables. So we need to push the Lua variable ‘initEngineConfig’ into the stack with its arguments with the following C code:

(You may also use lua_pcall() instead of lua_call() to get more debug info when error occurs)

The above C codes is equivalent to calling a function in Lua:

We can also use similar technique to execute an object’s method in C. For instance, we can call an object’s method in Lua:

We can do this in C too. In Lua, the colon syntax is just a short form for writing the statement:

So, we need to push the ‘update’ function of ‘gameObjectA’ onto the Lua stack with 2 arguments:

This is basically how Lua interacts with C, but you also need to know how to represent C structure as user data or light user data. You may also need to know LUA_REGISTRY_INDEX for creating a variable in C without worrying conflicts in variable name. After knowing these things you may want to try some binding library to generate the binding. But I hope these little binding methods can help someone who want to bind Lua and C on their own.

Introduction

My game is an open world game. The player can freely explore the game world. It is impossible to load all the game objects when the game start, so my engine should be able to stream in the game objects when the player is playing.

Loading

In order to stream the game objects, I have to partition the whole game world into many square tiles:

I will load the world tile(s) according to the player position. I divide each world tile into 9 regions as below:

There are 3 types of region in each tile (marked as A, B & C). When the player is in region A, only the tile that the player inside is loaded. When the player is inside region B, 1 of the adjacent tile will be loaded:

And within region C, 3 of the adjacent tiles will be loaded:

So, maximum number of tiles in memory will be 4.

Unloading

To maintain the maximum number of tiles in memory, unseen tiles need to be unloaded. I divide each tile into 4 region for unloading:

Say, when the player is in region 0, the nearby tiles X, Y, Z in the below figure will be unloaded:

The only thing I need to ensure is tiles X, Y, Z are completely unloaded before new tile need to be loaded according to the above rules. If this situation happens, I will block the game until they are unloaded.

Memory and Threading

Since we already know that there are only 4 maximum tiles. Beside the pool memory allocator used for physics/script in the main thread. I also have 4 linear allocator for streaming world tile and all tiles are within this limit. The memory allocation pattern is related to how the threading model works in the game. In the game there are 2 threads: main thread and streaming thread. The main thread is responsible for updating the game logic, physics and rendering, while the streaming thread is for loading resources, decompress textures, etc. When the player update the position of the ship in the main thread, it will signal the streaming thread to load the tile if needed. Several frames later, the streaming thread signal back the main thread finished loading. The communication between 2 threads are double buffered to achieve minimum locking and also ensure the linear allocator will only be used in the streaming thread which can avoid using any mutex. But things go complicated when some of the objects should be created in main thread such as graphics objects and Lua objects. For example, the streaming thread should notify back to the main thread to create an openGL texture handle after it finished decompressing a texture.

Advantages

Using linear allocator for streaming can avoid memory fragmentation. Partition the game world into tiles make managing memory easier as each tile should have roughly the same amount of memory used.

Disadvantages

To unload a world tile, those resources created on CPU side are simply freed by reseting the linear allocator. However, things are not that easy as I think originally… For example, when I want to unload the physics objects in a tile, I need to remove all of them from the collision world first before resetting the allocator. Also, for the graphics objects, I need to release all of the openGL objects in that tile before resetting the allocator, otherwise, leak will occur in the GPU side. I also need to release the script object in the tile so that those scripts can be garbage collected in Lua. Therefore, it is nearly the same as ‘delete’ all game objects in a tile one by one and cannot free all resources simply by resetting the linear allocator. Besides, creating objects in bullet physics using another custom allocator other than the currently hooked up allocator using btAlignedAllocSetCustom() is not easy too. Since bullet is not designed for allocating objects to another memory allocator (i.e. in my case, the linear allocator for streamed objects such as the rigid bodies and collision shapes). I need to modify bullet source code to make it works.

Conclusion

After making the streaming system, I have a more clear understanding between inter-thread communication as it should be planned carefully, and some of the objects needed to be created on specific thread. Also, I think that it is not a wise choice to divide a dedicated memory region using linear allocator for streaming objects as those objects will be deleted one by one when they need to be removed from the game world. And it costs too much work to modify bullet physics to cope with this memory model and this result in hard to maintain and update bullet physics library. (Source: AltDevBlogADay part 4part 5)

 


上一篇:

下一篇: