게임 엔진/Unreal

[Unreal/C++] "Unable to destroy previously allocated sequence instance - this could indicate a memory leak." 에러 처리

niamdank 2023. 6. 17. 18:08

정석적인 방법은 Component 등 플레이 마다 Begin과 End 함수가 항상 동작하는 액터에서 시퀀스 플레이어를 관리하여 Begin에 생성, End에 제거하는 식으로 사용하는 것이다.

코드 참고 - ActorSequenceComponent

코드 참고 - ActorSequenceComponent

void UActorSequenceComponent::BeginPlay()
{
   Super::BeginPlay();

   if (Sequence != nullptr)
   {
      SequencePlayer = NewObject<UActorSequencePlayer>(this, "SequencePlayer");

      SequencePlayer->SetPlaybackClient(this);

      // Initialize this player for tick as soon as possible to ensure that a persistent
      // reference to the tick manager is maintained
      SequencePlayer->InitializeForTick(this);

      SequencePlayer->Initialize(Sequence, PlaybackSettings);

      if (PlaybackSettings.bAutoPlay)
      {
         SequencePlayer->Play();
      }
   }
}

void UActorSequenceComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
   if (SequencePlayer)
   {
      // Stop the internal sequence player during EndPlay when it's safer
      // to modify other actor's state during state restoration.
      SequencePlayer->Stop();
      SequencePlayer->TearDown();
   }
   
   Super::EndPlay(EndPlayReason);
}

 

약간의 꼼수를 부리자면 SequencePlayer의 엔진 코드에 Reset 함수를 추가하여 assert에 걸리는 부분을 해결해주는 것이다.

void UMovieSceneSequencePlayer::Reset()
{
   RegisteredTickInterval.Reset();
   RootTemplateInstance = FMovieSceneRootEvaluationTemplateInstance();
}

 

크래시가 발생하는 이유는 크게 두 가지다.

1. check(RunnerToUse);가 없다고 나옴->이유: TickManager는 UPROPERTY(transient)라서 게임 끄면 사라짐. 그런데 RegisteredTickInterval은 한 번 셋 되면 에디터 끄지 않으면 살아있음 -> 등록 안 하고 찾으려고 하게 됨.

코드 참고 - SceneSequencePlayer::Initialize

코드 참고 - SceneSequencePlayer::Initialize

