using System; using System.Collections.Generic; using Cysharp.Threading.Tasks; using ShrinkEventBus; using ShrinkNetwork; using ShrinkNetwork.Integration; using UnityEngine; [ShrinkEventSubscriber(OwnerId = "shrink.demo.embedded-host", DefaultBus = "game")] public sealed class ShrinkEmbeddedHostNetworkDemoSceneController : MonoBehaviour { private const long DemoSessionId = 1; private const string DemoPlayerId = "local-player"; private const string FireballSkillId = "fireball"; private const string UltimateSkillId = "ultimate"; private readonly List _logs = new(); private ShrinkNetworkService _embeddedHostService; private ShrinkNetworkService _localClientService; private ShrinkLoopbackTransport _embeddedHostTransport; private ShrinkLoopbackTransport _localClientTransport; private ShrinkNetworkSession _localClientSession; private int _clientHp; private int _hostObservedHp; private int _hostMana; private long _nextDeltaVersion; private long _lastAppliedDeltaVersion; private string _lastSkillDecision = "未请求"; private Vector2 _logScroll; private bool _initialized; private IDisposable _eventBinding; private void Start() { ResetDemo(); } private void OnDestroy() { ShutdownDemo(); } [ContextMenu("重置演示")] public void ResetDemo() { ShutdownDemo(); ResetState(); _eventBinding = EventBus.Attach(this); SetupEmbeddedHostTopology(); _initialized = true; AppendLog("演示已就绪:本地客户端已接入内置 Host Server。"); } private void OnGUI() { var areaHeight = Mathf.Min(Screen.height - 32f, 860f); var logHeight = Mathf.Max(180f, areaHeight - 610f); var area = new Rect(16f, 16f, 560f, areaHeight); GUILayout.BeginArea(area, GUI.skin.window); GUILayout.Label("ShrinkSDK 内置 Host 联机演示"); GUILayout.Label("拓扑:本地客户端 -> 内置 Host Server(演示阶段使用 Loopback 代替 SteamP2P/LAN)"); GUILayout.Space(8f); GUILayout.Label("客户端状态"); GUILayout.Label($"血量:{_clientHp}"); GUILayout.Label($"下一个增量版本号:{_nextDeltaVersion + 1}"); GUILayout.Label($"最近一次技能裁决:{_lastSkillDecision}"); GUILayout.Space(8f); GUILayout.Label("内置 Host 状态"); GUILayout.Label($"Host 观察到的客户端血量:{_hostObservedHp}"); GUILayout.Label($"Host 最近应用的增量版本:{_lastAppliedDeltaVersion}"); GUILayout.Label($"Host 当前可用法力:{_hostMana}"); GUILayout.Space(8f); GUI.enabled = _initialized && _localClientSession != null; if (GUILayout.Button("发送血量增量 -7", GUILayout.Height(32f))) { SendFreshHpDeltaAsync().Forget(); } GUILayout.Label("作用:模拟客户端只同步变化后的血量,并携带新的 DeltaVersion。"); if (GUILayout.Button("重放旧版本增量(hp=999)", GUILayout.Height(32f))) { SendStaleHpDeltaAsync().Forget(); } GUILayout.Label("作用:发送一个过期版本号的旧增量,验证 Host 会拒绝重复或回退版本。"); if (GUILayout.Button("请求释放火球术(消耗 3)", GUILayout.Height(32f))) { RequestSkillAsync(FireballSkillId, 3).Forget(); } GUILayout.Label("作用:客户端向 Host 发起 HasResult 请求,由 Host 判断法力是否足够并返回 ALLOW / DENY。"); if (GUILayout.Button("请求释放终极技能(消耗 6)", GUILayout.Height(32f))) { RequestSkillAsync(UltimateSkillId, 6).Forget(); } GUILayout.Label("作用:制造一次更容易失败的裁决请求,观察 IsCanceled 与 EventResult 回传。"); GUI.enabled = _initialized; if (GUILayout.Button("给 Host 补充法力 +2", GUILayout.Height(28f))) { _hostMana += 2; AppendLog($"Host 法力已补充到 {_hostMana}。"); } GUILayout.Label("作用:手动提高 Host 侧法力,用于重新测试技能裁决。"); if (GUILayout.Button("重置演示", GUILayout.Height(28f))) { ResetDemo(); } GUILayout.Label("作用:重建内置 Host / Client 拓扑并清空当前演示状态。"); GUI.enabled = true; GUILayout.Space(10f); GUILayout.Label("事件日志"); _logScroll = GUILayout.BeginScrollView(_logScroll, GUILayout.Height(logHeight)); for (var i = 0; i < _logs.Count; i++) { GUILayout.Label(_logs[i]); } GUILayout.EndScrollView(); GUILayout.EndArea(); } private void ResetState() { _clientHp = 100; _hostObservedHp = 100; _hostMana = 5; _nextDeltaVersion = 0; _lastAppliedDeltaVersion = 0; _lastSkillDecision = "未请求"; _logs.Clear(); _logScroll = Vector2.zero; } private void SetupEmbeddedHostTopology() { _embeddedHostService = new ShrinkNetworkService(new ShrinkJsonNetworkSerializer(), new ShrinkNetworkMessageRegistry(), new ShrinkNetworkRouter()); _localClientService = new ShrinkNetworkService(new ShrinkJsonNetworkSerializer(), new ShrinkNetworkMessageRegistry(), new ShrinkNetworkRouter()); _embeddedHostTransport = new ShrinkLoopbackTransport(); _localClientTransport = new ShrinkLoopbackTransport(); _embeddedHostTransport.LinkPeer(_localClientTransport); _embeddedHostService.OnSessionConnected += OnEmbeddedHostSessionConnected; _localClientService.OnSessionConnected += OnLocalClientSessionConnected; _embeddedHostService.BindTransport(_embeddedHostTransport); _localClientService.BindTransport(_localClientTransport); _localClientTransport.OpenSession(DemoSessionId, "embedded-host"); } private void ShutdownDemo() { _eventBinding?.Dispose(); _eventBinding = null; if (_localClientTransport != null) { try { _localClientTransport.CloseSession(DemoSessionId, "embedded-host"); } catch { } } if (_embeddedHostService != null) { _embeddedHostService.BindTransport(null); _embeddedHostService.OnSessionConnected -= OnEmbeddedHostSessionConnected; } if (_localClientService != null) { _localClientService.BindTransport(null); _localClientService.OnSessionConnected -= OnLocalClientSessionConnected; } _embeddedHostTransport?.Stop(); _localClientTransport?.Stop(); _localClientSession = null; _embeddedHostService = null; _localClientService = null; _embeddedHostTransport = null; _localClientTransport = null; _initialized = false; } private void OnEmbeddedHostSessionConnected(ShrinkNetworkSession session) { session.SetPeerKind(ShrinkNetworkPeerKind.Client); AppendLog($"内置 Host 已接收玩家会话 {session.SessionId}。"); } private void OnLocalClientSessionConnected(ShrinkNetworkSession session) { session.SetPeerKind(ShrinkNetworkPeerKind.Server); _localClientSession = session; AppendLog($"本地客户端已作为会话 {session.SessionId} 接入内置 Host。"); } private async UniTaskVoid SendFreshHpDeltaAsync() { if (_localClientSession == null) return; _clientHp = Mathf.Max(0, _clientHp - 7); var version = ++_nextDeltaVersion; AppendLog($"客户端发送血量增量 v{version},目标血量 {_clientHp}。"); await _localClientSession.PublishEventAsync(new DemoPlayerHpDeltaEvent { PlayerId = DemoPlayerId, TargetNode = DemoNetworkNodeRole.EmbeddedHost, Hp = _clientHp, DeltaVersion = version }); await UniTask.Yield(); AppendLog($"Host 应用后状态:血量={_hostObservedHp},版本={_lastAppliedDeltaVersion}。"); } private async UniTaskVoid SendStaleHpDeltaAsync() { if (_localClientSession == null) return; if (_nextDeltaVersion <= 1) { AppendLog("请先发送至少一次新的增量,再重放旧版本增量。"); return; } var staleVersion = _nextDeltaVersion - 1; AppendLog($"客户端重放旧版本增量 v{staleVersion},并伪造 hp=999。"); await _localClientSession.PublishEventAsync(new DemoPlayerHpDeltaEvent { PlayerId = DemoPlayerId, TargetNode = DemoNetworkNodeRole.EmbeddedHost, Hp = 999, DeltaVersion = staleVersion }); await UniTask.Yield(); AppendLog($"Host 已忽略旧增量,血量仍为 {_hostObservedHp},版本={_lastAppliedDeltaVersion}。"); } private async UniTaskVoid RequestSkillAsync(string skillId, int manaCost) { if (_localClientSession == null) return; var evt = new DemoCanUseSkillEvent { PlayerId = DemoPlayerId, SkillId = skillId, ManaCost = manaCost, TargetNode = DemoNetworkNodeRole.EmbeddedHost }; AppendLog($"客户端请求释放技能“{skillId}”,法力消耗 {manaCost}。"); var outcome = await _localClientSession.RequestEventAsync(evt); if (!outcome.IsSuccess) { _lastSkillDecision = $"错误 {outcome.ErrorCode}"; AppendLog($"技能请求失败:{outcome.ErrorMessage}"); return; } _lastSkillDecision = outcome.Result.ToString(); AppendLog($"客户端收到裁决:{outcome.Result},Canceled={outcome.IsCanceled},Host 法力={_hostMana}。"); } [ShrinkSubscribe] private void OnEmbeddedHostHpDelta(DemoPlayerHpDeltaEvent evt) { if (evt.TargetNode != DemoNetworkNodeRole.EmbeddedHost) return; _hostObservedHp = evt.Hp; _lastAppliedDeltaVersion = evt.DeltaVersion; AppendLog($"内置 Host 已应用血量增量 v{evt.DeltaVersion}:hp={evt.Hp}。"); } [ShrinkSubscribe(Priority = ShrinkEventPriority.High)] private void OnEmbeddedHostSkillRequest(DemoCanUseSkillEvent evt) { if (evt.TargetNode != DemoNetworkNodeRole.EmbeddedHost) return; if (_hostMana >= evt.ManaCost) { _hostMana -= evt.ManaCost; evt.SetResult(EventResult.ALLOW); AppendLog($"内置 Host 批准技能“{evt.SkillId}”,剩余法力 {_hostMana}。"); return; } evt.SetResult(EventResult.DENY); evt.SetCanceled(true); AppendLog($"内置 Host 拒绝技能“{evt.SkillId}”,需要 {evt.ManaCost},当前仅有 {_hostMana}。"); } private void AppendLog(string message) { _logs.Add($"[{DateTime.Now:HH:mm:ss}] {message}"); if (_logs.Count > 18) _logs.RemoveAt(0); _logScroll.y = float.MaxValue; } } public enum DemoNetworkNodeRole { LocalClient = 0, EmbeddedHost = 1 } [ShrinkNetworkEvent] [ShrinkNetworkMessage(3401, "demo/player_hp_delta")] public sealed class DemoPlayerHpDeltaEvent : IShrinkEvent, IShrinkNetworkMessage, IShrinkNetworkDeltaEvent { public DemoNetworkNodeRole TargetNode { get; set; } public string PlayerId { get; set; } = ""; public int Hp { get; set; } public long DeltaVersion { get; set; } public string DeltaKey => $"{TargetNode}:{PlayerId}"; } [ShrinkNetworkEvent] [ShrinkNetworkMessage(3410, "demo/can_use_skill")] public sealed class DemoCanUseSkillEvent : IShrinkCancelableEvent, IShrinkResultEvent, IShrinkNetworkRequest { private bool _isCanceled; private EventResult _result = EventResult.DEFAULT; public DemoNetworkNodeRole TargetNode { get; set; } public string PlayerId { get; set; } = ""; public string SkillId { get; set; } = ""; public int ManaCost { get; set; } public bool IsCanceled => _isCanceled; public EventResult Result => _result; public void SetCanceled(bool value) => _isCanceled = value; public void SetResult(EventResult result) => _result = result; }