Skip to content

Commit 84554bd

Browse files
committed
ScholaExamples v1.2.0
1 parent b09aa20 commit 84554bd

23 files changed

+300
-132
lines changed

Config/DefaultEngine.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ bUseManualIPAddress=False
233233
ManualIPAddress=
234234

235235
[/Script/Schola.ScholaManagerSubsystemSettings]
236-
bRunScriptOnPlay=False
236+
bRunScriptOnPlay=True
237237
GymConnectorClass=/Script/CoreUObject.Class'/Script/Schola.PythonGymConnector'
238238
ScriptSettings=(ScriptType=Python,EnvType=Default,CondaEnvName="",CustomPythonPath=(FilePath=""),PythonScriptType=SB3,CustomPythonScriptSettings=(LaunchScript=(FilePath=""),Args=(),Flags=),CustomScriptSettings=(LaunchScript=(FilePath=""),Args=(),Flags=),SB3Settings=(Timesteps=8000,LoggingSettings=(EnvLoggingVerbosity=0,TrainerLoggingVerbosity=0,bSaveTBLogs=False,LogFreq=10,LogDir=(Path=""),CallbackVerbosity=1),CheckpointSettings=(bSaveCheckpoints=False,bSaveFinalModel=False,bSaveVecNormalize=False,bSaveReplayBuffer=False,CheckpointDir=(Path=""),SaveFreq=1000,NamePrefix="ppo"),ResumeSettings=(bLoadModel=False,ModelPath=(FilePath=""),bLoadReplayBuffer=False,ReplayBufferPath=(FilePath=""),bLoadVecNormalize=False,VecNormalizePath=(FilePath="")),NetworkArchitectureSettings=(ActivationFunction=ReLU,ValueFunctionParameters=(256,256),PolicyParameters=(256,256)),bDisplayProgressBar=True,Algorithm=PPO,PPOSettings=(LearningRate=0.000300,NSteps=2048,BatchSize=64,NEpochs=10,Gamma=0.990000,GAELambda=0.950000,ClipRange=0.200000,NormalizeAdvantage=True,EntCoef=0.000000,VFCoef=0.050000,MaxGradNorm=0.500000,UseSDE=False,SDESampleFreq=-1),SACSettings=(LearningRate=0.000300,BufferSize=1000000,LearningStarts=100,BatchSize=256,Tau=0.005000,Gamma=0.990000,TrainFreq=1,GradientSteps=1,OptimizeMemoryUsage=False,LearnEntCoef=True,InitialEntCoef=1.000000,TargetUpdateInterval=1,TargetEntropy="auto",UseSDE=False,SDESampleFreq=-1)),RLlibSettings=(Timesteps=8000,LoggingSettings=(EnvLoggingVerbosity=0,TrainerLoggingVerbosity=1),CheckpointSettings=(bSaveFinalModel=True,bExportToONNX=False,bEnableCheckpoints=False,SaveFreq=1000,CheckpointDir=(Path="")),ResumeSettings=(bLoadModel=False,ModelPath=(FilePath="")),NetworkArchitectureSettings=(ActivationFunction=ReLU,FCNetHiddens=(512,512),MinibatchSize=256),ResourceSettings=(NumCPUsPerWorker=1,NumGPUs=0,NumCPUs=1)))
239239
CommunicatorSettings=(Address="127.0.0.1",Port=8002)

Plugins/Schola

Submodule Schola updated 51 files

pytest.ini

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ norecursedirs =
1616
__pycache__
1717
testpaths =
1818
python/tests
19-
engine_path = C:/Program Files/Epic Games/UE_5.5
19+
engine_path = C:/Program Files/Epic Games/UE_5.5
20+
clean_build = false

python/schola_examples/__init__.py

Lines changed: 3 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,6 @@
11
# Copyright (c) 2025 Advanced Micro Devices, Inc. All Rights Reserved.
2-
import sys
3-
import types
4-
from gymnasium.envs.registration import register
5-
from ray.tune.registry import register_env
6-
from schola.gym.env import GymVectorEnv
7-
from schola.sb3.env import VecEnv # todo: a bit slow, maybe figure out a way to delay the import upon first use
8-
from schola.ray.env import BaseEnv
9-
from .registry import envs
102

113

