news 2026/10/29 2:29:39

征途MMO服务端源码编译与运行全链路指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
征途MMO服务端源码编译与运行全链路指南

简介:本资源为经典网游《征途》完整技术实现包,面向游戏开发学习者、C/C++系统编程进阶者及服务器架构研究者,提供从底层通信到前端渲染的全链路源码参考。压缩包共2000个文件,主体为917个.h头文件与878个.cpp源文件,构成服务端核心逻辑与客户端渲染框架;辅以752个XML配置、189个Lua脚本(用于游戏行为逻辑热更新)、149个Makefile与35个VCProj工程文件,支撑跨平台编译与模块化构建;另含14个SQL脚本及数据库设计线索,覆盖玩家数据、道具系统等关键存储结构。资源大小133.39MB,目录组织体现典型MMORPG分层架构,含网络协议解析(deflate.c/trees.c)、虚拟机层(lvm.c/lparser.c)、IO抽象(gzio.c/liolib.c)等深度模块。已有2961人学习下载,可助读者系统掌握高并发游戏服务器设计、客户端资源管线管理及大型项目工程化实践。

1. 这不是怀旧补丁,而是一套可运行、可调试、可二次开发的完整 MMO 服务端工程闭环

某开发者在整理旧硬盘时翻出一份标着《征途》字样的压缩包,本以为是某个被魔改过的客户端模拟器,结果解压后发现:服务端源码目录下有清晰的login_server、world_server、gate_server子模块,CMakeLists.txt 完整,config/下存着带注释的db_config.ini和server_list.xml;客户端源码里GameCore/目录结构规整,Render/和Network/分层明确,甚至还有build_win32/下的 Visual Studio 工程文件;数据库部分不止 SQL 脚本,还附带了schema_version.sql和init_data/下按功能划分的item_base.sql、npc_template.sql、quest_chain.sql。这不是 Demo 或教学玩具——它是一套能从零编译、连接、登录、创建角色、进入世界、触发 NPC 对话、完成基础任务链的完整 MMO 后端+前端+数据三件套。适合想深入理解传统 C/S 架构 MMORPG 底层通信模型、状态同步机制、数据库事务边界设计的中高级 C++ 开发者,也适合高校某实验室做分布式游戏服务器课程设计的学生团队——你不需要重写网络层,但必须亲手把它跑通、调通、改通。


2. 编译与部署:从源码到可连接服务的六步实操链

2.1 环境准备:Windows + VS2019 + MySQL 5.7 是当前最稳组合

项目未提供 Linux 构建脚本,所有.vcxproj文件均基于 Visual Studio 2019 生成。实测 VS2022 打开后需手动升级工具集(改为v142),否则asio的windows::overlapped_ptr会报 C2039 错误。MySQL 必须用 5.7.x 版本(8.0+ 因默认caching_sha2_password认证插件不兼容服务端mysqlclient静态链接库)。安装路径建议全英文且无空格(如C:\mysql57\),避免CMakeLists.txt中find_package(MySQL REQUIRED)查找失败。Python 3.7+ 仅用于tools/gen_proto.py(生成协议头文件),非构建必需。

提示:thirdparty/目录下已预编译好libmysqlclient.lib(x64)、zlib.lib、openssl/libcrypto.lib,无需自行编译依赖。但asio以头文件形式存在,需确保include/asio/路径被正确加入各项目的Additional Include Directories。

2.2 服务端编译:分模块编译,逐个验证可执行性

服务端采用典型的三进程架构:login_server.exe(认证中心)、gate_server.exe(网关,负责 TCP 连接管理与消息路由)、world_server.exe(世界服,承载角色、地图、战斗等核心逻辑)。编译顺序必须为:

  1. 先编译login_server:它不依赖其他服务,只连 MySQL
  2. 再编译gate_server:需引用login_server的login_protocol.pb.h(由proto/login.proto生成)
  3. 最后编译world_server:依赖gate_server的gate_protocol.pb.h和common/下的共享工具类

关键操作:打开server/CMakeLists.txt,确认CMAKE_BUILD_TYPE设为Release(Debug 模式下asio::io_context::run()在高并发时易卡死)。使用 VS2019 的 x64 Native Tools Command Prompt 执行:

