使用lua已经1年多了, 至今还常常惊叹于其作者设计的简洁和提供给用户的一套机制, "工具就在那里摆着, 去发挥你的想象力吧"~~~ lua的接口最好的体现了提供机制而不是策略的思想. 在游戏编程精粹中, 看到一篇关于介绍脚本语言的文章, 其中不光介绍了lua, 也介绍了GameMonkey :) 大概做了一下了解, 发现国内好像尚无使用的先例, 资料也比较少, 本着学习借鉴, 开拓思路的态度, 决定翻译GameMonkey的官方资料, 希望能对需要的人有帮助. 其中也有我自己认为要详细说一下的, 提醒一下的, 用rotc注出来了
comments 注释
// 和c++一样注释到本行末
/*
和c / c++ 一样的注释块
*/
注释块会被编译器忽略掉, 它的作用是给代码做注释或者临时使一段代码失效[调试脚本时常用]
变量和常量
GameMonkey不像c, Pascal那样的强类型语言, 它更像是Basic语言.
GM中的变量是以下类型中的一个:
null -- 没有值, 这有类型
int -- 32bit的有符号整数
float -- 32bit的浮点数
string -- null结尾的ansi字符串
table -- 数组/hash容器
function -- 函数
user -- 用户自定义类型
rotc: 如何理解null 是一种类型, 而不是一个值?
对c++er 和 cer来说, null/NULL基本就是0, 或(void*)0, 它的确是一个具体的值. 这里说GM中的null是个类型, 可能会在理解上有一定的困难. 根源是静态类型和动态类型造成的. 静态类型语言中, 类型和值是分裂开来的, 对于静态类型语言中的变量i来说, 如果能够通过编译, 那么i的类型就肯定是确定的. 静态类型语言中的类型, 只是用来说明如何对&i这样一个内存地址做解释(但问题在于这个说明是在编译期就必须决定的). 也就是说c中的变量直接映射到了内存和如何解释内存. 而动态语言通过引入一个间接层, 其值的结构是 Value(type, realvalue), 也就是说, 一个变量由一个内存和附带一个指示如何解释该内存的标志(及类型)组成的. 这样的好处是显而易见的, 可以在运行时改变变量类型(也就是改变对内存的解释方式), 下面演示动态语言中如何实现变量赋值时决定类型.
比如我创造了一门动态语言, 这门语言中有2个类型, 那么这样实现
enum {null, man, woman}; // 两个类型加一个null类型
struct Value{ Value(){type = null; pData = 0;} char type; void* pData}; // 动态语言中的变量
struct Man {Man(int h, int c){housevalue = h; carvalue = c;} int housevalue; int carvalue}; // 男类型内容是房产和车产
struct Woman { Woman(char* name) {strcpy(sweetname, name);} char sweetname[12]; }; // 女类型有一个可爱的名字
在我的脚本中:
Value pp; // 定义个一个变量, 注意, 现在这个变量不是一个Man, 也不是一个Woman, [但它有类型--null, 但是它没有值]
pp = Man(5,3); //制造一个富家男, 注意pp 现在的类型由null变成man, 值是一个Man
// 实现 void operator = (Value& v, Man& m) {
v.type = man; // 赋类型
v.pData = &m; // 赋值
}
pp = Woman(“X姐"); // 制造了一个X姐[芙蓉姐, 凤姐], 注意pp现在的类型由man变成women了, 值是一个Woman
// 实现 void operator = (Value& v, Man& m) {
v.type = woman; // 赋类型
v.pData = &m; // 赋值
}
pp = null;
// 实现 ..... v.type = null;
当你掩去c++的实现时, 脚本:
Value pp;
pp = Man(5, 3);
pp = Woman(“X姐”);
pp = null;
上面就展示了如何在脚本语言中实现所谓的一个变量既能存int (Man), 又能存string(Woman), 还能只有类型没有值(type==null)的奥秘, 其实就是引入了一个间接层, 把静态语言编译时就要确定某个内存地址要怎么解释, 变成了{解释方式, 内存}这种形式, 这样的话, 可以在运行期改变解释方式和值[当然他们是匹配的], [ 可以注意到, 动态分配内存是支持这种实现的不可缺少的机制, 垃圾收集的需求也伴随而来]
最后总结: null表示不解释~~~:) 你懂的
GM中, 变量名是大小写敏感的, __t0 和 __t1保留做内部使用.
变量名 = [a..zA..Z] + [a..zA..Z_0..9]*
例子:
a = null; // a 没有值
b = 4; // b 是int类型
c = 4.4; // c 是float类型
d = “hello”; // d 是string类型
e = table(); // e是一个空的表
f = function() {}; // f 是一个函数
更多的例子:
a = ‘SMID’; // a 是一个int, 值为(‘S’<<24 | ‘M’<<16 | ‘I’<<8 | ‘D’)
b = .23; // b 是一个float
c = 2.4f; // c 是一个float
d = ‘c:\windows\sys’; // d是一个string
语言和它的标准函数总是试图保留值, 然而不是保留类型. 具体规则是当不同类型的变量在一起运算时, 高级别的类型将被保留. 类型从低级到高级的顺序是: int, float, string.
例子:
print(“hello” + 4); // 输出: hello 4, 4的类型被提高
print(2 + 5); // 输出: 7, int类型被保留
print(2.0 + 5); // 输出: 7.0, 5的类型被提高
print(sqrt(17.0)); // 输出: 4.1231, float类型被保留
print(sqrt(17)); // 输出: 4, int类型被保留
int类型赋值的例子:
a = 179; // 十进制
a = 0xB3; // 十六进制
a = 0b0011001 // 二进制
a = ‘BLCK’; // 字符转成4个byte分别赋予int的四个byte中
float类型赋值例子:
b = 45.34; // float十进制
b = .345; // float
b = 289.0; // float
b = 12.34f; // c风格float
b = 2.3E-3; // 科学计数法
字符串赋值例子:
c = “c:\\path\\file.ext”; // 标准双引, 用\做转义字符
c = ‘c:\path\file.ext’; // 和上面一样, 单引情况下, \不做转义字符用
c = “Mary says \”hello\””; // 相当于'Mary says "hello"'
c = 'Chris' 's bike'; // 相当于'Chris's bike', 也就是说在单引内部表示单引的方法是连续两个单引
c = “My ” “house”; // 相当于"My house"
基础类型可以使用标准内建库进行显示的转换, Int(), Float(), String()
例子:
a = 10;
b = a.String(); // 这样是最好的, 显示的调用类型转化函数, 返回转化后的值
b = “” + a; // 这样不好, 赋值会将a的类型提升到string, 但是效率底下
b = (10).String(); // 丑陋的
b = 10.String(); // 错误的, 编译不过, 因为编译器不认同这种语法
引用类型变量的可引用类型有String, Function, Table, User. 当这些变量被赋值时, 并不发生full copy, 而只是让变量指向具体的obj
例子:
a = table(“apple”, "orange"); // a是一个指向table的引用
b = a; // b 现在和a指向同一个table
b[1] = "banana"; // 设置b[1]
print(a[0], a[1]); // >> banana orange
print(b[0], b[1]); // >> banana orange
当一个变量被赋新值时, 该变量原来持有的值就有可能丢失掉了.
例子:
Oper = function(a, b){
return a + b
}; // Oper现在指向一个函数
Oper = “hello”; // Oper现在指向字符串, 原来的函数被丢失了
函数
语法: function(<params>) { <statements> };
一个函数体是一个值, 而函数是一个类型 {type = GM_FUNCTION, value=function...}
注意: 记住在将函数赋值给变量后面那个分号, 这是语法必须的
例子
// 将一个创建一个rect table的函数赋值给CreateRect
CreateRect = function(posX, posY, sizeX, sizeY){
rect = table(x=posX, y=posY, width=sizeX, height=sizeY);
rect.Area = function() {return .width * height; };
return rect;
};
myRect = CreateRect(0, 0, 5, 10); // 创建一个用于描述rect的table
area = myRect.Area();
// 可以用:代替.来隐式的传递一个this指针
Size = function(){
return .width * .height;
};
s = myRect:Size(); // 调用时, myRect会当做this指针传入Size中
作用域
和作用域有关的一些关键字, 语法:
global <variable>
Local <variable>
member <variable>
this
this.<variable>
.<variable>
函数中的变量.
默认情况下, 一个在函数中使用的变量就是这个函数的本地变量. 如果要声明一个全局变量, 需要使用global关键字. 访问成员变量必须通过this或者是使用member关键字声明它是一个成员变量. 在局部使用的变量可以用local关键字声明.
例子:
Access = function(){ // Access 是一个local变量, 它引用着一个函数
apple = 3; // apple 是函数的一个local变量
global apple; // 把apple声明成全局作用域
local apple; // 把apple声明成局部作用域
member apple; // 把apple声明成 this的member变量
this.apple; // 明确的访问this.apple
.apple // 隐式的访问this.apple
};
例子:
a = 13; // a 是一个local作用域变量
print(b); // b是null
global b = function() { // b是一个全局作用域的变量, 类型是GM_FUNCTION
global c = 2; // c是一个全局作用域的变量
d = 3; // d是函数局部作用域变量
{ if (c == 2)
{ local e = 3; } // e 从这一刻开始成为函数局部作用域变量, 注意没有块作用域变量
}
print(e); // e = 3
}
在查找一个变量时, 按照local 和 parameters, 然后global的顺序查找.
成员变量有微妙的不同:
h = function() { // h 是一个local变量
global a = 3; // a 是一个全局变量
member n; // n可以从this被访问和创建
d = 3; // d是函数局部作用域
this.b = 3; // b是member作用域
.b = .x + 1; // b, x都是member作用域
print(b); // b 是 null, 因为这里并没有local的b
print(n); // 就像print(this.n)一样, 因为上面显示声明过了
};
全局作用域中的语句.
x = 7; // local
global x = 8; // global
a = function(y) {
local x = 5; // function local
dostring(“print(x);”); // 这里打出8, 和lua一样, dostring总是在全局环境下编译运行的, 无法访问function的变量和parameters
};
变量可以是虚拟机全局作用域的, 也可以是某个函数作用域的, 或者是某个obj比如table的成员作用域的. 当执行一个文件或者是执行一个字符串的时候, 和lua一样, 文件或者是字符串被编译成一个无名的函数, 所以默认情况下, 其中最外层的未加特别申明的变量是该无名函数的函数作用域的.
this总是存在的. 它或者是null, 或者是一个有效的值. 你可以传递this, 或者是使用重载冒号操作符默认的this. 这一特性多用在创建诸如类似模板行为, 那些地方的obj的操作往往只有run-time时才能确认. this也用在创建线程, 例子:
obj:thread(obj.DoThings) // 开始一个线程, 并把obj作为this传递给它
obj:MakeFruit(apple, orange) // 调用MakeFruit, 并把obj当做this传给它
语法和操作符
! Not 逻辑取反
~ 每一个bit位取反
^ bit位使用与或 XOR
| bit位使用或 OR
& bit位使用与 AND
>> bit位右移
<< bit位左移
~= bit位取反赋值
^= bit位 XOR 赋值
|= bit位 OR 赋值
&= bit位 AND 赋值
>>= bit位 右移 赋值
<<= bit位 左移 赋值
= 赋值
' 单引 其中的字符会当做int值
" 双引 字符串(处理转义字符)
` 反引 字符串(不处理转义字符)
[] 方阔 用index取talbe元素
. 取table元素
: 给函数传递this
+ 数学+
- 数学-
* 数学*
/ 数学/
% 模取余
+=, –=, *=, /=, %= 数学运算并赋值
{} 界定语句块
; 语句结束标志
<, <=, >, >=, == 数学比较
&&或and 逻辑AND
|| 或or 逻辑OR
Tables 表
语法: table(<key> = <value>, ...);
table(<value>, …);
{<key>=<value>, …, };
{<value>, …, };
table可以被同时认为是array 和 map. 因为table中可以容纳data和function, 所以table也可以被认为是class, table中也可以容纳别的table, 这时它也已被认为是Tree.
初始化table的例子:
fruit = table("apple", "banana", favorite= "tomato", "cherry");
fruit = {"apple", "banana", favorite="tomato", "cherry"};
这时, fruit的样子就是:
fruit[0] = “apple”;
fruit[1] = “banana”;
fruit[2] = “cherry”;
fruit[“favorite”] = "tomato"; 也可以写作是 fruit.favorite = "tomato"
可以注意到, fruit.favorite="tomato"并没有占据 element[2], 虽然它在逻辑上应该是element[2]的位置, 但是它不是一个index索引成员, 是一个{key, value}成员.
从表中取得元素的例子.
a = thing.other; // other 是table thing中的一个成员
b = thing[“other”]; // 相当于b = thing.other
c = thing[2]; // c取得了thing中的第三个indexd索引成员
index = 3;
d = thing[index]; // 用int做下标, 就可以把table当数组访问
accoc = “fav”;
e = thing[accoc]; // 用string做下标, 就可以把table当map访问
注意, thing["Fav"]和thing["fav"]是两个不同的东西, 因为GM是大小写敏感的. 这样做设计上的考虑是:
1) 赋值可能是string, 也可能是任何类型的值.
2) 要做到大小写无关, 底层需要一些额外的工作量, 这会产生一定量的效率问题.
设置table中成员的值的例子.
thing.other = 4;
thing[3] = “hello”;
嵌套表的例子:
matrix = { {1, 2, 3,}, {4, 5, 6}, {7, 8, 9,}, } //
print(“matrix[2][1] = ”, matrix[2][1]); // 输出"matrix[2][1] = 8"
关键字if和else
语法: if ( <condition> ) { <statements> }
或者 if ( <condition> ) { <statements> } else { <statements> }
或者 if ( <condition> ) { <statements> } else if ( <condition> ) { <statements> } else { <statements> }
例子:
foo = 3;
bar = 5;
if ((foo * 2) > bar){
print(foo * 2, “is greater than”, bar);
}
else{
print(foo * 2, “is less than”, bar);
}
// 输出: 6 is greater then 5
if 会计算条件表达式的值, 并根据其结果的true/false来选择执行那一段语句.
if 在计算条件时, 会像大多数语言那样, 并且实现了短路求值, 下面是一些例子:
if (3 * 4 + 2 > 13) == if ( ( (3*4) + 2) > 13 )
if (3 > 0 || 0 > 1) 3 > 0恒真, 那么永远不会去对0 > 1求值
对c程序员的提示: 你不能把condition和一个单语句无语句块标示的statements写在同一行, 这是语法不允许的
例: if ( a > 3) b = 4; // 错误, b = 4 必须被{}包起来
关键字for
语法: for (<statement1>; <condition>; <statement2>) { <statements> }
例子:
for (index = 0; index < 6; index = index + 2){
print(“Index = ”, index);
}
输出是:
Index = 0
Index = 2
Index = 4
for 语句的执行和大多数语言一样, 循序是
1. 执行 statement1
2. 执行condition, 是false就退出for
3. 执行statements
4. 执行statement2, goto 2
关键字foreach
语法: foreach (<key> and <value> in <table>) { <statements> }
foreach (<value> in <table>) { <statements> }
例子:
fruitnveg = table("apple", "orange", favorite = "pear", yucky="turnip", "pinapple");
foreach(keyVar and valVar in fruitnveg){
print(keyVar, “=", valVar);
}
输出是:
2 = pinapple
0 = apple
favorite = pear
1 = orange
yucky = turnip
注意到遍历tale的时候, 它并没有按料想的顺序来输出. 事实上, 这种顺序会在table中的元素填入和删除时发生变化.
在foreach的每次迭代过程中, key和value将会作为循环体的local作用域变量. 在迭代过程中, 对table执行删除元素操作是安全的, 但是向table中新增元素和从table删除元素是[原文是: Although the foreach iteration is ‘delete safe’, the behaviour of adding and removing items from a table while iterating is undefined. 我理解不了delete safe 和 removing items from a table] 请大家告诉我好的理解, 我好改正
关键字 while
语法: while( <condition> ) { <statements> }
例子:
index = 0;
while ( index < 3 ) {
print("index = ", index);
index = index + 1;
}
输出:
index = 0
index = 1
index = 2
while结构先检查条件, 如果条件为真就执行循环体并反复执行这一过程直到第一次检查到条件为假. 如果一开始条件就为假, 那么循环体中的代码一次也不会执行.
关键字 dowhile
语法: dowhile (<condition>) { <statements> }
例子:
index = 0;
dowhile (index > 0) {
print("index = ", index);
}
输出:
index = 0
dowhile和while不同, 它先执行循环体, 然后检测条件已决定是否要再次执行循环体, 循环体中的代码至少执行一次.
关键字 break, continue, return
break的例子:
for (index = 0; index < 4; index = index + 1) {
if (index == 2) {
break;
}
print(“index =”, index);
}
输出:
index = 0
index = 1
continue的例子:
for (index = 0; index < 4; index = index + 1) {
if (index == 2) {
coutinue;
}
print(“index = ”, index);
}
输出:
index = 0
index = 1
index = 3
return 的例子:
Add = function(a, b) {
return a + b;
}
print(“Add res = ”, Add(4, 5));
输出:
Add res = 9
Early = function(a) {
if (a < = 0) {
print(“Dont want zero here.”);
return ;
}
print(“Above zero we handle.”);
}
Early(-2);
输出:
Dont want zero here.
break和continue用来退出或者是忽略 for, while, dowhile, foreach 循环. break会使执行流跳出循环语句块, continue导致终止本轮迭代, 并立即进行下一轮迭代, return不光是能跳出循环, 它是直接跳出当前函数.
关键字true, false, null
在GM中, true和false分别表示0和1. 除此之外没有别的意义. 注意, 和其他语言不太一样的地方
例子:
a = 3;
if (a == true) {
print(a, “==", true);
}else{
print(a, “!=", true);
}
输出:
3 != 1
null是一种类型, 而不是一个值, 它通常用来表示错误. 当它和其他类型混用时, 它的类型会被提升, 值为0. 当一个变量被声明但没有赋值时, 这个变量就是一个null. 对于table中的元素, 如果被赋值为null, 就表示这个元素被从table中删掉了.
例子:
local var;
if (var) { // var声明了但没赋值, 所以是null, 这里类型提升了, 值为0 == false
print(“var was initialised or non zero : ”, var);
} else {
print(“var is zero or null : ”, var);
}
输出:
var is zero or null : null
--------------------------------------------------- 高潮分割线 ---------------------------------------------------------
上面那些我觉得正常人1~2个小时应该能掌握了, 我翻译的昏昏欲睡了, 下面是一些GM内建的机制, 能够体现出一些特色, 这个正是我想要的:), 其实可以把下面的这些东西看做是GM的库, 也可以看成是GM内置的功能.
Thread
rotc: 这里有几点要说的
1. GM里的thread不是通常的线程, 其实就是lua里的协程.
2. GameMonkey开发的初衷有弥补当时的LuaV4没有协程的遗憾, 现在luaV5已经有了.
3. 从接口来看, GM中的协程接口更加丰富易用.
1. int thread(function a_function, …)
创建一个线程.
a_function 是线程的执行体
... 是传给a_function的参数
该函数返回一个thread id, 控制和查询线程都必须通过这个id来进行.
2. void yield()
调用该函数导致当前执行体让出对GM虚拟机的控制权.
3. void exit()
调用本函数导致当前执行体立即退出.
4. void threadKill(int a_threadId)
kill掉指定的线程, 被kill掉的线程不能再次运行.
a_threadId是要kill的线程的id, 由thread()函数返回.
如果调用threadKill()将导致当前线程被kill掉.
5. void threadKillAll(bool a_killCurrentThread = false)
kill掉所有的线程, 参数为false的话kill掉出自己外的所有线程, 否者连自己也kill掉.
6. void sleep(float a_time)
停止当前执行体指定的秒数.
7. int threadTime()
返回当前线程执行的总时间, 单位是毫秒.
8. int threadId()
返回当前线程的id
9. table threadAllIds()
用一个table返回所有的线程id.
10. void signal(var a_event)
引发一个事件. a_event是任意类型的, 表示一个事件.
11. void block(var a_event, ...)
让当前线程阻塞在一个或多个事件上. 只到事件发生, 该线程才能被转化为可执行的.
States
在游戏编程中, 常使用状态的概念来描述一个游戏实体的行为, 就是常常说到的有限状态机. 在GM中, states允许一个线程结束后马上丢弃这个线程的栈并跳到另一个执行点开始执行.
1. void stateSet(function a_function, …)
设置当前执行线程的新的状态函数.
a_function 表示要执行的状态函数.
... 表示要传给状态函数的参数.
2. function stateGet()
获取当前执行的状态函数. 如果之前没有调用过stateSet, 那么将返回null.
3. function stateGetLast()
获取当前状态的前一个状态, 可以用来得知变迁信息.
4. void stateSetExitFunction(function a_function)
设置一个函数, 该函数将在状态变迁时调用. 可以用来在进入下一个状态前本次状态函数本身的一些清理工作, 如果没有下一个状态, 那么这个函数不会被执行.
rotc: 其实这个state的实现要求虚拟机实现了尾递归, 否者在状态过多的时候会导致满栈, 实现了尾递归和协程的语言都可以做出states类似的功能, 但是GM中显示的给出的支持, 也是很方便的.
System
1. void debug()
使调试器在这里产生一个断点.
2. void assert(int a_condition)
检查a_condition, 它必须是非0值, 否者产生一个异常然后退出线程.
3. int sysTime()
返回虚拟机执行的实现, 单位是毫秒.
4. int doString(string a_script, int a_executeNow = true)
编译并执行a_script中的脚本
a_executeNow == true的话, script将马上被执行, 然后doString函数才返回, 否者返回新建的thread id.
实质的步骤是:
1. 把 a_script 编译成一个函数func
2. 调用 id = thread(func)
3. if a_executeNow == true
yield()
else
return id
5. int typeId(variable a_var)
返回a_var的类型值
6. string typeName(variable a_var)
返回a_var的类型名字
7. int typeRegisterOperator(int a_typeid, string a_opName, function a_func)
给指定的类型注册一个操作
a_typeid 目标类型
a_opName 操作名
a_func 现实的操作函数
返回1成功, 0失败.
a_opName的取值: getdot, setdot, getind, setind, add, sub, mul, dov, mod, inc, dec, bitor, botxor, shiftleft, shiftright, bitinv, lt, gt, lte, gte, eq, neq, neg, pos, not
8. int typeRegisterVariable(int a_typeid, string a_varName, variable a_var)
给指定的类型注册一个变量, 使用(type).varname的方式就可以获得这个变量
a_typeid 目标类型
a_varName 要加的变量名
a_var 要加的变量
返回1成功, 0失败.
9. int sysCollectGarbage(int a_forceFullCollect = false)
如果当前内存使用量超过了指定的内存使用量, 那么执行垃圾回收.
a_forceFullCollect 如果垃圾回收可用的话, a_forceFullCollect=true将马上开始执行
返回1表示垃圾回收执行了, 其他情况返回0.
10. int sysGetMemoryUsage()
返回当前的内存使用量, 单位byte.
11. void sysSetDesiredMemoryUsageHard(int a_desired)
设置内存使用量硬限制. 在垃圾回收时会根据这个值来决定要不要执行一次完整的回收.
a_desired 内存使用硬限制, 单位是byte.
12. void sysSetDesiredMemoryUsageSoft(int a_desired)
设置内存使用量软限制. 在垃圾回收时会根据这个值来决定是否开始增量回收. soft值必须小于上面的hard值, 谢谢
a_desired 内存使用软限制, 单位是byte.
13. void sysSetDesiredMemoryUsageAuto(int a_enable)
开启或者关闭在接下来的垃圾收集中是否能自动调整内存限制.
a_enable 1 开启 0 关闭
14. int sysGetDesiredMemoryUsageHard()
获取内存使用量硬限制, 单位是byte. 注意, 这个值是用在开始一次完整的垃圾回收前的检测.
15. int sysGetDesiredMemoryUsageSoft()
获取内存使用量软限制, 单位是byte. 注意, 这个值是用在开始增量垃圾回收前的检测.
16. int sysGetStatsGCNumFullCollects()
返回虚拟机执行完整垃圾回收的次数.
17. int sysGetStatsGCNumIncCollects()
返回虚拟机执行增量垃圾回收的次数. 注意在restart的那一次回收中, 这个值会+2.
18. int sysGetStatsGCNumIncWarnings()
返回GC 或者VM因为配置的问题[soft和hard限制]而导致的警告的数量. 如果这个数庞大而且急速增加, 那么gc的软硬内存限制应该重新配置以获得更好的性能表现. 这些警告的出现一般意味着gc次数过于平凡或不足. 如果这个值很小, 或者是增长很慢, 那么不用去担心它. 可以阅读介绍GM的gc的文档[翻译完这个, 我就翻译GM gc的文档]来获取关于gc话题的很多信息. 我们将在以后的版本中改进这个函数, 以便让它的意义很明确易懂.
表操作
1. int tableCount(table a_table)
计算table中元素的个数.
2. table tableDuplicate(table a_table)
返回传入table的一个副本.
我测过了, copy的深度就是a_table的元素这一层, 比如
t1={a=9}; t2={uu=t1, b=45};
t3 = tableDuplicate(t2);
t3.b = 78;
t3.uu.a = 80;
print(“t2.b = ”, t2.b); // t2.b = 45
print(“t3.b = ”, t3.b); // t3.b = 78
print(“t2.uu.a = ”, t2.uu.a); // t2.uu.a = 80
print(“t3.uu.a = ”, t3.uu.a); // t3.uu.a = 80
啥内涵大家一看就明白了
------------------------------------------华丽的风格线-------------------------------------------
绑定C函数到GM脚本中
C函数可以绑定到类型上, 也可以绑定到一个全局变量.
一个可以绑定到GM中的C函数的原型是:
int GM_CDECL gmVersion(gmThread* a_thread)
a_thread->GetNumParams() 可以获得参数的个数
a_thread->Param*() 获取各个参数
a_thread->GetThis() 访问this
a_thread->Push*() 向脚本中返回值
还有一些有用的宏和简写的功能函数.
C函数的返回值描述如下:
GM_OK 函数执行成功
GM_EXCEPTION 函数执行失败, 在函数运行的thread中产生一个运行时异常
当然函数也可能返回一些控制thread行为的值, 比如GM_SYS_SLEEP, GM_SYS_YIELD, GM_SYS_KILL, 这些值可以让脚本的高级用户实现和修改虚拟机的行为. 用户拥有强大的控制权, 可以更高效的实现参数变量, 重载函数(通过支持不同类型的参数), 检查错误或无效的输入.
一个GM操作符绑定函数的原型是:
void GM_CDECL dunc(gmThread* a_thread, gmVariable* a_operands)
a_operands[0] 是左参数
a_operands[1] 是右参数
a_operands[0] 同时也是返回值
如果操作符函数不能执行该操作(比如错误的参数类型等), 就把a_operands[0]置为null
对于二元操作符来说, 比如O_MUL, 调用操作符函数时将选择两个参数类型较高的参数的绑定函数. NOT是一个一元操作符(这时将使用a_operands[0].m_type的绑定函数). 这一点和c++是不一样的, 在c++中, 如果你创建了一个类 Vec3, 那么Vec3 * float 的运算就需要重载一个*操作符, 而float * Vec3需要重载一个全局的友元函数. GM这样做是为了降低原生类型的处理代价和易于用户定义类型的扩展. 所以原生的int 和 float 类型不需要在意那些比他们高级的类型, 但是用户自定义类型例如Vec3可以很有弹性的和低级类型一起工作, 它的绑定函数将被调用.
可能发生冲突的地方就是当用户自定义类型之间发生运算时, 如果用户知道注册的顺序的话, 他们可以依据这个来编码, 否者可能要实现同样的操作符函数来保证不会发生因为注册顺序而导致的问题. 两个用户类型可以给一个操作符绑定同样的操作符函数, 这样可以避免不必要的重复.
rotc: 上面这两段话我翻译的不好, 先放着, 等对这部分知识有了更深的理解再来修改
例子1, 实现一个可以注册到GM中的C函数, 比较简单, 不写注释了
// int GetHealth(GObj* a_obj, int a_limit)
int _cdecl GetHealth(gmThread* a_thread) {
GM_CHECK_NUM_PARAMS(2);
GM_CHECK_USER_PARAM(GObj::s_scrUserType, userObj, 0);
GM_CHECK_INT_PARAM(limit, 1);
Gobj* gob = (Gobj* )userObj->m_user;
if (gob->m_health > a_limit) {
gob->m_health = a_limit;
}
a_thread->PushInt(gob->m_health);
return GM_OK;
}
例子, 向GM中导入一个函数, 使得在GM中可以使用sqrt(56)或者sqrt(67.8), 过程比较简单就不写注释了
int __cdecl gmfSqrt(gmThread* a_thread) {
GM_CHECK_NUM_PARAMS(1);
if (a_thread->ParamType(0) == GM_INT) {
int intVal = a_thread->Param(0).m_value.m_int;
a_thread->PushInt((int)sqrt(intVal));
return GM_OK;
} else if (a_thread->ParamType(0) == GM_FLOAT) {
float floatVal = a_thread->Param(0).m_value.m_float;
a_thread->PushFloat(sqrtf(floatVal));
return GM_OK;
}
return GM_EXCEPTION;
}
static gmFunctionEntry s_mathLib[] = {
{"sqrt", gmfSqrt}, };
machine->RegisterLibrary(s_mathLib, sizeof(s_mathLib) / sizeof(s_mathLib[0]));
例子, 为String类型加上一个Compare操作的演示, 使得可以在GM中使用 "hihi".Compare("hihi") , 因为比较重要, 给出完整代码.
#include "gmThread.h"
int GM_CDECL gmfStringCompare(gmThread* a_thread)
{
GM_CHECK_NUM_PARAMS(1);
// Compare的参数必须是string, 因为这个函数预期将进行字符串的比较
if (a_thread->ParamType(0) == GM_STRING)
{
// 获取调用Compare的变量
const gmVariable* var = a_thread->GetThis();
// 这个变量一定也是一个string
GM_ASSERT(var->m_type == GM_STRING);
// gm str ----> c str
gmStringObject* obj = (gmStringObject* )GM_OBJECT(var->m_value.m_ref);
const char* thisStr = (const char* )*obj;
const char* otherStr = a_thread->ParamString(0);
// 具体的操作
a_thread->PushInt(strcmp(thisStr, otherStr) ? 0 : 1);
return GM_OK;
}
return GM_EXCEPTION;
}
static gmFunctionEntry s_stringlib[] = {
{"Compare", gmfStringCompare},
};
int main(int argc, char* argv[])
{
// 先创建虚拟机
gmMachine machine;
// 注册到虚拟机
machine.RegisterTypeLibrary(GM_STRING, s_stringlib, 1);
// 好了可以用了:)
machine.ExecuteString("print("res = ", \"hihi\".Compare(\"hihi\"));");
getchar(); // Keypress before exit
return 0;
}
程序执行结果是输出 res = 1
从C中调用GM脚本
从C中调用GM脚本时使用gmCall辅助类会让整个事情变得很简单, 下面就是一个例子:
#include “gmCall.h” // 要使用gmCall就必须包含这个头文件
gmMachine machine; // 初始化一个GM虚拟机
// 我们要调用的函数是: global Add = function(a, b) { return a + b; };
gmCall call;
int resultInt = 0;
if (call.BeginGlobalFunction(&machine, “Add”)) {
call.AddParamInt(3);
call.AddParamInt(5);
call.End();
call.GetReturnedInt(resultInt); // 取结果
}
警告: 如果你从函数中返回一个string, 那么你就马上使用它, 或者是把它copy出来, 不要长期的持有这个指针. 因为这个字符串不会一直有效, 说不定在下一轮的垃圾收集中就把它回收了, 这样的话, 你再次使用它的指针时就很危险了.
游戏对象扩展
我怎样才能扩展GM, 向它中添加一个我自己定义的类型, 就像table那样子.
怎样在GM中表达一个game obj?
下面的代码就是完整的将GameObject类型导入到GM中, 包含创建, 访问, 内存管理的各个方面
struct GameObject {
gmTableObject* m_table; // 容纳表功能
gmUserObject* m_userObject;
static gmType s_typeId; // 存储自己定义的类型
};
gmType GameObject::s_typeId = GM_NULL;
#if GM_USE_INCGC
static bool GM_CDECL GCTrace(gmMachine* a_machine, gmUserObject* a_object, gmGarbagCollector* a_gc, const int a_workLeftToDo, int& a_workDone) {
GM_ASSERT(a_object->m_userType == GameObject::s_typeId);
GameObject* object = (GameObject* ) a_object->m_user;
if (object->m_table)
a_gc->GetNextObject(object->m_table);
a_workDone += 2;
return true;
}
static void GM_CDECL GCDestruct(gmMachine* a_machine, gmUserObject* a_object) {
GM_ASSERT(a_object->m_userType == GameObject::s_typeId);
GameObject* object = (GameObject* )a_object->m_user;
object->m_table = NULL;
}
#else
// 垃圾回收的标记函数
void GM_CDECL GameObjectMark(gmMachine* a_machine, gmUserObject* a_object, gmuint32 a_mark) {
GM_ASSERT(a_object->m_userType == GameObject::s_typeId);
GameObject* obecjt = (GameObject* )a_object->m_user;
object->m_table->Mark(a_machine, a_mark);
}
// 垃圾回收的回收函数
void GM_CDECL GameObjectGC(gmMachine* a_machine, gmUserObject* a_object, gmuint32 a_mark) {
GM_ASSERT(a_object->m_userType == GameObject::s_typeId);
GameObject* object = (GameObject* )a_object->m_user;
object->m_table.Destruct(a_machine);
delete object;
}
#endif
// 设置一个用来描述类型的字符串以便在调用"AdString"得到它
static void GM_CDECL AsString(gmUserObject* a_object, char* a_buffer, int a_bufferLen) {
GM_ASSERT(a_ojbect->m_userType == GameObject::s_typeId);
GameObject* object = (GameObject* ) a_object->m_user;
char mixBuffer[128];
sprintf(mixBuffer, “GameObject Cptr = %x”, object);
int mixLength = strlen(mixBuffer);
int useLength = GM_MIN(mixLength, a_bufferLen - 1);
GM_ASSERT(useLenght > 0);
strncpy(a_buffer, mixBuffer, useLength);
a_buffer[useLengrh] = 0;
}
// get dot操作符用来访问table
void GM_CDECL GameObjectGetDot(gmThread* a_thread, gmVariable* a_operands) {
GM_ASSERT(a_operands[0].m_type == GameObject::s_typeId);
gmUserObject* user = (gmUserObject* )GM_OBJECT(a_operands[0].m_value.m_ref);
GameObject* object = (GmaeObject* )user->m_user;
a_operands[0] = object->m_table->Get(a_operands[1]);
}
// set dot操作符用来访问table
void GM_CDECL GameObjectSetDot(gmThread* a_thread, gmVariable* a_operands) {
GM_ASSERT(a_operands[0].m_type == GameObject::s_typeId);
gmUserObject* user = (gmUserObject* )GM_OBJECT(a_operands[0].m_value.m_ref);
GameObject* object = (GameObject* )user->m_user;
object->m_table->Set(a_thread->GetMachine(), a_operands[2], a_operands[1]);
}
// 从脚本中创建GameObject
// 注意: 游戏中像这样直接创建对象实体并不常见, 还有, 可能并不像保存对象的c指针, 取而代之的是保持一个32bit的UID来代表对象, 在使用的时候通过UID来查询和验证对象
int GM_CDECL CreateGameObject(gmThread* a_thread) {
GameObject* object = new GameObject();
object->m_table = a_thread->GetMachine()->AllocTableObject();
object->m_userObject = a_thread->CreateUser(object, GameObject::s_typeId);
return GM_OK;
}
// 获取一个object, 这种情况下的obj在c和脚本中是一对一的.
int GM_CDECL GetGameObject(gmThread* a_thread) {
GameObject* foundObj = NULL;
GM_CHECK_NUM_PARAMS(1);
if (a_thread->ParamType(0) == GM_STRING) {
// todo: foundObj = FindByName(a_thread->ParamString(0));
// 如果找到的话
a_thread->PushUser(foundObj->m_userObject);
a_thread->PushNull();
return GM_OK;
}
else if (a_thread->ParamType(0) == GM_INT) {
// todo: foundObj = FindById(a_thread->ParamInt(0));
// 如果找到的话
a_thread->PushUser(foundObj->m_userObject);
a_thread->PushNull();
return GM_OK;
}
return GM_EXCEPTION;
}
// 注册函数
gmFunctionEntry regFuncList[] = {
{"GameObject", CreateGameObject},
}
// 向虚拟机注册类型, 假定虚拟机已经构造好了
// 1. 注册新类型
GameObject::s_typeId = machine.CreateUserType(“GameObject”);
// 2. 注册垃圾回收要用到的
#if GM_USE_INCGC
a_machine->RegisterUserCallbacks(GameObject::s_typeId, GameObjectTrace, GameObjectDestruct, AsString);
#else
a_machine->RegisterUserCallbacks(GameObject::s_typeId, GameObjectMark, GameObjectGC, AsString);
#endif
// 为类型绑定get dot操作
machine.RegisterTypeOperator(GameObject::s_typeId, O_GETDOT, NULL, GameObjectGetDot);
// 为类型绑定set dot操作
machine.RegisterTypeOperator(GameObject::s_typeId, O_SETDOT, NULL, GameObjectSetDot);
// 注册函数
machine.RegisterLibrary(regFuncList, sizeof(regFuncList) / sizeof(regFuncList[0]));
虚拟机的回调
如果一个应用程序拥有它自己的gmObject, 它必须让垃圾回收器知道这个对象正在被使用. 要做到这一点, 你需要在虚拟机回调中捕获MC_COLLECT_GARBAGE 消息, 这个回调发生在垃圾回收器开始扫描根时. 一个让gc正确管理c++持有的gmObject的代换方案是使用gmMachine::AddCPPOwnedGMObject() 和 gmMachine::RemoveCPPOwnedGMObject(). 第三种方法是使用gmGCRoot<>指针来实现.你可以通过查阅GM gc的文档来获取更多这方面的知识.
应用程序可能希望在thread创建和销毁时执行一些动作, 比如管理这个thread专有的object等. 此外, 应用程序可能希望将thread的异常信息导入到error stream中. 下面是一些列子.
// 假定你在别处已经建立了虚拟机, 现在你要注册callback函数
gmMachine::s_machineCallback = ScriptCallback_Machine;
//
bool GM_CDECL ScriptCallback_Machine(gmMachine* a_machine, gmMachineCommand a_command, const void*a_context) {
switch (a_command) {
case MC_THREAD_EXCEPTION: {
// dump 异常信息导到标准输出
bool first = true;
const char* entry;
while ( (entry = a_machine->GetLog().GetEntry(first)) ) {
fprintf(stderr, “%s“, entry);
}
a_machine->GetLog().Reset();
}
break;
case MC_COLLECT_GARBAGE": {
#if GM_USE_INCGC
gmGarbageCollector* gc = a_machine->GetGC();
// 对于所有的c 拥有的obj
// gc->GetNextObject(obj);
#else
gmuint32 mark = *(gmuint32 *)a_context;
// 对于所有的c 拥有的obj
// if (object->NeedsMark(mark)) {
// object->GetTableObject()->Mark(a_machine, mark);
// }
#endif
}
break;
case MC_THREAD_CREATE: {
// 线程创建时的回调
}
break;
case MC_THREAD_DESTROY: {
// 线程销毁时的回调
}
break;
}
return false;
}
翻译后记
前前后后翻译了一周多, 终于算是告一段落了, 通过翻译, 增加了对GameMonkey的一些理解.
因为它是从lua发展起来的, 有很多概念有一些像, 但是经过仔细的观察和研究代码, 发现GM除了借鉴了一些lua的概念, 从实现上和lua是完全不一样的. 比如元方法的实现等等. GM使用起来将会感觉更加复杂, 有很多问题都需要去coding解决, 而不像lua那样美丽:) 但是从另外一方面来讲, GM的确是给了程序足够的控制力, 的确称的上是一门面向程序员的语言.
翻译的比较匆忙, 有什么错尽管指出:) 谢谢