欢迎, 游客
您必须先 注册 才能在我们的网站上发帖.

用户名
  

密码
  





搜索论坛



(高级搜索)

论坛统计
» 会员总数: 5
» 新进会员: 老白金
» 主题总数: 13
» 帖子总数: 17

完整统计

在线用户
当前共有 1 位用户在线
» 0 会员 | 1 游客

最新主题
openmp/samp联机服务器插件开发 完全指南
板块: 教程
最后发表: XiaoNiao
7 小时 前
» 回复: 0
» 浏览: 20
[转发][库] tdialogs 异步处理对话框
板块:
最后发表: siwode
03-03-2026, 11:12 PM
» 回复: 1
» 浏览: 35
[转载][工具]DAWNO - PAWN 编辑器
板块: 发布
最后发表: XiaoNiao
03-02-2026, 11:27 AM
» 回复: 0
» 浏览: 17
[插件] kook-connect
板块: 插件
最后发表: siwode
02-28-2026, 07:05 PM
» 回复: 0
» 浏览: 20
[转载]SA-MP/OpenMP反作弊系统 by ...
板块: 脚本
最后发表: XiaoNiao
02-28-2026, 04:08 PM
» 回复: 0
» 浏览: 23
欢迎各位!
板块: 聊天
最后发表: XiaoDai
02-28-2026, 02:56 PM
» 回复: 2
» 浏览: 72
[库][分享转发] sa-mp/pawn 标准库包
板块:
最后发表: XiaoNiao
02-28-2026, 02:39 PM
» 回复: 0
» 浏览: 15
SinglePlayer-Role Play 1....
板块: 游戏模式
最后发表: XiaoNiao
02-28-2026, 02:16 PM
» 回复: 0
» 浏览: 20
[教程] open.mp/sa-mp 服务器开发规...
板块: 教程
最后发表: XiaoNiao
02-28-2026, 11:55 AM
» 回复: 0
» 浏览: 40
[教程] 代码优化 #2
板块: 教程
最后发表: XiaoNiao
02-28-2026, 11:46 AM
» 回复: 1
» 浏览: 34

 
  openmp/samp联机服务器插件开发 完全指南
发布者: XiaoNiao - 7 小时 前 - 板块: 教程 - 暂无回复

openmp/samp联机服务器插件开发 完全指南

引用:从零开始,用 C++ 为 samp/openmp 服务器开发自定义插件。
本教程面向有 Pawn 脚本基础、想转 C++ 开发的 SAMP/openmp 服务器开发者。
本教程将教大家制作一个完整的查克拉系统简单插件实现

为什么标题包含samp但是确实openmp类的教程,因为很多人搜索sa-mp,但sa-mp已经彻底消逝了,有许多遗留问题且不再维护更新,openmp是最优解,你应该使用openmp,而且它向下兼容samp(我指的是pawn层),开发过程基本一模一样

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
目录

  1. 环境搭建
  2. 创建项目
  3. 最小可运行组件
  4. 组件的生命周期
  5. 创建自己的native函数
  6. 事件处理
  7. 玩家数据扩展
  8. 如何扫描amx的public
  9. 完整示例
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
1. 环境搭建

这里将演示轻量级环境搭建,你只需要安装三样东西和 VSCode

1.1 安装工具

Visual Studio Code(代码编辑器)

https://code.visualstudio.com/

Visual Studio Build Tools(不是完整的 Visual Studio)

这是微软的 C++ 编译器。你可以理解成pawn里的pawncc.exe,安装时只勾选「使用 C++ 的桌面开发」工作负载,其他全不选。

下载:https://visualstudio.microsoft.com/visua...ild-tools/

CMake

构建系统,管理编译流程。

下载:https://cmake.org/download/

推荐下载 ZIP 版,解压到固定目录(如 D:\tools\cmake\),然后把 D:\tools\cmake\bin 加入系统 PATH 环境变量。干净利落,不想用了直接删文件夹。

如果选 Installer 版,安装时勾选 "Add CMake to the system PATH"。

Git

用于完整获取 open.mp SDK,以及后续各种开发所需的资源。

下载:https://git-scm.com

1.2 VSCode 扩展

打开 VSCode,安装以下扩展:
  • **C/C++**(Microsoft)— 代码提示、语法高亮、调试
  • CMake Tools(Microsoft)— CMake 项目集成,一键编译
1.3 验证安装

任意终端(比如 cmd)输入以下指令逐个验证是否安装配置成功:

代码:
cmake --version    # 应该显示版本号
git --version      # 应该显示版本号
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
2. 创建项目

2.1 初始化目录

选一个你喜欢的位置,创建项目:

代码:
新建文件夹 my-component
在根目录新建 src 的文件夹
在根目录新建 lib 的文件夹
2.2 添加 open.mp SDK 和相关依赖

lib文件夹内点击鼠标右键,选择 Open Git Bash here 并执行以下命令

代码:
git clone --recursive https://github.com/openmultiplayer/open.mp-sdk sdk
git clone --recursive https://github.com/openmultiplayer/pawn-natives
git clone --recursive https://github.com/openmultiplayer/compiler pawn
等待获取完成,这会在lib文件夹内创建 sdk/ pawn/ pawn-natives/目录。

2.3 创建 CMakeLists.txt

在项目根目录创建 CMakeLists.txt 并复制以下内容粘贴进去:

代码:
cmake_minimum_required(VERSION 3.19)
project(my-component)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_subdirectory(lib/sdk)
add_library(${PROJECT_NAME} SHARED
    src/main.cpp
)
# 解决库的兼容性问题
add_definitions(
    -DHAVE_STDINT_H=1
    -DGLM_FORCE_SSE2=1
    -DGLM_FORCE_QUAT_DATA_WXYZ=1
    -DNOMINMAX=1
    -Dnssv_CONFIG_SELECT_STRING_VIEW=nssv_STRING_VIEW_NONSTD
    -Dspan_CONFIG_SELECT_SPAN=span_SPAN_NONSTD
    -DPAWN_CELL_SIZE=32
)
target_include_directories(${PROJECT_NAME} PRIVATE
    lib/
    lib/pawn/source
    lib/pawn/source/linux
    lib/pawn-natives
)
target_link_libraries(${PROJECT_NAME} PRIVATE
    OMP-SDK
)
2.4 项目结构

最终你的目录应该长这样:

代码:
my-component/
├── CMakeLists.txt      ← 构建配置
├── lib/
    ├── sdk/            ← open.mp SDK
    ├── pawn-natives/  ← 用于快速定义 PAWN 原生函数
    └── pawn/          ← 关联库
└── src/
    └── main.cpp        ← 你的代码
2.5 配置 VSCode

用 VSCode 打开 my-component 文件夹。CMake Tools 扩展会自动检测到 CMakeLists.txt

选择编译器(Kit):

  1. Ctrl+Shift+P,输入 CMake: Select a Kit
  2. 选择 Visual Studio 生成工具 2026 Release x86,如果没有这个选项,选择[扫描工具包]即可搜索。
选择构建类型:

  1. Ctrl+Shift+P,输入 CMake: Select Variant
  2. 开发时选 Debug,发布时选 Release
引用:注意: 如果你使用的是 CMake 4.x,可能会遇到旧版依赖的兼容性警告。在项目根目录创建 .vscode/settings.json
代码:
{
    "cmake.configureArgs": [
        "-DCMAKE_POLICY_VERSION_MINIMUM=3.10"
    ]
}
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
3. 最小可运行组件

创建 src/main.cpp,写入以下代码:

代码:
#include <sdk.hpp>
class MyComponent final : public IComponent
{
private:
    ICore* core_ = nullptr;
public:
    // 每个组件必须有一个全局唯一 ID
    // 去 https://open.mp/uid 生成一个替换下面的ID
    PROVIDE_UID(0x123456789ABCDEF0);
    // 组件名称,显示在服务端加载日志里
    StringView componentName() const override
    {
        return "MyComponent";
    }
    // 版本号
    SemanticVersion componentVersion() const override
    {
        return SemanticVersion(1, 0, 0, 0);
    }
    // 组件加载时调用
    void onLoad(ICore* c) override
    {
        core_ = c;
        core_->printLn("Hello World!");
    }
    // 其他组件初始化完成后调用
    void onInit(IComponentList* components) override {}
    // 所有组件和脚本都就绪后调用
    void onReady() override {}
    // 其他组件卸载时调用
    void onFree(IComponent* component) override {}
    // 组件自身被释放
    void free() override { delete this; }
    // GMX(换图)时调用
    void reset() override {}
};
// 组件入口点 —— 服务端通过这个宏发现并加载你的组件
COMPONENT_ENTRY_POINT()
{
    return new MyComponent();
}
编译:

F7(或 Ctrl+Shift+PCMake: Build)。编译成功后,在 build/ 目录下会生成 my-component.dll

测试:

.dll 复制到 open.mp 服务端的 components/ 目录,启动服务端,你应该能在控制台看到:

代码:
Loading component MyComponent.dll
        Successfully loaded component MyComponent (1.0.0.0) with UID 123456789abcdef0
Hello World!
恭喜,你的第一个组件跑起来了。

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
4. 组件的生命周期

组件的函数按固定顺序调用。理解这个顺序非常重要:

代码:
服务端启动
  │
  ├── onLoad(ICore*)          ← 拿到核心接口
  │
  ├── onInit(IComponentList*)  ← 查询其他组件(如 IPawnComponent)
  │
  ├── onAmxLoad(IPawnScript&)  ← Pawn 脚本加载,扫描 public 函数
  │
  ├── onReady()                ← 一切就绪,可以开始工作
  │
  │  ... 服务器运行中 ...
  │
  ├── onAmxUnload(IPawnScript&) ← Pawn 脚本卸载,清理相关数据
  │
  ├── onFree(IComponent*)      ← 组件卸载时
  │
  └── free()                  ← 释放自身
关键点:
  • onLoad 里只保存 ICore* 指针,不要访问其他组件(它们可能还没加载)
  • onInit 里通过 components->queryComponent<IPawnComponent>() 获取 Pawn 组件
  • onAmxLoad 是注册 native 函数和扫描 public 的地方
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
5. 创建自己的native函数

Native 函数让 Pawn 脚本能够调用你的 C++ 代码。

5.1 基本概念

在 Pawn 里写:

代码:
native MyAdd(a, b);
public OnGameModeInit()
{
    new a = 10, b = 20;
    new result = MyAdd(a, b);
    printf("%d + %d = %d", a, b, result);
    return 1;
}
在 C++ 里你需要实现函数并注册到 AMX。

5.2 使用 SCRIPT_API 宏(推荐)

open.mp 提供了 SCRIPT_API 宏,自动处理参数的类型转换:

代码:
#include <sdk.hpp>
#include <Server/Components/Pawn/pawn.hpp>
#include <Server/Components/Pawn/Impl/pawn_natives.hpp>
#include <Server/Components/Pawn/Impl/pawn_impl.hpp>
SCRIPT_API(MyAdd, int(int a, int b))
{
    return a + b;
}
SCRIPT_API 的参数含义:
  • 第一个参数:函数名,必须和 Pawn 里的 native 名称完全一致
  • 第二个参数:返回类型(参数列表)
5.3 注册时机

在组件的 onAmxLoad 里调用 pawn_natives::AmxLoad,它会自动把所有 SCRIPT_API 定义的函数注册到 AMX:

代码:
void onAmxLoad(IPawnScript& script) override
{
    pawn_natives::AmxLoad(script.GetAMX());
}
5.4 完整的组件 + Native 示例

代码:
#include <sdk.hpp>
#include <Server/Components/Pawn/pawn.hpp>
#include <Server/Components/Pawn/Impl/pawn_natives.hpp>
#include <Server/Components/Pawn/Impl/pawn_impl.hpp>
class MyComponent final : public IComponent, public PawnEventHandler
{
private:
    ICore* core_ = nullptr;
    IPawnComponent* pawn_ = nullptr;
public:
    // 每个组件必须有一个全局唯一 ID
    // 去 https://open.mp/uid 生成一个替换下面的ID
    PROVIDE_UID(0x123456789ABCDEF0);
    StringView componentName() const override { return "MyComponent"; }
    SemanticVersion componentVersion() const override { return SemanticVersion(1, 0, 0, 0); }
    void onLoad(ICore* c) override
    {
        core_ = c;
        // 初始化 AMX 查找表
        setAmxLookups(core_); 
    }
    void onInit(IComponentList* components) override
    {
        pawn_ = components->queryComponent<IPawnComponent>();
        if (pawn_)
        {
            setAmxFunctions(pawn_->getAmxFunctions());
            setAmxLookups(components);
            pawn_->getEventDispatcher().addEventHandler(this);
        }
    }
    void onAmxLoad(IPawnScript& script) override
    {
        pawn_natives::AmxLoad(script.GetAMX());
    }
    void onAmxUnload(IPawnScript& script) override {}
    void onReady() override {}
    void onFree(IComponent* component) override {}
    void free() override { delete this; }
    void reset() override {}
    ~MyComponent()
    {
        if (pawn_) pawn_->getEventDispatcher().removeEventHandler(this);
    }
};
COMPONENT_ENTRY_POINT()
{
    return new MyComponent();
}
SCRIPT_API(MyAdd, int(int a, int b))
{
    return a + b;
}
重新编译插件并开启服务器,你就可以在控制台看到 10 + 20 = 30

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
6. 事件处理

open.mp 通过 EventHandler 接口分发事件。你的组件实现对应接口,注册到分发器,就能收到事件。

6.1 常见的 EventHandler

代码:
| 接口 | 事件 |
|--|--|
| `PlayerConnectEventHandler` | 玩家连接/断开 |
| `PlayerTextEventHandler` | 玩家聊天/命令 |
| `PawnEventHandler` | AMX 脚本加载/卸载 |


6.2 示例:监听玩家连接

代码:
class MyComponent final
    : public IComponent
    , public PawnEventHandler
    , public PlayerConnectEventHandler  // 实现连接事件接口
{
    // ... 省略其他成员 ...
    void onInit(IComponentList* components) override
    {
        // 注册 Pawn 事件
        pawn_ = components->queryComponent<IPawnComponent>();
        if (pawn_)
        {
            setAmxFunctions(pawn_->getAmxFunctions());
            setAmxLookups(components);
            pawn_->getEventDispatcher().addEventHandler(this);
        }
        // 注册玩家连接事件
        core_->getPlayers().getPlayerConnectDispatcher().addEventHandler(this);
    }
    // 析构函数里注销
    ~MyComponent()
    {
        if (pawn_)  pawn_->getEventDispatcher().removeEventHandler(this);
        if (core_) core_->getPlayers().getPlayerConnectDispatcher().removeEventHandler(this);
    }
    // 玩家连接时调用
    void onPlayerConnect(IPlayer& player) override
    {
        core_->printLn("玩家 %d 进入了服务器!", player.getID());
    }
};
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
7. 玩家数据扩展

在 Pawn 里,你可能习惯用全局数组存储玩家数据:

代码:
new pScore[MAX_PLAYERS];
new pLevel[MAX_PLAYERS];
new bool:pLoggedIn[MAX_PLAYERS];
在 open.mp 的 C++ 组件里,对应的机制是 IExtension。它把数据"挂"在玩家对象上。

