rimworld-animation-studio/Assets/Scripts/Managers/AnimationController.cs

565 lines
23 KiB
C#

using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Xml.Serialization;
using UnityEngine;
using SFB;
using UnityEngine.UI;
namespace RimWorldAnimationStudio
{
public class AnimationController : Singleton<AnimationController>
{
[Header("Animation settings")]
public bool isAnimating = false;
public int stageTick = 1;
[Header("Object references")]
public Slider stageTimelineSlider;
public Dropdown stageLoopDropdown;
public InputField cyclesNormalField;
public InputField cyclesFastField;
public InputField animationClipTimeField;
public InputField animationClipLengthField;
public ActorCard actorCard;
public Transform animationTimelines;
public Transform actorBodies;
public Toggle stretchkeyframesToggle;
public InputField playBackSpeedField;
public Button playToggleButton;
[Header("Prefabs")]
public ActorBody actorBodyPrefab;
public AnimationTimeline animationTimelinePrefab;
// Private timing variables
private int lastStageTick = 1;
private float timeSinceLastUpdate = 0;
private int cycleIndex = 0;
private bool isDirty = true;
private bool isTimelineDirty = true;
private float playBackSpeed = 1f;
public void MakeDirty()
{ isDirty = true; }
public void MakeTimelineDirty()
{ isTimelineDirty = true; }
public void Update()
{
// No animation, exit
if (Workspace.animationDef == null) { return; }
// Dirty animation, reset
if (Workspace.animationDef != null && isDirty)
{ Initialize(); }
// Update tick if animating
if (isAnimating)
{
timeSinceLastUpdate += Time.deltaTime;
if (timeSinceLastUpdate < 1 / (playBackSpeed * 60f))
{ return; }
timeSinceLastUpdate -= 1 / (playBackSpeed * 60f);
stageTick += 1;
if (stageTick > Workspace.StageWindowSize)
{
if (stageLoopDropdown.value == 1)
{ stageTick = 1; }
else if (stageLoopDropdown.value >= 2)
{
++cycleIndex;
stageTick = 1;
if ((stageLoopDropdown.value == 2 && cycleIndex >= int.Parse(cyclesNormalField.text)) ||
(stageLoopDropdown.value == 3 && cycleIndex >= int.Parse(cyclesFastField.text)))
{
++Workspace.stageID;
cycleIndex = 0;
}
if (Workspace.stageID > Workspace.animationDef.animationStages.Count - 1)
{
Workspace.stageID = Workspace.animationDef.animationStages.Count - 1;
stageTick = Workspace.StageWindowSize;
isAnimating = false;
}
}
else
{
stageTick = Workspace.StageWindowSize;
isAnimating = false;
}
}
}
// Update stage timeline
animationClipTimeField.interactable = isAnimating == false;
animationClipLengthField.interactable = isAnimating == false;
if (lastStageTick != stageTick)
{
stageTimelineSlider.value = stageTick;
animationClipTimeField.text = stageTick.ToString();
lastStageTick = stageTick;
}
playToggleButton.image.color = isAnimating ? Constants.ColorGoldYellow : Constants.ColorWhite;
// Update animation
UpdateAnimation();
}
public void UpdateAnimation()
{
if (AnimationTimelinesNeedUpdate())
{
ResetAnimationTimeline();
InitializeAnimationTimeline();
}
List<ActorBody> _actorBodies = actorBodies.GetComponentsInChildren<ActorBody>().ToList();
for (int actorID = 0; actorID < _actorBodies.Count; actorID++)
{
if (Workspace.stageID >= Workspace.animationDef?.animationStages.Count)
{ Debug.Log("Waiting for animation stage data to initialize..."); return; }
if (actorID >= Workspace.animationDef?.animationStages[Workspace.stageID]?.animationClips.Count)
{ Debug.Log("Waiting for animation clip data to initialize..."); return; }
Actor actor = Workspace.animationDef.actors[actorID];
PawnAnimationClip clip = Workspace.animationDef?.animationStages[Workspace.stageID]?.animationClips[actorID];
if (clip == null)
{ continue; }
float clipPercent = (float)(stageTick % clip.duration) / clip.duration;
if (stageTick == clip.duration) clipPercent = 1f;
AlienRaceDef alienRaceDef = actor.GetAlienRaceDef();
ActorBody actorBody = _actorBodies[actorID];
string bodyType = alienRaceDef.isHumanoid ? actor.bodyType : "None";
Vector3 deltaPos = new Vector3(clip.BodyOffsetX.Evaluate(clipPercent), 0, clip.BodyOffsetZ.Evaluate(clipPercent));
deltaPos += actor.bodyTypeOffset.GetOffset(bodyType) + actor.GetAlienRaceOffset();
float bodyAngle = clip.BodyAngle.Evaluate(clipPercent);
float headAngle = clip.HeadAngle.Evaluate(clipPercent);
/*if (bodyAngle < 0) bodyAngle = 360 - ((-1f * bodyAngle) % 360);
if (bodyAngle > 360) bodyAngle %= 360;
if (headAngle < 0) headAngle = 360 - ((-1f * headAngle) % 360);
if (headAngle > 360) headAngle %= 360;
bodyAngle = bodyAngle > 180 ? 180 - bodyAngle : bodyAngle;
headAngle = headAngle > 180 ? 180 - headAngle : headAngle;*/
int bodyFacing = (int)clip.BodyFacing.Evaluate(clipPercent);
int headFacing = (int)clip.HeadFacing.Evaluate(clipPercent);
float headBob = clip.HeadBob.Evaluate(clipPercent);
Vector3 headOffset = new Vector3(0, 0, headBob) + PawnUtility.BaseHeadOffsetAt(bodyType, bodyFacing);
Vector3 bodyPos = new Vector3(deltaPos.x, deltaPos.z, 0);
Vector3 headPos = new Vector3(headOffset.x, headOffset.z, 0);
Vector3 appendagePos = PawnUtility.AppendageOffsetAt(bodyType, bodyFacing);
float appendageRotation = clip.GenitalAngle.Evaluate(clipPercent);
actorBody.transform.position = bodyPos;
actorBody.transform.eulerAngles = new Vector3(0, 0, -bodyAngle);
actorBody.headRenderer.transform.localPosition = headPos;
actorBody.headRenderer.transform.eulerAngles = new Vector3(0, 0, -headAngle);
actorBody.appendageRenderer.transform.localPosition = new Vector3(appendagePos.x, appendagePos.z, 0f);
actorBody.appendageRenderer.transform.eulerAngles = new Vector3(0, 0, -appendageRotation);
actorBody.bodyRenderer.sprite = alienRaceDef.GetBodyTypeGraphic((CardinalDirection)bodyFacing, bodyType);
actorBody.headRenderer.sprite = alienRaceDef.isHumanoid ? alienRaceDef.GetHeadGraphic((CardinalDirection)headFacing) : null;
actorBody.appendageRenderer.sprite = alienRaceDef.isHumanoid && bodyFacing != 0 ? Resources.Load<Sprite>("Textures/Humanlike/Appendages/Appendage" + bodyFacing) : null;
actorBody.bodyRenderer.gameObject.SetActive(actorBody.bodyRenderer.sprite != null);
actorBody.headRenderer.gameObject.SetActive(actorBody.headRenderer.sprite != null);
actorBody.appendageRenderer.gameObject.SetActive(actorBody.appendageRenderer.sprite != null);
actorBody.bodyRenderer.sortingLayerName = clip.layer;
actorBody.headRenderer.sortingLayerName = clip.layer;
actorBody.headRenderer.sortingOrder = bodyFacing == 0 ? -1 : 1;
actorBody.appendageRenderer.sortingLayerName = clip.layer;
actorBody.bodyRenderer.flipX = bodyFacing == 3;
actorBody.headRenderer.flipX = headFacing == 3;
//actorBody.appendageRenderer.flipX = bodyFacing == 3;
actorBody.transform.localScale = new Vector3(alienRaceDef.scale, alienRaceDef.scale, alienRaceDef.scale);
// ActorKeyframeCard update
if (actorID != Workspace.actorID) continue;
if (ActorKeyframeCard.Instance.positionXField.isFocused == false) { ActorKeyframeCard.Instance.positionXField.text = bodyPos.x.ToString("0.000"); }
if (ActorKeyframeCard.Instance.positionZField.isFocused == false) { ActorKeyframeCard.Instance.positionZField.text = bodyPos.y.ToString("0.000"); }
if (ActorKeyframeCard.Instance.rotationField.isFocused == false) { ActorKeyframeCard.Instance.rotationField.text = bodyAngle.ToString("0.000"); }
if (ActorKeyframeCard.Instance.headBobField.isFocused == false) { ActorKeyframeCard.Instance.headBobField.text = headBob.ToString("0.000"); }
if (ActorKeyframeCard.Instance.headRotationField.isFocused == false) { ActorKeyframeCard.Instance.headRotationField.text = headAngle.ToString("0.000"); }
if (ActorKeyframeCard.Instance.appendageRotationField.isFocused == false) { ActorKeyframeCard.Instance.appendageRotationField.text = appendageRotation.ToString("0.000"); }
}
}
public void Initialize()
{
Debug.Log("Initializing animation preview");
foreach (Transform child in transform)
{ child.gameObject.SetActive(true); }
Reset();
InitializeAnimationTimeline();
StageCardManager.Instance.Initialize();
isDirty = false;
}
public void InitializeAnimationTimeline()
{
cyclesNormalField.text = Mathf.Max(Mathf.CeilToInt((float)Workspace.animationDef.animationStages[Workspace.stageID].playTimeTicks / Workspace.StageWindowSize), 1).ToString();
cyclesFastField.text = Mathf.Max(Mathf.CeilToInt((float)Workspace.animationDef.animationStages[Workspace.stageID].playTimeTicksQuick / Workspace.StageWindowSize), 1).ToString();
for (int actorID = 0; actorID < Workspace.animationDef.actors.Count; actorID++)
{
ActorBody actorBody = Instantiate(actorBodyPrefab, actorBodies.transform);
actorBody.Initialize(actorID);
AnimationTimeline animationTimeline = Instantiate(animationTimelinePrefab, animationTimelines);
animationTimeline.Initialize(actorID);
}
animationClipLengthField.text = Workspace.StageWindowSize.ToString();
animationClipTimeField.text = "1";
stageTimelineSlider.maxValue = Workspace.StageWindowSize;
stageTimelineSlider.value = 1;
stageTick = 1;
isTimelineDirty = false;
}
public void Reset()
{
Workspace.stageID = 0;
isAnimating = false;
ResetAnimationTimeline();
StageCardManager.Instance.Reset();
}
public void ResetAnimationTimeline()
{
timeSinceLastUpdate = 0;
cycleIndex = 0;
foreach (ActorBody actorBody in actorBodies.GetComponentsInChildren<ActorBody>())
{ Destroy(actorBody.gameObject); }
foreach (AnimationTimeline animationTimeline in animationTimelines.GetComponentsInChildren<AnimationTimeline>())
{ Destroy(animationTimeline.gameObject); }
}
public void AddActor()
{
Actor actor = new Actor();
actor.MakeNew();
Workspace.Instance.RecordEvent("Actor addition");
}
public void RemoveActor()
{
if (Workspace.animationDef.actors.Count == 1)
{
Debug.LogWarning("Cannot delete actor - the animation must contain at least one actor.");
return;
}
foreach (AnimationStage stage in Workspace.animationDef.animationStages)
{ stage.animationClips.RemoveAt(Workspace.actorID); }
Workspace.animationDef.actors.RemoveAt(Workspace.actorID);
Workspace.actorID = Workspace.actorID >= Workspace.animationDef.actors.Count ? Workspace.actorID = Workspace.animationDef.actors.Count - 1 : Workspace.actorID;
Workspace.Instance.RecordEvent("Actor deletion");
}
public void AddPawnKeyframe()
{
PawnAnimationClip clip = Workspace.Instance.GetCurrentPawnAnimationClip();
List<PawnKeyframe> keyframes = clip?.keyframes;
if (clip == null || keyframes == null)
{ Debug.LogWarning("Cannot add pawn keyframe - the AnimationDef is invalid"); return; }
if (keyframes.FirstOrDefault(x => x.atTick == stageTick) != null)
{ Debug.LogWarning("Cannot add pawn keyframe - a keyframe already exists at this tick"); return; }
float clipPercent = (float)(stageTick % clip.duration) / clip.duration;
PawnKeyframe keyframe = new PawnKeyframe();
keyframe.bodyAngle = clip.BodyAngle.Evaluate(clipPercent);
keyframe.headAngle = clip.HeadAngle.Evaluate(clipPercent);
keyframe.headBob = clip.HeadBob.Evaluate(clipPercent);
keyframe.bodyOffsetX = clip.BodyOffsetX.Evaluate(clipPercent);
keyframe.bodyOffsetZ = clip.BodyOffsetZ.Evaluate(clipPercent);
keyframe.headFacing = clip.HeadFacing.Evaluate(clipPercent);
keyframe.bodyFacing = clip.BodyFacing.Evaluate(clipPercent);
keyframe.genitalAngle = clip.GenitalAngle.Evaluate(clipPercent);
keyframe.atTick = stageTick;
PawnKeyframe nextKeyframe = keyframes.FirstOrDefault(x => x.atTick > stageTick);
if (nextKeyframe != null)
{ keyframes.Insert(keyframes.IndexOf(nextKeyframe), keyframe); }
else
{ keyframes.Add(keyframe); }
clip.BuildSimpleCurves();
animationTimelines.GetComponentsInChildren<AnimationTimeline>()[Workspace.actorID].AddPawnKeyFrame(keyframe.keyframeID);
Workspace.Instance.RecordEvent("Keyframe addition");
}
public void ClonePawnKeyframe()
{
PawnAnimationClip clip = Workspace.Instance.GetCurrentPawnAnimationClip();
List<PawnKeyframe> keyframes = clip?.keyframes;
PawnKeyframe keyframe = Workspace.Instance.GetPawnKeyframe(Workspace.actorID, Workspace.keyframeID);
if (clip == null || keyframes == null)
{ Debug.LogWarning("Cannot clone pawn keyframe - the AnimationDef is invalid"); return; }
if (keyframes.FirstOrDefault(x => x.atTick == stageTick) != null)
{ Debug.LogWarning("Cannot clone pawn keyframe - a keyframe already exists at this tick"); return; }
if (keyframe == null)
{ Debug.LogWarning("Cannot clone pawn keyframe - no keyframe has been selected for cloning"); return; }
PawnKeyframe cloneFrame = keyframe.Copy();
cloneFrame.GenerateKeyframeID();
cloneFrame.atTick = stageTick;
PawnKeyframe nextKeyframe = keyframes.FirstOrDefault(x => x.atTick > stageTick);
if (nextKeyframe != null)
{ keyframes.Insert(keyframes.IndexOf(nextKeyframe), cloneFrame); }
else
{ keyframes.Add(cloneFrame); }
clip.BuildSimpleCurves();
animationTimelines.GetComponentsInChildren<AnimationTimeline>()[Workspace.actorID].AddPawnKeyFrame(cloneFrame.keyframeID);
}
public void RemovePawnKeyframe()
{
RemovePawnKeyframe(Workspace.actorID, Workspace.keyframeID);
}
public void RemovePawnKeyframe(int actorID, int keyframeID)
{
PawnKeyframe keyframe = Workspace.Instance.GetPawnKeyframe(actorID, keyframeID);
PawnAnimationClip clip = Workspace.animationDef.animationStages[Workspace.stageID].animationClips[actorID];
if (keyframe == null || clip == null) return;
if (keyframe.atTick == 1)
{ Debug.LogWarning("Cannot delete key frame - the first key frame of an animation clip cannot be deleted"); return; }
if (clip.keyframes.Count <= 2)
{ Debug.LogWarning("Cannot delete key frame - an animation clip must have two or more keyframes"); return; }
animationTimelines.GetComponentsInChildren<AnimationTimeline>()[Workspace.actorID].RemovePawnKeyFrame(keyframe.keyframeID);
clip.keyframes.Remove(keyframe);
clip.BuildSimpleCurves();
Workspace.Instance.RecordEvent("Keyframe deletion");
}
public void ToggleAnimation()
{
isAnimating = !isAnimating;
}
public void ToggleActorManipulationMode(int mode)
{
Workspace.actorManipulationMode = (ActorManipulationMode)mode;
}
public void OnStageTimelineSliderChange()
{
if (Workspace.animationDef == null) return;
if (stageTick != (int)stageTimelineSlider.value)
{
stageTick = (int)stageTimelineSlider.value;
animationClipTimeField.text = stageTick.ToString();
}
}
public void OnAnimationClipTimeFieldChange()
{
if (Workspace.animationDef == null) return;
int.TryParse(animationClipTimeField.text, out int newStageTick);
stageTick = Mathf.Clamp(newStageTick, 1, Workspace.StageWindowSize);
stageTimelineSlider.value = stageTick;
}
public void OnAnimationClipLengthFieldChange()
{
if (Workspace.animationDef == null) return;
int.TryParse(animationClipLengthField.text, out int newStageWindowSize);
newStageWindowSize = Mathf.Clamp(newStageWindowSize, Constants.minAnimationClipLength, Constants.maxAnimationClipLength);
Debug.Log("Resizing animation clip length to " + newStageWindowSize.ToString() + " ticks.");
if (stretchkeyframesToggle.isOn)
{ StretchKeyframes(newStageWindowSize); }
else
{
for (int i = 0; i < Workspace.animationDef.animationStages[Workspace.stageID].animationClips.Count; i++)
{
PawnAnimationClip clip = Workspace.animationDef.animationStages[Workspace.stageID].animationClips[i];
List<PawnKeyframe> keyframes = clip.keyframes.Where(x => x.atTick > newStageWindowSize)?.ToList();
if (keyframes.NullOrEmpty())
{ continue; }
foreach (PawnKeyframe keyframe in keyframes)
{
RemovePawnKeyframe(i, keyframe.keyframeID);
if (Workspace.animationDef.animationStages[Workspace.stageID].animationClips[i].keyframes.Count <= 2)
{ break; }
}
}
}
animationClipLengthField.text = newStageWindowSize.ToString();
Workspace.animationDef.animationStages[Workspace.stageID].stageWindowSize = newStageWindowSize;
Workspace.Instance.RecordEvent("Stage length");
}
public void StretchKeyframes(int newStageWindowSize)
{
float scale = (float)newStageWindowSize / Workspace.StageWindowSize;
foreach (PawnAnimationClip clip in Workspace.animationDef.animationStages[Workspace.stageID].animationClips)
{
foreach (PawnKeyframe keyframe in clip.keyframes)
{
keyframe.tickDuration = Mathf.RoundToInt(keyframe.tickDuration * scale);
keyframe.atTick = null;
}
clip.BuildSimpleCurves();
}
}
public void OnCycleNormalFieldChange()
{
if (Workspace.animationDef == null) return;
if (int.TryParse(cyclesNormalField.text, out int cycles))
{
cycles = cycles <= 0 ? 1 : cycles;
Workspace.animationDef.animationStages[Workspace.stageID].playTimeTicks = cycles * Workspace.StageWindowSize;
Workspace.animationDef.animationStages[Workspace.stageID].isLooping = cycles > 1;
foreach(AnimationTimeline animationTimeline in animationTimelines.GetComponentsInChildren<AnimationTimeline>())
{ animationTimeline.InitiateUpdateOfGhostFrames(); }
}
Workspace.Instance.RecordEvent("Cycle count (normal)");
}
public void OnCycleFastFieldChange()
{
if (Workspace.animationDef == null) return;
if (int.TryParse(cyclesFastField.text, out int fastCycles))
{
fastCycles = fastCycles < 0 ? 0 : fastCycles;
int.TryParse(cyclesNormalField.text, out int cycles);
if (fastCycles > cycles) fastCycles = cycles;
cyclesFastField.text = fastCycles.ToString();
Workspace.animationDef.animationStages[Workspace.stageID].playTimeTicksQuick = fastCycles * Workspace.StageWindowSize;
}
Workspace.Instance.RecordEvent("Cycle count (fast)");
}
public void OnPlayBackSpeedChange()
{
if (float.TryParse(playBackSpeedField.text, out playBackSpeed))
{ playBackSpeed = Mathf.Clamp(playBackSpeed, 0.01f, 16f); }
playBackSpeedField.text = playBackSpeed.ToString();
}
private int lastactorCount = 0;
private int lastStageID = 0;
private int lastStageCount = 0;
private int lastStageWindowSize = 0;
public bool AnimationTimelinesNeedUpdate()
{
if (Workspace.animationDef == null) return false;
bool update = isTimelineDirty;
if (lastStageID != Workspace.stageID)
{ update = true; }
if (lastStageCount != Workspace.animationDef.animationStages.Count)
{ update = true; }
if (lastactorCount != Workspace.animationDef.actors.Count)
{ update = true; }
if (lastStageWindowSize != Workspace.StageWindowSize)
{ update = true; }
if (update)
{
lastStageID = Workspace.stageID;
lastStageCount = Workspace.animationDef.animationStages.Count;
lastactorCount = Workspace.animationDef.actors.Count;
lastStageWindowSize = Workspace.StageWindowSize;
return true;
}
return false;
}
}
}