|
本主题写给那些还没有完全理解“把模组拆分成 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 - 模组 的交互关系应该大体都遵循这种思路。
再说 依赖 系统,例如 派系。流程类似,只不过我们允许在该系统里调用其他系统的函数,比如账号系统:拿到玩家 ID 或名字后再做后续处理。
本质上就是:把该系统的所有动作都放进它的 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)
*/
|