7.1 定义数据结构

代码:
struct PlayerEnergy final : IExtension
{
    // 每个 Extension 需要一个全局唯一 ID
    // 去 https://open.mp/uid 生成
    PROVIDE_EXT_UID(0xABCDEF1234567890);
    // 你的自定义数据
    int energy = 100;
    int maxEnergy = 100;
    void freeExtension() override { delete this; }
    void reset() override
    {
        energy = 100;
        maxEnergy = 100;
    }
};
7.2 在玩家连接时挂载

代码:
void onPlayerConnect(IPlayer& player) override
{
    // 创建 PlayerEnergy 实例并挂到玩家身上
    // 第二个参数 true 表示玩家断开时自动调用 freeExtension
    player.addExtension(new PlayerEnergy(), true);
}
7.3 操作数据

代码:
auto* playerData = queryExtension<PlayerEnergy>(player);
if (playerData)
{
    playerData->energy += 100;
}
7.4 为什么不用 std::unordered_map<int, PlayerEnergy>

你可以用,但 IExtension 有几个优势:
  • 生命周期自动管理:玩家断开时 freeExtension 自动调用,不会忘记清理
  • GMX 自动重置:换图时 reset 自动调用
  • 数据跟着对象走:不需要传 playerid 再去查 map,直接从 IPlayer&
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
8. 如何扫描amx的public

这是反方向的交互:你的 C++ 组件主动调用 Pawn 脚本里定义的 public 函数。

8.1 理解 AMX 虚拟机

Pawn 脚本编译后是 .amx 文件,由 AMX 虚拟机执行。每个 public 函数在 AMX 里有一个编号(index)。要调用它,你需要:

  1. 找到编号 — 用 amx_FindPublic 或在 onAmxLoad 时扫描
  2. 压入参数 — 用 amx_Push(整数)或 amx_PushString(字符串)
  3. 执行 — 用 amx_Exec
  4. 清理 — 用 amx_Release 释放字符串内存
8.2 扫描 Public 函数

假设 Pawn 里有:

代码:
forward OnPlayerEnergyChange(playerid, oldEnergy, newEnergy);
public OnPlayerEnergyChange(playerid, oldEnergy, newEnergy)
{
    printf("玩家 %d 的查克拉发生了变化: %d -> %d", playerid, oldEnergy, newEnergy);
}
onAmxLoad 时扫描它:

代码:
// 保存 AMX 指针和 public index
struct ScriptInfo
{
    AMX* amx = nullptr;
    int onEnergyChange = -1;  // -1 表示该脚本里没有这个函数
};
std::vector<ScriptInfo> scripts_;
void onAmxLoad(IPawnScript& script) override
{
    pawn_natives::AmxLoad(script.GetAMX());
    AMX* amx = script.GetAMX();
    ScriptInfo info;
    info.amx = amx;
    // 方法一:按名称精确查找
    amx_FindPublic(amx, "OnPlayerEnergyChange", &info.onEnergyChange);
    // 如果找到,info.onEnergyChange >= 0
    // 如果没找到,info.onEnergyChange 保持 -1
    scripts_.push_back(info);
}
8.3 调用 Public 函数

找到 index 后,就可以调用了。AMX 是栈式虚拟机,参数要倒序压入。

调用无参数函数:

代码:
// Pawn 侧
forward OnSomethingHappen();
public OnSomethingHappen()
{
    print("damn!");
}
// C++ 侧
void callOnSomethingHappen(AMX* amx, int index)
{
    cell returnValue;
    amx_Exec(amx, &returnValue, index);
    // returnValue 是 Pawn 函数的返回值
}
调用带整数参数的函数:

代码:
// Pawn 侧
forward OnPlayerEnergyChange(playerid, oldEnergy, newEnergy);
public OnPlayerEnergyChange(playerid, oldEnergy, newEnergy)
{
    printf("玩家 %d 的查克拉发生了变化: %d -> %d", playerid, oldEnergy, newEnergy);
    return 1;
}
// C++ 侧
void callOnPlayerEnergyChange(AMX* amx, int index, int playerid, int oldEnergy, int newEnergy)
{
    // Pawn: OnPlayerEnergyChange(playerid, oldEnergy, newEnergy)
    // 倒序压栈
    amx_Push(amx, newEnergy);
    amx_Push(amx, oldEnergy);
    amx_Push(amx, playerid);
    cell returnValue;
    amx_Exec(amx, &returnValue, index);
}
调用带字符串参数的函数:

代码:
// Pawn 侧
forward OnPlayerMessage(playerid, const message[]);
public OnPlayerMessage(playerid, const message[])
{
    printf("Player %d: %s", playerid, message);
    return 1;
}
// C++ 侧
void callMessage(AMX* amx, int index, int playerid, const char* message)
{
    cell addr_message;
    amx_PushString(amx, &addr_message, nullptr, message, 0, 0);
    amx_Push(amx, playerid);
    cell retval;
    amx_Exec(amx, &retval, index);
    // 必须释放 addr_message 不然会导致内存泄漏
    amx_Release(amx, addr_message);
}
8.4 AMX API 速查表

可查阅:https://open.mp/docs/tutorials/PluginDev...-functions

代码:
| 函数 | 用途 |
|--|--|
| `amx_NumPublics(amx, &count)` | 获取 public 函数数量 |
| `amx_GetPublic(amx, index, name)` | 按 index 获取函数名 |
| `amx_FindPublic(amx, name, &index)` | 按名称查找 index |
| `amx_Push(amx, value)` | 压入整数参数 |
| `amx_PushString(amx, &addr, nullptr, str, 0, 0)` | 压入字符串参数 |
| `amx_Exec(amx, &retval, index)` | 执行 public 函数 |
| `amx_Release(amx, addr)` | 释放 PushString 分配的内存 |

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
9. 完整示例

下面是一个完整的组件,包含上述所有概念。功能很简单:追踪玩家的"查克拉",提供 native 函数给 Pawn 操作,并在查克拉变化时回调 Pawn。

9.1 C++ 组件(src/main.cpp)

代码:
#include <sdk.hpp>
#include <Server/Components/Pawn/pawn.hpp>
#include <Server/Components/Pawn/Impl/pawn_natives.hpp>
#include <Server/Components/Pawn/Impl/pawn_impl.hpp>
#include <vector>
#include <string>
// 玩家数据扩展(查克拉)
struct PlayerEnergy final : IExtension
{
    // 每个组件必须有一个全局唯一 ID
    // 去 https://open.mp/uid 生成一个替换下面的ID
    PROVIDE_EXT_UID(0x1A2B3C4D5E6F7890);
    int energy = 100;
    int maxEnergy = 100;
    void freeExtension() override { delete this; }
    void reset() override
    {
        energy = 100;
        maxEnergy = 100;
    }
};
// 组件主类
class EnergyComponent final
    : public IComponent
    , public PawnEventHandler
    , public PlayerConnectEventHandler
{
private:
    ICore* core_ = nullptr;
    IPawnComponent* pawn_ = nullptr;
    // 保存 AMX 脚本中 OnPlayerEnergyChange 的 index
    struct ScriptInfo
    {
        AMX* amx = nullptr;
        int onEnergyChange = -1;
    };
    std::vector<ScriptInfo> scripts_;
public:
    PROVIDE_UID(0x9F8E7D6C5B4A3210);
    StringView componentName() const override { return "EnergySystem"; }
    SemanticVersion componentVersion() const override { return SemanticVersion(1, 0, 0, 0); }
    void onLoad(ICore* c) override
    {
        core_ = c;
        setAmxLookups(core_);
        core_->printLn("[查克拉插件] 查克拉系统已加载.");
    }
    void onInit(IComponentList* components) override
    {
        pawn_ = components->queryComponent<IPawnComponent>();
        if (pawn_)
        {
            setAmxFunctions(pawn_->getAmxFunctions());
            setAmxLookups(components);
            pawn_->getEventDispatcher().addEventHandler(this);
        }
        core_->getPlayers().getPlayerConnectDispatcher().addEventHandler(this);
    }
    // Pawn 事件
    void onAmxLoad(IPawnScript& script) override
    {
        pawn_natives::AmxLoad(script.GetAMX());
        // 扫描 Pawn 脚本中的回调函数
        AMX* amx = script.GetAMX();
        ScriptInfo info;
        info.amx = amx;
        amx_FindPublic(amx, "OnPlayerEnergyChange", &info.onEnergyChange);
        scripts_.push_back(info);
    }
    void onAmxUnload(IPawnScript& script) override
    {
        AMX* amx = script.GetAMX();
        scripts_.erase(
            std::remove_if(scripts_.begin(), scripts_.end(), [amx](const ScriptInfo& s) { return s.amx == amx; }), scripts_.end());
    }
    // 玩家事件
    void onPlayerConnect(IPlayer& player) override
    {
        player.addExtension(new PlayerEnergy(), true);
    }
    // C++ 调用 Pawn 回调
    void callOnPlayerEnergyChange(int playerid, int oldEnergy, int newEnergy)
    {
        for (auto& s : scripts_)
        {
            if (s.onEnergyChange < 0)
                continue;
            // Pawn: OnPlayerEnergyChange(playerid, oldEnergy, newEnergy)
            // 倒序压栈
            amx_Push(s.amx, newEnergy);
            amx_Push(s.amx, oldEnergy);
            amx_Push(s.amx, playerid);
            cell retval;
            amx_Exec(s.amx, &retval, s.onEnergyChange);
        }
    }
    // 生命周期
    void onReady() override {}
    void onFree(IComponent* component) override
    {
        if (component == pawn_)
        {
            pawn_ = nullptr;
            setAmxFunctions();
            setAmxLookups();
        }
    }
    void free() override { delete this; }
    void reset() override {}
    ~EnergyComponent()
    {
        if (pawn_) pawn_->getEventDispatcher().removeEventHandler(this);
        if (core_) core_->getPlayers().getPlayerConnectDispatcher().removeEventHandler(this);
    }
};
// 全局指针,给 SCRIPT_API 用
static EnergyComponent* gComponent = nullptr;
COMPONENT_ENTRY_POINT()
{
    gComponent = new EnergyComponent();
    return gComponent;
}
// Native 函数
// native GetPlayerEnergy(playerid);
SCRIPT_API(GetPlayerEnergy, int(IPlayer& player))
{
    auto* data = queryExtension<PlayerEnergy>(player);
    return data ? data->energy : 0;
}
// native SetPlayerEnergy(playerid, energy);
SCRIPT_API(SetPlayerEnergy, bool(IPlayer& player, int energy))
{
    auto* data = queryExtension<PlayerEnergy>(player);
    if (!data)
        return false;
    int oldEnergy = data->energy;
    data->energy = (energy > data->maxEnergy) ? data->maxEnergy : energy;
    // 触发 Pawn 回调
    if (data->energy != oldEnergy && gComponent)
        gComponent->callOnPlayerEnergyChange(player.getID(), oldEnergy, data->energy);
    return true;
}
// native GetPlayerMaxEnergy(playerid);
SCRIPT_API(GetPlayerMaxEnergy, int(IPlayer& player))
{
    auto* data = queryExtension<PlayerEnergy>(player);
    return data ? data->maxEnergy : 0;
}
// native SetPlayerMaxEnergy(playerid, maxEnergy);
SCRIPT_API(SetPlayerMaxEnergy, bool(IPlayer& player, int maxEnergy))
{
    auto* data = queryExtension<PlayerEnergy>(player);
    if (!data)
        return false;
    data->maxEnergy = maxEnergy;
    if (data->energy > data->maxEnergy)
        data->energy = data->maxEnergy;
    return true;
}
9.2 Pawn Include 文件(energy.inc)

代码:
// 函数声明
native GetPlayerEnergy(playerid);
native SetPlayerEnergy(playerid, energy);
native GetPlayerMaxEnergy(playerid);
native SetPlayerMaxEnergy(playerid, maxEnergy);
// 回调声明
forward OnPlayerEnergyChange(playerid, oldEnergy, newEnergy);
9.3 Pawn 测试脚本

代码:
#include <open.mp>
#include "energy.inc"
main() {}
public OnPlayerConnect(playerid)
{
    // 连接时默认 energy = 100
    new energy = GetPlayerEnergy(playerid);
    printf("[查克拉系统] Player %d 进入了服务器, 查克拉 = %d", playerid, energy);
    // 设置为 50,会触发 OnPlayerEnergyChange
    SetPlayerEnergy(playerid, 50);
    return 1;
}
// C++ 组件在查克拉变化时回调这个函数
public OnPlayerEnergyChange(playerid, oldEnergy, newEnergy)
{
    printf("[回调] 玩家 %d 的查克拉发生了变化: %d -> %d", playerid, oldEnergy, newEnergy);
    return 1;
}
9.4 数据流向图

代码:
玩家连接
  │
  ├─ open.mp 触发 onPlayerConnect
  │  └─ C++ 创建 PlayerEnergy, 挂到玩家对象上
  │
  ├─ Pawn OnPlayerConnect 被调用
  │  └─ 调用 native SetPlayerEnergy(playerid, 50)
  │        │
  │        ├─ SCRIPT_API 自动解析 playerid → IPlayer&
  │        ├─ queryExtension<PlayerEnergy>(player) 取出数据
  │        ├─ 修改 energy: 100 → 50
  │        └─ callOnPlayerEnergyChange(playerid, 100, 50)
  │            │
  │            ├─ amx_Push(amx, 50)        ← newEnergy
  │            ├─ amx_Push(amx, 100)        ← oldEnergy
  │            ├─ amx_Push(amx, playerid)
  │            └─ amx_Exec → 调用 Pawn 的 OnPlayerEnergyChange
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
附录:资源
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯


  [转发][库] tdialogs 异步处理对话框
发布者: XiaoNiao - 03-02-2026, 12:46 PM - 板块: - 回复 (1)

tdialogs

tdialogs 是一个库,它通过 PawnPlus 任务系统的强大功能,提供了多种函数来在 open.mp 服务器中异步处理对话框响应。

作者: TommyB123
GitHub地址: https://github.com/TommyB123/tdialogs

依赖项

本库需要以下依赖项。 * PawnPlus * sscanf

安装

只需安装到您的项目中:

代码:
sampctl install TommyB123/tdialogs

在您的代码中包含并开始使用该库:

代码:
#include <tdialogs>

介绍

如上所述,tdialogs 是一个基于 PawnPlus 的库,可用于在单个 PAWN 函数中异步处理对话框响应。这里有一个简单的示例。

代码:
MyCoolFunction()
{
    yield 1;
    new response[DIALOG_RESPONSE];
    await_arr(response) ShowAsyncDialog(playerid, DIALOG_STYLE_LIST, "酷炫对话框", "条目 1\n条目 2\n条目 3", "不错!", "返回");
    if(response[DIALOG_RESPONSE_RESPONSE])
    {
        SendClientMessage(playerid, -1, "您点击了第 %i 行,并收到了消息:%s",
            response[DIALOG_RESPONSE_LISTITEM], response[DIALOG_RESPONSE_INPUTTEXT]); // 值可能分别为 1 和 "条目 2"
    }
    else
    {
        SendClientMessage(playerid, -1, "您关闭了对话框");
    }
}

