Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added Changelog/0.improvement.rst
Empty file.
121 changes: 104 additions & 17 deletions Source/UnrealMLAgents/Private/Academy.cpp
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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)
Expand Down Expand Up @@ -77,7 +64,7 @@ void UAcademy::InitializeEnvironment()
{
UE_LOG(LogTemp, Log, TEXT("Initialize Environement"));

bEnableStepping = true;
SetAutomaticSteppingEnabled(true);
ParseCommandLineArgs();

RpcCommunicator = NewObject<URpcCommunicator>();
Expand Down Expand Up @@ -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())
{
Expand Down Expand Up @@ -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<AAcademyStepper>(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<const AActor>(MaybeOwner);
return StepperActor.IsValid() && (StepperActor.Get() == OwningActor);
}
48 changes: 48 additions & 0 deletions Source/UnrealMLAgents/Private/AcademyStepper.cpp
Original file line number Diff line number Diff line change
@@ -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();
}
83 changes: 46 additions & 37 deletions Source/UnrealMLAgents/Public/UnrealMLAgents/Academy.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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();

Expand All @@ -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.

/**
Expand Down Expand Up @@ -247,16 +236,36 @@ 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;

/// Communicator used for interacting with remote agents or policies.
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<class AAcademyStepper> StepperActor;

/** @brief World that owns the stepper (used for validation). */
TWeakObjectPtr<UWorld> OwningWorld;

/**
* @brief Lazily initializes the Academy.
*
Expand Down
46 changes: 46 additions & 0 deletions Source/UnrealMLAgents/Public/UnrealMLAgents/AcademyStepper.h
Original file line number Diff line number Diff line change
@@ -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;
};