一个人用常驻服架构做了个50人同服塔防,踩过的坑全在这了
精华修改于04/05202 浏览开发心得
《梦境守卫者》是我做的一款竖版塔防+英灵合成的多人游戏。常驻服架构,支持50人同服,有抽卡、竞技场、双人合作、好友系统、排行榜、战斗通行证——基本上你在商业手游里见过的系统,我都做了一遍。
从搭建常驻服务器那天算起,到正式上线,最终写了 41,917行Lua代码、28个界面、17个网络模块。上线当天就开始被玩家反馈教做人。
这篇文章会完整记录:每个阶段做了什么、怎么做的、踩了什么坑、上线后怎么被锤的、以及我从中学到了什么。


项目最终规模
| 类别 | 数量 | 说明 |
|------|------|------|
| 总代码量 | 41,917 行 | 纯Lua,不含引擎代码 |
| 界面数 | 28 个 | 从登录到GM后台全覆盖 |
| 游戏模式 | 5 个 | 主线/每日/爬塔/双人合作/竞技场 |
| 核心系统 | 30+ 个 | 存档、抽卡、商店、任务、天赋... |
| 英灵总数 | 21+ 个 | R/SR/SSR/UR 四种稀有度 |
| 技能总数 | 90 个 | 职业技能+英灵专属+全局Buff+地形 |
| 网络模块 | 17 个 | 客户端7个 + 服务端8个 + 共享2个 |
| 货币种类 | 6 种 | 梦晶/梦尘/天赋点/觉醒石/碎片/竞技币 |
| 数据配置 | 17 个文件 | 技能库/关卡/商店/任务/抽卡池... |


一、开发历程
阶段一:核心战斗原型
一切从一个最简单的塔防开始。
战场设计:竖版5列x4行网格。怪物从屏幕上方涌入,英灵站在格子里自动攻击。选竖版是因为手机竖屏握持更自然,5x4的格子数量适中——太少没策略,太多操作麻烦。
核心循环:
花梦晶召唤英灵(1星) → 放到格子里自动攻击 → 击杀怪物赚梦晶 → 召唤更多英灵
合成系统是玩法的核心差异点:
2合1:两个同星英灵拖到一起,随机合成高一星的英灵。简单粗暴,前期快速升星
3合1:三个同名同星英灵合成,可以指定目标阵营。后期定向培养的关键手段
阵营克制设计了六大阵营的四角克制链:
星穹守护者(金) → 深渊梦魇(紫) → 幻境编织者(青) → 混沌使者(红) → 星穹守护者
远古灵兽和梦境行者是中立阵营,不参与克制但有独立优势。克制方攻击加25%伤害+10%暴击,被克制方额外承受15%伤害。
同阵营英灵上场还有同调Buff:2个同阵营攻击力+8%,3个+15%且攻速+5%,4个+25%且攻速+10%。这让阵容构筑有了策略深度——是凑同阵营Buff还是利用克制关系。
7种职业各有攻击方式:
| 职业 | 攻击类型 | 射程 | 特点 |
|------|---------|------|------|
| 射手 | 单体 | 远程(2.5x) | 稳定输出 |
| 法师 | 范围AOE | 远程(2.2x) | 群伤 |
| 坦克 | 单体 | 近程(1.0x) | 肉 |
| 刺客 | 单体(优先低血) | 近程(1.0x) | 收割 |
| 辅助 | 增益 | 中程(1.6x) | 回血 |
| 召唤师 | 召唤 | 中远程(2.0x) | 多段 |
| 控制师 | 控制 | 中远程(1.8x) | 减速 |
星级倍率决定了高星英灵的碾压力:
lua
1星 = 1.0倍
2星 = 2.5倍
3星 = 6.0倍
4星 = 15.0倍
5星 = 35.0倍
一个5星英灵顶35个1星,所以合成升星是游戏的第一优先级。
波次设计:每局8波,每波间隔5秒。怪物数量和强度逐波递增,第8波是最终波。初始给100梦晶,每次召唤花30,击杀怪物奖8。经济压力让玩家不能无脑堆人,必须在"召唤数量"和"合成质量"之间做选择。
这个阶段代码量不大,单文件就能跑。核心玩法验证通过后,才开始往下推。