如果您熟悉所有可用的对话框处理库,您可能会想知道这个库与它们有何不同。简单的答案是,我包含了大量函数来快速从对话框响应中获取特定/单个值。您只需要显示一个快速的是/否确认对话框吗?没问题。使用 ShowAsyncConfirmationDialog。需要从游戏内商店购买用户输入的数量吗?ShowAsyncNumberInputDialog 可以满足您的要求。示例用法可以在这里找到。

从 1.1.0 版本开始,自动分页对话框也被包含在内,可以通过 AddPaginatedDialogRow 和 ShowAsyncPaginatedDialog 创建。完整的函数参数和示例如下。

对话框数据

tdialogs 还有一个虽小但实用的功能,用于在整个对话框流程中跟踪 ID 或其他数据。每个玩家在登录时都会获得一个动态的 PawnPlus 列表 分配给他们。这个列表可用于存储诸如房屋索引、车辆 ID 或其他与对话框内文本行匹配的数据。这样做是为了保留标识符,并避免进行不必要的循环查找或其他成本较高的方法来获取您已经找到的数据。

除了这个列表之外,还有一个专门结合其使用而设计的对话框函数。当您格式化了一个对话框并将相应的实体 ID(例如房屋 ID)推送到 DialogData 列表后,可以使用 ShowAsyncEntityIndexDialog 通过一行代码获取玩家点击的房屋 ID。您可以在下面的示例部分看到它的实际应用。

需要注意的是,在显示新对话框之前,必须使用 list_clear 清理该列表。否则,先前显示的对话框中的数据会混入后续的对话框显示中。

常量


 常量                                              默认值                               描述 
  • TDIALOG_DIALOG_ID_BEGIN           1234                                  该库为每种对话框类型(不要与对话框样式混淆)预留了 9 个对话框 ID TDIALOG_DIALOG_ID_BEGIN 是第一个将被使用的 ID,随后依次是接下来的 8 个数字。请注意您已有的任何对话框 ID 并相应地进行调整
  • PAGINATED_NEXT_TEXT                 "--> 下一页"                        当分页对话框提示您查看下一页时将显示的字符串。[/td][/tr]
  • PAGINATED_PREVIOUS_TEXT          "<-- 上一页"                        当您在分页对话框中可以返回上一页时将显示的字符串。[/td][/tr]


前提条件

建议您在包含 PawnPlus 之前添加 #define PP_SYNTAX_AWAIT。这允许您使用本库编写时所考虑的正确的 await 方法。

同时建议也添加 #define PP_SYNTAX_YIELD。这是 task_yield 的一个快速别名,用于在函数等待任务时将临时值让出给函数。例如,如果您在命令中没有 yield 1;,您很可能会在聊天中收到错误的未知命令错误。

函数

以下所有函数都有 PawnPlus 字符串变体。ShowAsyncDialog 对应 ShowAsyncDialog_s,以此类推。

代码:
ShowPlayerDialog_s(playerid, dialogid, DIALOG_STYLE:style, ConstAmxString:title, ConstAmxString:body, const button1[], const button2[])

ShowPlayerDialog 的简单 PawnPlus 字符串封装。

代码:
ShowAsyncDialog(playerid, DIALOG_STYLE:style, const title[], const body[], const button1[], const button2[] = "")

显示一个异步对话框,返回完整的对话框响应数组。

与 await_arr(response) ShowAsyncDialog(...) 一起使用。

代码:
ShowAsyncNumberInputDialog(playerid, const title[], const body[], const button1[], const button2[])

显示一个专门用于解析数字输入的异步对话框。

如果未接收到整数,对话框将简单地再次显示。

如果玩家关闭对话框/按下 ESC,则返回 cellmin。

与 new number = await ShowAsyncNumberInputDialog(...) 一起使用。

代码:
ShowAsyncFloatInputDialog(playerid, const title[], const body[], const button1[], const button2[])

显示一个专门用于解析数字输入的异步对话框。

如果未接收到浮点数,对话框将简单地再次显示。

如果玩家关闭对话框/按下 ESC,则返回 FLOAT_NAN。

与 new Float:number = Float:await ShowAsyncFloatInputDialog(...) 一起使用。

代码:
ShowAsyncStringInputDialog(playerid, const title[], const body[], const button1[], const button2[])

显示一个专门用于解析字符串输入的异步对话框。

如果接收到空字符串,对话框将简单地再次显示。

如果玩家关闭对话框/按下 ESC,则返回空字符串。使用 isnull 进行检查。

与 await_str(string) ShowAsyncStringInputDialog(...) 一起使用。

代码:
ShowAsyncPasswordDialog(playerid, const title[], const body[], const button1[], const button2[])

与 ShowAsyncStringInputDialog 相同,但使用密码对话框样式。

代码:
ShowAsyncListitemTextDialog(playerid, DIALOG_STYLE:style, const title[], const body[], const button1[], const button2[])

显示一个异步对话框,仅返回通过列表项传递的文本。

如果玩家关闭对话框/按下 ESC,则返回空字符串。使用 isnull 进行检查。

与 await_str(string) ShowAsyncListitemTextDialog(...) 一起使用。

代码:
ShowAsyncListitemIndexDialog(playerid, DIALOG_STYLE:style, const title[], const body[], const button1[], const button2[])

显示一个异步对话框,仅返回被点击的列表项的索引。

如果玩家关闭对话框/按下 ESC,则返回 -1。

与 new index = await ShowAsyncListitemIndexDialog(...) 一起使用。

代码:
ShowAsyncConfirmationDialog(playerid, const title[], const body[], const button1[], const button2[] = "")

显示一个异步对话框,仅将响应状态作为布尔值返回。

与 new bool:confirm = bool:await ShowAsyncConfirmationDialog(...) 一起使用。

代码:
ShowAsyncEntityIndexDialog(playerid, DIALOG_STYLE:style, const title[], const body[], const button1[], const button2[])

显示一个异步对话框,返回在玩家 DialogData 列表中位于 listitem 位置的值。在此处或下面的示例中阅读更多信息。

如果玩家关闭对话框/按下 ESC,则返回 -1。

与 new id = await ShowAsyncEntityIndexDialog(...) 一起使用。

代码:
ShowAsyncPaginatedDialog(playerid, DIALOG_STYLE:style, rows_per_page, const title[], const button1[], const button2[], const tablist_header_text[] = "")

显示一个异步对话框,根据提供的每页行数对提供的文本进行分页。返回完整的对话框响应。

此函数仅兼容 DIALOG_STYLE_TABLIST_HEADERS、DIALOG_STYLE_TABLIST 和 DIALOG_STYLE_LIST。

tablist_header_text 参数仅用于 DIALOG_STYLE_TABLIST_HEADERS,顾名思义,是对话框的标题头文本。

与 await_arr(response) ShowAsyncPaginatedDialog 一起使用。

代码:
AddPaginatedDialogRow(playerid, const text[], extraid = 0)

向分页对话框添加一行。extraid 参数可用于传递与您追加的文本行相关的额外 ID。

构建分页对话框时必需。

示例

这里有一些稍微深入一点的示例。

快速确认用户是否想出售他们的房子。

代码:
CMD:sellmyhouse(playerid)
{
    if(PlayerOwnsHouse(playerid))
    {
        task_yield(1);
        new bool:confirm = bool:await ShowAsyncConfirmationDialog(playerid, "出售您的房子?", "您确定要出售您的房子吗?", "是", "否");
        if(confirm)
        {
            SellPlayerHouse(playerid);
        }
    }
}

输入您想从商店购买多少鱼饵。

代码:
new baitamount = await ShowAsyncNumberInputDialog(playerid, "鱼饵数量", "在下面输入您想购买的鱼饵数量", "购买", "取消");
if(baitamount == cellmin) return false; // 当玩家取消对话框时返回 cellmin
if(baitamount <= 0 || baitamount > 10000)
{
    SendClientMessage(playerid, -1, "无效的鱼饵数量。");
    return false;
}
GivePlayerFishBait(playerid, baitamount);
return true;

通过使用 DialogData 列表从对话框中解析出房屋 ID。

代码:
ShowHouseTeleportDialog(playerid)
{
    new string[256], substring[64];
    list_clear(DialogData[playerid]); // 清空持久的 DialogData 列表,使其与我们即将显示的对话框中的行数匹配
    for(new i = 0; i < MAX_HOUSES; ++i)
    {
        if(HouseData[i][HouseOwner] == playerid)
        {
            // 将房屋名称追加到字符串,并将房屋索引添加到 DialogData 列表。
            // 这样做是为了使房屋名称行与任意的房屋 ID 保持一致。
            // 可能有 500 栋房子,但玩家只拥有其中少数(甚至一栋)。以这种方式存储
            // 房屋索引让我们能够快速获取正确的 ID,而无需进行额外的、不必要的查找。
            format(substring, sizeof(substring), "%s\n", HouseData[i][HouseAddress]);
            strcat(string, substring);
            list_add(DialogData[playerid], i);
        }
    }
    new houseid = await ShowAsyncEntityIndexDialog(playerid, DIALOG_STYLE_LIST, "传送到您的家", string, "传送!", "算了");
    if(houseid != -1) // 如果玩家取消对话框,返回 -1
    {
        SetPlayerPos(playerid, HouseData[houseid][HouseX], HouseData[houseid][HouseY], HouseData[houseid][HouseZ]);
    }
}

显示一个每页 10 行的分页对话框。

代码:
ShowOwnedVehiclesDialog(playerid)
{
    new string[128];
    list_clear(DialogData[playerid]);
    for(new i = 1; i <= MAX_VEHICLES; ++i)
    {
        if(PlayerOwnsVehicle(playerid, i))
        {
            format(string, sizeof(string), "%s\t%s\n", ReturnVehicleName(i), ReturnVehicleLocation(i));
            AddPaginatedDialogRow(playerid, string, i);
        }
    }
    new response[DIALOG_RESPONSE];
    await_arr(response) ShowAsyncPaginatedDialog(playerid, DIALOG_STYLE_TABLIST_HEADERS, 10, "您的车辆", "传送", "取消", "车辆\t位置");
    if(!response[DIALOG_RESPONSE_RESPONSE]) return false;
    new vehicleid = response[DIALOG_RESPONSE_EXTRAID], Float:x, Float:y, Float:z;
    GetVehiclePos(vehicleid, x, y, z);
    SetPlayerPos(playerid, x, y, z);
    SendClientMessage(playerid, -1, "您已传送到您的 %s", ReturnVehicleName(vehicleid));
}

致谢

TommyB123

特别感谢 Graber。在使用他的原创 async dialogs 库多年后,我才受到启发制作了自己的库。如果您不需要我的库提供的额外功能,可以考虑使用他的库。


  [转载][工具]DAWNO - PAWN 编辑器
发布者: XiaoNiao - 03-02-2026, 11:27 AM - 板块: 发布 - 暂无回复

# DAWNO - PAWN 编辑器

## 作者

