零基础开发openmp/SAMP服务器基础教程 - 第二章本教程面向完全零基础的新手每一节只讲一个知识点循序渐进。本章节先以快速了解 Pawn 语言和 openmp 游戏实际应用为开端让新手更快通过实际游戏表现获得反馈以避免一开始就接触过多语法建议阅读完本章节再开始第三章巩固进阶编程语言细节目录#章节1open.mp 是什么2开启服务器3第一个脚本4注释5变量6define7字符串与 format8运算符9if / else 判断10switch 多分支11for 循环12while 循环13函数14数组15回调函数Callback16SendClientMessage — 发消息17GameText — 屏幕大字18TextDraw — 屏幕 UI19Dialog — 对话框20Pickup — 拾取物21Checkpoint — 检查点22Vehicle — 载具23Actor — NPC24Timer — 计时器25Keys — 按键检测1. open.mp 是什么open.mpopen multiplayer是一个全新的 侠盗猎车手圣安地列斯 的多人游戏服务端完全向下兼容SA-MP。你可以用Pawn 脚本语言来编写游戏服务器的逻辑比如玩家加入时显示欢迎消息在地图上放置 NPC、载具、拾取物制作商店、任务、角色扮演系统Pawn是一门类似 C 语言的脚本语言语法简单非常适合入门。open.mp vs SA-MP两者语法完全一样。open.mp 是活跃维护的现代版本。2. 开启服务器第一步下载 open.mp 服务端前往官网 https://www.open.mp 下载最新版本解压到任意文件夹。解压后你会看到这样的目录结构components: open.mp 核心组件 filterscripts: 服务器脚本文件辅助脚本 gamemodes: 服务器游戏模式文件主图 models: 服务器自定义模型纹理 .txd .dff plugins: 服务器插件文件传统插件 qawno: Pawn 编辑器程序及包含文件 scriptfiles: INI 配置文件及其他资源 bans.json: 封禁列表文件 config.json: 服务器配置文件 omp-server.exe: open.mp 服务器主程序第二步配置 config.json打开config.json找到main_scripts这一行改成你的主图名称 mygamemode根据需求设置端口 服务器名称 需要添加的插件 辅助脚本 修改高强度的rcon密码{name:我的服务器,network:{port:7777},max_bots:0,max_players:100,pawn:{main_scripts:[mygamemode 1]},rcon:{password:changeme1},}关于config.json更多详情可查阅 https://open.mp/docs/server/config.json 但几乎大部分设置保持默认即可第三步打开 qawno 编辑器双击qawno/qawno.exe这就是你写代码和编译脚本的地方。提示推荐使用 VSCode代码补全更强大。入门阶段用 qawno 就够了。3. 第一个脚本创建脚本文件在 qawno 中点击菜单File → New Blank然后输入以下代码#include open.mp main(){} public OnGameModeInit() { SetGameModeText(我的第一个open.mp服务器); AddPlayerClass(1, 2495.3547, -1688.2319, 13.6774, 351.1646, WEAPON_M4, 500, WEAPON_KNIFE, 1, WEAPON_COLORT45, 100); AddStaticVehicle(522, 2493.7583, -1683.6482, 12.9099, 270.8069, -1, -1); return 1; }保存并编译点击File → Save保存为mygamemode.pwn保存到gamemodes/目录下。然后按F5编译。编译成功后在同目录会生成mygamemode.amx文件。启动服务器双击omp-server.exe就成功启动服务器了。代码解释代码含义#include open.mp引入 open.mp 的所有函数必须写在第一行public OnGameModeInit()服务器启动时自动执行的函数SetGameModeText(...)设置服务器模式名称显示在服务器列表AddPlayerClass(...)在职业选择中添加一个职业 用于让玩家可以以自己选择的皮肤出生AddStaticVehicle(...)在游戏模式中添加静态车辆模型会预加载给玩家return 1;函数结束返回 1 表示成功规则每条语句末尾必须有分号;花括号{}表示代码块的开始和结束。4. 注释注释是写给人看的说明文字编译器会完全忽略它不影响程序运行。写注释的好处是以后回看代码时会轻松很多。单行注释用//开头从这里到行尾都是注释// 这是一行注释不会被执行 SetGameModeText(我的第一个open.mp服务器); // 也可以写在代码后面多行注释用/* ... */包裹可以跨越多行/* 这里是多行注释。 可以写很多行。 常用于说明。 */ public OnGameModeInit() { return 1; }建议新手可以给每个函数写一行注释说明它的用途给复杂逻辑写注释说明思路。5. 变量变量是用来存储数据的容器就像一个贴了标签的盒子。声明变量用new关键字声明变量new myScore; // 声明一个整数变量初始值默认是 0 new myScore 10; // 声明并赋初始值变量类型整数默认类型new myAge 25; new myGold 100; new myScore 0;浮点数小数浮点数需要在变量名前加Float:标签new Float:myHealth 100.0; new Float:mySpeed 3.14; new Float:myX 1234.5; // 坐标通常是浮点数变量命名规范变量名只能包含字母、数字、下划线且不能以数字开头推荐g_ 前缀表示全局变量new g_Score 0; new g_Speed 0.0; new g_Name[32];这种前缀命名法不是强制要求但能让代码更易读虽然不是主流实践但比较适用于pawn开发。全局变量 vs 局部变量// 全局变量写在所有函数外面整个脚本都能访问g_ 前缀表示全局global new g_PlayerScore[MAX_PLAYERS]; public OnPlayerConnect(playerid) { // 局部变量写在函数内部只在这个函数里有效驼峰命名法camelCase首词小写后续大写 new defaultScore 50; g_PlayerScore[playerid] defaultScore; return 1; }6. define它本质上是一个文本替换符号而不是一个真正的变量或常量从使用效果看它表现得像一个常量运行过程中不能改变其数值在编译时直接替换不占用内存常量常用于替代魔法数字使代码更具可读性和可维护性在这篇教程中我们暂时称它为常量。定义常量#define SERVER_NAME 我的第一个open.mp服务器 #define MAX_GOLD 999 #define START_GOLD 50 #define PI 3.14159如何使用public OnGameModeInit() { SetGameModeText(SERVER_NAME); // 等同于 SetGameModeText(我的第一个open.mp服务器); return 1; } public OnPlayerConnect(playerid) { g_Gold[playerid] START_GOLD; // 等同于 g_Gold[playerid] 50; return 1; }规范全部大写单词间用下划线分隔例如MAX_PLAYERS、SKIN_NORMAL。7. 字符串与 format字符串就是一段文字。在 Pawn 中字符串用字符数组存储。声明字符串new g_Name[32]; // 最多存 31 个字符留一位给结束符 new g_Msg[128]; // 消息通常用 128 或 256 new g_Name[MAX_PLAYER_NAME]; // 用内置常量等同于 g_Name[24]获取玩家名字new g_Name[MAX_PLAYER_NAME]; GetPlayerName(playerid, g_Name, sizeof(g_Name)); // sizeof(g_Name) 自动计算数组大小不需要手动填数字format — 格式化字符串format可以把变量的值拼进字符串里类似其他语言的字符串模板new string[128]; new name[MAX_PLAYER_NAME]; new gold 100; GetPlayerName(playerid, name, sizeof(name)); format(string, sizeof(string), 你好%s你有 %d 枚金币。, name, gold); // 结果你好CJ你有 100 枚金币。格式占位符占位符含义示例%s字符串Hello, %s%d整数金币%d%f浮点数血量%.1f%i整数同%d等级%i%.1f表示保留 1 位小数%.2f保留 2 位以此类推。8. 运算符算术运算符new a 10; new b 3; new add a b; // 13 加 new sub a - b; // 7 减 new mul a * b; // 30 乘 new div a / b; // 3 除整数除法舍去小数 new mod a % b; // 1 取余10 除以 3 余 1快捷赋值new gold 100; gold 50; // 等同于 gold 100 50 → 150 gold - 20; // 等同于 gold 100 - 20 → 80 gold * 2; // 等同于 gold 100 * 2 → 200 gold / 4; // 等同于 gold 100 / 4 → 25自增 / 自减new i 0; i; // i 变成 1等同于 i i 1 i--; // i 变成 0等同于 i i - 1比较运算符返回真/假运算符含义等于!不等于大于小于大于等于小于等于new gold 100; if(gold 100) { /* 如果gold等于100 则成立 */ } if(gold 50) { /* 如果gold大于50 则成立 */ } if(gold ! 0) { /* 如果gold不等于0 则成立 */ }常见错误判断用两个等号赋值用一个等号。写成if(iGold 100)是赋值不是判断逻辑运算符运算符含义示例且两个都成立gold 0 isAlive||或任意一个成立isAdmin || isVIP!非取反!IsPlayerConnected(i)9. if / else 判断if / else根据条件决定执行哪段代码。基础用法new gold 80; // 如果金币大于100 if(gold 100) { SendClientMessage(playerid, 0xFFFF00FF, 金币充足可以购买); } // 否则 else { SendClientMessage(playerid, 0xFF4444FF, 金币不足); }if / else if / elsenew myLevel 5; if(myLevel 10) { SendClientMessage(playerid, -1, 你是高级玩家); } else if(myLevel 5) { SendClientMessage(playerid, -1, 你是中级玩家。); } else { SendClientMessage(playerid, -1, 你是新手。); }10. switch 多分支当你需要根据一个变量的多个具体值做不同处理时switch比一堆else if更清晰。基础用法new g_Class 2; switch(g_Class) { case 0: { SendClientMessage(playerid, -1, 你选择了农民); SetPlayerSkin(playerid, 86); } case 1: { SendClientMessage(playerid, -1, 你选择了骑士); SetPlayerSkin(playerid, 287); } case 2: { SendClientMessage(playerid, -1, 你选择了商人); SetPlayerSkin(playerid, 19); } default: { // 以上都不匹配时 SendClientMessage(playerid, -1, 未知职业); } }实际应用处理对话框响应public OnDialogResponse(playerid, dialogid, response, listitem, inputtext[]) { if(dialogid DIALOG_SHOP) { if(!response) return 1; // 点了取消不处理等同于if(response false) switch(listitem) { case 0: SetPlayerSkin(playerid, 86); // 选择了对话框的第一个选项 case 1: SetPlayerSkin(playerid, 287); // 选择了对话框的第二个选项 case 2: SetPlayerSkin(playerid, 19); // 选择了对话框的第三个选项 } } return 1; }switch中每个case后面的值必须是整数或常量不能是字符串或浮点数。11. for 循环当你需要重复执行某段代码时用循环。for循环适合已知重复次数的场景。基础结构// 初始值; 条件; 每次循环后执行 for(new i 0; i 5; i) { // 这里的代码会执行 5 次i 0, 1, 2, 3, 4 print(循环中...(已循环%d次), i 1); }遍历所有在线玩家// 给所有在线玩家发送消息 for(new i 0; i MAX_PLAYERS; i) { if(IsPlayerNPC(i) false inGame[i] true) { SendClientMessage(i, 0xFFFF00FF, [公告] 比赛开始); GivePlayerWeapon(i, WEAPON_AK47, 1500); SetPlayerPos(i, 0.0, 0.0, 2.5); // 给所有玩家加金币 g_Gold[i] 10; } }但通常历遍玩家我们会选择foreach库更高效。注意i MAX_PLAYERS不能写成i MAX_PLAYERS否则会越界。MAX_PLAYERS是最大玩家数有效的 playerid 是0到MAX_PLAYERS - 1。12. while 循环while循环适合不知道要循环多少次、只知道循环条件的场景。基础结构new i 0; while(i 5) { print(执行中...); i; // 一定要记得让条件向终止靠近否则会导致无限死循环 }while vs for场景推荐固定次数的重复如遍历所有玩家for条件不满足就一直循环如等待某个状态while警告循环体内一定要有让条件变化的语句否则会进入死循环导致服务器卡死。13. 函数函数是把一段代码打包起来命名方便重复调用避免重复写同样的代码。定义函数// 函数名(参数列表) // { // 代码内容 // return 返回值; // } GiveGold(playerid, amount) { g_Gold[playerid] amount; SendClientMessage(playerid, 0xFFDD44FF, 你获得了金币); return 1; }调用函数public OnPlayerConnect(playerid) { GiveGold(playerid, 50); // 玩家连入时给 50 金币 return 1; }带特定返回值的函数// 检查玩家金币是否足够返回 true足够或 false不足 bool:HasEnoughGold(playerid, cost) { if(g_Gold[playerid] cost) { return true; // 足够 } return false; // 不足 } // 使用 if(HasEnoughGold(playerid, 100) true) { SendClientMessage(playerid, -1, 可以购买); }不需要带任何返回值的函数void:ResetGold(playerid) { g_Gold[playerid] 0; SendClientMessage(playerid, 0xFFDD44FF, 你的金币被清空了); }stock 函数在 Pawn 中如果你定义了函数但某些情况下没用到编译器会发出警告。加上stock关键字可以消除这个警告stock GetPlayerGold(playerid) { return g_Gold[playerid]; }原则把重复用到 3 次以上的代码封装成函数。函数名需要清晰描述它做什么例如GiveGold、ShowShop、KickPlayer。14. 数组数组是一组同类型数据的集合用一个名字访问多个值。声明数组new g_Scores[10]; // 存 10 个整数 new Float:g_Positions[3]; // 存 3 个浮点数X, Y, Z new g_Name[MAX_PLAYER_NAME]; // 字符串本质也是字符数组访问数组元素数组下标从0开始new g_Scores[5]; g_Scores[0] 100; // 第 1 个元素 g_Scores[1] 85; // 第 2 个元素 g_Scores[4] 60; // 第 5 个最后一个 // g_Scores[5] ← 越界不能访问会报错最常见每个玩家存一份数据// 全局声明每个玩家 ID 对应一个值 new g_Gold[MAX_PLAYERS]; new g_Level[MAX_PLAYERS]; new g_Skin[MAX_PLAYERS]; // 使用 playerid 作为下标访问对应玩家的数据 public OnPlayerConnect(playerid) { g_Gold[playerid] 50; g_Level[playerid] 1; g_Skin[playerid] 86; return 1; }二维数组// 存 5 个检查点的坐标每个坐标有 X、Y、Z 三个值 new Float:g_CpPos[5][3] { {100.0, 200.0, 10.0}, // 检查点 0 {150.0, 220.0, 10.0}, // 检查点 1 {200.0, 240.0, 10.0}, // 检查点 2 {250.0, 260.0, 10.0}, // 检查点 3 {300.0, 280.0, 10.0} // 检查点 4 }; // 访问检查点 2 的 Y 坐标 new Float:y g_CpPos[2][1]; // 240.015. 回调函数Callback回调函数是 open.mp服务端自动调用的特殊函数。你不需要手动调用它们只需要在脚本里定义它们当对应事件发生时服务端会自动执行。规律所有回调函数都以public On开头public OnGameModeInit() // 服务器启动时 public OnGameModeExit() // 服务器关闭时 public OnPlayerConnect() // 玩家进入服务器时 public OnPlayerDisconnect() // 玩家离开服务器时 public OnPlayerSpawn() // 玩家出生时 public OnPlayerDeath() // 玩家死亡时 public OnPlayerText() // 玩家发送聊天消息时示例完整的玩家连入/断开流程public OnPlayerConnect(playerid) { new name[MAX_PLAYER_NAME]; GetPlayerName(playerid, name, sizeof(name)); SendClientMessageToAll(0x88FF88FF, [服务器] %s 加入了游戏, name); return 1; } public OnPlayerDisconnect(playerid, reason) { // reason: 0超时 1主动退出 2被踢 new name[MAX_PLAYER_NAME]; GetPlayerName(playerid, name, sizeof(name)); SendClientMessageToAll(0xAAAAAAFF, [服务器] %s 离开了游戏, name); return 1; }返回值大多数回调函数返回 1 表示正常处理。返回 0 在某些回调里有特殊含义如阻止某些行为16. SendClientMessage — 发消息SendClientMessage是向玩家的聊天框发送消息是最基础的通知手段。注意SendClientMessage支持中文不支持 emoji 表情符号。语法SendClientMessage(playerid, color, 消息内容);颜色格式RGBA颜色用 8 位十六进制数表示0xRRGGBBAA参数含义RR红色分量00~FFGG绿色分量00~FFBB蓝色分量00~FFAA透明度FF 完全不透明SendClientMessage(playerid, 0xFFFFFFFF, 白色消息); SendClientMessage(playerid, 0xFF4444FF, 红色消息); SendClientMessage(playerid, 0x44FF44FF, 绿色消息); SendClientMessage(playerid, 0xFFDD44FF, 金黄色消息); SendClientMessage(playerid, -1, 也是白色-1 是快捷写法);颜色常量推荐定义// 在脚本顶部定义常用颜色 #define COLOR_WHITE 0xFFFFFFFF #define COLOR_RED 0xFF4444FF #define COLOR_GREEN 0x44FF44FF #define COLOR_YELLOW 0xFFDD44FF #define COLOR_GRAY 0xAAAAAAFF // 使用时更直观 SendClientMessage(playerid, COLOR_GREEN, [商店] 购买成功); SendClientMessage(playerid, COLOR_RED, [商店] 金币不足);发给所有玩家SendClientMessageToAll(COLOR_YELLOW, [公告] 服务器将在 5 分钟后重启。);在消息里嵌入变量SendClientMessage(playerid, COLOR_GREEN, [商店] 购买成功剩余金币%d 枚, g_Gold[playerid]);17. GameText — 屏幕大字GameText在玩家屏幕显示文字会自动消失。适合用于重要事件提示、区域名称、倒计时。注意GameText不支持中文只能写英文和符号。语法GameTextForPlayer(playerid, 文字内容, 持续毫秒, 样式); GameTextForAll(文字内容, 持续毫秒, 样式); // 对所有玩家显示颜色标签在文字中插入颜色标签改变颜色~w~ 白色 ~r~ 红色 ~g~ 绿色 ~b~ 蓝色 ~y~ 黄色 ~p~ 紫色 ~n~ 换行 ~h~ 加亮样式速查https://open.mp/docs/scripting/resources/gametextstyles示例// 进入区域时提示 GameTextForPlayer(playerid, ~y~Town Market, 3000, 3); // 组合颜色与换行 GameTextForPlayer(playerid, ~g~Quest Complete!~n~~w~Reward: 100 Gold, 4000, 3); // 全服公告 GameTextForAll(~r~Pirates~w~ are attacking!, 5000, 3);18. TextDraw — 屏幕 UITextDraw 是固定显示在屏幕上的文字不像 GameText 那样会消失。常用于服务器 Logo、血量 HUD、分数显示。详细资料https://open.mp/docs/scripting/resources/TextDraws创建一个右上角 Logo第一步声明变量new Text:g_MyText; // Text: 是 TextDraw 的类型标签第二步在 OnGameModeInit 里创建public OnGameModeInit() { // x, y坐标是基于640x480画布与屏幕分辨率无关 g_MyText TextDrawCreate(240.0, 580.0, Welcome to my OPEN.MP server); return 1; }第三步在 OnPlayerSpawn 里显示给玩家public OnPlayerSpawn(playerid) { TextDrawShowForPlayer(playerid, g_MyText); return 1; }玩家断开时隐藏public OnPlayerDisconnect(playerid, reason) { TextDrawHideForPlayer(playerid, g_MyText); return 1; }字体样式说明https://open.mp/docs/scripting/functions/TextDrawFont19. Dialog — 对话框Dialog 是弹出在玩家屏幕上的交互窗口是最核心的 UI 交互方式。对话框类型https://open.mp/docs/scripting/resources/dialogstyles第一步定义对话框 ID// 每个对话框需要一个唯一 ID用常量管理 #define DIALOG_NULL 0 #define DIALOG_LOGIN 1 #define DIALOG_WELCOME 2 #define DIALOG_WEAPONS 3// 你也可以用枚举他会自动分配ID第一个是0 后面自动递增 enum { DIALOG_NULL, // 0 DIALOG_LOGIN, // 1 DIALOG_WELCOME, // 2 DIALOG_WEAPONS // 3 }第二步显示对话框// ShowPlayerDialog(玩家ID, 对话框ID, 样式, 标题, 内容, 按钮1, 按钮2) // 对话框样式示例: DIALOG_STYLE_MSGBOX: ShowPlayerDialog(playerid, DIALOG_WELCOME, DIALOG_STYLE_MSGBOX, 提示, 你已进入服务器, 关闭, ); // 对话框样式示例: DIALOG_STYLE_INPUT: ShowPlayerDialog(playerid, DIALOG_LOGIN, DIALOG_STYLE_INPUT, 登录, 请在下方输入密码登陆服务器:, 登录, 取消); // 对话框样式示例: DIALOG_STYLE_LIST: ShowPlayerDialog(playerid, DIALOG_WEAPONS, DIALOG_STYLE_LIST, 武器, AK47\nM4\n狙击枪, 选择, 关闭); // 对话框样式示例: DIALOG_STYLE_PASSWORD: ShowPlayerDialog(playerid, DIALOG_LOGIN, DIALOG_STYLE_PASSWORD, 登录, 请在下方输入密码登陆服务器:, 登录, 取消); // 对话框样式示例: DIALOG_STYLE_TABLIST: ShowPlayerDialog(playerid, DIALOG_WEAPONS, DIALOG_STYLE_TABLIST, 武器商店, 沙鹰\t$5000\t100\n短管散弹枪\t$5000\t100\n手枪\t$1000\t50, 购买, 取消); // 对话框样式示例: DIALOG_STYLE_TABLIST_HEADERS: ShowPlayerDialog(playerid, DIALOG_WEAPONS, DIALOG_STYLE_TABLIST_HEADERS, 武器商店, 武器\t价格\t弹药数量\n沙鹰\t$5000\t100\n短管散弹枪\t$5000\t100\n手枪\t$1000\t50, 购买, 取消);\n是换行符在DIALOG_STYLE_LIST里每个\n分隔一个选项。第三步处理玩家的响应public OnDialogResponse(playerid, dialogid, response, listitem, inputtext[]) { // response 1 点了左边按钮 // 0 点了右边按钮或按了 ESC // listitem 玩家选的是第几行从 0 开始 // inputtext 玩家输入的文字 if (dialogid DIALOG_RULES) { if (response) // 如果玩家点击了左边的按钮 { SendClientMessage(playerid, COLOR_GREEN, 你点击了左边的按钮!); } else // 否则(玩家点击了右边的按钮或按下ESC键) { SendClientMessage(playerid, COLOR_GREEN, 你点击了右边的按钮!); } return 1; // 返回停止后续代码的执行 } return 0; }20. Pickup — 拾取物Pickup 是放在地图上的可触发拾取物玩家走触碰到之后会自动触发不需要按键。常用于金币、道具、任务触发点、商店入口。创建 Pickupnew g_pickupGold; // 保存 pickup 的 ID响应时用来判断 public OnGameModeInit() { // CreatePickup(模型ID, 类型, X, Y, Z, 虚拟世界) // 类型 2 拾取后消失15 秒后重生 g_pickupGold CreatePickup(1274, 2, 2009.8658, 1220.0293, 10.8206, -1); // ↑模型 ↑类型 ↑坐标 ↑-1所有世界可见 return 1; }常用模型 ID模型 ID外观1210公文包1212金钱1239信息1240爱心1273绿色房子更多可查阅: https://open.mp/docs/scripting/resources/pickupids响应拾取事件public OnPlayerPickUpPickup(playerid, pickupid) { if(pickupid g_pickupGold) { g_Gold[playerid] 20; SendClientMessage(playerid, COLOR_YELLOW, 捡到了金币20); GameTextForPlayer(playerid, ~y~20 Gold, 1500, 5); } return 1; }Pickup 类型说明类型值行为0无特殊属性不可被拾取1永久存在 仅允许脚本控制14仅限车辆拾取触发检查点音效后消失更多类型可查阅: https://open.mp/docs/scripting/resources/pickuptypes21. Checkpoint — 检查点Checkpoint 是地图上的红色发光圆柱区域玩家走进去自动触发。常用于任务目标点、区域进入触发、引导新玩家每个玩家最多只能显示一个检查点。当玩家进入检查点时会调用OnPlayerEnterCheckpoint回调设置检查点// 每个玩家同时只能有一个活动的 Checkpoint // SetPlayerCheckpoint(玩家ID, X, Y, Z, 检查点尺寸半径) SetPlayerCheckpoint(playerid, 1982.6150, -220.6680, -0.2432, 3.0);响应进入事件public OnPlayerEnterCheckpoint(playerid) { SendClientMessage(playerid, COLOR_GREEN, 到达目标地点); // 触发后通常需要清除避免反复触发 DisablePlayerCheckpoint(playerid); // 在这里做你想做的事比如给与玩家金钱 GivePlayerMoney(playerid, 1000); return 1; }RaceCheckpoint — 比赛检查点当玩家进入时会调用OnPlayerEnterRaceCheckPoint回调// SetPlayerRaceCheckpoint(玩家ID, 类型, 当前X,Y,Z, 下一目标X,Y,Z, 半径) SetPlayerRaceCheckpoint(playerid, CP_TYPE_GROUND_NORMAL, 644.3091, 1767.0223, 4.9970, 650.6734, 1812.0367, 4.9970, 3.0); public OnPlayerEnterRaceCheckpoint(playerid) { DisablePlayerRaceCheckpoint(playerid); SendClientMessage(playerid, COLOR_GREEN, 到达); return 1; }22. Vehicle — 载具创建载具// CreateVehicle(模型, X, Y, Z, 角度, 颜色1, 颜色2, 重生延迟秒) new vehicleid CreateVehicle(411, 100.0, 200.0, 10.0, 90.0, -1, -1, 60); // -1 随机颜色60 无人使用 60 秒后重生让玩家进入载具// PutPlayerInVehicle(玩家ID, 载具ID, 座位) // 座位 0 驾驶座1 副驾驶2/3 后排 PutPlayerInVehicle(playerid, vehicleid, 0);载具模型 ID请查阅: https://open.mp/docs/scripting/resources/vehicleid进出载具回调public OnPlayerEnterVehicle(playerid, vehicleid, ispassenger) { // ispassenger: 0 驾驶座1 乘客 if(!ispassenger) { SendClientMessage(playerid, COLOR_WHITE, 你正在进入载具的驾驶位。); } return 1; } public OnPlayerExitVehicle(playerid, vehicleid) { SendClientMessage(playerid, COLOR_WHITE, 你正在离开载具); return 1; }23. Actor — NPCActor 是站在世界里的静态 NPC 角色可以设置外观和动作动画但不会移动但功能有限创建 Actornew g_ActorSmith; // 保存 actor ID public OnGameModeInit() { // CreateActor(皮肤ID, X, Y, Z, 朝向角度) g_ActorSmith CreateActor(179, 316.1, -134.0, 999.6, 90.0); return 1; }给 Actor 播放动画// ApplyActorAnimation(actorID, 动画库, 动画名, 速度, 循环, lockX, lockY, 冻结, 时长) ApplyActorAnimation(g_ActorSmith, DEALER, shop_pay, 4.1, false, false, false, false, 0);玩家靠近时按键交互// 工具函数计算玩家到 Actor 的距离 stock Float:DistToActor(playerid, actorid) { new Float:px, Float:py, Float:pz; new Float:ax, Float:ay, Float:az; GetPlayerPos(playerid, px, py, pz); GetActorPos(actorid, ax, ay, az); return floatsqroot((px-ax)*(px-ax)(py-ay)*(py-ay)(pz-az)*(pz-az)); } // 在按键回调里检测距离并触发对话 public OnPlayerKeyStateChange(playerid, KEY:newkeys, KEY:oldkeys) { if(PRESSED(KEY_YES)) { if(DistToActor(playerid, g_ActorSmith) 3.0) { ShowPlayerDialog(playerid, DIALOG_SMITH, DIALOG_STYLE_MSGBOX, [ 武器商人 ], 欢迎我能为你提供精良的武器。, 好的, ); } } return 1; }服务器关闭时清理public OnGameModeExit() { DestroyActor(g_ActorSmith); return 1; }24. Timer — 计时器Timer 用于延迟执行或定期执行某个函数是实现游戏事件、冷却时间、倒计时的基础工具。创建计时器// 一次性2000 毫秒2 秒后执行false 不重复 SetTimer(MyFunction, 2000, false); // 重复每 5000 毫秒5 秒执行一次true 重复 new g_Timer SetTimer(MyRepeatFunc, 5000, true); // 停止计时器需要提前保存它的 ID KillTimer(g_Timer);带参数的计时器// SetTimerEx(函数名, 间隔ms, 是否重复, 参数格式, 参数值...) SetTimerEx(SendDelayedMsg, 3000, false, i, playerid); // 对应函数必须先用 forward 声明 forward SendDelayedMsg(playerid); public SendDelayedMsg(playerid) { if(!IsPlayerConnected(playerid)) return 0; GameTextForPlayer(playerid, ~w~Welcome to~n~~y~Island Town!, 3000, 3); return 1; }参数格式字符格式符含义i整数intf浮点数floats字符串实际应用每 60 秒播报随机事件new g_EventTimer; public OnGameModeInit() { g_EventTimer SetTimer(IslandEvent, 60000, true); return 1; } public OnGameModeExit() { KillTimer(g_EventTimer); // 服务器关闭时记得停止 return 1; } forward IslandEvent(); public IslandEvent() { switch(random(3)) { case 0: SendClientMessageToAll(COLOR_ORANGE, [小镇] 市集开放来广场逛逛。); case 1: SendClientMessageToAll(COLOR_RED, [小镇] 海湾发现海盗船); case 2: SendClientMessageToAll(COLOR_GREEN, [小镇] 今日好天气渔获翻倍); } return 1; }注意使用SetTimerEx的函数必须在使用前加forward声明否则编译会报错或运行异常。25. Keys — 按键检测OnPlayerKeyStateChange在玩家按下或松开按键时触发用于实现交互操作。官方文档: https://open.mp/docs/scripting/callbacks/OnPlayerKeyStateChange基础宏定义// 在脚本顶部定义这两个宏后面判断按键时更方便 #define PRESSED(%0) (((newkeys) (%0)) !((oldkeys) (%0))) #define RELEASED(%0) (!((newkeys) (%0)) ((oldkeys) (%0)))基础用法public OnPlayerKeyStateChange(playerid, KEY:newkeys, KEY:oldkeys) { // PRESSED 刚按下本帧按着上帧没按 if(PRESSED(KEY_FIRE)) { SendClientMessage(playerid, COLOR_WHITE, 你按了鼠标左键); } // RELEASED 刚松开 if(RELEASED(KEY_FIRE)) { SendClientMessage(playerid, COLOR_WHITE, 你松开了鼠标左键); } return 1; }按键常量请查阅: https://open.mp/docs/scripting/resources/keys完整交互示例按 Enter 与 NPC 对话#define PRESSED(%0) (((newkeys) (%0)) !((oldkeys) (%0))) public OnPlayerKeyStateChange(playerid, newkeys, oldkeys) { if(PRESSED(KEY_YES)) { // 检查玩家是否靠近铁匠 NPC if(DistToActor(playerid, g_ActorSmith) 3.0) { ShowPlayerDialog(playerid, DIALOG_SMITH, DIALOG_STYLE_MSGBOX, [ 武器商人 ], 你好旅人需要武器吗, 需要, 不了 ); } } return 1; }#GTA# #圣安地列斯# #侠盗猎车手# #圣安地列斯联机# #samp# #gta联机# #gtasa联机# #openmp# #omp# #open.mp# #gtasa#