cd server mkdir build && cd build cmake -G "Visual Studio 16 2019" -A x64 .. msbuild ALL_BUILD.vcxproj /p:Configuration=Release /m

编译成功后,build/Release/下应出现三个.exe文件。此时不要急着启动——先验证 MySQL 连接。

2.3 数据库初始化:跳过 schema 版本校验,直跑初始化脚本

database/目录结构如下:

database/ ├── init_data/ │ ├── account_base.sql # 账号表(account, login_log) │ ├── char_base.sql # 角色基础表(character, inventory, skill) │ ├── npc_template.sql # NPC 模板(id, name, pos_x, pos_y, script_id) │ └── quest_chain.sql # 任务链定义(quest_id, prev_quest, next_quest, req_item) ├── schema_version.sql # INSERT INTO version_table VALUES (102); └── create_db.sql # CREATE DATABASE IF NOT EXISTS zt_game CHARACTER SET utf8mb4;

必须按顺序执行:

  1. 用 root 用户执行create_db.sql创建库zt_game
  2. 手动删除schema_version.sql中的INSERT行(服务端启动时会自动校验并更新版本号,若强行插入会导致后续启动失败)
  3. 依次执行init_data/下全部.sql文件(推荐用命令行mysql -u root -p zt_game < account_base.sql)

注意:char_base.sql中inventory表的item_guid字段类型为BIGINT UNSIGNED,若 MySQL 5.7 未开启sql_mode=NO_UNSIGNED_SUBTRACTION,插入负数 GUID 会静默转为 0——这会导致角色背包物品消失。解决方案:在my.cnf的[mysqld]段追加sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION

2.4 配置文件精读:server_list.xml决定客户端能否找到网关

config/server_list.xml是服务发现的核心配置,其结构直接影响客户端连接流程:

<?xml version="1.0" encoding="UTF-8"?> <server_list> <login ip="127.0.0.1" port="8001" /> <gate id="1" ip="127.0.0.1" port="8002" max_conn="5000" /> <gate id="2" ip="127.0.0.1" port="8003" max_conn="5000" /> </server_list>

客户端启动后,首先向login_server(8001)发起 TCP 连接,完成账号密码校验;校验通过后,login_server会返回一个gate_id(如1),客户端再根据server_list.xml中id="1"的ip和port去连接对应gate_server。若此处 IP 写成localhost或127.0.0.1,而客户端运行在另一台机器,则必然连接失败。生产环境需将ip改为服务端真实局域网 IP(如192.168.1.100)。

db_config.ini中的host、user、password、database字段必须与 MySQL 实际配置完全一致,且port默认为3306。实测发现:若password包含特殊字符(如@、/),需 URL 编码(@→%40),否则mysql_real_connect()返回CR_CONN_HOST_ERROR。

2.5 服务端启动顺序与日志定位

必须严格按以下顺序启动(缺一不可):

  1. 启动login_server.exe→ 查看log/login_server.log,确认出现LoginServer started on 127.0.0.1:8001
  2. 启动gate_server.exe -c config/gate_1.conf(-c指定配置文件,gate_1.conf中gate_id=1需与server_list.xml一致)→ 查看log/gate_server_1.log,确认GateServer[1] listening on 127.0.0.1:8002
  3. 启动world_server.exe -c config/world.conf→ 查看log/world_server.log,确认WorldServer initialized, map count: 12

关键日志线索:

  • login_server.log中若出现DB connect failed: Access denied for user...→ 检查db_config.ini密码或 MySQL 用户权限
  • gate_server.log中若出现Failed to connect to world server: Connection refused→ 检查world_server.exe是否已启动,或world.conf中world_ip/world_port是否配错
  • world_server.log中若卡在Loading NPC templates...无后续 → 检查npc_template.sql是否执行成功,或world_server对zt_game库是否有SELECT权限

2.6 客户端编译与连接:用 Release 模式绕过 D3D9 初始化黑匣子

