Files
AIC-Project/Client/Assets/Script/xmain/MiniGame/MiniGameHost.cs
T
2026-07-10 10:24:29 +08:00

379 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections;
using System.Text;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using XWorld.Framework;
using XWorld.Framework.Protocol;
namespace XGame.MiniGame
{
// 一次小游戏会话的宿主。EnterGame 启动整条链路;每帧驱动;ExitGame 收尾。
public sealed class MiniGameHost : MonoBehaviour
{
// 标准 UGUI 结算弹窗预设(由 Doc/UIPrefabCreater/UI_MiniGameResult.json 经管线生成,进 game_art_ui_prefab AB
private const string ResultDialogPrefabPath = "Assets/Game/Art/UI/Prefab/UI_MiniGameResult.prefab";
private IGameClient _client;
private MiniGameNetChannel _net;
private GameClientCtx _ctx;
private MiniGameAssetLoader _assets;
private bool _running;
private bool _waitingForResultConfirm;
private string _gameId;
private int _version;
private string _roomId;
private int _selfPlayerId;
private GameObject _resultDialog;
public Action OnGameExited;
// 入口:cdnBase 小游戏 CDN 根;sock 已连接的 XWebSocket(来自大厅/或本宿主新建)
public void EnterGame(string cdnBase, string gameId, int version, XWebSocket sock)
=> EnterGame(cdnBase, gameId, version, sock, true);
public void EnterGame(string cdnBase, string gameId, int version, XWebSocket sock, bool sendMatchRequest)
{
_gameId = gameId; _version = version;
// FIX: XWCoroutine.StartCo is a static method (not Instance.StartCo)
XWCoroutine.StartCo(CoEnter(cdnBase, gameId, version, sock, sendMatchRequest));
}
// 大厅链路:MatchFound 由大厅通道消费(sendMatchRequest=false 时宿主收不到),
// 由外部把房间信息注入,否则结算弹框无法判断胜负(_selfPlayerId 恒 0)。
public void SetMatchInfo(string roomId, int selfPlayerId)
{
_roomId = roomId;
_selfPlayerId = selfPlayerId;
}
private IEnumerator CoEnter(string cdnBase, string gameId, int version, XWebSocket sock, bool sendMatchRequest)
{
// 1) 下载 + 校验
var dl = new MiniGameDownloader(cdnBase, gameId, version);
bool ok = false;
yield return dl.Run(r => ok = r);
if (!ok) { Debug.LogError("[MiniGameHost] 下载失败: " + dl.Error); yield break; }
// 2) 加载 Core/Client DLL,反射入口
var asmLoader = new MiniGameAssemblyLoader();
if (!asmLoader.Load(dl.LocalDir, dl.Manifest))
{ Debug.LogError("[MiniGameHost] 加载程序集失败: " + asmLoader.Error); yield break; }
// 3) 网络通道
_net = new MiniGameNetChannel(sock);
_net.OnGameMessage = msg => { if (_client != null) SafeCall(() => _client.OnNetMessage(msg), "OnNetMessage"); };
_net.OnFrameworkFrame = OnFrameworkFrame;
// 4) 反射实例化 IGameClient + 注入 ctx
_client = asmLoader.CreateClient();
_assets = new MiniGameAssetLoader(dl.LocalDir, dl.Manifest);
_ctx = new GameClientCtx(
_assets,
new UnityLogger($"[{gameId}]"),
msg => { if (_net != null) _net.SendGame(msg); },
() => ExitGame());
if (sendMatchRequest)
{
_net.SendFramework(FrameworkOpcode.MatchRequest,
new MatchRequestMsg { GameId = gameId, Version = version }.Encode());
}
// 6) OnEnter 接管
// 注意:OnEnter 在 MatchFound 到达之前调用,房间可能尚未就绪;小游戏应等待后续网络消息再依赖房间状态
SafeCall(() => _client.OnEnter(_ctx), "OnEnter");
_running = true;
Debug.Log("[MiniGameHost] 小游戏已进入: " + gameId);
}
private void OnFrameworkFrame(Frame f)
{
switch ((FrameworkOpcode)f.Opcode)
{
case FrameworkOpcode.MatchFound:
MatchFoundMsg found = MatchFoundMsg.Decode(f.Payload);
_roomId = found.RoomId;
_selfPlayerId = found.SelfPlayerId;
Debug.Log("[MiniGameHost] 房间就绪: " + _roomId);
break;
case FrameworkOpcode.RoomEnd:
Debug.Log("[MiniGameHost] 房间结束,弹结算框");
ShowResultDialog(RoomEndMsg.Decode(f.Payload));
break;
// Snapshot/Heartbeat 等:本步骤交给小游戏自行通过 Game 帧处理;如需框架级快照可在此扩展
}
}
private void Update()
{
if (!_running) return;
_net?.Pump();
if (_client != null) SafeCall(() => _client.OnUpdate(Time.deltaTime), "OnUpdate");
}
public void ExitGame()
{
if (!_running && _client == null) return;
DestroyResultDialog();
_waitingForResultConfirm = false;
_running = false;
if (_client != null) { SafeCall(() => _client.OnExit(), "OnExit"); _client = null; }
_net = null;
_ctx = null;
if (_assets != null) { _assets.UnloadAll(); _assets = null; } // 卸载本小游戏资源 AB
Debug.Log("[MiniGameHost] 已退出小游戏: " + _gameId);
OnGameExited?.Invoke();
// 返回大厅:交回 Lua game_module_runner 或大厅 UI(按现有大厅返回流程)
}
private void ShowResultDialog(RoomEndMsg roomEnd)
{
if (_waitingForResultConfirm)
{
return;
}
_waitingForResultConfirm = true;
_running = false;
DestroyResultDialog();
EnsureEventSystem();
string resultText = DecodeResultBlob(roomEnd.ResultBlob);
string title = ResultTitle(roomEnd, resultText);
string message = ResultMessage(resultText);
// 项目规则:资源一律异步加载(同步只读本地不下载);预设不可用时回退代码构建——结算窗口宁丑不缺
XWCoroutine.StartCo(CoShowResultDialog(title, message));
}
private IEnumerator CoShowResultDialog(string title, string message)
{
GameObject prefab = null;
yield return XResLoader.coLoadRes(ResultDialogPrefabPath, typeof(GameObject), obj => prefab = obj as GameObject);
if (!_waitingForResultConfirm)
{
yield break; // 加载期间用户已退出(ExitGame),不再弹结算
}
if (prefab == null || !TryShowResultDialogFromPrefab(prefab, title, message))
{
Debug.LogWarning("[MiniGameHost] 结算预设不可用,回退代码构建: " + ResultDialogPrefabPath);
ShowResultDialogFallback(title, message);
}
}
// 标准 UGUI 结算预设(Doc/UIPrefabCreater/UI_MiniGameResult.json 生成),按名绑定节点
private bool TryShowResultDialogFromPrefab(GameObject prefab, string title, string message)
{
GameObject instance = Instantiate(prefab);
instance.name = "MiniGameResultDialog";
TextMeshProUGUI titleText = FindChildComponent<TextMeshProUGUI>(instance.transform, "Txt_Title");
TextMeshProUGUI messageText = FindChildComponent<TextMeshProUGUI>(instance.transform, "Txt_Message");
Button okButton = FindChildComponent<Button>(instance.transform, "Btn_Ok");
if (titleText == null || messageText == null || okButton == null)
{
Destroy(instance); // 预设结构不符(旧资源包),回退代码构建
return false;
}
titleText.text = title;
messageText.text = message;
okButton.onClick.AddListener(ExitGame);
_resultDialog = instance;
return true;
}
private static T FindChildComponent<T>(Transform root, string name) where T : Component
{
Transform[] transforms = root.GetComponentsInChildren<Transform>(true);
for (int i = 0; i < transforms.Length; i++)
{
if (transforms[i].name == name)
{
return transforms[i].GetComponent<T>();
}
}
return null;
}
// 回退路径:预设缺失/结构不符时代码构建(旧底包兼容),样式从简
private void ShowResultDialogFallback(string title, string message)
{
_resultDialog = new GameObject("MiniGameResultDialog");
Canvas canvas = _resultDialog.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 30000;
_resultDialog.AddComponent<CanvasScaler>().uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
_resultDialog.AddComponent<GraphicRaycaster>();
GameObject mask = CreateUiObject("Mask", _resultDialog.transform);
Image maskImage = mask.AddComponent<Image>();
maskImage.color = new Color(0f, 0f, 0f, 0.58f);
Stretch(mask.GetComponent<RectTransform>());
GameObject panel = CreateUiObject("Panel", mask.transform);
Image panelImage = panel.AddComponent<Image>();
panelImage.color = new Color(0.08f, 0.09f, 0.11f, 0.96f);
RectTransform panelRect = panel.GetComponent<RectTransform>();
panelRect.anchorMin = new Vector2(0.5f, 0.5f);
panelRect.anchorMax = new Vector2(0.5f, 0.5f);
panelRect.pivot = new Vector2(0.5f, 0.5f);
panelRect.sizeDelta = new Vector2(520f, 300f);
panelRect.anchoredPosition = Vector2.zero;
Text titleText = CreateText("Title", panel.transform, title, 34, FontStyle.Bold, TextAnchor.MiddleCenter);
RectTransform titleRect = titleText.GetComponent<RectTransform>();
titleRect.anchorMin = new Vector2(0f, 1f);
titleRect.anchorMax = new Vector2(1f, 1f);
titleRect.pivot = new Vector2(0.5f, 1f);
titleRect.offsetMin = new Vector2(32f, -92f);
titleRect.offsetMax = new Vector2(-32f, -28f);
Text messageText = CreateText("Message", panel.transform, message, 22, FontStyle.Normal, TextAnchor.MiddleCenter);
RectTransform messageRect = messageText.GetComponent<RectTransform>();
messageRect.anchorMin = new Vector2(0f, 0f);
messageRect.anchorMax = new Vector2(1f, 1f);
messageRect.offsetMin = new Vector2(42f, 98f);
messageRect.offsetMax = new Vector2(-42f, -106f);
Button okButton = CreateButton(panel.transform, "确定");
RectTransform buttonRect = okButton.GetComponent<RectTransform>();
buttonRect.anchorMin = new Vector2(0.5f, 0f);
buttonRect.anchorMax = new Vector2(0.5f, 0f);
buttonRect.pivot = new Vector2(0.5f, 0f);
buttonRect.sizeDelta = new Vector2(180f, 56f);
buttonRect.anchoredPosition = new Vector2(0f, 34f);
okButton.onClick.AddListener(ExitGame);
}
private string ResultTitle(RoomEndMsg roomEnd, string resultText)
{
if (roomEnd.WinnerPlayerId == 0)
{
// 有结算文本 = 游戏明确上报了"无胜者"→ 平局;无文本 = 旧模块/异常结束,保持中性标题(见 RoomEndResult 字段约定)
return string.IsNullOrEmpty(resultText) ? "游戏结束" : "平局";
}
if (_selfPlayerId == 0)
{
// 身份未知(未收到 MatchFound 也未注入):不猜胜负,给中性标题
return "游戏结束";
}
if (roomEnd.WinnerPlayerId == _selfPlayerId)
{
return "胜利";
}
return "失败";
}
private string ResultMessage(string resultText)
{
// 游戏上报的结算文本(如"最终比分 2 : 1")优先;裸 pid 对玩家无意义,不再显示
if (!string.IsNullOrEmpty(resultText))
{
return resultText;
}
return string.IsNullOrEmpty(_roomId) ? $"{_gameId}@{_version}" : $"房间 {_roomId}";
}
private static string DecodeResultBlob(byte[] resultBlob)
{
if (resultBlob == null || resultBlob.Length == 0)
{
return string.Empty;
}
if (resultBlob.Length > 4096)
{
return string.Empty; // 结算文本不应这么大,视为异常数据
}
try
{
string text = Encoding.UTF8.GetString(resultBlob);
for (int i = 0; i < text.Length; i++)
{
if (char.IsControl(text[i]) && text[i] != '\n' && text[i] != '\r' && text[i] != '\t')
{
return string.Empty;
}
}
return text;
}
catch
{
return string.Empty;
}
}
private void DestroyResultDialog()
{
if (_resultDialog != null)
{
Destroy(_resultDialog);
_resultDialog = null;
}
}
private static void EnsureEventSystem()
{
if (EventSystem.current != null)
{
return;
}
GameObject eventSystem = new GameObject("EventSystem");
eventSystem.AddComponent<EventSystem>();
eventSystem.AddComponent<StandaloneInputModule>();
}
private static GameObject CreateUiObject(string name, Transform parent)
{
GameObject obj = new GameObject(name);
obj.transform.SetParent(parent, false);
obj.AddComponent<RectTransform>();
return obj;
}
private static void Stretch(RectTransform rect)
{
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.offsetMin = Vector2.zero;
rect.offsetMax = Vector2.zero;
}
private static Text CreateText(string name, Transform parent, string content, int fontSize, FontStyle style, TextAnchor alignment)
{
GameObject obj = CreateUiObject(name, parent);
Text text = obj.AddComponent<Text>();
text.text = content;
// Unity 2022.2+ 移除了内置 Arial 字体,必须用 LegacyRuntime.ttf,否则抛 ArgumentException
text.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
text.fontSize = fontSize;
text.fontStyle = style;
text.alignment = alignment;
text.color = Color.white;
text.horizontalOverflow = HorizontalWrapMode.Wrap;
text.verticalOverflow = VerticalWrapMode.Truncate;
return text;
}
private static Button CreateButton(Transform parent, string label)
{
GameObject obj = CreateUiObject("ButtonOk", parent);
Image image = obj.AddComponent<Image>();
image.color = new Color(0.2f, 0.55f, 1f, 1f);
Button button = obj.AddComponent<Button>();
Text text = CreateText("Text", obj.transform, label, 24, FontStyle.Bold, TextAnchor.MiddleCenter);
Stretch(text.GetComponent<RectTransform>());
return button;
}
private static void SafeCall(Action a, string where)
{
try { a(); }
catch (Exception e) { Debug.LogError($"[MiniGameHost] 小游戏 {where} 抛异常: {e}"); }
}
}
}