using System; using System.Text; using System.Collections.Generic; using System.Linq; using System.IO; using System.Xml.Serialization; using UnityEngine; using UnityEngine.UI; namespace RimWorldAnimationStudio { public class AnimationController : Singleton { [Header("Animation settings")] public bool isAnimating = false; public int stageTick = Constants.minTick; [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; public Text stageLengthText; public Text animationLengthText; [Header("Prefabs")] public ActorBody actorBodyPrefab; public GameObject animationTimelinePrefab; // Private timing variables private int lastStageTick = Constants.minTick; 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 bool IsDirty() { return isDirty; } public bool IsTimelineDirty() { return isTimelineDirty; } public void Update() { // No animation, exit if (Workspace.animationDef == null) { return; } // Dirty animation, reset if (Workspace.animationDef != null && isDirty) { Initialize(); } // Update animation lengths if (stageLoopDropdown.value == 3) { stageLengthText.text = "Stage length (quickie): " + Workspace.animationDef.animationStages[Workspace.stageID].playTimeTicksQuick + " (" + Workspace.animationDef.animationStages[Workspace.stageID].playTimeTicksQuick / 60f + " s)"; animationLengthText.text = "Animation length (quickie): " + Workspace.animationDef.animationTimeTicksQuick + " (" + Workspace.animationDef.animationTimeTicksQuick / 60f + " s)"; LayoutRebuilder.ForceRebuildLayoutImmediate(stageLengthText.GetComponent()); LayoutRebuilder.ForceRebuildLayoutImmediate(animationLengthText.GetComponent()); } else { stageLengthText.text = "Stage length (normal): " + Workspace.animationDef.animationStages[Workspace.stageID].playTimeTicks + " (" + Workspace.animationDef.animationStages[Workspace.stageID].playTimeTicks / 60f + " s)"; animationLengthText.text = "Animation length (normal): " + Workspace.animationDef.animationTimeTicks + " (" + Workspace.animationDef.animationTimeTicks / 60f + " s)"; LayoutRebuilder.ForceRebuildLayoutImmediate(stageLengthText.GetComponent()); LayoutRebuilder.ForceRebuildLayoutImmediate(animationLengthText.GetComponent()); } // Update tick if animating stageTick = Mathf.Clamp(stageTick, Constants.minTick, Workspace.StageWindowSize); 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 = Constants.minTick; } else if (stageLoopDropdown.value >= 2) { ++cycleIndex; stageTick = Constants.minTick; 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()) { InitializeAnimationTimeline(); } List _actorBodies = actorBodies.GetComponentsInChildren().ToList(); for (int actorID = 0; actorID < _actorBodies.Count; actorID++) { if (Workspace.stageID >= Workspace.animationDef?.animationStages.Count) { return; } if (actorID >= Workspace.animationDef?.animationStages[Workspace.stageID]?.animationClips.Count) { return; } Actor actor = Workspace.animationDef.actors[actorID]; PawnAnimationClip clip = Workspace.animationDef?.animationStages[Workspace.stageID]?.animationClips[actorID]; if (clip == null) { continue; } bool quiver = isAnimating && Workspace.Instance.GetCurrentOrPreviousKeyframe(actorID).quiver == true; bool requiresGenitals = actor.requiredGenitals.Any(x => x == "Penis") || Workspace.animationDef.actors[actorID].isFucking; float clipPercent = (float)(stageTick % clip.duration) / clip.duration; if (stageTick > Constants.minTick && stageTick == clip.duration) clipPercent = 1f; if (Workspace.animationDef.animationStages[Workspace.stageID].isLooping == false) { clipPercent = (float)stageTick / clip.duration; } 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)); float bodyAngle = clip.BodyAngle.Evaluate(clipPercent); bodyAngle += quiver ? UnityEngine.Random.value * 2f - 1f : 0f; float headAngle = clip.HeadAngle.Evaluate(clipPercent); 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 + actor.GetFinalTransformOffset(); 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.localEulerAngles = 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 = requiresGenitals && 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() { isDirty = true; Debug.Log("Initializing animation preview"); foreach (Transform child in transform) { child.gameObject.SetActive(true); } InitializeAnimationTimeline(); StageCardManager.Instance.Initialize(); isDirty = false; } public void Reset() { Workspace.stageID = 0; isAnimating = false; timeSinceLastUpdate = 0; cycleIndex = 0; } public void InitializeAnimationTimeline() { isTimelineDirty = true; 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), 0).ToString(); Workspace.animationDef.animationStages[Workspace.stageID].isLooping = int.Parse(cyclesNormalField.text) > 1 ? true : false; int actorCount = Workspace.animationDef.actors.Count; int childCount = animationTimelines.GetComponentsInChildren().Count(); for (int actorID = 0; actorID < Mathf.Max(actorCount, childCount); actorID++) { // Add new actors as required if (actorID >= childCount) { Instantiate(animationTimelinePrefab, animationTimelines); Instantiate(actorBodyPrefab, actorBodies.transform); } // Get objects to update AnimationTimeline animationTimeline = animationTimelines.GetComponentsInChildren()[actorID]; ActorBody actorBody = actorBodies.GetComponentsInChildren()[actorID]; // Update values if (actorID < actorCount) { animationTimeline.Initialize(actorID); actorBody.Initialize(actorID); } // Remove excess objects as required else { Destroy(animationTimeline.transform.parent.gameObject); Destroy(actorBody.gameObject); } } animationClipLengthField.text = Workspace.StageWindowSize.ToString(); stageTimelineSlider.maxValue = Workspace.StageWindowSize; isTimelineDirty = false; foreach (AnimationTimeline timeline in animationTimelines.GetComponentsInChildren()) { timeline.InitiateUpdateOfGhostFrames(); } } 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() { List keyframesToClone = Workspace.Instance.GetPawnKeyframes(Workspace.keyframeID); foreach (PawnKeyframe keyframe in keyframesToClone) { PawnAnimationClip clip = Workspace.Instance.GetAnimationClipThatOwnsKeyframe(keyframe.keyframeID, out int clipID); if (clip == null) { Debug.LogWarning("Cannot clone pawn keyframe - no clip owns this keyframe"); continue; } if (clip.keyframes.FirstOrDefault(x => x.atTick == stageTick) != null) { Debug.LogWarning("Cannot clone pawn keyframe - a keyframe already exists at this tick"); return; } PawnKeyframe cloneFrame = keyframe.Copy(); cloneFrame.GenerateKeyframeID(clipID); cloneFrame.atTick = stageTick; PawnKeyframe nextKeyframe = clip.keyframes.FirstOrDefault(x => x.atTick > stageTick); if (nextKeyframe != null) { clip.keyframes.Insert(clip.keyframes.IndexOf(nextKeyframe), cloneFrame); } else { clip.keyframes.Add(cloneFrame); } clip.BuildSimpleCurves(); animationTimelines.GetComponentsInChildren()[clipID].AddPawnKeyFrame(cloneFrame.keyframeID); } Workspace.Instance.RecordEvent("Keyframe clone"); } public void CopyPawnKeyframes() { Workspace.copiedKeyframes.Clear(); List keyframesToClone = Workspace.Instance.GetPawnKeyframes(Workspace.keyframeID); foreach (PawnKeyframe keyframe in keyframesToClone) { Workspace.copiedKeyframes.Add(keyframe.Copy()); } } public void PastePawnKeyframes() { List actorsInvolved = Workspace.copiedKeyframes.Select(x => x.actorID)?.ToList(); actorsInvolved = actorsInvolved?.Distinct()?.ToList(); foreach (int i in actorsInvolved) { Debug.Log("Actor: " + i); } if (actorsInvolved.NullOrEmpty()) { Debug.Log("Cannot paste keyframes - there were no copied keyframes to paste"); return; } if (actorsInvolved.Count > 1 && actorsInvolved.Contains(Workspace.actorID) == false) { Debug.Log("Cannot paste keyframes - keyframes copied across multiple timelines can only be pasted back into these source timelines"); return; } int earliestTick = actorsInvolved.Count == 1 ? Workspace.Instance.GetEarliestAtTickInCopiedKeyframes(actorsInvolved[0]) : Workspace.Instance.GetEarliestAtTickInCopiedKeyframes(Workspace.actorID); if (earliestTick < 1) { Debug.Log("Unknown error occured during keyframe paste operation"); return; } foreach (PawnKeyframe copiedKeyframe in Workspace.copiedKeyframes) { int tickToPasteAt = stageTick + (copiedKeyframe.atTick.Value - earliestTick); if (tickToPasteAt < 1) continue; if (tickToPasteAt > Workspace.StageWindowSize) { if (stretchKeyframesToggle.isOn) { ResizeStageWindowSize(tickToPasteAt); } else continue; } int targetActorID = actorsInvolved.Count == 1 ? Workspace.actorID : copiedKeyframe.actorID; if (Workspace.Instance.DoesPawnKeyframeExistAtTick(Workspace.stageID, targetActorID, tickToPasteAt)) { PawnKeyframe oldKeyframe = Workspace.animationDef.animationStages[Workspace.stageID].animationClips[targetActorID].keyframes.First(x => x.atTick == tickToPasteAt); RemovePawnKeyframe(targetActorID, oldKeyframe.keyframeID, true); } PawnKeyframe clonedKeyframe = copiedKeyframe.Copy(); clonedKeyframe.GenerateKeyframeID(targetActorID); clonedKeyframe.atTick = tickToPasteAt; PawnAnimationClip clip = Workspace.animationDef.animationStages[Workspace.stageID].animationClips[targetActorID]; PawnKeyframe nextKeyframe = clip.keyframes.FirstOrDefault(x => x.atTick > stageTick); if (nextKeyframe != null) { clip.keyframes.Insert(clip.keyframes.IndexOf(nextKeyframe), clonedKeyframe); } else { clip.keyframes.Add(clonedKeyframe); } clip.BuildSimpleCurves(); animationTimelines.GetComponentsInChildren()[clonedKeyframe.actorID].AddPawnKeyFrame(clonedKeyframe.keyframeID); } Workspace.Instance.RecordEvent("Keyframe pasted"); } public void RemovePawnKeyframe() { foreach (int keyframeID in Workspace.keyframeID) { if (Workspace.Instance.GetAnimationClipThatOwnsKeyframe(keyframeID, out int clipID) != null) { RemovePawnKeyframe(clipID, keyframeID); } } } public void RemovePawnKeyframe(int actorID, int keyframeID, bool force = false) { 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 == Constants.minTick && force == false) { Debug.LogWarning("Cannot delete key frame - the first key frame of an animation clip cannot be deleted"); return; } if (clip.keyframes.Count <= 2 && force == false) { Debug.LogWarning("Cannot delete key frame - an animation clip must have two or more keyframes"); return; } animationTimelines.GetComponentsInChildren()[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, Constants.minTick, 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; } } } } ResizeStageWindowSize(newStageWindowSize); } 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 ResizeStageWindowSize(int newStageWindowSize) { animationClipLengthField.text = newStageWindowSize.ToString(); Workspace.animationDef.animationStages[Workspace.stageID].stageWindowSize = newStageWindowSize; Workspace.animationDef.animationStages[Workspace.stageID].playTimeTicks = newStageWindowSize * int.Parse(cyclesNormalField.text); Workspace.animationDef.animationStages[Workspace.stageID].playTimeTicksQuick = newStageWindowSize * int.Parse(cyclesFastField.text); Workspace.Instance.RecordEvent("Stage length"); } public void OnCycleNormalFieldChange() { if (Workspace.animationDef == null) return; if (int.TryParse(cyclesNormalField.text, out int cycles)) { cycles = cycles <= 0 ? 1 : cycles; cyclesNormalField.text = cycles.ToString(); 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; } } }