Add networked Fighter3D minigame
This commit is contained in:
@@ -527,7 +527,23 @@ namespace XGame.EditorTools
|
||||
string spriteName = (string)comp["sprite"];
|
||||
Sprite sprite = ResolveSprite(spriteName, fileName);
|
||||
img.sprite = sprite;
|
||||
img.type = string.Equals((string)comp["imageType"], "Sliced", StringComparison.OrdinalIgnoreCase) ? Image.Type.Sliced : Image.Type.Simple;
|
||||
string imageType = (string)comp["imageType"];
|
||||
if (string.Equals(imageType, "Sliced", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
img.type = Image.Type.Sliced;
|
||||
}
|
||||
else if (string.Equals(imageType, "Filled", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
img.type = Image.Type.Filled;
|
||||
img.fillMethod = ParseFillMethod((string)comp["fillMethod"]);
|
||||
img.fillOrigin = 0;
|
||||
img.fillClockwise = true;
|
||||
img.fillAmount = comp["fillAmount"] != null ? Mathf.Clamp01((float)comp["fillAmount"]) : 1f;
|
||||
}
|
||||
else
|
||||
{
|
||||
img.type = Image.Type.Simple;
|
||||
}
|
||||
if (comp["preserveAspect"] != null) img.preserveAspect = (bool)comp["preserveAspect"];
|
||||
img.raycastTarget = comp["raycastTarget"] == null || (bool)comp["raycastTarget"];
|
||||
img.color = ParseColor((string)comp["color"], Color.white);
|
||||
@@ -932,6 +948,20 @@ namespace XGame.EditorTools
|
||||
}
|
||||
}
|
||||
|
||||
private static Image.FillMethod ParseFillMethod(string method)
|
||||
{
|
||||
switch (method)
|
||||
{
|
||||
case "Vertical": return Image.FillMethod.Vertical;
|
||||
case "Radial90": return Image.FillMethod.Radial90;
|
||||
case "Radial180": return Image.FillMethod.Radial180;
|
||||
case "Radial360": return Image.FillMethod.Radial360;
|
||||
case "Horizontal":
|
||||
default:
|
||||
return Image.FillMethod.Horizontal;
|
||||
}
|
||||
}
|
||||
|
||||
private static TMP_InputField.ContentType ParseInputContentType(string contentType)
|
||||
{
|
||||
switch (contentType)
|
||||
|
||||
@@ -11,6 +11,14 @@ public sealed class ActorBehaviorAnimatorControllerPathTests
|
||||
Is.EqualTo("Assets/Game/Art/Actor/Controller/fighter.controller"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetControllerPath_Fighter3D_ReturnsGeneratedFighterController()
|
||||
{
|
||||
Assert.That(
|
||||
ActorBehaviorAnimatorControllerPaths.GetControllerPath("fighter3d"),
|
||||
Is.EqualTo("Assets/Game/Art/Actor/Controller/fighter.controller"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetControllerPath_UnknownType_ReturnsNull()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
using NUnit.Framework;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using TMPro;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using XGame;
|
||||
using XGame.EditorTools;
|
||||
using XWorld.Framework.ActorBehavior;
|
||||
|
||||
public sealed class GameLobbyControllerLayoutTests
|
||||
{
|
||||
private const string PrefabPath = "Assets/Game/Art/UI/Prefab/UI_GameLobby.prefab";
|
||||
private const string Fighter3DJsonPath = "../Doc/UIPrefabCreater/UI_Fighter3D.json";
|
||||
private const string Fighter3DPrefabPath = "Assets/MiniGames/Fighter3D/res/UI/Prefab/UI_Fighter3D.prefab";
|
||||
private const string Fighter3DStagePrefabPath = "Assets/MiniGames/Fighter3D/res/Prefab/Fighter3D_BattleStage.prefab";
|
||||
private const string Fighter3DBehaviorConfigRoot = "Assets/MiniGames/Fighter3D/res/Config/ActorBehavior";
|
||||
|
||||
[Test]
|
||||
public void LobbyPanel_DocksToLeftMiddle()
|
||||
{
|
||||
GameObject instance = CreateLobbyInstance();
|
||||
try
|
||||
{
|
||||
GameLobbyController controller = instance.GetComponent<GameLobbyController>();
|
||||
Assert.That(controller, Is.Not.Null);
|
||||
|
||||
controller.BeginGames();
|
||||
|
||||
RectTransform panel = FindRect(instance.transform, "Panel_Lobby");
|
||||
Assert.That(panel.anchorMin, Is.EqualTo(new Vector2(0f, 0.5f)));
|
||||
Assert.That(panel.anchorMax, Is.EqualTo(new Vector2(0f, 0.5f)));
|
||||
Assert.That(panel.pivot, Is.EqualTo(new Vector2(0f, 0.5f)));
|
||||
Assert.That(panel.anchoredPosition, Is.EqualTo(new Vector2(24f, 0f)));
|
||||
Assert.That(panel.sizeDelta, Is.EqualTo(new Vector2(520f, 296f)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(instance);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CollapseButton_StaysAttachedToLobbyPanelInLeftMiddleLayout()
|
||||
{
|
||||
GameObject instance = CreateLobbyInstance();
|
||||
try
|
||||
{
|
||||
GameLobbyController controller = instance.GetComponent<GameLobbyController>();
|
||||
Assert.That(controller, Is.Not.Null);
|
||||
|
||||
controller.BeginGames();
|
||||
|
||||
RectTransform button = FindRect(instance.transform, "Btn_Collapse");
|
||||
Assert.That(button.anchorMin, Is.EqualTo(new Vector2(0f, 0.5f)));
|
||||
Assert.That(button.anchorMax, Is.EqualTo(new Vector2(0f, 0.5f)));
|
||||
Assert.That(button.pivot, Is.EqualTo(new Vector2(0f, 0.5f)));
|
||||
Assert.That(button.anchoredPosition, Is.EqualTo(new Vector2(544f, 0f)));
|
||||
Assert.That(button.sizeDelta, Is.EqualTo(new Vector2(48f, 72f)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(instance);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LobbyGameListPoll_WaitsForRefreshInterval()
|
||||
{
|
||||
MethodInfo method = FindGameListPollMethod();
|
||||
|
||||
Assert.That((bool)method.Invoke(null, new object[] { true, true, false, 4f, 5f }), Is.False);
|
||||
Assert.That((bool)method.Invoke(null, new object[] { true, true, false, 5f, 5f }), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LobbyGameListPoll_DoesNotRunOutsideIdleConnectedLobby()
|
||||
{
|
||||
MethodInfo method = FindGameListPollMethod();
|
||||
|
||||
Assert.That((bool)method.Invoke(null, new object[] { false, true, false, 5f, 5f }), Is.False);
|
||||
Assert.That((bool)method.Invoke(null, new object[] { true, false, false, 5f, 5f }), Is.False);
|
||||
Assert.That((bool)method.Invoke(null, new object[] { true, true, true, 5f, 5f }), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Fighter3DFallbackHud_UsesUnity2022BuiltinFont()
|
||||
{
|
||||
string source = File.ReadAllText("Assets/MiniGames/Fighter3D/scripts~/Client/FighterGameClient.cs");
|
||||
|
||||
Assert.That(source, Does.Contain("LegacyRuntime.ttf"));
|
||||
Assert.That(source, Does.Not.Contain("Arial.ttf"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Fighter3DHudSource_CanBindGeneratedTmpText()
|
||||
{
|
||||
string source = File.ReadAllText("Assets/MiniGames/Fighter3D/scripts~/Client/FighterGameClient.cs");
|
||||
|
||||
Assert.That(source, Does.Contain("using TMPro;"));
|
||||
Assert.That(source, Does.Contain("TMP_Text"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Fighter3DClientSource_UsesCommonActorPresentation()
|
||||
{
|
||||
string source = File.ReadAllText("Assets/MiniGames/Fighter3D/scripts~/Client/FighterGameClient.cs");
|
||||
|
||||
Assert.That(source, Does.Contain("MiniGameActorPresentation"));
|
||||
Assert.That(source, Does.Contain(Fighter3DBehaviorConfigRoot));
|
||||
Assert.That(source, Does.Not.Contain(Fighter3DStagePrefabPath));
|
||||
Assert.That(source, Does.Not.Contain("OnStagePrefabLoaded"));
|
||||
Assert.That(source, Does.Not.Contain("FighterStage"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Fighter3DBehaviorConfigs_AreMiniGameScopedAndValid()
|
||||
{
|
||||
string skillJson = ReadAssetText(Fighter3DBehaviorConfigRoot + "/fighter3d_skill.json");
|
||||
string flyObjectJson = ReadAssetText(Fighter3DBehaviorConfigRoot + "/fighter3d_flyobj.json");
|
||||
string buffJson = ReadAssetText(Fighter3DBehaviorConfigRoot + "/fighter3d_buff.json");
|
||||
|
||||
BehaviorConfigCatalog catalog = BehaviorConfigCatalog.LoadFromJson("fighter3d", skillJson, flyObjectJson, buffJson);
|
||||
|
||||
Assert.That(catalog.Type, Is.EqualTo("fighter3d"));
|
||||
Assert.That(catalog.Behaviors.ContainsKey("fighter3d_light"), Is.True);
|
||||
Assert.That(catalog.Behaviors.ContainsKey("fighter3d_heavy"), Is.True);
|
||||
Assert.That(catalog.FlyObjects.ContainsKey("fighter3d_blade_wave"), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Fighter3DUiJson_PassesPrefabBuilderValidation()
|
||||
{
|
||||
ValidationResult result = JsonUIPrefabBuilder.Validate(Fighter3DJsonPath);
|
||||
|
||||
Assert.That(result.Ok, Is.True, string.Join("\n", result.Errors));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Fighter3DPrefab_ProvidesHudBindings()
|
||||
{
|
||||
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(Fighter3DPrefabPath);
|
||||
|
||||
Assert.That(prefab, Is.Not.Null);
|
||||
Assert.That(FindComponent<TextMeshProUGUI>(prefab.transform, "Txt_Title"), Is.Not.Null);
|
||||
Assert.That(FindComponent<TextMeshProUGUI>(prefab.transform, "Txt_Left"), Is.Not.Null);
|
||||
Assert.That(FindComponent<TextMeshProUGUI>(prefab.transform, "Txt_Right"), Is.Not.Null);
|
||||
Image leftHp = FindComponent<Image>(prefab.transform, "Img_LeftHpFill");
|
||||
Image rightHp = FindComponent<Image>(prefab.transform, "Img_RightHpFill");
|
||||
|
||||
Assert.That(leftHp, Is.Not.Null);
|
||||
Assert.That(rightHp, Is.Not.Null);
|
||||
Assert.That(leftHp.type, Is.EqualTo(Image.Type.Filled));
|
||||
Assert.That(leftHp.fillMethod, Is.EqualTo(Image.FillMethod.Horizontal));
|
||||
Assert.That(rightHp.type, Is.EqualTo(Image.Type.Filled));
|
||||
Assert.That(rightHp.fillMethod, Is.EqualTo(Image.FillMethod.Horizontal));
|
||||
}
|
||||
|
||||
private static GameObject CreateLobbyInstance()
|
||||
{
|
||||
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(PrefabPath);
|
||||
Assert.That(prefab, Is.Not.Null);
|
||||
|
||||
return Object.Instantiate(prefab);
|
||||
}
|
||||
|
||||
private static RectTransform FindRect(Transform root, string name)
|
||||
{
|
||||
Transform child = root.FindTransform(name);
|
||||
Assert.That(child, Is.Not.Null);
|
||||
|
||||
RectTransform rect = child.GetComponent<RectTransform>();
|
||||
Assert.That(rect, Is.Not.Null);
|
||||
return rect;
|
||||
}
|
||||
|
||||
private static MethodInfo FindGameListPollMethod()
|
||||
{
|
||||
MethodInfo method = typeof(CSharpClientApp).GetMethod(
|
||||
"ShouldPollGameList",
|
||||
BindingFlags.Static | BindingFlags.NonPublic);
|
||||
Assert.That(method, Is.Not.Null);
|
||||
return method;
|
||||
}
|
||||
|
||||
private static T FindComponent<T>(Transform root, string name) where T : Component
|
||||
{
|
||||
Transform child = root.FindTransform(name);
|
||||
return child == null ? null : child.GetComponent<T>();
|
||||
}
|
||||
|
||||
private static string ReadAssetText(string assetPath)
|
||||
{
|
||||
TextAsset asset = AssetDatabase.LoadAssetAtPath<TextAsset>(assetPath);
|
||||
Assert.That(asset, Is.Not.Null, assetPath);
|
||||
return asset.text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e7a70267c2bce264fb04b0eb9dd0db99
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -14,6 +14,7 @@ namespace XGame.ActorBehavior
|
||||
public static string GetControllerPath(string catalogType)
|
||||
{
|
||||
return string.Equals(catalogType, LobbyActorBehaviorRuntime.DefaultCatalogType, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(catalogType, "fighter3d", StringComparison.OrdinalIgnoreCase)
|
||||
? "Assets/Game/Art/Actor/Controller/fighter.controller"
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -16,18 +16,23 @@ namespace XGame.ActorBehavior
|
||||
public const string ConfigRoot = "Assets/Game/Config/ActorBehavior";
|
||||
|
||||
public static IEnumerator LoadCatalog(string type, Action<BehaviorConfigCatalog> onLoaded)
|
||||
{
|
||||
yield return LoadCatalogFromRoot(type, ConfigRoot, onLoaded);
|
||||
}
|
||||
|
||||
public static IEnumerator LoadCatalogFromRoot(string type, string configRoot, Action<BehaviorConfigCatalog> onLoaded)
|
||||
{
|
||||
string skillJson = null;
|
||||
string flyObjectJson = null;
|
||||
string buffJson = null;
|
||||
|
||||
yield return LoadText(GetSkillPath(type), text => skillJson = text);
|
||||
yield return LoadText(GetFlyObjectPath(type), text => flyObjectJson = text);
|
||||
yield return LoadText(GetBuffPath(type), text => buffJson = text);
|
||||
yield return LoadText(GetSkillPath(type, configRoot), text => skillJson = text);
|
||||
yield return LoadText(GetFlyObjectPath(type, configRoot), text => flyObjectJson = text);
|
||||
yield return LoadText(GetBuffPath(type, configRoot), text => buffJson = text);
|
||||
|
||||
if (string.IsNullOrEmpty(skillJson) || string.IsNullOrEmpty(flyObjectJson) || string.IsNullOrEmpty(buffJson))
|
||||
{
|
||||
Debug.LogError("[ActorBehaviorConfigLoader] missing behavior config for type=" + type);
|
||||
Debug.LogError("[ActorBehaviorConfigLoader] missing behavior config for type=" + type + ", root=" + configRoot);
|
||||
onLoaded?.Invoke(null);
|
||||
yield break;
|
||||
}
|
||||
@@ -48,16 +53,38 @@ namespace XGame.ActorBehavior
|
||||
return ConfigRoot + "/" + type + "_skill.json";
|
||||
}
|
||||
|
||||
public static string GetSkillPath(string type, string configRoot)
|
||||
{
|
||||
return NormalizeRoot(configRoot) + "/" + type + "_skill.json";
|
||||
}
|
||||
|
||||
public static string GetFlyObjectPath(string type)
|
||||
{
|
||||
return ConfigRoot + "/" + type + "_flyobj.json";
|
||||
}
|
||||
|
||||
public static string GetFlyObjectPath(string type, string configRoot)
|
||||
{
|
||||
return NormalizeRoot(configRoot) + "/" + type + "_flyobj.json";
|
||||
}
|
||||
|
||||
public static string GetBuffPath(string type)
|
||||
{
|
||||
return ConfigRoot + "/" + type + "_buff.json";
|
||||
}
|
||||
|
||||
public static string GetBuffPath(string type, string configRoot)
|
||||
{
|
||||
return NormalizeRoot(configRoot) + "/" + type + "_buff.json";
|
||||
}
|
||||
|
||||
private static string NormalizeRoot(string configRoot)
|
||||
{
|
||||
return string.IsNullOrEmpty(configRoot)
|
||||
? ConfigRoot
|
||||
: configRoot.TrimEnd('/', '\\');
|
||||
}
|
||||
|
||||
private static IEnumerator LoadText(string assetPath, Action<string> onLoaded)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace XGame
|
||||
private const string LoginPlayerIdKey = "LoginPlayerId";
|
||||
private const string LocalClientIdKey = "LocalClientId";
|
||||
private const double TokenValidSeconds = 7 * 24 * 60 * 60;
|
||||
private const float LobbyGameListRefreshIntervalSeconds = 3f;
|
||||
|
||||
public static CSharpClientApp Instance { get; private set; }
|
||||
|
||||
@@ -55,6 +56,7 @@ namespace XGame
|
||||
private string matchMessage = string.Empty;
|
||||
private string pendingGameId = string.Empty;
|
||||
private int pendingGameVersion;
|
||||
private float nextGameListRefreshTime;
|
||||
private float nextLobbyMoveFrameLogTime;
|
||||
private int m_pid;//当前已鉴权的玩家 id(来自登录/注册返回的 token)
|
||||
|
||||
@@ -113,6 +115,11 @@ namespace XGame
|
||||
if (lobbyActive)
|
||||
{
|
||||
lobbyNet?.Pump();
|
||||
float now = Time.unscaledTime;
|
||||
if (ShouldPollGameList(lobbyActive, lobbyNet != null && lobbyNet.IsConnected, matchRequestActive, now, nextGameListRefreshTime))
|
||||
{
|
||||
RequestGameList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,9 +442,13 @@ namespace XGame
|
||||
RefreshLobby();
|
||||
return;
|
||||
}
|
||||
nextGameListRefreshTime = Time.unscaledTime + LobbyGameListRefreshIntervalSeconds;
|
||||
lobbyNet.SendFramework(FrameworkOpcode.GameListRequest, Array.Empty<byte>());
|
||||
}
|
||||
|
||||
private static bool ShouldPollGameList(bool isLobbyActive, bool hasConnectedLobbyNet, bool isMatchRequestActive, float now, float nextPollTime)
|
||||
=> isLobbyActive && hasConnectedLobbyNet && !isMatchRequestActive && now >= nextPollTime;
|
||||
|
||||
private void RefreshLobby()
|
||||
{
|
||||
if (lobbyController == null)
|
||||
|
||||
@@ -0,0 +1,625 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using XGame.ActorBehavior;
|
||||
using XWorld.Framework.ActorBehavior;
|
||||
using UObject = UnityEngine.Object;
|
||||
|
||||
namespace XGame.MiniGame
|
||||
{
|
||||
public struct MiniGameActorFrame
|
||||
{
|
||||
public int ActorId;
|
||||
public int Figure;
|
||||
public int TeamId;
|
||||
public Vector3 Position;
|
||||
public Vector3 Forward;
|
||||
public bool Moving;
|
||||
public bool Alive;
|
||||
public string TriggerKey;
|
||||
public string BehaviorId;
|
||||
}
|
||||
|
||||
public sealed class MiniGameActorPresentation
|
||||
{
|
||||
private sealed class ActorAvatar
|
||||
{
|
||||
public int ActorId;
|
||||
public int Figure;
|
||||
public int TeamId;
|
||||
public GameObject Obj;
|
||||
public Animator Animator;
|
||||
public Vector3 Target;
|
||||
public bool Moving;
|
||||
public bool Alive;
|
||||
public bool IsPlaceholder;
|
||||
public bool HasPosition;
|
||||
public string AnimState;
|
||||
public string LastTriggerKey;
|
||||
public Vector3 LastForward = Vector3.forward;
|
||||
}
|
||||
|
||||
private sealed class CoroutineHost : MonoBehaviour
|
||||
{
|
||||
}
|
||||
|
||||
private const string CharacterConfigResourcePath = "Config/CharacterConfig";
|
||||
private const string FighterStandState = "140_stand01_qt";
|
||||
private const string FighterWalkState = "140_walk01_qt";
|
||||
private const string LegacyStandState = "Stand";
|
||||
private const string LegacyWalkState = "Walk";
|
||||
private const float MoveSpeed = 12f;
|
||||
private const float StopDistance = 0.04f;
|
||||
|
||||
private readonly Dictionary<int, ActorAvatar> _avatars = new Dictionary<int, ActorAvatar>();
|
||||
private readonly Dictionary<int, GameObject> _prefabCache = new Dictionary<int, GameObject>();
|
||||
private readonly HashSet<int> _prefabLoading = new HashSet<int>();
|
||||
private readonly string _catalogType;
|
||||
private readonly string _behaviorConfigRoot;
|
||||
private readonly GameObject _root;
|
||||
private readonly Transform _actorRoot;
|
||||
private readonly CoroutineHost _coroutineHost;
|
||||
private readonly ActorBehaviorPresentation _behaviorPresentation;
|
||||
private readonly BehaviorWorld _behaviorWorld;
|
||||
private CharacterConfigData _characterConfig;
|
||||
private bool _catalogLoading;
|
||||
private bool _catalogLoaded;
|
||||
private bool _disposed;
|
||||
|
||||
private MiniGameActorPresentation(string rootName, string catalogType, string behaviorConfigRoot)
|
||||
{
|
||||
_catalogType = catalogType;
|
||||
_behaviorConfigRoot = behaviorConfigRoot;
|
||||
_root = new GameObject(string.IsNullOrEmpty(rootName) ? "MiniGameActorPresentation" : rootName);
|
||||
UObject.DontDestroyOnLoad(_root);
|
||||
|
||||
_coroutineHost = _root.AddComponent<CoroutineHost>();
|
||||
_behaviorPresentation = _root.AddComponent<ActorBehaviorPresentation>();
|
||||
_behaviorWorld = new BehaviorWorld(new BehaviorWorldOptions
|
||||
{
|
||||
Mode = BehaviorRunMode.ClientPresentation
|
||||
});
|
||||
|
||||
_actorRoot = new GameObject("Actors").transform;
|
||||
_actorRoot.SetParent(_root.transform, false);
|
||||
EnsureScenePresentation();
|
||||
EnsureCharacterConfig();
|
||||
EnsureBehaviorCatalog();
|
||||
}
|
||||
|
||||
public static MiniGameActorPresentation Create(string rootName, string catalogType, string behaviorConfigRoot)
|
||||
{
|
||||
if (string.IsNullOrEmpty(catalogType))
|
||||
{
|
||||
throw new ArgumentException("catalogType is required", nameof(catalogType));
|
||||
}
|
||||
|
||||
return new MiniGameActorPresentation(rootName, catalogType, behaviorConfigRoot);
|
||||
}
|
||||
|
||||
public void RenderActor(MiniGameActorFrame frame)
|
||||
{
|
||||
if (_disposed || frame.ActorId <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int figure = frame.Figure <= 0 ? 1 : frame.Figure;
|
||||
ActorAvatar avatar = GetOrCreateAvatar(frame.ActorId, figure);
|
||||
if (avatar == null || avatar.Obj == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
avatar.TeamId = frame.TeamId;
|
||||
avatar.Alive = frame.Alive;
|
||||
avatar.Target = frame.Position;
|
||||
avatar.Moving = frame.Moving;
|
||||
Vector3 forward = ResolveForward(frame.Forward, avatar.LastForward);
|
||||
avatar.LastForward = forward;
|
||||
|
||||
if (!avatar.HasPosition)
|
||||
{
|
||||
avatar.Obj.transform.position = frame.Position;
|
||||
avatar.Target = frame.Position;
|
||||
avatar.HasPosition = true;
|
||||
}
|
||||
|
||||
avatar.Obj.transform.forward = forward;
|
||||
RegisterOrUpdateActor(avatar);
|
||||
StartBehaviorIfNeeded(avatar, frame.BehaviorId, frame.TriggerKey, forward);
|
||||
SetMoveState(avatar, IsAvatarMoving(avatar));
|
||||
}
|
||||
|
||||
public void Tick(float deltaTime)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float step = Mathf.Max(MoveSpeed * Mathf.Max(deltaTime, 0f), 0.01f);
|
||||
foreach (ActorAvatar avatar in _avatars.Values)
|
||||
{
|
||||
if (avatar == null || avatar.Obj == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector3 before = avatar.Obj.transform.position;
|
||||
avatar.Obj.transform.position = Vector3.MoveTowards(before, avatar.Target, step);
|
||||
RegisterOrUpdateActor(avatar);
|
||||
if (!_behaviorWorld.HasActiveBehavior(avatar.ActorId))
|
||||
{
|
||||
SetMoveState(avatar, IsAvatarMoving(avatar));
|
||||
}
|
||||
}
|
||||
|
||||
_behaviorWorld.Tick(deltaTime);
|
||||
DrainBehaviorEvents();
|
||||
}
|
||||
|
||||
public void RemoveActor(int actorId)
|
||||
{
|
||||
if (!_avatars.TryGetValue(actorId, out ActorAvatar avatar))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_behaviorPresentation.UnregisterActor(actorId, true);
|
||||
_behaviorWorld.RemoveActor(actorId);
|
||||
DestroyUnityObject(avatar.Obj);
|
||||
_avatars.Remove(actorId);
|
||||
}
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
_avatars.Clear();
|
||||
DestroyUnityObject(_root);
|
||||
}
|
||||
|
||||
private ActorAvatar GetOrCreateAvatar(int actorId, int figure)
|
||||
{
|
||||
if (_avatars.TryGetValue(actorId, out ActorAvatar avatar))
|
||||
{
|
||||
if (avatar.Figure == figure)
|
||||
{
|
||||
return avatar;
|
||||
}
|
||||
|
||||
RemoveActor(actorId);
|
||||
}
|
||||
|
||||
GameObject obj = InstantiateAvatar(figure, out bool isPlaceholder);
|
||||
avatar = new ActorAvatar
|
||||
{
|
||||
ActorId = actorId,
|
||||
Figure = figure,
|
||||
TeamId = actorId,
|
||||
Obj = obj,
|
||||
Animator = obj != null ? obj.GetComponentInChildren<Animator>() : null,
|
||||
Target = obj != null ? obj.transform.position : Vector3.zero,
|
||||
Alive = true,
|
||||
IsPlaceholder = isPlaceholder
|
||||
};
|
||||
if (obj != null)
|
||||
{
|
||||
obj.name = "MiniGameActor_" + actorId + "_F" + figure;
|
||||
_behaviorPresentation.RegisterActor(actorId, avatar.Animator, obj.transform, obj.transform);
|
||||
ApplyActorBehaviorAnimatorController(avatar);
|
||||
}
|
||||
_avatars[actorId] = avatar;
|
||||
return avatar;
|
||||
}
|
||||
|
||||
private GameObject InstantiateAvatar(int figure, out bool isPlaceholder)
|
||||
{
|
||||
GameObject prefab = GetCachedPrefabOrLoadAsync(figure);
|
||||
isPlaceholder = prefab == null;
|
||||
GameObject obj = prefab != null
|
||||
? UObject.Instantiate(prefab, _actorRoot)
|
||||
: GameObject.CreatePrimitive(PrimitiveType.Capsule);
|
||||
if (prefab == null)
|
||||
{
|
||||
obj.transform.SetParent(_actorRoot, false);
|
||||
obj.transform.localScale = new Vector3(0.7f, 1f, 0.7f);
|
||||
}
|
||||
obj.SetActive(true);
|
||||
ConfigureAvatarRenderers(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
private GameObject GetCachedPrefabOrLoadAsync(int figure)
|
||||
{
|
||||
if (_prefabCache.TryGetValue(figure, out GameObject prefab) && prefab != null)
|
||||
{
|
||||
return prefab;
|
||||
}
|
||||
|
||||
if (_prefabLoading.Add(figure))
|
||||
{
|
||||
_coroutineHost.StartCoroutine(CoLoadActorPrefab(figure));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private IEnumerator CoLoadActorPrefab(int figure)
|
||||
{
|
||||
string prefabPath = GetPrefabPath(figure);
|
||||
if (string.IsNullOrEmpty(prefabPath))
|
||||
{
|
||||
_prefabLoading.Remove(figure);
|
||||
Debug.LogError("[MiniGameActorPresentation] missing prefab path for figure=" + figure);
|
||||
yield break;
|
||||
}
|
||||
|
||||
UObject loaded = null;
|
||||
yield return XResLoader.coLoadRes(prefabPath, typeof(GameObject), obj => loaded = obj);
|
||||
_prefabLoading.Remove(figure);
|
||||
GameObject prefab = loaded as GameObject;
|
||||
if (prefab == null)
|
||||
{
|
||||
Debug.LogWarning("[MiniGameActorPresentation] actor prefab load failed: " + prefabPath);
|
||||
yield break;
|
||||
}
|
||||
|
||||
_prefabCache[figure] = prefab;
|
||||
UpgradePlaceholders(figure, prefab);
|
||||
}
|
||||
|
||||
private void UpgradePlaceholders(int figure, GameObject prefab)
|
||||
{
|
||||
foreach (ActorAvatar avatar in _avatars.Values)
|
||||
{
|
||||
if (avatar == null || avatar.Figure != figure || !avatar.IsPlaceholder || avatar.Obj == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector3 position = avatar.Obj.transform.position;
|
||||
Quaternion rotation = avatar.Obj.transform.rotation;
|
||||
string objectName = avatar.Obj.name;
|
||||
DestroyUnityObject(avatar.Obj);
|
||||
avatar.Obj = UObject.Instantiate(prefab, _actorRoot);
|
||||
avatar.Obj.name = objectName;
|
||||
avatar.Obj.transform.position = position;
|
||||
avatar.Obj.transform.rotation = rotation;
|
||||
avatar.Obj.SetActive(true);
|
||||
ConfigureAvatarRenderers(avatar.Obj);
|
||||
avatar.Animator = avatar.Obj.GetComponentInChildren<Animator>();
|
||||
avatar.IsPlaceholder = false;
|
||||
avatar.AnimState = null;
|
||||
_behaviorPresentation.RegisterActor(avatar.ActorId, avatar.Animator, avatar.Obj.transform, avatar.Obj.transform);
|
||||
ApplyActorBehaviorAnimatorController(avatar);
|
||||
SetMoveState(avatar, avatar.Moving);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyActorBehaviorAnimatorController(ActorAvatar avatar)
|
||||
{
|
||||
if (avatar == null || avatar.Obj == null || avatar.Animator == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_coroutineHost.StartCoroutine(ApplyActorBehaviorAnimatorControllerAsync(
|
||||
avatar.ActorId,
|
||||
avatar.Obj,
|
||||
avatar.Animator));
|
||||
}
|
||||
|
||||
private IEnumerator ApplyActorBehaviorAnimatorControllerAsync(int actorId, GameObject actorObject, Animator animator)
|
||||
{
|
||||
bool applied = false;
|
||||
yield return ActorBehaviorAnimatorControllerLoader.ApplyController(_catalogType, animator, value => applied = value);
|
||||
if (!applied || actorObject == null || animator == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (!_avatars.TryGetValue(actorId, out ActorAvatar avatar) || avatar.Obj != actorObject || avatar.Animator != animator)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
avatar.AnimState = null;
|
||||
SetMoveState(avatar, avatar.Moving);
|
||||
}
|
||||
|
||||
private void StartBehaviorIfNeeded(ActorAvatar avatar, string behaviorId, string triggerKey, Vector3 forward)
|
||||
{
|
||||
if (avatar == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(behaviorId))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(triggerKey))
|
||||
{
|
||||
avatar.LastTriggerKey = triggerKey;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(triggerKey) ? behaviorId : triggerKey;
|
||||
if (avatar.LastTriggerKey == key)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
avatar.LastTriggerKey = key;
|
||||
if (!_catalogLoaded || _behaviorWorld.HasActiveBehavior(avatar.ActorId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BehaviorStartResult result = _behaviorWorld.TryStartBehavior(
|
||||
avatar.ActorId,
|
||||
behaviorId,
|
||||
BehaviorInput.FromDirection(forward.x, forward.z));
|
||||
if (!result.Success)
|
||||
{
|
||||
Debug.LogWarning("[MiniGameActorPresentation] start behavior failed actorId=" + avatar.ActorId +
|
||||
", behaviorId=" + behaviorId + ", error=" + result.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterOrUpdateActor(ActorAvatar avatar)
|
||||
{
|
||||
if (avatar == null || avatar.Obj == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_behaviorWorld.AddActor(new BehaviorActorState
|
||||
{
|
||||
ActorId = avatar.ActorId,
|
||||
CatalogType = _catalogType,
|
||||
TeamId = avatar.TeamId,
|
||||
Position = ToBehavior(avatar.Obj.transform.position),
|
||||
Forward = ToBehaviorForward(avatar.Obj.transform.forward),
|
||||
Radius = 0.5f,
|
||||
Alive = avatar.Alive
|
||||
});
|
||||
}
|
||||
|
||||
private void DrainBehaviorEvents()
|
||||
{
|
||||
IReadOnlyList<BehaviorEvent> events = _behaviorWorld.DrainEvents();
|
||||
if (events.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_behaviorPresentation.PlayEvents(events);
|
||||
for (int i = 0; i < events.Count; i++)
|
||||
{
|
||||
BehaviorEvent evt = events[i];
|
||||
if (evt.Kind != BehaviorEventKind.BehaviorStopped)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_avatars.TryGetValue(evt.ActorId, out ActorAvatar avatar))
|
||||
{
|
||||
avatar.AnimState = null;
|
||||
SetMoveState(avatar, avatar.Moving);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureBehaviorCatalog()
|
||||
{
|
||||
if (_catalogLoaded || _catalogLoading)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_catalogLoading = true;
|
||||
_coroutineHost.StartCoroutine(CoLoadBehaviorCatalog());
|
||||
}
|
||||
|
||||
private IEnumerator CoLoadBehaviorCatalog()
|
||||
{
|
||||
BehaviorConfigCatalog catalog = null;
|
||||
yield return ActorBehaviorConfigLoader.LoadCatalogFromRoot(_catalogType, _behaviorConfigRoot, loaded => catalog = loaded);
|
||||
_catalogLoading = false;
|
||||
if (catalog == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
_behaviorWorld.RegisterCatalog(catalog);
|
||||
_catalogLoaded = true;
|
||||
}
|
||||
|
||||
private bool EnsureCharacterConfig()
|
||||
{
|
||||
if (_characterConfig != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
TextAsset asset = Resources.Load<TextAsset>(CharacterConfigResourcePath);
|
||||
if (asset == null)
|
||||
{
|
||||
Debug.LogError("[MiniGameActorPresentation] missing character config resource: " + CharacterConfigResourcePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
_characterConfig = JsonUtility.FromJson<CharacterConfigData>(asset.text);
|
||||
if (_characterConfig == null || _characterConfig.GetDefault() == null)
|
||||
{
|
||||
Debug.LogError("[MiniGameActorPresentation] invalid character config.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private string GetPrefabPath(int figure)
|
||||
{
|
||||
return EnsureCharacterConfig() ? _characterConfig.GetPrefabPath(figure) : null;
|
||||
}
|
||||
|
||||
private static bool IsAvatarMoving(ActorAvatar avatar)
|
||||
{
|
||||
return avatar != null
|
||||
&& (avatar.Moving || Vector3.Distance(avatar.Obj.transform.position, avatar.Target) > StopDistance);
|
||||
}
|
||||
|
||||
private void SetMoveState(ActorAvatar avatar, bool moving)
|
||||
{
|
||||
if (avatar == null || avatar.Animator == null || _behaviorWorld.HasActiveBehavior(avatar.ActorId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string state = ResolveMoveState(avatar.Animator, moving);
|
||||
if (string.IsNullOrEmpty(state) || avatar.AnimState == state)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
avatar.AnimState = state;
|
||||
avatar.Animator.CrossFade(state, 0.1f);
|
||||
}
|
||||
|
||||
private static string ResolveMoveState(Animator animator, bool moving)
|
||||
{
|
||||
string fighterState = moving ? FighterWalkState : FighterStandState;
|
||||
if (HasAnimatorState(animator, fighterState))
|
||||
{
|
||||
return fighterState;
|
||||
}
|
||||
|
||||
string legacyState = moving ? LegacyWalkState : LegacyStandState;
|
||||
return HasAnimatorState(animator, legacyState) ? legacyState : null;
|
||||
}
|
||||
|
||||
private static bool HasAnimatorState(Animator animator, string state)
|
||||
{
|
||||
return animator != null
|
||||
&& animator.runtimeAnimatorController != null
|
||||
&& animator.HasState(0, Animator.StringToHash(state));
|
||||
}
|
||||
|
||||
private void EnsureScenePresentation()
|
||||
{
|
||||
GameObject lightObject = new GameObject("MiniGameDirectionalLight");
|
||||
lightObject.transform.SetParent(_root.transform, false);
|
||||
lightObject.transform.rotation = Quaternion.Euler(45f, -35f, 0f);
|
||||
Light light = lightObject.AddComponent<Light>();
|
||||
light.type = LightType.Directional;
|
||||
light.intensity = 1.15f;
|
||||
|
||||
GameObject cameraObject = new GameObject("MiniGameCamera");
|
||||
cameraObject.transform.SetParent(_root.transform, false);
|
||||
cameraObject.transform.position = new Vector3(0f, 4.2f, -6.2f);
|
||||
cameraObject.transform.rotation = Quaternion.Euler(31f, 0f, 0f);
|
||||
Camera camera = cameraObject.AddComponent<Camera>();
|
||||
camera.clearFlags = CameraClearFlags.SolidColor;
|
||||
camera.backgroundColor = new Color(0.06f, 0.075f, 0.09f, 1f);
|
||||
camera.fieldOfView = 42f;
|
||||
camera.nearClipPlane = 0.1f;
|
||||
camera.farClipPlane = 80f;
|
||||
|
||||
GameObject ground = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
||||
ground.name = "MiniGameGround";
|
||||
ground.transform.SetParent(_root.transform, false);
|
||||
ground.transform.localPosition = new Vector3(0f, -0.05f, 0f);
|
||||
ground.transform.localScale = new Vector3(7.5f, 0.1f, 2.8f);
|
||||
Renderer renderer = ground.GetComponent<Renderer>();
|
||||
if (renderer != null)
|
||||
{
|
||||
renderer.sharedMaterial = CreateRuntimeMaterial(new Color(0.16f, 0.18f, 0.19f, 1f));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConfigureAvatarRenderers(GameObject obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Renderer[] renderers = obj.GetComponentsInChildren<Renderer>(true);
|
||||
for (int i = 0; i < renderers.Length; i++)
|
||||
{
|
||||
Renderer renderer = renderers[i];
|
||||
if (renderer == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
renderer.lightProbeUsage = LightProbeUsage.BlendProbes;
|
||||
renderer.reflectionProbeUsage = ReflectionProbeUsage.BlendProbes;
|
||||
}
|
||||
}
|
||||
|
||||
private static Material CreateRuntimeMaterial(Color color)
|
||||
{
|
||||
Shader shader = Shader.Find("Universal Render Pipeline/Lit");
|
||||
if (shader == null)
|
||||
{
|
||||
shader = Shader.Find("Standard");
|
||||
}
|
||||
|
||||
var material = new Material(shader);
|
||||
material.color = color;
|
||||
return material;
|
||||
}
|
||||
|
||||
private static Vector3 ResolveForward(Vector3 forward, Vector3 fallback)
|
||||
{
|
||||
forward.y = 0f;
|
||||
if (forward.sqrMagnitude <= 0.0001f)
|
||||
{
|
||||
forward = fallback;
|
||||
}
|
||||
forward.y = 0f;
|
||||
if (forward.sqrMagnitude <= 0.0001f)
|
||||
{
|
||||
forward = Vector3.forward;
|
||||
}
|
||||
return forward.normalized;
|
||||
}
|
||||
|
||||
private static BehaviorVector3 ToBehavior(Vector3 value)
|
||||
{
|
||||
return new BehaviorVector3(value.x, value.y, value.z);
|
||||
}
|
||||
|
||||
private static BehaviorVector3 ToBehaviorForward(Vector3 value)
|
||||
{
|
||||
Vector3 forward = ResolveForward(value, Vector3.forward);
|
||||
return new BehaviorVector3(forward.x, 0f, forward.z);
|
||||
}
|
||||
|
||||
private static void DestroyUnityObject(UObject obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
UObject.Destroy(obj);
|
||||
}
|
||||
else
|
||||
{
|
||||
UObject.DestroyImmediate(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 61b12bbed7cddf446a576ec50f6a915c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -30,7 +30,7 @@ namespace XGame
|
||||
private const float LobbyPanelWidth = 520f;
|
||||
private const float LobbyPanelHeight = 296f;
|
||||
private const float LobbyPanelLeft = 24f;
|
||||
private const float LobbyPanelTop = -24f;
|
||||
private const float LobbyPanelMiddleY = 0f;
|
||||
private const float CollapsedPanelWidth = 56f;
|
||||
|
||||
private void Awake()
|
||||
@@ -208,10 +208,10 @@ namespace XGame
|
||||
buttonGo.transform.SetParent(parent, false);
|
||||
|
||||
RectTransform rt = buttonGo.GetComponent<RectTransform>();
|
||||
rt.anchorMin = new Vector2(0f, 1f);
|
||||
rt.anchorMax = new Vector2(0f, 1f);
|
||||
rt.pivot = new Vector2(0f, 1f);
|
||||
rt.anchoredPosition = new Vector2(LobbyPanelLeft + LobbyPanelWidth, LobbyPanelTop);
|
||||
rt.anchorMin = new Vector2(0f, 0.5f);
|
||||
rt.anchorMax = new Vector2(0f, 0.5f);
|
||||
rt.pivot = new Vector2(0f, 0.5f);
|
||||
rt.anchoredPosition = new Vector2(LobbyPanelLeft + LobbyPanelWidth, LobbyPanelMiddleY);
|
||||
rt.sizeDelta = new Vector2(48f, 72f);
|
||||
|
||||
Image img = buttonGo.GetComponent<Image>();
|
||||
@@ -254,18 +254,18 @@ namespace XGame
|
||||
|
||||
if (panelLobby != null)
|
||||
{
|
||||
panelLobby.anchorMin = new Vector2(0f, 1f);
|
||||
panelLobby.anchorMax = new Vector2(0f, 1f);
|
||||
panelLobby.pivot = new Vector2(0f, 1f);
|
||||
panelLobby.anchoredPosition = new Vector2(LobbyPanelLeft, LobbyPanelTop);
|
||||
panelLobby.anchorMin = new Vector2(0f, 0.5f);
|
||||
panelLobby.anchorMax = new Vector2(0f, 0.5f);
|
||||
panelLobby.pivot = new Vector2(0f, 0.5f);
|
||||
panelLobby.anchoredPosition = new Vector2(LobbyPanelLeft, LobbyPanelMiddleY);
|
||||
panelLobby.sizeDelta = new Vector2(LobbyPanelWidth, LobbyPanelHeight);
|
||||
}
|
||||
if (collapseButtonRect != null)
|
||||
{
|
||||
collapseButtonRect.anchorMin = new Vector2(0f, 1f);
|
||||
collapseButtonRect.anchorMax = new Vector2(0f, 1f);
|
||||
collapseButtonRect.pivot = new Vector2(0f, 1f);
|
||||
collapseButtonRect.anchoredPosition = new Vector2(LobbyPanelLeft + LobbyPanelWidth, LobbyPanelTop);
|
||||
collapseButtonRect.anchorMin = new Vector2(0f, 0.5f);
|
||||
collapseButtonRect.anchorMax = new Vector2(0f, 0.5f);
|
||||
collapseButtonRect.pivot = new Vector2(0f, 0.5f);
|
||||
collapseButtonRect.anchoredPosition = new Vector2(LobbyPanelLeft + LobbyPanelWidth, LobbyPanelMiddleY);
|
||||
collapseButtonRect.sizeDelta = new Vector2(48f, 72f);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user