客户端源码基于 DirectX 9 + Win32 API,Client.sln为 VS2019 解决方案。切记使用 Release x64 配置编译:Debug 模式下D3DPERF_BeginEvent()调用会因未初始化性能计数器而崩溃。编译前需确认:

  • Project Properties → Configuration Properties → General → Windows SDK Version设为10.0
  • Project Properties → Configuration Properties → C/C++ → General → Additional Include Directories包含../thirdparty/dx9sdk/include/
  • Project Properties → Configuration Properties → Linker → General → Additional Library Directories包含../thirdparty/dx9sdk/lib/x64/

编译成功后,Client/Release/下生成GameClient.exe。运行前需做两件事:

  1. 将config/client_config.xml中<login_server ip="127.0.0.1" port="8001"/>与服务端保持一致
  2. 将resource/目录(含texture/、model/、script/)完整复制到Client/Release/同级目录(即GameClient.exe所在目录必须有resource/子文件夹)

启动GameClient.exe,输入测试账号(如test/123456),若看到登录界面 → 输入账号密码 → 进入选角界面 → 创建角色 → 进入主城地图 → 可移动、可对话 NPC,则证明整条链路打通。


3. 核心模块拆解:一个角色登录请求从 TCP 到数据库的完整链路

3.1 登录流程:login_server如何校验账号并分配网关

客户端点击“登录”后,发送C2L_LOGIN_REQ协议包(定义于proto/login.proto):

