0x00 背景
在开发 rAthena 的 NPC 脚本的时候,我们有时希望随机打乱一个数组。
在其他高级语言中,如 python,这是一件手到拿来的事情:
import random
array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
random.shuffle(array) # 随机打乱数组
但在 NPC 脚本中却并不容易,主要原因有两个:
- NPC 脚本没有默认内置 shuffle 之类的数组操作函数,需要自己封装
function F_SHUFFLE
- 即使封装了
F_SHUFFLE
,NPC 脚本不接受「数组变量」的入参和出参
不接受「数组变量」的原因主要还是因为变量的作用域问题:当在 F_SHUFFLE
外定义的数组、作为入参传递进 F_SHUFFLE
内的函数作用域时,因为已经脱离了原本的作用域,导致 F_SHUFFLE
内取得的值为空。
rAthena 的 NPC 作用域和我们一般认知的语言(例如 C 语言)有些区别,不能直接代入。
那是不是没有任何办法呢 ?
答案是否定的。
0x10 角色临时变量
其实只要找到一种变量的作用域,可以跨越 F_SHUFFLE
函数内外,就能实现间接传参。
在 rAthena 的 NPC 脚本中,不同类型的变量有不同的生命周期:
- 全局变量:在整个服务器运行期间存在。例如:
$global_var
,变量会写入数据表mapreg
。 - 账号变量:玩家同一账号下所有角色都可以使用,重启服务器不会失效。例如:
#account_var
,变量会写入数据表acc_reg_str
或acc_reg_num
。 - 角色变量:绑定到角色,一旦角色登出或删除,变量消失。例如:
char_var
,变量会写入数据表char_reg_str
或char_reg_num
。 - 角色临时变量:在脚本执行期间存在,执行结束后销毁。例如:
@temp_char_var
。 - 范围临时变量:相当于其他语言的函数内局部变量,离开方法的作用域范围失效。例如:
.@scope_var
。 - NPC 变量:绑定到特定 NPC,只在 NPC 存在期间有效。例如:
.npc_var
。 - 副本变量:在每一个副本中独立存在,互不影响。例如:
'instance_var
。
不难发现,「全局变量」「账号变量」「角色变量」都符合我们的 “变量作用域跨越函数内外” 的要求,但是它们对这个功能而言实在太重了:
- 这些变量会读写数据表中,过多的临时数组不但会污染数据库、频繁读写也会导致性能下降
- 「全局变量」甚至在服务端重启前都不会释放内存,如果为了实现一个小功能随便定义大量临时的「全局变量」,会导致内存泄漏
除非没有更好的选择,否则我是不会采用这 3 种变量的。
「NPC 变量」和「副本变量」是两个特殊场景:
- 「NPC 变量」指的是 NPC 内的各个 Label 之间,定义为
function
的函数已经超出作用域 - 「副本变量」专用于副本 NPC 内,同样不适用
function
函数的作用域
至于「范围临时变量」是我最开始测试的,其作用域一旦进入函数内部就会失效。
此时可选的变量剩下「角色临时变量」,它是否符合要求呢 ?
我从 script_commands.txt
的内置函数 getinventorylist
中找到了提示,此函数通过「角色临时变量」返回了多个数组:
这佐证了「角色临时变量」可以跨越 F_SHUFFLE
函数内外,且不会读写数据库。
0x20 实现代码
剩下的问题是:内置函数 getinventorylist
约定了一组固定名字的出参数组;而我期望的则是 F_SHUFFLE
的入参数组和返回数组都是可以按需变化的 —— 为了可以随时打乱任何命名的数组。
这个其实不难解决,入参虽然不能传递数组,但是可以传递「数组的名字」,配合 getd
恢复数组变量、getelementofarray
获取数组元素,即可实现我的诉求。
这种做法会在
F_SHUFFLE
内部改变外部定义的数组,从程序设计理念出发是不提倡的,但此处确实不得已而为之。
以下是实现随机打乱数组的 NPC 脚本函数:
// 随机打乱数组(存在局限性,必须依附角色使用)
//============================================================
// - param: .@char_array_name$ 一维数组的名称(数组必须定义为角色变量,同时作为出/入参)
// - param: .@is_str 是否为字符串数组
// - return: null
function script F_SHUFFLE {
.@array_name$ = getarg(0);
.@is_str = getarg(1, false);
.@size = getarraysize(getd(.@array_name$ + "[0]"));
for (.@i = 0; .@i < .@size; .@i++) {
.@j = rand(.@size);
if (.@is_str) {
.@ei$ = getelementofarray(getd(.@array_name$), .@i);
.@ej$ = getelementofarray(getd(.@array_name$), .@j);
set getd(.@array_name$ + "[" + .@i + "]"), .@ej$;
set getd(.@array_name$ + "[" + .@j + "]"), .@ei$;
} else {
.@ei = getelementofarray(getd(.@array_name$), .@i);
.@ej = getelementofarray(getd(.@array_name$), .@j);
set getd(.@array_name$ + "[" + .@i + "]"), .@ej;
set getd(.@array_name$ + "[" + .@j + "]"), .@ei;
// copyarray getd(.@array_name$ + "[" + .@i + "]"), .@ej, 1;
// copyarray getd(.@array_name$ + "[" + .@j + "]"), .@ei, 1;
}
}
return;
}
这个函数存在局限性,只能打乱定义为「角色临时变量」的数组,因此需要依附于角色才能执行,简单来说就是要由玩家作为主体触发 NPC 。对于系统级的触发事件,因为没有明确绑定某个玩家,是没办法使用的这个函数的。
0x30 调用示例
0x31 打乱整型数组
setarray @tmp_int[0], 1, 2, 3, 4, 5, 6, 7, 8, 9;
callfunc("F_SHUFFLE", "@tmp_int");
0x32 打乱字符串数组
setarray @tmp_str$[0], "a", "b", "c", "d", "e", "x", "y", "z";
callfunc("F_SHUFFLE", "@tmp_str$", true);