12-
# Register all environments with gym
13-
for env_id in envs.all():
14-
register(
15-
id = f"Schola/{env_id}-v0",
16-
vector_entry_point = lambda env_id=env_id, **kwargs: GymVectorEnv(envs.make_unreal_connection(env_id, **kwargs)),
17-
order_enforce = True,
18-
disable_env_checker = False,
19-
apply_api_compatibility = False
20-
)
21-
22-
23-
# Register all environments with Ray
24-
for env_id in envs.all():
25-
register_env(
26-
name = f"Schola/{env_id}-v0",
27-
env_creator = lambda env_id=env_id, **kwargs: BaseEnv(envs.make_unreal_connection(env_id, **kwargs)),)
28-
29-
30-
# Create a virtual module for 'gym'
31-
gym = types.ModuleType('schola_examples.gym')
32-
# Dynamically create functions like `Basic`, `BasicVec`, `BallShooter`, etc.
33-
for env_id in envs.all():
34-
func_name = env_id.replace('-', '')
35-
setattr(gym, func_name, lambda env_id=env_id, **kwargs: GymVectorEnv(envs.make_unreal_connection(env_id, **kwargs)) )
36-
sys.modules['schola_examples.gym'] = gym # Add the virtual module to sys.modules
37-
38-
39-
# Create a virtual module for 'sb3'
40-
sb3 = types.ModuleType('schola_examples.sb3')
41-
# Dynamically create functions like `Basic`, `BasicVec`, `BallShooter`, etc.
42-
for env_id in envs.all():
43-
func_name = env_id.replace('-', '')
44-
setattr(sb3, func_name, lambda env_id=env_id, **kwargs: VecEnv(envs.make_unreal_connection(env_id, **kwargs)) )
45-
sys.modules['schola_examples.sb3'] = sb3 # Add the virtual module to sys.modules
46-
47-
48-
# Create a virtual module for 'ray'
49-
ray = types.ModuleType('schola_examples.ray')
50-
# Dynamically create functions like `Basic`, `BasicVec`, `BallShooter`, etc.
51-
for env_id in envs.all():
52-
func_name = env_id.replace('-', '')
53-
setattr(ray, func_name, lambda env_id=env_id, **kwargs: BaseEnv(envs.make_unreal_connection(env_id, **kwargs)) )
54-
sys.modules['schola_examples.ray'] = ray # Add the virtual module to sys.modules
4+
import schola_examples.gym
5+
import schola_examples.ray
6+
import schola_examples.sb3

python/schola_examples/gym.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Copyright (c) 2025 Advanced Micro Devices, Inc. All Rights Reserved.
2+
3+
from gymnasium import register
4+
from schola.gym.env import GymEnv, GymVectorEnv
5+
from schola_examples.registry import envs
6+
7+
def make_gym_env(env_path):
8+
class PartialGymEnv(GymEnv):
9+
def __init__(self, **kwargs):
10+
super().__init__(envs.make_unreal_connection(env_path, **kwargs))
11+
return PartialGymEnv
12+
13+
def make_gym_vector_env(env_path):
14+
class PartialGymVectorEnv(GymVectorEnv):
15+
def __init__(self, **kwargs):
16+
super().__init__(envs.make_unreal_connection(env_path, **kwargs))
17+
return PartialGymVectorEnv
18+
19+
20+
for env in filter(lambda x: x.gym_compat, envs.all()):
21+
register(
22+
id = f"Schola/{env.name}-v0",
23+
entry_point= make_gym_env(env.path),
24+
vector_entry_point = make_gym_vector_env(env.vec_path),
25+
order_enforce = True,
26+
disable_env_checker = False,
27+
apply_api_compatibility = False
28+
)
29+
30+
for env in filter(lambda x: x.gym_compat, envs.all()):
31+
func_name = env.name.replace('-', '')
32+
globals()[func_name] = make_gym_env(env.path)
33+
func_name = env.vec_name.replace('-', '')
34+
globals()[func_name] = make_gym_vector_env(env.vec_path)

