归档说明:本文档概括当前项目的整体实现细节,涵盖游戏架构分层、流程控制原理、客户端单机实现、服务端权威实现、联机 FlowStep 同步协议及客户端消费层。内容自洽,可直接作为开发存档阅读,于2026.6.12日留存于博客备份。
目录项目总览 整体架构分层 GameCore 共享逻辑层 流程控制模块详解 客户端单机实现 服务端权威实现 联机 FlowStep 协议与同步模型 客户端联机消费层 双端对照与架构模式总结 扩展接入与调试验证 源码文件索引 1. 项目总览《恶魔轮盘》是一款四人回合制卡牌博弈游戏,支持 Unity 客户端单机 (人类 + 本地 AI)与 Unity WebGL / Standalone 联机 (权威服务器 + 多人类 + AI 补位)两种运行模式。
技术栈概览:
层次 技术 职责 Unity 客户端 C# + UGUI/IMGUI 表现、输入、单机本地逻辑驱动 服务端 ASP.NET Core + SignalR Hub 登录、大厅、房间、权威对局 共享逻辑 GameCore(Assets/Scripts 链接至服务端) 规则、流程、技能、道具、AI 决策 协议 Protobuf(server/proto/v1/) 客户端与服务端消息 DTO 持久化 MySQL 账号、排行榜;房间与对局在内存
核心设计目标:
规则只写一份 :FlowController 在 Unity 单机与服务端联机共用,避免双端逻辑漂移。流程与表现分离 :流程层只发 PresentationCueSpec 和快照,不直接操作 UI。联机防作弊 :随机数、伤害、道具数量均在服务端 GameSession 内结算;客户端只收 FlowStep 展示。可中断协程流程 :用 IEnumerator + yield 表达「等玩家决策」,单机用 Unity 协程,服务端用 CoroutineDriver。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 flowchart TB subgraph Client["Unity 客户端"] GE[GameEntry] DS[DebugService] HUD[NormalGameUIInputView / NormalHud] NET[OnlineSession + OnlineHudCoordinator] GE -->|单机| DS GE -->|联机| NET DS --> HUD NET --> HUD end subgraph Server["ASP.NET 服务端"] HUB[GameHub SignalR] GLS[GameLoopService] SDH[ServerDebugHost] FP[FlowPublisher] HUB --> GLS GLS --> SDH SDH --> FP FP --> HUB end subgraph Core["GameCore 共享"] GS[GameSession] FC[FlowController] SK[SkillExecutor] IT[ItemService] FC --> GS GS --> SK GS --> IT end DS --> FC SDH --> FC HUB <-->|FlowStep / SubmitDecision| NET
2. 整体架构分层 2.1 模块职责边界模块 职责 禁止事项 FlowController 回合流转、五阶段状态机、响应链轮询、状态栈压栈/出栈 不直接画 UI、不读 WebSocket GameSession 聚合玩家列表、枪膛、事件总线、技能执行器、回合指针 不决定 UI 如何展示 IDebugHost 决策输入、表现 cue 队列、快照推送(双端差异插槽) 不修改游戏规则 SkillExecutor / ItemService 响应事件、执行主动/被动技能、道具 CRUD 不推进回合阶段 AIDecisionMaker / GameDebugAiResolver 输出 PlayerDecision 不直接改实体状态 FlowPublisher (仅服务端)合并 cue/战报/快照为 FlowStep 广播 不含游戏规则 OnlineHudCoordinator (仅联机客户端)revision 排序、表现门禁、权威态/显示投影 不跑 FlowController
2.2 服务端进程结构1 2 3 4 5 6 7 NewRingGame.Server ├── Auth/ 登录、注册、Token 签发与校验 ├── Lobby/ 在线玩家、房间列表 ├── Room/ 房间生命周期、准备状态、AI 补位、自动开局 ├── Game/ GameLoopService、ServerDebugHost、FlowPublisher、MatchStore ├── Connection / WebSocket 连接映射、断线清理 └── Hubs/GameHub SignalR 实时消息入口
内存热数据(进程内字典,重启丢失):Session Token、ConnectionId→PlayerId、房间实例、进行中对局 MatchInstance(含 GameSession + seed)。MySQL 仅存账号与排行榜等冷数据。
2.3 客户端运行模式GameEntry 通过三个布尔标志区分状态:
模式 IsMatchRunningIsOnlineMatch流程引擎位置 单机正常/Debug true false 本地 Session.Flow.RunGameCoroutine() 联机对局 true true 不跑 本地 FlowController,只消费 FlowStep大厅/结算 false false 无
联机入口 BeginOnlineMatch 设置 IsOnlineMatch = true,由 NormalGameUIInputView.EnterOnlineMatch 初始化 HUD 与 OnlineHudCoordinator,等待服务端推送。
2.4 通信方式HTTPS REST :登录(账号不存在则自动注册)、健康检查。SignalR WebSocket(GameHub) :大厅、房间、准备、对局 FlowStep 下行、SubmitDecision 上行。Unity WebGL 必须使用 WebSocket(wss),因此服务端选用 SignalR 而非裸 TCP。 连接流程:客户端 POST /api/auth/login 获 Token → ConnectHub?access_token=xxx → EnterLobby → 创建/加入房间 → 全员 Ready 后服务端 StartMatch → 广播 GameStarted + 首帧 FlowStep。
3. GameCore 共享逻辑层GameCore 即 Assets/Scripts 下无 UnityEngine 依赖(或 #if HEADLESS 隔离)的纯 C# 逻辑,服务端项目通过 csproj 链接同目录源码编译。
3.1 GameSession:对局会话聚合根GameSession 持有单局全部可变状态与模块引用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class GameSession { public Random Random { get ; private set ; } public GameEventBus EventBus { get ; } = new (); public GameStateStack StateStack { get ; } = new (); public SkillExecutor SkillExecutor { get ; } = new (); public ItemService ItemService { get ; private set ; } public ChamberState Chamber { get ; } = new (); public TurnPhase CurrentPhase { get ; private set ; } public string TurnOwnerId { get ; private set ; } public FlowController Flow { get ; private set ; } public IDebugHost Debug { get ; } public GameSession (IDebugHost debug ) { Debug = debug; ItemService = new ItemService(SkillExecutor, Random); AI = new AIDecisionMaker(this ); Flow = new FlowController(this ); } }
SetupGame(seats, seed) 初始化流程:
注入 Random(seed)(联机可复现)或本地随机。 清空事件总线、状态栈、技能注册。 按 SeatConfig 创建 PlayerEntity(座次、职业、ControlType、开局生命)。 注册全局被动(狂暴增伤、护身符格挡等)。 枪膛 Chamber.LoadRandom 装入实弹/空弹序列。 全员发放开局道具,批次写入 _startingDealBatches 供表现层发牌动画。 回合指针 _currentTurnIndex 按座次 0→1→2→3→0 顺时针流转;AdvanceTurn 跳过已淘汰玩家;仅剩一人存活时 SetGameOver。
3.2 核心枚举TurnPhase(五阶段) :
1 TurnStart (0 ) → MainAction (1 ) → Judgment (2 ) → Discard (3 ) → TurnEnd (4 )
FlowStateType(状态栈条目类型) :
值 含义 TurnPhase 常规阶段(栈调试描述用) ShootResponse 开枪响应窗口 DyingResponse 濒死自救 TraitorChoice 血量 1 点转职抉择
MainActionType :Shoot / UseItem / EndAction。
ControlType :Human / AI(断线时人类可切 AI 托管)。
3.3 GameStateStack:分层状态栈1 2 3 4 5 6 7 8 public class GameStateStack { private readonly Stack<FlowStackEntry> _stack = new (); public FlowStackEntry Peek () => _stack.Count > 0 ? _stack.Peek() : null ; public void Push (FlowStackEntry entry ) => _stack.Push(entry); public FlowStackEntry Pop () => _stack.Count > 0 ? _stack.Pop() : null ; public int Count => _stack.Count; }
规则:栈顶为当前唯一活跃流程 ;下层挂起保留协程断点;临时流程结算后 Pop 恢复。支持开枪响应内再嵌套濒死等多层场景。
3.4 事件总线与技能GameEventBus 发布 GameEventType(TurnStart、ShootDeclared、DamageApplied、PlayerDying、ItemAfterUse 等)。角色技能与道具技能统一注册为 SkillExecutor 实例,被动技能订阅事件、主动技能由流程层在决策后调用 ExecuteActive。
背叛者转职 :玩家血量 1 且未抉择时进入 TraitorChoice 压栈;接受则 ConvertToTraitor 注销全部角色技能,仅保留开枪/道具基础权限。
3.5 PresentationCueRelay:权威表现指令流程结算点不直接操作 HUD,而是构造 PresentationCueSpec 交给 IDebugHost.QueuePresentationCue:
Cue 类型 触发场景 Shoot 开枪结算(含 bullet_type = FinalBullet) ItemFly / StealHand / EjectDrink 道具使用 HealthDelta / HitImpact 血量变化 DealCard 发牌(回合开始或被动奖励) MagicianTrajectory 魔术师弹道翻转 HideItemSlot 道具消耗前隐藏栏位(供 ItemFly 捕获起点)
CanEmit 条件:session.IsNormalPlayMode && session.Debug != null。Debug 面板模式跳过表现 cue,加速测试。
4. 流程控制模块详解 4.1 模块定位FlowController 是游戏顶层调度中枢:负责回合流转、五阶段推进、全局状态栈、事件广播、响应链轮询。对标三国杀式「主流程 + 插入响应窗口」机制。
4.2 对局主循环1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public IEnumerator RunGameCoroutine (){ yield return PresentStartingDealsIfAny () ; while (!_session.IsGameOver) { var player = _session.GetCurrentTurnPlayer(); if (player == null ) { _session.SetGameOver(); break ; } _session.SetTurnOwner(player); player.ShootLockedThisTurn = false ; yield return RunTurnCoroutine (player ) ; if (_session.IsGameOver) break ; _session.AdvanceTurn(); PushAuthoritativeHudSnapshot(FlowBoundaryKind.TurnChange); } }
开局 PresentStartingDealsIfAny:取出 _startingDealBatches,经 PresentationCueRelay.EmitDealCards 播发牌动画,WaitActionPresentation 等待结束后再刷新 HUD。
4.3 单回合五阶段1 2 3 4 5 6 7 8 public IEnumerator RunTurnCoroutine (PlayerEntity player ){ yield return RunTurnStartPhase (player ) ; yield return RunMainActionPhase (player ) ; yield return RunJudgmentPhase (player ) ; yield return RunDiscardPhase (player ) ; RunTurnEndPhase(player); }
各阶段要点:
阶段 行为 玩家响应 TurnStart 强化剂衰减、发回合道具(默认 2 件)、枪膛空则装弹、TurnStart 被动 无 MainAction 循环:开枪 / 用道具 / 结束行动 是(主行动决策) Judgment 濒死结算、血量 1 转职扫描 是(濒死/转职) Discard 道具超上限则循环弃牌 是(弃牌决策) TurnEnd TurnEnd 被动、清理临时标记 无
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 flowchart TD A[TurnStart 发道具装弹] --> B[MainAction 主行动循环] B --> C{操作类型} C -->|Shoot| D[压栈 ShootResponse] D --> E[顺时针响应轮询] E --> F[Pop 栈 + ResolveShoot] F --> G{目标濒死?} G -->|是| H[HandlePlayerDying 压栈] H --> B G -->|否| B C -->|UseItem| I[ExecuteUseItem 即时结算] I --> B C -->|EndAction| J[Judgment 裁决] J --> K[Discard 弃牌] K --> L[TurnEnd] L --> M[AdvanceTurn]
4.4 主行动决策循环1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 private IEnumerator RunMainActionPhase (PlayerEntity player ){ _session.Debug.NotifyPhase(_session, TurnPhase.MainAction, player); var ended = false ; var failedShootAttempts = 0 ; while (!ended && player.IsAlive) { PlayerDecision decision = null ; yield return _session.Debug.RequestMainActionDecision(player, _session, d => decision = d); if (decision?.MainAction == null ) continue ; switch (decision.MainAction) { case MainActionType.Shoot: yield return ExecuteShoot (player, decision.TargetPlayerId, ... ) ; yield return WaitActionPresentation () ; break ; case MainActionType.UseItem: yield return ExecuteUseItem (player, decision, ... ) ; yield return WaitActionPresentation () ; break ; case MainActionType.EndAction: ended = true ; break ; } } }
连续无法开枪达到上限(MaxFailedShootAttemptsBeforeAutoEnd)时自动结束主行动。
4.5 开枪完整流程1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private IEnumerator ExecuteShoot (PlayerEntity shooter, string targetId, Action<bool > onExecuted ){ // 校验锁枪、目标、Peek 初始子弹 var ctx = new ShootContext { ShooterId, TargetId, InitialBullet, FinalBullet }; _session.StateStack.Push(new FlowStackEntry { StateType = FlowStateType.ShootResponse, Description = "开枪响应" , Payload = ctx }); _session.Publish(GameEventType.ShootDeclared); yield return RunShootResponseWindow (shooter, ctx ) ; _session.StateStack.Pop(); yield return ResolveShoot (shooter, target, ctx ) ; }
响应轮询规则 (RunShootResponseWindow):
起点:当前回合玩家的顺时针下家。 依次询问存活玩家;仅 魔术师且道具数 ≥ MagicianResponseMinItems 时轮询(ShouldPollShootResponse)。 发动「弹道」主动技能则翻转 ctx.FinalBullet,播 MagicianTrajectory cue。 结算规则 (ResolveShoot):
Chamber.PopNext 出弹;实弹则 DamageAboutToApply 事件链 → 可能护身符格挡 → EnhancementItemService.ApplyDamage。锁枪规则:自射空弹不锁;对他人开枪或任意实弹均锁枪(ShootLockedThisTurn = true)。 实弹击杀:背叛者累计 TraitorKills;目标 Health≤0 进入 HandlePlayerDying。 结算后 PresentationCueRelay.EmitShootResolve(bullet 用 FinalBullet ,非初始 Peek 值)。 4.6 濒死与转职濒死 (HandlePlayerDying):压栈 DyingResponse → RequestDyingDecision → 可选奶茶/道具自救 → 仍 ≤0 则清空道具并 Eliminate。
转职 (HandleTraitorChoice):压栈 TraitorChoice → RequestTraitorDecision → 接受则 ConvertToTraitor。
主行动中 ExecuteUseItem 后可调用 ProcessImmediateDying 即时处理濒死,不等到 Judgment 阶段。
4.7 弃牌阶段RunDiscardPhase:当 player.ItemCount > player.GetItemCap() 时循环 RequestDiscardDecision,移除选中道具并记录战报。
4.8 PushAuthoritativeHudSnapshot 条件编译1 2 3 4 5 6 private void PushAuthoritativeHudSnapshot (FlowBoundaryKind boundary = FlowBoundaryKind.ActionSettle ){ #if HEADLESS _session.Debug.PushAuthoritativeSnapshot(_session, boundary); #endif }
单机 Unity 编译时不调用(本地 HUD 由 DebugService.RefreshHud 驱动);服务端 HEADLESS 编译时每次行动/阶段/回合边界推送 FlowStep。
4.9 IDebugHost 接口契约流程层通过以下方法与宿主交互:
方法 用途 RequestMainActionDecision主行动:开枪/道具/结束 RequestShootResponseDecision开枪响应(魔术师弹道) RequestDyingDecision濒死自救 RequestTraitorDecision转职抉择 RequestDiscardDecision超限弃牌 WaitActionPresentation等待表现动画结束 QueuePresentationCue入队表现 spec PushAuthoritativeSnapshot联机推送权威快照(服务端) LogEvent战报文本 NotifyPhase / RefreshHud阶段与 HUD 元数据
5. 客户端单机实现 5.1 启动链路1 2 3 4 5 6 7 8 // GameEntry.Start Session = new GameSession(new DebugService()); // GameEntry.RunMatchCoroutine Session.IsNormalPlayMode = LastPlayMode != PlayModeKind.Debug; Session.HideOpponentHandItems = LastPlayMode == PlayModeKind.Master; Session.SetupGame(seats, null ); yield return Session.Flow.RunGameCoroutine();
DebugService 实现 IDebugHost:承担 IMGUI Debug 面板或 NormalHud 的决策 UI、本地 cue 协程队列、战报日志。
5.2 决策路径DebugService.RequestDecision 分支:
AI 或托管 (AiDelegation.ShouldAutoDecide)→ ExecuteAiDecision → AIDecisionMaker 立即返回。人类 → WaitHumanDecision:设置 _waitingHuman = true,UI 展示选项;玩家调用 SubmitHumanDecision(decision) 后协程继续。Debug 模式支持控制台指令(shoot player2 | use item1 | end);正常模式由 NormalGameUIInputView 渲染按钮与道具栏。
5.3 表现播放DebugService.QueuePresentationCue:
PresentationCuePlayback.TryBuildFromSpec 将 spec 转为 Unity 协程。入队 _presentationCueQueue。 WaitActionPresentation 顺序 Dequeue 并 yield return 每个 cue 协程。播放完毕后 FlushPendingHealthDeltas 等收尾。 单机无 revision 概念;流程 yield 等待本地动画自然结束后再开下一决策窗。
1 2 3 4 5 6 7 8 9 flowchart LR FC[FlowController] --> PCR[PresentationCueRelay] FC -->|Request*Decision| DS[DebugService] PCR -->|QueuePresentationCue| DS DS --> PCP[PresentationCuePlayback] PCP --> FX[NormalHudPresentationFx] DS --> HUD[NormalGameUIInputView] HUD -->|SubmitHumanDecision| DS DS --> FC
5.4 玩法模式差异PlayModeKind 表现 手牌可见性 Simple NormalHud,无 Debug 面板 他人手牌可见 Master NormalHud HideOpponentHandItems=true,他人道具栏显示卡背Debug 完整 Debug IMGUI + 可选 AI 托管 全信息
6. 服务端权威实现 6.1 对局启动:GameLoopService1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public void StartMatch (RoomModel room, GameStarted started ){ var debugHost = new ServerDebugHost(); var session = new GameSession(debugHost) { IsNormalPlayMode = true , HideOpponentHandItems = room.PlayMode == PlayMode.Master }; var seatConfigs = RoomMatchSeatBuilder.Build(room, _connections, _matchOptions); session.SetupGame(seatConfigs, started.Seed); var match = new MatchInstance { MatchId, RoomId, Session, DebugHost, Seed, ... }; var publisher = new FlowPublisher(match, buildSnapshot, BroadcastFlowStepAsync); match.FlowPublisher = publisher; debugHost.FlowPublisher = publisher; _matches.Add(match); _ = RunMatchAsync(match); }
RunMatchAsync:
1 2 3 4 5 6 await match.FlowPublisher.PublishMatchStartAsync();await CoroutineDriver.RunAsync( match.Session.Flow.RunGameCoroutine(), onIdle: null , match.Cancellation.Token); await FinishMatchAsync(match); // 排行榜计分 + GameOver 广播
6.2 MatchInstance 与 MatchStoreMatchInstance 持有:GameSession、ServerDebugHost、FlowPublisher、RoomModel、Seed、CancellationTokenSource。
PresentationSync 锁 + PresentationPipeline Task 链:串行化 FlowStep 广播 ,避免并发 Commit 乱序。
MatchStore 维护 MatchId / PlayerId / RoomId 三向索引,供 Hub 路由决策与断线处理。
6.3 ServerDebugHost:决策与推送AI 即时决策 :
1 2 3 4 5 6 7 8 if (player.Control == ControlType.AI || AiDelegation.ShouldAutoDecide(player)){ var ai = GameDebugAiResolver.Resolve(session, player, kind, shoot); if (session.IsNormalPlayMode) yield return CoroutineDelay.Seconds(AiThinkPauseSeconds); // 0.8s onComplete(ai); yield break ; }
人类等待 WebSocket :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 BeginWait(player, session, kind, shoot); // 设置 PendingDecision _ = FlowPublisher?.PublishDecisionOpenAsync(); while (_waiting){ if (超时 DecisionTimeoutMs) // 30s { _resolvedDecision = GameDebugAiResolver.Resolve(...); _ = FlowPublisher?.PublishDecisionCloseAsync(); break ; } yield return null ; } onComplete(_resolvedDecision);
TrySubmitDecision (Hub 调用链:GameLoopService.TrySubmitDecision):
校验 PendingDecision.Scene 与 payload 一致。 校验 player_id 为当前等待者。 主行动用道具时服务端预校验 MainActionRules.CanUseItem。 写入 _resolvedDecision,_waiting = false,PublishDecisionCloseAsync。 表现等待 :
1 2 3 4 5 6 public IEnumerator WaitActionPresentation (GameSession session ){ var holdMs = FlowPublisher.LastPresentationHoldMs; if (holdMs <= 0 ) yield break ; yield return CoroutineDelay.Seconds(holdMs / 1000f ); }
PushAuthoritativeSnapshot 映射 boundary 到 Publish 方法:
FlowBoundaryKind Publish 方法 ActionSettle PublishActionSettleAsync(含 hold) TurnChange PublishTurnChangeAsync PhaseChange PublishPhaseChangeAsync DecisionOpen PublishDecisionOpenAsync DecisionClose PublishDecisionCloseAsync MatchStart PublishMatchStartAsync
6.4 FlowPublisher:revision 合并广播核心约束:同一逻辑步内,先 BufferCue / BufferEvent,再 Publish,保证 cue 与 snapshot 同一 revision 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private Task PublishAsync (ProtoFlowBoundary boundary, bool includeHold ){ _revision++; var holdMs = includeHold ? PresentationHoldCalculator.ComputeMs(_bufferedCueSpecs) : 0 ; LastPresentationHoldMs = holdMs; var step = new FlowStep { Revision = _revision, Boundary = boundary, PresentationHoldMs = holdMs }; // 填充 cues(带 flow_revision、cue_index) // 填充 events(战报 GameEventLine) // 填充 snapshot(GameProtoMapper.ToSnapshot,per-viewer 大师模式掩码) _bufferedCueSpecs.Clear(); _bufferedEvents.Clear(); // PresentationPipeline 串行 await _broadcast return _match.PresentationPipeline; }
BuildResyncStep(viewerPlayerId):断线重连用,不递增 revision ,boundary = MATCH_RESYNC。
大师模式(Master):BroadcastFlowStepPerViewerAsync 对每个真实玩家单独克隆 Step,掩码他人手牌事件文本与 cue,重建 per-viewer snapshot。
6.5 CoroutineDriver:无 Unity 协程运行时1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static async Task RunAsync (IEnumerator root, Func<Task>? onIdle, CancellationToken ct ){ var stack = new Stack<IEnumerator>(); stack.Push(root); while (stack.Count > 0 ) { if (current.Current is IEnumerator nested) { stack.Push(nested); continue ; } if (current.Current is CoroutineDelay delay) { await Task.Delay(delay.Milliseconds, ct); continue ; } await Task.Delay(100 , ct); // yield return null → 100ms,供 WS 决策与超时检测 } }
6.6 断线、重连、超时事件 服务端行为 人类断线 HandleDisconnect:entity.Control = AI,广播最新 snapshot重连 ResyncPlayerAsync:恢复 Human 控制,单播 MatchResynced(含完整 FlowStep + RoomUpdated)决策 30s 超时 AI 代打,LogEvent [AI] 决策超时,PublishDecisionClose 对局异常 不广播 GameOver(避免多人存活误弹结算),Remove match,房间回 Waiting
6.7 PresentationHoldCalculator(双端共用)Cue 类型 估算时长 Shoot 2800ms ItemFly 1200ms StealHand 1600ms EjectDrink 1400ms HealthDelta / HitImpact 1150ms MagicianTrajectory 2000ms DealCard 800ms × 张数 HideItemSlot 50ms 合计上下限 400ms ~ 6000ms
7. 联机 FlowStep 协议与同步模型 7.1 设计优先级优先级 层级 职责 1 流程流转 回合/阶段/决策权何时转移;服务端 gate 下一决策 2 状态同步 血量、道具、枪膛、PendingDecision 等权威 HUD 数据 3 特效推送 开枪、道具飞行、回血/掉血飘字等纯表现
核心原则:同一逻辑步使用同一个 revision,cue 与 snapshot 捆绑下发 ,客户端按序处理。
7.2 FlowStep 消息结构1 2 3 4 5 6 7 8 message FlowStep { int64 revision = 1 ; FlowBoundaryKind boundary = 2 ; GameStateSnapshot snapshot = 3 ; repeated PresentationCue cues = 4 ; repeated GameEventLine events = 5 ; int32 presentation_hold_ms = 6 ; }
GameStateSnapshot 主要字段:
字段 含义 match_id / turn_owner_id / phase 对局与回合上下文 flow_state 状态栈顶描述(调试/Intel) chamber 枪膛剩余、已知下一发、已消耗序列 players[] 各玩家血量、道具栏、ControlType、shoot_locked 等 pending_decision 当前等待决策的玩家、scene、timeout、wait_started_at revision / boundary 与 FlowStep 对齐
PlayerDecisionPayload 上行字段:scene、main_action、target_player_id、item_instance_id、use_shoot_response_skill、use_dying_save、accept_traitor_conversion、items_to_discard 等。
Hub 消息种类:HUB_MESSAGE_KIND_FLOW_STEP = 111;重连 MatchResynced 含完整 FlowStep state(boundary = MATCH_RESYNC)。
旧版分通道 game_state_snapshot / presentation_cue / game_event 仍保留解析,FlowStep 启用后对局内应忽略(OnlineSession 丢弃 stale legacy 消息)。
7.3 FlowBoundaryKind 与客户端行为boundary 含义 客户端行为 ACTION_SETTLE 一次行动结算完成 播 cues → 应用 snapshot;服务端按 hold 等待 TURN_CHANGE 回合切换 Abort 积压特效 → 全量刷新 HUD PHASE_CHANGE 阶段切换 同上 DECISION_OPEN 打开决策窗 应用 snapshot + 展示决策 UI;不 abort 特效 DECISION_CLOSE 提交/关闭决策 关闭决策 UI;等后续 ACTION_SETTLE MATCH_START 对局开始 Abort + 初始化 HUD MATCH_RESYNC 断线重连 Abort + 直接应用 resync snapshot
7.4 服务端推送流水线1 2 3 4 5 FlowController 结算点 ├─ PresentationCueRelay.Emit* → ServerDebugHost.QueuePresentationCue → FlowPublisher.BufferCue ├─ LogEvent → FlowPublisher.BufferEvent └─ PushAuthoritativeSnapshot → FlowPublisher.Publish*Async └─ 合并 → FlowStep → SignalR 广播
决策推送:人类进入等待发 DECISION_OPEN(含 pending_decision);提交或超时发 DECISION_CLOSE。不再 每 300ms 轮询推全量快照。
7.5 联机对局配置(OnlineMatchOptions)配置 默认 说明 AiFillMax 3 AllowAiFill 开局最多补位 AI 数 AiStartingHealth 2 AI 开局生命(人类仍为 3)
固定四角座位(seatIndex 0–3),UI 旋转使本机永远在左上;空座隐藏即可。
8. 客户端联机消费层联机客户端不运行 FlowController ,职责是:按 revision 顺序接收 FlowStep → 更新权威态 → 播放 cue → 提交显示投影 → 在 DECISION_OPEN 时展示决策 UI → 上行 SubmitDecision。
8.1 组件链路1 2 3 4 5 6 7 8 9 10 11 12 13 OnlineSession(Hub 消息入口) └─ OnlineHudCoordinator(统一协调) ├─ OnlineFlowOrchestrator(revision 排序 + Presenting 门禁) ├─ OnlineHudState(权威态 vs 显示投影) └─ NormalGameUIInputView.DispatchOnlineFlowStep ├─ 硬边界? → AbortQueuedPresentation + 清 gate ├─ BeginFlowStep + EnqueueCue(HideItemSlot 前捕获飞行起点) ├─ ApplyOnlineSnapshotData(写权威态) ├─ DecisionOpen/Close → 立即刷新决策 UI ├─ events → OnlineBattleLogStore 战报 └─ 表现 idle → CommitDisplay → NotifyPresentationIdle └─ OnlineMatchPresentationPlayer(cue 队列 + hold 计时) └─ PresentationCuePlayback → NormalHudPresentationFx
8.2 OnlineFlowOrchestratorEnqueue(step, dispatch):revision ≤ lastApplied 丢弃;乱序入 SortedList 缓冲。dispatch 返回 true → 设置 _presentationGated,暂停后续派发。NotifyPresentationIdle → 清 gate,继续 TryDispatchSequential。ApplyResync / FastForwardToRevision:重连或追帧用。 8.3 OnlineHudState:双态模型状态 变量 用途 权威态 _authoritative + _authoritativePlayers决策合法性、Intel、PendingDecision 显示投影 _display实际渲染 HUD 的快照 待提交 _pendingFullCommit表现 busy 时排队,idle 后 CommitDisplay
表现播放中:权威态立即更新,显示投影延后 ,避免动画未播完道具栏已刷新导致穿帮。
OnlineHudCoordinator.ShouldDeferHudCommit 统一判定:决策边界不延后;硬边界不延后;表现 busy / orchestrator gated / 有待提交时延后。
8.4 客户端消费状态机状态 行为 Idle Orchestrator 可派发下一 revision Presenting 播放 cue 和/或 hold 计时;门禁后续 Step;权威态更新,显示排队 Applying 表现 idle → CommitDisplay → NotifyPresentationIdle
硬边界(TurnChange / PhaseChange / MatchStart / MatchResync)调用 AbortQueuedPresentation,清 gate,立即全量刷新。
8.5 DispatchOnlineFlowStep 处理顺序判定 ShouldAbortPresentation → abort + 清 pending commit。 BeginFlowStep:HideItemSlot cue 前 TryCaptureItemFlyStart。Cue 入队 OnlineMatchPresentationPlayer。 SetAuthoritative(snapshot) 写权威态。DecisionOpen/Close 边界立即刷新决策 UI(不能等表现 idle,否则「提交中」状态卡住)。 events 追加战报。 按规则 QueueFullCommit 或立即 CommitDisplay。 8.6 道具栏与 ItemFly 策略快照 players[].items 为权威数量。 处理顺序:先 cue 入队,再写权威态 ;表现 busy 时不重建道具栏 DOM。 _onlinePresentationHiddenItems 仅本机 optimistic hide;观察他人以 snapshot 为准。发牌 pendingReveal 有 12s fallback(TryFinalizeOpeningDealPresentation)。 8.7 开枪特效规则权威子弹类型:PresentationCue.bullet_type = ctx.FinalBullet(魔术师翻转后的最终子弹)。 同一 FlowStep 内 Shoot cue 入队成功后,禁止用战报 regex 二次播放。 实弹 + 护身符:Shoot 仍播实弹动画,另播护身符反馈 cue,不播 HealthDelta。 8.8 重连 MatchResynced1 2 3 4 Hub 收到 MatchResynced → QueueOnlineResync(EnterOnlineMatch 前暂存) → PrepareOnlineResync:Abort 表现 + BattleLog.ReplaceFromFlowStep + ApplyResync → EnterOnlineMatch 时若有 pending:从 state .events 重建战报
服务端 ResyncPlayerAsync 同时恢复 ControlType.Human 并 PublishSnapshotAsync 广播给他人。
8.9 双通道兼容通道 入口 FlowStep 启用后 FlowStep FlowStepReceived 主路径 legacy snapshot SnapshotReceived 表现 busy 时 partial;校验 IsStaleLegacySnapshot legacy cue/event PresentationCueReceived OnlineSession 丢弃,防重复
OnlineSession.UsesFlowStepChannel:收到首帧 FlowStep 后切换,丢弃 stale legacy。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 sequenceDiagram participant FC as FlowController participant SDH as ServerDebugHost participant FP as FlowPublisher participant Hub as SignalR participant OS as OnlineSession participant OHC as OnlineHudCoordinator participant HUD as NormalGameUIInputView FC->>SDH: RequestMainActionDecision SDH->>FP: PublishDecisionOpen FP->>Hub: FlowStep rev=N boundary=DECISION_OPEN Hub->>OS: FlowStepReceived OS->>OHC: EnqueueFlowStep OHC->>HUD: 展示决策 UI HUD->>Hub: SubmitDecision Hub->>SDH: TrySubmitDecision SDH->>FP: PublishDecisionClose FC->>FC: ExecuteShoot FC->>SDH: QueuePresentationCue + PushSnapshot SDH->>FP: PublishActionSettle hold=2800ms FP->>Hub: FlowStep rev=N+1 cues+snapshot Hub->>OHC: EnqueueFlowStep OHC->>HUD: 播放开枪 + 应用权威态 FC->>SDH: WaitActionPresentation 2.8s FC->>SDH: RequestMainActionDecision
9. 双端对照与架构模式总结 9.1 实现对照表维度 客户端单机 服务端联机 客户端联机 流程引擎 本地 FlowController 同一份 FlowController 不运行 宿主 DebugService ServerDebugHost OnlineHudCoordinator 决策 IMGUI/NormalHud WS SubmitDecision 同左,经 Hub 上行 AI AIDecisionMaker GameDebugAiResolver 无(服务端算) 表现 本地 cue 协程 cue 进 FlowStep OnlineMatchPresentationPlayer 快照 RefreshHud 本地 FlowPublisher → proto ApplyOnlineSnapshotData 随机数 本地 Random seed 注入 不使用本地 Random 协程 Unity StartCoroutine CoroutineDriver Unity 仅播表现
9.2 三种架构模式组合分层状态机 + 状态栈 :主回合五阶段 + 开枪/濒死/转职插入流程。协程脚本化工作流 :yield return Request*Decision 表达异步人机交互;服务端 CoroutineDriver 移植。权威服务器 + 捆绑同步 :逻辑步 = FlowStep(revision, boundary, snapshot, cues, events, hold)。 9.3 联机客户端不跑 FlowController 的原因防作弊:Random、伤害、道具必须以服务端为准。 单点真相:避免客户端先算再上报的冲突。 表现可慢不可错:动画可延长,revision 严格单调。 10. 扩展接入与调试验证 10.1 新增动作接入步骤流程层(GameCore) :在 FlowController 结算点调用 PresentationCueRelay.Emit*;新决策场景扩展 IDebugHost.Request*Decision。表现时长 :PresentationCueKind + PresentationHoldCalculator.EstimateCueMs。协议 :proto PresentationCueKind + PresentationCueMapper.ToProto。Unity 表现 :PresentationCuePlayback.TryBuildFromSpec 新分支。不要 单独广播 snapshot/cue/event;统一 PushAuthoritativeSnapshot 一次 Commit。 10.2 调试要点观察 FlowStep.revision 是否严格单调递增。 特效丢失:检查 cue 是否为空、TryBuildFromSpec 是否 false。 道具数量错乱:检查 snapshot.items 与是否误用 hidden 集合过滤他人。 服务端:确认 BufferCue 在 Publish 之前完成。 10.3 自动化测试客户端 FlowSync 单元测试(无需启动服务端):
1 2 cd server/tools/ProtoSmokeTestdotnet run -- --flow-sync-unit
覆盖:OnlineFlowOrchestrator 排序/门禁、OnlineHudState 显示延后、OnlineFlowSync 规则。
联机对局时序冒烟(需 API + 自动对局):
1 2 cd server/tools/ProtoSmokeTestdotnet run -- --match-flow --minutes 8 http://127.0.0.1:8080 用户名 密码
校验:revision 连续、bundle 内 snapshot/cue revision 对齐、ActionSettle 含 cue 时 hold>0、无 legacy 分通道消息。
Proto 代码生成:
1 cd server && ./scripts/generate-proto.sh
11. 源码文件索引 GameCore(Assets/Scripts,服务端链接编译)1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Flow/FlowController.cs 流程主控:五阶段、响应链、压栈 Flow/GameSession.cs 对局会话聚合 Flow/PresentationCueRelay.cs 权威表现 cue 发射 Flow/PresentationHoldCalculator.cs 表现时长估算 Flow/FlowBoundaryKind.cs 边界枚举(与 proto 对齐) Core/GameStateStack.cs 分层状态栈 Core/GameEnums.cs TurnPhase, FlowStateType, MainActionType Core/GameEventBus.cs 全局事件总线 Debug/IDebugHost.cs 双端宿主接口 Debug/DebugService.cs Unity 单机宿主 Character/PlayerEntity.cs 玩家实体 Item/ItemService.cs 道具服务 Skill/SkillExecutor.cs 技能执行 AI/AIDecisionMaker.cs 单机 AI 决策 Game/GameEntry.cs 单机/联机入口
服务端(server/src/NewRingGame.Server)1 2 3 4 5 6 7 8 9 Services/Game/GameLoopService.cs 对局启动、决策路由、断线重连 Services/Game/ServerDebugHost.cs IDebugHost 服务端实现 Services/Game/FlowPublisher.cs FlowStep 合并广播 Services/Game/CoroutineDriver.cs 无 Unity 协程驱动 Services/Game/GameProtoMapper.cs GameSession → proto snapshot Services/Game/GameDecisionMapper.cs proto ↔ PlayerDecision Services/Game/MatchModels.cs MatchInstance、MatchStore Services/Game/MasterModePrivacy.cs 大师模式掩码 Hubs/GameHub.cs SignalR 入口
协议1 2 3 server/proto/v1/game.proto FlowStep、GameStateSnapshot、PlayerDecisionPayload server/proto/v1/enums.proto FlowBoundaryKind、HubMessageKind server/proto/v1/room.proto 房间与座位
联机客户端(Assets/Scripts/Network + UI)1 2 3 4 5 6 7 8 9 10 11 12 Network/OnlineSession.cs Hub 连接、消息分发 Network/OnlineHudCoordinator.cs 协调 Orchestrator + HudState Network/OnlineFlowOrchestrator.cs revision 排序与门禁 Network/OnlineHudState.cs 权威态/显示投影双态 Network/OnlineFlowSync.cs 边界判定与延后规则 Network/OnlineFlowChannel.cs FlowStep/legacy 通道切换 Network/OnlineBattleLogStore.cs 战报存储 Network/GameHubClient.cs SignalR 客户端 UI/NormalGameUIInputView.Online .cs 联机 FlowStep 派发、决策 UI UI/OnlineMatchPresentationPlayer.cs cue 队列与 hold UI/PresentationCuePlayback.cs cue spec → 动画协程 UI/NormalHudPresentationFx.cs HUD 特效实现
文档版本:GameCore 共用 FlowController + FlowStep 联机方案。归档日期:2026-06。