feat: trigger fighter behavior from lobby action button

This commit is contained in:
ud18010
2026-07-30 17:00:31 +08:00
parent 03cee1036d
commit e9efa8a567
9 changed files with 674 additions and 0 deletions
@@ -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.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<FrameworkOpcode, byte[]> 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<Animator>();
avatars[playerId] = avatar;
RegisterActorBehavior(avatar);
SetMoveState(avatar, false);
return avatar;
}
@@ -689,6 +720,7 @@ namespace XGame
a.Animator = a.Obj.GetComponentInChildren<Animator>();
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<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()
{
EnsureEventSystem();