python/schola_examples/ray.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright (c) 2025 Advanced Micro Devices, Inc. All Rights Reserved.
2+
3+
from schola.ray.env import BaseEnv
4+
from .registry import envs
5+
from ray.tune.registry import register_env
6+
7+
def make_ray_env(env_path):
8+
class PartialRayEnv(BaseEnv):
9+
def __init__(self, **kwargs):
10+
super().__init__(envs.make_unreal_connection(env_path, **kwargs))
11+
return PartialRayEnv
12+
13+
for env in envs.all():
14+
func_name = env.name.replace('-', '')
15+
globals()[func_name] = make_ray_env(env.path)
16+
func_name = env.vec_name.replace('-', '')
17+
globals()[func_name] = make_ray_env(env.vec_path)
18+
19+
# Register all environments with Ray
20+
for env in envs.all():
21+
register_env(
22+
name = f"Schola/{env.name}-v0",
23+
env_creator = make_ray_env(env.path),
24+
)
25+
register_env(
26+
name = f"Schola/{env.vec_name}-v0",
27+
env_creator = make_ray_env(env.vec_path),
28+
)

python/schola_examples/registry.py

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,51 @@
11
# Copyright (c) 2025 Advanced Micro Devices, Inc. All Rights Reserved.
2+
from typing import NamedTuple
23
from schola.core.unreal_connections import StandaloneUnrealConnection
34
import os
45
import platform
56

7+
class EnvRegistryEntry:
8+
9+
def __init__(self,name,gym_compatible=True):
10+
self.name = name
11+
self.gym_compat = gym_compatible
12+
13+
@property
14+
def path(self):
15+
return f"/Game/Examples/{self.name}/Maps/{self.name}Train"
16+
17+
@property
18+
def vec_path(self):
19+
return f"/Game/Examples/{self.name}/Maps/{self.name}VecTrain"
20+
21+
@property
22+
def vec_name(self):
23+
return self.name + "-Vec"
24+
625
class ScholaExamplesEnvRegistry:
726
def __init__(self):
827
self.registry = {
9-
"Basic": "/Game/Examples/Basic/Maps/BasicTrain",
10-
"Basic-Vec": "/Game/Examples/Basic/Maps/BasicVecTrain",
11-
"3DBall": "/Game/Examples/3DBall/Maps/3DBallTrain",
12-
"3DBall-Vec": "/Game/Examples/3DBall/Maps/3DBallVecTrain",
13-
"BallShooter": "/Game/Examples/BallShooter/Maps/BallShooterTrain",
14-
"BallShooter-Vec": "/Game/Examples/BallShooter/Maps/BallShooterVecTrain",
15-
"BrickBreaker": "/Game/Examples/BrickBreaker/Maps/BrickBreakerTrain",
16-
"BrickBreaker-Vec": "/Game/Examples/BrickBreaker/Maps/BrickBreakerVecTrain",
17-
"MazeSolver": "/Game/Examples/MazeSolver/Maps/MazeSolverTrain",
18-
"MazeSolver-Vec": "/Game/Examples/MazeSolver/Maps/MazeSolverVecTrain",
19-
"Pong": "/Game/Examples/Pong/Maps/PongTrain",
20-
"Pong-Vec": "/Game/Examples/Pong/Maps/PongVecTrain",
21-
"RaceTrack": "/Game/Examples/RaceTrack/Maps/RaceTrackTrain",
22-
"RaceTrack-Vec": "/Game/Examples/RaceTrack/Maps/RaceTrackVecTrain",
23-
"Tag": "/Game/Examples/Tag/Maps/TagTrain",
24-
"Tag-Vec": "/Game/Examples/Tag/Maps/TagVecTrain",
28+
"Basic": EnvRegistryEntry("Basic"),
29+
"3DBall": EnvRegistryEntry("3DBall"),
30+
"BallShooter": EnvRegistryEntry("BallShooter"),
31+
"MazeSolver": EnvRegistryEntry("MazeSolver"),
32+
"Pong": EnvRegistryEntry("Pong",gym_compatible=False),
33+
"RaceTrack": EnvRegistryEntry("RaceTrack"),
34+
"Tag": EnvRegistryEntry("Tag",gym_compatible=False),
2535
}
2636
dir_path = os.path.dirname(os.path.realpath(__file__))
2737
os_name = "Windows" if platform.system() == "Windows" else "Linux"
2838
entry_point_filename = "ScholaExamples" + (".exe" if platform.system() == "Windows" else ".sh")
2939
self.built_game_path = os.path.join(dir_path, "bin", os_name, entry_point_filename)
3040

