using System;
using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace AIGame.Review
{
/// Controls scene-authored Animator instances and JSON-authored UI.
public sealed class BotAnimationLab : MonoBehaviour
{
[Serializable] public sealed class Actor
{
public string label;
public Animator animator;
public Transform stage;
public AnimationClip[] clips;
public SkinnedMeshRenderer[] skins;
public Material[] originalMaterials;
}
public Actor[] actors;
public Transform ui;
public Camera reviewCamera;
public Camera[] comparisonCameras;
public Material clayMaterial;
public bool playing = true;
public bool repeatAttacks;
public bool gray;
public float speed = 1;
public int selectedMotion;
public int focus = -1;
public int detail;
public float yaw;
public float zoom = 1;
public static readonly string[] StateNames = { "Idle", "Walk", "Attack01", "Attack02" };
TMP_Text stateText, timeText, playText, loopText, materialText, detailText;
RectTransform progress;
float progressWidth;
Vector3 lastMouse;
bool dragging;
bool previousRunInBackground;
Actor ReferenceActor => actors[focus >= 0 ? focus : 0];
public float Duration => ReferenceActor.clips[selectedMotion].length;
public float TimePosition => Mathf.Clamp01(ReferenceActor.animator.GetCurrentAnimatorStateInfo(0).normalizedTime % 1f) * Duration;
void OnEnable()
{
if (!Application.isPlaying) return;
previousRunInBackground = Application.runInBackground;
Application.runInBackground = true;
}
void OnDisable()
{
if (Application.isPlaying) Application.runInBackground = previousRunInBackground;
}
void Awake()
{
var buttons = ui.GetComponentsInChildren(true).ToDictionary(b => b.name);
void Bind(string name, UnityEngine.Events.UnityAction action) => buttons["Btn_" + name].onClick.AddListener(action);
Bind("Idle", () => SelectMotion(0)); Bind("Walk", () => SelectMotion(1));
Bind("Attack01", () => SelectMotion(2)); Bind("Attack02", () => SelectMotion(3));
Bind("Play", () => { playing = !playing; RefreshUI(); });
Bind("Previous", () => Step(-1)); Bind("Next", () => Step(1));
Bind("Restart", () => { bool resume = playing; Seek(0); playing = resume; RefreshUI(); });
Bind("Speed025", () => SetSpeed(.25f)); Bind("Speed05", () => SetSpeed(.5f));
Bind("Speed1", () => SetSpeed(1)); Bind("Speed2", () => SetSpeed(2));
Bind("Loop", () => { repeatAttacks = !repeatAttacks; RefreshUI(); });
Bind("Front", () => SetYaw(0)); Bind("Side", () => SetYaw(90)); Bind("Back", () => SetYaw(180));
Bind("Material", () => { gray = !gray; RefreshView(); });
Bind("All", () => SetFocus(-1));
Bind("Focus1", () => SetFocus(0)); Bind("Focus2", () => SetFocus(1));
Bind("Focus3", () => SetFocus(2)); Bind("Focus4", () => SetFocus(3));
Bind("Detail", () => { detail = (detail + 1) % 3; if (detail > 0 && focus < 0) focus = 0; zoom = 1; RefreshView(); });
TMP_Text Text(string name) => ui.GetComponentsInChildren(true).First(t => t.name == "Txt_" + name);
stateText = Text("State"); timeText = Text("Time"); playText = Text("Play");
loopText = Text("Loop"); materialText = Text("Material"); detailText = Text("Detail");
progress = ui.GetComponentsInChildren(true).First(t => t.name == "Img_Progress");
progressWidth = ((RectTransform)progress.parent).rect.width;
ui.GetComponentInChildren(true).viewer = this;
foreach (var actor in actors)
{
actor.animator.applyRootMotion = false;
actor.animator.cullingMode = AnimatorCullingMode.AlwaysAnimate;
actor.animator.Rebind(); actor.animator.speed = 0; actor.animator.Update(0);
}
RefreshView();
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space)) playing = !playing;
if (Input.GetKeyDown(KeyCode.Alpha1)) SelectMotion(0);
if (Input.GetKeyDown(KeyCode.Alpha2)) SelectMotion(1);
if (Input.GetKeyDown(KeyCode.Alpha3)) SelectMotion(2);
if (Input.GetKeyDown(KeyCode.Alpha4)) SelectMotion(3);
if (Input.GetKeyDown(KeyCode.LeftArrow)) Step(-1);
if (Input.GetKeyDown(KeyCode.RightArrow)) Step(1);
bool overUI = EventSystem.current && EventSystem.current.IsPointerOverGameObject();
if (Input.GetMouseButtonDown(0) && !overUI) { dragging = true; lastMouse = Input.mousePosition; }
if (!Input.GetMouseButton(0)) dragging = false;
if (dragging) { yaw -= (Input.mousePosition.x - lastMouse.x) * .35f; lastMouse = Input.mousePosition; RefreshView(); }
if (!overUI && Mathf.Abs(Input.mouseScrollDelta.y) > .01f)
{ zoom = Mathf.Clamp(zoom * Mathf.Pow(.88f, Input.mouseScrollDelta.y), .35f, 2.5f); RefreshView(); }
if (playing) Tick(Time.unscaledDeltaTime * speed);
RefreshView();
}
public void Tick(float delta)
{
foreach (var actor in actors)
{
var a = actor.animator;
if (repeatAttacks && selectedMotion >= 2 && !a.IsInTransition(0) && a.GetCurrentAnimatorStateInfo(0).IsName("Base Layer.Idle"))
a.SetTrigger(StateNames[selectedMotion]);
a.speed = 1; a.Update(delta); a.speed = 0;
}
RefreshUI();
}
public void SelectMotion(int value)
{
selectedMotion = Mathf.Clamp(value, 0, 3);
foreach (var actor in actors)
{
var a = actor.animator;
a.ResetTrigger("Attack01"); a.ResetTrigger("Attack02");
a.SetInteger("Motion", selectedMotion == 1 ? 1 : 0);
if (selectedMotion >= 2) a.SetTrigger(StateNames[selectedMotion]);
else if (!a.GetCurrentAnimatorStateInfo(0).IsName("Base Layer.Idle") && !a.GetCurrentAnimatorStateInfo(0).IsName("Base Layer.Walk"))
a.CrossFadeInFixedTime(StateNames[selectedMotion], .15f);
}
playing = true; Tick(0); RefreshUI();
}
public void Seek(float phase)
{
playing = false;
foreach (var actor in actors)
{
var a = actor.animator;
a.ResetTrigger("Attack01"); a.ResetTrigger("Attack02");
a.SetInteger("Motion", selectedMotion == 1 ? 1 : 0);
a.Play("Base Layer." + StateNames[selectedMotion], 0, Mathf.Clamp(phase, 0, .99999f));
a.speed = 1; a.Update(0); a.speed = 0;
}
RefreshUI();
}
public void Step(int direction)
{
playing = false;
if (direction > 0) Tick(1f / 30);
else Seek(Mathf.Max(0, TimePosition - 1f / 30) / Duration);
RefreshUI();
}
public void SetSpeed(float value) { speed = value; RefreshUI(); }
public void SetYaw(float value) { yaw = value; RefreshView(); }
public void SetFocus(int value) { focus = value; detail = 0; zoom = 1; RefreshView(); }
public void RefreshView()
{
if (actors == null || !reviewCamera) return;
foreach (var actor in actors)
{
actor.stage.localRotation = Quaternion.Euler(0, yaw, 0);
for (int i = 0; i < actor.skins.Length; i++)
{
actor.skins[i].enabled = focus < 0 || actor == actors[focus];
actor.skins[i].sharedMaterial = gray ? clayMaterial : actor.originalMaterials[i];
}
}
Vector3 target = new Vector3(focus < 0 ? 0 : actors[focus].stage.position.x, .85f, 0);
if (focus >= 0 && detail == 1) target.y = 1.25f;
if (focus >= 0 && detail == 2) target = actors[focus].animator.GetBoneTransform(HumanBodyBones.RightHand).position;
reviewCamera.transform.position = target + new Vector3(0, .12f, 8);
reviewCamera.transform.LookAt(target);
reviewCamera.orthographicSize = (focus < 0 ? Mathf.Max(1.1f, 3.45f / Mathf.Max(.1f, reviewCamera.aspect)) : detail == 1 ? .55f : detail == 2 ? .28f : 1.05f) * zoom;
reviewCamera.enabled = focus >= 0;
if (comparisonCameras != null)
for (int i = 0; i < comparisonCameras.Length; i++)
{
var camera = comparisonCameras[i]; camera.enabled = focus < 0;
var aim = new Vector3(actors[i].stage.position.x, .82f, 0);
camera.transform.position = aim + new Vector3(0,.1f,8); camera.transform.LookAt(aim);
camera.orthographicSize = 1.12f * zoom;
}
if (ui) foreach (var text in ui.GetComponentsInChildren(true).Where(t => t.name.StartsWith("Txt_Actor"))) text.gameObject.SetActive(focus < 0);
RefreshUI();
}
public void RefreshUI()
{
if (!stateText) return;
string State(Animator a) => StateNames.FirstOrDefault(s => a.GetCurrentAnimatorStateInfo(0).IsName("Base Layer." + s)) ?? "过渡";
stateText.text = string.Join(" ", actors.Select(a => a.label.Replace("XBot", "X").Replace("YBot", "Y") + ": " + State(a.animator)));
var reference = actors[focus >= 0 ? focus : 0];
var info = reference.animator.GetCurrentAnimatorStateInfo(0);
float phase = Mathf.Repeat(info.normalizedTime, 1);
int index = Array.IndexOf(StateNames, State(reference.animator));
float length = reference.clips[Mathf.Max(0,index)].length;
timeText.text = reference.label + " · " + State(reference.animator) + " " + (phase * length).ToString("0.00") + " / " + length.ToString("0.00") + " 秒 " + speed.ToString("0.##") + "×";
playText.text = playing ? "暂停" : "播放";
loopText.text = repeatAttacks ? "循环攻击" : "单次攻击";
materialText.text = gray ? "彩色" : "灰模";
detailText.text = new[] { "全身", "上身", "手部" }[detail];
progress.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, Mathf.Max(2, progressWidth * phase));
}
}
}