由 deksdeveloper 开发。
更多信息,请访问 [GitHub 仓库](https://github.com/deksdeveloper/dawno)。

DAWNO 是一款现代化、注重性能的 PAWN 代码编辑器,专为 SA-MP(圣安地列斯多人游戏)和 open.mp 开发者设计。它基于 Next.js、Electron 和 Monaco 编辑器技术构建,是经典 Pawno 编辑器的现代替代品。

## 概述

传统 PAWN 编辑过程因工具过时而显得繁琐。DAWNO 弥补了这一不足,提供了一个专业环境,在保持针对圣安地列斯模组开发的轻量级特性的同时,兼具了 Visual Studio Code 等现代代码编辑器的功能和美学。

## 主要特点

- **现代化用户界面与体验**:采用 VS Code 风格的时尚界面,支持原生深色模式,带来高级质感。
- **高级 Monaco 编辑器**:高性能文本编辑,具备语法高亮、智能代码补全和平滑滚动功能。
- **自动化检测**:深度扫描(最多 4 级目录),自动识别服务器可执行文件(samp-server.exe, omp-server.exe)和配置文件(server.cfg, config.json)。
- **集成服务器管理器**:直接在编辑器中控制服务器,支持启动、停止、重启操作,并实时查看控制台日志。
- **配置编辑器**:提供专用的表格界面管理服务器设置,无需手动编辑文本文件。
- **编码支持**:全面的字符编码列表,包括对土耳其语(Windows-1254)等的完整支持,并支持即时切换编码。
- **Discord 富文本状态**:自动更新您的 Discord 状态,显示当前正在处理的项目和文件。
- **多语言支持**:内置国际化(i18n)功能,开箱即支持英语、土耳其语和德语。

## 国际化 (i18n)

DAWNO 通过基于 React Context 的自定义 i18n 解决方案支持多种语言。开发者可以通过在 `renderer/i18n/locales/` 中添加新的语言文件来轻松扩展语言支持。

当前支持的语言:
- 英语
- 土耳其语
- 德语

## 许可证

本项目采用 MIT 许可证。详情请参阅 LICENSE 文件。


  [插件] kook-connect
发布者: siwode - 02-28-2026, 07:05 PM - 板块: 插件 - 暂无回复

.


  [转载]SA-MP/OpenMP反作弊系统 by Pevenaider
发布者: XiaoNiao - 02-28-2026, 04:08 PM - 板块: 脚本 - 暂无回复

一款基于SendClientCheck函数(类型0x5)的反作弊系统。该系统专为检测玩家修改行为而设计,可识别以下工具:S0beit, Sobfox.asi, Sobfox Launcher, CLEO 4, CLEO 5 with Ultimate ASI Loader, MoonLoader, SilentPatch, SampFuncs, modified VorbisFile.dll, UltraWH, Improved Deagle, StealthRemastered, Sensfix, SilentAim v8 和 FakeMobile

支持: open.mp | sa-mp

地址:https://github.com/Pevenaider/AntyCheat


  [库][分享转发] sa-mp/pawn 标准库包
发布者: XiaoNiao - 02-28-2026, 02:39 PM - 板块: - 暂无回复

samp-stdlib | pawn-stdlib

默认 sa-mp 服务器包含文件的更新版本。支持const校验、文档齐全且完整

下载地址: 

 - https://github.com/pawn-lang/pawn-stdlib

 - https://github.com/pawn-lang/samp-stdlib

San Andreas Multiplayer Pawn 标准库包 - 专为 sampctl 包管理系统 设计。

当前最新版本为 0.3.7-R2-2-1 | 0.3.DL-R1

master 分支头部目前不包含任何 RC 库。

为什么?

sampctl 工具内置的包管理系统基于 GitHub(类似于 Go 语言),因此将标准库也存储在 GitHub 上可以简化流程。这意味着不需要为标准库编写任何特殊代码,它可以像其他所有包一样只是一个普通包。

版本如何运作?

版本通过 Git 标签 表示

使用 sampctl 时始终指定一个版本。要依赖最新版本,只需在依赖项中使用 sampctl/samp-stdlib:0.3.7-R2-2-1。

发布候选版本(RC)期间会发生什么?

master 分支将包含 RC 库(如果存在活跃的 RC)。最近的 RC 是 0.3.8,但由于该版本已取消,此代码库不包含任何 0.3.8 RC 库。

版本标签与 sampctl 完全兼容

sampctl 简化了版本控制,此代码库的存在是为了帮助使用多个 SA:MP 服务器版本的 sampctl 用户。

版本起始于 0.3z(具体为 0.3z-R1)

0.3z 是 sampctl 支持的最早版本。大多数服务器使用最新版本,因此似乎没有必要归档每个 SA:MP 库版本。

某些标签指向同一提交

有些版本仅更改了服务器的内部机制,从未涉及库文件。在这些情况下,版本标签仍存在于该代码库中,但它们仅指向同一提交。


  SinglePlayer-Role Play 1.0.5
发布者: XiaoNiao - 02-28-2026, 02:16 PM - 板块: 游戏模式 - 暂无回复

SP-RP 1.0.5

SP-RP 下载地址

这是于2019年编写的SOLS游戏模式演变而来的游戏模式,后来它逐步发展为GTA-C,并在2024年末转变为现在的SP-RP。

这是一个由众多热情开发者为一个不知感恩的社区倾力打造的激情项目。这个社区多年来免费享受游戏,而开发者们却夜以继日地工作,投入大量资源,却换来社区成员试图泄露制作团队个人信息的回报。

我希望通过发布这个脚本,人们能够学习我们的一些技术、创新解决方案,并从我们的错误中吸取教训。它在底层不算特别出色,但在其巅峰时期,曾同时托管超过200名玩家。

在发布这个脚本时,我们共有2300次提交和8位GitHub合作者。感谢这些人让它变得如此出色。

致谢

主要贡献者:

  • DignitySAMP / PXL-DavyL:主要贡献者 / 项目负责人和创建者
  • Spooky/Sporky:贡献了从SOLS到GTAC的转变,包括数百项游戏体验优化
  • Rusu:整个项目期间的主要贡献者,专注于bug修复和游戏体验优化

次要贡献者:
  • Matz(篮球和卡车运输系统)
  • LorenC(台球脚本、扑克脚本等,全部由他为SFCNR编写)
  • LS-RP (DamianC):2011年之前的家具数组
  • PatrickGTR, Matz, DenNorske, Kubi 和 Patryk:开源了服务器使用的小型系统
  • Tramposo(次要游戏体验优化)
  • DTVSamp(次要游戏体验优化)
  • Shabo/plsegott(旧的物品道具系统,用于啤酒和食物,后来被重做,但脚本中仍有部分残留)

特别感谢:
  • TommyB123(更新SQL、YSI、主要优化、初始化sampctl)
  • DenNorske(帮助我们修复无法自行解决的问题,通常是隐藏的致命开关或无限循环,以及后来的DDoS保护调整)

概述

对于任何有经验的开发者来说,这是一个即插即用的脚本。它使用sampctl进行插件管理。我们提供了数据库结构以及运行所需的模型。不会提供如何运行它的支持。如果你知道自己在做什么,就不会有问题。我大约一个月前开始为代码添加文档,但许多功能仍然缺少关键说明。祝你好运解读它。

有一些小bug,但这个脚本已在生产环境中稳定运行超过5年。尽管如此,如果你想托管它,请投资一台专用服务器,至少配备3GB RAM和强大的CPU性能。车辆模块严重过时,会严重占用资源。车辆模块表现欠佳,但提交ce3fb3105057dfb25a027d9824b6c0f97950368d和69a4128b3ec070163073cd3381e16b1516db9b92包含了大幅提升性能的更改。目前,许多车辆原生函数仍使用for循环获取数据,这效率低下,因此在视为可接受之前,应将其转换为带有缓存机制的迭代器。

我会说脚本的80-85%是动态的,即数据保存在MySQL中。它支持UCP,你可以在这里找到配套的UCP:https://github.com/rzrusu/SPRP-UCP 由@rzrusu制作。我不推荐直接以此游戏模式作为基础,但经过一些完善,它几乎是任何中等规模角色扮演服务器的理想选择。

原始仓库保持私有状态。其中包含不宜公开的敏感数据提交。

功能

我们在2024年末正更新脚本至更现代版本。有些模块仍严重过时。我试图概述这里的内容,但要列举的东西实在太多。简而言之,这个脚本包含了你可能需要的一切,经过多年游戏体验优化的积累:

动态财产系统
  • 房屋和企业均属于财产范畴。
  • 每个企业在玩家使用/buy时都会生成脚本化的收入。
  • 全面的家具系统(仅限室内,玩家无需管理员即可升级室内装饰)。
  • 衣柜系统(保存你的皮肤,让你在房屋内轻松切换)。
  • 附近枪声警报系统(如果有人在房屋外开枪,你会收到通知)。
  • 房地产经纪系统:财产会随时间自动出售,并可在/realtor处集中查看。
  • 财产抢劫脚本,由PatrickGTR为开放CNR编写(包含EnEx模型和可抢劫的演员)。
  • 财产特定操作(/doorme, /doordo, /doorknock, /doorbell, /doorshout)。
  • 财产存储功能,用于存放枪支和毒品。
  • 众多小巧的游戏体验优化,如彩色财产名称等。
  • 大量管理员和用户命令,提供高度灵活性(管理员可修改企业的各方面设置)。
  • 丰富的购买类型,请查看/buy及其相关函数。

动态车辆系统
  • 支持静态车辆和玩家拥有的车辆。
  • 车辆改装系统,与改装店财产联动。涵盖游戏中所有可能的组件。
  • 车辆损坏保存,在停车后生效。
  • 服务器端喷漆系统,支持喷漆作业和所有单人模式颜色。
  • 基础燃料系统,包含动态燃料泵放置(对象)和可拥有的加油站。加油过程有专用界面。
  • 独特的单人模式风格车辆GUI,可在/settings中切换。
  • 汽车报废、转让和出售功能。
  • 扣押系统,专供LSPD使用。
  • 基础空中交通控制系统,用于飞机与LSPD通信。
  • 基础租车系统。
  • /rprf积分系统,用于工作、派系等,可修复和修理车辆。
  • 基础静态经销商系统,出售所有非过强车辆,价格与服务器经济匹配。
  • DMV系统,包含理论考试。
  • 服务器端颜色和重生系统(略有bug,此模块需重写)。
  • 车辆窗户脚本,可阻挡聊天,并与所有本地聊天同步。

动态派系系统
  • 全面成员管理(支持离线玩家)。
  • 派系层级决定玩家权限(3级最低,2级指挥,1级领导)。
  • 动态派系等级:无硬编码限制,使用/setrank id title。
  • 基础派系银行系统------功能正常,但未过多强调。
  • 派系皮肤半动态,但需重做。通过命令添加皮肤,并在派系重生点显示。
  • 军械库系统,专供LSPD,根据小队和层级获取枪支。
  • 通信系统(f聊天需管理员授权,2级以上可切换;派系寻呼机用于IC通信;LSPD无线电)。
  • 派系重生点,允许更改皮肤并在首次登录时重生于此。
  • LSPD小队系统。
  • 众多管理员和玩家命令,提供系统灵活性,无法一一列举。

小游戏
  • 台球脚本:由LorenC为SFCNR编写,移植至本游戏模式,仅作微调。
  • 篮球系统:由Matz编写的过时模块,模拟单人模式篮球游戏。虽旧且未优化,但功能正常。
  • 扑克脚本:曾正常运行,更改文本绘制处理后失效。可尝试修复,但不保证成功。从未投入时间修复。
  • /shakehands:包含游戏中大多数问候动作,并附带描述。

工作系统
  • 码头工人:乘坐叉车在3个点间往返。支付随机,连胜可增收至上限。这是入门级工作,便于起步,适合AFK。
  • 垃圾收集:进入垃圾车,在城市捡拾垃圾(地图标记),用LALT步行拾取并放入车中。每件支付。这是入门至中级工作,强度较高但可AFK。
  • 卡车运输:高级工作。需投资取货(从批发商),运至商店出售获利。长期收益最高。

伤害系统
  • 服务器端伤害计算,根据武器子弹、基础伤害(硬编码)、击中部位和距离。
  • 伤害模式和处决系统。
  • 电击枪、擒拿和豆袋弹系统,专供LSPD。
  • 与SKY插件同步的刀伤害。
  • 基础/damagelist显示伤害列表。
  • 手臂中枪:降低武器技能,屏幕抖动。
  • 腿部中枪:绊倒,无法冲刺或跳跃。
  • /surgery在医院治愈所有伤势。

健身房脚本
  • 受单人模式启发的健身房系统。
  • 可训练肌肉和耐力,消耗饥饿、口渴和能量。
  • 饥饿和口渴通过餐厅购买食物自动补充。
  • 能量随时间自动恢复。
  • 单人模式弹出界面,带进度条和绿/红指示(增/减)。
  • SPRINT+C 或 /gymstats 显示当前统计框。
  • 根据肌肉和耐力,提升近战伤害并降低所受近战伤害。
  • 最大化耐力和近战可获自定义战斗风格(管理员也可设置,早起捐赠包功能)。
  • 所有健身房位置硬编码。

武器系统
  • 服务器端武器,支持自定义枪名。
  • 服务器端弹药,影响伤害计算。易添加或修改。
  • 武器在/q后保存,必要时移除(医院、监狱、ajail、ban、伤害模式中/q等),可存于车辆或财产。
  • 动态经销商脚本:派系可分配"emmet",自动冷却与补充,如枪柜,派系成员优惠价。
  • Emmet箱子脚本:派系在经销商处获/emmettip,有几率生成强力武器箱。类似Fortnite箱子:在150随机位置生成,用/emmetcrate抢劫。警察可没收,其他玩家可偶遇抢夺。靠近时小地图显示蓝色骷髅。
  • 未用"visible.pwn":原型(有bug),用于可见武器附着。若启用,先调整附件限制。
  • 服务器端驾驶射击:动画中用H返回车内。
  • 滥用时,管理员用/setgunrights禁枪,阻止使用。
  • 服务器端ID和弹药,便于取消无效数据,杜绝武器作弊。

dp系统
  • dp1:购种子种植。随时间成长,多阶段,有生病几率。使用渐愈伤势。
  • dp2:同dp1。使用增近战伤害。
  • dp3:同上。使用获临时护甲,持续补充。
  • 效果时长依品种和用量。
  • 分装系统,将dp分至不同容器。从杂货店或餐厅购容器。

拆车场
  • 带车至此赚快钱,模拟报废损坏车辆,然后开车离开获奖励。
  • 可派系拥有,抽取小额分成。

偷车系统
  • 撬锁和热线,带GUI小游戏。
  • 受GTA-W旧系统启发,仿真现实。

音箱系统
  • 流官方电台或自定义链接。
  • 通过SAMP对象编辑器放置,显示标签。
  • 所有者退出后,15分钟内可认领。
  • 管理员可移除。
  • 使用proxdetector通知附近玩家无人认领或移除。
  • 多年调试,确保与车辆/财产电台自动同步优先。

反作弊系统
  • 全面反作弊,2024年重写用Raknet。
  • 可切换部分反作弊(2024 WIP,未完成)。
  • 支持暂停玩家检测。
  • 检测作弊包括:
    • AFK
    • 空中破墙
    • 机器人连接(假连接)
    • 反兔跳
    • 偷车检测
    • 车辆改装作弊
    • 车辆粒子作弊
    • 车辆喷漆作弊
    • 车辆摇摆作弊
    • 车辆恶搞作弊
    • 车辆传送检测
    • 对话框防护(防崩溃、伪造等)
    • 双连接检测(登录已连账户或共享IP新连接)
    • 简单封禁规避脚本(扫描封禁玩家子网,匹配警报管理员)
    • 假死亡检测
    • 生命作弊检测(WIP,非近战武器易误报)
    • 金钱作弊检测
    • 无换弹作弊(含滚轮规避检测)
    • 快速射击检测
    • 监视检测
    • 喷气背包检测
    • 传送作弊检测
    • 弹药作弊
    • 武器作弊

附件系统
  • 动态购买点,可链接财产,业主获收入。
  • 购对象附着玩家。
  • 保存玩具及最后编辑位置于/q。
  • 用SAMP附件编辑系统。
  • 基础但实用。
  • 辅助函数识别骨骼、槽位等(与其他模块同步)。

动画系统
  • 基础动画模块。
  • 分类动画,带选项,每文件单动画。
  • "粒子"动画,如/piss, /shit, /puke, /shakebottle带效果。
  • 反滥用/stopanim(检测用于规避近战眩晕)。
  • 感谢Emmet_, Reyo, niCe, BigBear 和 DamianC(借鉴部分LS-RP动画)。

管理员系统
  • 数百命令,覆盖一切。
  • 监视、监听等系统。
  • 其他系统额外命令。
  • 报告存储,管理员获提醒(/ar接受,/dr拒绝)。
  • 基础级别系统,带标题、/ahide等。
  • 功能繁多,无法尽述。

助手系统
  • 问题经/ask提交,助手用/ah接受、/answer回复。
  • 问题存储,助手获提醒。
  • 部分小命令可用,见/staffhelp。

贡献者系统
  • 邀请加入/admin聊天,有/无权限(无、初级、管理者=隐藏管理员)。

喷漆标签系统
  • 静态喷漆,仿单人模式。
  • 自定义文本喷漆,带颜色系统。
  • 管理员追踪并擦除。
  • LSPD也可擦除。

手机脚本
  • 可点击文本绘制手机系统。
  • 保存联系人,用名呼叫/短信。
  • 基于对话/文本或基本命令。
  • 自定义背景、铃声、颜色。

警察脚本
  • 支持/carsign。
  • 小队和军械库。
  • Spooky建模的精美LSPD室内。
  • /bk 和 /panic(小地图标记)。
  • 全刑法代码,与/charge /ticket同步。
  • 复杂罚单系统,自动过期需支付。
  • 自定义0.3DL警笛光效。
  • 自定义/siren,与光效同步。
  • 间谍脚本。
  • 钉刺脚本。
  • 隔音审讯室。
  • 枪架。
  • 无MDC,但有命令查询财产、角色和车辆信息。
  • 复杂对话/请求搜身系统。

杀手脚本
  • 当前未启用:拨666创建合同。
  • 杀手访问呼叫者,手动菜单输入数据防元游戏。
  • 接受后标记目标。
  • 标记下击杀目标,禁用角色24小时。

玩家账户系统
  • 游戏内注册。
  • 基础教程讲解要领。
  • UCP支持邮件提示。
  • 最多5角色槽。
  • 复杂重生系统:财产、派系、公共点或最后位置。
  • 添加角色属性(身高、体重、眼色、发色、体型等)=> /examine系统。
  • /settings自定义界面设置,按玩家。
  • 众多其他功能,无法尽列。

大门系统
  • 创建自定义门、大门、车库门。
  • 分配派系、财产或玩家。
  • 支持动画或自动开。
  • 可转为收费站,警察/policetolls。
  • 支持自动关闭。

通行点系统
  • 添加/pass点,用于后门或后窗。
  • 全自定义:范围、颜色、名称、使用权限。
  • 支持步行或车辆通行(依类型)。

GPS系统
  • 导航至兴趣点。
  • 至enex /buy点。
  • 至房屋或财产。
  • 总找最近实体并排序显示。

自定义皮肤系统
  • 无需重启加载:拖文件至正确文件夹。
  • 特定文件名/目录格式解析baseid和character id。
  • /wardrobe可用。
  • 设计用于UCP上传,但无需也可,手动放置文件即可。
  • 自定义错误处理、缓存、加载容器。

自定义姓名标签系统
  • 玩家切换名/姓。
  • Raknet驱动。
  • 同步计分板、标签列表、proxdetector。
  • 自定义sscanf处理兼容玩家ID、氏族标签、firstname_lastname。
  • 易配置:移除氏族选项即禁用切换。
  • 首次重生或/settings选择偏好。

新闻系统
  • /breaking突发新闻(管理员/abreaking)。
  • 直播邀请脱口秀(/broadcasts调入/出)。
  • 派系类型=新闻。
  • 为企业加/mic支持。

玩家日志系统
  • SA-MP最先进日志,由Spooky编写。
  • 游戏内浏览器,按类型排序。
  • 全脚本使用,存储关键数据。
  • 显示最后75条。
  • 支持全日志、在线/离线玩家日志(/plogs, /plog, /oplog)。
  • 管理员级别限制。

服务器配置
  • 动态存储信息:
  • 服务器年份时间。
  • 公共/管理员motd。
  • 登录音乐。
  • 管理员聊天十六进制色。
  • 其他次要设置。

可放置/可持有物品系统
  • 玩家动态放置或持有对象。
  • 适用于举办BBQ或事件:/spawn自行映射对象。
  • 酒吧中/hold装备啤酒瓶或玻璃。还有食物等RP辅助物品。
  • Spooky制作:最佳沉浸方式,玩家自置RP对象或持道具,提升截图和环境氛围。


  [教程] open.mp/sa-mp 服务器开发规范
发布者: XiaoNiao - 02-28-2026, 11:55 AM - 板块: 教程 - 暂无回复

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)


  [教程] 代码优化 #2