阶段二:内容扩充
战斗能玩了,但太单调。
技能系统是这个阶段最大的工作量。做了90个技能,分五大类:
| 类型 | 数量 | 举例 |
|------|------|------|
| 职业技能 | 28个 | 射手连射、法师火雨、坦克嘲讽 |
| 英灵专属 | 30+个 | 嗒啦啦的梦境祝福 |
| 全局Buff | 20个 | 全体攻击+15%、暴击率+10% |
| 地形效果 | 10个 | 减速带、加速圈、伤害区 |
| 觉醒技能 | 10个 | 5星英灵解锁的终极技能 |
每波结束后弹出3张技能卡(普通/稀有/史诗/传说),玩家选一张。技能有品质权重:普通10分、稀有25分、史诗50分、传说100分。AI自动战斗时会综合评分选最优技能——匹配职业+20分、匹配英灵+30分、全局技能每个英灵+3分、缺钱时经济技能+25分、血量低时防御技能+30分。
地形系统做了9种,每局随机生成。有的格子踩上去攻速翻倍,有的格子有持续伤害。这给战场增加了空间维度的策略。
英灵扩充从13个补到21个,确保每个职业至少3个代表。每个阵营也至少有3-4个英灵,让阵营Buff更容易凑。最后加了一个UR神话级英灵"嗒啦啦",梦境行者阵营辅助,只能在限时池抽到。
视觉升级:背景从纯色渐变升级为动态星空——粒子星辰、飘浮星云、月相变化。英灵按阵营颜色渲染,金色星穹守护者、暗紫深渊梦魇、青蓝幻境编织者,一眼就能分辨。全部用NanoVG矢量绘制,不依赖贴图资源。


阶段三:常驻服务器——整个项目最关键的转折点
这是从"单机小游戏"变成"真正的多人游戏"的阶段。
为什么要做常驻服?
因为我要做的功能列表里,排行榜、好友系统、双人合作、竞技场PvP——每一个都需要服务端。而且我不想做"房间制"那种断开就没了的联机,我要的是玩家登录就在同一个世界里,随时能看到好友在线、随时能发起合作。
架构设计
整个网络层分成三部分:
服务端(8个模块):
server_main.lua # 主服务,管理在线玩家、路由消息
server_authority.lua # SA(Server Authority)验证框架
server_matchmaking.lua # 匹配系统,处理共鸣/竞技场的排队和配对
server_save.lua # 存档同步,serverCloud读写
server_friends.lua # 好友状态管理,在线/离线追踪
server_gacha.lua # 抽卡服务端执行,概率计算+扣费+保底
server_currency.lua # 货币操作验证
server_cdk.lua # 兑换码验证和发放
客户端(7个模块):
client_main.lua # 主客户端,维持连接,处理推送
client_authority.lua # SA请求封装,发送+等待+回调
client_friends.lua # 好友列表UI数据同步
arena_net.lua # 竞技场对战网络
resonance_net.lua # 共鸣双人协作网络
共享(2个模块):
shared.lua # 客户端/服务端共用的工具函数
SaveEvents.lua # 存档事件定义
核心原则:Server Authority(SA)
这是整个网络架构的基石。所有涉及"价值"的操作,客户端只能请求,服务端验证后才生效。
完整流程:
1. 玩家点"十连抽"
2. 客户端发SA请求:{action: "gacha_pull10"}
3. 服务端收到请求:
a. BatchGet读取玩家存档
b. 检查梦晶余额 >= 300
c. 执行10次抽卡概率计算(含保底逻辑)
d. 扣减梦晶
e. BatchSet写回存档
4. 服务端返回:{results: [...], remainingCrystal: 700}
5. 客户端收到结果:
a. 更新本地英灵数据
b. 同步梦晶余额(直接用服务端返回的绝对值)
c. 播放抽卡动画
为什么不让客户端自己算?因为客户端的代码玩家理论上能修改。如果客户端算概率,玩家可以改成100%出UR。SA架构下,客户端只是一个"提交请求+展示结果"的界面。
存档系统:双保险策略
这是我花了很多心思设计的部分:
```
本地存档(JSON文件)
├── 实时保存:每次数据变化立即写本地
├── 断网可用:没网也能玩单机模式
└── 快速读取:启动时先读本地,零延迟
云端存档(serverCloud)
├── 节流上传:15秒间隔,防止频繁写入
├── 权威数据:冲突时以云端为准
└── 跨设备:换手机登录数据不丢
```
登录流程:
1. 读本地存档 → 立即可用
2. 拉云端存档 → 对比版本号
3. 云端更新 → 覆盖本地(云端优先)
4. 本地更新 → 上传云端(同步)
5. 都没有 → 创建新存档
数据迁移框架是第一天就写的,后来证明这个决策非常正确:
lua
-- migration.lua
local MIGRATIONS = {
-- v1 → v2: 通用碎片拆分为品质碎片
[2] = function(data)
local oldFragments = data.fragments or 0
data.fragments_R = math.floor(oldFragments * 0.4)
data.fragments_SR = math.floor(oldFragments * 0.3)
data.fragments_SSR = math.floor(oldFragments * 0.2)
data.fragments_UR = math.floor(oldFragments * 0.1)
data.fragments = nil
data.version = 2
end,
-- v2 → v3: 音频设置独立化
[3] = function(data)
local oldVolume = data.settings and data.settings.volume or 80
data.settings = data.settings or {}
data.settings.musicVolume = oldVolume
data.settings.sfxVolume = oldVolume
data.version = 3
end,
}
上线后改了两次数据结构,一次都没丢过玩家数据。