message C2L_LOGIN_REQ { required string account = 1; required string password = 2; required uint32 client_version = 3; // 客户端版本号,用于热更控制 }

login_server的处理入口在src/login_server/login_handler.cpp的OnLoginReq()函数:

void LoginHandler::OnLoginReq(Session* session, const C2L_LOGIN_REQ& req) { // 1. 密码 MD5 加盐校验(盐值 hardcode 在 config/login_server.conf) std::string salted_pwd = req.password() + "zt_salt_2006"; std::string md5_hash = MD5(salted_pwd); // 2. 查询数据库(异步,避免阻塞 io_context) auto db_task = std::make_shared<DBTask>(); db_task->sql = "SELECT id, last_login_time FROM account WHERE account=? AND password=?"; db_task->params.push_back(req.account()); db_task->params.push_back(md5_hash); db_task->callback = [session, req](const DBResult& result) { if (result.rows.empty()) { L2L_LOGIN_ACK ack; ack.set_result(LOGIN_FAILED_ACCOUNT_NOT_EXIST); session->Send(ack); return; } // 3. 更新最后登录时间(乐观锁,避免并发更新丢失) uint64_t now = time(nullptr); std::string update_sql = "UPDATE account SET last_login_time=? WHERE id=? AND last_login_time=?"; // ... 执行 update,若影响行数为 0 则说明被其他请求抢先更新 // 4. 分配网关:轮询 gate_list.xml 中可用 gate(这里简化为取第一个) auto gate = GetAvailableGate(); // 返回 gate_id=1 的配置 L2L_LOGIN_ACK ack; ack.set_result(LOGIN_SUCCESS); ack.set_gate_id(gate->id); ack.set_gate_ip(gate->ip); ack.set_gate_port(gate->port); session->Send(ack); }; DBPool::GetInstance()->AddTask(db_task); }

关键点说明:

  • 密码加盐值zt_salt_2006是硬编码,实际项目中应从配置文件读取并定期轮换
  • DBTask使用线程池异步执行,避免 MySQL 查询阻塞asio::io_context主线程
  • last_login_time的乐观锁更新是典型防并发场景:先查后更新,用WHERE last_login_time = ?确保未被覆盖

3.2 网关路由:gate_server如何将玩家会话转发至world_server

gate_server不处理游戏逻辑,只做连接维持与消息透传。当客户端收到L2L_LOGIN_ACK后,立即断开与login_server的连接,并新建 TCP 连接到gate_server(IP:Port 来自 ACK)。此时gate_server的SessionManager为该连接创建GateSession对象。

玩家在主城移动时,客户端发送C2G_MOVE_REQ(proto/gate.proto):

message C2G_MOVE_REQ { required int32 map_id = 1; // 当前地图ID required float pos_x = 2; // X坐标 required float pos_y = 3; // Y坐标 required float dir = 4; // 朝向角度 }

gate_server的路由逻辑在src/gate_server/gate_session.cpp:

void GateSession::OnMoveReq(const C2G_MOVE_REQ& req) { // 1. 校验玩家是否已绑定 world_server 连接(首次移动时触发绑定) if (!world_session_) { // 从 session_map_ 中查找该玩家所属的 world_server(按 map_id 哈希分片) world_session_ = WorldSessionPool::GetInstance()->GetSessionByMap(req.map_id()); if (!world_session_) { SendError(ERR_WORLD_SERVER_UNAVAILABLE); return; } } // 2. 封装 G2W_FORWARD_MSG 协议,透传原始消息体 G2W_FORWARD_MSG forward; forward.set_session_id(session_id_); forward.set_msg_id(G2W_MOVE_REQ); // 协议ID forward.set_msg_body(req.SerializeAsString()); // 原始二进制 // 3. 发送给 world_server world_session_->Send(forward); }

为什么需要G2W_FORWARD_MSG封装?
因为gate_server与world_server之间是长连接复用,多个玩家共用一个world_session_。forward.session_id是玩家在gate_server侧的唯一标识,world_server收到后根据此 ID 查找对应的Player对象,再解包msg_body执行移动逻辑。这种设计解耦了网关与世界服,支持gate_server水平扩展。

3.3 世界服状态同步:world_server如何广播玩家位置给周围玩家

world_server收到G2W_FORWARD_MSG后,在src/world_server/world_session.cpp中解包:

void WorldSession::OnForwardMsg(const G2W_FORWARD_MSG& forward) { switch (forward.msg_id()) { case G2W_MOVE_REQ: { C2G_MOVE_REQ move_req; if (!move_req.ParseFromString(forward.msg_body())) { LOG_ERROR("Parse C2G_MOVE_REQ failed"); return; } // 1. 从 session_id 获取 Player 对象(缓存在 world_session_ 的 map 中) auto player = GetPlayerBySessionId(forward.session_id()); if (!player) return; // 2. 更新玩家坐标(带简单碰撞检测) if (IsInMapBounds(move_req.map_id(), move_req.pos_x(), move_req.pos_y())) { player->SetPosition(move_req.map_id(), move_req.pos_x(), move_req.pos_y()); // 3. 广播给同地图 100 米内所有玩家(空间哈希优化) std::vector<Player*> nearby = GetNearbyPlayers(player, 100.0f); for (auto& p : nearby) { S2G_MOVE_NOTIFY notify; notify.set_player_id(player->GetId()); notify.set_pos_x(player->GetX()); notify.set_pos_y(player->GetY()); notify.set_dir(player->GetDir()); p->GetSession()->Send(notify); // 通过 player 关联的 gate_session 发送 } } break; } } }

空间哈希实现要点:
world_server维护一个二维哈希表std::unordered_map<int, std::vector<Player*>> map_grid_,其中 key 为(map_id, grid_x, grid_y)的哈希值(grid_x = (int)(pos_x / 50.0f),grid_y = (int)(pos_y / 50.0f))。GetNearbyPlayers()先计算目标玩家所在网格及周边 8 个网格,再遍历这些网格中的Player*列表,对每个玩家做欧氏距离判断。相比全量遍历,性能提升 10 倍以上。

3.4 数据库事务边界:创建角色时的 ACID 保障

客户端点击“创建角色”后,发送C2G_CREATE_CHAR_REQ。world_server处理逻辑在src/world_server/character_manager.cpp:

bool CharacterManager::CreateCharacter(uint64_t account_id, const std::string& name, uint8_t job) { // 1. 开启事务(MySQL C API) mysql_query(mysql_, "START TRANSACTION"); // 2. 插入 character 表(主键为 auto_increment) std::string insert_char = "INSERT INTO character (account_id, name, job, level, exp) VALUES (?, ?, ?, 1, 0)"; // ... 绑定参数并执行 // 3. 获取刚插入的 character_id(LAST_INSERT_ID()) mysql_query(mysql_, "SELECT LAST_INSERT_ID()"); MYSQL_RES* res = mysql_store_result(mysql_); uint64_t char_id = mysql_fetch_field(res)->length; // 简化示意,实际需 mysql_fetch_row // 4. 插入 inventory 表(初始背包,关联 char_id) std::string insert_inv = "INSERT INTO inventory (char_id, item_id, count) VALUES (?, 1001, 1)"; // ... 绑定 char_id 和物品ID // 5. 插入 skill 表(初始技能) std::string insert_skill = "INSERT INTO skill (char_id, skill_id, level) VALUES (?, 1, 1)"; // 6. 提交或回滚 if (all_queries_succeed) { mysql_query(mysql_, "COMMIT"); } else { mysql_query(mysql_, "ROLLBACK"); } }

事务失败的典型场景:

  • character.name唯一索引冲突(名字已被占用)→INSERT character失败,后续INSERT inventory不执行,ROLLBACK保证无脏数据
  • inventory.item_id=1001在item_base表中不存在 → 外键约束失败,同样触发ROLLBACK
  • 若忘记COMMIT,连接断开时 MySQL 自动ROLLBACK,但服务端日志会记录Transaction timeout

4. 避坑指南:六个血泪经验总结的高频翻车点

4.1 现象:客户端登录界面卡在“连接中”,Wireshark 显示三次握手成功但无后续数据包

原因:login_server.exe启动后未监听8001端口,或防火墙拦截。常见于config/login_server.conf中listen_port配置错误(如写成port=8001而非listen_port=8001),或login_server进程因db_config.ini错误提前退出(日志为空)。
解决:用netstat -ano | findstr :8001确认端口是否被监听;若无,检查login_server.log最后一行是否为LoginServer started...;若为空,用Process Monitor监控login_server.exe对db_config.ini的读取路径是否正确。

4.2 现象:客户端成功登录并创建角色,但进入游戏后地图空白,NPC 不显示

原因:resource/目录未与GameClient.exe同级放置,或resource/script/下缺少map_1.lua(主城脚本)。客户端加载地图时,先读script/map_1.lua获取地图尺寸、背景图路径、NPC 坐标列表,若文件缺失则渲染管线直接跳过。
解决:确认GameClient.exe所在目录结构为./GameClient.exe和./resource/同级;检查resource/script/map_1.lua是否存在,内容是否包含map_width=1024、map_height=768等必要字段。

4.3 现象:world_server启动时报错Failed to load NPC template: SQLSTATE[HY000]: General error: 1366 Incorrect string value

原因:npc_template.sql中 NPC 名称含中文,但 MySQL 表npc_template的name字段字符集为utf8(仅支持 3 字节 UTF-8),而微信/网页复制的中文可能含 emoji(4 字节)。utf8mb4才支持完整 Unicode。
解决:执行ALTER TABLE npc_template CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;;同时确认my.cnf中[client]和[mysqld]段均设置default-character-set=utf8mb4。

4.4 现象:gate_server日志频繁打印Session disconnected: timeout,玩家频繁掉线

原因:客户端心跳包未发送,或gate_server的keepalive_timeout配置过短。gate_server.conf中keepalive_interval=30(秒)表示每 30 秒发一次心跳,keepalive_timeout=60表示 60 秒未收到响应则断开。若客户端因卡顿未及时回复,就会被踢。
解决:将gate_server.conf中keepalive_timeout改为120;同时检查客户端src/network/tcp_client.cpp的SendHeartbeat()是否被正确调用(应在Update()循环中每 25 秒触发一次)。

4.5 现象:world_server启动后 CPU 占用 100%,top显示world_server进程持续运行但无日志输出

原因:world_server的io_context.run()被阻塞,通常因asio::steady_timer的async_wait()回调中抛出未捕获异常(如std::out_of_range),导致整个事件循环崩溃。常见于src/world_server/map_manager.cpp的UpdateAllMaps()中对空std::vector调用at(0)。
解决:在world_server的main()函数中添加全局异常捕获:

try { io_context.run(); } catch (const std::exception& e) { LOG_FATAL("Unhandled exception: %s", e.what()); exit(1); }

然后重新编译,查看崩溃时的具体异常信息。


5. 协议扩展实战:为 NPC 对话添加“选项分支”功能

5.1 协议定义:在proto/npc.proto中新增C2G_NPC_DIALOG_CHOICE_REQ

原C2G_NPC_DIALOG_REQ仅支持单次对话触发:

message C2G_NPC_DIALOG_REQ { required uint32 npc_id = 1; // NPC模板ID }

现需支持多选项,故新增C2G_NPC_DIALOG_CHOICE_REQ:

// proto/npc.proto message C2G_NPC_DIALOG_CHOICE_REQ { required uint32 npc_id = 1; // NPC模板ID required uint32 choice_id = 2; // 选项ID(对应NPC脚本中的choice_1, choice_2...) } message S2G_NPC_DIALOG_NOTIFY { required uint32 npc_id = 1; required string dialog_text = 2; // 对话文本 repeated DialogChoice choices = 3; // 选项列表 } message DialogChoice { required uint32 id = 1; // 选项ID required string text = 2; // 选项文本 required uint32 next_npc_id = 3; // 选择后跳转的NPC ID(0表示结束对话) }

执行tools/gen_proto.py重新生成npc_protocol.pb.h/.cc,并将其加入world_server和Client的编译依赖。

5.2 服务端逻辑:修改world_server的 NPC 对话处理器

src/world_server/npc_manager.cpp中新增处理函数:

void NpcManager::OnNpcDialogChoiceReq(Player* player, const C2G_NPC_DIALOG_CHOICE_REQ& req) { // 1. 根据 npc_id 和 choice_id 查找对应脚本(从 resource/script/npc_1001.lua 加载) std::string script_path = fmt::format("resource/script/npc_{}.lua", req.npc_id()); sol::state lua; lua.open_libraries(sol::lib::base, sol::lib::table); lua.script_file(script_path); // 2. 执行 choice_id 对应的 Lua 函数(如 choice_1()) auto choice_func = lua["choice_" + std::to_string(req.choice_id())]; if (choice_func.is<sol::function>()) { sol::protected_function_result result = choice_func(player->GetId(), req.choice_id()); if (!result.valid()) { sol::error err = result; LOG_WARN("Lua choice_%d error: %s", req.choice_id(), err.what()); return; } // 3. 获取 Lua 返回的 next_npc_id(脚本中 return next_id) uint32_t next_npc_id = result.get<uint32_t>(); // 4. 构造 S2G_NPC_DIALOG_NOTIFY 并发送 S2G_NPC_DIALOG_NOTIFY notify; notify.set_npc_id(req.npc_id()); notify.set_dialog_text(GetDialogText(req.npc_id(), req.choice_id())); // 从数据库或Lua中获取文本 // 动态生成选项(示例:若 next_npc_id != 0,则添加“继续对话”选项) if (next_npc_id != 0) { DialogChoice* choice = notify.add_choices(); choice->set_id(1); choice->set_text("继续与" + GetNpcName(next_npc_id) + "交谈"); choice->set_next_npc_id(next_npc_id); } player->GetSession()->Send(notify); } }

5.3 客户端渲染:在Client/src/ui/dialog_window.cpp中解析选项列表

void DialogWindow::OnNpcDialogNotify(const S2G_NPC_DIALOG_NOTIFY& notify) { // 1. 清空现有选项按钮 for (auto& btn : choice_buttons_) { DestroyWindow(btn.hwnd); } choice_buttons_.clear(); // 2. 设置对话文本 SetWindowTextA(text_hwnd_, notify.dialog_text().c_str()); // 3. 为每个选项创建按钮 int y_offset = 100; for (int i = 0; i < notify.choices_size(); ++i) { const DialogChoice& choice = notify.choices(i); HWND btn = CreateWindowA("BUTTON", choice.text().c_str(), WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 50, y_offset, 200, 30, hwnd_, (HMENU)(1000 + i), hInstance_, nullptr); // 4. 绑定点击事件:发送 C2G_NPC_DIALOG_CHOICE_REQ choice_buttons_.push_back({btn, notify.npc_id(), choice.id()}); y_offset += 40; } } // 按钮消息处理 case WM_COMMAND: if (LOWORD(wParam) >= 1000 && LOWORD(wParam) < 1000 + choice_buttons_.size()) { int idx = LOWORD(wParam) - 1000; C2G_NPC_DIALOG_CHOICE_REQ req; req.set_npc_id(choice_buttons_[idx].npc_id); req.set_choice_id(choice_buttons_[idx].choice_id); network_client_->Send(req); } break;

5.4 数据库支持:扩展npc_template表增加dialog_script字段

原表无脚本字段,需执行 SQL:

ALTER TABLE npc_template ADD COLUMN dialog_script VARCHAR(255) DEFAULT '' COMMENT 'Lua对话脚本名,如 npc_1001';

然后在init_data/npc_template.sql中为测试 NPC 添加:

INSERT INTO npc_template (id, name, pos_x, pos_y, dialog_script) VALUES (1001, '村长', 100.0, 200.0, 'npc_1001');

并在resource/script/npc_1001.lua中编写:

-- resource/script/npc_1001.lua function dialog_1() return "欢迎来到新手村!有什么可以帮你的吗?" end function choice_1() return 1002 -- 选择后跳转到ID为1002的NPC end function choice_2() return 0 -- 结束对话 end

从那以后我每次扩展协议,都强制走一遍「定义 .proto → 生成代码 → 编译服务端 → 编译客户端 → 启动服务 → 抓包验证二进制格式 → 手动构造请求测试」的完整链路,哪怕只是加一个字段。因为漏掉任何一环,都会在深夜三点面对一个不报错但就是不工作的黑匣子。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 2:29:36

Zotero 7 插件开发实战:TypeScript 集成 scite 引文统计与避坑指南

简介&#xff1a;这是一款面向科研人员与文献管理用户的Zotero插件&#xff0c;基于TypeScript开发&#xff0c;用于在Zotero中接入scite的Smart Citation数据&#xff0c;帮助读者快速判断一篇论文被后续研究支持、提及还是质疑&#xff0c;从而提升文献筛选与综述写作的效率。…

作者头像 李华
网站建设 2026/10/29 2:29:31

驱动CE拆解:Windows内核交互与内存扫描实战

简介&#xff1a;这份资源是面向嵌入式与逆向调试方向学习者的驱动级 Cheat Engine 工具包&#xff0c;聚焦于驱动保护绕过、内存访问查看与断点调试等分析场景&#xff0c;适合具备一定 Windows 内核与单片机基础的中高级读者研究使用。压缩包共 330 个文件&#xff0c;约 14.…

作者头像 李华
网站建设 2026/10/29 2:29:30

网络技术期末设计答辩通关指南:拓扑、配置与故障排查全解析

简介&#xff1a;这份资源是面向计算机网络相关专业学生的期末设计与课程设计参考包&#xff0c;聚焦某中学网络拓扑规划与eNSP仿真实践。内容围绕网络拓扑选型、综合布线系统规划、VLAN划分与路由配置等关键环节展开&#xff0c;适合正在完成网络课程设计、需要对照完整方案与…

作者头像 李华
网站建设 2026/10/29 2:29:29

Windows流媒体服务端EasyDSS部署实战:从安装到推流播放全流程

简介&#xff1a;EasyDSS-windows-2.2.7-1912281435.zip 是面向 Windows 平台的流媒体视频管理解决方案安装包&#xff0c;适合需要在个人电脑上快速部署视频直播、点播与分发服务的企业用户和开发者。压缩包共 207 个文件&#xff0c;约 55.93MB&#xff0c;以 117 个 js 前端…

作者头像 李华
网站建设 2026/10/29 2:29:23

eNsp网络设计实战:从拓扑到NAT的完整配置与避坑指南

简介&#xff1a;这份资源是面向计算机网络相关专业学生的期末设计与课程设计参考包&#xff0c;聚焦某中学网络拓扑规划与eNsp仿真实践&#xff0c;适合正在完成网络技术大作业、需要完整方案对照与配置思路的学习者。压缩包共21个文件&#xff0c;约7.09MB&#xff0c;包含do…

作者头像 李华
网站建设 2026/10/29 2:28:28

Hydra Windows版部署指南:OpenSSL依赖配置与HTTP爆破实战

简介&#xff1a;本资源为专为Windows平台渗透测试人员准备的Hydra暴力破解工具配套字典合集&#xff0c;适用于网络安全初学者、CTF参赛者及红队实战人员&#xff0c;解决常见服务&#xff08;如SSH、FTP、HTTP表单等&#xff09;密码爆破时字典匮乏的问题。压缩包共99个文件&…

作者头像 李华