发布者: XiaoNiao - 02-28-2026, 11:46 AM - 板块: 教程 - 回复 (1)

内容

本线程由 Y_LESS 编写,我不对其内容负责。我只是希望重新发布它,因为它可以帮助许多脚本编写者。

目录


引言

这些只是我在路上学到的一些让你的代码运行得更快的技巧。请注意,我绝不自称是这个领域的权威,这些只是我所知道的,其他人可能知道其他方法,在这种情况下请务必分享它们,因为即使没有其他人关心,我也会有兴趣了解它们。还要注意,这些技巧中有许多适用于 PAWN 以外的语言(我写的最后一个技巧被指责为愚蠢,因为 PAWN 没有动态内存分配(我个人认为这是一件好事,但就这样吧)),然而很多其他语言可能会将其中一些技巧融入编译器中,以进行内联优化。

这并不保证能让你的代码变得优秀,只是希望能变得更好。另外请注意,有些部分相当复杂,它针对的是更高级的教程,所以它在某些领域确实假设了一些知识。

测试

首先,我将解释我的测试过程。如果我有两段代码做同样的事情,但方式不同,我想知道哪段更快,我会对它们进行计时:

代码:
#define CODE_1 printf("%d", 42);
#define CODE_2 new str[4]; format(str, sizeof (str), "%d", 42); print(str);
#define ITERATIONS (10000)

Test()
{
    new
        t0,
        t1,
        t2,
        i;
    t0 = GetTickCount();
    for (i = 0; i < ITERATIONS; i++)
    {
        CODE_1
    }
    t1 = GetTickCount();
    for (i = 0; i < ITERATIONS; i++)
    {
        CODE_2
    }
    t2 = GetTickCount();
    printf("时间1: %04d, 时间2: %04d", t1 - t0, t2 - t1);
}

显然,这两段代码都会在服务器控制台中显示数字"42",但它们以不同的方式实现。希望没有人需要运行这段代码来知道哪种方法更快,但这是一个很好的简单例子,用来测试两段等效代码哪段更快。ITERATIONS 循环很重要,这两段代码很可能各自耗时不到一毫秒,所以它们报告的时间都将是零。而且,如果你只做一次,线程会成为主要问题,如果一个版本被操作系统中断,它可能会报告自己花费了相当长的时间,而实际上它更快。如果两者都做了很多很多次,那么中断有望相互抵消,并且每个循环将耗时超过一毫秒。代码的布局也很重要,所有变量都首先声明,以将其开销移到循环之外(它们的执行时间如此之短,可能不会影响结果,但为了保持一致,这样很好)。

有时将 Test 函数包装在另一个循环中也很好,特别是当结果非常接近时,以验证结果。由于线程的原因,执行时间可能会有轻微变化,如果你多次运行测试,可以看到这种变化,表现为每次时间可能有几毫秒的差异。如果你重复运行接近的结果,就可以检查哪一个持续更快,而不是只快一次,因为那可能是一次偶然,90% 的时间另一个更快,只是那一次不是。

有时你可能需要更高级的测试代码,例如测试两个以上的等效代码,这很容易扩展:

代码:
#define CODE_1 printf("%d", 42);
#define CODE_2 new str[4]; format(str, sizeof (str), "%d", 42); print(str);
#define CODE_3 print("42");
#define ITERATIONS (10000)

Test()
{
    new
        t0,
        t1,
        t2,
        t3,
        i;
    t0 = GetTickCount();
    for (i = 0; i < ITERATIONS; i++)
    {
        CODE_1
    }
    t1 = GetTickCount();
    for (i = 0; i < ITERATIONS; i++)
    {
        CODE_2
    }
    t2 = GetTickCount();
    for (i = 0; i < ITERATIONS; i++)
    {
        CODE_3
    }
    t3 = GetTickCount();
    printf("时间1: %04d, 时间2: %04d, 时间3: %04d", t1 - t0, t2 - t1, t3 - t2);
}

等等...

foreach

我最近对我的 foreach 函数与默认的 for/IsPlayerConnected (IPC) 循环进行了计时。foreach 使用玩家的链接列表,因此循环时遍历连接的玩家,而 IPC 代码则遍历所有玩家并检查他们是否连接。我知道在玩家不多的大型服务器上它更快,但我不确定在服务器满员的情况下。所以我进行了计时。我没有 200 名玩家进行测试,但我知道 IsPlayerConnected 的运行时间无论返回 true 还是 false 都差不多(它基本上只是返回服务器中的一个变量,该变量可以是任一值),所以这不是问题,因为对于给定数量的玩家,IPC 将以恒定速度运行,无论他们是否在线。foreach 的运行方式很大程度上取决于有多少玩家在线,在没有玩家时几乎不花时间,而在服务器满员时需要更长时间,我只是想知道这个最长执行时间是否比 IPC 运行的恒定时间更长。我实际上想分析所有玩家数量下的性能,这意味着在 foreach 中模拟玩家连接,这并不难,因为它是我的代码,你只需调用一个连接函数。这个的测试代码最终看起来像这样:

代码:
#define FAKE_MAX 200
#define SKIP 0
Iter_Create(P2, FAKE_MAX);
*
TestFunc()
{
    new
        fep = 0,
        fet = 0,
        fip = 0,
        fit = 0,
        i = 0;
    while (i < SKIP)
    {
        Itter_Add(P2, i++);
    }
    while (i <= FAKE_MAX)
    {
        new
            t0,
            t1,
            t2,
            j;
        t0 = GetTickCount();
        for (j = 0; j < 10000; j++)
        {
            for (new playerid = 0; playerid < FAKE_MAX; playerid++)
            {
                if (IsPlayerConnected(playerid))
                {
                    // 做点什么
                }
            }
        }
        t1 = GetTickCount();
        for (j = 0; j < 10000; j++)
        {
            foreach(P2, playerid)
            {
                // 做点什么
            }
        }
        t2 = GetTickCount();
        printf("玩家数: %04d, for: %04d, foreach: %04d", i, t1 - t0, t2 - t1);
        fit = fit + t1 - t0;
        fet = fet + t2 - t1;
        fip += FAKE_MAX;
        fep += i;
        if (i < FAKE_MAX)
        {
            Itter_Add(P2, i);
        }
        i++;
    }
    printf("for ms/p: %04d, foreach ms/p: %04d", (fit * 100) / fip, (fet * 100) / fep);
}

这段代码运行了 201 次,每个玩家数量(0-200)一次(对于 foreach 和 IPC,哪个玩家在线并不影响速度)。它还允许我模拟更多玩家,例如测试这段代码在具有 500 名玩家的 0.3 服务器上如何运行,如果你测试的玩家不存在,IPC 实际上会稍微快一点,但它仍然较慢,所以这相当有结论性。我没有运行的一个测试是:

代码:
t0 = GetTickCount();
        for (j = 0; j < 10000; j++)
        {
            for (new playerid = 0; playerid < FAKE_MAX; playerid++)
            {
                Kick(playerid);
            }
        }
        t1 = GetTickCount();
        for (j = 0; j < 10000; j++)
        {
            foreach(P2, playerid)
            {
                Kick(playerid);
            }
        }
        t2 = GetTickCount();

Kick,实际上所有的玩家函数,内部都有一个 IsPlayerConnected 检查,所以如果你在一个循环中只运行一个函数,那么不调用 IsPlayerConnected 而直接调用该函数会更有效。如果玩家在线,你就节省了一次函数调用,如果他们不在线,你也没有损失,因为执行的代码与调用 IsPlayerConnected 相同。不幸的是,这个例子可能会影响 foreach 在高玩家数量下的速度,因为你将在同一个循环中使用 foreach 代码和 IPC 代码。如果你这样做:

代码:
t0 = GetTickCount();
        for (j = 0; j < 10000; j++)
        {
            for (new playerid = 0; playerid < FAKE_MAX; playerid++)
            {
                if (IsPlayerConnected(playerid))
                {
                    Kick(playerid);
                }
            }
        }
        t1 = GetTickCount();
        for (j = 0; j < 10000; j++)
        {
            foreach(P2, playerid)
            {
                Kick(playerid);
            }
        }
        t2 = GetTickCount();

那么 foreach 会更快,但这不是第一种情况下最有效的方式。

我实际上研究过修改编译器,以便你可以做类似的事情:

代码:
eqiv{{for (new playerid = 0; playerid < FAKE_MAX; playerid++){if (IsPlayerConnected(playerid)){Kick(playerid);}}}{foreach(P2, playerid){Kick(playerid);}}}

它将编译并优化这两个版本的代码,然后基于生成的代码和已知的操作码时钟周期精确计时,以查看哪个更快。然而,关于测试集存在各种各样的问题,我还没有做,所以这真的没有实际意义(说实话,我想对许多语言编译器进行一些改进,我只是还没有做)。

微优化

IEEE数字

有一种浮点数表示法表示正无穷大、负无穷大和无效数字。我见过人们尝试各种表示法来表示大数或无效浮点数,看看这些例子:

代码:
if (!IsPlayerConnected(playerid))
{
    return -1.0;
}
return GetDistance(playerid);

代码:
new
    bool:first = true,
    Float:distance = 0.0;
foreach (Player, playerid)
{
    if (first)
    {
        first = false;
        distance = GetDistance(playerid);
    }
    else
    {
        new
            Float:temp = GetDistance(playerid);
        if (temp < distance)
        {
            distance = temp;
        }
    }
}

第一个代码可以做任何事情,距离 -1.0 意味着玩家未连接,所以返回一个无效距离。第二个代码找到离某物最近的人(具体细节不重要)。第二段代码可以通过选择一个非常大的起始数来优化,而不是使用 "first" 变量:

代码:
new
    Float:distance = 100000.0;
foreach (Player, playerid)
{
    new
        Float:temp = GetDistance(playerid);
    if (temp < distance)
    {
        distance = temp;
    }
}

但这忽略了当玩家距离超过 100000 单位时的罕见情况(注意实际上你会使用平方值,如下所述,但这仅作为示例)。这两个例子都有明确定义的解决方案:

代码:
#define FLOAT_INFINITY  (Float:0x7F800000)
#define FLOAT_NEG_INFINITY (Float:0xFF800000)
#define FLOAT_NAN    (Float:0xFFFFFFFF)

这将使上面的代码变成:

代码:
new
    Float:distance = FLOAT_INFINITY;
foreach (Player, playerid)
{
    new
        Float:temp = GetDistance(playerid);
    if (temp < distance)
    {
        distance = temp;
    }
}

代码:
if (!IsPlayerConnected(playerid))
{
    return FLOAT_NAN;
}
return GetDistance(playerid);

"NaN" 是一个非常特殊的数字------将其与任何其他数字(包括它自身)比较都将返回 false。要检查 NaN,你可以这样做:

代码:
stock IsNaN(number)
{
    return !(number <= 0 || number > 0);
}

如果传递的数字小于、等于或大于 0,该函数返回 false------所有数字,包括正负无穷大,都符合这些条件------而 NaN 不符合,因为它不是一个数字,所以没有值。使用这些值可以保证(它们在 IEEE 浮点数规范中定义)你永远不会使用可能与实际值混淆的数字。由于 NaN 的独特属性,下面这个看起来非常奇怪的代码也应该可以工作:

代码:
stock IsNaN(number)
{
    return (number != number);
}

不要尝试

代码:
if (number == FLOAT_NAN)

这段代码会失败,因为如前所述,NaN 甚至不等于它自身。

让我们看看当你想要距离以检查某人是否在某物范围内时的情况:

代码:
if (DistanceWithConnectionCheck(playerid) < 100.0)
{
    // 他们在范围内且已连接
}

返回无穷大将意味着检查失败,返回 NaN 也是如此(它不大于、不小于也不等于 10)------相比之下,你需要在这里添加额外代码来检查 "-1.0":

代码:
new
    Float:distance = DistanceWithConnectionCheck(playerid);
if (distance == -1.0)
{
    // 他们没有连接
}
else if (distance < 100.0)
{
    // 他们在范围内且已连接
    // -1.0 小于 100,所以我们需要特别检查它
}

参见 YSI 对象加载器中的实际用法。

重新排列

编译器可以进行常量数学计算,这意味着如果你这样做:

代码:
printf("%d", 4 + 5);

编译器会这样做:

代码:
printf("%d", 9);

它不会费心加入代码进行数学计算,因为没必要------结果总是相同的。通常,简单的公式重排可以帮助你的代码:

代码:
new
    var = (4 + somevar) - 11;

这将编译为执行两步数学运算,首先将一个数加 4,然后从结果中减 11(一些编译器可能实际上能够按照我将要描述的方式优化这一点,但这只是一个简单的例子)。如果你重新排列这个求和,你会得到:

代码:
new
    var = somevar + (4 - 11);

编译器可以非常快速地优化为:

代码:
new
    var = somevar - 7;

一个没有编译器优化的更复杂的例子:

代码:
new
    gLastTime[MAX_PLAYERS];
*
#define EXPIRY 1000

代码:
new
    time = GetTickCount();
foreach (Player, playerid)
{
    if (time - gLastTime[playerid] > EXPIRY)
    {
        SendClientMessage(playerid, 0xFF0000AA, "你的时间已过期");
    }
}

