在线联机原型全集:第 16 章 简版 MOBA
简版 MOBA(Battle + AI Units|Tick Sync, Reconnect & Recovery)
- 类别:实时战斗 + 小规模 MOBA + AI 兵线/野怪
- 目标:在 3v3 或 4v4 的小场景中,验证服务器权威 Tick 同步、可重放的事件流、玩家断线→重连→完整恢复、以及**AI 单位(兵线/防御塔/野怪)**的确定性驱动。
- 原型代号:
proto-016-mini-moba - 依赖模块:
proto-007-snake-battle(实时房间/网关基线)、proto-014-room-escape-sync(事件溯源/快照思路)、proto-015-mini-race-rollback(回滚/插值经验复用) - 推荐语言栈:服务端 Go/Java(Go 栈优先),客户端 TypeScript(WebGL/Unity WebGL 任一),AI/导航可选 C/Rust 扩展
- 协议栈:WebSocket(可靠通道) + UDP(可选,玩家输入与位置增量)
- Tick 频率:服务器 20–30Hz(建议 20Hz=50ms/帧),客户端渲染 60fps
1. 核心玩法与约束
-
地图:单兵线小地图(直线或“L 型”),每方一座基地 + 2 座外塔(T1/T2)。
-
玩家:每队 3–4 人;英雄有 2 个主动技能 + 1 个大招(冷却长)。
-
AI 单位:
- 兵线(Creeps):每 20s 刷一波,顺路推进,遇敌/塔停留输出;
- 防御塔(Towers):锁定范围内最近/仇恨最高目标;
- 中立野怪(可选):提供临时增益。
-
胜负:推掉对方基地即胜(不引入买活/装备系统,专注同步/AI/恢复链路)。
-
确定性:同一种输入在相同 Tick 序列下,服务端必须得到一致结果(AI 与技能需可确定性重放)。
2. 同步模型(Tick-Based Authoritative Server)
2.1 模式选型
- 服务器权威 + 客户端轻预测:客户端仅对自身移动与非冲突性 UI 动画做局部预测;伤害、击杀、硬控等必须以服务器回传为准。
- 输入延迟缓冲:客户端发送输入时标记
client_tick,服务器按Tick 序执行;客户端渲染延后 2–3 Tick 以吸收抖动。 - 确定性物理/AI:避免不确定 API(随机数统一来自服务器种子、帧内固定迭代顺序、避免浮点不可重现差异)。
2.2 帧结构
- 输入帧(InputFrame):
{uid, tick, move_vec, cast_cmd?, target?} - 状态帧(StateFrame):
{tick, changed_entities[], acks[], rng_mark, hash} - 关键帧(Keyframe):每 N 帧(建议 1–2s)落盘快照;其余帧用事件重放恢复。
2.3 Tick 流程(服务器)
for each Tick t:
1) 收集输入(t-Δ 到 t),丢失则复用上次移动向量
2) 处理技能指令(排队/判定资源/冷却/施法前摇)
3) 推进 AI(兵线/塔/野怪):感知→决策→执行
4) 执行移动/碰撞/投射体/伤害结算
5) 生成状态增量(diff)与一致性哈希
6) Append event_log(t);必要时写关键帧 snapshot(t)
7) 广播 StateFrame(t)
2.4 客户端时序
- 渲染滞后:渲染
t_render = t_server - 2..3; - 自身移动预测:本地立即推进;当服务端状态回到达时,做微校正(< 0.2m 的偏差进行平滑 Lerp)。
- 他人/AI:仅插值/外推(外推长度 ≤ 2 帧),到权威帧即对齐。
3. 断线恢复(Reconnect & Recovery)
3.1 目标
- 10 秒内恢复战斗画面(场上状态“秒回”),输入可继续提交;
- 保证恢复后与服务器一致(校验 Hash),拒绝异常客户端。
3.2 恢复链路
-
客户端重连取得
room_id、last_acked_tick; -
服务器根据
last_acked_tick选择最近关键帧KF(k)(k ≤ last_acked_tick),返回:snapshot@k(二进制,压缩)events[k+1..t_now](事件流增量)- 当前
tick=t_now与rng_mark
-
客户端离线重放至
t_now,计算哈希比对hash@t_now; -
比对通过 → 切换到在线帧流;不通过 → 回落到全量快照再重放;仍失败 → 强制全量替换并提示“状态已对齐”。
3.3 数据与校验
- 状态快照:压缩的 ECS 数据包(只含必要组件字段,位置/血量/技能 CD/AI 状态等)。
- 事件流:输入、技能施放、投射体生成/命中、AI 选择目标、死亡事件等(按 tick 顺序)。
- 一致性哈希:服务器每帧计算
hash = SHA1(t || rng || ∑entity.comp_hash),客户端比对。
4. 实体与组件(ECS)
4.1 核心实体
- Hero:玩家操控英雄
- Creep:兵线小兵
- Tower:防御塔
- Projectile:投射体
- Neutral(可选):中立怪/增益点
4.2 核心组件(示例)
| 组件 | 字段(示例) |
|---|---|
Transform |
pos[x,y], rot, vel[x,y] |
Stats |
hp,max_hp, atk, def, ms, as |
Team |
camp(blue/red) |
AbilitySet |
Q,W,R(冷却、法力、前/后摇、施法范围) |
AIState |
state(enum), target(id), threat, leash |
Aggro |
table[targetId]=threat |
Projectile |
owner, speed, dir, range, onHit |
TowerBrain |
range, priority_rule, cooldown |
Respawn(可选) |
dead_until_tick |
5. AI 设计(兵线 & 塔 & 野怪)
5.1 通用行为树(BT)/状态机(FSM)
采用轻量 FSM + 条件表,确保确定性与可重放。
5.1.1 兵线(Creep)FSM
stateDiagram-v2
[*] --> Advance
Advance --> Fight: EnemyInRange
Fight --> Advance: NoEnemyInRange
Fight --> Retreat: LeashBroken or LowHP
Retreat --> Advance: ReEnterPath
- Advance:沿预计算路径点前进(A* 预烘焙,帧内线性跟踪)
- Fight:锁定最近敌方单位(英雄优先 > 小兵 > 建筑),普攻
- Retreat:超过牵引半径(leash)或生命过低则回到路径点
5.1.2 塔(Tower)规则
- 目标选择:优先攻击攻击过友方英雄的敌英雄,否则最近目标;
- 仇恨转移:若多个目标满足条件,选择仇恨最高者;
- 射击节奏:固定冷却(如 1s),投射体飞行可见;
- 伤害模型:塔对英雄伤害递增(防“越塔强杀”)。
5.1.3 野怪(可选)
- Idle → Patrol → Fight → LeashBack
- 仇恨丢失/牵引回位:离出生点过远或 3s 未受击,则回位回血。
5.2 威胁/仇恨(Threat)
- 基础威胁 = 造成伤害 × α + 治疗量 × β + 距离加权;
- 受控于上限,衰减系数 γ(每秒);
- 塔单独维护“英雄优先级”规则覆盖 Threat。
6. 技能与战斗(Ability System)
6.1 技能生命周期
sequenceDiagram
participant C as Client
participant S as Server
C->>S: cast(Q, target=posA, tick=1234)
S->>S: validate(cooldown, mana, range)
S-->>C: ack(cast_id, start_tick)
S->>S: apply pre-cast (lock, forward)
S->>S: spawn projectile / apply buff
S-->>All: state_diff(entities..., tick=1235..)
6.2 技能类型
- 定点投射体(Fireball):起点/方向/速度,命中即伤害 + 击退(可选);
- 瞬发指向(Bolt):直接对目标单位结算(需视野与距离);
- 圆形 AoE(Nova):延迟触发(地面提示),命中给减速/沉默等;
- 通用参数:施法前摇/后摇、冷却、施法距离、法力消耗、可打断与否。
6.3 伤害结算与数值
- 公式示例:
dmg = max(1, (atk * k_skill + flat) * (1 - def / (def + 100))) - 暴击(可选):
crit_chance、crit_mult; - 减益/控制:
slow%, stun_duration, silence_duration;控制统一在服务器侧打表。
7. 移动、碰撞与投射体
- 移动:英雄移动速度
ms,每帧pos += dir * ms * Δt; - 碰撞:简单圆形碰撞体,使用网格/四叉树加速邻域收集;
- 穿插:小兵之间可设置低优先级“软挤压”避免卡死;
- 投射体:服务器每帧推进,命中后触发 onHit;命中判断采用圆形交叠或射线到圆最短距。
- 地形:路径烘焙 + 站位点;非走地面(障碍)直接拒绝。
8. 数据结构与协议
8.1 WS 消息(示例)
// 输入帧:客户端 -> 服务器
{ "t":"in", "uid":1001, "tick":20250, "move":[0.7, -0.2], "cast":{"slot":"Q","target":[38.2,17.6]} }
// 状态帧:服务器 -> 客户端(增量)
{
"t":"st","tick":20250,"rng":39122,"hash":"a8f...3c",
"ents":[
{"id":1,"pos":[10.8,5.2],"hp":720,"cd":{"Q":4.1}},
{"id":201,"pos":[12.0,5.3],"hp":180} // creep
],
"ev":[
{"k":"hit","src":1,"dst":201,"dmg":65,"type":"phys"},
{"k":"death","id":201}
],
"ack":[20248,20249]
}
// 快照 + 事件流:用于断线恢复
{
"t":"recover","kf_tick":20000,
"snapshot":"<binary-zstd-base64>",
"events":[ /* 20001..20250 */ ],
"now":20250,"rng":39122,"hash":"a8f...3c"
}
8.2 服务器内部表
rooms(room_id, map_id, seed, tick, state)entities(room_id, id, archetype, comp_blob)event_log(room_id, tick, seq, payload)snapshots(room_id, kf_tick, blob_uri, size)matches(id, teamA, teamB, result, duration, stats_blob)
9. 一致性与确定性
- 随机数:
rng(seed, tick, salt)生成;事件内需以固定顺序迭代,避免迭代顺序差异; - 浮点问题:尽量改用定点数(例如
int32表示 1/1000 m),或限制计算路径(统一 round); - 组件遍历顺序:按照
entity_id升序; - 哈希:对关键字段做 stable-hash;忽略非关键的渲染字段。
10. 断线/重连 UX
- 软暂停:单人断线不暂停全局;其英雄进入“保守 AI”模式(护塔/后撤)。
- 重连面板:显示“同步进度/已重放 X 帧”;
- 技能与输入:重连前排队的技能若未开始施放 → 自动取消;移动命令不回放,避免“幽灵走位”。
11. 反作弊与风控
| 风险 | 说明 | 对策 |
|---|---|---|
| 输入回放/加速 | 高频或超前 tick | 服务器丢弃超前/回放帧;限流 |
| 修改位置 | 客户端篡改自身 | 位置/速度以服务器计算为准 |
| 改技能 CD | 本地绕过 | 服务器校验资源与冷却 |
| 透视/射线 | 客户端显示作弊 | 服务器做视野裁剪;仅送可见信息 |
| 事件篡改 | 伪造命中 | 投射体/伤害由服务器生成与结算 |
| 断线博弈 | 蓄意断线免死 | 断线进入“可击杀”AI,不免伤 |
12. 监控与指标
- 同步:
avg_rtt, jitter, loss, ack_delay - 断线恢复:
reconnect_count, mean_recover_ms, recover_fail_rate - AI:
lane_pressure, tower_shots/min, creep_kill_share - 战斗:
dps_avg, teamfight_length, stun_time_sum - 一致性:
hash_mismatch_rate, resim_frames_per_min - 性能:
tick_ms_p50/p95/p99, ents_count, diff_bytes_per_tick
13. 压测与测试计划
- 确定性回放:相同事件流重放 100 次哈希一致;
- 延迟/丢包:50–200ms RTT / 5–10% loss;测量恢复时间与插值抖动;
- AI 对齐:对不同 CPU/平台跑同一波次兵线,检查推进距离/存活率一致;
- 断线恢复:中途拔网线 5s,再连回,验证 2s 内画面对齐;
- 边界用例:关键帧损坏/丢失 → 回退策略有效;
- 压力:每房 8 人 + 60 AI,服务器 Tick p95 < 40ms。
14. 参考实现骨架(节选)
14.1 Go 服务器 Tick 循环(简化)
type Room struct {
Tick int64
RNG RNG
Ents *World // ECS
Bus EventBus
}
func (r *Room) Step() {
t := r.Tick + 1
inputs := DrainInputs(t) // 收集 t 的输入
ApplyInputs(r.Ents, inputs, t) // 写入意图组件
StepAbilities(r.Ents, t, &r.RNG) // 技能前后摇/冷却/投射体生成
StepAI(r.Ents, t, &r.RNG) // 兵线/塔/野怪决策
IntegrateMovement(r.Ents, t) // 移动/碰撞推进
ResolveCombat(r.Ents, t, &r.RNG) // 伤害结算/死亡/经验
diff := BuildDiff(r.Ents) // 生成增量
hash := StableHash(r.Ents, t, r.RNG) // 一致性哈希
AppendEventLog(t, inputs, diff, hash)
if t%40 == 0 { WriteKeyframe(t, r.Ents) }
BroadcastState(t, diff, hash)
r.Tick = t
}
14.2 TS 客户端重连恢复(简化)
async function recover(roomId: string, lastAck: number) {
const rec = await fetch(`/recover?rid=${roomId}&ack=${lastAck}`).then(r=>r.json());
const snap = decodeAndInflate(rec.snapshot);
world.loadSnapshot(snap); // ECS 重建
for (const ev of rec.events) applyEvent(world, ev);
const ok = hash(world, rec.now, rec.rng) === rec.hash;
if (!ok) {
// 回退到全量替换
world.loadSnapshot(decodeAndInflate(rec.full_snapshot));
}
connectRealtime(rec.now); // 切入在线流
}
15. 地图与兵线参数(示例)
| 参数 | 默认 |
|---|---|
| 刷兵间隔 | 20s |
| 单波数量 | 3 近战 + 1 远程 |
| 兵线 HP/ATK | 280 / 18 |
| 塔射程/冷却 | 8m / 1s |
| 英雄移速 | 4.5 m/s |
| 经验/金币 | 仅作统计,不影响战斗(原型阶段) |
16. 版本迭代路线
| 版本 | 目标 | 要点 |
|---|---|---|
| v0.1 | 房间框架 + Tick 同步 | 输入→AI→战斗→广播链路跑通;无断线 |
| v0.2 | 断线恢复 | 关键帧 + 事件流 + 哈希校验;2s 内恢复 |
| v0.3 | 塔与兵线行为打磨 | 仇恨/优先级/牵引;塔递增伤害 |
| v0.4 | 技能系统完善 | Q/W/R 三系;投射体/地表延迟技能 |
| v1.0 | 稳定性/压测 | p95<40ms;丢包/抖动容错;A/B 指标看板 |
17. MVP 勾选清单
- 权威 Tick 同步(20Hz),输入缓冲与 ACK
- 兵线/塔 AI(确定性 FSM),威胁表
- 技能 Q/W/R(瞬发/投射体/AoE 各一类)
- 断线恢复(关键帧 + 事件流 + 哈希)
- 客户端自身移动微预测 + 他方插值
- 回放(事件溯源)与赛后复盘
- 基本反作弊与视野裁剪