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; 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 _actorBodies = actorBodies.GetComponentsInChildren().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("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()) { Destroy(actorBody.gameObject); } foreach (AnimationTimeline animationTimeline in animationTimelines.GetComponentsInChildren()) { 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 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); Workspace.Instance.RecordEvent("Keyframe addition"); } public void ClonePawnKeyframe() { PawnAnimationClip clip = Workspace.Instance.GetCurrentPawnAnimationClip(); List 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()[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()[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 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.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; } } }