阶段四:玩法模式矩阵
常驻服搭好后,开始往上堆游戏模式。
主线梦境(单人PvE)
3个章节,每章多个关卡,难度递进。每关有独立的怪物配置——什么怪、多少波、血量倍率。打通一章解锁下一章,中间有剧情过渡。主线是新手最主要的资源来源。
每日挑战(单人PvE)
每天刷新不同的挑战条件——比如"只能用射手和法师"、"怪物移速翻倍"、"梦晶减半"。限制每天挑战次数,奖励梦晶和梦尘。目的是让玩家每天都有事做。
梦境之塔(单人PvE·无尽爬塔)
无尽模式,每层一波怪,打完选一个祝福Buff带到下一层。祝福是随机的,有攻击翻倍、全体加速、额外生命等。爬得越高排名越前。有专属的塔币货币,可以在塔商店买东西。
这个模式是排行榜竞争的主要内容。
共鸣回廊(双人合作)
这是开发量最大的模式,光网络模块就写了15,000行。
两个玩家共享一个战场,各自召唤英灵但放在同一个网格里。核心挑战是状态同步——两个客户端看到的战场必须一致。
匹配流程:
玩家A点"匹配" → 进入匹配队列 → 服务端找到玩家B →
通知双方 → 创建共享战场 → 同步初始状态 → 开始战斗
也支持好友邀请,跳过匹配队列直接组队。
要处理的边界情况非常多:一方掉线怎么办、一方退出怎么办、两个人同时操作同一个格子怎么办。最终的方案是服务端仲裁所有操作,两个客户端只做显示。
竞技场(PvP)
异步PvP——不是实时对战,而是打别人的AI控制阵容。玩家设置防守阵容,进攻时选对手,AI操控对方的英灵。
这样设计的好处是不需要两个玩家同时在线,匹配秒级完成。坏处是AI操控的阵容实力会打折扣,但对独立游戏来说够用了。
有天梯排名和竞技币奖励。


