// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. #include "SceneAnimationPlayer.h" #include "Engine/Core/Random.h" #include "Engine/Engine/Time.h" #include "Engine/Level/Scene/Scene.h" #include "Engine/Level/SceneObjectsFactory.h" #include "Engine/Level/Actors/Camera.h" #include "Engine/Serialization/Serialization.h" #include "Engine/Serialization/MemoryReadStream.h" #include "Engine/Audio/AudioClip.h" #include "Engine/Audio/AudioSource.h" #include "Engine/Graphics/RenderTask.h" #include "Engine/Renderer/RenderList.h" #include "Engine/Scripting/Scripting.h" #include "Engine/Scripting/Script.h" #include "Engine/Scripting/ManagedCLR/MException.h" #include "Engine/Scripting/ManagedCLR/MProperty.h" #include "Engine/Scripting/ManagedCLR/MUtils.h" #include "Engine/Scripting/ManagedCLR/MField.h" #include "Engine/Scripting/ManagedCLR/MClass.h" #include "Engine/Scripting/ManagedCLR/MMethod.h" #include "Engine/Scripting/Internal/ManagedSerialization.h" // This could be Update, LateUpdate or FixedUpdate #define UPDATE_POINT Update #define REGISTER_TICK GetScene()->Ticking.UPDATE_POINT.AddTick(this) #define UNREGISTER_TICK GetScene()->Ticking.UPDATE_POINT.RemoveTick(this) SceneAnimationPlayer::SceneAnimationPlayer(const SpawnParams& params) : Actor(params) { Animation.Changed.Bind(this); Animation.Loaded.Bind(this); } float SceneAnimationPlayer::GetTime() const { return _time; } void SceneAnimationPlayer::SetTime(float value) { _time = value; } void SceneAnimationPlayer::Play() { if (_state == PlayState::Playing) return; if (!IsDuringPlay()) { LOG(Warning, "Cannot play scene animation. Actor is not in a game."); return; } if (Animation == nullptr) { LOG(Warning, "Cannot play scene animation. No asset assigned."); return; } if (Animation->WaitForLoaded()) { LOG(Warning, "Cannot play scene animation. Failed to load asset."); return; } if (_state == PlayState::Stopped) { if (RandomStartTime) { _time = Random::Rand() * Animation->GetDuration(); } else { _time = StartTime; } GetSceneRendering()->AddPostFxProvider(this); } _state = PlayState::Playing; _lastTime = _time; if (IsActiveInHierarchy()) { REGISTER_TICK; } } void SceneAnimationPlayer::Pause() { if (_state != PlayState::Playing) return; if (IsActiveInHierarchy() && _state == PlayState::Playing) { UNREGISTER_TICK; } for (auto actor : _subActors) { if (auto audioSource = dynamic_cast(actor)) audioSource->Pause(); } _state = PlayState::Paused; } void SceneAnimationPlayer::Stop() { if (_state == PlayState::Stopped) return; if (IsActiveInHierarchy() && _state == PlayState::Playing) { UNREGISTER_TICK; } if (RestoreStateOnStop && _restoreData.HasItems()) { SceneAnimation* anim = Animation.Get(); if (anim && anim->IsLoaded()) { Restore(anim, 0); } } if (_isUsingCameraCuts && _cameraCutCam == Camera::CutSceneCamera) Camera::CutSceneCamera = _cameraCutCam = nullptr; _isUsingCameraCuts = false; for (auto actor : _subActors) { if (auto audioSource = dynamic_cast(actor)) audioSource->Stop(); } GetSceneRendering()->RemovePostFxProvider(this); _state = PlayState::Stopped; _time = _lastTime = 0.0f; _tracksDataStack.Resize(0); } void SceneAnimationPlayer::Tick(float dt) { // Reset temporary state _postFxSettings.PostFxMaterials.Materials.Clear(); _postFxSettings.CameraArtifacts.OverrideFlags &= ~CameraArtifactsSettings::Override::ScreenFadeColor; // Skip tick if animation asset is not ready for playback SceneAnimation* anim = Animation.Get(); if (!anim || !anim->IsLoaded()) return; // Setup state if (_tracks.Count() != anim->TrackStatesCount) { ResetState(); _tracks.Resize(anim->TrackStatesCount); } // Update timing float time = _time; if (Math::NearEqual(_lastTime, _time)) { // Delta time animation time += dt; } else { // Time was changed via SetTime } const float fps = anim->FramesPerSecond; const float duration = anim->DurationFrames / fps; if (time > duration) { if (Loop) { // Loop time = Math::Mod(time, duration); } else { // End Stop(); return; } } const Camera* prevCamera = Camera::GetMainCamera(); if (_isUsingCameraCuts && _cameraCutCam == Camera::CutSceneCamera) { Camera::CutSceneCamera = _cameraCutCam = nullptr; } _isUsingCameraCuts = false; _cameraCutCam = nullptr; // Tick the animation CallStack callStack; Tick(anim, time, dt, 0, callStack); #if !BUILD_RELEASE if (_tracksDataStack.Count() != 0) { _tracksDataStack.Resize(0); LOG(Warning, "Invalid track states data stack size."); } #endif if (_isUsingCameraCuts) { Camera::CutSceneCamera = _cameraCutCam; // Automatic camera-cuts injection for renderer if (prevCamera != Camera::GetMainCamera() && MainRenderTask::Instance) MainRenderTask::Instance->IsCameraCut = true; } // Update time _lastTime = _time = time; } void SceneAnimationPlayer::MapObject(const Guid& from, const Guid& to) { _objectsMapping[from] = to; } void SceneAnimationPlayer::MapTrack(const StringView& from, const Guid& to) { SceneAnimation* anim = Animation.Get(); if (!anim || !anim->IsLoaded()) return; for (int32 j = 0; j < anim->Tracks.Count(); j++) { const auto& track = anim->Tracks[j]; if (track.Name != from) continue; switch (track.Type) { case SceneAnimation::Track::Types::Actor: { const auto trackData = track.GetData(); _objectsMapping[trackData->ID] = to; return; } case SceneAnimation::Track::Types::Script: { const auto trackData = track.GetData(); _objectsMapping[trackData->ID] = to; return; } case SceneAnimation::Track::Types::CameraCut: { const auto trackData = track.GetData(); _objectsMapping[trackData->ID] = to; return; } default: ; } } LOG(Warning, "Missing track '{0}' in scene animation '{1}' to map into object ID={2}", from, anim->ToString(), to); } void SceneAnimationPlayer::Restore(SceneAnimation* anim, int32 stateIndexOffset) { #if USE_CSHARP // Restore all tracks for (int32 j = 0; j < anim->Tracks.Count(); j++) { const auto& track = anim->Tracks[j]; if (track.Disabled) continue; switch (track.Type) { case SceneAnimation::Track::Types::Actor: case SceneAnimation::Track::Types::Script: case SceneAnimation::Track::Types::CameraCut: { auto& state = _tracks[stateIndexOffset + track.TrackStateIndex]; state.ManagedObject = state.Object.GetOrCreateManagedInstance(); break; } case SceneAnimation::Track::Types::KeyframesProperty: case SceneAnimation::Track::Types::CurveProperty: case SceneAnimation::Track::Types::StringProperty: case SceneAnimation::Track::Types::ObjectReferenceProperty: case SceneAnimation::Track::Types::StructProperty: case SceneAnimation::Track::Types::ObjectProperty: { if (track.ParentIndex == -1) break; auto& state = _tracks[stateIndexOffset + track.TrackStateIndex]; const auto& parentTrack = anim->Tracks[track.ParentIndex]; // Skip if cannot restore state if (parentTrack.Type == SceneAnimation::Track::Types::StructProperty || state.RestoreStateIndex == -1 || (state.Field == nullptr && state.Property == nullptr)) break; MObject* instance = _tracks[stateIndexOffset + parentTrack.TrackStateIndex].ManagedObject; if (!instance) break; // Get the value data void* value; switch (track.Type) { case SceneAnimation::Track::Types::StringProperty: value = &_restoreData[state.RestoreStateIndex]; value = MUtils::ToString(StringView((Char*)value)); break; case SceneAnimation::Track::Types::ObjectReferenceProperty: { value = &_restoreData[state.RestoreStateIndex]; Guid id = *(Guid*)value; _objectsMapping.TryGet(id, id); auto obj = Scripting::FindObject(id); value = obj ? obj->GetOrCreateManagedInstance() : nullptr; break; } case SceneAnimation::Track::Types::ObjectProperty: { if (state.Property) { MObject* exception = nullptr; state.ManagedObject = state.Property->GetValue(instance, &exception); if (exception) { MException ex(exception); ex.Log(LogType::Error, TEXT("Property")); state.ManagedObject = nullptr; } } else { state.Field->GetValue(instance, &state.ManagedObject); } value = state.ManagedObject; break; } default: value = &_restoreData[state.RestoreStateIndex]; } // Set the value if (state.Property) { MObject* exception = nullptr; state.Property->SetValue(instance, value, &exception); if (exception) { MException ex(exception); ex.Log(LogType::Error, TEXT("Property")); } } else { state.Field->SetValue(instance, value); } break; } default: ; } } #endif } bool SceneAnimationPlayer::TickPropertyTrack(int32 trackIndex, int32 stateIndexOffset, SceneAnimation* anim, float time, const SceneAnimation::Track& track, TrackInstance& state, void* target) { #if USE_CSHARP switch (track.Type) { case SceneAnimation::Track::Types::KeyframesProperty: case SceneAnimation::Track::Types::ObjectReferenceProperty: { const auto trackRuntime = track.GetRuntimeData(); const int32 count = trackRuntime->KeyframesCount; if (count == 0) return false; // If size is 0 then track uses Json storage for keyframes data (variable memory length of keyframes), otherwise it's optimized simple data with O(1) access if (trackRuntime->ValueSize != 0) { // Find the keyframe at time (binary search) int32 keyframeSize = sizeof(float) + trackRuntime->ValueSize; #define GET_KEY_TIME(idx) *(float*)((byte*)trackRuntime->Keyframes + keyframeSize * (idx)) const float keyTime = Math::Clamp(time, 0.0f, GET_KEY_TIME(count - 1)); int32 start = 0; int32 searchLength = count; while (searchLength > 0) { const int32 half = searchLength >> 1; int32 mid = start + half; if (keyTime < GET_KEY_TIME(mid)) { searchLength = half; } else { start = mid + 1; searchLength -= half + 1; } } int32 leftKey = Math::Max(0, start - 1); #undef GET_KEY_TIME // Return the value void* value = (void*)((byte*)trackRuntime->Keyframes + keyframeSize * (leftKey) + sizeof(float)); if (track.Type == SceneAnimation::Track::Types::ObjectReferenceProperty) { // Object ref track uses Guid for object Id storage Guid id = *(Guid*)value; _objectsMapping.TryGet(id, id); auto obj = Scripting::FindObject(id); value = obj ? obj->GetOrCreateManagedInstance() : nullptr; *(void**)target = value; } else { // POD memory Platform::MemoryCopy(target, value, trackRuntime->ValueSize); } } else { // Clear pointer *(void**)target = nullptr; // Find the keyframe at time (linear search) MemoryReadStream stream((byte*)trackRuntime->Keyframes, trackRuntime->KeyframesSize); int32 prevKeyPos = sizeof(float); int32 jsonLen; for (int32 key = 0; key < count; key++) { float keyTime; stream.ReadFloat(&keyTime); if (keyTime > time) break; prevKeyPos = stream.GetPosition(); stream.ReadInt32(&jsonLen); stream.Move(jsonLen); } // Read json text stream.SetPosition(prevKeyPos); stream.ReadInt32(&jsonLen); const StringAnsiView json((const char*)stream.GetPositionHandle(), jsonLen); // Create empty value of the keyframe type const auto trackData = track.GetData(); const StringAnsiView propertyTypeName(trackRuntime->PropertyTypeName, trackData->PropertyTypeNameLength); MClass* klass = Scripting::FindClass(propertyTypeName); if (!klass) return false; MObject* obj = MCore::Object::New(klass); if (!obj) return false; if (!klass->IsValueType()) MCore::Object::Init(obj); // Deserialize value from json ManagedSerialization::Deserialize(json, obj); // Set value *(void**)target = obj; } break; } case SceneAnimation::Track::Types::CurveProperty: { const auto trackDataKeyframes = track.GetRuntimeData(); const int32 count = trackDataKeyframes->KeyframesCount; if (count == 0) return false; // Evaluate the curve byte valueData[sizeof(Double4)]; void* curveDst = trackDataKeyframes->DataType == trackDataKeyframes->ValueType ? target : valueData; switch (trackDataKeyframes->DataType) { #define IMPL_CURVE(dataType, valueType) \ case SceneAnimation::CurvePropertyTrack::DataTypes::dataType: \ { \ CurveBase> volumeCurve; \ CurveBase>::KeyFrameData data((BezierCurveKeyframe*)trackDataKeyframes->Keyframes, trackDataKeyframes->KeyframesCount); \ static_assert(sizeof(valueData) >= sizeof(valueType), "Invalid valueData size."); \ volumeCurve.Evaluate(data, *(valueType*)curveDst, time, false); \ break; \ } IMPL_CURVE(Float, float); IMPL_CURVE(Double, double); IMPL_CURVE(Float2, Float2); IMPL_CURVE(Float3, Float3); IMPL_CURVE(Float4, Float4); IMPL_CURVE(Double2, Double2); IMPL_CURVE(Double3, Double3); IMPL_CURVE(Double4, Double4); IMPL_CURVE(Quaternion, Quaternion); IMPL_CURVE(Color, Color); IMPL_CURVE(Color32, Color32); #undef IMPL_CURVE default: ; } if (trackDataKeyframes->DataType != trackDataKeyframes->ValueType) { // Convert evaluated curve data into the runtime type (eg. when using animation saved with Vector3=Double3 and playing it in a build with Vector3=Float3) switch (trackDataKeyframes->DataType) { // Assume just Vector type converting case SceneAnimation::CurvePropertyTrack::DataTypes::Float2: *(Double2*)target = *(Float2*)valueData; break; case SceneAnimation::CurvePropertyTrack::DataTypes::Float3: *(Double3*)target = *(Float3*)valueData; break; case SceneAnimation::CurvePropertyTrack::DataTypes::Float4: *(Double4*)target = *(Float4*)valueData; break; case SceneAnimation::CurvePropertyTrack::DataTypes::Double2: *(Float2*)target = *(Double2*)valueData; break; case SceneAnimation::CurvePropertyTrack::DataTypes::Double3: *(Float3*)target = *(Double3*)valueData; break; case SceneAnimation::CurvePropertyTrack::DataTypes::Double4: *(Float4*)target = *(Double4*)valueData; break; } } break; } case SceneAnimation::Track::Types::StringProperty: { const auto trackRuntime = track.GetRuntimeData(); const int32 count = trackRuntime->KeyframesCount; if (count == 0) return false; const auto keyframesTimes = (float*)((byte*)trackRuntime + sizeof(SceneAnimation::StringPropertyTrack::Runtime)); const auto keyframesLengths = (int32*)((byte*)keyframesTimes + sizeof(float) * trackRuntime->KeyframesCount); const auto keyframesValues = (Char**)((byte*)keyframesLengths + sizeof(int32) * trackRuntime->KeyframesCount); // Find the keyframe at time #define GET_KEY_TIME(idx) keyframesTimes[idx] const float keyTime = Math::Clamp(time, 0.0f, GET_KEY_TIME(count - 1)); int32 start = 0; int32 searchLength = count; while (searchLength > 0) { const int32 half = searchLength >> 1; int32 mid = start + half; if (keyTime < GET_KEY_TIME(mid)) { searchLength = half; } else { start = mid + 1; searchLength -= half + 1; } } int32 leftKey = Math::Max(0, start - 1); #undef GET_KEY_TIME // Return the value StringView str(keyframesValues[leftKey], keyframesLengths[leftKey]); *(MString**)target = MUtils::ToString(str); break; } case SceneAnimation::Track::Types::StructProperty: { // Evaluate all child tracks for (int32 childTrackIndex = trackIndex + 1; childTrackIndex < anim->Tracks.Count(); childTrackIndex++) { auto& childTrack = anim->Tracks[childTrackIndex]; if (childTrack.Disabled || childTrack.ParentIndex != trackIndex) continue; const auto childTrackRuntime = childTrack.GetRuntimeData(); auto& childTrackState = _tracks[stateIndexOffset + childTrack.TrackStateIndex]; // Cache field if (!childTrackState.Field) { MType* type = state.Property ? state.Property->GetType() : (state.Field ? state.Field->GetType() : nullptr); if (!type) continue; MClass* mclass = MCore::Type::GetClass(type); childTrackState.Field = mclass->GetField(childTrackRuntime->PropertyName); if (!childTrackState.Field) continue; } // Sample child track TickPropertyTrack(childTrackIndex, stateIndexOffset, anim, time, childTrack, childTrackState, (byte*)target + childTrackState.Field->GetOffset()); } break; } case SceneAnimation::Track::Types::ObjectProperty: { // Cache the sub-object pointer for the sub-tracks state.ManagedObject = *(MObject**)target; return false; } default: ; } #endif return true; } void SceneAnimationPlayer::Tick(SceneAnimation* anim, float time, float dt, int32 stateIndexOffset, CallStack& callStack) { #if USE_CSHARP const float fps = anim->FramesPerSecond; #if !BUILD_RELEASE || USE_EDITOR callStack.Add(anim); #endif // Update all tracks for (int32 j = 0; j < anim->Tracks.Count(); j++) { const auto& track = anim->Tracks[j]; if (track.Disabled) continue; switch (track.Type) { case SceneAnimation::Track::Types::PostProcessMaterial: { const auto runtimeData = track.GetRuntimeData(); for (int32 k = 0; k < runtimeData->Count; k++) { const auto& media = runtimeData->Media[k]; const float startTime = media.StartFrame / fps; const float durationTime = media.DurationFrames / fps; const bool isActive = Math::IsInRange(time, startTime, startTime + durationTime); if (isActive && _postFxSettings.PostFxMaterials.Materials.Count() < POST_PROCESS_SETTINGS_MAX_MATERIALS) { _postFxSettings.PostFxMaterials.Materials.Add(track.Asset.As()); break; } } break; } case SceneAnimation::Track::Types::NestedSceneAnimation: { const auto nestedAnim = track.Asset.As(); if (!nestedAnim || !nestedAnim->IsLoaded()) break; const auto trackData = track.GetData(); const float startTime = trackData->StartFrame / fps; const float durationTime = trackData->DurationFrames / fps; const bool loop = ((int32)track.Flag & (int32)SceneAnimation::Track::Flags::Loop) == (int32)SceneAnimation::Track::Flags::Loop; float mediaTime = time - startTime; if (mediaTime >= 0.0f && mediaTime <= durationTime) { const float mediaDuration = nestedAnim->DurationFrames / nestedAnim->FramesPerSecond; if (mediaTime > mediaDuration) { // Loop or clamp at the end if (loop) mediaTime = Math::Mod(mediaTime, mediaDuration); else mediaTime = mediaDuration; } // Validate state data space if (stateIndexOffset + nestedAnim->TrackStatesCount > _tracks.Count()) { LOG(Warning, "Not enough tracks state data buckets. Has {0} but need {1}. Animation {2} for nested track {3} on actor {4}.", _tracks.Count(), stateIndexOffset + nestedAnim->TrackStatesCount, Animation.Get()->ToString(), nestedAnim->ToString(), ToString()); return; } #if !BUILD_RELEASE || USE_EDITOR // Validate recursive call if (callStack.Contains(nestedAnim)) { LOG(Warning, "Recursive nested scene animation. Animation {0} for nested track {1} on actor {2}.", callStack.Last()->ToString(), nestedAnim->ToString(), ToString()); return; } #endif Tick(nestedAnim, mediaTime, dt, stateIndexOffset + track.TrackStateIndex, callStack); } break; } case SceneAnimation::Track::Types::ScreenFade: { const auto trackData = track.GetData(); const float startTime = trackData->StartFrame / fps; const float durationTime = trackData->DurationFrames / fps; const float mediaTime = time - startTime; if (mediaTime >= 0.0f && mediaTime <= durationTime) { const auto runtimeData = track.GetRuntimeData(); _postFxSettings.CameraArtifacts.OverrideFlags |= CameraArtifactsSettings::Override::ScreenFadeColor; Color& color = _postFxSettings.CameraArtifacts.ScreenFadeColor; const int32 count = trackData->GradientStopsCount; const auto stops = runtimeData->GradientStops; if (mediaTime >= stops[count - 1].Frame / fps) { // Outside the range color = stops[count - 1].Value; } else { // Find 2 samples to blend between them float prevTime = stops[0].Frame / fps; Color prevColor = stops[0].Value; for (int32 i = 1; i < count; i++) { const float curTime = stops[i].Frame / fps; const Color curColor = stops[i].Value; if (mediaTime <= curTime) { color = Color::Lerp(prevColor, curColor, Math::Saturate((mediaTime - prevTime) / (curTime - prevTime))); break; } prevTime = curTime; prevColor = curColor; } } } break; } case SceneAnimation::Track::Types::Audio: { const auto clip = track.Asset.As(); if (!clip || !clip->IsLoaded()) break; const auto runtimeData = track.GetRuntimeData(); float mediaTime = -1, mediaDuration, playTime; for (int32 k = 0; k < runtimeData->Count; k++) { const auto& media = runtimeData->Media[k]; const float startTime = media.StartFrame / fps; const float durationTime = media.DurationFrames / fps; if (Math::IsInRange(time, startTime, startTime + durationTime)) { mediaTime = time - startTime; playTime = mediaTime + media.Offset; mediaDuration = durationTime; break; } } auto& state = _tracks[stateIndexOffset + track.TrackStateIndex]; auto audioSource = state.Object.As(); if (mediaTime >= 0.0f && mediaTime <= mediaDuration) { const bool loop = ((int32)track.Flag & (int32)SceneAnimation::Track::Flags::Loop) == (int32)SceneAnimation::Track::Flags::Loop; if (!audioSource) { // Spawn audio source to play the clip audioSource = New(); audioSource->SetStaticFlags(StaticFlags::None); audioSource->HideFlags = HideFlags::FullyHidden; audioSource->Clip = clip; audioSource->SetIsLooping(loop); audioSource->SetParent(this, false, false); _subActors.Add(audioSource); state.Object = audioSource; } // Sample volume track float volume = 1.0f; if (runtimeData->VolumeTrackIndex != -1) { SceneAnimation::AudioVolumeTrack::CurveType volumeCurve(volume); const auto volumeTrackRuntimeData = (anim->Tracks[runtimeData->VolumeTrackIndex].GetRuntimeData()); if (volumeTrackRuntimeData) { SceneAnimation::AudioVolumeTrack::CurveType::KeyFrameData data(volumeTrackRuntimeData->Keyframes, volumeTrackRuntimeData->KeyframesCount); const auto& firstMedia = runtimeData->Media[0]; auto firstMediaTime = time - firstMedia.StartFrame / fps; volumeCurve.Evaluate(data, volume, firstMediaTime, false); } } const float clipLength = clip->GetLength(); if (loop) { // Loop position playTime = Math::Mod(playTime, clipLength); } else if (playTime >= clipLength) { // Stop updating after end break; } // Sync playback options audioSource->SetPitch(Speed); audioSource->SetVolume(volume); #if USE_EDITOR // Sync more in editor for better changes preview audioSource->Clip = clip; audioSource->SetIsLooping(loop); #endif // Synchronize playback position const float maxAudioLag = 0.3f; const auto audioTime = audioSource->GetTime(); //LOG(Info, "Audio: {0}, Media : {1}", audioTime, playTime); if (Math::Abs(audioTime - playTime) > maxAudioLag && Math::Abs(audioTime + clipLength - playTime) > maxAudioLag && Math::Abs(playTime + clipLength - audioTime) > maxAudioLag) { audioSource->SetTime(playTime); //LOG(Info, "Set Time (current audio time: {0})", audioSource->GetTime()); } // Keep playing if (_state == PlayState::Playing) audioSource->Play(); else audioSource->Pause(); } else if (audioSource) { // End playback audioSource->Stop(); } break; } case SceneAnimation::Track::Types::AudioVolume: { // Audio track samples the volume curve itself break; } case SceneAnimation::Track::Types::Actor: { // Cache actor to animate auto& state = _tracks[stateIndexOffset + track.TrackStateIndex]; if (!state.Object) { state.ManagedObject = nullptr; // Find actor const auto trackData = track.GetData(); Guid id = trackData->ID; _objectsMapping.TryGet(id, id); state.Object = Scripting::TryFindObject(id); if (!state.Object) { if (state.Warn) LOG(Warning, "Failed to find {3} of ID={0} for track '{1}' in scene animation '{2}'", id, track.Name, anim->ToString(), TEXT("actor")); state.Warn = false; break; } } state.ManagedObject = state.Object.GetOrCreateManagedInstance(); break; } case SceneAnimation::Track::Types::Script: { // Cache script to animate auto& state = _tracks[stateIndexOffset + track.TrackStateIndex]; if (!state.Object) { state.ManagedObject = nullptr; // Skip if parent track actor is missing const auto trackData = track.GetData(); ASSERT(track.ParentIndex != -1 && (anim->Tracks[track.ParentIndex].Type == SceneAnimation::Track::Types::Actor || anim->Tracks[track.ParentIndex].Type == SceneAnimation::Track::Types::CameraCut)); const auto& parentTrack = anim->Tracks[track.ParentIndex]; const auto& parentState = _tracks[stateIndexOffset + parentTrack.TrackStateIndex]; const auto parentActor = parentState.Object.As(); if (parentActor == nullptr) break; // Find script Guid id = trackData->ID; _objectsMapping.TryGet(id, id); state.Object = Scripting::TryFindObject