





















归档说明:本文档概括当前项目的整体实现细节,涵盖游戏架构分层、流程控制原理、客户端单机实现、服务端权威实现、联机 FlowStep 同步协议及客户端消费层。内容自洽,可直接作为开发存档阅读,于2026.6.12日留存于博客备份。
《恶魔轮盘》是一款四人回合制卡牌博弈游戏,支持 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 | flowchart TB |
| 模块 | 职责 | 禁止事项 |
|---|---|---|
| FlowController | 回合流转、五阶段状态机、响应链轮询、状态栈压栈/出栈 | 不直接画 UI、不读 WebSocket |
| GameSession | 聚合玩家列表、枪膛、事件总线、技能执行器、回合指针 | 不决定 UI 如何展示 |
| IDebugHost | 决策输入、表现 cue 队列、快照推送(双端差异插槽) | 不修改游戏规则 |
| SkillExecutor / ItemService | 响应事件、执行主动/被动技能、道具 CRUD | 不推进回合阶段 |
| AIDecisionMaker / GameDebugAiResolver | 输出 PlayerDecision |
不直接改实体状态 |
| FlowPublisher(仅服务端) | 合并 cue/战报/快照为 FlowStep 广播 |
不含游戏规则 |
| OnlineHudCoordinator(仅联机客户端) | revision 排序、表现门禁、权威态/显示投影 | 不跑 FlowController |
1 | NewRingGame.Server |
内存热数据(进程内字典,重启丢失):Session Token、ConnectionId→PlayerId、房间实例、进行中对局 MatchInstance(含 GameSession + seed)。MySQL 仅存账号与排行榜等冷数据。
GameEntry 通过三个布尔标志区分状态:
| 模式 | IsMatchRunning |
IsOnlineMatch |
流程引擎位置 |
|---|---|---|---|
| 单机正常/Debug | true | false | 本地 Session.Flow.RunGameCoroutine() |
| 联机对局 | true | true | 不跑本地 FlowController,只消费 FlowStep |
| 大厅/结算 | false | false | 无 |
联机入口 BeginOnlineMatch 设置 IsOnlineMatch = true,由 NormalGameUIInputView.EnterOnlineMatch 初始化 HUD 与 OnlineHudCoordinator,等待服务端推送。
FlowStep 下行、SubmitDecision 上行。连接流程:客户端 POST /api/auth/login 获 Token → ConnectHub?access_token=xxx → EnterLobby → 创建/加入房间 → 全员 Ready 后服务端 StartMatch → 广播 GameStarted + 首帧 FlowStep。
GameCore 即 Assets/Scripts 下无 UnityEngine 依赖(或 #if HEADLESS 隔离)的纯 C# 逻辑,服务端项目通过 csproj 链接同目录源码编译。
GameSession 持有单局全部可变状态与模块引用:
1 | public class GameSession |
SetupGame(seats, seed) 初始化流程:
Random(seed)(联机可复现)或本地随机。SeatConfig 创建 PlayerEntity(座次、职业、ControlType、开局生命)。Chamber.LoadRandom 装入实弹/空弹序列。_startingDealBatches 供表现层发牌动画。回合指针 _currentTurnIndex 按座次 0→1→2→3→0 顺时针流转;AdvanceTurn 跳过已淘汰玩家;仅剩一人存活时 SetGameOver。
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 托管)。
1 | public class GameStateStack |
规则:栈顶为当前唯一活跃流程;下层挂起保留协程断点;临时流程结算后 Pop 恢复。支持开枪响应内再嵌套濒死等多层场景。
GameEventBus 发布 GameEventType(TurnStart、ShootDeclared、DamageApplied、PlayerDying、ItemAfterUse 等)。角色技能与道具技能统一注册为 SkillExecutor 实例,被动技能订阅事件、主动技能由流程层在决策后调用 ExecuteActive。
背叛者转职:玩家血量 1 且未抉择时进入 TraitorChoice 压栈;接受则 ConvertToTraitor 注销全部角色技能,仅保留开枪/道具基础权限。
流程结算点不直接操作 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,加速测试。
FlowController 是游戏顶层调度中枢:负责回合流转、五阶段推进、全局状态栈、事件广播、响应链轮询。对标三国杀式「主流程 + 插入响应窗口」机制。
1 | public IEnumerator RunGameCoroutine() |
开局 PresentStartingDealsIfAny:取出 _startingDealBatches,经 PresentationCueRelay.EmitDealCards 播发牌动画,WaitActionPresentation 等待结束后再刷新 HUD。
1 | public IEnumerator RunTurnCoroutine(PlayerEntity player) |
各阶段要点:
| 阶段 | 行为 | 玩家响应 |
|---|---|---|
| TurnStart | 强化剂衰减、发回合道具(默认 2 件)、枪膛空则装弹、TurnStart 被动 | 无 |
| MainAction | 循环:开枪 / 用道具 / 结束行动 | 是(主行动决策) |
| Judgment | 濒死结算、血量 1 转职扫描 | 是(濒死/转职) |
| Discard | 道具超上限则循环弃牌 | 是(弃牌决策) |
| TurnEnd | TurnEnd 被动、清理临时标记 | 无 |
1 | flowchart TD |
1 | private IEnumerator RunMainActionPhase(PlayerEntity player) |
连续无法开枪达到上限(MaxFailedShootAttemptsBeforeAutoEnd)时自动结束主行动。
1 | private IEnumerator ExecuteShoot(PlayerEntity shooter, string targetId, Action<bool> onExecuted) |
响应轮询规则(RunShootResponseWindow):
ShouldPollShootResponse)。ctx.FinalBullet,播 MagicianTrajectory cue。结算规则(ResolveShoot):
Chamber.PopNext 出弹;实弹则 DamageAboutToApply 事件链 → 可能护身符格挡 → EnhancementItemService.ApplyDamage。ShootLockedThisTurn = true)。TraitorKills;目标 Health≤0 进入 HandlePlayerDying。PresentationCueRelay.EmitShootResolve(bullet 用 FinalBullet,非初始 Peek 值)。濒死(HandlePlayerDying):压栈 DyingResponse → RequestDyingDecision → 可选奶茶/道具自救 → 仍 ≤0 则清空道具并 Eliminate。
转职(HandleTraitorChoice):压栈 TraitorChoice → RequestTraitorDecision → 接受则 ConvertToTraitor。
主行动中 ExecuteUseItem 后可调用 ProcessImmediateDying 即时处理濒死,不等到 Judgment 阶段。
RunDiscardPhase:当 player.ItemCount > player.GetItemCap() 时循环 RequestDiscardDecision,移除选中道具并记录战报。
1 | private void PushAuthoritativeHudSnapshot(FlowBoundaryKind boundary = FlowBoundaryKind.ActionSettle) |
单机 Unity 编译时不调用(本地 HUD 由 DebugService.RefreshHud 驱动);服务端 HEADLESS 编译时每次行动/阶段/回合边界推送 FlowStep。
流程层通过以下方法与宿主交互:
| 方法 | 用途 |
|---|---|
RequestMainActionDecision |
主行动:开枪/道具/结束 |
RequestShootResponseDecision |
开枪响应(魔术师弹道) |
RequestDyingDecision |
濒死自救 |
RequestTraitorDecision |
转职抉择 |
RequestDiscardDecision |
超限弃牌 |
WaitActionPresentation |
等待表现动画结束 |
QueuePresentationCue |
入队表现 spec |
PushAuthoritativeSnapshot |
联机推送权威快照(服务端) |
LogEvent |
战报文本 |
NotifyPhase / RefreshHud |
阶段与 HUD 元数据 |
1 |
|
DebugService 实现 IDebugHost:承担 IMGUI Debug 面板或 NormalHud 的决策 UI、本地 cue 协程队列、战报日志。
DebugService.RequestDecision 分支:
AiDelegation.ShouldAutoDecide)→ ExecuteAiDecision → AIDecisionMaker 立即返回。WaitHumanDecision:设置 _waitingHuman = true,UI 展示选项;玩家调用 SubmitHumanDecision(decision) 后协程继续。Debug 模式支持控制台指令(shoot player2 | use item1 | end);正常模式由 NormalGameUIInputView 渲染按钮与道具栏。
DebugService.QueuePresentationCue:
PresentationCuePlayback.TryBuildFromSpec 将 spec 转为 Unity 协程。_presentationCueQueue。WaitActionPresentation 顺序 Dequeue 并 yield return 每个 cue 协程。FlushPendingHealthDeltas 等收尾。单机无 revision 概念;流程 yield 等待本地动画自然结束后再开下一决策窗。
1 | flowchart LR |
| PlayModeKind | 表现 | 手牌可见性 |
|---|---|---|
| Simple | NormalHud,无 Debug 面板 | 他人手牌可见 |
| Master | NormalHud | HideOpponentHandItems=true,他人道具栏显示卡背 |
| Debug | 完整 Debug IMGUI + 可选 AI 托管 | 全信息 |
1 | public void StartMatch(RoomModel room, GameStarted started) |
RunMatchAsync:
1 | await match.FlowPublisher.PublishMatchStartAsync(); |
MatchInstance 持有:GameSession、ServerDebugHost、FlowPublisher、RoomModel、Seed、CancellationTokenSource。
PresentationSync 锁 + PresentationPipeline Task 链:串行化 FlowStep 广播,避免并发 Commit 乱序。
MatchStore 维护 MatchId / PlayerId / RoomId 三向索引,供 Hub 路由决策与断线处理。
AI 即时决策:
1 | if (player.Control == ControlType.AI || AiDelegation.ShouldAutoDecide(player)) |
人类等待 WebSocket:
1 | BeginWait(player, session, kind, shoot); |
TrySubmitDecision(Hub 调用链:GameLoopService.TrySubmitDecision):
PendingDecision.Scene 与 payload 一致。player_id 为当前等待者。MainActionRules.CanUseItem。_resolvedDecision,_waiting = false,PublishDecisionCloseAsync。表现等待:
1 | public IEnumerator WaitActionPresentation(GameSession session) |
PushAuthoritativeSnapshot 映射 boundary 到 Publish 方法:
| FlowBoundaryKind | Publish 方法 |
|---|---|
| ActionSettle | PublishActionSettleAsync(含 hold) |
| TurnChange | PublishTurnChangeAsync |
| PhaseChange | PublishPhaseChangeAsync |
| DecisionOpen | PublishDecisionOpenAsync |
| DecisionClose | PublishDecisionCloseAsync |
| MatchStart | PublishMatchStartAsync |
核心约束:同一逻辑步内,先 BufferCue / BufferEvent,再 Publish,保证 cue 与 snapshot 同一 revision。
1 | private Task PublishAsync(ProtoFlowBoundary boundary, bool includeHold) |
BuildResyncStep(viewerPlayerId):断线重连用,不递增 revision,boundary = MATCH_RESYNC。
大师模式(Master):BroadcastFlowStepPerViewerAsync 对每个真实玩家单独克隆 Step,掩码他人手牌事件文本与 cue,重建 per-viewer snapshot。
1 | public static async Task RunAsync(IEnumerator root, Func<Task>? onIdle, CancellationToken ct) |
| 事件 | 服务端行为 |
|---|---|
| 人类断线 | HandleDisconnect:entity.Control = AI,广播最新 snapshot |
| 重连 | ResyncPlayerAsync:恢复 Human 控制,单播 MatchResynced(含完整 FlowStep + RoomUpdated) |
| 决策 30s 超时 | AI 代打,LogEvent [AI] 决策超时,PublishDecisionClose |
| 对局异常 | 不广播 GameOver(避免多人存活误弹结算),Remove match,房间回 Waiting |
| Cue 类型 | 估算时长 |
|---|---|
| Shoot | 2800ms |
| ItemFly | 1200ms |
| StealHand | 1600ms |
| EjectDrink | 1400ms |
| HealthDelta / HitImpact | 1150ms |
| MagicianTrajectory | 2000ms |
| DealCard | 800ms × 张数 |
| HideItemSlot | 50ms |
| 合计上下限 | 400ms ~ 6000ms |
| 优先级 | 层级 | 职责 |
|---|---|---|
| 1 | 流程流转 | 回合/阶段/决策权何时转移;服务端 gate 下一决策 |
| 2 | 状态同步 | 血量、道具、枪膛、PendingDecision 等权威 HUD 数据 |
| 3 | 特效推送 | 开枪、道具飞行、回血/掉血飘字等纯表现 |
核心原则:同一逻辑步使用同一个 revision,cue 与 snapshot 捆绑下发,客户端按序处理。
1 | message FlowStep { |
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 消息)。
| 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 |
1 | FlowController 结算点 |
决策推送:人类进入等待发 DECISION_OPEN(含 pending_decision);提交或超时发 DECISION_CLOSE。不再每 300ms 轮询推全量快照。
| 配置 | 默认 | 说明 |
|---|---|---|
| AiFillMax | 3 | AllowAiFill 开局最多补位 AI 数 |
| AiStartingHealth | 2 | AI 开局生命(人类仍为 3) |
固定四角座位(seatIndex 0–3),UI 旋转使本机永远在左上;空座隐藏即可。
联机客户端不运行 FlowController,职责是:按 revision 顺序接收 FlowStep → 更新权威态 → 播放 cue → 提交显示投影 → 在 DECISION_OPEN 时展示决策 UI → 上行 SubmitDecision。
1 | OnlineSession(Hub 消息入口) |
Enqueue(step, dispatch):revision ≤ lastApplied 丢弃;乱序入 SortedList 缓冲。dispatch 返回 true → 设置 _presentationGated,暂停后续派发。NotifyPresentationIdle → 清 gate,继续 TryDispatchSequential。ApplyResync / FastForwardToRevision:重连或追帧用。| 状态 | 变量 | 用途 |
|---|---|---|
| 权威态 | _authoritative + _authoritativePlayers |
决策合法性、Intel、PendingDecision |
| 显示投影 | _display |
实际渲染 HUD 的快照 |
| 待提交 | _pendingFullCommit |
表现 busy 时排队,idle 后 CommitDisplay |
表现播放中:权威态立即更新,显示投影延后,避免动画未播完道具栏已刷新导致穿帮。
OnlineHudCoordinator.ShouldDeferHudCommit 统一判定:决策边界不延后;硬边界不延后;表现 busy / orchestrator gated / 有待提交时延后。
| 状态 | 行为 |
|---|---|
| Idle | Orchestrator 可派发下一 revision |
| Presenting | 播放 cue 和/或 hold 计时;门禁后续 Step;权威态更新,显示排队 |
| Applying | 表现 idle → CommitDisplay → NotifyPresentationIdle |
硬边界(TurnChange / PhaseChange / MatchStart / MatchResync)调用 AbortQueuedPresentation,清 gate,立即全量刷新。
ShouldAbortPresentation → abort + 清 pending commit。BeginFlowStep:HideItemSlot cue 前 TryCaptureItemFlyStart。OnlineMatchPresentationPlayer。SetAuthoritative(snapshot) 写权威态。players[].items 为权威数量。_onlinePresentationHiddenItems 仅本机 optimistic hide;观察他人以 snapshot 为准。pendingReveal 有 12s fallback(TryFinalizeOpeningDealPresentation)。PresentationCue.bullet_type = ctx.FinalBullet(魔术师翻转后的最终子弹)。1 | Hub 收到 MatchResynced |
服务端 ResyncPlayerAsync 同时恢复 ControlType.Human 并 PublishSnapshotAsync 广播给他人。
| 通道 | 入口 | FlowStep 启用后 |
|---|---|---|
| FlowStep | FlowStepReceived | 主路径 |
| legacy snapshot | SnapshotReceived | 表现 busy 时 partial;校验 IsStaleLegacySnapshot |
| legacy cue/event | PresentationCueReceived | OnlineSession 丢弃,防重复 |
OnlineSession.UsesFlowStepChannel:收到首帧 FlowStep 后切换,丢弃 stale legacy。
1 | sequenceDiagram |
| 维度 | 客户端单机 | 服务端联机 | 客户端联机 |
|---|---|---|---|
| 流程引擎 | 本地 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 仅播表现 |
yield return Request*Decision 表达异步人机交互;服务端 CoroutineDriver 移植。FlowController 结算点调用 PresentationCueRelay.Emit*;新决策场景扩展 IDebugHost.Request*Decision。PresentationCueKind + PresentationHoldCalculator.EstimateCueMs。PresentationCueKind + PresentationCueMapper.ToProto。PresentationCuePlayback.TryBuildFromSpec 新分支。PushAuthoritativeSnapshot 一次 Commit。FlowStep.revision 是否严格单调递增。TryBuildFromSpec 是否 false。客户端 FlowSync 单元测试(无需启动服务端):
1 | cd server/tools/ProtoSmokeTest |
覆盖:OnlineFlowOrchestrator 排序/门禁、OnlineHudState 显示延后、OnlineFlowSync 规则。
联机对局时序冒烟(需 API + 自动对局):
1 | cd server/tools/ProtoSmokeTest |
校验:revision 连续、bundle 内 snapshot/cue revision 对齐、ActionSettle 含 cue 时 hold>0、无 legacy 分通道消息。
Proto 代码生成:
1 | cd server && ./scripts/generate-proto.sh |
1 | Flow/FlowController.cs 流程主控:五阶段、响应链、压栈 |
1 | Services/Game/GameLoopService.cs 对局启动、决策路由、断线重连 |
1 | server/proto/v1/game.proto FlowStep、GameStateSnapshot、PlayerDecisionPayload |
1 | Network/OnlineSession.cs Hub 连接、消息分发 |
文档版本:GameCore 共用 FlowController + FlowStep 联机方案。归档日期:2026-06。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。