mirror of
https://gitgud.io/AbstractConcept/rimworld-animation-studio.git
synced 2024-08-15 00:43:27 +00:00
406 lines
16 KiB
C#
406 lines
16 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;
|
|
|
|
[Header("Prefabs")]
|
|
public ActorBody actorBodyPrefab;
|
|
public AnimationTimeline animationTimelinePrefab;
|
|
|
|
// Private timing variables
|
|
private float timeSinceLastUpdate = 0;
|
|
private int cycleIndex = 0;
|
|
|
|
public void Update()
|
|
{
|
|
// No animation, exit
|
|
if (Workspace.animationDef == null) { return; }
|
|
|
|
// Dirty animation, reset
|
|
if (Workspace.animationDef != null && Workspace.isDirty)
|
|
{ Initialize(); return; }
|
|
|
|
// Update tick if animating
|
|
if (isAnimating)
|
|
{
|
|
timeSinceLastUpdate += Time.deltaTime;
|
|
|
|
if (timeSinceLastUpdate < 1f / 60f)
|
|
{ return; }
|
|
|
|
timeSinceLastUpdate -= 1f / 60f;
|
|
stageTick += 1;
|
|
|
|
stageTimelineSlider.value = stageTick;
|
|
animationClipTimeField.text = stageTick.ToString();
|
|
|
|
if (stageTick > Workspace.StageWindowSize)
|
|
{
|
|
if (stageLoopDropdown.value == 1)
|
|
{ stageTick = 1; }
|
|
|
|
else if (stageLoopDropdown.value >= 2 && Workspace.stageID < Workspace.animationDef.animationStages.Count - 1)
|
|
{
|
|
++cycleIndex;
|
|
stageTick = 1;
|
|
|
|
if ((stageLoopDropdown.value == 2 && cycleIndex > int.Parse(cyclesNormalField.text)) ||
|
|
(stageLoopDropdown.value == 3 && cycleIndex > int.Parse(cyclesFastField.text)))
|
|
{
|
|
++Workspace.stageID;
|
|
cycleIndex = 0;
|
|
|
|
ResetAnimationTimeline();
|
|
InitializeAnimationTimeline();
|
|
}
|
|
}
|
|
|
|
else
|
|
{ stageTick = Workspace.StageWindowSize; }
|
|
}
|
|
}
|
|
|
|
// Update stage timeline
|
|
animationClipTimeField.interactable = isAnimating == false;
|
|
animationClipLengthField.interactable = isAnimating == false;
|
|
|
|
// Update animation
|
|
UpdateAnimation();
|
|
}
|
|
|
|
public void UpdateAnimation()
|
|
{
|
|
List<ActorBody> actorBodies = GetComponentsInChildren<ActorBody>().ToList();
|
|
|
|
for (int actorID = 0; actorID < actorBodies.Count; actorID++)
|
|
{
|
|
PawnAnimationClip clip = Workspace.animationDef?.animationStages[Workspace.stageID]?.animationClips[actorID];
|
|
|
|
if (clip == null)
|
|
{ continue; }
|
|
|
|
float clipPercent = (float)(stageTick % clip.duration) / clip.duration;
|
|
|
|
ActorBody actorBody = actorBodies[actorID];
|
|
string bodyType = actorBody.bodyType;
|
|
|
|
Vector3 deltaPos = new Vector3(clip.BodyOffsetX.Evaluate(clipPercent), 0, clip.BodyOffsetZ.Evaluate(clipPercent));
|
|
deltaPos += Workspace.animationDef.actors[actorID].bodyTypeOffset.GetOffset(bodyType);
|
|
|
|
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;
|
|
|
|
int bodyFacing = (int)clip.BodyFacing.Evaluate(clipPercent);
|
|
int headFacing = (int)clip.HeadFacing.Evaluate(clipPercent);
|
|
|
|
Vector3 headBob = new Vector3(0, 0, clip.HeadBob.Evaluate(clipPercent)) + PawnUtility.BaseHeadOffsetAt(bodyType, bodyFacing);
|
|
|
|
Vector3 bodyPos = new Vector3(deltaPos.x, deltaPos.z, 0);
|
|
Vector3 headPos = new Vector3(headBob.x, headBob.z, 0);
|
|
|
|
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.bodyRenderer.sprite = Resources.Load<Sprite>("Textures/Humanlike/Bodies/" + bodyType + bodyFacing);
|
|
actorBody.headRenderer.sprite = Resources.Load<Sprite>("Textures/Humanlike/Heads/Head" + headFacing);
|
|
|
|
actorBody.bodyRenderer.sortingLayerName = clip.layer;
|
|
actorBody.headRenderer.sortingLayerName = clip.layer;
|
|
actorBody.headRenderer.sortingOrder = headFacing == 0 ? -1 : 1;
|
|
}
|
|
}
|
|
|
|
public void Initialize()
|
|
{
|
|
Debug.Log("Initializing animation preview");
|
|
Reset();
|
|
|
|
InitializeAnimationTimeline();
|
|
StageCardManager.Instance.Initialize();
|
|
|
|
Workspace.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, 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;
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
Workspace.stageID = 0;
|
|
|
|
ResetAnimationTimeline();
|
|
StageCardManager.Instance.Reset();
|
|
}
|
|
|
|
public void ResetAnimationTimeline()
|
|
{
|
|
isAnimating = false;
|
|
timeSinceLastUpdate = 0;
|
|
cycleIndex = 0;
|
|
|
|
foreach (ActorBody actorBody in GetComponentsInChildren<ActorBody>())
|
|
{ Destroy(actorBody.gameObject); }
|
|
|
|
foreach (Transform animationTimeline in animationTimelines)
|
|
{ Destroy(animationTimeline.gameObject); }
|
|
}
|
|
|
|
public bool AddAnimationStage()
|
|
{
|
|
AnimationStage stage = new AnimationStage();
|
|
|
|
return stage.MakeNew();
|
|
}
|
|
|
|
public bool CloneAnimationStage()
|
|
{
|
|
AnimationStage stage = Workspace.animationDef.animationStages[Workspace.stageID].Copy();
|
|
stage.Initialize();
|
|
stage.stageName += " (Clone)";
|
|
|
|
Workspace.animationDef.animationStages.Insert(Workspace.stageID + 1, stage);
|
|
|
|
return true;
|
|
}
|
|
|
|
public bool MoveAnimationStage(int startIndex, int delta)
|
|
{
|
|
if (startIndex + delta < 0 || startIndex + delta >= Workspace.animationDef.animationStages.Count)
|
|
{ return false; }
|
|
|
|
AnimationStage stage = Workspace.animationDef.animationStages[startIndex];
|
|
Workspace.animationDef.animationStages[startIndex] = Workspace.animationDef.animationStages[startIndex + delta];
|
|
Workspace.animationDef.animationStages[startIndex + delta] = stage;
|
|
|
|
return true;
|
|
}
|
|
|
|
public bool RemoveAnimationStage()
|
|
{
|
|
if (Workspace.animationDef.animationStages.Count == 1)
|
|
{
|
|
Debug.LogWarning("Cannot delete animation stage - the animation must contain at least one animation stage.");
|
|
return false;
|
|
}
|
|
|
|
Workspace.animationDef.animationStages.RemoveAt(Workspace.stageID);
|
|
Workspace.stageID = Workspace.stageID >= Workspace.animationDef.animationStages.Count ? Workspace.stageID = Workspace.animationDef.animationStages.Count - 1 : Workspace.stageID;
|
|
|
|
return true;
|
|
}
|
|
|
|
public void AddActor()
|
|
{
|
|
Actor actor = new Actor();
|
|
|
|
if (actor.MakeNew())
|
|
{ Initialize(); }
|
|
}
|
|
|
|
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;
|
|
|
|
Initialize();
|
|
}
|
|
|
|
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.GetChild(Workspace.actorID).GetComponent<AnimationTimeline>().AddPawnKeyFrame(keyframe.keyframeID);
|
|
}
|
|
|
|
public void RemovePawnKeyframe()
|
|
{
|
|
RemovePawnKeyframe(Workspace.actorID, Workspace.keyframeID);
|
|
}
|
|
|
|
public void RemovePawnKeyframe(int actorID, int keyframeID)
|
|
{
|
|
PawnKeyframe keyframe = Workspace.Instance.GetPawnKeyframe(actorID, keyframeID);
|
|
|
|
if (keyframe.atTick == 1)
|
|
{ Debug.LogWarning("Cannot delete key frame - the first key frame of an animation cannot be deleted"); return; }
|
|
|
|
if (keyframe != null)
|
|
{
|
|
animationTimelines.GetChild(actorID).GetComponent<AnimationTimeline>().RemovePawnKeyFrame(keyframe.keyframeID);
|
|
|
|
PawnAnimationClip clip = Workspace.animationDef.animationStages[Workspace.stageID].animationClips[actorID];
|
|
clip.keyframes.Remove(keyframe);
|
|
clip.BuildSimpleCurves();
|
|
}
|
|
}
|
|
|
|
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.");
|
|
|
|
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;
|
|
|
|
ResetAnimationTimeline();
|
|
InitializeAnimationTimeline();
|
|
}
|
|
|
|
public void OnCycleNormalFieldChange()
|
|
{
|
|
if (Workspace.animationDef == null) return;
|
|
|
|
if (int.TryParse(cyclesNormalField.text, out int cycles))
|
|
{ Workspace.animationDef.animationStages[Workspace.stageID].playTimeTicks = cycles * Workspace.StageWindowSize; }
|
|
}
|
|
|
|
public void OnCycleFastFieldChange()
|
|
{
|
|
if (Workspace.animationDef == null) return;
|
|
|
|
if (int.TryParse(cyclesFastField.text, out int cycles))
|
|
{ Workspace.animationDef.animationStages[Workspace.stageID].playTimeTicksQuick = cycles * Workspace.StageWindowSize; }
|
|
}
|
|
}
|
|
}
|