这是一个基本的例子,检测自玩家上次做某事以来是否经过了一定时间。你可能认为那里没有优化的选项,也许你是对的,但尝试总是好的。这里的方程是:

代码:
time - gLastTime[playerid] > EXPIRY

=, ==, >= 等都可以用同样的方式重新排列,所以上面的方程等价于:

代码:
time > EXPIRY + gLastTime[playerid]

更重要的是,它也等价于:

代码:
time - EXPIRY > gLastTime[playerid]

为什么这很重要?就循环而言,"time - EXPIRY"现在是一个常数,因为两者在循环中都不改变。这意味着你可以这样做:

代码:
new
    time = GetTickCount() - EXPIRY;
foreach (Player, playerid)
{
    if (time > gLastTime[playerid])
    {
        SendClientMessage(playerid, 0xFF0000AA, "你的时间已过期");
    }
}

你刚刚几乎不费吹灰之力就减少了多达 200 次重复的减法运算。你能在求和式中获得的常数或伪常数元素越多越好,尤其是当你多次执行它们时。如果你只检查一个玩家,我会倾向于把所有东西,包括 GetTickCount() 调用,都放在 if 语句中,但如果你多次执行,就不要这样做。

数据重排

另一件要考虑的事情是你的数据如何布局以及你想如何访问它。以下面的数据为例:

代码:
#define MAX_OWNDED_VEHICLES 10

new gVehicleOwner[MAX_OWNDED_VEHICLES] = {0, 2, 4, 6, 8, 10, 12, 14, 16, 18};

这里有 10 辆车,每辆车都有一个拥有它的玩家(假设在这个例子中一个玩家最多只能拥有一辆车)。如果你想找出谁拥有一辆车,你可以简单地这样做:

代码:
printf("车辆 %d 的车主是 %d", vehicleid, gVehicleOwner[vehicleid]);

但是如果你想找出一个玩家拥有哪辆车呢?为此,你需要做类似这样的事情:

代码:
new i = 0;
while (i < MAX_OWNDED_VEHICLES)
{
    if (gVehicleOwner[i] == playerid)
    {
        printf("玩家 %d 拥有车辆 %d", playerid, i);
        break;
    }
    i++;
}
if (i == MAX_OWNDED_VEHICLES)
    printf("玩家 %d 没有拥有车辆", playerid);

现在让我们换一种方式来看:

代码:
#define MAX_PLAYERS 20

new gPlayerVehicle[MAX_PLAYERS] = {0, -1, 1, -1, 2, -1, 3, -1, 4, -1, 5, -1, 6, -1, 7, -1, 8, -1, 9, -1};

现在如果你想找出一个玩家拥有哪辆车,只需简单的数组查找,但如果你想看看谁拥有一辆车,就需要一个循环。这里你需要考虑的问题是,你更想知道哪个?如果你不在乎给定车辆的车主是谁,但确实在乎某人拥有哪辆车,那么使用第二种布局,反之亦然使用第一种。如果你两者都经常使用,你可能实际上要考虑在两个不同的数组中镜像数据。这是速度和内存之间的权衡,这是人们经常面临的一个非常常见的权衡。我个人认为在现代 32 位和 64 位处理器上,速度比内存更重要,所以我会使用两个数组,但考虑到它们运行在千兆赫兹的频率下,你可能会不同意,所以会使用一个数组和一个循环。

让我们看一个更清晰的例子,这是我最近读到一个话题中的。这个例子非常精简,这里我只用 10 个模型:

代码:
new
    gCars[] = {400, 403, 404, 406, 408, 409},
    gHeavyVehicles[] = {400, 402, 408},
    gBoats[] = {401, 407},
    gFireEngines[] = {402, 405};

如果你想了解一个模型是否是汽车,你需要遍历"gCar"直到找到该模型或到达末尾。另一方面,使用这段代码很容易知道给定位置的车是什么型号,但这毫无意义,因为这是你可能永远不想知道的数据。所以问题是;为什么容易得到你不想要的数据,而难以得到你想要的数据?这完全说不通......我们知道对于给定的模型,我们想知道它是不是汽车,所以我们需要更改代码,将模型用作数组索引(减去 400),就像我们上面做的那样,找出玩家拥有哪辆车:

代码:
new
    gIsACar[] = {1, 0, 0, 1, 1, 0, 1, 0, 1, 1},
    gIsAHeavyVehicle[] = {1, 0, 1, 0, 0, 0, 0, 0, 1, 0},
    gIsABoat[] = {0, 1, 0, 0, 0, 0, 0, 1, 0, 0},
    gIsAFireEngine[] = {0, 0, 1, 0, 0, 1, 0, 0, 0, 0};

现在如果你想找出一个模型是不是汽车,你只需这样做:

代码:
if (gIsACar[model - 400])

这比循环简单和快速多了,不是吗?

使用整个单元格来存储布尔值(1 或 0)也非常低效,但我们稍后会讨论这个。

了解语言

我的意思是非常了解。正如有人前几天提醒我的那样(我在 IRC 上评论过),不久前我发现:

代码:
if (2 <= a <= 4)

在 PAWN 中有效(在 C 中无效),我之前以为它和 C 中一样,所以一直在做:

代码:
if (2 <= a && a <= 4)

虽然不是很大的改进,但大多数都不是,重要的是综合和重复的效果。

例如,如果你不知道 "&" 运算符,并且想测试一个数字的第二位是否被设置,你需要做类似这样的事情:

代码:
if ((a << 30) >>> 31)

或者:

代码:
if ((a % 4) >>> 1)

这两段代码都会确保只设置了一个数字的一位,并且会做你想做的事,但了解 "&" 要好得多:

代码:
if (a & 2)

显然更快(移位不算太坏,但 MOD 非常慢,在其他两个版本中,如果你必须选一个,总是选第一个),而且你试图做什么也更明显。如果你不知道它是做什么的------去阅读 pawn-lang.pdf。

这显然是一个非常基本的例子,但还有许多许多其他的例子。当我告诉他们阅读 pawn-lang.pdf 时,人们往往会退缩,然后想知道为什么我似乎比他们更了解 PAWN,2 + 2 = ...... 我把它加入书签是有原因的,并不是为了快速复制链接发帖给别人(尽管这也很方便)。

正如本节开头的例子所示,无论你可能已经知道多少,总有新东西等着你去学习。我可以想到 PAWN 的两大块我完全不了解,而我没有意识到其他领域的事实只是意味着我还不知道它们,并不意味着它们不存在(很难知道你不知道什么)。

距离检查

0.3 现在自动支持距离检查,只需使用:

代码:
IsPlayerInRangeOfPoint(playerid, Float:range, Float:x, Float:y, Float:z);

这是人们非常常做的事情,也是非常容易出错的事情。例如:

代码:
if (PlayerDistanceToPoint(playerid, 10.0, 20.0, 2.0) <= 5.0)
{
    // 他们在 10.0, 20.0, 2.0 附近 - 做点什么
}

这段代码可以工作,会在玩家距离某点 5.0 单位内时触发,但计算到某点的距离非常慢。计算两点(x1, y1, z1 和 x2, y2, z2)之间距离的公式是:

代码:
(((x1 - x2) ^ 2) + ((y1 - y2) ^ 2) + ((z1 - z2) ^ 2)) ^ 0.5

这里的 "^" 表示乘方,不是异或,"^ 0.5" 是平方根(相信我------在计算器上试试)。最常见的实现是:

代码:
floatsqroot(floatadd(floatadd(floatpower(floatsub(x1, x2), 2), floatpower(floatsub(y1, y2), 2)), floatpower(floatsub(z1, z2), 2)));

不要问我为什么最初写它的人不使用标准运算符,我不知道,但简化后这段代码是:

代码:
floatsqroot(floatpower(x1 - x2, 2) + floatpower(y1 - y2, 2) + floatpower(z1 - z2, 2));

现在,首先要注意的是,计算任意次幂的代码很复杂,它不会针对像 2 这样的简单次幂进行优化,而是使用基本算法。我们知道一个数的 2 次幂就是这个数乘以它自身(或者你应该知道)。3^2 等于 33,57^2 等于 5757 等等。乘法比乘方简单得多,所以代码变成:

代码:
floatsqroot(((x1 - x2) * (x1 - x2)) + ((y1 - y2) * (y1 - y2)) + ((z1 - z2) * (z1 - z2)));

你可以去掉一些括号,但没必要,这和减少括号的版本一样快,而且更明确。这里的代码更多了,但速度快得多。现在优化追求更有趣的东西。我们每个减法做了两次,你可以将它们导出到变量中,只做一次,但这样就有了额外的变量写入,这可能无法抵消速度的提升:

代码:
x1 -= x2;
y1 -= y2;
z1 -= z2;
floatsqroot((x1 * x1) + (y1 * y1) + (z1 * z1));

现在我们有了获取某人到某物距离的高效代码,但还有一个主要问题------到目前为止所做的所有优化都远不及 floatsqroot,这是一个极其低效的函数(嗯,它并不是低效,实际上它非常高效,但这并不意味着它很快,因为它太复杂了)。信不信由你,大多数时候你实际上并不需要确切知道某人离某点多远,只需要知道他们是否在点附近。现在你应该已经阅读了关于重新排列的部分(如果没有,回去再读一遍),所以让我们在这里应用它:

代码:
((x * x) + (y * y) + (z * z)) ^ 0.5 <= 5.0

你可以像常规方程一样重新排列不等式:

代码:
((x * x) + (y * y) + (z * z)) ^ 0.5 <= 5.0
(x * x) + (y * y) + (z * z) <= 5.0 ^ 2
(x * x) + (y * y) + (z * z) <= 5.0 * 5.0

任何熟悉数学的人都应该能够证明这个非常简单的重排是正确的。我们知道如何快速计算平方(正如我刚才告诉你的),并且我们知道平方根非常慢,所以这是一个巨大的改进:

代码:
if (PlayerDistanceToPointSquared(playerid, 10.0, 20.0, 2.0) <= 5.0 * 5.0)
{
    // 他们在 10.0, 20.0, 2.0 附近 - 做点什么
}

或者:

代码:
if (IsPlayerInRangeOfPoint(playerid, 5.0, 10.0, 20.0, 2.0))
{
    // 他们在 10.0, 20.0, 2.0 附近 - 做点什么
}

代码:
stock IsPlayerInRangeOfPoint(playerid, Float:range, Float:x, Float:y, Float:z)
{
    new
        Float:px,
        Float:py,
        Float:pz;
    GetPlayerPos(playerid, px, py, pz);
    px -= x;
    py -= y;
    pz -= z;
    return ((px * px) + (py * py) + (pz * pz)) < (range * range);
}

当然你可以自己编写代码,但由于一些我不能深入的原因,我强烈建议使用那个函数名和参数顺序。

更新:

我找到了我之前在这个领域做的计时分析,结果并不像某些人预期的那样。我比较了一大堆不同的距离分析函数,包括人们为了速度而使用的精度较低的函数,例如:

代码:
Type1(Float:x1, Float:y1, Float:z1, Float:x2, Float:y2, Float:z2, Float:dist)
{
    x1 = (x1 > x2) ? x1 - x2 : x2 - x1;
    if (x1 > dist) return false;
    y1 = (y1 > y2) ? y1 - y2 : y2 - y1;
    if (y1 > dist) return false;
    z1 = (z1 > z2) ? z1 - z2 : z2 - z1;
    if (z1 > dist) return false;
    return true;
}

结果是,即使是这些"更快"的实现也比上面概述的实现慢,而且精度更低。

我得到的结果是:

代码:
1703 1781 1594 1641 2265 1782 2281 1891

1703 是"更快"版本的时间,1594 是我的版本的时间。结论------不要试图通过让距离检查变得更差来优化它们------原始的版本既更快又更准确。

注意,运行链接的代码会产生 9 个值,由于一个错误------忽略最后一个值(我犯的一个致命错误)。

我的分析代码可以在这里找到:链接

速度顺序

不同的语言特性执行时间不同,一般来说,顺序是(从最快到最慢):

  • 常量
  • 变量
  • 数组
  • 原生函数
  • 自定义函数
  • 远程函数

例如:

代码:
for (new i = 0; i < MAX_PLAYERS; i++)

比以下更快:

代码:
for (new i = 0, j = GetMaxPlayers(); i < j; i++)

因为第一个循环的主要部分使用常量,而第二个使用变量(循环中单次函数调用的开销与重复检查相比微不足道)。第二个版本本身比以下更快:

代码:
for (new i = 0; i < GetMaxPlayers(); i++)

因为这第三个版本使用重复的函数调用,而不是变量或常量。

我不确定控制结构在列表中的位置,例如,我不确定以下哪个更快:

代码:
new var = a ? 0 : 1;
printf("%d", var);
printf("%d", var);
printf("%d", var);
printf("%d", var);
printf("%d", var);

代码:
printf("%d", a ? 0 : 1);
printf("%d", a ? 0 : 1);
printf("%d", a ? 0 : 1);
printf("%d", a ? 0 : 1);
printf("%d", a ? 0 : 1);

我怀疑第一个,除非你只有一个打印,那么肯定是第二个,但同样,对于更复杂的例子,就不那么清楚了。这需要计时,但我还没做(而且有很多控制结构,所以我只是将我的通用规则(见下文)应用于这些情况)。

那么为什么"无"在列表中?考虑以下两段代码:

代码:
new var = random(10);
printf("%d", var);

代码:
printf("%d", random(10));

显然第二个更快,因为没有中间步骤。实际上,大多数编译器会优化掉变量,但并非所有都这样做。

当涉及到重复调用时,界限开始变得模糊。例如,以下哪个更快:

代码:
new var = gArr[10];
printf("%d %d", var, var);

代码:
printf("%d %d", gArr[10], gArr[10]);

第一个有一次数组访问、一次变量写入和两次变量读取,第二个有两次数组访问。老实说,我不确定哪个更快,但我有一个通用规则:

超过一个函数调用------将其保存在变量中,超过两次数组读取------将其保存在变量中,所以对于上面的代码,我可能会使用第二个版本,然而三个元素的打印需要三次数组访问,这超过了两次,因此我会使用一个中间变量:

代码:
new var = gArr[10];
printf("%d %d %d", var, var, var);

而且我从不重复调用同一个函数超过一次(特别是因为这可能产生意想不到的结果,如果函数有变化的返回值或副作用)。

了解你的值

另一个常见的代码片段(我相信你们大多数人都会认出它)是这样的:

代码:
if (killerid == INVALID_PLAYER_ID)
    SendDeathMessage(INVALID_PLAYER_ID, playerid, reason);
else
    SendDeathMessage(killerid, playerid, reason);

让我们看看另一个类似代码的例子,试图更清楚地说明这到底在做什么以及为什么它很愚蠢:

代码:
if (var == 1)
    printf("%d", 1);
else
    printf("%d", var);

如果 var 是 1,这打印 '1',如果 var 不是 1,这打印 var 的值,无论哪种方式,var 的值都被打印出来,那么 if 做了什么?这可以简单地写成:

代码:
printf("%d", var);

出于同样的原因,这与上面的代码功能相同:

代码:
SendDeathMessage(killerid, playerid, reason);