阶段五:养成与留存系统
游戏模式做完了,但留不住人。这个阶段集中做"让玩家每天都想登录"的系统。
抽卡系统
这是最重要的内容来源。设计了两个卡池:
标准池:
R/SR/SSR都能出,没有UR
80抽硬保底SSR
十连保底至少一个SR
保底计数跨池保留
限时池:
概率UP的UR英灵(嗒啦啦)
70抽硬保底UR
心愿单系统:选2个英灵,概率额外+30%
活动结束后UR英灵进入标准池但概率极低
抽卡是SA验证的——概率在服务端算,客户端完全无法干预结果。
```lua
-- 服务端抽卡核心逻辑(简化)
function ServerGacha.Pull(playerData)
playerData.pity = playerData.pity + 1
if playerData.pity >= 80 then
-- 硬保底
playerData.pity = 0
return rollSSR()
end
local roll = math.random()
if roll < 0.02 then -- 2% SSR
playerData.pity = 0
return rollSSR()
elseif roll < 0.15 then -- 13% SR
return rollSR()
else -- 85% R
return rollR()
end
end
```
抽卡动画
专门写了一个独立界面 gacha_animation_screen.lua
。单抽有光球升空→爆开→揭示的流程,十连有排队展示+跳过功能。UR出场时全屏金光特效。
这个界面后来是Bug最多的地方——UR遗漏排序、动画卡死、超时无保护。后面Bug部分会详细讲。
登录奖励
7天日历式领取,第7天给大奖(SSR碎片或大量梦晶)。领完7天循环刷新。简单但有效,每天登录就有东西拿。
战斗通行证
免费+付费双轨道。通过做每日/每周任务获取通行证经验,升级解锁奖励。免费轨道给基础资源,付费轨道给稀有材料和英灵碎片。
商店系统
三个商店页签:
梦晶商店:花梦晶买经验药水、碎片
梦尘商店:花梦尘买觉醒材料
碎片兑换:碎片换指定英灵
每天自动刷新商品。
任务系统
三类任务同时运行:
日常任务:打3局、合成5次、召唤10个英灵(每日重置)
周常任务:通关主线2章、爬塔到20层(每周重置)
成就:收集所有阵营英灵、5星英灵达到5个(永久)
任务完成后手动领奖,给梦晶、梦尘、碎片等。
CDK兑换码系统
配合QQ群发福利。服务端验证CDK有效性、是否已使用、绑定UID。奖励直接发到玩家账户。


阶段六:社交与打磨
最后补齐社交系统和用户体验。
好友系统
搜索UID加好友
好友列表显示在线/离线状态
好友间可以直接邀请打共鸣回廊
好友排行榜(独立于全服排行)
服务端持续追踪在线玩家状态,好友上线/下线时推送通知。
排行榜
四个维度:
全服排名:按综合战力
塔排名:按爬塔最高层数
竞技场排名:按天梯分数
好友排名:只看好友圈
排行榜数据用 serverCloud 的排行榜API,实时更新。
新手引导
8步教程,分两个阶段:
主菜单引导(4步):欢迎→第一场战斗→第一次抽卡→查看图鉴
战斗引导(4步):召唤英灵→合成升星→选择技能→拖拽移位
每步完成后标记,不会重复弹出。如果玩家跳过也不影响游戏。
自动战斗
AI托管全部操作:
自动召唤:有钱有空位就召
自动合成:优先3合1同名,其次2合1低星
自动选技能:综合评分选最优
自动选阵营:3合1时选场上最多的阵营
自动出售:战场满时卖最低星+最低品质英灵腾位
自动连关:打完一关自动进下一关
自动重开:失败自动重新挑战
AI决策间隔0.6秒,重定位间隔1.2秒。每次只移动一个英灵避免视觉混乱。
性能监控
内置FPS/帧时间/内存监控,设置里可以开关。开发阶段常开,帮我发现了几个内存泄漏。


