本节书摘来异步社区《Lua游戏AI开发指南》一书中的第1章,第1.1节,作者: 【美】David Young(杨) 译者: 王磊 责编: 陈冀康,更多章节内容可以访问云栖社区“异步社区”公众号查看。
1.1 AI沙箱简介
AI沙箱是一个特别设计的软件框架,它摆脱了应用管理、资源处置、内存管理、Lua绑定这些无聊的工作,让你能够立即着手应用Lua进行AI编程。虽然这个沙箱承担了一个小型游戏引擎的工作,但是它的内部结构是完全开放的。本章会详尽描述和解析它的内部代码,以便你在必要时对其进行扩展来获得更多的功能。
我们在设计AI沙箱时使用了一组预先编译好的开放源代码库,用以支持Lua代码实现的AI的快速原型开发和调试。C++代码维护和管理AI数据,而Lua脚本则管理AI的决策逻辑。数据和逻辑的分离使得Lua逻辑可以进行快速迭代,而不用担心当前AI状态的崩溃或失效。
1.1.1 理解沙箱
在开始构建AI之前,本章将介绍沙箱的内部结构和设置。由于所有的AI脚本都在Lua端,我们很有必要理解Lua如何与沙箱交互,以及与Lua相对应的C++代码的功能。
1.1.2 项目文件组织
沙箱项目的文件组织在共享媒体资源的同时可以轻松支持每个独立的项目。一个叫demo_framework的项目提供了本书用到的所有通用代码。各章C++代码的区别在于设置的待运行的Lua沙箱脚本不同。虽然从本书一开始,整个沙箱框架就是可用的,但在每章中都会继续添加一些新的功能。
bin
x32/debug
x32/release
x64/debug
x64/release
build (generated folders)
project
Learning Game AI Programming.sln
decoda
lib(generated folders)
x32/debug
x32/release
x64/debug
x64/release
media
animations
fonts
materials
models
packs
particle
programs
shaders
textures
premake
premake
SandboxDemos
src
chapter_1_movement (example)
include
script
src
ogre3d (example)
include
src
...
tools
decoda
premake
vs2008.bat
vs2010.bat
vs2012.bat
vs2013.bat
对于所有从Packt出版社购买的书籍,你都可以使用你在www.packpub.com网站的账号下载书籍的示例代码文件。如果你是从其他途径购买了本书,可以访问http://www.packtpub.com/support网站并注册你的信息,我们会将文件通过邮件发送给你。
下面我们来逐一检查每个文件夹的内容。
根据在Visual Studio中选择的构建配置,bin文件夹包含所有的可执行程序。解决方案无需重新编译就可以同时生成沙箱的32位和64位版本。
虽然沙箱可以编译成32位和64位应用,但只有32位版本能够支持在Decoda中进行Lua脚本调试。
build文件夹包含Visual Studio解决方案文件。build/projects文件夹包含了每个Visual Studio项目和解决方案。可以随时删除build文件夹,并使用vs2008.bat, vs2010.bat, vs2012.bat或者vs2013.bat批处理文件来重新生成项目及工程文件。应该避免直接修改Visual Studio项目文件,因为重新生成解决方案文件时会覆盖掉本地所做的修改。
deocda文件夹包含了对应各章节示例程序的Decoda IDE项目文件。这些项目文件不是从构建脚本文件生成的。
lib文件夹是静态库编译时的中间输出文件夹。删除这个文件夹是安全的,因为Visual Studio在下次构建沙箱时也会生成任何缺失的库文件。
media文件夹包含了各章示例程序所共享的所有资源。沙箱使用的资源同时以零散文件和压缩包的方式存在。
premake文件夹包含了用于生成沙箱解决方案和项目文件的构建脚本。对Visual Studio解决方案或者项目的刻意修改都应该放在Premake脚本中。
Premake脚本会检测项目文件夹结构中添加的任何C++和Lua文件。在添加了新的Lua脚本或者C++文件后,只需要重新运行构建脚本来更新Visual Studio解决方案。
src文件夹包含了各个开源库的源代码和沙箱源代码。沙箱解决方案中的每个项目都有相应的src文件夹,其中的头文件和源代码文件是分开的。每个章节示例都有一个额外的script文件夹来存放各自的Lua脚本。
每个开源库都包含一个VERSION.txt文件和一个LICENSE.txt文件。前者声明了该开源库的版本号,后者则声明了用户必须遵守的许可协议。
tools文件夹包含Decoda IDE的安装文件以及用于创建VisualStudio解决方案的Premake工具程序。
1.1.3 预先做好的构建
Premake是一个基于Lua的构建配置工具。AI沙箱使用Premake为不同的版本的Visual Studio和配置项生成解决方案,以同时支持多个版本的Visual Studio。
执行vs2008.bat批处理文件就能在build文件夹下生成一个Visual Studio 2008版本的解决方案。相应地,vs2010.bat和vs2012.bat批处理文件生成沙箱的Visual Studio 2010和2012版本的解决方案。
1.1.4 使用Visual Studio 2008/2010/2012/2013编译沙箱项目
编译沙箱只依赖DirectX SDK。你需要确保系统上安装了DirectX SDK,如果没有的话,可以到微软网站上免费下载,网址是http://www.microsoft.com/en-us/download/details.aspx?id=6812。
沙箱的解决方案文件是build/Leaning Game AI Programming.sln,它可以通过运行vs2008.bat、 VS2010.bat、 VS2012.bat、 VS2013.bat这几个批处理文件中的一个来生成。沙箱的首次构建需要编译使用的所有开源库,这将花费几分钟的时间,这之后的编译将会快很多。
1.1.5 开源库
沙箱使用的是Lua版本是5.1.5,而不是更新的5.2.x。因为最新版的Decoda IDE的调试器只能支持5.1.x版本的Lua。你可以将沙箱中的Lua替换为更新的版本,但这会使Lua调试器无法工作。
本书编写时Ogre3D图形库的最新稳定版是Ogre3D 1.9.0。沙箱只使用了Ogre3D库的最精简配置,所以它只需要依赖最少量的库来进行图像处理、字体处理、ZIP压缩和对DirectX图形的支持。
Ogre3D需要的依赖项有:
- FreeImage 3.15.4;
- FreeType 2.4.12;
- libjpeg 8d;
- OpenJPEG 1.5.1;
- libpng 1.5.13;
- LibRaw 0.14.7;
- LibTIFF 4.0.3;
- OpenEXR 1.5.0;
- Imbase 0.9.0;
- zlib 1.2.8;
- zzip 0.13.62。
沙箱构建使用的是9.29.1962版本的DirectX SDK,但任何更新的DirectX SDK版本都可以使用。此外还有其他一些开源库用于图形的调试、输出处理、物理模拟、转向模拟和寻路算法等,列举如下。
- Ogre3D Procedural 0.2:这是一个程序几何体库,提供了创建诸如球体、平面、圆柱体和胶囊等几何体的简便方法。这些几何体在沙箱中常用于关卡的调试和原型构建。
- OIS 1.3:这个平台无关的库负责沙箱中所有的输入处理和输入设备管理。
- Bullet Physics 2.81-rev2613:这是沙箱的物理引擎,负责驱动AI的运动和碰撞检测。
- OpenSteer revision 190:这是一个本地转向控制库,用于计算AI智能体的转向力。
- Recast 1.4:这个库为沙箱提供了实时的导航网格生成。
- Detour 1.4:这个库提供了基于生成的导航网格的A*寻路算法。
1.1.6 开源工具
Premake dev e7a41f90fb80是Premake的一个开发版本,基于Premake的开发分支。沙箱的Premake配置文件使用了一些只在开发分支才有的最新特性。
Decode 1.6 build 1034 通过检查沙箱的调试符号文件来提供Lua代码的无缝调试功能。
1.1.7 Lua IDE-Decoda
Decode是一个专业的Lua集成开发环境(IDE),它由Unknown Worlds Entertainment公司开源发布,这个公司也是Natural Selection 2的制造商。网址:http://unknownworlds.com/decoda/。
Decode采用一种独特的方式来进行Lua脚本调试,这种与应用程序的集成方式比其他的Lua调试器要优秀很多。其他调试器普遍使用基于网络的方法,这就要求应用内部的Lua虚拟机必须配置为支持调试的。Decoda使用由Visual Studio生成的调试符号文件来支持Lua调试。这种方法的最大优势在于,不需要对应用程序进行任何修改就能支持Lua调试。Decoda的这一重要差异使得调试沙箱中的Lua虚拟机调试变得容易。
1.1.8 在Decoda中运行AI沙箱
只需打开本章的Decoda项目(decoda/chapter_1_movement.deproj)。每个沙箱Decoda项目都已经设置好以运行对应的沙箱可执行程序。同时按下Ctrl+F5键,或者点击Debug菜单下的“Start Without Debugging”选项就可以在Decoda中运行沙箱了。
1.1.9 创建一个新的Decoda项目
创建一个新的Decoda项目,只需简单几步来为Decoda设置正确的可执行程序和调试符号。
以下几个步骤就可以配置一个新的Decoda项目。
1.打开Project菜单下的Settings菜单,如图1-1所示。
2.设置Command文本框来指向新的沙箱可执行程序。
3.设置Working Directory和Symbols Directory指向可执行程序所在目录。
当使用调试符号时,Decoda只能调试32位应用程序。AI沙箱为Release和Debug构建配置都生成调试符号。
项目设置界面如图1-2所示。
1.1.10 调试Lua脚本
在Decode中按下F5键,可以启动沙箱的Lua脚本调试。F5键将启动沙箱应用程序,并把Decoda附加到运行的进程上。从Debug菜单选择Break,或者在执行的脚本中设置一个断点,就可以暂停沙箱来进行调试,如图1-3所示。
1.1.11 Decoda的Watch窗口
如果熟悉Visual Studio的Watch窗口,你会发现Decoda的Watch窗口非常相似,如图1-4所示。调试时可以在Watch窗口中键入任意变量来监视该变量的值。Decoda也允许你在Watch窗口键入任意的Lua语句。这些Lua语句会在调试器的当前范围内执行。
1.1.12 Decoda的Call Stack窗口
Call Stack窗口显示当前执行的Lua调用堆栈,如图1-5所示。在任意一行上双击可以跳转到调用处。Watch窗口将根据调用堆栈的当前范围自动刷新。
1.1.13 Decoda的Virtual Machines窗口
Virtual Machines窗口显示在沙箱中运行的每个Lua虚拟机,如图1-6所示。沙箱应用有一个单独的虚拟机,每个运行中的智能体也有一个单独的虚拟机。
1.1.14 同时调试Lua与C++代码
有几种方法可以同时调试C++沙箱和运行中的Lua脚本。
1.1.15 Visual Studio——附加到进程
如果沙箱是从Decoda启动的,可以通过Visual Studio的Debug菜单下的Attach To Process选项附加到运行中的进程,如图1-7所示。
1.1.16 Decoda——附加到进程
Decode也可以通过Debug菜单附加到一个运行中进程。如果沙箱是通过Visual Studio运行的,你可以在任何时候把Decoda附加到它上面,方法与Visual Studio附加到沙箱上一致,如图1-8所示。
1.1.17 Decoda——附加到系统调试器
可以在Decoda启动沙箱时同时附加Decoda和Visual Studio,只需要在Debug菜单下选择Attach System Debugger。从Decoda运行应用程序时,Windows会提示你立即附加一个即时(Just-In-Time,JIT)调试器。
如果你安装的Visual Studio在可选项中没有显示即时调试器选项,可以从菜单Tools|Options|Debugging|Just- In-Time来启动原生应用的JIT调试。
图1-9显示了用来附加系统调试器的Debug选项。
1.1.18 关联Lua脚本代码到Decoda
为了让Decoda知道将哪个Lua文件联系到当前正在运行的Lua脚本上,需要使用Lua API函数luaL_loadbuffer来加载这个Lua文件,文件名作为chunkName参数传入。luaL_loadbuffer函数是在lauxlib.h文件中提供的一个Lua辅助函数。
lauxlib.h
int luaL_loadbuffer(
lua_State* luaVM, const char* buffer,
size_t bufferSize, const char* chunkName);
1.1.19 Lua虚拟机
Lua虚拟机是由一个定义在lstate.h头文件中的lua_State结构来代表的。这个结构完全是自包含的,不使用任何全局数据,因此非常适合支持多线程应用程序。
lstate.h
struct lua_State;
沙箱同时运行多个Lua虚拟机。一个主虚拟机被分配给沙箱自己使用,而每个构造出来的智能体都会运行它自己的一个虚拟机。使用多个独立的虚拟机会消耗沙箱的性能和内存,但也使得实时遍历每个智能体的Lua脚本成为可能。
1.1.20 Lua堆栈
Lua是一种弱类型语言,它的函数能接收任意数量的参数,也能有任意个数的返回值,因此它和C++代码的交互就比较棘手。
为了和强类型的C++语言交互,Lua使用一个先进先出的堆栈来发送和接收Lua虚拟机中的数据。例如,当C++想调用一个Lua函数,则将Lua函数以及调用参数推入堆栈中,然后由虚拟机来执行这个函数。函数的任何返回值也会被推入堆栈中,交由调用的C++代码处理。
在Lua代码中调用C++代码的过程正好相反。首先,Lua会将C++函数推入栈中,接着推入发送给函数的参数。代码执行结束后,返回值会被推入栈中,以便Lua脚本处理。
Lua堆栈数据可以从下至上或从上至下进行访问。栈顶元素可以用索引值-1来访问,栈底元素的索引值是1,相应地其他元素的索引则是-2、-3、2、3等,如图1-10所示。
》Lua和大多数编程语言的一个差别在于它是从1而不是0开始索引。
1.1.21 Lua基础类型
在Lua中有8种基础类型:nil(空)、Boolean(布尔)、number(数字)、string(字符串)、function(函数)、userdata(自定义类型)、thread(线程)和table(表)。
- Nil:空值对应于C中的NULL值。
- Boolean:对应于C++中的布尔类型,代表true或者false。
- Number:Lua数值类型在内部用double实现,可存储整数、长整数、单精度浮点数和双精度浮点数。
- String:可表示任意的字符序列。
- Function:Lua把函数也看作基础类型,因此可以把函数赋值给变量。
- Userdata:这种特别的Lua类型用于将一个Lua变量映射到一个在C代码中管理的数据。
- Thread:Lua使用线程类型来实现协程(coroutine)。
Table:表示一种关联数组,将一个索引映射到一个其他基础类型变量。Lua表可以使用任意的Lua基础类型来索引。
1.1.22 元表
Lua中的元表是一个表类型,利用元表可以用自定义函数来覆盖已有的通用操作,例如加、减、赋值等待。沙箱中大量使用了元表来为由C++管理的自定义类型提供通用操作。
Lua中获取元表的函数是getmetatable,函数参数就是要获取其元表的对象:
metatable getmetatable(object);
Lua中设置元表函数是setmetatable,两个参数分别是要设置其元表的对象和新的元表:
nil setmetatable(object, metatable);
由于沙箱在自定义类型上大量使用了元表,你总可以使用getmetatable函数来获取自定义类型的元表,以查看该自定义类型支持的操作。
1.1.23 元方法
元方法是元表中的特殊表项,它会在Lua需要某个被覆盖的操作时被调用。通常,所有的Lua元方法函数名都以两个下划线作为开头。
在元表中添加元方法的方法是,把函数赋值给由方法名索引的元表表项。例如:
local metatable = getmetatable(table);
metatable.__add = function(left, right)
return left.value + right.value;
end
setmetatable(table, metatable);
1.1.24 自定义类型
自定义类型是一块任意的数据,它的生命周期是由Lua的垃圾收集器管理的。每当代码创建一个自定义类型对象并推入Lua时,lua_newuserdata函数会请求一块由Lua管理的内存。
lua.h
void* lua_newuserdata(lua_State* luaVM, size_t userdataSize);
虽然沙箱大量使用了自定义类型,它使用的内存的构造和析构仍然是在沙箱内部处理的。这使得运行Lua脚本时不必担心Lua内部的内存管理。例如,当通过自定义类型将一个智能体暴露给Lua时,Lua管理的只是一个指向这个智能体的指针。Lua可以自由地对这个指针进行垃圾收集,但对智能体本身不会造成任何影响。
1.1.25 C/C++调用Lua函数
沙箱通过Sandbox_Initialize、Sandbox_Cleanup和Sandbox_Update这3个预定义的全局Lua函数来连接到Lua脚本上。在将相应的Lua脚本首次附加到沙箱时会调用Sandbox_Initialize函数。沙箱在每次更新循环时会调用Lua脚本中的Sandbox_Update函数。当沙箱被销毁或者重新加载时, Sandbox_Cleanup函数将被调用以执行任何脚本端的清理工作。
为了让C++调用Lua函数,该函数需要能在Lua中获取到并推入堆栈中。然后将函数参数推入栈顶,接着就可以调用lua_pcall函数来执行Lua函数了。通过lua_pcall函数可以指定Lua函数接收的参数个数、返回值个数和错误处理的方式。
lua.h
int lua_pcall(
lua_State* luaVM, int numberOfArguments,
int numberOfResults, int errorFunction);
例如,AgentUtiltities类使用下面的方式来调用Agent_Initialize Lua脚本函数:
Agent.lua
function Agent_Initialize(agent)
...
end
首先,这个Lua函数在Lua中通过名字获取并推入堆栈中。接下来,将智能体本身作为Agent_Initalize函数的唯一参数推入堆栈。最后,调用lua_pcall函数会执行这个脚本函数并检查它是否执行成功,如果未成功,则沙箱会生成一个断言。
AgentUtilities.cpp
void AgentUtilities::Initialize(Agent* const agent)
{
// Retrieves the lua virtual machine the agent script is
// running on. lua_State* luaVM = agent->GetLuaVM();
lua_getglobal(luaVM, "Agent_Initialize");
// Agent_Initialize accepts one parameter, an Agent.
AgentUtilities::PushAgent(luaVM, agent);
// Execute the Agent_Initialize function and check for
// success.
if (lua_pcall(luaVM, 1, 0, 0) != 0)
{
assert(false);
}
}
1.1.26 Lua调用C/C++函数
可以通过函数绑定过程将C++函数暴露给Lua。任何暴露给Lua的被绑定的函数可以作为一个全局函数来访问,或者通过一个包来访问。Lua中的包类似于C++中的名空间,它是使用Lua中的一个全局表来实现的。
函数绑定
任何暴露给Lua的函数都必须符合lua_Cfunction声明。一个lua_CFunction声明接受一个Lua虚拟机作为参数,并返回被推入Lua堆栈中的返回值的个数。
lua.h
typedef int (*lua_CFunction) (lua_State *L);
例如,沙箱中暴露的C++函数GetRadius在LuaScriptBidings.h文件中是这样声明的:
LuaScriptBindings.h
int Lua_Script_AgentGetRadius(lua_State* luaVM);
函数的实际实现定义在LuaScriptBindings.cpp文件中,它包含了从堆栈中获取参数和将数据推入堆栈的代码。GetRadius函数需要一个智能体指针作为第一个也是唯一一个参数,然后使用AgentUtilities类中的一个辅助函数从堆栈中获取这个指针引用的自定义数据。由一个额外的辅助函数来实际计算智能体的半径然后把结果推入到堆栈中:
LuaScriptBindings.cpp
int Lua_Script_AgentGetRadius(lua_State* luaVM)
{
if (lua_gettop(luaVM) == 1)
{
Agent* const agent = AgentUtilities::GetAgent(
luaVM, 1);
return AgentUtilities::PushRadius(luaVM, agent);
}
return 0;
}
为了完成绑定,我们定义一个常数数组来把Lua中的函数映射到实际调用的C函数上。这个映射数组必须以一个空的lua_Reg类型结构来结束。当处理函数映射时,Lua使用这个空的LuaL_Reg类型结构作为终止符:
AgentUtilities.cpp
const luaL_Reg AgentFunctions[] =
{
{ "GetRadius", Lua_Script_AgentGetRadius },
{ NULL, NULL }
};
函数绑定到Lua虚拟机实际发生在luaL_register辅助函数中。这个注册函数将表中的函数名称绑定到它们对应的C回调函数。同时还可以指定一个包名称并在映射时关联到每个函数上。
AgentUtilities.cpp
void AgentUtilities::BindVMFunctions(lua_State* const luaVM)
{
luaL_register(luaVM, "Agent", AgentFunctions);
}
如果传入NULL作为包名,Lua会查询位于Lua堆栈顶部的表。Lua会将C函数添加到这个堆栈顶部的表中。
1.1.27 创建自定义数据类型
沙箱使用自定义类型来传递智能体和沙箱本身,同时也用来添加一些基础类型。这些基础类型完全由Lua的垃圾收集器控制。
沙箱中的向量类型就是一个完全由Lua控制的自定义类型的例子。向量基本上只是包含3个数值的一个结构体,因此让Lua来管理它的创建和销毁是正确的选择。与Lua向量交互的C++代码不能够持有Lua返回的内存地址,而是拷贝数据并保存在本地。
向量数据类型
把向量实现为Lua的一个基础类型意味着需要支持用户可能对向量进行的所有操作。向量应该支持加、减、乘、索引以及所有其他Lua支持的基础操作符。
为实现这些操作,向量数据类型使用元方法来支持基础的数学运算符,并用点操作符来支持“.x”、“.y”和“.z”这样的语法。
LuaScriptUtilities.cpp
const luaL_Reg LuaVector3Metatable[] =
{
{ "__add", Lua_Script_Vector3Add },
{ "__div", Lua_Script_Vector3Divide },
{ "__eq", Lua_Script_Vector3Equal },
{ "__index", Lua_Script_Vector3Index },
{ "__mul", Lua_Script_Vector3Multiply },
{ "__newindex", Lua_Script_Vector3NewIndex },
{ "__sub", Lua_Script_Vector3Subtract },
{ "__tostring", Lua_Script_Vector3ToString },
{ "__towatch", Lua_Script_Vector3ToWatch },
{ "__unm", Lua_Script_Vector3Negation },
{ NULL, NULL }
};
LuaScriptUtilities.h
#define LUA_VECTOR3_METATABLE "Vector3Type"
为了让代码支持这些功能,Lua需要在分配内存时知道它正在操作的自定义类型的具体类型。LuaScriptUtilities头文件定义了向量类型的元表名:
LuaScriptUtilities.cpp
void LuaScriptUtilities::BindVMFunctions(lua_State* const luaVM)
{
...
luaL_newmetatable(luaVM, LUA_VECTOR3_METATABLE);
luaL_register(luaVM, NULL, LuaVector3Metatable);
...
}
当把C++函数绑定到Lua虚拟机时,需要一个额外的步骤来支持向量。LuaL_newmetatable函数会创建一个新的元表,并把它关联到向量自定义类型上。在新建元表并推入Lua堆栈之后,调用luaL_register函数来把列在luaVector3Metatable中的元方法加入到元表中:
LuaScriptUtilities.cpp
int LuaScriptUtilities::PushVector3(
lua_State* const luaVM, const Ogre::Vector3& vector)
{
const size_t vectorSize = sizeof(Ogre::Vector3);
Ogre::Vector3* const scriptType =
static_cast<Ogre::Vector3*>(
lua_newuserdata(luaVM, vectorSize));
*scriptType = vector;
luaL_getmetatable(luaVM, LUA_VECTOR3_METATABLE);
lua_setmetatable(luaVM, -2);
return 1;
}
每当在Lua中创建一个向量时,lua_newuserdata函数会分配所需内存,Lua会获取向量的元表并关联到这个自定义类型上。这使得Lua知道自定义类型的具体类型以及它支持的所有函数。
1.1.28 Demo框架
Demo框架的设计遵循了沙箱中许多其他类的设计,包含了简单的初始化、更新和清理的功能。
BaseApplication.h头文件的类概览图如图1-11所示。
BaseApplication类的主要功能有配置应用程序窗口、处理输入命令以及和配置并处理Ogre3D。BaseApplication类还包含Cleanup、Draw、Initialize和Update函数,但这些函数的实现都是空的。BaseApplication类的继承类可以重载这些函数以插入自定义的逻辑。
在继承类中,Initialize函数会在应用程序启动时在Ogre初始化之后调用一次。
Cleanup函数会在应用程序准备关闭时,在Ogre清理之前调用。
Draw函数会在图形处理单元(Graphics Processing Unit,GPU)渲染当前应用程序帧之前调用。
Update函数的调用紧跟在GPU将所有处理当前帧的渲染调用列队之后。这使得GPU可以和CPU开始准备下一个渲染帧时同步工作。
1.Ogre
Ogre3D处理沙箱的全局更新循环和窗口管理。BaseApplication实现了Ogre:: FrameListener接口以实现沙箱的Update和Draw调用。
OgreFrameListener.h头文件的类概览图如图1-12所示。
BaseApplication实现的另一个接口是Ogre::WindowEventListener,它使沙箱能够接受特定的窗口事件,比如窗口移动、尺寸调整关闭前关闭后以及窗口焦点变化等。
OgreWindowEventListener.h头文件中的类概览图如图1-13所示。
这两个接口的函数都是在Ogre的主线程中调用的,因此在处理事件时不会存在竞争条件。
2.面向对象输入系统
面向对象输入系统(Object-Oriented Input System,OIS)库负责处理沙箱中所有的键盘和鼠标事件。BaseApplication类实现了OIS系统中的两个接口来接收来自按键点击、鼠标点击和鼠标移动的事件。BaseApplication一旦接收到这些事件,就把它们依次转发到沙箱中。
OISKeyboard.h头文件的类概览图如图1-14所示。
OISMouse.h头文件中的类概览图如图1-15所示。
3.SandboxApplication
SandboxApplication类是AI沙箱的主应用程序类,它继承自BaseApplication基类,实现了基类中的Cleanup、Draw、Initialize和Update函数。CreateSandbox函数创建一个沙箱实例,然后把它关联到一个由文件名参数指定的Lua脚本上。
SandboxApplication.h头文件中的类概览图如图1-16所示。
4.Sandbox类
沙箱类封装了沙箱数据并处理对Lua沙箱脚本的调用。构造沙箱对象需要一个SceneNode对象来定位它在游戏世界中的位置。沙箱的SceneNode实例也是其他所有用于渲染的几何体SceneNode的父节点,也包含沙箱中的AI智能体类。
Sandbox.h头文件中的类概览图如图1-17所示。
5.Agent类
代理类封装了代理数据,还能执行通过LoadScript函数绑定的Lua脚本。构造agent实例时需要一个SceneNode对象,用来维持代理对象在游戏世界中的方向和位置。
Agent.h头文件的类概览图如图1-18所示。
6.工具类
AI沙箱使用了很多工具模式来分离逻辑和数据。沙箱和代理类各自保存它们自己相关的数据,和Lua虚拟机交互的数据的处理则由工具类来完成。
比如,AgentUtilities类处理Lua AI代理执行的所有动作,而SandboxUtilities类处理Lua沙箱执行的所有动作。
任何通用功能或与Lua虚拟机的其他各种交互都由LuaScriptUtilities类来处理。
7.Lua绑定
LuaScriptBindings.h头文件描述了沙箱暴露给Lua虚拟机的所有C++函数。你可以把这个文件作为AI沙箱的应用程序接口(API)的参考文档。每个函数都有功能描述、函数参数、返回值和Lua代码调用示例。