原始代码来自 0.2 版本的 LVDM,但在那里还做了其他事情,使得检查并非毫无意义,但人们拿走了这段代码,删除了其他部分,并没有思考代码现在实际上在做什么。如果 killerid 无效也没关系,因为如上所示,INVALID_PLAYER_ID 对于 SendDeathMessage 来说是一个完全可以接受的输入。

返回值

与维基百科所说的相反,许多函数的返回值很重要,因为它们指示(原生)函数是否成功。这可以被利用,因为我们之前知道变量比函数调用快,而且一次做事比两次快。一个例子:

代码:
new Float:health;
for (new i = 0; i < MAX_PLAYERS; i++)
    if (IsPlayerConnected(i))
    {
        GetPlayerHealth(i, health);
        SetPlayerHealth(i, health + 10.0);
    }

非常不言自明且典型的代码,但如果我们理解错误返回,这可以优化。由于 0.1 版本中的错误,所有玩家和车辆函数现在都有检查以确保你操作的东西确实存在,所以如果你对一个不存在的玩家执行 GetPlayerHealth,函数将失败,health 变量将和之前的值相同。更重要的是,几乎所有没有重要返回值的函数在失败时返回 0,成功时返回 1。在 GetPlayerHealth 内部的玩家连接检查与 IsPlayerConnected 中的几乎完全相同,所以我们两次检查玩家是否连接。如果他们未连接,GetPlayerHealth 会立即结束,所以我们可以用以下代码代替上面的代码:

代码:
new Float:health;
for (new i = 0; i < MAX_PLAYERS; i++)
    if (GetPlayerHealth(i, health))
        SetPlayerHealth(i, health + 10.0);

对于未连接的玩家,这不会更慢,对于已连接的玩家则更快,所以最坏情况下你没有改进,最好情况下有很大改进。

这也可以应用于其他函数,即使它们有返回值:

代码:
if (IsPlayerInAnyVehicle(playerid))
{
    new vehicleid = GetPlayerVehicleID(playerid);
    SetVehiclePos(vehicleid, 0.0, 0.0, 10.0);
}

同样,这是非常常见的代码,但再次我们需要问这些函数实际返回什么?GetPlayerVehicleID 返回玩家所在车辆的 ID,如果玩家不在车里,它返回 0(因为这是一个无效的车辆 ID)。所以,如果我们要获取他们所在车辆的 ID,并且这个函数知道他们是否在车里并且可以告诉你,那为什么要检查他们是否在车里呢?

代码:
new vehicleid = GetPlayerVehicleID(playerid);
if (vehicleid)
    SetVehiclePos(vehicleid, 0.0, 0.0, 10.0);

现在,不是两个函数调用,你只有一个,并且检查该调用的返回值是否有效(即不是 0)。

返回值的另一个方面是它们存在多长时间。如果你在 if 语句中设置一个变量(如果不小心,会给出意外的赋值警告),你刚刚设置的值仍然在 if 语句中,所以如果你这样做:

代码:
new
    a = 1,
    b = 0;
if ((b = a))

(注意双括号以避免意外的赋值警告)

然后 "a" 会被赋给 "b",所以 "b" 将是 1,而这个 1 仍然在 if 中作为赋值的结果有效,所以这个 if 为真,然而:

代码:
new
    a = 0,
    b = 1;
if ((b = a))

在这段代码之后,"b" 将是 0,因为 "a" 已成功赋给 "b",但由于这个赋值的结果是 0,if 失败。只是为了说明这一点,弄清楚这段代码(如果需要,重新阅读关于字符串的部分):

代码:
stock strcpy(dest[], src[])
{
    new i = 0;
    while ((dest[i] = src[i])) i++;
}

这也意味着你可以这样做:

代码:
new vehicleid;
for (new i = 0; i < MAX_PLAYERS; i++)
    if ((vehicleid = GetPlayerVehicleID(i)))
        SetVehiclePos(vehicleid, 0.0, 0.0, 10.0);

使用此方法在 PAWN 中实现玩家名称检查的示例:

代码:
NameCheck(name[])
{
    new
        i,
        ch;
    while ((ch = name[i++]) && ((ch == ']') || (ch == '[') || (ch == '_') || ('0' <= ch <= '9') || ((ch |= 0x20) && ('a' <= ch <= 'z')))) {}
    return !ch;
}

如果名称有效(即所有字符都是 0-9、a-z、A-Z、[、] 或 _),此函数返回 true,否则返回 false。(ch |= 0x20) 是另一个小技巧,用于将字符转换为小写,无论之前是什么大小写,基于 ASCII 中大写和小写字符正好相差 0x20 的事实(A = 0x40,a = 0x60)。

小片段

等价性

多件事情可以表示同一件事,例如:

代码:
if (string[1] == 65)

等同于:

代码:
if (string[1] == 0x41)

等同于:

代码:
if (string[1] == 0b01000001)

等同于:

代码:
if (string[1] == 'A')

尽管它们都表示同一件事,因此不会带来任何速度提升,但最后一个更明显地表明你正在尝试做什么(检查一个字符是否为 'A')。然而,在其他情况下,其他两个之一可能更合适,例如,你想看看某物是否为 65,而它恰好与 'A' 相同,这仅仅是巧合。

这可以更进一步用零:

代码:
if (string[1] == 0)

等同于:

代码:
if (string[1] == 0x00)

等同于:

代码:
if (string[1] == ((27 + 3) / 5) - 6)

等同于:

代码:
if (string[1] == 0b00000000)

等同于:

代码:
if (string[1] == '\0')

等同于:

代码:
if (string[1] == false)

等同于:

代码:
if (string[1] != true)

等同于:

代码:
if (string[1] == 0.0)

等同于:

代码:
if (!string[1])

实际上,这可以更进一步(floatround_round、radian、SPECIAL_ACTION_NONE、seek_start、io_read 和 PLAYER_STATE_NONE 也都是 0)。

空字符串

检查字符串是否为空的常用方法是:

代码:
if (strlen(string) == 0)
{
    // 字符串为空,因为它的长度是 0
}

这显然检查了字符串是否为空,如果字符串为空则很快,但如果字符串不为空则较慢。strlen 的工作方式是循环遍历字符串直到找到结尾(正如你应该从关于字符串的另一主题中知道的,结尾由 NULL 字符表示),一旦命中结尾,就返回长度,所以如果字符串不为空,找到结尾需要一些时间。

正如我们所知,字符串的结尾由 NULL 字符表示,我们只需要查看第一个字符是否是字符串的结尾:

代码:
if (string[0] == '\0')
{
    // 字符串为空,因为它的第一个字符就是结尾
}

这可以进一步改进:

代码:
if (!string[0])
{
    // 字符串为空,因为它的第一个字符不存在
}

这不适用于通过 CallRemoteFunction 和 CallLocalFunction 传递的字符串。由于 PAWN VM 的工作方式,这些字符串不能长度为 0,所以空字符串作为 "\1\0" 传递(即字符 1 (SOH),字符 0 (NULL))。要检查这个,请执行:

代码:
if (string[0] == '\1' && string[1] == '\0')
{
    // 字符串为空,因为它被特别标记为空
}

或者,使用 YSI 中的 isnull:

代码:
#define isnull(%1) \
    ((!(%1[0])) || (((%1[0]) == '\1') && (!(%1[1]))))

代码:
if (isnull(string))
{
    // 字符串为空,因为 isnull 这么说的
}

感谢 Simon 的建议。

复制字符串

很多人倾向于这样复制字符串:

代码:
format(dest, sizeof (dest), "%s", src);

这是最糟糕的方法之一!我对六种不同的字符串复制方法进行了计时,在所有情况下,"b" 是目标,"a" 是源。"strcpy" 是一个手工编写的 PAWN 函数,用于复制字符串:

代码:
strmid(b, a, 0, strlen(a), sizeof (b));

format(b, sizeof (b), "%s", a);

b[0] = '\0';
strcat(b, a, sizeof (b));

memcpy(b, a, 0, strlen(a) * 4 + 4, sizeof (b)); // 长度以字节为单位,不是单元格。

strcpy(b, a);

b = a;

注意,"b = a;" 是标准的 PAWN 数组复制,并且仅适用于在编译时已知大小相同或目标更大的数组。不幸的是,我运行了一系列测试,它们并没有指向一个单一的最佳函数。它们确实非常清楚地表明,手工编码的 PAWN 版本和 format 在复制字符串时非常慢:

对于短字符串在小数组中,"b = a;" 在适用时最快,先 NULL 终止的 strcat(重要)其次。

对于短字符串在大数组中,strcat 最快。

对于较长字符串在较长数组中,"b = a;" 再次最快,memcpy 其次。

对于巨大数组,"b = a;" 似乎最快。

在可能的情况下,使用标准的数组赋值,但这并不总是可能的,例如,当未知大小的字符串传递给函数时。在这些情况下,我建议使用 strcat(如果你感兴趣,注意奇怪的语法):

代码:
#define strcpy(%0,%1,%2) \
    strcat((%0[0] = '\0', %0), %1, %2)

用法:

代码:
strcpy(dest, src, sizeof (dest));

感谢 Simon 的建议。

假设

引言

底线是不要做假设!

假设是指你认为某物总是某种情况,仅仅因为它经常是这样。例如:

代码:
public OnGameModeInit()
{
    AddPlayerClass(167, 0.0, 0.0, 5.0, 0.0, 0, 0, 0, 0, 0, 0); // Clucking Bell 员工
    AddPlayerClass(179, 0.0, 0.0, 5.0, 0.0, 0, 0, 0, 0, 0, 0); // Ammunation 员工
}

public OnPlayerRequestClass(playerid, classid)
{
    switch (classid)
    {
        case 0:
        {
            GameTextForPlayer(playerid, "Clucking Bell", 5000, 3);
        }
        case 1:
        {
            GameTextForPlayer(playerid, "Ammunation", 5000, 3);
        }
    }
    return 1;
}

这段代码对你们几乎所有人来说可能非常熟悉------设置你的模式的皮肤并根据所选皮肤执行操作(我在这里省略了设置摄像机,因为它与要点无关)。在你自己的私人服务器上,这可能没问题,你知道只有两个皮肤,你知道它们添加的顺序,你知道它们永远不会被修改------没问题。但是如果你要发布一个模式,这是非常危险的。可能出现的一些问题:
  • 使用你模式的人也有添加皮肤的资源过滤器。这会打乱你的类值。
  • 使用你模式的人决定添加皮肤,并将它们添加到列表的开头。这再次打乱你的类值。
  • SA:MP 版本更改改变了 ID 的分配方式。完全改变了你的类 ID。

我相信还有更多,但这些是基础。这里的问题是你使用常量并假设它们永远不会改变,无论是通过模式修改还是通过 AddPlayerClass 不返回你期望的值。对于这个问题,有几种解决方案。

解决方案

忽略它

如果人们想以不同于你意图的方式使用你的模式,那是他们自己的问题,他们可以相应地修改代码。我相信这里的每个人都有这样的经验:当人们在 GF 中添加新车时,为什么所有其他房子和工作的车辆都会乱套。这是因为 GF 是为单个服务器编写的,目的是不被修改,它对有哪些车辆做了假设。

这显然根本不是解决方案。

使修改更容易

第二种解决方案,平衡效率和修改,是做出假设,但尽量减少它们的使用。你可能有一些依赖于所选皮肤的命令,在这种情况下你可能会有如下代码:

代码:
dcmd_chicken(playerid, params[])
{
    if (gClass[playerid] != 0) return SendClientMessage(playerid, 0xFF0000AA, "抱歉,你不是 Clucking Bell 员工");
    ...
}

你对 Clucking Bell 类是类 0 的假设现在在你的模式中出现了两次------一次在 OnPlayerRequestClass 中,一次在 dcmd_chicken 中。假设你的模式超过 20 行,你最终可能会有数百次出现,使得修改成为一场噩梦,因为你必须确保修改每个实例。解决这个问题的最简单方法是使用 define(或 enum):

代码:
#define CLUCKING_BELL_CLASS (0)
#define AMMUNATION_CLASS (1)

public OnGameModeInit()
{
    AddPlayerClass(167, 0.0, 0.0, 5.0, 0.0, 0, 0, 0, 0, 0, 0); // Clucking Bell 员工
    AddPlayerClass(179, 0.0, 0.0, 5.0, 0.0, 0, 0, 0, 0, 0, 0); // Ammunation 员工
}

public OnPlayerRequestClass(playerid, classid)
{
    switch (classid)
    {
        case CLUCKING_BELL_CLASS:
        {
            GameTextForPlayer(playerid, "Clucking Bell", 5000, 3);
        }
        case AMMUNATION_CLASS:
        {
            GameTextForPlayer(playerid, "Ammunation", 5000, 3);
        }
    }
    return 1;
}

dcmd_chicken(playerid, params[])
{
    if (gClass[playerid] != CLUCKING_BELL_CLASS) return SendClientMessage(playerid, 0xFF0000AA, "抱歉,你不是 Clucking Bell 员工");
    ...
}

现在当你在列表开头添加一个新皮肤时,或者当你运行带有自己皮肤的资源过滤器时,你只需要修改模式中的一个部分来反映这个变化。然而,如果你不能保证返回值总是常量,这仍然会引起问题。

防御性编码

确保永远不会遇到问题的唯一方法是根本不做任何假设。保存返回值并使用这些已知的保存值,不要试图猜测它可能是什么:

代码:
new
    gCluckingBellSkin = -1,
    gAmmunationSkin = -1;

public OnGameModeInit()
{
    gCluckingBellSkin = AddPlayerClass(167, 0.0, 0.0, 5.0, 0.0, 0, 0, 0, 0, 0, 0); // Clucking Bell 员工
    gAmmunationSkin = AddPlayerClass(179, 0.0, 0.0, 5.0, 0.0, 0, 0, 0, 0, 0, 0); // Ammunation 员工
}

public OnPlayerRequestClass(playerid, classid)
{
    if (classid == gCluckingBellSkin)
    {
        GameTextForPlayer(playerid, "Clucking Bell", 5000, 3);
    }
    else if (classid == gAmmunationSkin)
    {
        GameTextForPlayer(playerid, "Ammunation", 5000, 3);
    }
    return 1;
}

dcmd_chicken(playerid, params[])
{
    if (gClass[playerid] != gCluckingBellSkin) return SendClientMessage(playerid, 0xFF0000AA, "抱歉,你不是 Clucking Bell 员工");
    ...
}

现在返回值可能是什么并不重要,因为它们在保存和使用之间不可能改变。

重要提示

有些时候假设是可以的,就像我说的,如果你知道你的模式不会被发布,并且你知道你自己不会修改它,或者愿意接受修改所需的所有额外工作,那就去做吧。但不要惊讶,当一段时间后,因为你错过了一些东西,一切都崩溃了。

内存减少

回想一下前面的代码:

代码:
new
    gIsACar[] = {1, 0, 0, 1, 1, 0, 1, 0, 1, 1},
    gIsAHeavyVehicle[] = {1, 0, 1, 0, 0, 0, 0, 0, 1, 0},
    gIsABoat[] = {0, 1, 0, 0, 0, 0, 0, 1, 0, 0},
    gIsAFireEngine[] = {0, 0, 1, 0, 0, 1, 0, 0, 0, 0};

