From 98528d7f74c8d7112ff327d3d2eb9b1281b3ab13 Mon Sep 17 00:00:00 2001 From: Stephane Capponi Date: Tue, 19 Aug 2025 14:38:24 +0200 Subject: [PATCH] Switch Academy ticking from FTickableGameObject to physics-driven actor --- Changelog/0.improvement.rst | 0 Source/UnrealMLAgents/Private/Academy.cpp | 121 +++++++++++++++--- .../UnrealMLAgents/Private/AcademyStepper.cpp | 48 +++++++ .../Public/UnrealMLAgents/Academy.h | 83 ++++++------ .../Public/UnrealMLAgents/AcademyStepper.h | 46 +++++++ 5 files changed, 244 insertions(+), 54 deletions(-) create mode 100644 Changelog/0.improvement.rst create mode 100644 Source/UnrealMLAgents/Private/AcademyStepper.cpp create mode 100644 Source/UnrealMLAgents/Public/UnrealMLAgents/AcademyStepper.h diff --git a/Changelog/0.improvement.rst b/Changelog/0.improvement.rst new file mode 100644 index 0000000..e69de29 diff --git a/Source/UnrealMLAgents/Private/Academy.cpp b/Source/UnrealMLAgents/Private/Academy.cpp index 91d65bd..2863098 100644 --- a/Source/UnrealMLAgents/Private/Academy.cpp +++ b/Source/UnrealMLAgents/Private/Academy.cpp @@ -1,6 +1,9 @@ // Copyright © 2025 Stephane Capponi and individual contributors. All Rights Reserved. #include "UnrealMLAgents/Academy.h" +#include "UnrealMLAgents/AcademyStepper.h" +#include "Engine/Engine.h" +#include "Engine/World.h" #if WITH_EDITOR #include "Editor/EditorEngine.h" @@ -22,22 +25,6 @@ UAcademy::UAcademy() #endif } -// Make the Academy Tick -bool UAcademy::IsTickable() const -{ - return true; -} - -void UAcademy::Tick(float DeltaTime) -{ - EnvironmentStep(); -} - -TStatId UAcademy::GetStatId() const -{ - RETURN_QUICK_DECLARE_CYCLE_STAT(ThisClassName, STATGROUP_Tickables); -} - UAcademy* UAcademy::GetInstance() { if (!Instance) @@ -77,7 +64,7 @@ void UAcademy::InitializeEnvironment() { UE_LOG(LogTemp, Log, TEXT("Initialize Environement")); - bEnableStepping = true; + SetAutomaticSteppingEnabled(true); ParseCommandLineArgs(); RpcCommunicator = NewObject(); @@ -191,6 +178,8 @@ void UAcademy::Dispose() void UAcademy::Dispose(bool bIsSimulating) { + DisableAutomaticStepping(); + // Signal to listeners that the academy is being destroyed now if (OnDestroyAction.IsBound()) { @@ -246,3 +235,101 @@ bool UAcademy::IsCommunicatorOn() { return RpcCommunicator != nullptr; } + +/** @brief Find a suitable PIE/Game world to spawn the stepper into. */ +UWorld* UAcademy::ResolveGameWorld() const +{ + if (OwningWorld.IsValid()) + { + return OwningWorld.Get(); + } + + if (GEngine) + { + // Prefer PIE or Game worlds + for (const FWorldContext& Ctx : GEngine->GetWorldContexts()) + { + if (Ctx.World() && (Ctx.WorldType == EWorldType::PIE || Ctx.WorldType == EWorldType::Game)) + { + return Ctx.World(); + } + } + // Fallback to any valid world + for (const FWorldContext& Ctx : GEngine->GetWorldContexts()) + { + if (Ctx.World()) + { + return Ctx.World(); + } + } + } + return nullptr; +} + +/** @brief Spawn the hidden stepper Actor and begin physics-phase stepping. */ +void UAcademy::EnableAutomaticStepping() +{ + if (StepperActor.IsValid()) + { + return; // already enabled + } + + if (UWorld* World = ResolveGameWorld()) + { + OwningWorld = World; + + FActorSpawnParameters Params; + Params.Name = TEXT("AcademyFixedUpdateStepper"); + Params.ObjectFlags |= RF_Transient; + Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + + AAcademyStepper* NewStepper = World->SpawnActor(Params); + if (NewStepper) + { + NewStepper->SetActorHiddenInGame(true); + StepperActor = NewStepper; + UE_LOG(LogTemp, Log, TEXT("Automatic stepping enabled using AAcademyStepper.")); + } + else + { + UE_LOG(LogTemp, Warning, TEXT("Failed to spawn AAcademyStepper. Automatic stepping disabled.")); + } + } + else + { + UE_LOG(LogTemp, Warning, TEXT("No valid world found; cannot enable automatic stepping yet.")); + } +} + +/** @brief Destroy the stepper Actor and stop automatic stepping. */ +void UAcademy::DisableAutomaticStepping() +{ + if (StepperActor.IsValid()) + { + if (AActor* Actor = StepperActor.Get()) + { + Actor->Destroy(); + } + StepperActor.Reset(); + } +} + +/** @brief Public API to flip automatic stepping on/off. */ +void UAcademy::SetAutomaticSteppingEnabled(bool bEnable) +{ + if (bEnable) + EnableAutomaticStepping(); + else + DisableAutomaticStepping(); +} + +/** @brief Ownership check used by the stepper to self-destruct if stale. */ +bool UAcademy::IsStepperOwner(const UObject* MaybeOwner) const +{ + if (!MaybeOwner) + { + return false; + } + const AActor* OwningActor = Cast(MaybeOwner); + return StepperActor.IsValid() && (StepperActor.Get() == OwningActor); +} diff --git a/Source/UnrealMLAgents/Private/AcademyStepper.cpp b/Source/UnrealMLAgents/Private/AcademyStepper.cpp new file mode 100644 index 0000000..4fe2cf2 --- /dev/null +++ b/Source/UnrealMLAgents/Private/AcademyStepper.cpp @@ -0,0 +1,48 @@ +// AAcademyStepper.cpp +#include "UnrealMLAgents/AcademyStepper.h" +#include "UnrealMLAgents/Academy.h" + +/* static analysis note: ensure this matches your module include path */ + +AAcademyStepper::AAcademyStepper() +{ + // Be a silent, transient helper Actor. + SetActorHiddenInGame(true); + SetCanBeDamaged(false); + SetActorEnableCollision(false); + SetReplicates(false); + SetFlags(RF_Transient); + + PrimaryActorTick.bCanEverTick = true; + PrimaryActorTick.bStartWithTickEnabled = true; + + // Unity's FixedUpdate analogue: run BEFORE physics simulation so forces/inputs apply this frame. + PrimaryActorTick.TickGroup = TG_PrePhysics; +} + +void AAcademyStepper::BeginPlay() +{ + Super::BeginPlay(); +} + +void AAcademyStepper::Tick(float DeltaSeconds) +{ + Super::Tick(DeltaSeconds); + + // If Academy isn't ready yet, do nothing. + if (!UAcademy::IsInitialized()) + { + return; + } + + UAcademy* Academy = UAcademy::GetInstance(); + + // Leak-proofing (PIE restarts/world switches): if we don't belong to the current Academy, self-destruct. + if (!Academy->IsStepperOwner(this)) + { + Destroy(); + return; + } + + Academy->EnvironmentStep(); +} diff --git a/Source/UnrealMLAgents/Public/UnrealMLAgents/Academy.h b/Source/UnrealMLAgents/Public/UnrealMLAgents/Academy.h index 2e25021..b62aa8f 100644 --- a/Source/UnrealMLAgents/Public/UnrealMLAgents/Academy.h +++ b/Source/UnrealMLAgents/Public/UnrealMLAgents/Academy.h @@ -8,7 +8,7 @@ #include "Communicator/RpcCommunicator.h" #include "RecursionChecker.h" #include "UnrealMLAgents/Policies/RemotePolicy.h" -#include "Tickable.h" +#include "UnrealMLAgents/AcademyStepper.h" #include "Academy.generated.h" // Define the delegate types @@ -66,41 +66,11 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnEnvironmentReset); * This class is designed as a singleton, ensuring that only one instance of the Academy is active at any time. */ UCLASS() -class UNREALMLAGENTS_API UAcademy : public UObject, public FTickableGameObject +class UNREALMLAGENTS_API UAcademy : public UObject { GENERATED_BODY() public: - /** - * @brief Updates the Academy at each tick, stepping through the environment's simulation. - * - * This method is called every frame (or every tick) and updates the simulation by stepping through - * the environment. It also processes agent actions and observations as needed. - * - * @param DeltaTime The time passed since the last frame, used to control the tick rate of the simulation. - */ - virtual void Tick(float DeltaTime) override; - - /** - * @brief Checks if the Academy is currently tickable. - * - * Determines whether the Academy should be ticked in the current frame. This is used to control when - * the Academy should update its internal state. - * - * @return True if the Academy should be updated every tick, false otherwise. - */ - virtual bool IsTickable() const override; - - /** - * @brief Retrieves the statistical ID used for performance monitoring. - * - * This method is used for tracking performance metrics within the simulation, allowing the Academy - * to report its usage to Unreal Engine’s stat system. - * - * @return The stat ID used for performance tracking. - */ - virtual TStatId GetStatId() const override; - /// Total number of steps taken in the simulation since the Academy started. int32 TotalStepCount; @@ -145,9 +115,10 @@ class UNREALMLAGENTS_API UAcademy : public UObject, public FTickableGameObject void Dispose(); /** - * @brief Steps through the environment, advancing the simulation. + * @brief Advances the environment by one simulation step. * - * This method processes a single simulation step, updating the environment and triggering agent actions. + * Called automatically by a hidden AAcademyStepper Actor when automatic stepping is enabled, + * or can be called manually in custom workflows. */ void EnvironmentStep(); @@ -169,6 +140,24 @@ class UNREALMLAGENTS_API UAcademy : public UObject, public FTickableGameObject */ bool IsCommunicatorOn(); + /** + * @brief Enable or disable Unity-style automatic stepping driven by a hidden Actor ticking in physics. + * @param bEnable True to enable, false to disable. + */ + void SetAutomaticSteppingEnabled(bool bEnable); + + /** + * @brief True if a stepper Actor is currently managing physics-phase stepping. + */ + bool IsAutomaticSteppingEnabled() const { return StepperActor.IsValid(); } + + /** + * @brief Ownership check used by the stepper to avoid leaks across world transitions. + * @param MaybeOwner Any UObject that claims to be the current stepper (Actor or Component). + * @return True if the provided object belongs to this Academy’s stepper Actor. + */ + bool IsStepperOwner(const UObject* MaybeOwner) const; + // Declare events related to agent and environment lifecycle. /** @@ -247,9 +236,6 @@ class UNREALMLAGENTS_API UAcademy : public UObject, public FTickableGameObject /// Whether the Academy has been fully initialized. bool bInitialized; - /// Whether stepping through the environment is enabled. - bool bEnableStepping; - /// Whether the first reset has occurred. bool bHadFirstReset; @@ -257,6 +243,29 @@ class UNREALMLAGENTS_API UAcademy : public UObject, public FTickableGameObject UPROPERTY() URpcCommunicator* RpcCommunicator; + // ---------- Unity-like automatic stepping support ---------- + + /** + * @brief Spawn the hidden stepper Actor and start physics-phase stepping. + */ + void EnableAutomaticStepping(); + + /** + * @brief Destroy the stepper Actor and stop automatic stepping. + */ + void DisableAutomaticStepping(); + + /** + * @brief Resolve a suitable UWorld (PIE or Game) to spawn the stepper into. + */ + UWorld* ResolveGameWorld() const; + + /** @brief Cached stepper Actor (hidden, transient). */ + TWeakObjectPtr StepperActor; + + /** @brief World that owns the stepper (used for validation). */ + TWeakObjectPtr OwningWorld; + /** * @brief Lazily initializes the Academy. * diff --git a/Source/UnrealMLAgents/Public/UnrealMLAgents/AcademyStepper.h b/Source/UnrealMLAgents/Public/UnrealMLAgents/AcademyStepper.h new file mode 100644 index 0000000..8df56d5 --- /dev/null +++ b/Source/UnrealMLAgents/Public/UnrealMLAgents/AcademyStepper.h @@ -0,0 +1,46 @@ +// AAcademyStepper.h +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "AcademyStepper.generated.h" + +/** + * @class AAcademyStepper + * @brief Hidden helper Actor that ticks during the physics phase and advances the ML environment. + * + * This class mirrors Unity's "hidden GameObject with a MonoBehaviour.FixedUpdate()" pattern. + * It is spawned by UAcademy when automatic stepping is enabled, and destroyed when disabled or on teardown. + * + * The Actor: + * - Ticks in the physics phase (TG_PostPhysics by default) to emulate Unity's FixedUpdate timing. + * - Calls UAcademy::EnvironmentStep() every physics tick. + * - Verifies it still belongs to the current UAcademy instance; if not, it self-destructs to avoid leaks + * across PIE restarts or world transitions (akin to Unity's IsStepperOwner check). + */ +UCLASS() +class UNREALMLAGENTS_API AAcademyStepper : public AActor +{ + GENERATED_BODY() + +public: + /** + * @brief Construct the stepper with physics-phase ticking enabled. + * + * The Actor is configured to be hidden, transient, non-replicated, and collisionless. + * We disable the Actor's own tick in favor of PrimaryActorTick, and set TickGroup to TG_PrePhysics. + */ + AAcademyStepper(); + + /** + * @brief Per-frame callback in the chosen tick group (physics phase). + * @param DeltaSeconds Time in seconds since last tick. + */ + virtual void Tick(float DeltaSeconds) override; + +protected: + /** + * @brief Called when play begins for the stepper Actor. + */ + virtual void BeginPlay() override; +};