"""Creates /Game/Maps/L_Gym if it does not exist: a flat floor, a player start and daylight. Idempotent. Step 1 wants an empty gym; step 3 extends this script with the greybox sections from Movement.md (stairs, slopes, gaps, ledges, doorways, beams, corridor, surfaces, arena). Run headless: UnrealEditor-Cmd.exe Salty.uproject -run=pythonscript -script=Scripts/Authoring/create_gym.py or from the editor's Python console. Never hand-edit the .umap; rerun this. """ import unreal LEVEL_PATH = "/Game/Maps/L_Gym" FLOOR_HALF_SIZE_CM = 5000.0 # a 100 m square; the cube mesh is 100 cm, so scale = size / 100 CUBE = "/Engine/BasicShapes/Cube.Cube" FLOOR_MATERIAL = "/Game/LevelPrototyping/Materials/MI_PrototypeGrid_Gray.MI_PrototypeGrid_Gray" level_subsystem = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem) actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) asset_lib = unreal.EditorAssetLibrary def spawn(actor_class, label, location=unreal.Vector(0, 0, 0), rotation=unreal.Rotator(0, 0, 0)): actor = actor_subsystem.spawn_actor_from_class(actor_class, location, rotation) actor.set_actor_label(label) return actor def find_actor(label): for actor in actor_subsystem.get_all_level_actors(): if actor.get_actor_label() == label: return actor return None def ensure_floor(): if find_actor("Gym_Floor"): return floor = spawn(unreal.StaticMeshActor, "Gym_Floor", unreal.Vector(0, 0, -50)) mesh = unreal.load_asset(CUBE) component = floor.static_mesh_component component.set_editor_property("mobility", unreal.ComponentMobility.STATIC) component.set_static_mesh(mesh) scale = FLOOR_HALF_SIZE_CM * 2.0 / 100.0 floor.set_actor_scale3d(unreal.Vector(scale, scale, 1.0)) material = unreal.load_asset(FLOOR_MATERIAL) if asset_lib.does_asset_exist(FLOOR_MATERIAL) else None if material: component.set_material(0, material) def ensure_daylight(): sun = find_actor("Gym_Sun") if not sun: sun = spawn(unreal.DirectionalLight, "Gym_Sun", unreal.Vector(0, 0, 500)) sun.light_component.set_editor_property("intensity", 10.0) sun.light_component.set_editor_property("atmosphere_sun_light", True) # Always re-applied, with keyword arguments: positional unreal.Rotator is (roll, pitch, yaw), and the first # version of this script pitched the sun 30 degrees upward, which lit the gym as night. sun.set_actor_rotation(unreal.Rotator(roll=0.0, pitch=-45.0, yaw=30.0), False) if not find_actor("Gym_Sky"): spawn(unreal.SkyAtmosphere, "Gym_Sky", unreal.Vector(0, 0, 0)) if not find_actor("Gym_SkyLight"): sky_light = spawn(unreal.SkyLight, "Gym_SkyLight", unreal.Vector(0, 0, 500)) sky_light.light_component.set_editor_property("real_time_capture", True) if not find_actor("Gym_Fog"): spawn(unreal.ExponentialHeightFog, "Gym_Fog", unreal.Vector(0, 0, 0)) def ensure_player_starts(): # Two starts so two PIE clients on a dedicated server both spawn without a collision warning. for index, y in enumerate((-150, 150)): label = f"Gym_PlayerStart_{index}" if not find_actor(label): spawn(unreal.PlayerStart, label, unreal.Vector(0, y, 100)) def main(): if asset_lib.does_asset_exist(LEVEL_PATH): unreal.log(f"{LEVEL_PATH} exists; loading it to top up missing actors") level_subsystem.load_level(LEVEL_PATH) else: unreal.log(f"creating {LEVEL_PATH}") level_subsystem.new_level(LEVEL_PATH, False) # not world-partitioned: the gym is small and hand-laid ensure_floor() ensure_daylight() ensure_player_starts() level_subsystem.save_current_level() unreal.log(f"{LEVEL_PATH} saved") main()