在 PAWN 中,所有变量都是 32 位大小,这意味着它们可以存储高达 4294967296,这段代码只存储 0 或 1------你可以在一个位中做到这一点。这里有十辆车,每辆车有四个信息片段(暂时忽略像 IsACar/IsABoat 这样的互斥信息),那只是 40 个二进制信息片段(即 40 个真/假),以 32 位每单元格计算,那是两个单元格的数据存储在 40 个单元格中(5 字节的数据(对齐到 8 字节)存储在 160 字节中------32 倍的增加,巨大的浪费)。有几种简单的方法可以减少这种使用(尽管这里列出的方法都无法达到完全压缩)。第一种是用一个变量标记所有车辆的一个属性,第二种是用一个变量标记所有车辆的所有属性。

所有车辆

每辆车要么是某物,要么不是;在这个例子中,它们要么是汽车,要么不是。还有 10 辆车,这意味着有 10 位二进制数据,我们将存储在一个变量中,这仍然浪费了 22 位,但这比浪费 310 位要好,并且最多 32 辆车将减少浪费,而不是增加浪费。

代码:
位: 0 1 2 3 4 5 6 7 8 9 10 ...31
值: 1 2 4 8 16 32 64 128 256 512 1024 ... 2147483648
1/0: 1 0 0 1 1 0 1 0 1 1 x ... x

x 意味着我们不关心这些位的值,因为它们不代表车辆(理论上我们甚至可以在这些浪费的位中塞入另一条信息,但现在不会)。假设所有其他位都是 0,那么这个数字是"857",不幸的是,看十进制它没什么意义,所以我们需要以二进制形式读取它。

假设我们想知道车辆型号 403 是否是汽车。首先减去 400 得到范围内的数字,然后我们需要测试第四位(0, 1, 2, 3)。第四位的值是八,所以我们需要测试数字 857 是否设置了八位,这是通过按位 AND 完成的:

代码:
if (857 & 8)
{
    // 位被设置,这辆车是汽车
}

或者,使用我们的数组(现在只是一个变量,因为我们已将其减少到 1 个单元格):

代码:
if (gIsACar & 8)
{
    // 位被设置,这辆车是汽车
}

很好,但目前这将产生如下代码:

代码:
switch (model - 400)
{
case 0: if (gIsACar & 1) ...
case 1: if (gIsACar & 2) ...
case 2: if (gIsACar & 4) ...
case 3: if (gIsACar & 8) ...
case 4: if (gIsACar & 16) ...

这毫无意义,因为如果你要费那个劲,你不如直接做:

代码:
switch (model - 400)
{
    case 0: return true;
    case 1: return false;
    case 2: return false;
    case 3: return true;
    case 4: return true;
}

我们需要某种方法来从型号生成位。1 是 2^0(这里 ^ 表示"的次方",不是异或),2 是 2^1,4 是 2^2,8 是 2^3,所以我们想要的位 8 是 2^3,其中 3 是 403-400:

代码:
new
    bit = 1;
model -= 400;
for (new i = 0; i < model; i++)
{
    bit *= 2;
}

那将得到正确的结果,但有一种更简单的方法来计算 2 的 n 次幂,即左移位运算符:

代码:
8 = 1 << 3

所以现在我们可以这样做:

代码:
if (gIsACar & (1 << (model - 400)))
{
    return true;
}
return false;

比这更好的是,if 检查一个布尔值,当它为真时我们返回真,为假时返回假,所以我们可以完全跳过检查:

代码:
stock IsACar(model)
{
    return gIsACar & (1 << (model - 400));
}

这个函数现在很小,你可以直接把它做成一个 define:

代码:
#define IsACar(%0) \
    (gIsACar & (1 << ((%0) - 400)))

所有属性

每辆车都有一个标记,表示它是否是汽车、是否是船、是否是消防车、是否重型。这是每辆车 4 位信息,正如我们刚刚展示的,你可以使用位在单个单元格中存储多条信息,其中每一位都是一个单独的真/假,我们可以单独访问这些位。那么,如果我们不是用每个位代表不同的车辆,而是用每个位代表不同的属性呢;所以如果位 0 被设置,它是汽车,如果位 1 被设置,它是船,位 2 是重型,位 3 是消防车?让我们看一个例子:

型号 402 是一辆重型消防车,它不是汽车或船。所以我们设置了位 2 和 3,即 4 和 8,4 + 8(技术上 4 | 8)是 12,所以我们有:

代码:
new gModel402 = 12;

现在我们要检查它是否是船,那是位 1,或数字 2:

代码:
return gModel402 & 2;

这将返回 false(如果为真,它将返回 TWO,而不是 ONE,这是许多人错误检查的)。

这可以转化为所有型号的属性数组:

代码:
new gModels[] = {x, x, 12, x, x, x, x, x, x, x};

(x 表示未知的其他)

代码:
return gModels[model - 400] & 2;

无法简化 2,你只需使用像这样的函数:

代码:
#define IsACar(%0) \
    (gModels[(%0) - 400] & 1)

#define IsABoat(%0) \
    (gModels[(%0) - 400] & 2)

#define IsAHeavy(%0) \
    (gModels[(%0) - 400] & 4)

这无法进一步优化,但可以使它更具可读性和更易于编辑。如果我告诉你一个型号是类型 10,你可能需要查看上面的列表才能知道它是一艘消防船,但如果我告诉你它是一个重型汽车,你会立刻知道它是什么。所以让我们这样做:

代码:
#define MODEL_CAR  (1)
#define MODEL_BOAT  (2)
#define MODEL_HEAVY (4)
#define MODEL_FIRE  (8)

new gModels[] =
    {
        x,
        x,
        MODEL_HEAVY | MODEL_FIRE,
        x,
        x,
        x,
        x,
        x,
        x,
        x
    };

#define IsACar(%0) \
    (gModels[(%0) - 400] & MODEL_CAR)

#define IsABoat(%0) \
    (gModels[(%0) - 400] & MODEL_BOAT)

#define IsAHeavy(%0) \
    (gModels[(%0) - 400] & MODEL_HEAVY)

你现在可以立即从数组条目中看出车辆是什么,但还有一种更好的方法。我不会详细讨论这个,因为它在 pawn-lang.pdf 中有解释,但你可以这样做:

代码:
enum (<<= 1)
{
    MODEL_CAR = 1,
    MODEL_BOAT,
    MODEL_HEAVY,
    MODEL_FIRE
}

其余代码完全相同,但这减少了输入,并且意味着你可以在 MODEL_CAR 和 MODEL_BOAT 之间添加内容而无需更新任何值。

超过32个值

当你有超过 32 个值要存储时会发生什么?在这种情况下,你需要一个数组,其索引是值超过 32 的次数。所以 1 将是单元格 0(因为它没超过 32),位 2;32 将是 1,0;66 将是 2,3。在 YSI 的 YSI_bit 中有简化此操作的代码。在这种情况下,你的代码看起来会像这样:

代码:
enum e_MODELS
{
    MODEL_CAR,
    MODEL_BOAT,
    MODEL_HEAVY,
    MODEL_FIRE,
    ...
    MODEL_SOMETHING
}

new Bit:gModels[410 - 400][Bit_Bits(e_MODELS)];

#define IsACar(%0) \
    (Bit_Get(gModels[(%0) - 400], MODEL_CAR))

#define IsSomething(%0) \
    (Bit_Get(gModels[(%0) - 400], MODEL_SOMETHING))

此代码的最新版本(我在写本教程时重写了它,因为它让我重新审视了我自己的代码)可以在这里找到。

多余的维度

我不知道为什么人们会这样做,我敢肯定他们是从更常见的模式中复制过来的,但这并不意味着它是正确的,我也不知道为什么一开始要这样做。如果你有一个值数组,不要浪费维度。例子:

我有一个包含 10 个值的数组,为了讨论方便,我们称它们为武器价格。所以我们有 10 种武器,每种都有一个价格,我们想将它们存储在一个数组中。每种武器都有一个 ID,从 0 到 9,所以要获得该武器的价格,你需要访问数组中的该索引:

代码:
new
    gPrices[10] = // 10 种武器,因此 10 个价格
    {
        1000,
        2000,
        5000,
        2000,
        10000,
        500,
        3000,
        2000,
        100,
        750
    };

代码:
new
    weaponPrice = gPrices[5];

这就是你所需要的------这是基本的数组访问,然而由于某些原因,人们坚持做以下事情:

代码:
new
    gPrices[10][1] = // 10 种武器,因此 10 个价格
    {
        {1000},
        {2000},
        {5000},
        {2000},
        {10000},
        {500},
        {3000},
        {2000},
        {100},
        {750}
    };

代码:
new
    weaponPrice = gPrices[5][0];

额外的维度有什么作用?完全没有!如果你每种武器有两个价格,那么是的------你需要额外的维度,但你没有,所以你就不要------就不要这样做,简单明了!这是浪费时间------速度更慢,而且浪费空间------它更大。

CPUvs内存

这是编写代码时非常常见的因素,值得特别提及。几乎在所有事情上,你都有如何做的选择,通常一种方式会很快但使用大量内存,另一种方式会很慢但使用很少内存。例如,在上面的 foreach 示例中,foreach 代码更快,但使用了一个大数组,IPC 更慢但几乎不使用内存,因为它没有任何存储。在所有这些情况下,你只需要根据情况做出决定。前面关于内存减少的部分列出了各种使用更少内存的方法,但它们都使用了额外的代码来实现,使得它们比原始的大数组慢。然而在这种情况下,减少幅度如此之大(内存使用大约减少了 32 倍),以至于它超过了轻微的速度下降(如上所述,位运算符也非常快)。

然而,还有第三种选择------复杂性。

在 foreach/IPC 示例中,foreach 速度更快,IPC 内存占用更小,但通过编写更复杂的代码,有可能结合两者的优点。更复杂的代码通常更明确地处理更多情况,这意味着你不必编写处理所有可能性的通用代码。在玩家循环示例中,这将表现为类似:

代码:
if (IsPlayerConnected(0))
{
    // 做点什么
}
if (IsPlayerConnected(1))
{
    // 做点什么
}
if (IsPlayerConnected(2))
{
    // 做点什么
}

...

if (IsPlayerConnected(199))
{
    // 做点什么
}

显然,这段代码看起来非常低效,也很难维护,但你摆脱了循环和变量,使代码更快,并且内存占用为 0 个单元格。从表中我们还知道常量(0、1 等)比变量(i)更快。我没有对这段代码进行计时,所以我不知道对于大量玩家(在低数量时没有竞争)来说,它与 foreach 相比哪个更快,但它肯定比 IPC 循环快。显然,这个例子仍然更适合使用更多内存,但有些情况下可能不是这样。

列表

这一节和下一节基本上直接来自维基百科,因为这是我最初写在那里的。

列表是一种非常有用的结构类型,它们基本上是一个数组,其中下一个相关数据由上一个数据指向。

例子:

假设你有以下数组:

代码:
3, 1, 64, 2, 4, 786, 2, 9

如果你想对数组进行排序,你会得到:

代码:
1, 2, 2, 3, 4, 9, 64, 786

然而,如果你想保留数据的原始顺序,但又想以某种顺序知道数字(这只是一个例子),你就有问题了,你如何让数字同时按两种顺序排列?这将是列表的一个好用途。要从此数据构建一个列表,你需要将数组变成二维数组,其中第二维有 2 个单元格大,一个包含原始数字,另一个包含下一个最大数字的索引。你还需要一个单独的变量来保存最小数字的索引,所以你的新数组看起来像:

代码:
start = 1
data: 3, 1, 64, 2, 4, 786, 2, 9
next: 3, 5, 6, 7, -1, 0, 2, 4

与 786 关联的下一个索引是 -1,这是一个无效的数组索引,表示列表的结束,即没有更多数字了。两个 2 显然可以任意顺序,数组中第一个出现在列表中的也是第一个,因为它是更可能首先遇到的。

这种排序方法的另一个优点是添加更多数字要快得多。如果你想在排序数组中添加另一个数字 3,你需要首先至少将 4 个数字向右移动一个槽位,在这里不算太糟,但在更大的数组中非常慢。使用列表版本,你可以简单地将 3 附加到数组的末尾,并修改列表中的一个值;

代码:
start = 1
data: 3, 1, 64, 2, 4, 786, 2, 9, 3
next: 8, 3, 5, 6, 7, -1, 0, 2, 4
      ^ 修改这个值
        ^ 下一个更高的槽位

其他数字都没有移动,所以其他索引都不需要更新,只需让下一个最低的数字指向新数字,并让新数字指向下一个最低数字曾经指向的数字。删除一个值甚至更容易:

代码:
start = 1
data: 3, 1, 64, X, 4, 786, 2, 9, 3
next: 8, 6, 5, 6, 7, -1, 0, 2, 4
      ^ 更改为跳过失删除的值

这里第一个 2 已被移除,指向该数字的数字(1)已被更新,以指向被移除数字所指向的数字。在这个例子中,被移除的数字的指针和数字都没有被删除,但你不可能通过跟随列表到达那个槽位,所以没关系,它被有效地移除了。

类型

上面例子中的列表只是基本的单链表,你还可以有双链表,其中每个值都指向下一个值和上一个值,这些往往也有一个指向列表末尾的指针,以便向后遍历(例如,以降序获取数字):

代码:
start = 1
end = 5
value: 3, 1, 64, 2, 4, 786, 2, 9, 3
next: 8, 3, 5, 6, 7, -1, 0, 2, 4
last: 6, -1, 7, 1, 8, 2, 3, 4, 0

你必须小心处理这些,特别是当你有多个相同值时,要确保 last 指针指向的数字的 next 指针直接指向回它,例如这是错误的:

代码:
2, 3, 3
1, 2, -1
-1, 2, 0

2 的 next 指针指向槽位 1 中的 3,但那个 3 的 last 指针不指向回 2,两个列表各自有序(因为两个 3 可以任意顺序),但在一起时是错误的,正确的版本应该是:

代码:
2, 3, 3
1, 2, -1
-1, 0, 1

这两个列表都从末尾的两个数字开始和结束,错误例子中的反向列表从中间的数字开始。

另一种列表类型是循环列表,其中最后一个值指向第一个值。这样做明显的好处是,你可以从任何值到达任何其他值,而无需事先知道目标是在起点之前还是之后,你只需要小心不要进入无限循环,因为没有明确的 -1 结束点。这些列表仍然有起点。你还可以做双循环列表,其中有一个 next 和一个 last 列表,两者都循环:

代码:
start = 1
end = 5
value: 3, 1, 64, 2, 4, 786, 2, 9, 3
next: 8, 3, 5, 6, 7, 1, 0, 2, 4
last: 6, 5, 7, 1, 8, 2, 3, 4, 0


  [教程] 代码优化 #1
发布者: XiaoNiao - 02-28-2026, 11:25 AM - 板块: 教程 - 暂无回复

[u][教程] 代码优化 #1[/u]

优化技巧总数:13  | 原文作者: Yashas

本文包含以下技巧:

  • 数组比普通变量慢
  • 当提前知道函数名时,不要使用 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 不提供指针。