samp | open.mp 联机社区论坛
[教程] open.mp/sa-mp 服务器开发规范 - 打印版本

+- samp | open.mp 联机社区论坛 (https://open-mp.cn)
+-- 板块: SA-MP (https://open-mp.cn/forumdisplay.php?fid=12)
+--- 板块: 教程 (https://open-mp.cn/forumdisplay.php?fid=17)
+--- 主题: [教程] open.mp/sa-mp 服务器开发规范 (/showthread.php?tid=6)



[教程] open.mp/sa-mp 服务器开发规范 - XiaoNiao - 02-28-2026

open.mp/sa-mp 服务器开发规范

最新 github 资料地址

为了避免开发者在服务器开发过程中,系统功能逐渐坍塌。本规范的引入,并非为了增加开发负担,而是为了建立一套对抗系统熵增的秩序,共同维护一个默认的社区标准和规范。

本规范欢迎所有人提交 PR 或 Issue 进行改进。所有采纳的修改都将记入贡献者名单,共同维护 SA‑MP/open.mp 中文开发社区的良好生态。规范的生命力在于持续迭代,而非一成不变。

文件区分

其实文件夹怎么分没有铁打的标准,每个项目情况都不一样,但大致的思路都是差不多的

先按"谁先用谁、谁依赖谁"排顺序,再按"这是游戏里的哪个系统"来分组,没必要一开始就搞得很复杂,先写着写着自然就知道哪里该拆了

这里仅提供示范思路:

代码:
/Server
  ├── gamemodes/
  │    └── main.pwn          // 主文件,负责 #include 模块
  │    └── locale/            // 如果你打算做多语言的话
  │    └── utils/            // 自定义封装好的通用函数、工具(和 libraries 差不多)
  │    └── core/              // 低层、必须先加载的核心系统(不依赖业务)
  │    │  ├── config/        // 配置相关 如 服务器名称 版本 规则 服务器设置
  │    │  ├── shared/        // 所有模块的“通用语言” 共享静态定义数据 用于校验 杜绝跨模块的变量污染
  │    │  └── database/      // 数据库(如:MySQL)
  │    ├── world/            // 游戏世界环境相关(地图、动态对象、时间等等)
  │    └── modules/          // 所有功能模块存放处
  │        ├── players/      // 玩家模块
  │        ├── vehicles/    // 车辆模块
  │        ├── houses/      // 房屋模块
  │        └── admin/        // 管理员模块
  │    └── libraries/        // 第三方库 (如 mysql, sscanf等)
  │    └── filterscripts/    // 可动态加载的独立功能(脚本玩法、工具之类的附加项内容)

模块间通信准则

模块之间严禁直接操作对方的 static 变量,必须通过 API

如果 vehicles 模块需要获取玩家的某个状态,必须调用 Player_GetSomeStatus(playerid),严禁直接读取 gPlayerData

命名规范
  • 函数:PascalCase(所有单词首字母大写), 前缀是模块名 (如: Player_Load(playerid))。
  • 全局变量:

代码:
前缀 g_ 代表全局变量 (gobal) 比如: new g_Var;
前缀 gc_ 代表全局变量 (gobal const) 比如: new const gc_Var;
前缀 gs_ 代表全局静态变量(作用域仅在当前模块内) (gobal static) 比如: static gs_Var;
前缀 gsc_ 表示全局静态常数变量 (gobal static const) 比如: static const gsc_Var;
  • 局部变量: 首单词小写, 后面单词首字母大写,或小写的缩写 (如: new number, vehicleIndex, id, pos)
  • 常量/宏:大写 SNAKE_CASE (如: #define MAX_VEHICLES 2000`)。
  • 文件:全小写,分割使用 '-' (如: player-main.inc、player-impl.inc);主文件 main.pwn。
  • 枚举与数据结构规范: 采用 E_MODULENAME_DATA 格式(如 E_PLAYER_DATA),全大写,采用 'E_' 开头
  • 枚举成员采用: 模块名_字段名 全大写 SNAKE_CASE, 便于快速区分、选择、修改等等

代码:
// 房屋模块
enum E_HOUSE_DATA
{
    HOUSE_DBID,
    HOUSE_OWNER[MAX_PLAYER_NAME],
    Float:HOUSE_ENTRANCE_X,
    Float:HOUSE_ENTRANCE_Y,
    Float:HOUSE_ENTRANCE_Z,
    bool:HOUSE_IS_LOCKED,
    HOUSE_PRICE
}
static gs_HouseData[MAX_HOUSES][E_HOUSE_DATA];

// 载具模块
enum E_VEHICLE_DATA
{
    VEHICLE_DBID,
    VEHICLE_MODEL,
    Float:VEHICLE_HEALTH,
    VEHICLE_OWNER_ID
}
static gs_VehicleData[MAX_VEHICLES][E_VEHICLE_DATA];

缩写准则

只允许通用缩写

如:ID (Identity), Pos (Position), Rot (Rotation), Max/Min, Msg (Message), Cmd (Command) 等等

错误示例:pLvl(到底是 Level 还是 Leave?)

禁止:将 Owner 缩写为 O, 将 Price 缩写为 Pr,禁止使用无意义缩写

包含守卫:

打开任何一个 .inc 文件,第一眼能看到"这个文件要放在哪里、依赖谁",不用翻 main.pwn 的 include 列表猜顺序或者人工记忆顺序,长期维护更加直观清晰

尤其是相同层级的脚本,由于相互之间可能存在依赖关系,比如房屋模块需要使用到玩家模块的信息(金钱等等),包含守卫可以很好地理清关系,避免脚本运行时出现问题,在编译阶段完美规避问题的出现

注意注释保持简短,别写成大段文档

代码:
// 防止重复包含 替换 SCRIPT_NAME 即可
#if defined _INC_SCRIPT_NAME
    #endinput
#endif
#define _INC_SCRIPT_NAME

// 模块之间的依赖说明
#if !defined _INC_OTHER_SCRIPT_NAME
    #error 需要包含 other-script.inc.
#endif

// 环境约束 确保编译环境正确
#if !defined _INC_open_mp
    #error 需要包含 open.mp.inc.
#endif

// 依赖项的显式校验 快速定位缺失依赖
#tryinclude <Pawn.RakNet>
#if !defined PAWNRAKNET_INC_
    #error 需要使用 Pawn.RakNet 插件
#endif

// 插件版本与功能对齐
#tryinclude <streamer>
#if !defined Streamer_IncludeFileVersion
    #error cannot read from file: "streamer.inc"
#elseif Streamer_IncludeFileVersion != 0x296
    #error 不兼容的 streamer 插件版本, 请使用 2.9.6 版本.
#endif

// 零开销的默认配置 可在后续代码中随时关闭或开启调试
#if !defined SCR_DEBUG
    #define SCR_DEBUG false
#endif

// 或者这样
#if SCR_DEBUG
    #define DebugMessage(%1) printf("[Script Debug] " %1)
    #define ErrorMessage(%1) printf("[Script Debug-Error] " %1)
#else
    #define DebugMessage(%1);
    #define ErrorMessage(%1);
#endif

ALS 钩子标准

用传统 ALS 钩子扩展回调/函数

示例:回调钩子 (OnGameModeInit)

代码:
// 替换 SCR 即可
public OnGameModeInit()
{
    #if defined SCR_OnGameModeInit
        return SCR_OnGameModeInit();
    #else
        return 1;
    #endif
}
#if defined _ALS_OnGameModeInit
    #undef OnGameModeInit
#else
    #define _ALS_OnGameModeInit
#endif
#define OnGameModeInit SCR_OnGameModeInit
#if defined SCR_OnGameModeInit
    forward SCR_OnGameModeInit();
#endif

示例:stock 函数钩子 (SetPlayerScore)

代码:
// 替换 SCR 即可
stock bool:SCR_SetPlayerScore(playerid, score)
{
    if(SetPlayerScore(playerid, score))
    {
        // ......
        return true;
    }
    return false;
}
#if defined _ALS_SetPlayerScore
    #undef SetPlayerScore
#else
    #define _ALS_SetPlayerScore
#endif
#define SetPlayerScore SCR_SetPlayerScore

函数作用域规范

  1. static stock (模块私有函数)
  • 作用域仅在当前 .inc 文件中, 它只在内部使用
  • 命名以 下划线 开头,采用 _ModuleName_FunctionName 格式(如 _House_CheckDistance)

  1. stock (模块公开接口)
  • 定义:作用域全服务器, 命名采用 ModuleName_FunctionName 格式

代码:
#define HOUSE_TAX_RATE 0.1

// 只有本模块能用,外部无法使用,也不会冲突
static stock _House_CalculateTax(houseid)
{
    return floatround(float(gs_HouseData[houseid][HOUSE_PRICE]) * HOUSE_TAX_RATE);
}

// 对外接口
stock bool:House_GetTotalCost(houseid, &cost)
{
    if(houseid < 0 || houseid >= MAX_HOUSES)
        return false;

    // 内部逻辑调用私有函数
    cost = gs_HouseData[houseid][HOUSE_PRICE] + _House_CalculateTax(houseid);
    return true;
}

注释标准

对于函数的说明注释使用 Doxygen 风格,直接关系到长期维护,至少包含 功能简述、参数、返回值、注意事项

代码:
/**
* 获取玩家当前拥有的房产数量。
* @param playerid 玩家ID
* @return 房产数量,失败返回 -1
*/
stock Player_GetPropertyCount(playerid);

标签规范

用 enum + 宏定义标签,确保类型安全。

示例:WEATHER

代码:
enum WEATHER:
{
    WEATHER_UNKNOWN = -1,
    WEATHER_EXTRASUNNY_LA = 0,
    WEATHER_SUNNY_LA,
    WEATHER_EXTRASUNNY_SMOG_LA,
    WEATHER_SUNNY_SMOG_LA,
    WEATHER_CLOUDY_LA,
    WEATHER_SUNNY_SF,
    ...,
    ...
}

错误处理标准

规范:返回值语义化

逻辑判断返回类型 bool:

成功返回 ID ( >= 0),失败返回 INVALID_..._ID (-1)