Run-time(运行时)
启动游戏
要想启动游戏,就需要使用稍作改动的system(系统)脚本。 改动步骤如下:
- 包含头文件data/framework/game/game_system.h,它用来实施框架的system(系统)脚本
- 在init()功能中调用Game::init()方法
- 在update()功能中调用Game::update()方法
- 在shutdown()功能中调用Game::shutdown()方法
Game Framework(游戏框架)的system(系统)脚本的例子如下(它存放在路径下文件data/framework/game/game_system_script.cpp中):
#include <unigine.h>
#include <core/scripts/system/system.h>
#include <core/scripts/system/stereo.h>
#include <core/scripts/system/wall.h>
#include <framework/game/game_system.h>
int init() {
systemInit();
stereoInit();
wallInit();
Game::init();
return 1;
}
int shutdown() {
systemShutdown();
stereoShutdown();
wallShutdown();
Game::shutdown();
return 1;
}
int update() {
systemUpdate();
stereoUpdate();
Game::update();
return 1;
}
int render() {
stereoRender();
wallRender();
return 1;
}
要想启动游戏,须将参数[-game file]指定为引擎加载时的CLI,其中的file指的是相对于data目录的游戏路径名称。 您也可以指定附加参数[-game_level index],其中的index指要被加载的关卡的数量。 如果该参数未被指定,那就会加载第一个关卡。 如果游戏中不存在关卡,那游戏将不会被加载,并且相应的消息也会出现在日志中。
添加游戏路径和游戏关卡参数的另一种方法是按如下方式在引擎配置文件中正确设置它们:
<!-- 指游戏路径参数 -->
<item name="game" type="string">framework/samples/strategy/strategy.game</item>
<!-- 指起始关卡参数 -->
<item name="game_level" type="int">0</item>
<!-- 指system(系统)脚本的路径 -->
<item name="system_script" type="string">framework/game/game_system_script.cpp</item>
system(系统)脚本的执行序列:
当初始化框架的system(系统)脚本实例时,MasterGame类的实例就会被创建(该类存放在路径下文件data/framework/game/master_game.h中)。 该实例控制着框架和游戏自身,因此它包含了有关所有关卡以及这些关卡中的哪一个是当前正运行的信息。 关卡只能通过MasterGame类的使用来被加载或重载。 它从引擎初始化时开始执行直到引擎关停时结束。
关卡(Level)的加载和World(世界)脚本
任何.world文件都拥有对应的.cpp文件,而要执行的就是位于其中的world(世界)脚本。 对每一个虚拟世界而言,Game Framework(游戏框架)都会为其应用相同的.cpp文件,方法是只需包含如下一行代码即可:
#include <framework/game/game_world_script.h>
对要运行的框架而言这就足够了。
World(世界)脚本的执行序列:
初始化(Initialization)
init()功能被用来创建游戏以及初始化游戏启动时的所有必要资源。
初始化会从将XML文件中的所有游戏数据都加载到特殊类GameData中开始。 在此之后,如果发生初始化错误则会创建标准的Player类。 再然后,带有指定的,当前加载的虚拟世界的Game类就会被创建。
Game类会加载关卡逻辑(前提是存在),以及加载所有用户实体(entities)的逻辑,并会将该逻辑放进带有命名空间的"EntitiesLogic"表达式中。 如果用户代码有错误且不能被编译,那日志中就会出现相应的消息,游戏也将不能被启动。
待用户逻辑编译通过之后,框架就创建了用户类Level的实例,该实例适用当前加载的关卡。 如果该类不能被创建,那日志中就会出现相应的消息,游戏也将不能被启动。
下一步是对用户代码的分析以及对所有实体(entities)属性的检查。 所有的用户实体(entities)都会在字段(fields)出现时被检查。 如果找到了字段,那对应的属性就将被检查。 如果存在任何不匹配的情况,那日志中就会出现相应的消息,游戏也将不能被启动。
当初始化Level类时,集成框架的所有系统(调度器(scheduler),事件系统(event system),实体池(entity pool))都会被创建。
之后框架会分析关卡的所有节点引用并检查这些引用是否属于实体(entities)。 如果某个节点引用是实体(entity),那框架就会创建用户类Entity同时为其分配节点引用的名称。 所有分配了名称的实体(entities)都被放置在了特殊的映射文件中,在这里每个实体(entity)都可以通过其名称来查找。 如果存在两个或更多实体(entities)具有相同的名称,那搜索期间找到的最后那个实体(entity)则会被放置进映射文件。
在某个实体(entity)创建之初,其节点就获得了Node::setVariable()函数,其中的variable指实体(entity)的实例。 之后,当对该节点进行分析时我们就可以假设它是否为实体(entity)。
待虚拟世界的所有实体(entities)都被创建完之后,框架就会设置它们的字段(fields)。 换句话说,set()方法会被调用给每个字段(field)。 set()方法会传递属性中的对应参数。 此外,所有的实体(entities)都会由update组来分配,其分为:可更新,可清除或可渲染的实体(entities)。 在此之后,用户实体(entity)和Level类 onInit()方法才会被调用。
如果节点或实体(entities)在初始化期间就已经被添加给了虚拟世界,那就会调用engine.world.updateSpatial()函数。
Update,Flush,Render
update()功能是框架中最重要的部分,因为游戏的逻辑和过程都是在它里面被执行的。 框架的首要功能是提供最优的实体(entities)更新,update组的动态安全交换,顺序的更改,或禁用根本不需要update的实体(entities)。 此外,框架还会实施实体(entities)的安全创建和删除,会将已删除的实体(entities)放进内存池以便后面再使用它们。 它也提供对函数的定期调用和自动时间扩展。
框架的update()功能按如下方式执行:
- 引擎调用world(世界)脚本的update()功能,该功能再调用Game类的update()功能。
- Game类再调用当前使用的Level类的update()功能。 关卡的更新则被划分成如下几个部分:
- 集成框架的所有系统(周期函数调用调度器(scheduler),分析器(profilers)等等),其都将被更新。
- 实体(entities),最后一次更新期间被删除,其都将被放进内存池或被完全删除。
- 实体(entities),最后一次更新期间被创建,其都将被添加进列表。
下次更新时实体(entity)才会出现在实体列表中。
- onPreUpdate()函数供关卡调用。 该函数需要用户代码,会在实体(entities)更新前被实现(例如,控制更新)。
- onUpdate()函数供所有的可更新实体(entities)调用。
- onPostUpdate()函数供关卡调用。
render()功能每次都在update()之后被调用。 框架会调用当前Level类的render()功能,而反过来该Level类又会调用所有可渲染实体(entities)的onRender()函数。 之后,onRender()函数会供当前关卡调用。
flush()功能的调用与render()的调用相似,不过它是在带有固定FPS的独立引擎线程中进行的。
实体(Entities)的onUpdate(),onRender()和onFlush()函数调用
每个实体(entity)都拥有onUpdate(),onRender()和onFlush()这三个虚函数。 这些函数调用会在实体(entity)有需要时被禁用。 可通过调用函数setUpdateable(),setRendereable(int mode)和setFlushable(int mode)或通过更改之前设置的字段(Field)值来动态禁用它们。
此外,每个实体(entity)也都拥有Update Order参数,该参数确定了要被更新的实体(entities)的顺序。 例如,如果您需要摄像机实体要在角色实体之后被更新,那摄像机实体的Update order参数就应比角色实体的该参数要大。
Update order参数可以在Game Framework Editor(游戏框架编辑器)中设置,或是通过实体(entity)函数setUpdateOrder(int order)来动态设置。 它的取值有17个,范围是从0到16(包含这两个值)。 其默认值为0。 该参数会影响函数onUpdate(),onRender()和onFlush()的调用顺序。
回调(Callbacks)
回调(Callbacks),或者说是由引擎在某个方便的时间所执行的用户代码块,它们在您开发项目时起着非常重要的作用。 其种类有Unigine Widgets回调(按下的按钮,未聚焦的窗口),body回调(碰撞之后被调用),或是由Physical triggers(物理触发器)或World triggers(对象触发器)触发的回调。
在使用回调(callbacks)时,您应该记住,所有的用户逻辑都是被动态编译进带有命名空间的独立表达式中的,而该命名空间是由此表达式定义的(通过调用Game::getLogicNamespaceName()方法可返回该命名空间的名称)。 换句话说,在将回调(callback)设置给引擎的对象时,请您确保将命名空间指定为了您函数的前缀。
Unigine Widgets回调在自定义Entity类中的用法举例:
class MyEntity : Entity {
private:
WidgetButton button;
// 按钮点击的回调
void button_clicked() {
log.message(“my button clicked\n”);
}
// 按钮回调的重定向器
void button_callback_redirector( MyEntity entity) {
entity.button_clicked();
}
public:
void onInit() {
Gui gui = engine.getGui();
button = new WidgetButton(gui,"Close");
gui.addChild(button,GUI_ALIGN_CENTER);
button.setCallback(GUI_CLICKED,game.getLogicNamespaceName() + "::MyEntity::button_callback_redirector",this);
}
void onShutdown() {
// 不要忘记从widget中删除回调(callbacks)
button.setCallback(GUI_CLICKED,NULL);
// 或删除widget
delete button;
}
};
在shutdown()阶段,您需要从Unigine对象上取消绑定所有引用了表达式的回调(在shutdown()阶段,表达式连同所有的用户逻辑都会被删除,并且如果存在有绑定到了Unigine对象上的回调,那该回调就会引用不存在的表达式,而这就很可能会致使调用该回调时发生引擎崩溃)。
调度器(Scheduler)
调度器提供了对函数或函数组的定期调用和自动时间扩展。 它可以管理复杂的逻辑,例如,如果某些操作无需每次更新都计数,像:寻路,对象状态的复杂计算,甚至包括带有数据库请求的您的C++插件的调用。 时间扩展有助于避免尖峰(指如果存在大量对象,那更新时间就会大幅增加)。
每秒钟的调用频率可在1次到60次之间选择。 调度器可调用静态函数,或是类实例的函数。
对于要被定期调用的函数而言,须在当前加载的关卡中调用Level::setPeriodicUpdate(int instance,string function,int frequency,int priority)函数。
此外,在定期调用函数时您也可以传递多达4个附加参数。
您可随时安全取消函数的定期调用,方法是:在当前加载的关卡中调用Level::removePeriodicUpdate(int instance,string function,int num_args = 0)函数。
如果您想要更改自己函数的调用频率,须为设置了新频率的该函数调用Level::setPeriodicUpdate(int instance,string function,int frequency,int priority)。
您不能调用带有不同频率的同一函数。
所有的Scheduler函数都是由调用频率分进组里的。 当您订阅定期的函数更新时,函数(任务)就会根据自身的频率,被自动移至相应的组里。 之后调度器会为tick设置一个时间,在这个时间段里需要完成一个任务。 而该时间则取决于组中任务的数量。 对每个虚拟世界的更新而言,Scheduler都将那些需要被执行的任务形成了一个组。 如果虚拟世界的更新时间超过了某个组的tick时间,那该组中的多个任务就会留在列表中。 然后,Scheduler会通过优先级排序这些任务,并执行它们的函数调用。 这种系统为复杂任务提供了正确的时间扩展,也易于执行带有指定频率的任务。 如果虚拟世界的更新时间过长(低FPS),Scheduler便会一直执行任务,不过任务的调用频率就得不到保证了。 如果FPS极低,那Scheduler就能在同一次更新中执行不止一次任务(换句话说,即使存在挂起,Scheduler仍然会执行指定任务)。
下面给出调用MyEntity类的"myPeriodicFunction"函数的例子,该函数每秒钟要被调用10次,其优先级为0,且带有一个自定义的int类型参数:
class MyEntity : Entity {
public:
void onInit() {
int my_value = 333;
// 将定期更新设置给"myPeriodicFunction"函数,其调用参数为:
// 每秒钟的调用频率为10次,优先级为0,
// 还带有一个自定义的int类型参数。
level.setPeriodicUpdate(this,”myPeriodicFunction”,10,0,my_value);
}
void onShutdown() {
// 移除定期更新
level.removePeriodicUpdate(this,”myPeriodicFunction”,0);
}
void onUpdate() {
}
void onRender() {
}
void onFlush() {
}
void myPeriodicFunction(int some_value) {
log.message(“some_value = %i\n”,some_value);
}
};
如果您想让某个静态函数在用户代码中描述和编译,同时还要被定期调用,那就不要忘记将框架表达式的命名空间指定为其前缀(您可通过调用当前游戏中的Game::getLogicNamespaceName()函数来获取该命名空间的名称)。 例如:
namespace MyNamespace {
void myPeriodicFunction() {
log.message(__FUNC__ + “ called\n”);
}
}
class MyEntity : Entity {
private:
public:
void onInit() {
level.setPeriodicUpdate(NULL,game.getLogicNamespaceName() +
”::MyNamespace::myPeriodicFunction”,10,0);
}
void onShutdown() {
level.removePeriodicUpdate(NULL,game.getLogicNamespaceName() +
”::MyNamespace::myPeriodicFunction”);
}
void onUpdate() {
}
void onRender() {
}
void onFlush() {
}
};
事件系统(Event System)
事件系统是每个游戏的基本组成。 它服务于游戏对象间的交互,或是虚拟世界与其它对象间的交互。 每个用户类的实例都可以被附加给事件,因此当事件发生时,所设置函数就将被调用。 框架允许用户随时创建,调用,禁用或删除事件。 此外,在指定区域内也存在有针对实体(entities)的事件调用。
要想创建事件,须在当前关卡中调用Level::createEvent(string name)函数,这样,系统就将创建具有指定名称的一个事件,而您也就能对它进行附加这样的操作了。
与定期调用的调度器相似,您也可以通过调用Level::subscribe(string name,int instance,string function)函数来订阅当前关卡中的事件。 您可以将最多4个用户参数传递给函数。
您可通过调用Level::unsubscribe(string name,int instance,string function,int num_args = 0)函数来取消订阅函数。
您可通过调用Level::callEvent(string name)函数来调用当前关卡的事件,因此该函数会被调用给关卡中的所有订阅者。
在指定区域内也存在特殊的事件调用。 如果订阅者(subscriber)是一个Entity类的实例,那它就会进入特殊列表,而指定区域内的调用则会被应用于该列表。 如果您是通过Level::callEvent(string name,variable p0,variable p1)函数调用的事件,那根据穿过所有节点的p0和p1的类型的不同,就会存在不同的搜索(通过engine.world.getIntersectionNodes(variable p0, variable p1,int type,int ret_id[])函数)。 如果节点是实体(entity),那事件将会供它调用。
参数p0和p1的类型包括:
- vec3和vec3 — 在包围盒(bounding box)内执行搜索。 变量用来设置包围盒的最小点和最大点(通过x,y和z轴)。
- vec3和float — 在包围球体(bounding sphere)内执行搜索。 变量用来设置球体的中心及其半径。
- mat4和mat4 — 在视锥体(view frustum)内执行搜索。 变量用来设置投影和modelview矩阵。
要想启用或禁用事件,须调用Level::setEventEnabled(string name,int mode)函数。 您可通过调用Level::isEventEnabled(string name)函数来检查是否启用了该函数。 您可通过调用Level::isEvent(string name)函数来检查事件是否存在。
实体(Entity)的动态创建和删除
凭借框架,您就能动态创建或删除实体(entities)。
要想删除实体(entity),须在当前关卡中调用Level::removeEntity(Entity entity,int mode = 0)函数。 实体的删除模式有3种,分别为:
- REMOVE_ENTITY_MODE_DEFAULT - 删除用户实体(entity)的实例
- REMOVE_ENTITY_MODE_DELETE_NODE - 删除实体(entity)以及与其连接的节点引用
- REMOVE_ENTITY_MODE_POOL - 将实体(entity)及其节点引用放进内存池。 这种模式对于创建新实体会容易的多,因为实体已经在内存池中了。
不管实体的删除模式是哪种,onShutdown()方法都会供其调用。 您应该在其中清空自己的资源,从事件取消订阅并取消定期的函数调用。
为了能动态创建新实体,须在当前关卡中调用Level::createEntity(string type,string name = “”)函数。 创建实体的第一步就是对内存池进行指定实体类型以及实体存在与否的检查,如果存在,就将其从中取走。 如果不存在,新实体的实例和节点引用就会被创建,它们之间的对应关系也会被设置。 在此之后,所有的字段(fields)都会被设置给实体(entity)。 最后,onInit()函数会供新实体调用。
框架的另一个功能是:在调用了Level::removeEntity(Entity entity,int mode = 0)函数且实体(entity)自身(entity = this)被作为参数传递的情况下,实体(entity)可随时安全删除其自身。
实体(Entities)的交互
除了事件系统,还存在其它实体交互的方式。 由于用户逻辑是被写入某个表达式中的,因此您可以将一个实体的方法调用给别的实体。 为此,您需要查找目标实体(entity):
- 通过名称查找实体。 为此须调用Level::getEntity(variable index)函数。 如果存在具有该名称的实体,那就返回该实体;否则返回NULL。
- 从节点定义实体(最快的方式)。 您可通过调用engine.world.getIntersectionObjects()函数或通过body回调来查找节点。 如果该节点是节点引用的根节点,且被连接到了实体(entity),那所需实体(entity)的实例就可通过Node::getVariable()来查找。
- 通过索引查找实体。 为此须调用Level::getEntity(int index)。 您可通过调用Level::getNumEntities()函数来获取实体的数量。
- 收集所有的实体。 为此须调用Level::getEntities(Entity ret[],string type = ””,int childs = true)函数。
当找到实体(entity)后,其方法也就可以供其它实体(entities)调用了。
通过关卡(Levels)传递参数
由于游戏逻辑是被放置在world(世界)脚本中的,并且每次关卡被重新加载时它也会被重新加载,因此不同关卡的通用参数也就不能被保存在world(世界)脚本中。
不过Game Framework(游戏框架)却能持有system(系统)脚本中的任何用户数据,也能将这些数据用在不同的关卡。 例如,您可以在下一关卡中使用游戏进程或角色状态的结果。
存在2个可设置或可获取参数的当前类实例的函数:Game::setGameParameter(string name,variable value)和Game::getGameParameter(string name)。
此外,您也可以在游戏结束之后将参数值保存进文件。 为此,须在游戏初始化(指Game::init();这行)之后,在system(系统)脚本中,通过Game::setGameParameter(string name)函数添加所需参数,在游戏被结束之后(指Game::shutdown();这行之前),通过调用Game::getGameParameter(string name)函数将参数保存进文件。