void UMovieSceneSequencePlayer::Initialize(UMovieSceneSequence* InSequence)
{
   check(InSequence);
   check(!bIsEvaluating);

   // If we have a valid sequence that may have been played back,
   // Explicitly stop and tear down the template instance before 
   // reinitializing it with the new sequence. Care should be taken
   // here that Stop is not called on the first Initialization as this
   // may be called during PostLoad.
   if (Sequence)
   {
      StopAtCurrentTime();
   }

   Sequence = InSequence;

   FFrameTime StartTimeWithOffset = StartTime;

   EUpdateClockSource ClockToUse = EUpdateClockSource::Tick;

   UMovieScene* MovieScene = Sequence->GetMovieScene();
   if (MovieScene)
   {
      EMovieSceneEvaluationType EvaluationType    = MovieScene->GetEvaluationType();
      FFrameRate                TickResolution    = MovieScene->GetTickResolution();
      FFrameRate                DisplayRate       = MovieScene->GetDisplayRate();

      UE_LOG(LogMovieScene, Verbose, TEXT("Initialize - MovieSceneSequence: %s, TickResolution: %f, DisplayRate: %d, CurrentTime: %d"), *InSequence->GetName(), TickResolution.Numerator, DisplayRate.Numerator);

      // We set the play position in terms of the display rate,
      // but want evaluation ranges in the moviescene's tick resolution
      PlayPosition.SetTimeBase(DisplayRate, TickResolution, EvaluationType);

      {
         // Set up the default frame range from the sequence's play range
         TRange<FFrameNumber> PlaybackRange   = MovieScene->GetPlaybackRange();

         const FFrameNumber SrcStartFrame = UE::MovieScene::DiscreteInclusiveLower(PlaybackRange);
         const FFrameNumber SrcEndFrame   = UE::MovieScene::DiscreteExclusiveUpper(PlaybackRange);

         const FFrameTime EndingTime = ConvertFrameTime(SrcEndFrame, TickResolution, DisplayRate);

         const FFrameNumber StartingFrame = ConvertFrameTime(SrcStartFrame, TickResolution, DisplayRate).FloorToFrame();
         const FFrameNumber EndingFrame   = EndingTime.FloorToFrame();

         SetFrameRange(StartingFrame.Value, (EndingFrame - StartingFrame).Value, EndingTime.GetSubFrame());
      }

      // Reset the play position based on the user-specified start offset, or a random time
      FFrameTime SpecifiedStartOffset = PlaybackSettings.StartTime * DisplayRate;

      // Setup the starting time
      FFrameTime StartingTimeOffset = PlaybackSettings.bRandomStartTime
         ? FFrameTime(FMath::Rand() % GetFrameDuration())
         : FMath::Clamp<FFrameTime>(SpecifiedStartOffset, 0, GetFrameDuration()-1);
         
      StartTimeWithOffset = StartTime + StartingTimeOffset;

      ClockToUse = MovieScene->GetClockSource();

      if (ClockToUse == EUpdateClockSource::Custom)
      {
         TimeController = MovieScene->MakeCustomTimeController(GetPlaybackContext());
      }
   }

   if (!TimeController.IsValid())
   {
      switch (ClockToUse)
      {
      case EUpdateClockSource::Audio:    TimeController = MakeShared<FMovieSceneTimeController_AudioClock>();    break;
      case EUpdateClockSource::Platform: TimeController = MakeShared<FMovieSceneTimeController_PlatformClock>(); break;
      case EUpdateClockSource::RelativeTimecode: TimeController = MakeShared<FMovieSceneTimeController_RelativeTimecodeClock>(); break;
      case EUpdateClockSource::Timecode: TimeController = MakeShared<FMovieSceneTimeController_TimecodeClock>(); break;
      case EUpdateClockSource::PlayEveryFrame: TimeController = MakeShared<FMovieSceneTimeController_PlayEveryFrame>(); break;
      default:                           TimeController = MakeShared<FMovieSceneTimeController_Tick>();          break;
      }

      if (!ensureMsgf(TimeController.IsValid(), TEXT("No time controller specified for sequence playback. Falling back to Engine Tick clock source.")))
      {
         TimeController = MakeShared<FMovieSceneTimeController_Tick>();
      }
   }

   if (!TickManager)
   {
      InitializeForTick(GetPlaybackContext());
   }

   FMovieSceneSequenceTickInterval TickInterval = PlaybackSettings.bInheritTickIntervalFromOwner
      ? FMovieSceneSequenceTickInterval::GetInheritedInterval(this)
      : PlaybackSettings.TickInterval;

   // If we haven't registered with the tick manager yet, register directly
   if (!RegisteredTickInterval.IsSet())
   {
      TickManager->RegisterTickClient(TickInterval, this);
   }
   // If we were already registered with a different Tick Interval we need to re-register with the new one, which involves tearing everything down and setting up a new instance
   else if (RegisteredTickInterval.GetValue() != TickInterval)
   {
      RootTemplateInstance.BeginDestroy();
      TickManager->UnregisterTickClient(this);

      TickManager->RegisterTickClient(RegisteredTickInterval.GetValue(), this);
   }

   RegisteredTickInterval = TickInterval;

   TSharedPtr<FMovieSceneEntitySystemRunner> RunnerToUse = TickManager->GetRunner(RegisteredTickInterval.GetValue());
   if (EnumHasAnyFlags(Sequence->GetFlags(), EMovieSceneSequenceFlags::BlockingEvaluation))
   {
      SynchronousRunner = MakeShared<FMovieSceneEntitySystemRunner>();
      RunnerToUse = SynchronousRunner;
   }

   check(RunnerToUse);
   RootTemplateInstance.Initialize(*Sequence, *this, nullptr, RunnerToUse);

   LatentActionManager.ClearLatentActions();

   // Set up playback position (with offset) after Stop(), which will reset the starting time to StartTime
   PlayPosition.Reset(StartTimeWithOffset);
   TimeController->Reset(GetCurrentTime());

   UpdateNetworkSyncProperties();
}

 

