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 { [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 int lastStageTick = 1; 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; 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; if (lastStageTick != stageTick) { stageTimelineSlider.value = stageTick; animationClipTimeField.text = stageTick.ToString(); lastStageTick = stageTick; } // Update animation UpdateAnimation(); } public void UpdateAnimation() { if (Workspace.Instance.AnimationTimelinesNeedUpdate()) { ResetAnimationTimeline(); InitializeAnimationTimeline(); } List actorBodies = GetComponentsInChildren().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("Textures/Humanlike/Bodies/" + bodyType + bodyFacing); actorBody.headRenderer.sprite = Resources.Load("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; isAnimating = false; ResetAnimationTimeline(); StageCardManager.Instance.Reset(); } public void ResetAnimationTimeline() { timeSinceLastUpdate = 0; cycleIndex = 0; foreach (ActorBody actorBody in GetComponentsInChildren()) { Destroy(actorBody.gameObject); } foreach (AnimationTimeline animationTimeline in animationTimelines.GetComponentsInChildren()) { 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 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()[Workspace.actorID].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 != null && keyframe.atTick == 1) { Debug.LogWarning("Cannot delete key frame - the first key frame of an animation cannot be deleted"); return; } if (keyframe != null) { animationTimelines.GetComponentsInChildren()[Workspace.actorID].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 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; } } } }