二、上线后的2天——Bug修复实录
2026年3月30日正式上线。玩家进来了,Bug也跟着来了。
Bug完整清单
| # | Bug描述 | 玩家原话 | 严重程度 | 根因类型 |
|---|--------|---------|---------|---------|
| 1 | 羁绊合成5星降为4星 | "合出来的英灵变4星了" | 严重 | 数据覆盖 |
| 2 | 合成产出英灵无模型 | "合出来的英灵是空的" | 严重 | 数据不完整 |
| 3 | CDK兑换码无法使用 | "CDK输了没反应" | 中等 | uid=0鉴权失败 |
| 4 | 抽卡显示梦晶不足 | "明明有梦晶却不让抽" | 严重 | 依赖系统未初始化 |
| 5 | 抽卡动画卡在"召唤中" | "抽卡卡住了,一直转圈" | 严重 | 遗漏枚举+无超时 |
| 6 | 梦晶扣费金额异常 | "抽完十连梦晶不对" | 严重 | 相对减法竞态 |
| 7 | 关卡列表滑动误触 | "滑列表老是点进关卡" | 中等 | 无滑动距离判定 |


我的排查方法:数据流追踪法
不管什么Bug我都用同一套路子——沿着数据流方向一步步查,找到数据"断裂"的那个节点。
玩家操作 → 客户端逻辑 → SA请求 → 服务端处理 → 返回结果 → 客户端回调 → UI更新
↑ ↑
检查点1 检查点2
本地数据对不对 回调同步对不对
对于每个Bug,我都是从左往右逐个节点排查,直到找到数据出错的那个点。下面用三个最典型的Bug详细演示这个过程。


Bug #4 详解:抽卡显示梦晶不足
玩家反馈:"明明有梦晶却提示不足"
排查过程:
抽卡前有个余额检查:
lua
local Money = require("systems.currency")
if Money.GetBalance("crystal") < cost then
return nil, "梦晶不足"
end
跟进 Money.GetBalance()
,发现它依赖 Money.Init()
初始化内部数据。但 Money.Init()
是在登录流程的某个异步回调里调用的——如果玩家操作快,在 Money.Init()
跑完之前就点了抽卡,余额读出来就是0。
修复:
绕过 Money 中间层,直接用底层 API 读取:
lua
-- 直接从 PlayerData 读余额,不依赖 Money 系统的初始化
local pd = PlayerData.Get()
if pd.crystal < cost then
return nil, "梦晶不足"
end
教训:异步初始化的系统,在调用前必须确认初始化完成。或者像这样直接用底层 API 绕过。


Bug #5 详解:抽卡动画卡在"召唤中"
玩家反馈:"抽卡卡住了,一直转圈出不来"
排查过程:
第一步,确认是动画卡了还是网络卡了。查服务端日志,抽卡请求正常返回了结果,说明网络没问题,是客户端动画卡死。
第二步,看动画状态机。gacha_animation_screen.lua
的动画流程是:
排序结果(按稀有度) → 逐个播放揭示动画 → 全部播完 → 显示结果总览
第三步,找到排序代码:
```lua
local order = { R = 1, SR = 2, SSR = 3 }
-- 没有 UR !!!
table.sort(results, function(a, b)
return order[a.rarity] < order[b.rarity] -- UR时 order[rarity] = nil
end)
```
根因:我后来加了UR稀有度,但排序表漏了。order["UR"]
返回nil,nil和数字比较直接报错,而且这个错误被外层吞掉了(没有pcall),导致动画状态机卡在"排序中"永远无法推进。
修复:三管齐下
```lua
-- 1. 补全枚举
local order = { R = 1, SR = 2, SSR = 3, UR = 4 }
-- 2. 加 fallback 防御
table.sort(results, function(a, b)
return (order[a.rarity] or 0) < (order[b.rarity] or 0)
end)
-- 3. 加超时保护 + pcall错误捕获
local animTimeout = 8.0
local ok, err = pcall(function()
playAnimation(results)
end)
if not ok then
print("[动画异常] " .. tostring(err))
skipToResults() -- 强制跳到结果界面
end
```
教训:新增枚举值后,全局搜索所有引用这个枚举的代码位置,逐一确认。同时异步流程必须有超时保护。


