博客

  • [教程] 代码优化 #1

    本文包含以下技巧:

    • 数组比普通变量慢
    • 当提前知道函数名时,不要使用 CallLocalFunction 和 funcidx
    • 原生函数比 Pawn 代码快得多
    • 循环中的条件
    • 将多个变量赋值为相同值
    • 延迟声明局部变量
    • 简化并改写数学表达式以避免昂贵操作
    • memcpy、strfind 等也适用于数组
    • 使用 CallRemoteFunction 真的值得吗?
    • 多次访问数组元素
    • 不要在表达式中混合浮点数和整数(由 Mauzen 贡献)
    • 不必要地使用 Streamer
    • 函数的良好与不良使用(优化 2D 数组操作代码)

    其中一些技巧会带来显著改进,而有些则不然。你可以忽略一些次要优化,并优先编写可读性强的代码

    优化技巧 1: 数组比普通变量慢

    以下代码效率低下:

    代码:

    new Float:pos[3];
    GetPlayerPos(playerid, pos[0], pos[1], pos[2]);

    这是上述代码的汇编版本:

    代码:

    zero.pri
    addr.alt fffffff4
    fill c ;These 3 instructions are responsible for zeroing all the array elements
    break  ; 38
    addr.pri fffffff4 ;Get the address of the array
    add.c 8 ;Add the index (index 2 means 2*4 bytes ahead)
    load.i ;This will get the value stored at that address
    push.pri ;Now push the argument
    addr.pri fffffff4 ;Same as above
    add.c 4
    load.i
    push.pri
    addr.pri fffffff4 ;Same as above
    load.i
    push.pri

    现在,这是等效的更高效代码:

    代码:

    new Float:x, Float:y, Float:z;
    GetPlayerPos(playerid, x, y , z);

    这是汇编版本:

    代码:

    push.c 0 //Making room for the variables on the stack
    push.c 0
    push.c 0
    push.adr fffffff4 //Pushing the arguments
    push.adr fffffff8
    push.adr fffffffc

    当你想访问数组元素时,编译器使用以下算法:

    第一个元素的地址 + 4*索引 = Array[Index] 存储的位置(此公式仅适用于一维数组)

    计算数组元素的地址后,即可检索元素中存储的数据。

    这并不意味着你不能使用数组。你必须明智地使用数组。当可以使用普通变量简单实现时,不要无缘无故创建数组。

    在我看来,使用 x、y、z 实际上比使用数组 pos[3] 更具可读性。

    速度测试:

    数组(10 次赋值):2444,2448,2473

    非数组(10 次赋值):972,975,963

    速度测试代码:http://pastebin.com/aMkNtaC2

    非数组版本比数组版本快 2.5 倍。

    优化技巧 2: 当提前知道函数名时,不要使用 CallLocalFunction 和 funcidx

    你知道 CallLocalFunction 和 funcidx 是慢函数吗?它们非常慢,因为它们需要在所有公共函数列表中检查你作为参数传递的函数名。这意味着大量内部 strcmp 操作。

    代码:

    if(funcidx("OnPlayerEatBanana") == -1)
    你实际上不需要那行代码。你可以用”0″指令实现相同功能。如果你已经知道函数名,只需使用预处理器指令来检查函数是否存在。

    代码:

    #if defined OnPlayerEatBanana
    //OnPlayerEatBanana has been declared
    #endif

    代码:

    if(CallLocalFunction("OnPlayerEatBanana","ii",playerid,bananaid"))
    你可以这样写:

    代码:

    #if defined OnPlayerEatBanana
    if(OnPlayerEatBanana(playerid,bananaid))
    #else
    //That function hasn't been declared
    #endif

    速度测试:(1 个零参数的公共函数)

    直接调用:204,226,218

    CallLocalFunction:1112,1097,1001

    请注意,这是 CallLocalFunction 的最佳情况。在现实中,由于有许多公共函数,CallLocalFunction 会慢得多。

    优化技巧 3: 原生函数比 Pawn 代码快得多

    当有原生函数可以实现时(或使用原生函数组合),避免创建自己的函数。

    原生函数快得多的原因是,原生函数直接由你的计算机执行,而所有 Pawn 代码都在虚拟机中执行。对于每个 Pawn 指令,AMX 机器(虚拟计算机)必须解码指令、获取操作数,然后执行指令。解码和获取操作数会消耗一些 CPU。

    代码:

    stock strcpy(dest[], src[], sz=sizeof(dest))
    {
      dest[0] = 0;
      return strcat(dest,src,sz); //Notice that I have used strcat instead of writing my own loops
    }

    速度测试:

    基于循环的 strcpy 与原生 strcat

    这里是两个等效的 strcpy 函数。

    http://pastebin.com/Y7RJ21tw

    原生:697,700,718,705

    非原生:5484,5422,5507,5562

    优化技巧 4: 循环中的条件

    我不知道我已经告诉过人们多少次了,但仍有一些人没有进行这个简单优化。

    代码 1:

    代码:

    for(new i = 0;i <= GetPlayerPoolSize();i++) {}
    代码 2:

    代码:

    for(new i = 0,j = GetPlayerPoolSize();i <= j;i++) {}
    在第一个代码中,每次迭代都会调用 GetPlayerPoolSize。在我们的时间框架中,GetPlayerPoolSize 每次调用都返回常量值。那么为什么每次迭代都调用 GetPlayerPoolSize?

    第二个代码避免了这一点。它创建一个局部变量来存储 GetPlayerPoolSize 返回的值,并在条件中使用它。因此只调用函数一次,避免函数开销。

    速度测试:

    优化后:1102,1080,1069,1091

    未优化:2374,2359,2429,2364

    测试代码:http://pastebin.com/SLZDGRG4

    虽然在上述情况下改进可能相对于循环内部代码微不足道,但有时你会使用更慢的函数。

    代码:

    for(new i = 0; i < CallRemoteFunction("GetPlayersInTeam", "i", TEAM_ID); i++)
    {
     
    }

    优化技巧 5: 将多个变量赋值为相同值 & 使用 memset

    代码 1:

    代码:

    x = abc;
    y = abc;
    z = abc;

    代码 2:

    代码:

    x =
    y =
    z = abc;

    你认为哪个代码更快?

    代码 1:

    代码:

    load.pri c ;Get abc
    stor.pri 8 ;Store it in X
    break  ; 20
    load.pri c ;Get abc
    stor.pri 4 ;Store it in Y
    break  ; 34
    load.pri c ;Get abc
    stor.pri 0 ;Store it in Z

    代码 2:

    代码:

    load.pri c ;Get abc
    stor.pri 0 ;Store in X
    stor.pri 4 ;Store in Y
    stor.pri 8 ;Store in Z

    看到区别了吗?第一个代码有额外无用的指令,它反复获取 abc,而它已经存在;第二个版本只获取 abc 一次,并设置 x、y、z。

    显而易见,代码 2 更快,但这可能无关紧要。

    当你有大数组需要设置为零、一或其他值时,使用 memset

    速度测试:

    使用 memset 将 100 个元素的三维数组的所有元素设置为零:363,367,372

    使用 for 循环将 100 个元素的三维数组的元素设置为零:6662,6642,6687

    优化技巧 6: 延迟声明局部变量

    我见过一些脚本将所有局部变量放在函数顶部,尽管有些变量有时才需要。示例应该能说明问题。

    不良代码:

    代码:

    public OnPlayerDoSomething(playerid)
    {
      new actionid = GetPlayerAction(playerid), pee_id, peed_on_whome, amount_of_pee;
      if(actionid == PLAYER_PEE)
      {
      }
    }

    良好代码:

    代码:

    public OnPlayerDoSomething(playerid)
    {
      new actionid = GetPlayerAction(playerid);
      if(actionid == PLAYER_PEE)
      {
      new pee_id,peed_on_whome,amount_of_pee;
      }
    }

    如果你阅读了前面的提示,你现在应该知道,当创建局部变量时,编译器首先在栈中为其创建空间,然后将其初始化为零。

    所以,如果你不确定是否会使用局部变量,就不要简单地创建它们。第二个代码仅在需要时创建局部变量,而第一个代码即使可能不使用也会创建它们。

    这对少数变量的性能没有显著影响,但它提高了代码的可读性。

    优化技巧 7: 简化并改写数学表达式以避免昂贵操作

    我在编写程序时总是保持笔和纸在桌子上。我在纸上写方程,进行一些移位和更改,得到更简单的方程。

    这是一个经典示例,它将提升此代码段的性能:

    代码:

    new Float:x,Float:y,Float:z;
    GetPlayerVelocity(playerid,x,y,z);
    if(floatsqrt( (x*x) + (y*y) + (z*z)) > 5.0)

    代码:

    new Float:x,Float:y,Float:z;
    GetPlayerVelocity(playerid,x,y,z);
    if( ((x*x) + (y*y) + (z*z)) > 25.0)

    你注意到变化了吗?

    我在 if 语句的条件两边平方,消除了慢函数 ‘floatsqrt’。

    另一个示例:

    代码:

    for(new i = 0, j = GetTickCount(); i < 10; i++)
    {
      if( j - LastTick[i] > MAX_TIME_ALLOWED)
      {
      }
    }

    代码:

    for(new i = 0, j = GetTickCount() - MAX_TIME_ALLOWED; i < 10; i++)
    {
      if(j > LastTick[i])
      {
      }
    }

    哇,我从条件中移除了 MAX_TIME_ALLOWED。现在减法只执行一次,而第一个代码中每次都执行。即使这个改进无关紧要,除非你有消耗大量 CPU 的操作。

    优化技巧 8: memcpy、strfind 等也适用于数组

    毕竟字符串和数组是一回事。唯一的区别是字符串以空字符终止,而普通数组没有。

    代码:

    new DefaultPlayerArray[100] = {1,2,3,4,5,6,7,8,9,10};
    new PlayerArray[MAX_PLAYERS][100];
    for(new i = sizeof(DefaultPlayerArray); i != -1; i--)
    {
      PlayerArray[playerid][i] = DefaultPlayerArray[i];
    }

    这是另一个等效代码:

    代码:

    memcpy(PlayerArray[playerid], DefaultPlayerArray, 0, sizeof(DefaultPlayerArray)*4, sizeof(PlayerArray[]));
    我对两个代码进行了基准测试,结果如下:

    循环版本:

    4286ms

    4309ms

    4410ms

    memcpy 版本:

    60ms

    62ms

    60ms

    同样,你可以使用 strfind、strmid 和许多其他字符串函数处理数组。唯一的问题是,当字符串函数在数组中找到元素 ‘0’ 时,函数会终止,因为值 0 表示 ‘\0’,即空字符。

    优化技巧 9: 使用 CallRemoteFunction 真的值得吗?

    首先,我想说 CallRemoteFunction 非常慢,必须尽可能避免。CallRemoteFunction 通常用于在其他脚本中有反作弊时更新玩家变量。

    你有没有想过在每个脚本中都有反作弊?我实际上在游戏模式中有一个反作弊,确保修改的数据不更新到数据库中;在管理过滤脚本中另一个反作弊处理对作弊的行动(独立工作)。

    为什么有两个反作弊?我们有两个选择,要么创建两个反作弊,要么使用 CallRemoteFunction 更新玩家变量。

    有时有两个独立的反作弊更快,事实上,有些反作弊检查只需 CallRemoteFunction 调用更新函数时间的四分之一。

    如果你在每个脚本中计算一些玩家变量也没关系。它比在一个脚本中更新并使用 CallRemoteFunction 访问要好得多。

    优化技巧 10: 多次访问数组元素

    让我们用一个示例来理解我们正在讨论的内容:

    代码:

    new val = value[x][y][z];
    for(new i = 50; i != -1; --i) Arr[i] = val;

    代码:

    for(new i = 50; i != -1; --i) Arr[i] = value[x][y][z];
    你认为哪个更快?

    如果你仔细阅读了技巧 #2,第一个更快。你知道从数组索引计算正确地址需要一些时间。在第二个代码中,每次将值复制到 Arr 时都会进行地址计算,而在第一个情况下,我们只计算地址一次。

    takeaway 信息是,如果你将多次访问数组元素,则在局部变量中创建数组元素的临时副本,并使用局部变量。

    速度测试:

    代码 1:2280,2330,2350

    代码 2:8008,8183,8147

    优化技巧 11: 不要在表达式中混合浮点数和整数(由 Mauzen 贡献)

    也许这个太简单了,但我至少想添加它,因为我经常看到人们犯这个”错误”。

    永远不要混用浮点数和整数(即使没有标签不匹配警告)。始终在单个语句中使用相同的数据类型。

    例如:

    代码:

    new Float:result = 2.0 + 1;
    // 被编译为
    new Float:result = 2.0 + float(1);
    // 这比以下慢得多
    new Float:result = 2.0 + 1.0;

    优化技巧 12: 不必要地使用 Streamer

    每个人都习惯使用 Streamer,即使只需要 10 或 20 个地图图标、50 个对象等。

    你知道什么是 Streamer 吗?Streamer 是一个插件/包含文件,允许你绕过 SAMP 限制。SAMP 允许最多 1000 个对象,你不能超过这个数量。

    Streamer 通过在玩家进入对象绘制距离时创建对象,并在没有玩家靠近时销毁对象来绕过限制。所以基本上,Streamer 在需要时创建对象,并在不需要时销毁它们。这样它允许你超过 SAMP 限制。

    当你使用 Streamer 函数,如 CreateDynamicObject 时,Streamer 并不真正创建对象。它将对象信息(X,Y,Z,RotX,RotY,RotZ….)添加到对象数据库中。经过一定数量的服务器 tick/周期后,它遍历数据库中的所有对象,检查是否有玩家靠近对象,并在需要时创建它。

    你可以在 这里 看到 Streamer 将对象信息添加到数据库。

    玩家更新从 这里 开始。

    这是负责更新对象的函数。

    当你少于 1000 个对象时,使用 Streamer 有意义吗?

    你真的需要 Streamer 吗?

    不!

    如果你确定不会超过 SAMP 限制,那么就不需要使用 Streamer。

    这带来了一个新问题,假设你的现有版本有 500 个对象,但你要更新脚本需要 1500 个对象。那么现在你需要将所有 SAMP 对象原生转换为 Streamer 原生吗?

    如果你的初始版本写得聪明,就不需要。

    这是我做的:

    代码:

    #define CreateDynamicObject CreateObject
    现在你可以在代码中使用 CreateDynamicObject,即使你没有 Streamer。

    当你知道需要 Streamer 时,只需移除定义并包含 Streamer。

    一个更聪明的方法是为位于热门区域的对象使用 CreateObject,例如生成点,你可以假设玩家几乎总是存在于该位置。对于位于偏远位置且玩家很少访问的对象,你绝对应该使用 CreateDynamicObject,因为这些对象不需要一直创建,而热门对象无论如何都会存在(即使使用 Streamer,所以为这样的对象使用 Streamer 不值得成本)。

    你必须为许多对象这样做才能看到合理的改进,因为 Streamer 是插件,因此它比 Pawn 代码快得多。

    同样,你可以为其他原生这样做。

    优化技巧 13: 函数的良好与不良使用(优化 2D 数组操作代码)

    一个常见神话是许多人相信”函数调用非常昂贵”,这不是真的。事实上,原始函数调用(空)比解引用 2D 数组快很多倍。

    代码:

    native SLE_algo_foreach_list_init(list:listid, &val);
    native SLE_algo_foreach_list_get(feid);
    #define foreach::list(%0(%1)) for(new %1, fel_%0@%1_id = SLE_algo_foreach_list_init(%0, %1); SLE_algo_foreach_list_get(fel_%0@%1_id);)

    如果你仔细看,它只是调用函数来获取值,这比解引用数组快。要完全消除疑虑,该函数定义在插件中,否则由于明显原因它不会更快,因为里面确实使用了数组,但那是插件内部。

    这个例子只是为了说明函数调用相对于你编写的其他代码并不那么昂贵。这意味着当必要时,你应该为执行特定任务的大块代码创建函数,特别是如果它提高了代码的可读性。

    然而,函数的误用可能代价高昂,特别是在循环中,这在前面已经讨论过。

    这里是一个使用 1D/2D 数组会更好的情况。

    引用:

    引用:最初由 Vince 发表于
    我经常看到的一件事,没有在第一帖中提到,是过度使用 GetPlayerName。玩家在连接时不能更改名称(当然,SetPlayerName 除外),所以反复调用该函数似乎是多余的。我会说使用包装器甚至更糟。只需在玩家连接时将其存储在变量中,然后在所有地方使用该变量。GetPlayerIP 也是如此。

    另一个神话是”创建函数总是更慢”,这在处理多维数组时完全不对。如果做得正确,创建函数实际上可以显著提高性能。

    代码:

    for(new y = 0; y < 100; y++)
    {
      Array[playerid][y] = y;
    }

    比以下慢得多:

    代码:

    stock DoSomething(arr[])
    {
      for(new y = 0; y < 100; y++)
      {
      arr[y] = y;
      }
    }

    原因在于汇编代码的根源。快速查看 Pawn 中数组如何解引用就能解释。

    这是解引用 2D 数组涉及的代码量:

    代码:

    #emit CONST.alt arr //Load the address of the array
    #emit CONST.pri 2 //We want to access the 2nd sub-array
    #emit IDXADDR //Address of the 2nd element of the major array
    #emit MOVE.alt //Keep a copy of that address since we need to add it to the offset to get the address of the sub-array
    //ALT = PRI = Address of the 2nd element of the major array
    #emit LOAD.I
    //ALT = Address of the 2nd element of the major array
    //PRI = offset relative to the address stored in the ALT to the 2nd sub-array
    #emit ADD
    //PRI now has the address of the sub-array
    #emit MOVE.alt //Move the address of the first element of the sub-array from PRI to ALT
    #emit CONST.pri 4 //We want the 4th element of the sub-array
    #emit LIDX//Load the value stored at arr[2][4]

    与解引用 1D 数组相比:

    代码:

    #emit CONST.alt array_address
    #emit CONST.pri n
    #emit IDXADDR //PRI now has the address of the (n + 1)th element
    #emit CONST.alt array_address
    #emit CONST.pri n
    #emit LIDX //PRI now has the value stored in the (n + 1)th element

    这是数组传递的方式:

    代码:

    //Pushing the address of the global string
      #emit PUSH.C global_str
      //Pushing a local string
      #emit PUSH.S cmdtext

    这清楚地解释了为什么有效。当你推送数组时,你推送数组的地址,因此在函数调用中你收到一个 1D 数组。在这种情况下,2D 数组解引用的第一部分代码本质上在每次迭代中被跳过,这使它快得多。

    遗憾的是,Pawn 不提供指针。

  • 脚本基础

    大家好,今天我想给大家讲讲 “enum“、”if“、”else“、”else if“。

    那么开始吧:

    Enum
    它是一个数组,你可以在其中存放多个变量(变量可以是不同类型)。声明方式如下:

    PHP代码:

    enum test
    {
    peremen1,
    peremen2,
    peremen3


    注意:最后一个变量后面不需要加逗号!
    另外,为了让这个数组正常工作,还需要再声明一个变量,用它来访问枚举中的各个字段:

    PHP代码:

    new cars[test]; // 这个变量是全局共用的(所有人一份)
    new cars[MAX_PLAYERS][test]; // 这个变量是每个玩家各自一份 

    示例:
    好,先说如何使用“共用变量”的情况。比如你做了 3 个仓库,对于所有玩家来说,仓库里的物资数量当然是一样的,那么就很适合用这种方式:

    PHP代码:

    enum test
    {
    sklad,
    sklad1,
    sklad2
    }
    new sss[test];
    // 现在来使用这个变量:
    sss[sklad] = 10; // 这里把它的值设为 10 

    后续你可以按自己的需求自由发挥。下面我们来看第二个例子:让每个变量都针对每个玩家单独保存:

    PHP代码:

    enum test
    {
    pLevel,
    pExp,
    pMoney
    }
    new Player[MAX_PLAYERS][test];
    // 变量这样使用:
    public OnPlayerConnect(playerid)
    {
    Player[playerid][pMoney] = 500; // 这里在玩家连接时,把 money 设为 500
    return 1;

    if , else , else if
    条件语句,用于做各种判断/检查。
    先给大家介绍一下常见的运算符(用于条件判断):
    && – 且(AND)
    || – 或(OR)
    == – 等于
    != – 不等于
    >= – 大于或等于
    <= – 小于或等于
    > – 大于
    < – 小于

    示例:
    首先,我来演示一下如何使用第一个运算符 &&

    PHP代码:

    new test = 1; // 创建变量并赋值为 1
    new ttt = 2; // 创建变量并赋值为 2
    // 现在进行判断:
    if(test == 1 && ttt == 2) // 如果 test 等于 1 且 ttt 等于 2,那么:
    {
    // 执行代码


    “或”运算符 || 也是一样的,只是把“且”换成“或”。我想你已经明白如何写判断了。
    下面我们来讲 else(否则) 和 else if(否则如果)

    PHP代码:

    new test = 10; // 创建变量并赋值为 10
    if(test == 10) // 如果 test 等于 10,那么
    {
    // 执行代码
    }
    else // 否则
    {
    // 执行代码
    }
    // 顺便说一下,你可能注意到了,else 也常用于对话框中,例如 DIALOG_STYLE_MSGBOX:
    if(dialogid == 1) // 如果对话框 ID 等于 1
    {
    if(response) 
    {
     // 玩家点了第 1 个按钮时的动作
    }
    else // 否则
    {
    // 玩家点了第 2 个按钮时的动作
    }


    下面我给出一个 else if 的例子:

    PHP代码:

    new test = 5; // 创建变量并赋值为 5
    new ttt = 1; // 创建变量并赋值为 1
    if(test > ttt) // 如果 test 大于 ttt
    {
    // 执行代码
    }
    else if(test == ttt) // 否则如果 test 等于 ttt
    {
    // 执行代码
    }
    else // 否则:如果 test 不大于 ttt 且不等于 ttt,那么
    {
    // 执行代码


    到这里就结束了。如果有问题,欢迎提问!

  • 如何将模组(脚本)拆分为多个 Include 文件(包含文件)

    本主题写给那些还没有完全理解“把模组拆分成 include(inc)文件”意味着什么的人。很多人把它理解为:从主文件(gamemode.pwn)里剪下一些代码片段,然后复制到单独的文件(name.inc)里。这个方法不能说好,反而更偏向于不好。

    这种拆分代码的逻辑通常是:“这里行数很多,我把它们挪到单独文件里,这样主模组里行数就少了。”但在后续开发中,这反而会把事情搞复杂:你不得不同时打开一堆文件来回切换,还要一直记住调用顺序、每个变量/函数的作用域等等。

    我会尽量解释:怎样做会更好;以及到底应该如何理解“模块化”,并且如何把这一切和模组(gamemode)配合起来。这里我所说的“模块”,指的是一个完全自治或部分自治的系统,它有自己的 API 函数用于交互,并被放在一个或多个独立的 include 文件中。

    核心原则:所有交互(处理流程)发生在模组里,而计算/实现细节放在 include 文件里完成。
    首先,你需要确定将会使用哪些系统。接着把这些系统分成两类:独立 与 依赖

    “独立”表示它可以脱离其他系统单独使用;“依赖”表示它需要与其他系统配合使用。在模组里连接 include 的顺序应该是:先引入 独立 系统,然后再引入 依赖 系统。
    那怎么判断一个系统属于哪一类?

    账号系统 是服务器上最重要的系统,没有它几乎无法想象服务器如何运行。这个系统几乎与所有其他系统都有关联。关键要理解:我们是在其他系统中调用账号系统的函数,而不是在账号系统内部去调用其他系统的函数。因此它属于 独立 系统(模块)。它应该包含什么?例如:

    代码:

    CheckPlayerAccount(playerid) - 检查账号是否存在(是否已注册)
    GetPlayerAccountID(playerid) - 获取账号 ID
    GetPlayerAccountName(playerid) - 获取账号(玩家)名称

    我们来详细看看 CheckPlayerAccount。它应该用在哪里、做什么?我们在 OnPlayerConnect 中调用它。在这个函数里,我们向数据库发送 SELECT 查询,根据玩家名字查找玩家信息。

    立刻会有个问题:名字从哪里获取、存在哪里?你当然可以在 OnPlayerConnect 里用 GetPlayerName 去取名字,但玩家名字本质上属于“账号信息”,对吧?既然如此,与其在 OnPlayerConnect 里调用 GetPlayerName,不如把这一步移动到 CheckPlayerAccount 里更合理。

    代码:

    [gamemode.pwn]
    public OnPlayerConnect(playerid)
    {
        CheckPlayerAccount(playerid);
        return 1;
    }

    代码:

    [account.inc]
    new account_name[MAX_PLAYERS][MAX_PLAYER_NAME];
    #define GetPlayerAccountName(%0) account_name[%0]
    stock CheckPlayerAccount(playerid)
    {
        GetPlayerName(playerid, account_name[playerid], MAX_PLAYER_NAME);
        static const string[] = "SELECT * FROM `accounts` WHERE `name` = '%e' LIMIT 1;";
        new query[szeof string + MAX_PLAYER_NAME];
        mysql_format(db, query, sizeof query, string, account_name[playerid]);
        mysql_tquery(db, query, "OnCheckPlayerAccount", "i", playerid);
      return 1;
    }

    再说两句数组 account_name 和宏 GetPlayerAccountName。既然我们在写一个独立系统,最好让它拥有自己的 API 函数,以及用来存储信息的变量/数组。专门写一个返回 字符串 的函数并不是最佳实践,所以我们用 new 声明数组 account_name,让它可以在其他 include 中被访问。然后我们做了一个“看起来像函数”的宏:GetPlayerAccountName

    那为什么要用宏/函数,而不是直接访问变量/数组?
    首先是为了避免未来自己犯错;同时提升可读性,避免以“不正确的方式”使用变量/数组。另一个便利是:所有 API 函数都能在命名上有自己的“根”,例如 Account,让你一眼知道这个函数属于哪个系统。需要强调:为每个变量都做一个函数/宏不是硬性规则,只是建议;有时直接用会更合理。
    发送查询后,我们在 OnCheckPlayerAccount 中等待结果。现在要决定:这个函数应该写在模组里还是 include 里?这里我们把它写在 include 里。
    在数组 account_name 后添加:

    代码:

    [account.inc]
    static enum E_ACCOUNT_INFO
    {
        account_id,
    }
    static account_info[MAX_PLAYERS][E_ACCOUNT_INFO];

    在宏 GetPlayerAccountName 后添加:

    代码:

    [account.inc]
    stock GetPlayerAccountID(playerid)
    {
        return account_info[playerid][account_id];
    }

    现在创建 OnCheckPlayerAccount

    代码:

    [account.inc]
    forward OnCheckPlayerAccount(playerid);
    public OnCheckPlayerAccount(playerid)
    {
        if(cache_num_rows())
        {
            cache_get_value_name_int(0, "pID", account_info[playerid][account_id]);
            // 加载其余所有信息
            return OnPlayerConnected(playerid, true);
        }
        else
            return OnPlayerConnected(playerid, false);
    }

    那么 OnPlayerConnected(playerid, bool: status) 是什么?在哪里用?我们传入的参数是什么?
    这个函数表示:我们已经向数据库查询并拿到了该玩家的响应。参数 status 表示玩家是否已注册。
    如果找到了任何信息,就加载并用 true 调用 OnPlayerConnected;否则用 false。这个函数的使用应该在 gamemode.pwn 中,但它的 forward 声明应写在 include 中。

    代码:

    [account.inc]
    forward OnPlayerConnected(playerid, bool:status);

    代码:

    [gamemode.pwn]
    public OnPlayerConnect(playerid)
    {
        CheckPlayerAccount(playerid);
        return 1;
    }
    public OnPlayerConnected(playerid, bool:status)
    {
        if(status)
            // 登录/授权
        else
            // 注册
        return 1;
    }

    这样一来,我们就完成了:从账号系统调用函数,在 include 内部完成处理,然后把结果回传到模组。
    模组 – include / include – 模组 的交互关系应该大体都遵循这种思路。

    [图片: img_1763919919__photo_2025-11-23_20-45-11.jpg]

    再说 依赖 系统,例如 派系。流程类似,只不过我们允许在该系统里调用其他系统的函数,比如账号系统:拿到玩家 ID 或名字后再做后续处理。

    [图片: img_1763920829__photo_2025-11-23_21-00-19.jpg]

    本质上就是:把该系统的所有动作都放进它的 include 里,而把得到的结果交给模组进行处理。
    如果你很难正确分配动作、尤其是遇到作用域问题,那通常就说明这段代码应该挪回模组中。
    还可以谈谈更复杂的系统:当一个系统使用不止一个 include 时,可以创建一个单独的文件夹,把需要的 include 都放进去。
    比如你有一个背包(inventory)系统:它需要保存玩家基础数据、UI 相关数据、以及与玩家交互的函数。你可以创建 inventory 文件夹,并创建 core 和 ui 两个 include。
    ui 里放绘制 UI 所需的一切,而 core 放主要数据与交互逻辑。
    如果在模组里这样引入:

    代码:

    #include "../modules/inventory/core.inc"
    #include "../modules/inventory/ui.inc"

    会出现问题:在 core 里无法访问 ui 的函数。
    如果这样引入:

    代码:

    #include "../modules/inventory/ui.inc"
    #include "../modules/inventory/core.inc"

    又会出现相反的问题:在 ui 中无法获取 core 中的数据。
    怎么解决?当然可以把一切都合成一个 include,让常量和函数彼此可见,但这里换个方式:把 ui include 在 core include 内部引入。

    代码:

    [core.inc]
    // 创建所有必要的宏、常量、数组和变量
    #include "../modules/inventory/ui.inc"
    // 创建与玩家交互的函数

    而在模组中只保留对 core include 的引入。这样就把系统的一个 include 的引入 “隐藏”在另一个(主)include 里,作用域问题也就不存在了。
    再补充一点:凡是只在 include 内部使用的变量/函数,应该用 static 来创建,避免它们在其他地方被随意调用。
    另外,也可以在函数名前面加下划线 “_” 来明确表示:该函数不对其他 include 暴露;例如 OnCheckPlayerAccount 可以写成 _OnCheckPlayerAccount
    当你有一个“由多个 include 组成”的系统(如背包),并且你需要某个函数在所有被引入的 include 中可见、但在模组中不可见时,单纯用 static 不行(因为 static 会导致其他 include 也看不到)。这里有个绕过方法:

    代码:

    [core.inc]
    stock TestFunction()
    {
        return 1;
    }
    // 在 core.inc 的末尾
    #define TestFunction _TestFunction

    我们先把函数做成全局的,然后再“隐藏”它。由于宏是在 include 的末尾定义的,所以它对模组生效,而对 include 本身不生效。
    同时宏在调用解析上优先于函数,因此通过这个宏,我们把对 TestFunction 的调用重定向到一个不存在的函数 _TestFunction。这样如果在模组里调用 TestFunction,就会报错:该函数不存在。
    再补充:还有一种方法,可以在不同 include 中使用相同的函数名,但该函数在模组中无法被调用。可能不太常用,不过示例如下:

    代码:

    [core_1.inc]
    stock TestFunction()
    {
        return 1;
    }
    // 在 core_1.inc 的末尾
    static stock PREFIX1_Test(){}
    #if defined _ALS_TestFunction
        #undef TestFunction
    #else
        #define _ALS_TestFunction
    #endif
    #define TestFunction PREFIX1_TestFunction

    代码:

    [core_2.inc]
    stock TestFunction()
    {
        return -1;
    }
    // 在 core_2.inc 的末尾
    static stock PREFIX2_Test(){}
    #if defined _ALS_TestFunction
        #undef TestFunction
    #else
        #define _ALS_TestFunction
    #endif
    #define TestFunction PREFIX2_TestFunction

    最后我个人建议:给自己的系统写注释,这样未来你能更清晰地知道:该系统哪些内容能在模组里使用。
    我通常会在引入之前加一个提示:

    代码:

    /*
    Name_... -> function
    NAME_... -> define / const
    name_... -> var

    # -> define
    * -> const
    @ -> callback
    i -> iterator
    e -> enum
    p -> pvar
    s -> svar
    - -> new
    > -> function
    cmd -> command
    */

    然后像这样引入 include:

    代码:

    #include "../modules/account.inc"
    /*
        # INVALID_ACCOUNT_ID (-1)
        # GetPlayerAccountName(%0) (account_name[%0])
        # GetPlayerAccountPassword(%0) (account_password[%0])
        * LEN_ACCOUNT_NAME (MAX_PLAYER_NAME)
        * SIZE_ACCOUNT_NAME (LEN_ACCOUNT_NAME+1)
        * LEN_ACCOUNT_PASSWORD (64)
        * SIZE_ACCOUNT_PASSWORD (LEN_ACCOUNT_PASSWORD+1)
        @ OnPlayerConnected(playerid, bool:status)
        i Account
        - account_name[MAX_PLAYERS][SIZE_ACCOUNT_NAME]
        - account_password[MAX_PLAYERS][SIZE_ACCOUNT_PASSWORD]
        > CheckPlayerAccount(playerid)
        > GetPlayerAccountID(playerid)
    */