Gamemonkey编程---高级进阶3
这些东西是平时遇到的, 觉得有一定的价值, 所以记录下来, 以后遇到类似的问题可以查阅, 同时分享出来也能方便需要的人, 转载请注明来自RingOfTheC[ring.of.the.c@gmail.com]
继续翻译GameMonkey脚本语言的文章, 这些文章都是在GameDev网站上找到的. 在翻译的过程中, 更加深了我对GM的了解和兴趣, 它的协程机制确实比Lua的协程在原生支持方面争强了很多, so enjoy! 上次GM参考手册的翻译放在了一篇文章, 感觉显的太长了, 所以这次我决定将这些长篇翻译分成多篇文章, 这样阅读起来比较方便, 而且可以避免一次信息过大
原文地址: http://www.gamedev.net/reference/programming/features/gmScriptAdv/page2.asp
将GameMonkey和你的游戏结合起来
从前面的学习中可以看出, GameMonkey提供了很多高级机制, 使得脚本程序员可以利用它们创建出非常复杂的脚本驱动实体. 在实际应用中脚本程序员往往希望使用更加简洁易用的脚本接口来使用GameMonkey的协程等等特性, 比如你要创建一个Npc并给它创建一个update协程时, 你往往希望写下如下的代码:
npcupdatefunc = function() { .. do stuff ...};
npc = createNpc(npcupdatefunc);
而不是下面的这种代码:
npcupdatefunc = function() {... do stuff ...};
npc = createNPC();
npc:thread(npcupdatefunc);
这样就可以实现在游戏中发生异步事件时调用npcupdatefunc函数, 比如说对话框或者是实体移动指令.
本小节将指导你通过使用GameMonkey C++ API来利用协程机制, 并教授你如何使用这些API给脚本程序员提供更加简洁的接口以避免由于脚本语言本身的灵活性而带来的细节方面的麻烦.
创建gmThread
任何时候你都可以通过调用API接口 gmMachine::CreateThread()在GameMonkey虚拟机中创建一个新的协程. 这个API创建了一个gmThread对象, 并且返回一个虚拟机中全局唯一的协程Id. 在调用该API的时候, 通过第一个参数你可以传递一个gmVariable作为this和协程绑定, 第二个参数gmVariable包含一个gmFunctionObject, 用来指定协程函数. 在下面的例子中将演示如何创建一个简单的协程,
#include <iostream>
#include "gmThread.h"
int main(int argc, char* argv[]) {
// 创建gm虚拟机
gmMachine gm;
const char* testscript = "global threadfunc = function() { print(\"Hello, threads!\"); };";
if (gm.ExecuteString(testscript, NULL, true)) {
bool first = true;
std::count << gm.GetLog().GetEntry(first);
return 1;
}
int new_threadId = 0;
gm.CreateThread(gmVariable::s_null, gm.GetGlobals()->Get(&gm, "threadfunc"), &new_threadId);
gm.Execute(1);
return 0;
}
这段代码很简单, testscript脚本执行时创建了一个GameMonkey脚本函数(即 threadfunc), 然后创建一个新协程来执行它. 这里有一点值得注意, 开始时我们使用GameMonkey脚本创建了一个函数, 然后创建了一个协程并把这个脚本函数作为协程函数, 接着我们必须调用gmMachine::Execute来运行协程, 因为最初的ExecuteString调用实际上也创建了一个协程, 而且在GameMonkey虚拟机中它依然被视为是一个活跃的协程.
下面演示如何向协程绑定一个值作为this变量, 在上面的例子中我们只是简单的传递了一个gmVariable::s_null变量绑定给新建的协程, 也就是说协程中的this是null. 在下面的例子中, 你可以注意到我们将一个字符串绑定给脚本函数作为this变量.
#include <iostream>
#include "gmThread.h"
int main(int argc, char* argv[]) {
gmMachine gm;
const char* testscript = "global threadfunc = function() { print(\"'this' passed as --\", this); };";
if (gm.ExecuteString(testscript, NULL, true)) {
bool first = true;
std::count << gm.GetLog().GetEntry(first);
return 1;
}
int new_threadId = 0;
gm.CreateThread(gmVariable(gm.AllocStringObject("Hello, this!")), gmGetGlobals()->Get(&gm, "threadfunc"), &new_threadId);
gm.Execute(1);
return 0;
}
就像你预期的那样, 把一个本地函数(即C函数)当做协程函数是可行的, 你只需要简单的把本地函数绑定到一个gmFunctionObject上, 并且用gmVariable将这个gmFunctionObject包裹起来即可. 下面的代码演示了和上一个例子有功能的用本地函数实现的方法
int GM_CDECL gmMyThreadFunc(gmThread* a_thread) {
std::cout << "Hello, threads!" << std::endl;
return GM_OK;
}
int main(int argc, char* argv[]) {
gmMachine gm;
gmFunctionObject* threadfunc = gm.AllocFunctionObject(gmMyThreadFunc);
int new_threadId = 0;
gm.CreateThread(gmVariable::s_null, gmVariable(threadfunc), &new_threadId);
return 0;
}
任何一个符合gmType中类型定义的, 向gmMachine注册, 然后通过gmVariable进行包裹的对象都可以传递给一个协程. 下面的例子演示了如何在协程中使用gmThread::GetThis方法来访问this变量. 在这个例子中我们演示的是一个string, 但是在你的代码中, 你可以使用任何你想的类型
int GM_CDECL gmMyThreadFunc(gmThread* a_thread) {
GM_ASSERT(a_thread->GetThis()->m_type == GM_STRING);
gmStringObject* thisstr = reinterpret_cast<gmStringObject* >(a_thread->GetThis()->m_value.m_ref);
std::cout << "'this' passed as " << thisstr->GetString() << std::endl;
return GM_OK;
}
int main(int argc, char* argv[]) {
gmMachine gm;
gmFunctionObject* threadfunc = gm.AllocFunctionObject(gmThreadFunc);
gm.GetGlobals()->Set(&gm, "threadfunc", gmVariable(threadfunc));
const char* script = "text = \"hello\"; text:thread(threadfunc);";
gm.ExecuteString(script);
return 0;
}
和this一起工作
迄今为止, 大部分c++绑定的例子中, 我们都依赖于使用我们传递的object作为callback函数的参数来工作. 然而你可能更想通过绑定一个对象给函数显式或者隐式的传递this, 就像我们之前很多脚本中演示的那样. 在这一小节中, 我们将演示怎样在你的本地回调函数中访问和使用this, 同时提供一些简单的例子来演示怎样扩展你的类型.
就像你之前看到的, 每个gmThread的栈上都给"this"变量分配有一个槽位, 你可以通过gmThread的接口gmThread::GetThis()来访问它. GetThis()和Param()接口最大的不同在于GetThis()接口返回一个指向gmVariable的const指针, 而Param()接口返回值或引用. 在前面的例子中, 演示了通过this访问到了一个string变量. 通常你必须确认this变量的类型, 在gmThread.h文件中定义了一系列有用的宏帮助你检查函数的参数和this变量的类型是否是你的预期相同, 比如GM_CHECK_THIS_STRING宏, 如果this变量不是string类型, 那么它会返回一个协程异常.
就像Param()接口, 有很多辅助函数可以用来自动, 安全的将this变量从GameMonkey类型转化为本地类型. 比如ThisString接口会将this转化成本地的char*, 而ThisInt接口把this转化为本地的int
int GM_CDECL gmThisTest(gmThread* a_thread) {
GM_CHECK_THIS_STRING;
std::cout << "'this' passed as " << a_thread->ThisString() << std::endl;
return GM_OK;
}
有三个接口用于返回你的用户自定义类型
ThisUser() 返回一个void*指向实际数据和object的gmType;
ThisUserCheckType() 只有在object的gmType和参数指定的gmType一致时, 才返回void*指向实际数据;
ThisUser_NoChecks() 只简单的返回实际数据, 不做任何判断;
当然, 他们都是简单通用的转化--你可以基于这些接口来创建出你自己的想要的更复杂完善的接口.
脚本扩展Object
this机制在你想要绑定函数和数据到自定义类型时非常有用,在下面的小节中将用C++实现一个很简单的游戏实体类, 这个类允许你使用GameMonkey脚本来扩展它.
首先我们要定义自己的游戏实体类. 我们声明该实体拥有以下的属性
Variable | Read/Write? | Description |
Id | Read | Guid of the entity |
X | Read, Write | Write X position of the entity |
Y | Read, Write | Write Y position of the entity |
如果你记得在以前的文章中我曾经演示过绑定Vector到GameMonkey中, 使用原生的Vector*指针保存数据, 并绑定Dot/Index操作用来获取和修改Vector中的数据元素. 现在我们利用GameMonkey的弹性和高效率来给对象添加属性. 比如可以在脚本中向对象添加一个name的属性.
struct Entity {
Entity() : Id(0), X(0), Y(0) { }
Entity(int id): Id(id), X(0), Y(0) {}
int Id, X, Y;
};
完成绑定目标的第一步是创建一个proxy对象来保存Entity数据, 并创建一个gmTableObject对象[这个table用来保存我们向对象中新加入的属性]; 我们使用ScriptEntity类来作为proxy对象. 在这里有两个选择, 一种是入侵式的, 修改Entity使其保存一个指向ScriptEntity的指针, 另一种是使用ScriptEntity来对Entity对象进行包裹. 我在这里选择在Entity中增加一个指针指向ScriptEntity; 这样就可以保证任何使用Entity类的用户都可以拥有Entity中新加入的属性, 而且可以创建它们或者是读取任何一个已经存在的 -- 这样做的好处是允许很好的数据驱动实体.
struct ScriptEntity {
ScriptEntity(Entity& ent) : EntityObject(ent), ScriptProperties(NULL) {}
Entity& EntityObject;
gmTableObject* ScriptProperties;
};
一旦一个基本object被定义了, 需要创建一个全局函数createEntity()用来从EntityManager中分配创建一个新的Entity, 同时也会为它创建新的ScriptEntity和gmTableObject对象.
int GM_CDECL gmCreateEntity(gmThread* a_thread) {
int entityId = 0;
Entity& ent = s_EntityManager.CreateEntity(entityId);
ScriptEntity* sent = reinterpret_cast<ScriptEntity* >(s_scriptents.Alloc());
GM_PLACEMENT_NEW(ScriptEntity(ent), sent);
ent.ScriptObject = sent;
sent->ScriptProperties = a_thread->GetMachine()->AllocTableObject();
int memadjust = sizeof(gmTableObject) + sizeof(Entity) + sizeof(ScriptEntity);
a_thread->GetMachine()->AdjustKnownMemoryUsed(memadjust);
a_thread->PushNewUser(sent, s_entitytype);
return GM_OK;
}
这里选择使用GameMonkey内建的内存分配器来分配ScriptEntity, 这是一个简单的固定内存分配器, 它拥有一个freelist列表用来管理内存. 你可以选择使用new和delete操作来管理内存.
在createEntity()函数建立以后, 你就可以在脚本中创建Entity对象. 因为还没有给它们绑定任何操作, 所以它们现在还没有什么太大用处. 接下来我将给它绑定GetDot和SetDot操作符. 它们的工作原理相同, 使用Dot操作符的时候, Dot后面的文字称为操作数, 比如"obj.name"中, name就是操作数, 这个操作数是作为一个gmStringObject传递的, Entity会用这个操作数[属性名]做索引来查找到对应的属性值, 如果这个属性名解析为X, Y, Id(也就是那些已经存在于Entity类中的属性), 我们就简单的访问Entity中对应的属性. 如果访问Entity中尚不存在的属性名, 会先在ScriptEntity的gmTableObject中加入该属性, 这种实现允许我们获取和设置Entity中尚不存在的属性, 下面实现以下SetDot操作
void GM_CDECL gmOpSetDot(gmThread* a_thread, gmVariable* a_operands) {
if (a_operands[0].m_type != s_entitytype) {
a_thread->GetMachine()->GetLog().LogEntry("gmEntity:OpSetDot invalid type passed");
a_operands[0] = gmVariable::s_null;
return ;
}
ScriptEntity* sent = reinterpret_cast<ScriptEntity* >(a_operands[0].GetUserSafe(s_entitytype));
std::string propname = a_operands[2].GetCStringSafe();
if (propname == "X") {
sent->EntityObject.X = a_operands[1].GetIntSafe();
} else if (propname == "Y") {
sent->EntityObject.Y = a_operands[1].GetIntSafe();
} else if (propname == "Id") {
a_thread->GetMachine()->GetLog().LogEntry("gmEntity::OpSetDot cannot set Id");
a_operands[0] = gmVariable::s_null;
}
else {
sent->ScriptProperties->Set(a_thread->GetMachine(), propname.c_str(), a_operands[1]);
}
}
你应该记得GameMonkey脚本函数可以保存在一个gmVariable中, 那么就可以开始向Entity添加脚本函数了---即用脚本来扩展Entity, 当然可以使用C++ API来完成这一工作. 通过对象调用函数会隐式的将对象作为this传递进去.下面是用GameMonkey脚本来扩展Entity的代码示例
ent = createEntiy();
ent.name = "somebody";
ent.sayname = function() {
print("My name is : ", this.name);
}
ent.sayname();
如果使用C++ API的方式, 你可以给ScriptEntity的ScriptProperties table中添加一些脚本函数. 下面的例子演示了一个显示entity的位置, id和名字的函数. 其中name属性是在脚本中绑定的
int GM_CDECL gmEntityInfo(gmThread* a_thread) {
ScriptEntity* sent = reinterpret_cast<ScriptEntity* >(a_thread->ThisUserCheckType(s_entitytype));
if (sent == NULL) {
a_thread->GetMachine()->GetLog().LogEntry("gmEntityInfo: Expected entity type as this");
return GM_EXCEPTION;
}
std::stringstream infotext;
infotext << "EntityId: ";
infotext << sent->EntityObject.Id;
infotext << " – Position(";
infotext << sent->EntityObject.X;
infotext << ", ";
infotext << sent->EntityObject.Y;
infotext << ") – Name : ";
gmVariable namevar = sent->ScriptProperties->Get(a_thread->GetMachine(), "name");
if (namevar.IsNull() || namevar.m_type != GM_STRING) {
infotext << " [No name]";
} else {
infotext << namevar.GetStringObjectSafe()->GetString();
}
std::count << infotext.str() << std::endl;
return GM_OK;
}
下面我把gmEntityInfo函数绑定到ScriptProperties table中, 以便每次用脚本创建Entity的时候能够自动的持有该函数
sent->ScriptProperties->Set(a_thread->GetMachine(), "ShowInfo", gmVariable(a_thread->GetMachine()->AllocFunctionObject(gmEntityInfo)));
脚本中我们就可以像下面展示的这样来使用新的函数了
ent = createEntity();
ent.name = "somebody";
ent.ShowInfo();
输出:
EnityId: 1 – Position(100,0) – Name: somebody
在本小节中, 我们学习了如何使用GameMonkey脚本来扩展一个现有的本地class, 给它添加新的数据和函数成员. 有了这件武器, 你就可以自由的创建强大的实体, 并且允许脚本程序员扩展它而不用修改任何引擎代码.
今天先翻译到这里.