Bug #6 详解:梦晶扣费金额异常
玩家反馈:"抽一次十连抽,梦晶有显示不足了,是不是没彻底修复"
注意"是不是没彻底修复"这句话——说明之前已经修过一版(Bug #4),但没修干净。这给了我重要提示:上一次修复改了什么?改的那个地方附近还有没有问题?
排查过程:
这次我翻了6个文件,沿数据流逐个节点排查:
| 步骤 | 文件 | 查了什么 | 结果 |
|-----|------|---------|------|
| 1 | gacha.lua | 客户端Pull10发起 | cost=300,正确 |
| 2 | gacha.lua | SA请求参数 | 正确 |
| 3 | servergacha.lua | BatchGet读取余额 | 正确 |
| 4 | servergacha.lua | 扣减+BatchSet写回 | 正确,服务端余额无误 |
| 5 | server_gacha.lua | 返回给客户端的数据 | 只返了cost,没返余额! |
| 6 | gacha.lua | 客户端回调同步 | pd.crystal = pd.crystal - cost
|
断裂点在第6步。客户端用"本地旧值 - 消耗"来计算新余额。但本地的 pd.crystal
可能因为以下原因和服务端不一致:
自动存档刚好在SA回调之前跑了,把旧值覆盖回来
另一个SA回调(比如商店购买)也在修改同一个字段
云端同步把一个更早的值拉了下来
修复:
服务端:
lua
-- 多返回一个字段
respond(true, {
results = results,
pityData = pityData,
cost = totalCost,
remainingCrystal = playerData.crystal, -- 新增:扣完后的确切余额
})
客户端:
```lua
-- 修复前:相对减法
pd.crystal = math.max(0, pd.crystal - cost)
-- 修复后:绝对赋值,以服务端为准
if remainingCrystal ~= nil then
pd.crystal = remainingCrystal
else
-- 向后兼容:老版本服务端没返回这个字段时,fallback到减法
pd.crystal = math.max(0, pd.crystal - cost)
end
```
一个等号替换了一个减号,Bug修好了。
教训:C/S架构中,服务端算完的最终状态要直接下发(绝对值),客户端不要自己再算一遍(相对操作)。


其他Bug简述
Bug #1 合成降星:合成函数先从模板创建英灵(默认1星),再覆盖星级。但中间有个 recalcStats()
函数又读了模板的默认星级,把覆盖的值冲掉了。修复:确保星级赋值在所有属性计算之后。
Bug #2 合成无模型:模板里没有模型引用字段,创建时没补上。修复:创建新英灵后逐字段检查完整性。
Bug #3 CDK uid=0:获取uid的代码在登录完成前就执行了,拿到默认值0。修复:确保在登录回调之后才获取uid。
Bug #7 滑动误触:PointerUp只检查了"在哪个卡片上松手",没检查手指移动了多远。修复:记录PointerDown坐标,PointerUp时算距离,超过10像素判定为滑动忽略点击。


三、六种踩坑模式总结
从这7个Bug里,我归纳了6种反复出现的模式:
模式1:遗漏枚举/分支
触发场景:新增了一个枚举值(比如UR稀有度),但某些引用这个枚举的代码没有覆盖新值。
典型后果:访问nil值 → 逻辑中断 → 状态机卡死
防御方法:
新增枚举值后,全局搜索所有引用点
所有 switch/map 查找都加 fallback 默认值
关键流程用 pcall 包裹
模式2:相对操作 vs 绝对同步
触发场景:客户端用 本地值 ± 差值
更新数据,但本地值可能过时。
典型后果:显示金额和实际不符,越操作偏差越大
防御方法:
服务端返回操作后的绝对值
客户端直接赋值而非自己计算
保留 fallback 向后兼容
模式3:数据不完整初始化
触发场景:创建新对象时,某些字段未填充或被模板默认值覆盖。
典型后果:字段为nil → 渲染空白/属性异常
防御方法:
创建对象后打印所有关键字段
区分"模板值"和"运行时计算值"的赋值顺序
模式4:异步时序依赖
触发场景:A系统依赖B系统初始化完成,但B是异步的,A在B之前就被调用了。
典型后果:读到未初始化的默认值(通常是0或nil)
防御方法:
确认初始化顺序,或用底层API绕过
关键系统用标志位标记初始化完成状态
模式5:交互手势误判
触发场景:触摸事件缺少滑动距离阈值。
典型后果:想滑动却触发了点击
防御方法:
PointerDown记坐标,PointerUp算距离
超过阈值判定为滑动,忽略点击事件
模式6:鉴权上下文缺失
触发场景:发请求时用户身份信息还没获取到。
典型后果:uid=0,服务端校验失败
防御方法:
请求发出前打印请求体确认字段值
确保身份信息在登录完成后才可用


四、关键技术决策复盘
决策1:Server Authority 架构
结论:做对了,但要做好。
好处是客户端无法作弊,货币和抽卡结果绝对安全。坏处是客户端-服务端之间的数据同步成了Bug的主要来源。
核心教训:SA架构下,服务端的返回结果一定要包含操作后的绝对状态("你现在有多少"),而不只是操作本身("扣了多少")。这一条能避免至少一半的同步Bug。
决策2:本地+云端双存档
结论:做对了,但要注意竞态。
玩家断网也能玩,体验好。但15秒的云端存档节流和SA异步回调之间可能产生竞态——SA回调修改了内存数据,但自动存档把旧数据覆盖上去了。
核心教训:SA回调里修改数据后应该立即触发一次本地存档,而不是等15秒定时器。
决策3:数据迁移框架
结论:做对了,从第一天就应该有。
上线后改了两次数据结构(碎片拆分、音频设置独立),零数据丢失。迁移链自动执行:v1→v2→v3,玩家无感知。
如果没有迁移框架,改数据结构就意味着老玩家数据要手动处理,或者写一堆兼容代码。
决策4:自动战斗系统
结论:做对了,是长线留存的刚需。
独立游戏的玩家时间碎片化,不可能一直手动操作。AI托管让玩家可以挂机刷资源,同时保留手动操作的乐趣。
AI的决策优先级是反复调过的:合成 > 召唤 > 出售低星英灵腾位。出售的排序标准是星级优先(低星先卖),同星按职业价值(坦克辅助 < 控制召唤 < 刺客射手法师)。


五、防御性编程清单
从这些坑里提炼出来的,现在每次写代码我都过一遍:
| # | 做法 | 防什么 | 优先级 |
|---|-----|-------|-------|
| 1 | 服务端返回绝对值,客户端直接赋值 | 同步偏差/竞态 | 高 |
| 2 | 新增枚举值后全局搜所有引用点 | 遗漏分支 | 高 |
| 3 | 异步操作加超时保护 | 状态机卡死 | 高 |
| 4 | pcall包裹关键回调 | 异常静默失败 | 高 |
| 5 | 触摸事件加滑动距离阈值 | 手势误判 | 中 |
| 6 | 创建对象后日志打印关键字段 | 数据不完整 | 中 |
| 7 | 请求发出前打印请求体 | 鉴权/参数缺失 | 中 |
| 8 | 数据迁移框架第一天就搭 | 版本升级丢数据 | 高 |


六、给同样在做多人游戏的独立开发者
1. 常驻服不难搭,难的是数据同步。你会发现80%的Bug都跟"客户端和服务端数据不一致"有关。从第一天就想好同步策略。
2. SA架构一开始就要做。如果先做客户端信任再改成服务端验证,基本等于重写。宁可一开始慢一点,也要把SA框架搭好。
3. 存档迁移框架第一天就该有。你上线后一定会改数据结构,这是100%确定的事。
4. 自动战斗不是可选功能。手游玩家的注意力是稀缺资源,没有挂机功能留不住人。
5. 枚举值是最容易漏的地方。每次加新类型、新稀有度、新货币,都要全局搜索确认所有引用点。这种Bug不会在开发时暴露,只在特定条件下触发。
6. 玩家的操作路径比你想象的丰富。你测试时走的是"正常流程",玩家会快速连点、网络波动时操作、在你没想到的时机切换界面。做好心理准备。
7. 上线前你觉得没Bug,上线后你会发现全是Bug。不是代码质量差,是真实环境的复杂度远超测试环境。


一句话总结
沿数据流追踪,在断裂点修复,用绝对值同步,对所有分支设fallback。


