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 _avatars = new Dictionary(); private readonly Dictionary _prefabCache = new Dictionary(); private readonly HashSet _prefabLoading = new HashSet(); 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(); _behaviorPresentation = _root.AddComponent(); _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() : 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(); 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 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(CharacterConfigResourcePath); if (asset == null) { Debug.LogError("[MiniGameActorPresentation] missing character config resource: " + CharacterConfigResourcePath); return false; } _characterConfig = JsonUtility.FromJson(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.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.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(); 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(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); } } } }