feat: trigger fighter behavior from lobby action button
This commit is contained in:
@@ -33,6 +33,49 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
_actors[actor.ActorId] = actor;
|
_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<int>(_buffs.Keys);
|
||||||
|
for (int i = 0; i < buffTargets.Count; i++)
|
||||||
|
{
|
||||||
|
List<ActiveBuff> 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)
|
public bool SwitchActorCatalog(int actorId, string catalogType)
|
||||||
{
|
{
|
||||||
if (!_actors.TryGetValue(actorId, out BehaviorActorState actor)) return false;
|
if (!_actors.TryGetValue(actorId, out BehaviorActorState actor)) return false;
|
||||||
|
|||||||
@@ -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"": [] }");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d8b2e829e8bf4782996532ed137c607c
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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<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);
|
||||||
|
|
||||||
|
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<string> onLoaded)
|
||||||
|
{
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
TextAsset asset = AssetDatabase.LoadAssetAtPath<TextAsset>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 749877ee582c42058dfc794500488f28
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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<BehaviorEvent> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8f25b35931c34b66982c41c5fd329fe2
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -5,7 +5,9 @@ using UnityEngine;
|
|||||||
using UnityEngine.EventSystems;
|
using UnityEngine.EventSystems;
|
||||||
using UnityEngine.Rendering;
|
using UnityEngine.Rendering;
|
||||||
using UnityEngine.UI;
|
using UnityEngine.UI;
|
||||||
|
using XGame.ActorBehavior;
|
||||||
using XGame.VirtualInput;
|
using XGame.VirtualInput;
|
||||||
|
using XWorld.Framework.ActorBehavior;
|
||||||
using XWorld.Framework.Protocol;
|
using XWorld.Framework.Protocol;
|
||||||
using UObject = UnityEngine.Object;
|
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 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 CharacterConfigResourcePath = "Config/CharacterConfig";
|
||||||
private const string SelectedCharacterKey = "CharacterSwitch.SelectedCharacterId";
|
private const string SelectedCharacterKey = "CharacterSwitch.SelectedCharacterId";
|
||||||
private const float MoveSpeed = 1f;
|
private const float MoveSpeed = 1f;
|
||||||
@@ -84,10 +87,17 @@ namespace XGame
|
|||||||
private RectTransform joystickRoot;
|
private RectTransform joystickRoot;
|
||||||
private RectTransform joystickWidget; // 视觉容器:新预设里的 "Joystick" 子节点(自带定位);找不到则回退为 joystickRoot
|
private RectTransform joystickWidget; // 视觉容器:新预设里的 "Joystick" 子节点(自带定位);找不到则回退为 joystickRoot
|
||||||
private VirtualJoystick virtualJoystick;
|
private VirtualJoystick virtualJoystick;
|
||||||
|
private RectTransform actionPadRoot;
|
||||||
|
private VirtualActionPad virtualActionPad;
|
||||||
|
private LobbyActorBehaviorRuntime actorBehaviorRuntime;
|
||||||
|
private ActorBehaviorPresentation actorBehaviorPresentation;
|
||||||
private RectTransform joystickKnob;
|
private RectTransform joystickKnob;
|
||||||
private bool joystickMouseCaptured;
|
private bool joystickMouseCaptured;
|
||||||
private int joystickTouchFingerId = -1;
|
private int joystickTouchFingerId = -1;
|
||||||
private bool joystickLoading;
|
private bool joystickLoading;
|
||||||
|
private bool actionPadLoading;
|
||||||
|
private bool actorBehaviorCatalogLoading;
|
||||||
|
private bool actorBehaviorCatalogLoaded;
|
||||||
private static int materialLogCount;
|
private static int materialLogCount;
|
||||||
|
|
||||||
public void Initialize(Action<FrameworkOpcode, byte[]> sendFramework, int playerId, string playerName, int figure)
|
public void Initialize(Action<FrameworkOpcode, byte[]> sendFramework, int playerId, string playerName, int figure)
|
||||||
@@ -109,7 +119,10 @@ namespace XGame
|
|||||||
EnsureWorldRoot();
|
EnsureWorldRoot();
|
||||||
worldRoot.gameObject.SetActive(true);
|
worldRoot.gameObject.SetActive(true);
|
||||||
EnsureCamera();
|
EnsureCamera();
|
||||||
|
EnsureActorBehaviorRuntime();
|
||||||
|
StartCoroutine(EnsureActorBehaviorCatalogAsync());
|
||||||
StartCoroutine(EnsureJoystickUIAsync());
|
StartCoroutine(EnsureJoystickUIAsync());
|
||||||
|
StartCoroutine(EnsureActionPadUIAsync());
|
||||||
if (GetOrCreateAvatar(selfPlayerId, selfFigure, selfName) == null)
|
if (GetOrCreateAvatar(selfPlayerId, selfFigure, selfName) == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -133,6 +146,10 @@ namespace XGame
|
|||||||
{
|
{
|
||||||
joystickRoot.gameObject.SetActive(true);
|
joystickRoot.gameObject.SetActive(true);
|
||||||
}
|
}
|
||||||
|
if (actionPadRoot != null)
|
||||||
|
{
|
||||||
|
actionPadRoot.gameObject.SetActive(true);
|
||||||
|
}
|
||||||
send(FrameworkOpcode.LobbyJoin, new LobbyJoinMsg { Name = selfName, Figure = selfFigure }.Encode());
|
send(FrameworkOpcode.LobbyJoin, new LobbyJoinMsg { Name = selfName, Figure = selfFigure }.Encode());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +169,10 @@ namespace XGame
|
|||||||
{
|
{
|
||||||
joystickRoot.gameObject.SetActive(false);
|
joystickRoot.gameObject.SetActive(false);
|
||||||
}
|
}
|
||||||
|
if (actionPadRoot != null)
|
||||||
|
{
|
||||||
|
actionPadRoot.gameObject.SetActive(false);
|
||||||
|
}
|
||||||
OnVirtualJoystickEnded();
|
OnVirtualJoystickEnded();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,6 +201,7 @@ namespace XGame
|
|||||||
rotation = avatar.Obj.transform.rotation;
|
rotation = avatar.Obj.transform.rotation;
|
||||||
hadAvatar = true;
|
hadAvatar = true;
|
||||||
}
|
}
|
||||||
|
RemoveActorBehavior(selfPlayerId);
|
||||||
Destroy(avatar.Obj);
|
Destroy(avatar.Obj);
|
||||||
avatars.Remove(selfPlayerId);
|
avatars.Remove(selfPlayerId);
|
||||||
}
|
}
|
||||||
@@ -217,12 +239,14 @@ namespace XGame
|
|||||||
{
|
{
|
||||||
UpdateRemoteAvatars();
|
UpdateRemoteAvatars();
|
||||||
UpdateLocalMove();
|
UpdateLocalMove();
|
||||||
|
UpdateActorBehavior();
|
||||||
UpdateCamera();
|
UpdateCamera();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDestroy()
|
private void OnDestroy()
|
||||||
{
|
{
|
||||||
ReleaseJoystickUI(true);
|
ReleaseJoystickUI(true);
|
||||||
|
ReleaseActionPadUI(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyState(LobbyStateMsg state)
|
private void ApplyState(LobbyStateMsg state)
|
||||||
@@ -269,6 +293,7 @@ namespace XGame
|
|||||||
}
|
}
|
||||||
foreach (int id in remove)
|
foreach (int id in remove)
|
||||||
{
|
{
|
||||||
|
RemoveActorBehavior(id);
|
||||||
Destroy(avatars[id].Obj);
|
Destroy(avatars[id].Obj);
|
||||||
avatars.Remove(id);
|
avatars.Remove(id);
|
||||||
}
|
}
|
||||||
@@ -336,11 +361,14 @@ namespace XGame
|
|||||||
avatars.Remove(oldPlayerId);
|
avatars.Remove(oldPlayerId);
|
||||||
if (!avatars.ContainsKey(serverPlayerId))
|
if (!avatars.ContainsKey(serverPlayerId))
|
||||||
{
|
{
|
||||||
|
RemoveActorBehavior(oldPlayerId);
|
||||||
oldAvatar.PlayerId = serverPlayerId;
|
oldAvatar.PlayerId = serverPlayerId;
|
||||||
avatars[serverPlayerId] = oldAvatar;
|
avatars[serverPlayerId] = oldAvatar;
|
||||||
|
RegisterActorBehavior(oldAvatar);
|
||||||
}
|
}
|
||||||
else if (oldAvatar.Obj != null)
|
else if (oldAvatar.Obj != null)
|
||||||
{
|
{
|
||||||
|
RemoveActorBehavior(oldPlayerId);
|
||||||
Destroy(oldAvatar.Obj);
|
Destroy(oldAvatar.Obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -567,8 +595,10 @@ namespace XGame
|
|||||||
{
|
{
|
||||||
if (avatar.Figure == figure)
|
if (avatar.Figure == figure)
|
||||||
{
|
{
|
||||||
|
UpdateActorBehaviorActor(avatar);
|
||||||
return avatar;
|
return avatar;
|
||||||
}
|
}
|
||||||
|
RemoveActorBehavior(playerId);
|
||||||
Destroy(avatar.Obj);
|
Destroy(avatar.Obj);
|
||||||
avatars.Remove(playerId);
|
avatars.Remove(playerId);
|
||||||
}
|
}
|
||||||
@@ -580,6 +610,7 @@ namespace XGame
|
|||||||
avatar.Target = avatar.Obj.transform.position;
|
avatar.Target = avatar.Obj.transform.position;
|
||||||
avatar.Animator = avatar.Obj.GetComponentInChildren<Animator>();
|
avatar.Animator = avatar.Obj.GetComponentInChildren<Animator>();
|
||||||
avatars[playerId] = avatar;
|
avatars[playerId] = avatar;
|
||||||
|
RegisterActorBehavior(avatar);
|
||||||
SetMoveState(avatar, false);
|
SetMoveState(avatar, false);
|
||||||
return avatar;
|
return avatar;
|
||||||
}
|
}
|
||||||
@@ -689,6 +720,7 @@ namespace XGame
|
|||||||
a.Animator = a.Obj.GetComponentInChildren<Animator>();
|
a.Animator = a.Obj.GetComponentInChildren<Animator>();
|
||||||
a.IsPlaceholder = false;
|
a.IsPlaceholder = false;
|
||||||
a.AnimState = null; // 让 SetMoveState 重新触发动画
|
a.AnimState = null; // 让 SetMoveState 重新触发动画
|
||||||
|
RegisterActorBehavior(a);
|
||||||
SetMoveState(a, a.Moving);
|
SetMoveState(a, a.Moving);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -769,6 +801,10 @@ namespace XGame
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (actorBehaviorRuntime != null && actorBehaviorRuntime.HasActiveBehavior(avatar.PlayerId))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
string state = moving ? "Walk" : "Stand";
|
string state = moving ? "Walk" : "Stand";
|
||||||
if (avatar.AnimState == state)
|
if (avatar.AnimState == state)
|
||||||
{
|
{
|
||||||
@@ -909,6 +945,247 @@ namespace XGame
|
|||||||
camObj.transform.rotation = Quaternion.Euler(55f, 0f, 0f);
|
camObj.transform.rotation = Quaternion.Euler(55f, 0f, 0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void EnsureActorBehaviorRuntime()
|
||||||
|
{
|
||||||
|
if (actorBehaviorRuntime == null)
|
||||||
|
{
|
||||||
|
actorBehaviorRuntime = new LobbyActorBehaviorRuntime();
|
||||||
|
}
|
||||||
|
if (actorBehaviorPresentation == null)
|
||||||
|
{
|
||||||
|
actorBehaviorPresentation = GetComponent<ActorBehaviorPresentation>();
|
||||||
|
if (actorBehaviorPresentation == null)
|
||||||
|
{
|
||||||
|
actorBehaviorPresentation = gameObject.AddComponent<ActorBehaviorPresentation>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<BehaviorEvent> 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<RectTransform>();
|
||||||
|
if (actionPadRoot == null)
|
||||||
|
{
|
||||||
|
actionPadRoot = rootObj.AddComponent<RectTransform>();
|
||||||
|
}
|
||||||
|
actionPadRoot.localScale = Vector3.one;
|
||||||
|
|
||||||
|
virtualActionPad = rootObj.GetComponent<VirtualActionPad>();
|
||||||
|
if (virtualActionPad == null)
|
||||||
|
{
|
||||||
|
virtualActionPad = rootObj.AddComponent<VirtualActionPad>();
|
||||||
|
}
|
||||||
|
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()
|
private IEnumerator EnsureJoystickUIAsync()
|
||||||
{
|
{
|
||||||
EnsureEventSystem();
|
EnsureEventSystem();
|
||||||
|
|||||||
@@ -30,6 +30,22 @@ namespace XWorld.Framework.Tests.ActorBehavior
|
|||||||
Assert.Contains(world.DrainEvents(), e => e.Kind == BehaviorEventKind.BehaviorStopped);
|
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]
|
[Fact]
|
||||||
public void Tick_TriggersTimelineEventsAtConfiguredTimes()
|
public void Tick_TriggersTimelineEventsAtConfiguredTimes()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user