feat: isolate lobby during minigames
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
31afce6882
commit
6a5e534d00
@@ -0,0 +1,113 @@
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using XGame.MiniGame;
|
||||
|
||||
public sealed class MiniGameStageIsolationTests
|
||||
{
|
||||
private GameObject _root;
|
||||
|
||||
private sealed class TestBehaviour : MonoBehaviour
|
||||
{
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (_root != null)
|
||||
{
|
||||
Object.DestroyImmediate(_root);
|
||||
_root = null;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SuspendAndResume_RestoresOriginalActiveAndEnabledStates()
|
||||
{
|
||||
_root = new GameObject("root");
|
||||
GameObject uiActive = new GameObject("ui-active");
|
||||
GameObject uiInactive = new GameObject("ui-inactive");
|
||||
GameObject sceneRoot = new GameObject("scene-root");
|
||||
uiActive.transform.SetParent(_root.transform);
|
||||
uiInactive.transform.SetParent(_root.transform);
|
||||
sceneRoot.transform.SetParent(_root.transform);
|
||||
uiInactive.SetActive(false);
|
||||
|
||||
var enabledInput = _root.AddComponent<TestBehaviour>();
|
||||
var disabledTick = _root.AddComponent<TestBehaviour>();
|
||||
disabledTick.enabled = false;
|
||||
|
||||
var canvas = _root.AddComponent<CanvasGroup>();
|
||||
canvas.interactable = true;
|
||||
canvas.blocksRaycasts = true;
|
||||
|
||||
var isolation = _root.AddComponent<MiniGameStageIsolation>();
|
||||
isolation.LobbyUiRoots = new[] { uiActive, uiInactive };
|
||||
isolation.LobbySceneRoots = new[] { sceneRoot };
|
||||
isolation.LobbyInputBehaviours = new Behaviour[] { enabledInput };
|
||||
isolation.LobbyTickBehaviours = new Behaviour[] { disabledTick };
|
||||
isolation.LobbyCanvasGroups = new[] { canvas };
|
||||
|
||||
isolation.SuspendLobbyForMiniGame();
|
||||
|
||||
Assert.IsTrue(isolation.IsSuspended);
|
||||
Assert.IsFalse(uiActive.activeSelf);
|
||||
Assert.IsFalse(uiInactive.activeSelf);
|
||||
Assert.IsFalse(sceneRoot.activeSelf);
|
||||
Assert.IsFalse(enabledInput.enabled);
|
||||
Assert.IsFalse(disabledTick.enabled);
|
||||
Assert.IsFalse(canvas.interactable);
|
||||
Assert.IsFalse(canvas.blocksRaycasts);
|
||||
|
||||
isolation.ResumeLobbyAfterMiniGame();
|
||||
|
||||
Assert.IsFalse(isolation.IsSuspended);
|
||||
Assert.IsTrue(uiActive.activeSelf);
|
||||
Assert.IsFalse(uiInactive.activeSelf);
|
||||
Assert.IsTrue(sceneRoot.activeSelf);
|
||||
Assert.IsTrue(enabledInput.enabled);
|
||||
Assert.IsFalse(disabledTick.enabled);
|
||||
Assert.IsTrue(canvas.interactable);
|
||||
Assert.IsTrue(canvas.blocksRaycasts);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SuspendAndResume_AreIdempotent()
|
||||
{
|
||||
_root = new GameObject("root");
|
||||
GameObject ui = new GameObject("ui");
|
||||
ui.transform.SetParent(_root.transform);
|
||||
var behaviour = _root.AddComponent<TestBehaviour>();
|
||||
var isolation = _root.AddComponent<MiniGameStageIsolation>();
|
||||
isolation.LobbyUiRoots = new[] { ui };
|
||||
isolation.LobbyInputBehaviours = new Behaviour[] { behaviour };
|
||||
|
||||
isolation.SuspendLobbyForMiniGame();
|
||||
ui.SetActive(true);
|
||||
behaviour.enabled = true;
|
||||
isolation.SuspendLobbyForMiniGame();
|
||||
|
||||
Assert.IsFalse(ui.activeSelf);
|
||||
Assert.IsFalse(behaviour.enabled);
|
||||
|
||||
isolation.ResumeLobbyAfterMiniGame();
|
||||
isolation.ResumeLobbyAfterMiniGame();
|
||||
|
||||
Assert.IsTrue(ui.activeSelf);
|
||||
Assert.IsTrue(behaviour.enabled);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SuspendAndResume_IgnoreNullEntries()
|
||||
{
|
||||
_root = new GameObject("root");
|
||||
var isolation = _root.AddComponent<MiniGameStageIsolation>();
|
||||
isolation.LobbyUiRoots = new GameObject[] { null };
|
||||
isolation.LobbySceneRoots = new GameObject[] { null };
|
||||
isolation.LobbyInputBehaviours = new Behaviour[] { null };
|
||||
isolation.LobbyTickBehaviours = new Behaviour[] { null };
|
||||
isolation.LobbyCanvasGroups = new CanvasGroup[] { null };
|
||||
|
||||
Assert.DoesNotThrow(() => isolation.SuspendLobbyForMiniGame());
|
||||
Assert.DoesNotThrow(() => isolation.ResumeLobbyAfterMiniGame());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0db89d5629e34042af0af9a432d1efb7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -69,12 +69,19 @@ namespace XGame.MiniGame
|
||||
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; }
|
||||
if (!ok)
|
||||
{
|
||||
FailEnterAndExit("下载失败: " + 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; }
|
||||
{
|
||||
FailEnterAndExit("加载程序集失败: " + asmLoader.Error);
|
||||
yield break;
|
||||
}
|
||||
|
||||
// 3) 网络通道
|
||||
_net = new MiniGameNetChannel(sock);
|
||||
@@ -82,7 +89,16 @@ namespace XGame.MiniGame
|
||||
_net.OnFrameworkFrame = OnFrameworkFrame;
|
||||
|
||||
// 4) 反射实例化 IGameClient + 注入 ctx
|
||||
_client = asmLoader.CreateClient();
|
||||
try
|
||||
{
|
||||
_client = asmLoader.CreateClient();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
FailEnterAndExit("创建客户端失败: " + e.Message);
|
||||
yield break;
|
||||
}
|
||||
|
||||
_assets = new MiniGameAssetLoader(dl.LocalDir, dl.Manifest);
|
||||
if (_players == null)
|
||||
{
|
||||
@@ -103,7 +119,15 @@ namespace XGame.MiniGame
|
||||
|
||||
// 6) OnEnter 接管
|
||||
// 注意:OnEnter 在 MatchFound 到达之前调用,房间可能尚未就绪;小游戏应等待后续网络消息再依赖房间状态
|
||||
SafeCall(() => _client.OnEnter(_ctx), "OnEnter");
|
||||
try
|
||||
{
|
||||
_client.OnEnter(_ctx);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
FailEnterAndExit("OnEnter 抛异常: " + e.Message);
|
||||
yield break;
|
||||
}
|
||||
_running = true;
|
||||
Debug.Log("[MiniGameHost] 小游戏已进入: " + gameId);
|
||||
}
|
||||
@@ -157,6 +181,27 @@ namespace XGame.MiniGame
|
||||
// 返回大厅:交回 Lua game_module_runner 或大厅 UI(按现有大厅返回流程)
|
||||
}
|
||||
|
||||
private void FailEnterAndExit(string reason)
|
||||
{
|
||||
DestroyResultDialog();
|
||||
_waitingForResultConfirm = false;
|
||||
_running = false;
|
||||
_client = null;
|
||||
_net = null;
|
||||
_ctx = null;
|
||||
if (_players != null)
|
||||
{
|
||||
_players.SetRoomPlayers(null);
|
||||
}
|
||||
if (_assets != null)
|
||||
{
|
||||
_assets.UnloadAll();
|
||||
_assets = null;
|
||||
}
|
||||
Debug.LogError("[MiniGameHost] 进入小游戏失败: " + reason);
|
||||
OnGameExited?.Invoke();
|
||||
}
|
||||
|
||||
private void ShowResultDialog(RoomEndMsg roomEnd)
|
||||
{
|
||||
if (_waitingForResultConfirm)
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XGame.MiniGame
|
||||
{
|
||||
public sealed class MiniGameStageIsolation : MonoBehaviour
|
||||
{
|
||||
public GameObject[] LobbyUiRoots;
|
||||
public GameObject[] LobbySceneRoots;
|
||||
public Behaviour[] LobbyInputBehaviours;
|
||||
public Behaviour[] LobbyTickBehaviours;
|
||||
public CanvasGroup[] LobbyCanvasGroups;
|
||||
|
||||
private readonly List<GameObjectState> _gameObjectStates = new List<GameObjectState>();
|
||||
private readonly List<BehaviourState> _behaviourStates = new List<BehaviourState>();
|
||||
private readonly List<CanvasGroupState> _canvasGroupStates = new List<CanvasGroupState>();
|
||||
|
||||
public bool IsSuspended { get; private set; }
|
||||
|
||||
public void SuspendLobbyForMiniGame()
|
||||
{
|
||||
if (IsSuspended)
|
||||
{
|
||||
ApplySuspendedState();
|
||||
return;
|
||||
}
|
||||
|
||||
CaptureState();
|
||||
IsSuspended = true;
|
||||
ApplySuspendedState();
|
||||
}
|
||||
|
||||
public void ResumeLobbyAfterMiniGame()
|
||||
{
|
||||
if (!IsSuspended)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _gameObjectStates.Count; i++)
|
||||
{
|
||||
GameObjectState state = _gameObjectStates[i];
|
||||
if (state.Target != null)
|
||||
{
|
||||
state.Target.SetActive(state.ActiveSelf);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < _behaviourStates.Count; i++)
|
||||
{
|
||||
BehaviourState state = _behaviourStates[i];
|
||||
if (state.Target != null)
|
||||
{
|
||||
state.Target.enabled = state.Enabled;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < _canvasGroupStates.Count; i++)
|
||||
{
|
||||
CanvasGroupState state = _canvasGroupStates[i];
|
||||
if (state.Target != null)
|
||||
{
|
||||
state.Target.interactable = state.Interactable;
|
||||
state.Target.blocksRaycasts = state.BlocksRaycasts;
|
||||
}
|
||||
}
|
||||
|
||||
_gameObjectStates.Clear();
|
||||
_behaviourStates.Clear();
|
||||
_canvasGroupStates.Clear();
|
||||
IsSuspended = false;
|
||||
}
|
||||
|
||||
private void CaptureState()
|
||||
{
|
||||
_gameObjectStates.Clear();
|
||||
_behaviourStates.Clear();
|
||||
_canvasGroupStates.Clear();
|
||||
CaptureGameObjects(LobbyUiRoots);
|
||||
CaptureGameObjects(LobbySceneRoots);
|
||||
CaptureBehaviours(LobbyInputBehaviours);
|
||||
CaptureBehaviours(LobbyTickBehaviours);
|
||||
CaptureCanvasGroups(LobbyCanvasGroups);
|
||||
}
|
||||
|
||||
private void ApplySuspendedState()
|
||||
{
|
||||
SetGameObjectsActive(LobbyUiRoots, false);
|
||||
SetGameObjectsActive(LobbySceneRoots, false);
|
||||
SetBehavioursEnabled(LobbyInputBehaviours, false);
|
||||
SetBehavioursEnabled(LobbyTickBehaviours, false);
|
||||
SetCanvasGroupsBlocked(LobbyCanvasGroups, false, false);
|
||||
}
|
||||
|
||||
private void CaptureGameObjects(GameObject[] targets)
|
||||
{
|
||||
if (targets == null) return;
|
||||
for (int i = 0; i < targets.Length; i++)
|
||||
{
|
||||
GameObject target = targets[i];
|
||||
if (target != null)
|
||||
{
|
||||
_gameObjectStates.Add(new GameObjectState(target, target.activeSelf));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CaptureBehaviours(Behaviour[] targets)
|
||||
{
|
||||
if (targets == null) return;
|
||||
for (int i = 0; i < targets.Length; i++)
|
||||
{
|
||||
Behaviour target = targets[i];
|
||||
if (target != null)
|
||||
{
|
||||
_behaviourStates.Add(new BehaviourState(target, target.enabled));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CaptureCanvasGroups(CanvasGroup[] targets)
|
||||
{
|
||||
if (targets == null) return;
|
||||
for (int i = 0; i < targets.Length; i++)
|
||||
{
|
||||
CanvasGroup target = targets[i];
|
||||
if (target != null)
|
||||
{
|
||||
_canvasGroupStates.Add(new CanvasGroupState(target, target.interactable, target.blocksRaycasts));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetGameObjectsActive(GameObject[] targets, bool active)
|
||||
{
|
||||
if (targets == null) return;
|
||||
for (int i = 0; i < targets.Length; i++)
|
||||
{
|
||||
if (targets[i] != null)
|
||||
{
|
||||
targets[i].SetActive(active);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetBehavioursEnabled(Behaviour[] targets, bool enabled)
|
||||
{
|
||||
if (targets == null) return;
|
||||
for (int i = 0; i < targets.Length; i++)
|
||||
{
|
||||
if (targets[i] != null)
|
||||
{
|
||||
targets[i].enabled = enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetCanvasGroupsBlocked(CanvasGroup[] targets, bool interactable, bool blocksRaycasts)
|
||||
{
|
||||
if (targets == null) return;
|
||||
for (int i = 0; i < targets.Length; i++)
|
||||
{
|
||||
CanvasGroup target = targets[i];
|
||||
if (target != null)
|
||||
{
|
||||
target.interactable = interactable;
|
||||
target.blocksRaycasts = blocksRaycasts;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct GameObjectState
|
||||
{
|
||||
public readonly GameObject Target;
|
||||
public readonly bool ActiveSelf;
|
||||
|
||||
public GameObjectState(GameObject target, bool activeSelf)
|
||||
{
|
||||
Target = target;
|
||||
ActiveSelf = activeSelf;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct BehaviourState
|
||||
{
|
||||
public readonly Behaviour Target;
|
||||
public readonly bool Enabled;
|
||||
|
||||
public BehaviourState(Behaviour target, bool enabled)
|
||||
{
|
||||
Target = target;
|
||||
Enabled = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct CanvasGroupState
|
||||
{
|
||||
public readonly CanvasGroup Target;
|
||||
public readonly bool Interactable;
|
||||
public readonly bool BlocksRaycasts;
|
||||
|
||||
public CanvasGroupState(CanvasGroup target, bool interactable, bool blocksRaycasts)
|
||||
{
|
||||
Target = target;
|
||||
Interactable = interactable;
|
||||
BlocksRaycasts = blocksRaycasts;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 51e84fcb0d45460891d9f0f8e60bf2a6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -12,6 +12,7 @@ namespace XGame.MiniGame
|
||||
public string GatewayUrl = "ws://127.0.0.1:5005/ws?pid=1";
|
||||
public string CdnBase = "";
|
||||
public bool AutoStart;
|
||||
public MiniGameStageIsolation StageIsolation;
|
||||
|
||||
private readonly List<GameInfoMsg> _games = new List<GameInfoMsg>();
|
||||
private string _status = "idle";
|
||||
@@ -141,10 +142,12 @@ namespace XGame.MiniGame
|
||||
{
|
||||
_lobbyActive = false;
|
||||
_lobbyNet = null;
|
||||
SuspendLobbyStage();
|
||||
_gameHost = gameObject.GetComponent<MiniGameHost>();
|
||||
if (_gameHost == null) _gameHost = gameObject.AddComponent<MiniGameHost>();
|
||||
_gameHost.OnGameExited = () =>
|
||||
{
|
||||
ResumeLobbyStage();
|
||||
_status = "returned to lobby";
|
||||
_matching = false;
|
||||
_lobbyNet = new MiniGameNetChannel(_socket);
|
||||
@@ -155,6 +158,35 @@ namespace XGame.MiniGame
|
||||
_gameHost.EnterGame(ResolvedCdnBase(), gameId, version, _socket, false);
|
||||
}
|
||||
|
||||
private MiniGameStageIsolation ResolveStageIsolation()
|
||||
{
|
||||
if (StageIsolation != null)
|
||||
{
|
||||
return StageIsolation;
|
||||
}
|
||||
|
||||
StageIsolation = GetComponent<MiniGameStageIsolation>();
|
||||
return StageIsolation;
|
||||
}
|
||||
|
||||
private void SuspendLobbyStage()
|
||||
{
|
||||
MiniGameStageIsolation isolation = ResolveStageIsolation();
|
||||
if (isolation != null)
|
||||
{
|
||||
isolation.SuspendLobbyForMiniGame();
|
||||
}
|
||||
}
|
||||
|
||||
private void ResumeLobbyStage()
|
||||
{
|
||||
MiniGameStageIsolation isolation = ResolveStageIsolation();
|
||||
if (isolation != null)
|
||||
{
|
||||
isolation.ResumeLobbyAfterMiniGame();
|
||||
}
|
||||
}
|
||||
|
||||
private string ResolvedCdnBase()
|
||||
{
|
||||
#if XW_DEVTEST
|
||||
|
||||
Reference in New Issue
Block a user