2. TemplateInitialize시 ensureMsgf(false, TEXT("Unable t... 에 걸림 -> 이유: RootInstanceHandle가 Valid로 체크 되면서 이거 가지고 뭘 하려고 하는데 실제로는 아무것도 없어서.

코드 참고 - RootEvaluationTemplateInstance::Initialize

코드 참고 - RootEvaluationTemplateInstance::Initialize

void FMovieSceneRootEvaluationTemplateInstance::Initialize(UMovieSceneSequence& InRootSequence, IMovieScenePlayer& Player, UMovieSceneCompiledDataManager* InCompiledDataManager, TWeakPtr<FMovieSceneEntitySystemRunner> InWeakRunner)
{
   bool bReinitialize = (
         // Initialize if we weren't initialized before and this is our first sequence.
         WeakRootSequence.Get() == nullptr ||
         // Initialize if we lost our linker.
         EntitySystemLinker == nullptr);

   const UMovieSceneCompiledDataManager* PreviousCompiledDataManager = CompiledDataManager;
   if (InCompiledDataManager)
   {
      CompiledDataManager = InCompiledDataManager;
   }
   else
   {
#if WITH_EDITOR
      EMovieSceneServerClientMask Mask = EmulatedNetworkMask;
      if (Mask == EMovieSceneServerClientMask::All)
      {
         UObject* PlaybackContext = Player.GetPlaybackContext();
         UWorld* World = PlaybackContext ? PlaybackContext->GetWorld() : nullptr;

         if (World)
         {
            ENetMode NetMode = World->GetNetMode();
            if (NetMode == ENetMode::NM_DedicatedServer)
            {
               Mask = EMovieSceneServerClientMask::Server;
            }
            else if (NetMode == ENetMode::NM_Client)
            {
               Mask = EMovieSceneServerClientMask::Client;
            }
         }
      }
      CompiledDataManager = UMovieSceneCompiledDataManager::GetPrecompiledData(Mask);
#else
      CompiledDataManager = UMovieSceneCompiledDataManager::GetPrecompiledData();
#endif
   }
   bReinitialize |= (PreviousCompiledDataManager != CompiledDataManager);

   TSharedPtr<FMovieSceneEntitySystemRunner> PreviousRunner = WeakRunner.Pin();

   if (UMovieSceneSequence* ExistingSequence = WeakRootSequence.Get())
   {
      if (ExistingSequence != &InRootSequence)
      {
         bReinitialize = true;
      }
   }

   CompiledDataID = CompiledDataManager->GetDataID(&InRootSequence);
   WeakRootSequence = &InRootSequence;
   RootID = MovieSceneSequenceID::Root;
   WeakRunner = InWeakRunner;

   if (bReinitialize)
   {
      if (RootInstanceHandle.IsValid())
      {
         if (PreviousRunner)
         {
            PreviousRunner->AbandonAndDestroyInstance(RootInstanceHandle);
         }
         else if (EntitySystemLinker)
         {
            EntitySystemLinker->GetInstanceRegistry()->DestroyInstance(RootInstanceHandle);
         }
         else
         {
            ensureMsgf(false, TEXT("Unable to destroy previously allocated sequence instance - this could indicate a memory leak."));
         }
      }

      Player.State.PersistentEntityData.Reset();
      Player.State.PersistentSharedData.Reset();

      EntitySystemLinker = ConstructEntityLinker(Player);
      RootInstanceHandle = EntitySystemLinker->GetInstanceRegistry()->AllocateRootInstance(&Player);

      TSharedPtr<FMovieSceneEntitySystemRunner> Runner = WeakRunner.Pin();
      if (ensure(Runner) && EntitySystemLinker)
      {
         if (Runner->IsAttachedToLinker() && Runner->GetLinker() != EntitySystemLinker)
         {
            Runner->DetachFromLinker();
         }

         if (!Runner->IsAttachedToLinker())
         {
            Runner->AttachToLinker(EntitySystemLinker);
         }
      }

      Player.PreAnimatedState.Initialize(EntitySystemLinker, RootInstanceHandle);
   }
}