31-
def make_unreal_connection(self, env_id, port=None, headless_mode=False, set_fps=None, built_game_path=None, display_logs=False):
41+
def make_unreal_connection(self, path:str, port=None, headless_mode=False, set_fps=None, built_game_path=None, display_logs=False):
3242
"""
3343
Creates a new environment connection instance.
3444
3545
Parameters
3646
----------
37-
env_id : str
38-
The environment ID.
47+
path : str
48+
The Unreal Engine Path to the map to load.
3949
port : int, optional
4050
The port to connect to, if None, an open port will be found.
4151
headless_mode : bool, optional
@@ -57,15 +67,15 @@ def make_unreal_connection(self, env_id, port=None, headless_mode=False, set_fps
5767
executable_path=built_game_path if built_game_path else self.built_game_path,
5868
port=port,
5969
headless_mode=headless_mode,
60-
map=self.registry[env_id],
70+
map=path,
6171
display_logs=display_logs,
6272
set_fps=set_fps,
6373
disable_script=True)
64-
65-
def all(self):
74+
75+
def all(self) -> EnvRegistryEntry:
6676
"""
6777
Returns a list of all environment IDs.
6878
"""
69-
return list(self.registry.keys())
79+
return list(self.registry.values())
7080

7181
envs = ScholaExamplesEnvRegistry()

python/schola_examples/sb3.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright (c) 2025 Advanced Micro Devices, Inc. All Rights Reserved.
2+
3+
from schola.sb3.env import VecEnv
4+
from .registry import envs
5+
6+
def make_sb3_env(env_path):
7+
class PartialSB3Env(VecEnv):
8+
def __init__(self, **kwargs):
9+
super().__init__(envs.make_unreal_connection(env_path, **kwargs))
10+
return PartialSB3Env
11+
12+
# Dynamically create functions like `Basic`, `BasicVec`, `BallShooter`, etc.
13+
for env in envs.all():
14+
func_name = env.name.replace('-', '')
15+
# Add the class to this module
16+
globals()[func_name] = make_sb3_env(env.path)
17+
func_name = env.vec_name.replace('-', '')
18+
globals()[func_name] = make_sb3_env(env.vec_path)
19+

python/tests/Maps/Examples/__init__.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,12 @@
3838
# All environments that can be converted to vectorized environments, including single agent environments, or multiagent environments where all agents
3939
# have the same observation/action space
4040
# use this for SB3 and Gym as they can't handle agents with different obs/action spaces
41-
VEC_COMPATIBLE_EXAMPLES = list(
41+
GYM_VEC_COMPATIBLE_EXAMPLES = list(
4242
filter(lambda x: not x.has_different_agents, ALL_EXAMPLES)
4343
)
4444

45-
VEC_COMPATIBLE_VEC_EXAMPLES = list(
46-
filter(lambda x: not x.has_different_agents, VECTORIZED_EXAMPLES)
47-
)
48-
49-
VEC_COMPATIBLE_SINGLE_ENV_EXAMPLES = list(
50-
filter(lambda x: not x.has_different_agents, SINGLE_ENV_EXAMPLES)
45+
GYM_COMPATIBLE_EXAMPLES = list(
46+
filter(lambda x: not x.has_different_agents and x.total_num_agents == 1, ALL_EXAMPLES)
5147
)
5248

5349
# Can use this for testing whether asserts are triggered notifying the user that the env is unsuported

python/tests/Maps/Examples/gym.py

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
# Copyright (c) 2024 Advanced Micro Devices, Inc. All Rights Reserved.
1+
# Copyright (c) 2024-2025 Advanced Micro Devices, Inc. All Rights Reserved.
22

3+
from typing import Type, Union
34
from schola.core.unreal_connections import StandaloneUnrealConnection
4-
from schola.gym.env import GymVectorEnv
5+
from schola.gym.env import GymEnv, GymVectorEnv
56
import pytest
67
from schola.core.spaces import DictSpace, BoxSpace, DiscreteSpace
78
import numpy as np
@@ -11,14 +12,50 @@
1112

1213
from .common import UnrealExample
1314

15+
def make_env(build_path, example, target_class = GymEnv):
16+
if isinstance(target_class,str):
17+
if target_class == "GymEnv":
18+
target_class = GymEnv
19+
elif target_class == "GymVectorEnv":
20+
target_class = GymVectorEnv
21+
else:
22+
raise ValueError(f"Unknown Gym Class: {target_class}. Must be a string of 'GymEnv' or 'GymVectorEnv' or the corresponding class object.")
23+
24+
if hasattr(schola_examples.gym, example.classname) and issubclass(getattr(schola_examples.gym, example.classname), target_class):
25+
env = getattr(schola_examples.gym, example.classname)(built_game_path = build_path, headless_mode=not example.requires_rendering, display_logs=False)
26+
return env
27+
else:
28+
unreal_connection = StandaloneUnrealConnection("localhost", build_path, headless_mode=not example.requires_rendering, display_logs=False, map = example.path)
29+
env = target_class(unreal_connection=unreal_connection)
30+
return env
31+
1432

1533
class AbstractTestGymExampleDefinition:
1634
example : UnrealExample = None # type : ignore
1735

1836
#We use one env for the various tests here since none of them modify state
1937
@pytest.fixture(scope="class")
2038
def env(self, example_build_path_arg):
21-
env = getattr(schola_examples.gym, self.example.classname)(built_game_path = example_build_path_arg, headless_mode=True, display_logs=False)
39+
env = make_env(example_build_path_arg, self.example, GymEnv)
40+
yield env
41+
env.close()
42+
43+
def test_environment_action_space(self, env):
44+
batched_action_space = self.example.single_action_space
45+
assert env.action_space == batched_action_space, f"Action Space Mismatch. Got: {env.action_space} Expected:{batched_action_space}"
46+
47+
def test_environment_observation_space(self, env):
48+
batched_observation_space = self.example.single_observation_space
49+
assert env.observation_space == batched_observation_space, f"Observation Space Mismatch. Got: {env.observation_space} Expected:{batched_observation_space}"
50+
51+
52+
class AbstractTestVecGymExampleDefinition:
53+
example : UnrealExample = None # type : ignore
54+
55+
#We use one env for the various tests here since none of them modify state
56+
@pytest.fixture(scope="class")
57+
def env(self, example_build_path_arg):
58+
env = make_env(example_build_path_arg, self.example, GymVectorEnv)
2259
yield env
2360
env.close()
2461

@@ -49,15 +86,17 @@ def env_sequencer(env: GymVectorEnv, episode_lengths):
4986
env.step(env.action_space.sample())
5087
env.reset()
5188

89+
# works for both normal gym and vectorized environments
5290
class AbstractTestGymExample:
5391
example: UnrealExample = None
92+
gym_class = "GymEnv"
5493

5594
@pytest.fixture(scope="function")
5695
def make_env(self, example_build_path_arg):
5796
envs = []
5897

5998
def factory(example : UnrealExample) -> GymVectorEnv:
60-
env = getattr(schola_examples.gym, example.classname)(built_game_path = example_build_path_arg, headless_mode=not example.requires_rendering, display_logs=False)
99+
env = make_env(example_build_path_arg, example, self.gym_class)
61100
envs.append(env)
62101
return env
63102

@@ -66,7 +105,6 @@ def factory(example : UnrealExample) -> GymVectorEnv:
66105
for env in envs:
67106
env.close()
68107

69-
70108
@pytest.mark.parametrize("episode_sequence", [[1,1],[0,0],[10,10,10],[100,100]], ids=lambda x: f"Episode Lengths: {x}")
71109
def test_episode_stepping(self, make_env, episode_sequence):
72110
env = make_env(self.example)
@@ -80,13 +118,14 @@ def test_passes_gym_checker(self, make_env):
80118

81119
class AbstractTestIncompatibleGymExample:
82120
example = None
121+
gym_class = "GymEnv"
83122

84123
@pytest.fixture(scope="function")
85124
def make_env(self, example_build_path_arg):
86125
envs = []
87126

88127
def factory(example : UnrealExample) -> GymVectorEnv:
89-
env = getattr(schola_examples.gym, example.classname)(built_game_path = example_build_path_arg, headless_mode=not example.requires_rendering, display_logs=False)
128+
env = make_env(example_build_path_arg, example, self.gym_class)
90129
envs.append(env)
91130
return env
92131

0 commit comments

Comments
 (0)