From e9efa8a56784a52f07c092a7815c64388e0e917a Mon Sep 17 00:00:00 2001 From: ud18010 Date: Thu, 30 Jul 2026 17:00:31 +0800 Subject: [PATCH] feat: trigger fighter behavior from lobby action button --- .../Shared/ActorBehavior/BehaviorWorld.cs | 43 +++ .../LobbyActorBehaviorRuntimeTests.cs | 102 +++++++ .../LobbyActorBehaviorRuntimeTests.cs.meta | 11 + .../ActorBehaviorConfigLoader.cs | 89 ++++++ .../ActorBehaviorConfigLoader.cs.meta | 11 + .../LobbyActorBehaviorRuntime.cs | 114 +++++++ .../LobbyActorBehaviorRuntime.cs.meta | 11 + .../xmain/Client/LobbyWorldController.cs | 277 ++++++++++++++++++ .../ActorBehavior/BehaviorWorldTests.cs | 16 + 9 files changed, 674 insertions(+) create mode 100644 Client/Assets/Script/Tests/EditMode/LobbyActorBehaviorRuntimeTests.cs create mode 100644 Client/Assets/Script/Tests/EditMode/LobbyActorBehaviorRuntimeTests.cs.meta create mode 100644 Client/Assets/Script/xmain/ActorBehavior/ActorBehaviorConfigLoader.cs create mode 100644 Client/Assets/Script/xmain/ActorBehavior/ActorBehaviorConfigLoader.cs.meta create mode 100644 Client/Assets/Script/xmain/ActorBehavior/LobbyActorBehaviorRuntime.cs create mode 100644 Client/Assets/Script/xmain/ActorBehavior/LobbyActorBehaviorRuntime.cs.meta diff --git a/Client/Assets/Framework/Shared/ActorBehavior/BehaviorWorld.cs b/Client/Assets/Framework/Shared/ActorBehavior/BehaviorWorld.cs index dccffc82..262d0654 100644 --- a/Client/Assets/Framework/Shared/ActorBehavior/BehaviorWorld.cs +++ b/Client/Assets/Framework/Shared/ActorBehavior/BehaviorWorld.cs @@ -33,6 +33,49 @@ namespace XWorld.Framework.ActorBehavior _actors[actor.ActorId] = actor; } + public bool RemoveActor(int actorId) + { + bool removed = _actors.Remove(actorId); + if (_activeBehaviors.TryGetValue(actorId, out ActiveBehavior active)) + { + _activeBehaviors.Remove(actorId); + _events.Add(new BehaviorEvent + { + Kind = BehaviorEventKind.BehaviorStopped, + ActorId = actorId, + BehaviorId = active.Spec.Id, + StopReason = BehaviorStopReason.ForcedSwitch + }); + removed = true; + } + + if (_buffs.Remove(actorId)) + { + removed = true; + } + var buffTargets = new List(_buffs.Keys); + for (int i = 0; i < buffTargets.Count; i++) + { + List activeBuffs = _buffs[buffTargets[i]]; + int before = activeBuffs.Count; + activeBuffs.RemoveAll(b => b.SourceActorId == actorId || b.TargetActorId == actorId); + if (activeBuffs.Count != before) + { + removed = true; + } + if (activeBuffs.Count == 0) + { + _buffs.Remove(buffTargets[i]); + } + } + + if (_projectiles.RemoveAll(p => p.SourceActorId == actorId || p.TargetActorId == actorId) > 0) + { + removed = true; + } + return removed; + } + public bool SwitchActorCatalog(int actorId, string catalogType) { if (!_actors.TryGetValue(actorId, out BehaviorActorState actor)) return false; diff --git a/Client/Assets/Script/Tests/EditMode/LobbyActorBehaviorRuntimeTests.cs b/Client/Assets/Script/Tests/EditMode/LobbyActorBehaviorRuntimeTests.cs new file mode 100644 index 00000000..96ee0a2f --- /dev/null +++ b/Client/Assets/Script/Tests/EditMode/LobbyActorBehaviorRuntimeTests.cs @@ -0,0 +1,102 @@ +using System.Linq; +using NUnit.Framework; +using UnityEngine; +using XGame.ActorBehavior; +using XGame.VirtualInput; +using XWorld.Framework.ActorBehavior; + +public sealed class LobbyActorBehaviorRuntimeTests +{ + [Test] + public void RegisterSelfActor_DefaultsCatalogToFighter() + { + var runtime = new LobbyActorBehaviorRuntime(); + runtime.RegisterCatalog(CreateFighterCatalog()); + + runtime.RegisterOrUpdateActor(10, Vector3.zero, Vector3.forward); + + Assert.That(runtime.SwitchActorCatalog(10, LobbyActorBehaviorRuntime.DefaultCatalogType), Is.True); + } + + [Test] + public void Skill1Tap_StartsQuantao01AndEmitsPresentationEvents() + { + var runtime = new LobbyActorBehaviorRuntime(); + runtime.RegisterCatalog(CreateFighterCatalog()); + runtime.RegisterOrUpdateActor(10, Vector3.zero, Vector3.forward); + + BehaviorStartResult result = runtime.HandleActionButton( + CreateButtonEvent(ActionButtonId.Skill1, ActionButtonPhase.Tap, Vector2.zero), + 10, + Vector3.forward); + + BehaviorEvent[] events = runtime.DrainEvents().ToArray(); + Assert.That(result.Success, Is.True); + Assert.That(events.Any(e => e.Kind == BehaviorEventKind.TimelineAnimation && e.Animation == "140_skill01a_qt"), Is.True); + Assert.That(events.Any(e => e.Kind == BehaviorEventKind.TimelineEffect && e.Prefab.Contains("9110004_zhujue_quantao_skill01a")), Is.True); + } + + [Test] + public void PrimaryTap_DoesNotStartQuantao01() + { + var runtime = new LobbyActorBehaviorRuntime(); + runtime.RegisterCatalog(CreateFighterCatalog()); + runtime.RegisterOrUpdateActor(10, Vector3.zero, Vector3.forward); + + BehaviorStartResult result = runtime.HandleActionButton( + CreateButtonEvent(ActionButtonId.Primary, ActionButtonPhase.Tap, Vector2.zero), + 10, + Vector3.forward); + + Assert.That(result.Success, Is.False); + Assert.That(result.Error, Is.EqualTo("button ignored")); + Assert.That(runtime.DrainEvents().Any(e => e.BehaviorId == LobbyActorBehaviorRuntime.SecondaryABehaviorId), Is.False); + } + + [Test] + public void Skill1Release_DoesNotStartQuantao01Again() + { + var runtime = new LobbyActorBehaviorRuntime(); + runtime.RegisterCatalog(CreateFighterCatalog()); + runtime.RegisterOrUpdateActor(10, Vector3.zero, Vector3.forward); + + BehaviorStartResult result = runtime.HandleActionButton( + CreateButtonEvent(ActionButtonId.Skill1, ActionButtonPhase.Release, Vector2.right), + 10, + Vector3.forward); + + Assert.That(result.Success, Is.False); + Assert.That(result.Error, Is.EqualTo("button ignored")); + Assert.That(runtime.DrainEvents().Any(e => e.BehaviorId == LobbyActorBehaviorRuntime.SecondaryABehaviorId), Is.False); + } + + private static ActionButtonEvent CreateButtonEvent(ActionButtonId buttonId, ActionButtonPhase phase, Vector2 direction) + { + return new ActionButtonEvent(buttonId, phase, Vector2.zero, Vector2.zero, direction, direction.magnitude, 0f, false, 1); + } + + private static BehaviorConfigCatalog CreateFighterCatalog() + { + return BehaviorConfigCatalog.LoadFromJson( + "fighter", + @"{ + ""type"": ""fighter"", + ""version"": 1, + ""behaviors"": [ + { + ""id"": ""quantao01"", + ""duration"": 1.0, + ""canBeInterrupted"": true, + ""input"": ""DirectionOrTarget"", + ""timeline"": [ + { ""time"": 0.0, ""kind"": ""Animation"", ""animation"": ""140_skill01a_qt"", ""duration"": 1.0 }, + { ""time"": 0.0, ""kind"": ""Effect"", ""prefab"": ""Assets/Game/Art/Effect/Prefab/skill/9110004_zhujue_quantao_skill01a.prefab"", ""duration"": 1.0 } + ], + ""logicEffects"": [] + } + ] +}", + @"{ ""type"": ""fighter"", ""version"": 1, ""flyObjects"": [] }", + @"{ ""type"": ""fighter"", ""version"": 1, ""buffs"": [] }"); + } +} diff --git a/Client/Assets/Script/Tests/EditMode/LobbyActorBehaviorRuntimeTests.cs.meta b/Client/Assets/Script/Tests/EditMode/LobbyActorBehaviorRuntimeTests.cs.meta new file mode 100644 index 00000000..f27c996e --- /dev/null +++ b/Client/Assets/Script/Tests/EditMode/LobbyActorBehaviorRuntimeTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d8b2e829e8bf4782996532ed137c607c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Assets/Script/xmain/ActorBehavior/ActorBehaviorConfigLoader.cs b/Client/Assets/Script/xmain/ActorBehavior/ActorBehaviorConfigLoader.cs new file mode 100644 index 00000000..f3121057 --- /dev/null +++ b/Client/Assets/Script/xmain/ActorBehavior/ActorBehaviorConfigLoader.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections; +using System.IO; +using System.Text; +using UnityEngine; +using XWorld.Framework.ActorBehavior; + +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace XGame.ActorBehavior +{ + public static class ActorBehaviorConfigLoader + { + public const string ConfigRoot = "Assets/Game/Config/ActorBehavior"; + + public static IEnumerator LoadCatalog(string type, Action 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); + + if (string.IsNullOrEmpty(skillJson) || string.IsNullOrEmpty(flyObjectJson) || string.IsNullOrEmpty(buffJson)) + { + Debug.LogError("[ActorBehaviorConfigLoader] missing behavior config for type=" + type); + onLoaded?.Invoke(null); + yield break; + } + + try + { + onLoaded?.Invoke(BehaviorConfigCatalog.LoadFromJson(type, skillJson, flyObjectJson, buffJson)); + } + catch (Exception ex) + { + Debug.LogError("[ActorBehaviorConfigLoader] parse behavior config failed type=" + type + ", error=" + ex.Message); + onLoaded?.Invoke(null); + } + } + + public static string GetSkillPath(string type) + { + return ConfigRoot + "/" + type + "_skill.json"; + } + + public static string GetFlyObjectPath(string type) + { + return ConfigRoot + "/" + type + "_flyobj.json"; + } + + public static string GetBuffPath(string type) + { + return ConfigRoot + "/" + type + "_buff.json"; + } + + private static IEnumerator LoadText(string assetPath, Action onLoaded) + { +#if UNITY_EDITOR + TextAsset asset = AssetDatabase.LoadAssetAtPath(assetPath); + if (asset != null) + { + onLoaded?.Invoke(asset.text); + yield break; + } +#endif + + string filePath = Path.Combine(Application.dataPath, assetPath.StartsWith("Assets/", StringComparison.Ordinal) + ? assetPath.Substring("Assets/".Length) + : assetPath); + if (File.Exists(filePath)) + { + onLoaded?.Invoke(File.ReadAllText(filePath, Encoding.UTF8)); + yield break; + } + + byte[] bytes = null; + string runtimePath = assetPath.StartsWith("Assets/", StringComparison.Ordinal) + ? assetPath.Substring("Assets/".Length) + : assetPath; + yield return XResLoader.coLoadFile(runtimePath, data => bytes = data); + onLoaded?.Invoke(bytes != null ? Encoding.UTF8.GetString(bytes) : null); + } + } +} diff --git a/Client/Assets/Script/xmain/ActorBehavior/ActorBehaviorConfigLoader.cs.meta b/Client/Assets/Script/xmain/ActorBehavior/ActorBehaviorConfigLoader.cs.meta new file mode 100644 index 00000000..369ecff9 --- /dev/null +++ b/Client/Assets/Script/xmain/ActorBehavior/ActorBehaviorConfigLoader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 749877ee582c42058dfc794500488f28 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Assets/Script/xmain/ActorBehavior/LobbyActorBehaviorRuntime.cs b/Client/Assets/Script/xmain/ActorBehavior/LobbyActorBehaviorRuntime.cs new file mode 100644 index 00000000..d2d13d80 --- /dev/null +++ b/Client/Assets/Script/xmain/ActorBehavior/LobbyActorBehaviorRuntime.cs @@ -0,0 +1,114 @@ +using System.Collections.Generic; +using UnityEngine; +using XGame.VirtualInput; +using XWorld.Framework.ActorBehavior; + +namespace XGame.ActorBehavior +{ + public sealed class LobbyActorBehaviorRuntime + { + public const string DefaultCatalogType = "fighter"; + public const string SecondaryABehaviorId = "quantao01"; + public const ActionButtonId SecondaryAButtonId = ActionButtonId.Skill1; + + private readonly BehaviorWorld world = new BehaviorWorld(new BehaviorWorldOptions + { + Mode = BehaviorRunMode.ClientPresentation + }); + + public void RegisterCatalog(BehaviorConfigCatalog catalog) + { + world.RegisterCatalog(catalog); + } + + public bool SwitchActorCatalog(int actorId, string catalogType) + { + return world.SwitchActorCatalog(actorId, catalogType); + } + + public bool RemoveActor(int actorId) + { + return world.RemoveActor(actorId); + } + + public void RegisterOrUpdateActor(int actorId, Vector3 position, Vector3 forward) + { + world.AddActor(new BehaviorActorState + { + ActorId = actorId, + CatalogType = DefaultCatalogType, + TeamId = actorId, + Position = ToBehavior(position), + Forward = ToBehaviorForward(forward), + Radius = 0.5f, + Alive = true + }); + } + + public BehaviorStartResult HandleActionButton(ActionButtonEvent evt, int actorId, Vector3 fallbackForward) + { + if (!IsSecondaryATrigger(evt)) + { + return BehaviorStartResult.Fail("button ignored"); + } + + BehaviorInput input = CreateDirectionInput(evt, fallbackForward); + return world.TryStartBehavior(actorId, SecondaryABehaviorId, input); + } + + public void Tick(float deltaTime) + { + world.Tick(deltaTime); + } + + public IReadOnlyList DrainEvents() + { + return world.DrainEvents(); + } + + public bool HasActiveBehavior(int actorId) + { + return world.HasActiveBehavior(actorId); + } + + public static bool IsSecondaryATrigger(ActionButtonEvent evt) + { + return evt.ButtonId == SecondaryAButtonId + && !evt.IsCanceled + && evt.Phase == ActionButtonPhase.Tap; + } + + private static BehaviorInput CreateDirectionInput(ActionButtonEvent evt, Vector3 fallbackForward) + { + Vector2 direction = evt.Direction; + if (direction.sqrMagnitude > 0.0001f) + { + return BehaviorInput.FromDirection(direction.x, direction.y); + } + + fallbackForward.y = 0f; + if (fallbackForward.sqrMagnitude <= 0.0001f) + { + fallbackForward = Vector3.forward; + } + fallbackForward.Normalize(); + return BehaviorInput.FromDirection(fallbackForward.x, fallbackForward.z); + } + + private static BehaviorVector3 ToBehavior(Vector3 value) + { + return new BehaviorVector3(value.x, value.y, value.z); + } + + private static BehaviorVector3 ToBehaviorForward(Vector3 value) + { + value.y = 0f; + if (value.sqrMagnitude <= 0.0001f) + { + value = Vector3.forward; + } + value.Normalize(); + return new BehaviorVector3(value.x, 0f, value.z); + } + } +} diff --git a/Client/Assets/Script/xmain/ActorBehavior/LobbyActorBehaviorRuntime.cs.meta b/Client/Assets/Script/xmain/ActorBehavior/LobbyActorBehaviorRuntime.cs.meta new file mode 100644 index 00000000..2e55503f --- /dev/null +++ b/Client/Assets/Script/xmain/ActorBehavior/LobbyActorBehaviorRuntime.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8f25b35931c34b66982c41c5fd329fe2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Assets/Script/xmain/Client/LobbyWorldController.cs b/Client/Assets/Script/xmain/Client/LobbyWorldController.cs index 9ffb1015..61a84491 100644 --- a/Client/Assets/Script/xmain/Client/LobbyWorldController.cs +++ b/Client/Assets/Script/xmain/Client/LobbyWorldController.cs @@ -5,7 +5,9 @@ using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Rendering; using UnityEngine.UI; +using XGame.ActorBehavior; using XGame.VirtualInput; +using XWorld.Framework.ActorBehavior; using XWorld.Framework.Protocol; using UObject = UnityEngine.Object; @@ -31,6 +33,7 @@ namespace XGame } private const string JoystickPrefabPath = "Assets/Game/Art/UI/Prefab/UI_VirtualJoystick.prefab"; + private const string ActionPadPrefabPath = "Assets/Game/Art/UI/Prefab/UI_VirtualActionPad.prefab"; private const string CharacterConfigResourcePath = "Config/CharacterConfig"; private const string SelectedCharacterKey = "CharacterSwitch.SelectedCharacterId"; private const float MoveSpeed = 1f; @@ -84,10 +87,17 @@ namespace XGame private RectTransform joystickRoot; private RectTransform joystickWidget; // 视觉容器:新预设里的 "Joystick" 子节点(自带定位);找不到则回退为 joystickRoot private VirtualJoystick virtualJoystick; + private RectTransform actionPadRoot; + private VirtualActionPad virtualActionPad; + private LobbyActorBehaviorRuntime actorBehaviorRuntime; + private ActorBehaviorPresentation actorBehaviorPresentation; private RectTransform joystickKnob; private bool joystickMouseCaptured; private int joystickTouchFingerId = -1; private bool joystickLoading; + private bool actionPadLoading; + private bool actorBehaviorCatalogLoading; + private bool actorBehaviorCatalogLoaded; private static int materialLogCount; public void Initialize(Action sendFramework, int playerId, string playerName, int figure) @@ -109,7 +119,10 @@ namespace XGame EnsureWorldRoot(); worldRoot.gameObject.SetActive(true); EnsureCamera(); + EnsureActorBehaviorRuntime(); + StartCoroutine(EnsureActorBehaviorCatalogAsync()); StartCoroutine(EnsureJoystickUIAsync()); + StartCoroutine(EnsureActionPadUIAsync()); if (GetOrCreateAvatar(selfPlayerId, selfFigure, selfName) == null) { return; @@ -133,6 +146,10 @@ namespace XGame { joystickRoot.gameObject.SetActive(true); } + if (actionPadRoot != null) + { + actionPadRoot.gameObject.SetActive(true); + } send(FrameworkOpcode.LobbyJoin, new LobbyJoinMsg { Name = selfName, Figure = selfFigure }.Encode()); } @@ -152,6 +169,10 @@ namespace XGame { joystickRoot.gameObject.SetActive(false); } + if (actionPadRoot != null) + { + actionPadRoot.gameObject.SetActive(false); + } OnVirtualJoystickEnded(); } @@ -180,6 +201,7 @@ namespace XGame rotation = avatar.Obj.transform.rotation; hadAvatar = true; } + RemoveActorBehavior(selfPlayerId); Destroy(avatar.Obj); avatars.Remove(selfPlayerId); } @@ -217,12 +239,14 @@ namespace XGame { UpdateRemoteAvatars(); UpdateLocalMove(); + UpdateActorBehavior(); UpdateCamera(); } private void OnDestroy() { ReleaseJoystickUI(true); + ReleaseActionPadUI(true); } private void ApplyState(LobbyStateMsg state) @@ -269,6 +293,7 @@ namespace XGame } foreach (int id in remove) { + RemoveActorBehavior(id); Destroy(avatars[id].Obj); avatars.Remove(id); } @@ -336,11 +361,14 @@ namespace XGame avatars.Remove(oldPlayerId); if (!avatars.ContainsKey(serverPlayerId)) { + RemoveActorBehavior(oldPlayerId); oldAvatar.PlayerId = serverPlayerId; avatars[serverPlayerId] = oldAvatar; + RegisterActorBehavior(oldAvatar); } else if (oldAvatar.Obj != null) { + RemoveActorBehavior(oldPlayerId); Destroy(oldAvatar.Obj); } } @@ -567,8 +595,10 @@ namespace XGame { if (avatar.Figure == figure) { + UpdateActorBehaviorActor(avatar); return avatar; } + RemoveActorBehavior(playerId); Destroy(avatar.Obj); avatars.Remove(playerId); } @@ -580,6 +610,7 @@ namespace XGame avatar.Target = avatar.Obj.transform.position; avatar.Animator = avatar.Obj.GetComponentInChildren(); avatars[playerId] = avatar; + RegisterActorBehavior(avatar); SetMoveState(avatar, false); return avatar; } @@ -689,6 +720,7 @@ namespace XGame a.Animator = a.Obj.GetComponentInChildren(); a.IsPlaceholder = false; a.AnimState = null; // 让 SetMoveState 重新触发动画 + RegisterActorBehavior(a); SetMoveState(a, a.Moving); } } @@ -769,6 +801,10 @@ namespace XGame { return; } + if (actorBehaviorRuntime != null && actorBehaviorRuntime.HasActiveBehavior(avatar.PlayerId)) + { + return; + } string state = moving ? "Walk" : "Stand"; if (avatar.AnimState == state) { @@ -909,6 +945,247 @@ namespace XGame camObj.transform.rotation = Quaternion.Euler(55f, 0f, 0f); } + private void EnsureActorBehaviorRuntime() + { + if (actorBehaviorRuntime == null) + { + actorBehaviorRuntime = new LobbyActorBehaviorRuntime(); + } + if (actorBehaviorPresentation == null) + { + actorBehaviorPresentation = GetComponent(); + if (actorBehaviorPresentation == null) + { + actorBehaviorPresentation = gameObject.AddComponent(); + } + } + } + + private IEnumerator EnsureActorBehaviorCatalogAsync() + { + EnsureActorBehaviorRuntime(); + if (actorBehaviorCatalogLoaded || actorBehaviorCatalogLoading) + { + yield break; + } + + actorBehaviorCatalogLoading = true; + BehaviorConfigCatalog catalog = null; + yield return ActorBehaviorConfigLoader.LoadCatalog(LobbyActorBehaviorRuntime.DefaultCatalogType, loaded => catalog = loaded); + actorBehaviorCatalogLoading = false; + if (catalog == null) + { + yield break; + } + + actorBehaviorRuntime.RegisterCatalog(catalog); + actorBehaviorCatalogLoaded = true; + UpdateActorBehaviorActors(); + } + + private void RegisterActorBehavior(Avatar avatar) + { + if (avatar == null || avatar.Obj == null) + { + return; + } + + EnsureActorBehaviorRuntime(); + actorBehaviorRuntime.RegisterOrUpdateActor(avatar.PlayerId, avatar.Obj.transform.position, avatar.Obj.transform.forward); + actorBehaviorRuntime.SwitchActorCatalog(avatar.PlayerId, LobbyActorBehaviorRuntime.DefaultCatalogType); + actorBehaviorPresentation.RegisterActor(avatar.PlayerId, avatar.Animator, avatar.Obj.transform, avatar.Obj.transform); + } + + private void UpdateActorBehaviorActor(Avatar avatar) + { + if (avatar == null || avatar.Obj == null || actorBehaviorRuntime == null) + { + return; + } + + actorBehaviorRuntime.RegisterOrUpdateActor(avatar.PlayerId, avatar.Obj.transform.position, avatar.Obj.transform.forward); + } + + private void RemoveActorBehavior(int actorId) + { + if (actorBehaviorPresentation != null) + { + actorBehaviorPresentation.UnregisterActor(actorId, true); + } + if (actorBehaviorRuntime != null) + { + actorBehaviorRuntime.RemoveActor(actorId); + } + } + + private void UpdateActorBehavior() + { + if (actorBehaviorRuntime == null) + { + return; + } + + UpdateActorBehaviorActors(); + actorBehaviorRuntime.Tick(Time.deltaTime); + DrainActorBehaviorEvents(); + } + + private void UpdateActorBehaviorActors() + { + if (actorBehaviorRuntime == null) + { + return; + } + + foreach (Avatar avatar in avatars.Values) + { + UpdateActorBehaviorActor(avatar); + } + } + + private void DrainActorBehaviorEvents() + { + if (actorBehaviorRuntime == null || actorBehaviorPresentation == null) + { + return; + } + + IReadOnlyList events = actorBehaviorRuntime.DrainEvents(); + if (events.Count == 0) + { + return; + } + + actorBehaviorPresentation.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 Avatar avatar)) + { + avatar.AnimState = null; + SetMoveState(avatar, avatar.Moving); + } + } + } + + private Vector3 GetSelfSkillForward() + { + if (lastMoveDirection.sqrMagnitude > 0.001f) + { + return lastMoveDirection; + } + if (avatars.TryGetValue(selfPlayerId, out Avatar avatar)) + { + if (avatar.LastFaceDirection.sqrMagnitude > 0.001f) + { + return avatar.LastFaceDirection; + } + if (avatar.Obj != null) + { + return avatar.Obj.transform.forward; + } + } + return Vector3.forward; + } + + private void OnVirtualActionButtonEvent(ActionButtonEvent evt) + { + EnsureActorBehaviorRuntime(); + if (!actorBehaviorCatalogLoaded) + { + if (LobbyActorBehaviorRuntime.IsSecondaryATrigger(evt)) + { + Debug.LogWarning("[LobbyWorldController] actor behavior catalog is not ready."); + } + return; + } + + BehaviorStartResult result = actorBehaviorRuntime.HandleActionButton(evt, selfPlayerId, GetSelfSkillForward()); + if (result.Success) + { + DrainActorBehaviorEvents(); + return; + } + + if (result.Error != "button ignored") + { + Debug.LogWarning("[LobbyWorldController] start behavior failed: " + result.Error); + } + } + + private IEnumerator EnsureActionPadUIAsync() + { + EnsureEventSystem(); + if (actionPadRoot != null) + { + actionPadRoot.gameObject.SetActive(joined); + yield break; + } + if (actionPadLoading) + { + yield break; + } + actionPadLoading = true; + + UObject prefab = null; + yield return XResLoader.coLoadRes(ActionPadPrefabPath, typeof(GameObject), o => prefab = o); + actionPadLoading = false; + if (actionPadRoot != null) + { + actionPadRoot.gameObject.SetActive(joined); + yield break; + } + + GameObject prefabObj = prefab as GameObject; + if (prefabObj == null) + { + Debug.LogError("[LobbyWorldController] load action pad prefab failed: " + ActionPadPrefabPath); + yield break; + } + + GameObject rootObj = Instantiate(prefabObj); + UIManager.AttachTo(UILayer.HUD, rootObj); + rootObj.name = "UI_VirtualActionPad"; + actionPadRoot = rootObj.GetComponent(); + if (actionPadRoot == null) + { + actionPadRoot = rootObj.AddComponent(); + } + actionPadRoot.localScale = Vector3.one; + + virtualActionPad = rootObj.GetComponent(); + if (virtualActionPad == null) + { + virtualActionPad = rootObj.AddComponent(); + } + virtualActionPad.ButtonEvent -= OnVirtualActionButtonEvent; + virtualActionPad.ButtonEvent += OnVirtualActionButtonEvent; + actionPadRoot.gameObject.SetActive(joined); + } + + private void ReleaseActionPadUI(bool destroyObject) + { + if (virtualActionPad != null) + { + virtualActionPad.ButtonEvent -= OnVirtualActionButtonEvent; + } + + GameObject rootObj = actionPadRoot != null ? actionPadRoot.gameObject : null; + virtualActionPad = null; + actionPadRoot = null; + actionPadLoading = false; + + if (destroyObject && rootObj != null) + { + DestroyUnityObject(rootObj); + } + } + private IEnumerator EnsureJoystickUIAsync() { EnsureEventSystem(); diff --git a/Server/Framework.Shared.Tests/ActorBehavior/BehaviorWorldTests.cs b/Server/Framework.Shared.Tests/ActorBehavior/BehaviorWorldTests.cs index 06433ad9..101b6484 100644 --- a/Server/Framework.Shared.Tests/ActorBehavior/BehaviorWorldTests.cs +++ b/Server/Framework.Shared.Tests/ActorBehavior/BehaviorWorldTests.cs @@ -30,6 +30,22 @@ namespace XWorld.Framework.Tests.ActorBehavior Assert.Contains(world.DrainEvents(), e => e.Kind == BehaviorEventKind.BehaviorStopped); } + [Fact] + public void RemoveActor_ClearsActorStateAndActiveEffects() + { + BehaviorWorld world = TestWorld.CreateServerWorld(); + world.TryStartBehavior(1, "slash", BehaviorInput.FromDirection(1, 0)); + Assert.True(world.ApplyBuff(1, "fighter", "slow_01", 1)); + Assert.True(world.ApplyBuff(2, "fighter", "slow_01", 1)); + + Assert.True(world.RemoveActor(1)); + + Assert.False(world.HasActiveBehavior(1)); + Assert.Equal(0, world.ActiveBuffCount(1)); + Assert.Equal(0, world.ActiveBuffCount(2)); + Assert.False(world.TryStartBehavior(1, "slash", BehaviorInput.FromDirection(1, 0)).Success); + } + [Fact] public void Tick_TriggersTimelineEventsAtConfiguredTimes() {