加载中...

「NPC 技巧」随机打乱数组 —— 如何数组传参?


0x00 背景

在开发 rAthena 的 NPC 脚本的时候,我们有时希望随机打乱一个数组。

在其他高级语言中,如 python,这是一件手到拿来的事情:

import random
array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
random.shuffle(array)    # 随机打乱数组

但在 NPC 脚本中却并不容易,主要原因有两个:

  1. NPC 脚本没有默认内置 shuffle 之类的数组操作函数,需要自己封装 function F_SHUFFLE
  2. 即使封装了 F_SHUFFLE,NPC 脚本不接受「数组变量」的入参和出参

不接受「数组变量」的原因主要还是因为变量的作用域问题:当在 F_SHUFFLE 外定义的数组、作为入参传递进 F_SHUFFLE 内的函数作用域时,因为已经脱离了原本的作用域,导致 F_SHUFFLE 内取得的值为空。

rAthena 的 NPC 作用域和我们一般认知的语言(例如 C 语言)有些区别,不能直接代入。

那是不是没有任何办法呢 ?

答案是否定的。

0x10 角色临时变量

其实只要找到一种变量的作用域,可以跨越 F_SHUFFLE 函数内外,就能实现间接传参。

在 rAthena 的 NPC 脚本中,不同类型的变量有不同的生命周期:

  • 全局变量:在整个服务器运行期间存在。例如:$global_var,变量会写入数据表 mapreg
  • 账号变量:玩家同一账号下所有角色都可以使用,重启服务器不会失效。例如:#account_var,变量会写入数据表 acc_reg_stracc_reg_num
  • 角色变量:绑定到角色,一旦角色登出或删除,变量消失。例如:char_var,变量会写入数据表 char_reg_strchar_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);


文章作者: EXP
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 EXP !
 上一篇
「NPC 技巧」利用邮件触发客户端缓存同步数据库 「NPC 技巧」利用邮件触发客户端缓存同步数据库
在 rAthena 中,当 NPC 脚本修改在线玩家的物品数据时,因为客户端缓存的存在、会导致数据库脏读脏写,本文从原理出发讲解解决方法 ...
2024-07-03
下一篇 
rAthena 的 NPC 脚本语法高亮设置指引 rAthena 的 NPC 脚本语法高亮设置指引
虽然 rAthena 的 NPC 脚本比较接近 C 语言,但苦于没有 IDE 环境,导致代码只能在 txt 中编写,体验极差 ...
2024-07-01
  目录