From 92b143f4f76ae491efc70e18cdc5ff48f54e8198 Mon Sep 17 00:00:00 2001 From: Ivan-267 <61947090+Ivan-267@users.noreply.github.com> Date: Tue, 20 May 2025 04:38:02 +0200 Subject: [PATCH 1/2] Adds 2DCoopBallChallenge Environment --- examples/2DCoopBallChallenge/.gitattributes | 2 + examples/2DCoopBallChallenge/.gitignore | 3 + .../2DCoopBallChallenge/Platform2D.csproj | 9 + examples/2DCoopBallChallenge/Platform2D.sln | 19 + .../controller/ai_controller_2d.gd | 136 ++++ .../controller/ai_controller_2d.gd.uid | 1 + .../controller/ai_controller_3d.gd | 136 ++++ .../controller/ai_controller_3d.gd.uid | 1 + .../addons/godot_rl_agents/godot_rl_agents.gd | 16 + .../godot_rl_agents/godot_rl_agents.gd.uid | 1 + .../addons/godot_rl_agents/icon.png | Bin 0 -> 198 bytes .../onnx/csharp/ONNXInference.cs | 109 ++++ .../onnx/csharp/ONNXInference.cs.uid | 1 + .../onnx/csharp/SessionConfigurator.cs | 131 ++++ .../onnx/csharp/SessionConfigurator.cs.uid | 1 + .../onnx/csharp/docs/ONNXInference.xml | 31 + .../onnx/csharp/docs/SessionConfigurator.xml | 29 + .../onnx/wrapper/ONNX_wrapper.gd | 51 ++ .../onnx/wrapper/ONNX_wrapper.gd.uid | 1 + .../addons/godot_rl_agents/plugin.cfg | 7 + .../rewards/ApproachNodeReward2D.gd | 30 + .../rewards/ApproachNodeReward2D.gd.uid | 1 + .../rewards/ApproachNodeReward3D.gd | 30 + .../rewards/ApproachNodeReward3D.gd.uid | 1 + .../rewards/RewardFunction2D.gd | 10 + .../rewards/RewardFunction2D.gd.uid | 1 + .../rewards/RewardFunction3D.gd | 10 + .../rewards/RewardFunction3D.gd.uid | 1 + .../sensors_2d/ExampleRaycastSensor2D.tscn | 48 ++ .../sensors/sensors_2d/GridSensor2D.gd | 235 +++++++ .../sensors/sensors_2d/GridSensor2D.gd.uid | 1 + .../sensors/sensors_2d/ISensor2D.gd | 25 + .../sensors/sensors_2d/ISensor2D.gd.uid | 1 + .../sensors/sensors_2d/PositionSensor2D.gd | 65 ++ .../sensors_2d/PositionSensor2D.gd.uid | 1 + .../sensors/sensors_2d/RGBCameraSensor2D.gd | 77 +++ .../sensors_2d/RGBCameraSensor2D.gd.uid | 1 + .../sensors/sensors_2d/RGBCameraSensor2D.tscn | 36 ++ .../sensors/sensors_2d/RaycastSensor2D.gd | 118 ++++ .../sensors/sensors_2d/RaycastSensor2D.gd.uid | 1 + .../sensors/sensors_2d/RaycastSensor2D.tscn | 7 + .../sensors_3d/ExampleRaycastSensor3D.tscn | 6 + .../sensors/sensors_3d/GridSensor3D.gd | 258 ++++++++ .../sensors/sensors_3d/GridSensor3D.gd.uid | 1 + .../sensors/sensors_3d/ISensor3D.gd | 25 + .../sensors/sensors_3d/ISensor3D.gd.uid | 1 + .../sensors/sensors_3d/PositionSensor3D.gd | 79 +++ .../sensors_3d/PositionSensor3D.gd.uid | 1 + .../sensors/sensors_3d/RGBCameraSensor3D.gd | 63 ++ .../sensors_3d/RGBCameraSensor3D.gd.uid | 1 + .../sensors/sensors_3d/RGBCameraSensor3D.tscn | 35 + .../sensors/sensors_3d/RaycastSensor3D.gd | 185 ++++++ .../sensors/sensors_3d/RaycastSensor3D.gd.uid | 1 + .../sensors/sensors_3d/RaycastSensor3D.tscn | 27 + .../addons/godot_rl_agents/sync.gd | 598 ++++++++++++++++++ .../addons/godot_rl_agents/sync.gd.uid | 1 + examples/2DCoopBallChallenge/assets/goal.png | Bin 0 -> 5757 bytes .../assets/player/jump/Player1Jump1.png | Bin 0 -> 3474 bytes .../assets/player/jump/Player1Jump2.png | Bin 0 -> 3232 bytes .../assets/player/jump/Player1Jump3.png | Bin 0 -> 3811 bytes .../assets/player/move/Player-1.png | Bin 0 -> 3729 bytes .../assets/player/move/Player-2.png | Bin 0 -> 3727 bytes .../assets/player/move/Player-3.png | Bin 0 -> 3763 bytes .../2DCoopBallChallenge/assets/tilesheet.png | Bin 0 -> 35040 bytes examples/2DCoopBallChallenge/icon.svg | 1 + examples/2DCoopBallChallenge/license.md | 5 + examples/2DCoopBallChallenge/onnx/model.onnx | Bin 0 -> 35684 bytes examples/2DCoopBallChallenge/project.godot | 72 +++ examples/2DCoopBallChallenge/readme.md | 21 + .../2DCoopBallChallenge/scenes/ball/ball.tscn | 50 ++ .../scenes/game_scene/all_game_scenes.tscn | 16 + .../scenes/game_scene/game_manager.gd | 43 ++ .../scenes/game_scene/game_manager.gd.uid | 1 + .../scenes/game_scene/game_scene_1.tscn | 41 ++ .../scenes/game_scene/game_scene_2.tscn | 41 ++ .../scenes/game_scene/game_scene_3.tscn | 41 ++ .../scenes/game_scene/game_scene_base.tscn | 65 ++ .../scenes/game_scene/goal.gd | 33 + .../scenes/game_scene/goal.gd.uid | 1 + .../game_scene/goal_position_manager.gd | 4 + .../game_scene/goal_position_manager.gd.uid | 1 + .../scenes/game_scene/spawn_positions.gd.uid | 1 + .../2DCoopBallChallenge/scenes/goal/goal.tscn | 22 + .../scenes/goal/goal_positions.tscn | 55 ++ .../scenes/player/VelocitySensor2D.gd | 19 + .../scenes/player/VelocitySensor2D.gd.uid | 1 + .../scenes/player/player.gd | 77 +++ .../scenes/player/player.gd.uid | 1 + .../scenes/player/player.tscn | 94 +++ .../scenes/player/player_ai_controller.gd | 79 +++ .../scenes/player/player_ai_controller.gd.uid | 1 + .../scenes/player/players.gd | 20 + .../scenes/player/players.gd.uid | 1 + .../scenes/player/players.tscn | 31 + .../scenes/tilemap/tile_map_layer.gd | 14 + .../scenes/tilemap/tile_map_layer.gd.uid | 1 + .../scenes/tileset/tileset.tres | 32 + .../scenes/training_scene/ball.gd | 97 +++ .../scenes/training_scene/ball.gd.uid | 1 + .../scenes/training_scene/camera_2d.gd | 5 + .../scenes/training_scene/camera_2d.gd.uid | 1 + .../training_scene/inference_scene.tscn | 65 ++ .../scenes/training_scene/training_scene.tscn | 204 ++++++ 103 files changed, 3925 insertions(+) create mode 100755 examples/2DCoopBallChallenge/.gitattributes create mode 100755 examples/2DCoopBallChallenge/.gitignore create mode 100755 examples/2DCoopBallChallenge/Platform2D.csproj create mode 100755 examples/2DCoopBallChallenge/Platform2D.sln create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/controller/ai_controller_2d.gd create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/controller/ai_controller_2d.gd.uid create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/controller/ai_controller_3d.gd create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/controller/ai_controller_3d.gd.uid create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/godot_rl_agents.gd create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/godot_rl_agents.gd.uid create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/icon.png create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs.uid create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/SessionConfigurator.cs create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/SessionConfigurator.cs.uid create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/docs/ONNXInference.xml create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/docs/SessionConfigurator.xml create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd.uid create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/plugin.cfg create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/ApproachNodeReward2D.gd create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/ApproachNodeReward2D.gd.uid create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/ApproachNodeReward3D.gd create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/ApproachNodeReward3D.gd.uid create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/RewardFunction2D.gd create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/RewardFunction2D.gd.uid create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/RewardFunction3D.gd create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/RewardFunction3D.gd.uid create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/ExampleRaycastSensor2D.tscn create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/GridSensor2D.gd create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/GridSensor2D.gd.uid create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/ISensor2D.gd create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/ISensor2D.gd.uid create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/PositionSensor2D.gd create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/PositionSensor2D.gd.uid create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RGBCameraSensor2D.gd create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RGBCameraSensor2D.gd.uid create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RGBCameraSensor2D.tscn create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd.uid create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.tscn create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/ExampleRaycastSensor3D.tscn create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/GridSensor3D.gd create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/GridSensor3D.gd.uid create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/ISensor3D.gd create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/ISensor3D.gd.uid create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/PositionSensor3D.gd create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/PositionSensor3D.gd.uid create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd.uid create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.tscn create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.gd create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.gd.uid create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.tscn create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sync.gd create mode 100755 examples/2DCoopBallChallenge/addons/godot_rl_agents/sync.gd.uid create mode 100755 examples/2DCoopBallChallenge/assets/goal.png create mode 100755 examples/2DCoopBallChallenge/assets/player/jump/Player1Jump1.png create mode 100755 examples/2DCoopBallChallenge/assets/player/jump/Player1Jump2.png create mode 100755 examples/2DCoopBallChallenge/assets/player/jump/Player1Jump3.png create mode 100755 examples/2DCoopBallChallenge/assets/player/move/Player-1.png create mode 100755 examples/2DCoopBallChallenge/assets/player/move/Player-2.png create mode 100755 examples/2DCoopBallChallenge/assets/player/move/Player-3.png create mode 100755 examples/2DCoopBallChallenge/assets/tilesheet.png create mode 100755 examples/2DCoopBallChallenge/icon.svg create mode 100755 examples/2DCoopBallChallenge/license.md create mode 100755 examples/2DCoopBallChallenge/onnx/model.onnx create mode 100755 examples/2DCoopBallChallenge/project.godot create mode 100755 examples/2DCoopBallChallenge/readme.md create mode 100755 examples/2DCoopBallChallenge/scenes/ball/ball.tscn create mode 100755 examples/2DCoopBallChallenge/scenes/game_scene/all_game_scenes.tscn create mode 100755 examples/2DCoopBallChallenge/scenes/game_scene/game_manager.gd create mode 100755 examples/2DCoopBallChallenge/scenes/game_scene/game_manager.gd.uid create mode 100755 examples/2DCoopBallChallenge/scenes/game_scene/game_scene_1.tscn create mode 100755 examples/2DCoopBallChallenge/scenes/game_scene/game_scene_2.tscn create mode 100755 examples/2DCoopBallChallenge/scenes/game_scene/game_scene_3.tscn create mode 100755 examples/2DCoopBallChallenge/scenes/game_scene/game_scene_base.tscn create mode 100755 examples/2DCoopBallChallenge/scenes/game_scene/goal.gd create mode 100755 examples/2DCoopBallChallenge/scenes/game_scene/goal.gd.uid create mode 100755 examples/2DCoopBallChallenge/scenes/game_scene/goal_position_manager.gd create mode 100755 examples/2DCoopBallChallenge/scenes/game_scene/goal_position_manager.gd.uid create mode 100755 examples/2DCoopBallChallenge/scenes/game_scene/spawn_positions.gd.uid create mode 100755 examples/2DCoopBallChallenge/scenes/goal/goal.tscn create mode 100755 examples/2DCoopBallChallenge/scenes/goal/goal_positions.tscn create mode 100755 examples/2DCoopBallChallenge/scenes/player/VelocitySensor2D.gd create mode 100755 examples/2DCoopBallChallenge/scenes/player/VelocitySensor2D.gd.uid create mode 100755 examples/2DCoopBallChallenge/scenes/player/player.gd create mode 100755 examples/2DCoopBallChallenge/scenes/player/player.gd.uid create mode 100755 examples/2DCoopBallChallenge/scenes/player/player.tscn create mode 100755 examples/2DCoopBallChallenge/scenes/player/player_ai_controller.gd create mode 100755 examples/2DCoopBallChallenge/scenes/player/player_ai_controller.gd.uid create mode 100755 examples/2DCoopBallChallenge/scenes/player/players.gd create mode 100755 examples/2DCoopBallChallenge/scenes/player/players.gd.uid create mode 100755 examples/2DCoopBallChallenge/scenes/player/players.tscn create mode 100755 examples/2DCoopBallChallenge/scenes/tilemap/tile_map_layer.gd create mode 100755 examples/2DCoopBallChallenge/scenes/tilemap/tile_map_layer.gd.uid create mode 100755 examples/2DCoopBallChallenge/scenes/tileset/tileset.tres create mode 100755 examples/2DCoopBallChallenge/scenes/training_scene/ball.gd create mode 100755 examples/2DCoopBallChallenge/scenes/training_scene/ball.gd.uid create mode 100755 examples/2DCoopBallChallenge/scenes/training_scene/camera_2d.gd create mode 100755 examples/2DCoopBallChallenge/scenes/training_scene/camera_2d.gd.uid create mode 100755 examples/2DCoopBallChallenge/scenes/training_scene/inference_scene.tscn create mode 100755 examples/2DCoopBallChallenge/scenes/training_scene/training_scene.tscn diff --git a/examples/2DCoopBallChallenge/.gitattributes b/examples/2DCoopBallChallenge/.gitattributes new file mode 100755 index 0000000..8ad74f7 --- /dev/null +++ b/examples/2DCoopBallChallenge/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/examples/2DCoopBallChallenge/.gitignore b/examples/2DCoopBallChallenge/.gitignore new file mode 100755 index 0000000..7de8ea5 --- /dev/null +++ b/examples/2DCoopBallChallenge/.gitignore @@ -0,0 +1,3 @@ +# Godot 4+ specific ignores +.godot/ +android/ diff --git a/examples/2DCoopBallChallenge/Platform2D.csproj b/examples/2DCoopBallChallenge/Platform2D.csproj new file mode 100755 index 0000000..0e0abed --- /dev/null +++ b/examples/2DCoopBallChallenge/Platform2D.csproj @@ -0,0 +1,9 @@ + + + net8.0 + true + + + + + \ No newline at end of file diff --git a/examples/2DCoopBallChallenge/Platform2D.sln b/examples/2DCoopBallChallenge/Platform2D.sln new file mode 100755 index 0000000..5427097 --- /dev/null +++ b/examples/2DCoopBallChallenge/Platform2D.sln @@ -0,0 +1,19 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2012 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Platform2D", "Platform2D.csproj", "{8552EC7B-EF81-42D4-828B-B6CD9D17C897}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + ExportDebug|Any CPU = ExportDebug|Any CPU + ExportRelease|Any CPU = ExportRelease|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8552EC7B-EF81-42D4-828B-B6CD9D17C897}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8552EC7B-EF81-42D4-828B-B6CD9D17C897}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8552EC7B-EF81-42D4-828B-B6CD9D17C897}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU + {8552EC7B-EF81-42D4-828B-B6CD9D17C897}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU + {8552EC7B-EF81-42D4-828B-B6CD9D17C897}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU + {8552EC7B-EF81-42D4-828B-B6CD9D17C897}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU + EndGlobalSection +EndGlobal diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/controller/ai_controller_2d.gd b/examples/2DCoopBallChallenge/addons/godot_rl_agents/controller/ai_controller_2d.gd new file mode 100755 index 0000000..06d928b --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/controller/ai_controller_2d.gd @@ -0,0 +1,136 @@ +extends Node2D +class_name AIController2D + +enum ControlModes { + INHERIT_FROM_SYNC, ## Inherit setting from sync node + HUMAN, ## Test the environment manually + TRAINING, ## Train a model + ONNX_INFERENCE, ## Load a pretrained model using an .onnx file + RECORD_EXPERT_DEMOS ## Record observations and actions for expert demonstrations +} +@export var control_mode: ControlModes = ControlModes.INHERIT_FROM_SYNC +## The path to a trained .onnx model file to use for inference (overrides the path set in sync node). +@export var onnx_model_path := "" +## Once the number of steps has passed, the flag 'needs_reset' will be set to 'true' for this instance. +@export var reset_after := 1000 + +@export_group("Record expert demos mode options") +## Path where the demos will be saved. The file can later be used for imitation learning. +@export var expert_demo_save_path: String +## The action that erases the last recorded episode from the currently recorded data. +@export var remove_last_episode_key: InputEvent +## Action will be repeated for n frames. Will introduce control lag if larger than 1. +## Can be used to ensure that action_repeat on inference and training matches +## the recorded demonstrations. +@export var action_repeat: int = 1 + +@export_group("Multi-policy mode options") +## Allows you to set certain agents to use different policies. +## Changing has no effect with default SB3 training. Works with Rllib example. +## Tutorial: https://github.com/edbeeching/godot_rl_agents/blob/main/docs/TRAINING_MULTIPLE_POLICIES.md +@export var policy_name: String = "shared_policy" + +var onnx_model: ONNXModel + +var heuristic := "human" +var done := false +var reward := 0.0 +var n_steps := 0 +var needs_reset := false + +var _player: Node2D + + +func _ready(): + add_to_group("AGENT") + + +func init(player: Node2D): + _player = player + + +#region Methods that need implementing using the "extend script" option in Godot +func get_obs() -> Dictionary: + assert(false, "the get_obs method is not implemented when extending from ai_controller") + return {"obs": []} + + +func get_reward() -> float: + assert(false, "the get_reward method is not implemented when extending from ai_controller") + return 0.0 + + +func get_action_space() -> Dictionary: + assert( + false, "the get_action_space method is not implemented when extending from ai_controller" + ) + return { + "example_actions_continous": {"size": 2, "action_type": "continuous"}, + "example_actions_discrete": {"size": 2, "action_type": "discrete"}, + } + + +func set_action(action) -> void: + assert(false, "the set_action method is not implemented when extending from ai_controller") + + +#endregion + + +#region Methods that sometimes need implementing using the "extend script" option in Godot +# Only needed if you are recording expert demos with this AIController +func get_action() -> Array: + assert( + false, + "the get_action method is not implemented in extended AIController but demo_recorder is used" + ) + return [] + + +# For providing additional info (e.g. `is_success` for SB3 training) +func get_info() -> Dictionary: + return {} + + +#endregion + + +func _physics_process(delta): + n_steps += 1 + if n_steps > reset_after: + needs_reset = true + + +func get_obs_space(): + # may need overriding if the obs space is complex + var obs = get_obs() + return { + "obs": {"size": [len(obs["obs"])], "space": "box"}, + } + + +func reset(): + n_steps = 0 + needs_reset = false + + +func reset_if_done(): + if done: + reset() + + +func set_heuristic(h): + # sets the heuristic from "human" or "model" nothing to change here + heuristic = h + + +func get_done(): + return done + + +func set_done_false(): + done = false + + +func zero_reward(): + reward = 0.0 diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/controller/ai_controller_2d.gd.uid b/examples/2DCoopBallChallenge/addons/godot_rl_agents/controller/ai_controller_2d.gd.uid new file mode 100755 index 0000000..323d04b --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/controller/ai_controller_2d.gd.uid @@ -0,0 +1 @@ +uid://23c15bj3ukdm diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/controller/ai_controller_3d.gd b/examples/2DCoopBallChallenge/addons/godot_rl_agents/controller/ai_controller_3d.gd new file mode 100755 index 0000000..61a0529 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/controller/ai_controller_3d.gd @@ -0,0 +1,136 @@ +extends Node3D +class_name AIController3D + +enum ControlModes { + INHERIT_FROM_SYNC, ## Inherit setting from sync node + HUMAN, ## Test the environment manually + TRAINING, ## Train a model + ONNX_INFERENCE, ## Load a pretrained model using an .onnx file + RECORD_EXPERT_DEMOS ## Record observations and actions for expert demonstrations +} +@export var control_mode: ControlModes = ControlModes.INHERIT_FROM_SYNC +## The path to a trained .onnx model file to use for inference (overrides the path set in sync node). +@export var onnx_model_path := "" +## Once the number of steps has passed, the flag 'needs_reset' will be set to 'true' for this instance. +@export var reset_after := 1000 + +@export_group("Record expert demos mode options") +## Path where the demos will be saved. The file can later be used for imitation learning. +@export var expert_demo_save_path: String +## The action that erases the last recorded episode from the currently recorded data. +@export var remove_last_episode_key: InputEvent +## Action will be repeated for n frames. Will introduce control lag if larger than 1. +## Can be used to ensure that action_repeat on inference and training matches +## the recorded demonstrations. +@export var action_repeat: int = 1 + +@export_group("Multi-policy mode options") +## Allows you to set certain agents to use different policies. +## Changing has no effect with default SB3 training. Works with Rllib example. +## Tutorial: https://github.com/edbeeching/godot_rl_agents/blob/main/docs/TRAINING_MULTIPLE_POLICIES.md +@export var policy_name: String = "shared_policy" + +var onnx_model: ONNXModel + +var heuristic := "human" +var done := false +var reward := 0.0 +var n_steps := 0 +var needs_reset := false + +var _player: Node3D + + +func _ready(): + add_to_group("AGENT") + + +func init(player: Node3D): + _player = player + + +#region Methods that need implementing using the "extend script" option in Godot +func get_obs() -> Dictionary: + assert(false, "the get_obs method is not implemented when extending from ai_controller") + return {"obs": []} + + +func get_reward() -> float: + assert(false, "the get_reward method is not implemented when extending from ai_controller") + return 0.0 + + +func get_action_space() -> Dictionary: + assert( + false, "the get_action_space method is not implemented when extending from ai_controller" + ) + return { + "example_actions_continous": {"size": 2, "action_type": "continuous"}, + "example_actions_discrete": {"size": 2, "action_type": "discrete"}, + } + + +func set_action(action) -> void: + assert(false, "the set_action method is not implemented when extending from ai_controller") + + +#endregion + + +#region Methods that sometimes need implementing using the "extend script" option in Godot +# Only needed if you are recording expert demos with this AIController +func get_action() -> Array: + assert( + false, + "the get_action method is not implemented in extended AIController but demo_recorder is used" + ) + return [] + + +# For providing additional info (e.g. `is_success` for SB3 training) +func get_info() -> Dictionary: + return {} + + +#endregion + + +func _physics_process(delta): + n_steps += 1 + if n_steps > reset_after: + needs_reset = true + + +func get_obs_space(): + # may need overriding if the obs space is complex + var obs = get_obs() + return { + "obs": {"size": [len(obs["obs"])], "space": "box"}, + } + + +func reset(): + n_steps = 0 + needs_reset = false + + +func reset_if_done(): + if done: + reset() + + +func set_heuristic(h): + # sets the heuristic from "human" or "model" nothing to change here + heuristic = h + + +func get_done(): + return done + + +func set_done_false(): + done = false + + +func zero_reward(): + reward = 0.0 diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/controller/ai_controller_3d.gd.uid b/examples/2DCoopBallChallenge/addons/godot_rl_agents/controller/ai_controller_3d.gd.uid new file mode 100755 index 0000000..22711e4 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/controller/ai_controller_3d.gd.uid @@ -0,0 +1 @@ +uid://sku0fuwlceyk diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/godot_rl_agents.gd b/examples/2DCoopBallChallenge/addons/godot_rl_agents/godot_rl_agents.gd new file mode 100755 index 0000000..e4fe136 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/godot_rl_agents.gd @@ -0,0 +1,16 @@ +@tool +extends EditorPlugin + + +func _enter_tree(): + # Initialization of the plugin goes here. + # Add the new type with a name, a parent type, a script and an icon. + add_custom_type("Sync", "Node", preload("sync.gd"), preload("icon.png")) + #add_custom_type("RaycastSensor2D2", "Node", preload("raycast_sensor_2d.gd"), preload("icon.png")) + + +func _exit_tree(): + # Clean-up of the plugin goes here. + # Always remember to remove it from the engine when deactivated. + remove_custom_type("Sync") + #remove_custom_type("RaycastSensor2D2") diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/godot_rl_agents.gd.uid b/examples/2DCoopBallChallenge/addons/godot_rl_agents/godot_rl_agents.gd.uid new file mode 100755 index 0000000..dffdab8 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/godot_rl_agents.gd.uid @@ -0,0 +1 @@ +uid://bydjyywoj7hh4 diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/icon.png b/examples/2DCoopBallChallenge/addons/godot_rl_agents/icon.png new file mode 100755 index 0000000000000000000000000000000000000000..fd8190e710eafcd44b723917e69f8a028e697f4b GIT binary patch literal 198 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!73?$#)eFPHF3h)VW1=92H&!72h^YV{tmc3iE z?8EA%@76ATw`S>kbo66k0w+*4Z%L3}Farmdxo=y?%=15f{`ngim75Ecu=8|r45_%4 zoZ!H;NBn|B&bH~o2YBx9-M+p-GO7B#h~pETS&{_}Yj&4UZ<5S@e{U~m+MT`C-}QRf eHb3AgVPI&!A~QdCx-=WmBnD4cKbLh*2~7Y1GgDds literal 0 HcmV?d00001 diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs b/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs new file mode 100755 index 0000000..58d4dc2 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs @@ -0,0 +1,109 @@ +using Godot; +using Microsoft.ML.OnnxRuntime; +using Microsoft.ML.OnnxRuntime.Tensors; +using System.Collections.Generic; +using System.Linq; + +namespace GodotONNX +{ + /// + public partial class ONNXInference : GodotObject + { + + private InferenceSession session; + /// + /// Path to the ONNX model. Use Initialize to change it. + /// + private string modelPath; + private int batchSize; + + private SessionOptions SessionOpt; + + /// + /// init function + /// + /// + /// + /// Returns the output size of the model + public int Initialize(string Path, int BatchSize) + { + modelPath = Path; + batchSize = BatchSize; + SessionOpt = SessionConfigurator.MakeConfiguredSessionOptions(); + session = LoadModel(modelPath); + return session.OutputMetadata["output"].Dimensions[1]; + } + + + /// + public Godot.Collections.Dictionary> RunInference(Godot.Collections.Array obs, int state_ins) + { + //Current model: Any (Godot Rl Agents) + //Expects a tensor of shape [batch_size, input_size] type float named obs and a tensor of shape [batch_size] type float named state_ins + + //Fill the input tensors + // create span from inputSize + var span = new float[obs.Count]; //There's probably a better way to do this + for (int i = 0; i < obs.Count; i++) + { + span[i] = obs[i]; + } + + IReadOnlyCollection inputs = new List + { + NamedOnnxValue.CreateFromTensor("obs", new DenseTensor(span, new int[] { batchSize, obs.Count })), + NamedOnnxValue.CreateFromTensor("state_ins", new DenseTensor(new float[] { state_ins }, new int[] { batchSize })) + }; + IReadOnlyCollection outputNames = new List { "output", "state_outs" }; //ONNX is sensible to these names, as well as the input names + + IDisposableReadOnlyCollection results; + //We do not use "using" here so we get a better exception explaination later + try + { + results = session.Run(inputs, outputNames); + } + catch (OnnxRuntimeException e) + { + //This error usually means that the model is not compatible with the input, beacause of the input shape (size) + GD.Print("Error at inference: ", e); + return null; + } + //Can't convert IEnumerable to Variant, so we have to convert it to an array or something + Godot.Collections.Dictionary> output = new Godot.Collections.Dictionary>(); + DisposableNamedOnnxValue output1 = results.First(); + DisposableNamedOnnxValue output2 = results.Last(); + Godot.Collections.Array output1Array = new Godot.Collections.Array(); + Godot.Collections.Array output2Array = new Godot.Collections.Array(); + + foreach (float f in output1.AsEnumerable()) + { + output1Array.Add(f); + } + + foreach (float f in output2.AsEnumerable()) + { + output2Array.Add(f); + } + + output.Add(output1.Name, output1Array); + output.Add(output2.Name, output2Array); + + //Output is a dictionary of arrays, ex: { "output" : [0.1, 0.2, 0.3, 0.4, ...], "state_outs" : [0.5, ...]} + results.Dispose(); + return output; + } + /// + public InferenceSession LoadModel(string Path) + { + using Godot.FileAccess file = FileAccess.Open(Path, Godot.FileAccess.ModeFlags.Read); + byte[] model = file.GetBuffer((int)file.GetLength()); + //file.Close(); file.Dispose(); //Close the file, then dispose the reference. + return new InferenceSession(model, SessionOpt); //Load the model + } + public void FreeDisposables() + { + session.Dispose(); + SessionOpt.Dispose(); + } + } +} diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs.uid b/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs.uid new file mode 100755 index 0000000..1281226 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs.uid @@ -0,0 +1 @@ +uid://bon0cie4ugxk8 diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/SessionConfigurator.cs b/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/SessionConfigurator.cs new file mode 100755 index 0000000..ad7a41c --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/SessionConfigurator.cs @@ -0,0 +1,131 @@ +using Godot; +using Microsoft.ML.OnnxRuntime; + +namespace GodotONNX +{ + /// + + public static class SessionConfigurator + { + public enum ComputeName + { + CUDA, + ROCm, + DirectML, + CoreML, + CPU + } + + /// + public static SessionOptions MakeConfiguredSessionOptions() + { + SessionOptions sessionOptions = new(); + SetOptions(sessionOptions); + return sessionOptions; + } + + private static void SetOptions(SessionOptions sessionOptions) + { + sessionOptions.LogSeverityLevel = OrtLoggingLevel.ORT_LOGGING_LEVEL_WARNING; + ApplySystemSpecificOptions(sessionOptions); + } + + /// + static public void ApplySystemSpecificOptions(SessionOptions sessionOptions) + { + //Most code for this function is verbose only, the only reason it exists is to track + //implementation progress of the different compute APIs. + + //December 2022: CUDA is not working. + + string OSName = OS.GetName(); //Get OS Name + + //ComputeName ComputeAPI = ComputeCheck(); //Get Compute API + // //TODO: Get CPU architecture + + //Linux can use OpenVINO (C#) on x64 and ROCm on x86 (GDNative/C++) + //Windows can use OpenVINO (C#) on x64 + //TODO: try TensorRT instead of CUDA + //TODO: Use OpenVINO for Intel Graphics + + // Temporarily using CPU on all platforms to avoid errors detected with DML + ComputeName ComputeAPI = ComputeName.CPU; + + //match OS and Compute API + GD.Print($"OS: {OSName} Compute API: {ComputeAPI}"); + + // CPU is set by default without appending necessary + // sessionOptions.AppendExecutionProvider_CPU(0); + + /* + switch (OSName) + { + case "Windows": //Can use CUDA, DirectML + if (ComputeAPI is ComputeName.CUDA) + { + //CUDA + //sessionOptions.AppendExecutionProvider_CUDA(0); + //sessionOptions.AppendExecutionProvider_DML(0); + } + else if (ComputeAPI is ComputeName.DirectML) + { + //DirectML + //sessionOptions.AppendExecutionProvider_DML(0); + } + break; + case "X11": //Can use CUDA, ROCm + if (ComputeAPI is ComputeName.CUDA) + { + //CUDA + //sessionOptions.AppendExecutionProvider_CUDA(0); + } + if (ComputeAPI is ComputeName.ROCm) + { + //ROCm, only works on x86 + //Research indicates that this has to be compiled as a GDNative plugin + //GD.Print("ROCm not supported yet, using CPU."); + //sessionOptions.AppendExecutionProvider_CPU(0); + } + break; + case "macOS": //Can use CoreML + if (ComputeAPI is ComputeName.CoreML) + { //CoreML + //TODO: Needs testing + //sessionOptions.AppendExecutionProvider_CoreML(0); + //CoreML on ARM64, out of the box, on x64 needs .tar file from GitHub + } + break; + default: + GD.Print("OS not Supported."); + break; + } + */ + } + + + /// + public static ComputeName ComputeCheck() + { + string adapterName = Godot.RenderingServer.GetVideoAdapterName(); + //string adapterVendor = Godot.RenderingServer.GetVideoAdapterVendor(); + adapterName = adapterName.ToUpper(new System.Globalization.CultureInfo("")); + //TODO: GPU vendors for MacOS, what do they even use these days? + + if (adapterName.Contains("INTEL")) + { + return ComputeName.DirectML; + } + if (adapterName.Contains("AMD") || adapterName.Contains("RADEON")) + { + return ComputeName.DirectML; + } + if (adapterName.Contains("NVIDIA")) + { + return ComputeName.CUDA; + } + + GD.Print("Graphics Card not recognized."); //Should use CPU + return ComputeName.CPU; + } + } +} diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/SessionConfigurator.cs.uid b/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/SessionConfigurator.cs.uid new file mode 100755 index 0000000..13f5ebc --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/SessionConfigurator.cs.uid @@ -0,0 +1 @@ +uid://o2y2nsk8q5qh diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/docs/ONNXInference.xml b/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/docs/ONNXInference.xml new file mode 100755 index 0000000..91b07d6 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/docs/ONNXInference.xml @@ -0,0 +1,31 @@ + + + + + The main ONNXInference Class that handles the inference process. + + + + + Starts the inference process. + + Path to the ONNX model, expects a path inside resources. + How many observations will the model recieve. + + + + Runs the given input through the model and returns the output. + + Dictionary containing all observations. + How many different agents are creating these observations. + A Dictionary of arrays, containing instructions based on the observations. + + + + Loads the given model into the inference process, using the best Execution provider available. + + Path to the ONNX model, expects a path inside resources. + InferenceSession ready to run. + + + \ No newline at end of file diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/docs/SessionConfigurator.xml b/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/docs/SessionConfigurator.xml new file mode 100755 index 0000000..f160c02 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/csharp/docs/SessionConfigurator.xml @@ -0,0 +1,29 @@ + + + + + The main SessionConfigurator Class that handles the execution options and providers for the inference process. + + + + + Creates a SessionOptions with all available execution providers. + + SessionOptions with all available execution providers. + + + + Appends any execution provider available in the current system. + + + This function is mainly verbose for tracking implementation progress of different compute APIs. + + + + + Checks for available GPUs. + + An integer identifier for each compute platform. + + + \ No newline at end of file diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd b/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd new file mode 100755 index 0000000..e27f2c3 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd @@ -0,0 +1,51 @@ +extends Resource +class_name ONNXModel +var inferencer_script = load("res://addons/godot_rl_agents/onnx/csharp/ONNXInference.cs") + +var inferencer = null + +## How many action values the model outputs +var action_output_size: int + +## Used to differentiate models +## that only output continuous action mean (e.g. sb3, cleanrl export) +## versus models that output mean and logstd (e.g. rllib export) +var action_means_only: bool + +## Whether action_means_value has been set already for this model +var action_means_only_set: bool + +# Must provide the path to the model and the batch size +func _init(model_path, batch_size): + inferencer = inferencer_script.new() + action_output_size = inferencer.Initialize(model_path, batch_size) + +# This function is the one that will be called from the game, +# requires the observation as an array and the state_ins as an int +# returns an Array containing the action the model takes. +func run_inference(obs: Array, state_ins: int) -> Dictionary: + if inferencer == null: + printerr("Inferencer not initialized") + return {} + return inferencer.RunInference(obs, state_ins) + + +func _notification(what): + if what == NOTIFICATION_PREDELETE: + inferencer.FreeDisposables() + inferencer.free() + +# Check whether agent uses a continuous actions model with only action means or not +func set_action_means_only(agent_action_space): + action_means_only_set = true + var continuous_only: bool = true + var continuous_actions: int + for action in agent_action_space: + if not agent_action_space[action]["action_type"] == "continuous": + continuous_only = false + break + else: + continuous_actions += agent_action_space[action]["size"] + if continuous_only: + if continuous_actions == action_output_size: + action_means_only = true diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd.uid b/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd.uid new file mode 100755 index 0000000..95f620d --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd.uid @@ -0,0 +1 @@ +uid://gfec60oin817 diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/plugin.cfg b/examples/2DCoopBallChallenge/addons/godot_rl_agents/plugin.cfg new file mode 100755 index 0000000..b1bc988 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="GodotRLAgents" +description="Custom nodes for the godot rl agents toolkit " +author="Edward Beeching" +version="0.1" +script="godot_rl_agents.gd" diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/ApproachNodeReward2D.gd b/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/ApproachNodeReward2D.gd new file mode 100755 index 0000000..c4aab2a --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/ApproachNodeReward2D.gd @@ -0,0 +1,30 @@ +extends RewardFunction2D +class_name ApproachNodeReward2D + +## Calculates the reward for approaching node +## a reward is only added when the agent reaches a new +## best distance to the target object. + +## Best distance reward will be calculated for this object +@export var target_node: Node2D + +## Scales the reward, 1.0 means the reward is equal to +## how much closer the agent is than the previous best. +@export_range(0.0, 1.0, 0.0001, "or_greater") var reward_scale: float = 1.0 + +var _best_distance + + +func get_reward() -> float: + var reward := 0.0 + var current_distance := global_position.distance_to(target_node.global_position) + if not _best_distance: + _best_distance = current_distance + if current_distance < _best_distance: + reward = (_best_distance - current_distance) * reward_scale + _best_distance = current_distance + return reward + + +func reset(): + _best_distance = null diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/ApproachNodeReward2D.gd.uid b/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/ApproachNodeReward2D.gd.uid new file mode 100755 index 0000000..75c0907 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/ApproachNodeReward2D.gd.uid @@ -0,0 +1 @@ +uid://b8bbgclt0ufcp diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/ApproachNodeReward3D.gd b/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/ApproachNodeReward3D.gd new file mode 100755 index 0000000..8333106 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/ApproachNodeReward3D.gd @@ -0,0 +1,30 @@ +extends RewardFunction3D +class_name ApproachNodeReward3D + +## Calculates the reward for approaching node +## a reward is only added when the agent reaches a new +## best distance to the target object. + +## Best distance reward will be calculated for this object +@export var target_node: Node3D + +## Scales the reward, 1.0 means the reward is equal to +## how much closer the agent is than the previous best. +@export var reward_scale: float = 1.0 + +var _best_distance + + +func get_reward() -> float: + var reward := 0.0 + var current_distance := global_position.distance_to(target_node.global_position) + if not _best_distance: + _best_distance = current_distance + if current_distance < _best_distance: + reward = (_best_distance - current_distance) * reward_scale + _best_distance = current_distance + return reward + + +func reset(): + _best_distance = null diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/ApproachNodeReward3D.gd.uid b/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/ApproachNodeReward3D.gd.uid new file mode 100755 index 0000000..8a4d694 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/ApproachNodeReward3D.gd.uid @@ -0,0 +1 @@ +uid://d2rb1qrjjb7g5 diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/RewardFunction2D.gd b/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/RewardFunction2D.gd new file mode 100755 index 0000000..0285f78 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/RewardFunction2D.gd @@ -0,0 +1,10 @@ +extends Node2D +class_name RewardFunction2D + + +func get_reward(): + return 0.0 + + +func reset(): + return diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/RewardFunction2D.gd.uid b/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/RewardFunction2D.gd.uid new file mode 100755 index 0000000..beae294 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/RewardFunction2D.gd.uid @@ -0,0 +1 @@ +uid://7waiw8dxm3eu diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/RewardFunction3D.gd b/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/RewardFunction3D.gd new file mode 100755 index 0000000..87da5f9 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/RewardFunction3D.gd @@ -0,0 +1,10 @@ +extends Node3D +class_name RewardFunction3D + + +func get_reward(): + return 0.0 + + +func reset(): + return diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/RewardFunction3D.gd.uid b/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/RewardFunction3D.gd.uid new file mode 100755 index 0000000..7b072a3 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/rewards/RewardFunction3D.gd.uid @@ -0,0 +1 @@ +uid://pb88x08p5gsm diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/ExampleRaycastSensor2D.tscn b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/ExampleRaycastSensor2D.tscn new file mode 100755 index 0000000..5edb6c7 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/ExampleRaycastSensor2D.tscn @@ -0,0 +1,48 @@ +[gd_scene load_steps=5 format=3 uid="uid://ddeq7mn1ealyc"] + +[ext_resource type="Script" path="res://addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd" id="1"] + +[sub_resource type="GDScript" id="2"] +script/source = "extends Node2D + + + +func _physics_process(delta: float) -> void: + print(\"step start\") + +" + +[sub_resource type="GDScript" id="1"] +script/source = "extends RayCast2D + +var steps = 1 + +func _physics_process(delta: float) -> void: + print(\"processing raycast\") + steps += 1 + if steps % 2: + force_raycast_update() + + print(is_colliding()) +" + +[sub_resource type="CircleShape2D" id="3"] + +[node name="ExampleRaycastSensor2D" type="Node2D"] +script = SubResource("2") + +[node name="ExampleAgent" type="Node2D" parent="."] +position = Vector2(573, 314) +rotation = 0.286234 + +[node name="RaycastSensor2D" type="Node2D" parent="ExampleAgent"] +script = ExtResource("1") + +[node name="TestRayCast2D" type="RayCast2D" parent="."] +script = SubResource("1") + +[node name="StaticBody2D" type="StaticBody2D" parent="."] +position = Vector2(1, 52) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="StaticBody2D"] +shape = SubResource("3") diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/GridSensor2D.gd b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/GridSensor2D.gd new file mode 100755 index 0000000..48b132e --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/GridSensor2D.gd @@ -0,0 +1,235 @@ +@tool +extends ISensor2D +class_name GridSensor2D + +@export var debug_view := false: + get: + return debug_view + set(value): + debug_view = value + _update() + +@export_flags_2d_physics var detection_mask := 0: + get: + return detection_mask + set(value): + detection_mask = value + _update() + +@export var collide_with_areas := false: + get: + return collide_with_areas + set(value): + collide_with_areas = value + _update() + +@export var collide_with_bodies := true: + get: + return collide_with_bodies + set(value): + collide_with_bodies = value + _update() + +@export_range(1, 200, 0.1) var cell_width := 20.0: + get: + return cell_width + set(value): + cell_width = value + _update() + +@export_range(1, 200, 0.1) var cell_height := 20.0: + get: + return cell_height + set(value): + cell_height = value + _update() + +@export_range(1, 21, 2, "or_greater") var grid_size_x := 3: + get: + return grid_size_x + set(value): + grid_size_x = value + _update() + +@export_range(1, 21, 2, "or_greater") var grid_size_y := 3: + get: + return grid_size_y + set(value): + grid_size_y = value + _update() + +var _obs_buffer: PackedFloat64Array +var _rectangle_shape: RectangleShape2D +var _collision_mapping: Dictionary +var _n_layers_per_cell: int + +var _highlighted_cell_color: Color +var _standard_cell_color: Color + + +func get_observation(): + return _obs_buffer + + +func _update(): + if Engine.is_editor_hint(): + if is_node_ready(): + _spawn_nodes() + + +func _ready() -> void: + _set_colors() + + if Engine.is_editor_hint(): + if get_child_count() == 0: + _spawn_nodes() + else: + _spawn_nodes() + + +func _set_colors() -> void: + _standard_cell_color = Color(100.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0) + _highlighted_cell_color = Color(255.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0) + + +func _get_collision_mapping() -> Dictionary: + # defines which layer is mapped to which cell obs index + var total_bits = 0 + var collision_mapping = {} + for i in 32: + var bit_mask = 2 ** i + if (detection_mask & bit_mask) > 0: + collision_mapping[i] = total_bits + total_bits += 1 + + return collision_mapping + + +func _spawn_nodes(): + for cell in get_children(): + cell.name = "_%s" % cell.name # Otherwise naming below will fail + cell.queue_free() + + _collision_mapping = _get_collision_mapping() + #prints("collision_mapping", _collision_mapping, len(_collision_mapping)) + # allocate memory for the observations + _n_layers_per_cell = len(_collision_mapping) + _obs_buffer = PackedFloat64Array() + _obs_buffer.resize(grid_size_x * grid_size_y * _n_layers_per_cell) + _obs_buffer.fill(0) + #prints(len(_obs_buffer), _obs_buffer ) + + _rectangle_shape = RectangleShape2D.new() + _rectangle_shape.set_size(Vector2(cell_width, cell_height)) + + var shift := Vector2( + -(grid_size_x / 2) * cell_width, + -(grid_size_y / 2) * cell_height, + ) + + for i in grid_size_x: + for j in grid_size_y: + var cell_position = Vector2(i * cell_width, j * cell_height) + shift + _create_cell(i, j, cell_position) + + +func _create_cell(i: int, j: int, position: Vector2): + var cell := Area2D.new() + cell.position = position + cell.name = "GridCell %s %s" % [i, j] + cell.modulate = _standard_cell_color + + if collide_with_areas: + cell.area_entered.connect(_on_cell_area_entered.bind(i, j)) + cell.area_exited.connect(_on_cell_area_exited.bind(i, j)) + + if collide_with_bodies: + cell.body_entered.connect(_on_cell_body_entered.bind(i, j)) + cell.body_exited.connect(_on_cell_body_exited.bind(i, j)) + + cell.collision_layer = 0 + cell.collision_mask = detection_mask + cell.monitorable = true + add_child(cell) + cell.set_owner(get_tree().edited_scene_root) + + var col_shape := CollisionShape2D.new() + col_shape.shape = _rectangle_shape + col_shape.name = "CollisionShape2D" + cell.add_child(col_shape) + col_shape.set_owner(get_tree().edited_scene_root) + + if debug_view: + var quad = MeshInstance2D.new() + quad.name = "MeshInstance2D" + var quad_mesh = QuadMesh.new() + + quad_mesh.set_size(Vector2(cell_width, cell_height)) + + quad.mesh = quad_mesh + cell.add_child(quad) + quad.set_owner(get_tree().edited_scene_root) + + +func _update_obs(cell_i: int, cell_j: int, collision_layer: int, entered: bool): + for key in _collision_mapping: + var bit_mask = 2 ** key + if (collision_layer & bit_mask) > 0: + var collison_map_index = _collision_mapping[key] + + var obs_index = ( + (cell_i * grid_size_y * _n_layers_per_cell) + + (cell_j * _n_layers_per_cell) + + collison_map_index + ) + #prints(obs_index, cell_i, cell_j) + if entered: + _obs_buffer[obs_index] += 1 + else: + _obs_buffer[obs_index] -= 1 + + +func _toggle_cell(cell_i: int, cell_j: int): + var cell = get_node_or_null("GridCell %s %s" % [cell_i, cell_j]) + + if cell == null: + print("cell not found, returning") + + var n_hits = 0 + var start_index = (cell_i * grid_size_y * _n_layers_per_cell) + (cell_j * _n_layers_per_cell) + for i in _n_layers_per_cell: + n_hits += _obs_buffer[start_index + i] + + if n_hits > 0: + cell.modulate = _highlighted_cell_color + else: + cell.modulate = _standard_cell_color + + +func _on_cell_area_entered(area: Area2D, cell_i: int, cell_j: int): + #prints("_on_cell_area_entered", cell_i, cell_j) + _update_obs(cell_i, cell_j, area.collision_layer, true) + if debug_view: + _toggle_cell(cell_i, cell_j) + #print(_obs_buffer) + + +func _on_cell_area_exited(area: Area2D, cell_i: int, cell_j: int): + #prints("_on_cell_area_exited", cell_i, cell_j) + _update_obs(cell_i, cell_j, area.collision_layer, false) + if debug_view: + _toggle_cell(cell_i, cell_j) + + +func _on_cell_body_entered(body: Node2D, cell_i: int, cell_j: int): + #prints("_on_cell_body_entered", cell_i, cell_j) + _update_obs(cell_i, cell_j, body.collision_layer, true) + if debug_view: + _toggle_cell(cell_i, cell_j) + + +func _on_cell_body_exited(body: Node2D, cell_i: int, cell_j: int): + #prints("_on_cell_body_exited", cell_i, cell_j) + _update_obs(cell_i, cell_j, body.collision_layer, false) + if debug_view: + _toggle_cell(cell_i, cell_j) diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/GridSensor2D.gd.uid b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/GridSensor2D.gd.uid new file mode 100755 index 0000000..574fa09 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/GridSensor2D.gd.uid @@ -0,0 +1 @@ +uid://u2fug3ps4b8q diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/ISensor2D.gd b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/ISensor2D.gd new file mode 100755 index 0000000..67669a1 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/ISensor2D.gd @@ -0,0 +1,25 @@ +extends Node2D +class_name ISensor2D + +var _obs: Array = [] +var _active := false + + +func get_observation(): + pass + + +func activate(): + _active = true + + +func deactivate(): + _active = false + + +func _update_observation(): + pass + + +func reset(): + pass diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/ISensor2D.gd.uid b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/ISensor2D.gd.uid new file mode 100755 index 0000000..c09ea17 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/ISensor2D.gd.uid @@ -0,0 +1 @@ +uid://cdiyvwaxj2cud diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/PositionSensor2D.gd b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/PositionSensor2D.gd new file mode 100755 index 0000000..34ed435 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/PositionSensor2D.gd @@ -0,0 +1,65 @@ +extends ISensor2D +class_name PositionSensor2D + +@export var objects_to_observe: Array[Node2D] + +## Whether to include relative x position in obs +@export var include_x := true +## Whether to include relative y position in obs +@export var include_y := true + +## Max distance, values in obs will be normalized, +## 0 will represent the closest distance possible, and 1 the farthest. +## Do not use a much larger value than needed, as it would make the obs +## very small after normalization. +@export_range(0.01, 20_000) var max_distance := 1.0 + +@export var use_separate_direction: bool = false + +@export var debug_lines: bool = true +@export var debug_color: Color = Color.GREEN + +@onready var line: Line2D + + +func _ready() -> void: + if debug_lines: + line = Line2D.new() + add_child(line) + line.width = 1 + line.default_color = debug_color + +func get_observation(): + var observations: Array[float] + + if debug_lines: + line.clear_points() + + for obj in objects_to_observe: + var relative_position := Vector2.ZERO + + ## If object has been removed, keep the zeroed position + if is_instance_valid(obj): relative_position = to_local(obj.global_position) + + if debug_lines: + line.add_point(Vector2.ZERO) + line.add_point(relative_position) + + var direction := Vector2.ZERO + var distance := 0.0 + if use_separate_direction: + direction = relative_position.normalized() + distance = min(relative_position.length() / max_distance, 1.0) + if include_x: + observations.append(direction.x) + if include_y: + observations.append(direction.y) + observations.append(distance) + else: + relative_position = relative_position.limit_length(max_distance) / max_distance + if include_x: + observations.append(relative_position.x) + if include_y: + observations.append(relative_position.y) + + return observations diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/PositionSensor2D.gd.uid b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/PositionSensor2D.gd.uid new file mode 100755 index 0000000..0e01291 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/PositionSensor2D.gd.uid @@ -0,0 +1 @@ +uid://cyrvpwvwcjebl diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RGBCameraSensor2D.gd b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RGBCameraSensor2D.gd new file mode 100755 index 0000000..3159c3a --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RGBCameraSensor2D.gd @@ -0,0 +1,77 @@ +extends Node2D +class_name RGBCameraSensor2D +var camera_pixels = null + +@export var camera_zoom_factor := Vector2(0.1, 0.1) +@onready var camera := $SubViewport/Camera +@onready var preview_window := $Control +@onready var camera_texture := $Control/CameraTexture as Sprite2D +@onready var processed_texture := $Control/ProcessedTexture as Sprite2D +@onready var sub_viewport := $SubViewport as SubViewport +@onready var displayed_image: ImageTexture + +@export var render_image_resolution := Vector2(36, 36) +## Display size does not affect rendered or sent image resolution. +## Scale is relative to either render image or downscale image resolution +## depending on which mode is set. +@export var displayed_image_scale_factor := Vector2(8, 8) + +@export_group("Downscale image options") +## Enable to downscale the rendered image before sending the obs. +@export var downscale_image: bool = false +## If downscale_image is true, will display the downscaled image instead of rendered image. +@export var display_downscaled_image: bool = true +## This is the resolution of the image that will be sent after downscaling +@export var resized_image_resolution := Vector2(36, 36) + + +func _ready(): + DisplayServer.register_additional_output(self) + + camera.zoom = camera_zoom_factor + + var preview_size: Vector2 + + sub_viewport.world_2d = get_tree().get_root().get_world_2d() + sub_viewport.size = render_image_resolution + camera_texture.scale = displayed_image_scale_factor + + if downscale_image and display_downscaled_image: + camera_texture.visible = false + processed_texture.scale = displayed_image_scale_factor + preview_size = displayed_image_scale_factor * resized_image_resolution + else: + processed_texture.visible = false + preview_size = displayed_image_scale_factor * render_image_resolution + + preview_window.size = preview_size + + +func get_camera_pixel_encoding(): + var image := camera_texture.get_texture().get_image() as Image + + if downscale_image: + image.resize( + resized_image_resolution.x, resized_image_resolution.y, Image.INTERPOLATE_NEAREST + ) + if display_downscaled_image: + if not processed_texture.texture: + displayed_image = ImageTexture.create_from_image(image) + processed_texture.texture = displayed_image + else: + displayed_image.update(image) + + return image.get_data().hex_encode() + + +func get_camera_shape() -> Array: + var size = resized_image_resolution if downscale_image else render_image_resolution + + assert( + size.x >= 36 and size.y >= 36, + "Camera sensor sent image resolution must be 36x36 or larger." + ) + if sub_viewport.transparent_bg: + return [4, size.y, size.x] + else: + return [3, size.y, size.x] diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RGBCameraSensor2D.gd.uid b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RGBCameraSensor2D.gd.uid new file mode 100755 index 0000000..e8b98c5 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RGBCameraSensor2D.gd.uid @@ -0,0 +1 @@ +uid://bcj2e56a0clpc diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RGBCameraSensor2D.tscn b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RGBCameraSensor2D.tscn new file mode 100755 index 0000000..94ab778 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RGBCameraSensor2D.tscn @@ -0,0 +1,36 @@ +[gd_scene load_steps=3 format=3 uid="uid://bav1cl8uwc45c"] + +[ext_resource type="Script" path="res://addons/godot_rl_agents/sensors/sensors_2d/RGBCameraSensor2D.gd" id="1_txpo2"] + +[sub_resource type="ViewportTexture" id="ViewportTexture_jks1s"] +viewport_path = NodePath("SubViewport") + +[node name="RGBCameraSensor2D" type="Node2D"] +script = ExtResource("1_txpo2") +displayed_image_scale_factor = Vector2(3, 3) + +[node name="RemoteTransform" type="RemoteTransform2D" parent="."] +remote_path = NodePath("../SubViewport/Camera") + +[node name="SubViewport" type="SubViewport" parent="."] +canvas_item_default_texture_filter = 0 +size = Vector2i(36, 36) +render_target_update_mode = 4 + +[node name="Camera" type="Camera2D" parent="SubViewport"] +position_smoothing_speed = 2.0 + +[node name="Control" type="Window" parent="."] +canvas_item_default_texture_filter = 0 +title = "CameraSensor" +position = Vector2i(20, 40) +size = Vector2i(64, 64) +theme_override_font_sizes/title_font_size = 12 +metadata/_edit_use_anchors_ = true + +[node name="CameraTexture" type="Sprite2D" parent="Control"] +texture = SubResource("ViewportTexture_jks1s") +centered = false + +[node name="ProcessedTexture" type="Sprite2D" parent="Control"] +centered = false diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd new file mode 100755 index 0000000..9bb54ed --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd @@ -0,0 +1,118 @@ +@tool +extends ISensor2D +class_name RaycastSensor2D + +@export_flags_2d_physics var collision_mask := 1: + get: + return collision_mask + set(value): + collision_mask = value + _update() + +@export var collide_with_areas := false: + get: + return collide_with_areas + set(value): + collide_with_areas = value + _update() + +@export var collide_with_bodies := true: + get: + return collide_with_bodies + set(value): + collide_with_bodies = value + _update() + +@export var n_rays := 16.0: + get: + return n_rays + set(value): + n_rays = value + _update() + +@export_range(5, 3000, 5.0) var ray_length := 200: + get: + return ray_length + set(value): + ray_length = value + _update() +@export_range(5, 360, 5.0) var cone_width := 360.0: + get: + return cone_width + set(value): + cone_width = value + _update() + +@export var debug_draw := true: + get: + return debug_draw + set(value): + debug_draw = value + _update() + +var _angles = [] +var rays := [] + + +func _update(): + if Engine.is_editor_hint(): + if debug_draw: + _spawn_nodes() + else: + for ray in get_children(): + if ray is RayCast2D: + remove_child(ray) + + +func _ready() -> void: + _spawn_nodes() + + +func _spawn_nodes(): + for ray in rays: + ray.queue_free() + rays = [] + + _angles = [] + var step = cone_width / (n_rays) + var start = step / 2 - cone_width / 2 + + for i in n_rays: + var angle = start + i * step + var ray = RayCast2D.new() + ray.set_target_position( + Vector2(ray_length * cos(deg_to_rad(angle)), ray_length * sin(deg_to_rad(angle))) + ) + ray.set_name("node_" + str(i)) + ray.enabled = false + ray.collide_with_areas = collide_with_areas + ray.collide_with_bodies = collide_with_bodies + ray.collision_mask = collision_mask + add_child(ray) + rays.append(ray) + + _angles.append(start + i * step) + + +func get_observation() -> Array: + return self.calculate_raycasts() + + +func calculate_raycasts() -> Array: + var result = [] + for ray in rays: + ray.enabled = true + ray.force_raycast_update() + var distance = _get_raycast_distance(ray) + result.append(distance) + ray.enabled = false + return result + + +func _get_raycast_distance(ray: RayCast2D) -> float: + if !ray.is_colliding(): + return 0.0 + + var distance = (global_position - ray.get_collision_point()).length() + distance = clamp(distance, 0.0, ray_length) + return (ray_length - distance) / ray_length diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd.uid b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd.uid new file mode 100755 index 0000000..363b7b7 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd.uid @@ -0,0 +1 @@ +uid://dar4d1peo5076 diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.tscn b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.tscn new file mode 100755 index 0000000..5ca402c --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.tscn @@ -0,0 +1,7 @@ +[gd_scene load_steps=2 format=3 uid="uid://drvfihk5esgmv"] + +[ext_resource type="Script" path="res://addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd" id="1"] + +[node name="RaycastSensor2D" type="Node2D"] +script = ExtResource("1") +n_rays = 17.0 diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/ExampleRaycastSensor3D.tscn b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/ExampleRaycastSensor3D.tscn new file mode 100755 index 0000000..a8057c7 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/ExampleRaycastSensor3D.tscn @@ -0,0 +1,6 @@ +[gd_scene format=3 uid="uid://biu787qh4woik"] + +[node name="ExampleRaycastSensor3D" type="Node3D"] + +[node name="Camera3D" type="Camera3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.804183, 0, 2.70146) diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/GridSensor3D.gd b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/GridSensor3D.gd new file mode 100755 index 0000000..24de9a4 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/GridSensor3D.gd @@ -0,0 +1,258 @@ +@tool +extends ISensor3D +class_name GridSensor3D + +@export var debug_view := false: + get: + return debug_view + set(value): + debug_view = value + _update() + +@export_flags_3d_physics var detection_mask := 0: + get: + return detection_mask + set(value): + detection_mask = value + _update() + +@export var collide_with_areas := false: + get: + return collide_with_areas + set(value): + collide_with_areas = value + _update() + +@export var collide_with_bodies := false: + # NOTE! The sensor will not detect StaticBody3D, add an area to static bodies to detect them + get: + return collide_with_bodies + set(value): + collide_with_bodies = value + _update() + +@export_range(0.1, 2, 0.1) var cell_width := 1.0: + get: + return cell_width + set(value): + cell_width = value + _update() + +@export_range(0.1, 2, 0.1) var cell_height := 1.0: + get: + return cell_height + set(value): + cell_height = value + _update() + +@export_range(1, 21, 1, "or_greater") var grid_size_x := 3: + get: + return grid_size_x + set(value): + grid_size_x = value + _update() + +@export_range(1, 21, 1, "or_greater") var grid_size_z := 3: + get: + return grid_size_z + set(value): + grid_size_z = value + _update() + +var _obs_buffer: PackedFloat64Array +var _box_shape: BoxShape3D +var _collision_mapping: Dictionary +var _n_layers_per_cell: int + +var _highlighted_box_material: StandardMaterial3D +var _standard_box_material: StandardMaterial3D + + +func get_observation(): + return _obs_buffer + + +func reset(): + _obs_buffer.fill(0) + + +func _update(): + if Engine.is_editor_hint(): + if is_node_ready(): + _spawn_nodes() + + +func _ready() -> void: + _make_materials() + + if Engine.is_editor_hint(): + if get_child_count() == 0: + _spawn_nodes() + else: + _spawn_nodes() + + +func _make_materials() -> void: + if _highlighted_box_material != null and _standard_box_material != null: + return + + _standard_box_material = StandardMaterial3D.new() + _standard_box_material.set_transparency(1) # ALPHA + _standard_box_material.albedo_color = Color( + 100.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0 + ) + + _highlighted_box_material = StandardMaterial3D.new() + _highlighted_box_material.set_transparency(1) # ALPHA + _highlighted_box_material.albedo_color = Color( + 255.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0 + ) + + +func _get_collision_mapping() -> Dictionary: + # defines which layer is mapped to which cell obs index + var total_bits = 0 + var collision_mapping = {} + for i in 32: + var bit_mask = 2 ** i + if (detection_mask & bit_mask) > 0: + collision_mapping[i] = total_bits + total_bits += 1 + + return collision_mapping + + +func _spawn_nodes(): + for cell in get_children(): + cell.name = "_%s" % cell.name # Otherwise naming below will fail + cell.queue_free() + + _collision_mapping = _get_collision_mapping() + #prints("collision_mapping", _collision_mapping, len(_collision_mapping)) + # allocate memory for the observations + _n_layers_per_cell = len(_collision_mapping) + _obs_buffer = PackedFloat64Array() + _obs_buffer.resize(grid_size_x * grid_size_z * _n_layers_per_cell) + _obs_buffer.fill(0) + #prints(len(_obs_buffer), _obs_buffer ) + + _box_shape = BoxShape3D.new() + _box_shape.set_size(Vector3(cell_width, cell_height, cell_width)) + + var shift := Vector3( + -(grid_size_x / 2) * cell_width, + 0, + -(grid_size_z / 2) * cell_width, + ) + + for i in grid_size_x: + for j in grid_size_z: + var cell_position = Vector3(i * cell_width, 0.0, j * cell_width) + shift + _create_cell(i, j, cell_position) + + +func _create_cell(i: int, j: int, position: Vector3): + var cell := Area3D.new() + cell.position = position + cell.name = "GridCell %s %s" % [i, j] + + if collide_with_areas: + cell.area_entered.connect(_on_cell_area_entered.bind(i, j)) + cell.area_exited.connect(_on_cell_area_exited.bind(i, j)) + + if collide_with_bodies: + cell.body_entered.connect(_on_cell_body_entered.bind(i, j)) + cell.body_exited.connect(_on_cell_body_exited.bind(i, j)) + +# cell.body_shape_entered.connect(_on_cell_body_shape_entered.bind(i, j)) +# cell.body_shape_exited.connect(_on_cell_body_shape_exited.bind(i, j)) + + cell.collision_layer = 0 + cell.collision_mask = detection_mask + cell.monitorable = true + cell.input_ray_pickable = false + add_child(cell) + cell.set_owner(get_tree().edited_scene_root) + + var col_shape := CollisionShape3D.new() + col_shape.shape = _box_shape + col_shape.name = "CollisionShape3D" + cell.add_child(col_shape) + col_shape.set_owner(get_tree().edited_scene_root) + + if debug_view: + var box = MeshInstance3D.new() + box.name = "MeshInstance3D" + var box_mesh = BoxMesh.new() + + box_mesh.set_size(Vector3(cell_width, cell_height, cell_width)) + box_mesh.material = _standard_box_material + + box.mesh = box_mesh + cell.add_child(box) + box.set_owner(get_tree().edited_scene_root) + + +func _update_obs(cell_i: int, cell_j: int, collision_layer: int, entered: bool): + for key in _collision_mapping: + var bit_mask = 2 ** key + if (collision_layer & bit_mask) > 0: + var collison_map_index = _collision_mapping[key] + + var obs_index = ( + (cell_i * grid_size_z * _n_layers_per_cell) + + (cell_j * _n_layers_per_cell) + + collison_map_index + ) + #prints(obs_index, cell_i, cell_j) + if entered: + _obs_buffer[obs_index] += 1 + else: + _obs_buffer[obs_index] -= 1 + + +func _toggle_cell(cell_i: int, cell_j: int): + var cell = get_node_or_null("GridCell %s %s" % [cell_i, cell_j]) + + if cell == null: + print("cell not found, returning") + + var n_hits = 0 + var start_index = (cell_i * grid_size_z * _n_layers_per_cell) + (cell_j * _n_layers_per_cell) + for i in _n_layers_per_cell: + n_hits += _obs_buffer[start_index + i] + + var cell_mesh = cell.get_node_or_null("MeshInstance3D") + if n_hits > 0: + cell_mesh.mesh.material = _highlighted_box_material + else: + cell_mesh.mesh.material = _standard_box_material + + +func _on_cell_area_entered(area: Area3D, cell_i: int, cell_j: int): + #prints("_on_cell_area_entered", cell_i, cell_j) + _update_obs(cell_i, cell_j, area.collision_layer, true) + if debug_view: + _toggle_cell(cell_i, cell_j) + #print(_obs_buffer) + + +func _on_cell_area_exited(area: Area3D, cell_i: int, cell_j: int): + #prints("_on_cell_area_exited", cell_i, cell_j) + _update_obs(cell_i, cell_j, area.collision_layer, false) + if debug_view: + _toggle_cell(cell_i, cell_j) + + +func _on_cell_body_entered(body: Node3D, cell_i: int, cell_j: int): + #prints("_on_cell_body_entered", cell_i, cell_j) + _update_obs(cell_i, cell_j, body.collision_layer, true) + if debug_view: + _toggle_cell(cell_i, cell_j) + + +func _on_cell_body_exited(body: Node3D, cell_i: int, cell_j: int): + #prints("_on_cell_body_exited", cell_i, cell_j) + _update_obs(cell_i, cell_j, body.collision_layer, false) + if debug_view: + _toggle_cell(cell_i, cell_j) diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/GridSensor3D.gd.uid b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/GridSensor3D.gd.uid new file mode 100755 index 0000000..bd607b3 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/GridSensor3D.gd.uid @@ -0,0 +1 @@ +uid://dws62r023ufl5 diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/ISensor3D.gd b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/ISensor3D.gd new file mode 100755 index 0000000..aca3c2d --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/ISensor3D.gd @@ -0,0 +1,25 @@ +extends Node3D +class_name ISensor3D + +var _obs: Array = [] +var _active := false + + +func get_observation(): + pass + + +func activate(): + _active = true + + +func deactivate(): + _active = false + + +func _update_observation(): + pass + + +func reset(): + pass diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/ISensor3D.gd.uid b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/ISensor3D.gd.uid new file mode 100755 index 0000000..57ef130 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/ISensor3D.gd.uid @@ -0,0 +1 @@ +uid://c8m1l06ajp0ll diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/PositionSensor3D.gd b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/PositionSensor3D.gd new file mode 100755 index 0000000..85fcf03 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/PositionSensor3D.gd @@ -0,0 +1,79 @@ +extends ISensor3D +class_name PositionSensor3D + +@export var objects_to_observe: Array[Node3D] + +## Whether to include relative x position in obs +@export var include_x := true +## Whether to include relative y position in obs +@export var include_y := true +## Whether to include relative z position in obs +@export var include_z := true + +## Max distance, values in obs will be normalized, +## 0 will represent the closest distance possible, and 1 the farthest. +## Do not use a much larger value than needed, as it would make the obs +## very small after normalization. +@export_range(0.01, 2_500) var max_distance := 1.0 + +@export var use_separate_direction: bool = false + +@export var debug_lines: bool = true +@export var debug_color: Color = Color.GREEN + +@onready var mesh: ImmediateMesh + + +func _ready() -> void: + if debug_lines: + var debug_mesh = MeshInstance3D.new() + add_child(debug_mesh) + var line_material := StandardMaterial3D.new() + line_material.albedo_color = debug_color + debug_mesh.material_override = line_material + debug_mesh.mesh = ImmediateMesh.new() + mesh = debug_mesh.mesh + + +func get_observation(): + var observations: Array[float] + + if debug_lines: + mesh.clear_surfaces() + mesh.surface_begin(Mesh.PRIMITIVE_LINES) + mesh.surface_set_color(debug_color) + + for obj in objects_to_observe: + var relative_position := Vector3.ZERO + + ## If object has been removed, keep the zeroed position + if is_instance_valid(obj): relative_position = to_local(obj.global_position) + + if debug_lines: + mesh.surface_add_vertex(Vector3.ZERO) + mesh.surface_add_vertex(relative_position) + + var direction := Vector3.ZERO + var distance := 0.0 + if use_separate_direction: + direction = relative_position.normalized() + distance = min(relative_position.length() / max_distance, 1.0) + if include_x: + observations.append(direction.x) + if include_y: + observations.append(direction.y) + if include_z: + observations.append(direction.z) + observations.append(distance) + else: + relative_position = relative_position.limit_length(max_distance) / max_distance + if include_x: + observations.append(relative_position.x) + if include_y: + observations.append(relative_position.y) + if include_z: + observations.append(relative_position.z) + + if debug_lines: + mesh.surface_end() + return observations diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/PositionSensor3D.gd.uid b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/PositionSensor3D.gd.uid new file mode 100755 index 0000000..0dd0de1 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/PositionSensor3D.gd.uid @@ -0,0 +1 @@ +uid://d0dmvwg7a0jcd diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd new file mode 100755 index 0000000..96dfb6a --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd @@ -0,0 +1,63 @@ +extends Node3D +class_name RGBCameraSensor3D +var camera_pixels = null + +@onready var camera_texture := $Control/CameraTexture as Sprite2D +@onready var processed_texture := $Control/ProcessedTexture as Sprite2D +@onready var sub_viewport := $SubViewport as SubViewport +@onready var displayed_image: ImageTexture + +@export var render_image_resolution := Vector2(36, 36) +## Display size does not affect rendered or sent image resolution. +## Scale is relative to either render image or downscale image resolution +## depending on which mode is set. +@export var displayed_image_scale_factor := Vector2(8, 8) + +@export_group("Downscale image options") +## Enable to downscale the rendered image before sending the obs. +@export var downscale_image: bool = false +## If downscale_image is true, will display the downscaled image instead of rendered image. +@export var display_downscaled_image: bool = true +## This is the resolution of the image that will be sent after downscaling +@export var resized_image_resolution := Vector2(36, 36) + + +func _ready(): + sub_viewport.size = render_image_resolution + camera_texture.scale = displayed_image_scale_factor + + if downscale_image and display_downscaled_image: + camera_texture.visible = false + processed_texture.scale = displayed_image_scale_factor + else: + processed_texture.visible = false + + +func get_camera_pixel_encoding(): + var image := camera_texture.get_texture().get_image() as Image + + if downscale_image: + image.resize( + resized_image_resolution.x, resized_image_resolution.y, Image.INTERPOLATE_NEAREST + ) + if display_downscaled_image: + if not processed_texture.texture: + displayed_image = ImageTexture.create_from_image(image) + processed_texture.texture = displayed_image + else: + displayed_image.update(image) + + return image.get_data().hex_encode() + + +func get_camera_shape() -> Array: + var size = resized_image_resolution if downscale_image else render_image_resolution + + assert( + size.x >= 36 and size.y >= 36, + "Camera sensor sent image resolution must be 36x36 or larger." + ) + if sub_viewport.transparent_bg: + return [4, size.y, size.x] + else: + return [3, size.y, size.x] diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd.uid b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd.uid new file mode 100755 index 0000000..c5c7614 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd.uid @@ -0,0 +1 @@ +uid://71gve1ya8dw8 diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.tscn b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.tscn new file mode 100755 index 0000000..d58649c --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.tscn @@ -0,0 +1,35 @@ +[gd_scene load_steps=3 format=3 uid="uid://baaywi3arsl2m"] + +[ext_resource type="Script" path="res://addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd" id="1"] + +[sub_resource type="ViewportTexture" id="ViewportTexture_y72s3"] +viewport_path = NodePath("SubViewport") + +[node name="RGBCameraSensor3D" type="Node3D"] +script = ExtResource("1") + +[node name="RemoteTransform" type="RemoteTransform3D" parent="."] +remote_path = NodePath("../SubViewport/Camera") + +[node name="SubViewport" type="SubViewport" parent="."] +size = Vector2i(36, 36) +render_target_update_mode = 3 + +[node name="Camera" type="Camera3D" parent="SubViewport"] +near = 0.5 + +[node name="Control" type="Control" parent="."] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +metadata/_edit_use_anchors_ = true + +[node name="CameraTexture" type="Sprite2D" parent="Control"] +texture = SubResource("ViewportTexture_y72s3") +centered = false + +[node name="ProcessedTexture" type="Sprite2D" parent="Control"] +centered = false diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.gd b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.gd new file mode 100755 index 0000000..1357529 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.gd @@ -0,0 +1,185 @@ +@tool +extends ISensor3D +class_name RayCastSensor3D +@export_flags_3d_physics var collision_mask = 1: + get: + return collision_mask + set(value): + collision_mask = value + _update() +@export_flags_3d_physics var boolean_class_mask = 1: + get: + return boolean_class_mask + set(value): + boolean_class_mask = value + _update() + +@export var n_rays_width := 6.0: + get: + return n_rays_width + set(value): + n_rays_width = value + _update() + +@export var n_rays_height := 6.0: + get: + return n_rays_height + set(value): + n_rays_height = value + _update() + +@export var ray_length := 10.0: + get: + return ray_length + set(value): + ray_length = value + _update() + +@export var cone_width := 60.0: + get: + return cone_width + set(value): + cone_width = value + _update() + +@export var cone_height := 60.0: + get: + return cone_height + set(value): + cone_height = value + _update() + +@export var collide_with_areas := false: + get: + return collide_with_areas + set(value): + collide_with_areas = value + _update() + +@export var collide_with_bodies := true: + get: + return collide_with_bodies + set(value): + collide_with_bodies = value + _update() + +@export var class_sensor := false + +var rays := [] +var geo = null + + +func _update(): + if Engine.is_editor_hint(): + if is_node_ready(): + _spawn_nodes() + + +func _ready() -> void: + if Engine.is_editor_hint(): + if get_child_count() == 0: + _spawn_nodes() + else: + _spawn_nodes() + + +func _spawn_nodes(): + print("spawning nodes") + for ray in get_children(): + ray.queue_free() + if geo: + geo.clear() + #$Lines.remove_points() + rays = [] + + var horizontal_step = cone_width / (n_rays_width) + var vertical_step = cone_height / (n_rays_height) + + var horizontal_start = horizontal_step / 2 - cone_width / 2 + var vertical_start = vertical_step / 2 - cone_height / 2 + + var points = [] + + for i in n_rays_width: + for j in n_rays_height: + var angle_w = horizontal_start + i * horizontal_step + var angle_h = vertical_start + j * vertical_step + #angle_h = 0.0 + var ray = RayCast3D.new() + var cast_to = to_spherical_coords(ray_length, angle_w, angle_h) + ray.set_target_position(cast_to) + + points.append(cast_to) + + ray.set_name("node_" + str(i) + " " + str(j)) + ray.enabled = true + ray.collide_with_bodies = collide_with_bodies + ray.collide_with_areas = collide_with_areas + ray.collision_mask = collision_mask + add_child(ray) + ray.set_owner(get_tree().edited_scene_root) + rays.append(ray) + ray.force_raycast_update() + + +# if Engine.editor_hint: +# _create_debug_lines(points) + + +func _create_debug_lines(points): + if not geo: + geo = ImmediateMesh.new() + add_child(geo) + + geo.clear() + geo.begin(Mesh.PRIMITIVE_LINES) + for point in points: + geo.set_color(Color.AQUA) + geo.add_vertex(Vector3.ZERO) + geo.add_vertex(point) + geo.end() + + +func display(): + if geo: + geo.display() + + +func to_spherical_coords(r, inc, azimuth) -> Vector3: + return Vector3( + r * sin(deg_to_rad(inc)) * cos(deg_to_rad(azimuth)), + r * sin(deg_to_rad(azimuth)), + r * cos(deg_to_rad(inc)) * cos(deg_to_rad(azimuth)) + ) + + +func get_observation() -> Array: + return self.calculate_raycasts() + + +func calculate_raycasts() -> Array: + var result = [] + for ray in rays: + ray.set_enabled(true) + ray.force_raycast_update() + var distance = _get_raycast_distance(ray) + + result.append(distance) + if class_sensor: + var hit_class: float = 0 + if ray.get_collider(): + var hit_collision_layer = ray.get_collider().collision_layer + hit_collision_layer = hit_collision_layer & collision_mask + hit_class = (hit_collision_layer & boolean_class_mask) > 0 + result.append(float(hit_class)) + ray.set_enabled(false) + return result + + +func _get_raycast_distance(ray: RayCast3D) -> float: + if !ray.is_colliding(): + return 0.0 + + var distance = (global_transform.origin - ray.get_collision_point()).length() + distance = clamp(distance, 0.0, ray_length) + return (ray_length - distance) / ray_length diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.gd.uid b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.gd.uid new file mode 100755 index 0000000..6872c71 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.gd.uid @@ -0,0 +1 @@ +uid://budiinoaq8rgn diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.tscn b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.tscn new file mode 100755 index 0000000..35f9796 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.tscn @@ -0,0 +1,27 @@ +[gd_scene load_steps=2 format=3 uid="uid://b803cbh1fmy66"] + +[ext_resource type="Script" path="res://addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.gd" id="1"] + +[node name="RaycastSensor3D" type="Node3D"] +script = ExtResource("1") +n_rays_width = 4.0 +n_rays_height = 2.0 +ray_length = 11.0 + +[node name="node_1 0" type="RayCast3D" parent="."] +target_position = Vector3(-1.38686, -2.84701, 10.5343) + +[node name="node_1 1" type="RayCast3D" parent="."] +target_position = Vector3(-1.38686, 2.84701, 10.5343) + +[node name="node_2 0" type="RayCast3D" parent="."] +target_position = Vector3(1.38686, -2.84701, 10.5343) + +[node name="node_2 1" type="RayCast3D" parent="."] +target_position = Vector3(1.38686, 2.84701, 10.5343) + +[node name="node_3 0" type="RayCast3D" parent="."] +target_position = Vector3(4.06608, -2.84701, 9.81639) + +[node name="node_3 1" type="RayCast3D" parent="."] +target_position = Vector3(4.06608, 2.84701, 9.81639) diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sync.gd b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sync.gd new file mode 100755 index 0000000..e9b7ca5 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sync.gd @@ -0,0 +1,598 @@ +extends Node +class_name Sync + +# --fixed-fps 2000 --disable-render-loop + +enum ControlModes { + HUMAN, ## Test the environment manually + TRAINING, ## Train a model + ONNX_INFERENCE ## Load a pretrained model using an .onnx file +} +@export var control_mode: ControlModes = ControlModes.TRAINING +## Action will be repeated for n frames (Godot physics steps). +@export_range(1, 10, 1, "or_greater") var action_repeat := 8 +## Speeds up the physics in the environment to enable faster training. +@export_range(0, 10, 0.1, "or_greater") var speed_up := 1.0 +## The path to a trained .onnx model file to use for inference (only needed for the 'Onnx Inference' control mode). +@export var onnx_model_path := "" + +# Onnx model stored for each requested path +var onnx_models: Dictionary + +@onready var start_time = Time.get_ticks_msec() + +const MAJOR_VERSION := "0" +const MINOR_VERSION := "7" +const DEFAULT_PORT := "11008" +const DEFAULT_SEED := "1" +var stream: StreamPeerTCP = null +var connected = false +var message_center +var should_connect = true + +var all_agents: Array +var agents_training: Array +## Policy name of each agent, for use with multi-policy multi-agent RL cases +var agents_training_policy_names: Array[String] = ["shared_policy"] +var agents_inference: Array +var agents_heuristic: Array + +## For recording expert demos +var agent_demo_record: Node +## File path for writing recorded trajectories +var expert_demo_save_path: String +## Stores recorded trajectories +var demo_trajectories: Array +## A trajectory includes obs: Array, acts: Array, terminal (set in Python env instead) +var current_demo_trajectory: Array + +var need_to_send_obs = false +var args = null +var initialized = false +var just_reset = false +var onnx_model = null +var n_action_steps = 0 + +var _action_space_training: Array[Dictionary] = [] +var _action_space_inference: Array[Dictionary] = [] +var _obs_space_training: Array[Dictionary] = [] + + +# Called when the node enters the scene tree for the first time. +func _ready(): + await get_parent().ready + get_tree().set_pause(true) + _initialize() + await get_tree().create_timer(1.0).timeout + get_tree().set_pause(false) + + +func _initialize(): + _get_agents() + args = _get_args() + Engine.physics_ticks_per_second = _get_speedup() * 60 # Replace with function body. + Engine.time_scale = _get_speedup() * 1.0 + prints( + "physics ticks", + Engine.physics_ticks_per_second, + Engine.time_scale, + _get_speedup(), + speed_up + ) + + _set_heuristic("human", all_agents) + + _initialize_training_agents() + _initialize_inference_agents() + _initialize_demo_recording() + + _set_seed() + _set_action_repeat() + initialized = true + + +func _initialize_training_agents(): + if agents_training.size() > 0: + _obs_space_training.resize(agents_training.size()) + _action_space_training.resize(agents_training.size()) + for agent_idx in range(0, agents_training.size()): + _obs_space_training[agent_idx] = agents_training[agent_idx].get_obs_space() + _action_space_training[agent_idx] = agents_training[agent_idx].get_action_space() + connected = connect_to_server() + if connected: + _set_heuristic("model", agents_training) + _handshake() + _send_env_info() + else: + push_warning( + "Couldn't connect to Python server, using human controls instead. ", + "Did you start the training server using e.g. `gdrl` from the console?" + ) + + +func _initialize_inference_agents(): + if agents_inference.size() > 0: + if control_mode == ControlModes.ONNX_INFERENCE: + assert( + FileAccess.file_exists(onnx_model_path), + "Onnx Model Path set on Sync node does not exist: %s" % onnx_model_path + ) + onnx_models[onnx_model_path] = ONNXModel.new(onnx_model_path, 1) + + for agent in agents_inference: + var action_space = agent.get_action_space() + _action_space_inference.append(action_space) + + var agent_onnx_model: ONNXModel + if agent.onnx_model_path.is_empty(): + assert( + onnx_models.has(onnx_model_path), + ( + "Node %s has no onnx model path set " % agent.get_path() + + "and sync node's control mode is not set to OnnxInference. " + + "Either add the path to the AIController, " + + "or if you want to use the path set on sync node instead, " + + "set control mode to OnnxInference." + ) + ) + prints( + "Info: AIController %s" % agent.get_path(), + "has no onnx model path set.", + "Using path set on the sync node instead." + ) + agent_onnx_model = onnx_models[onnx_model_path] + else: + if not onnx_models.has(agent.onnx_model_path): + assert( + FileAccess.file_exists(agent.onnx_model_path), + ( + "Onnx Model Path set on %s node does not exist: %s" + % [agent.get_path(), agent.onnx_model_path] + ) + ) + onnx_models[agent.onnx_model_path] = ONNXModel.new(agent.onnx_model_path, 1) + agent_onnx_model = onnx_models[agent.onnx_model_path] + + agent.onnx_model = agent_onnx_model + if not agent_onnx_model.action_means_only_set: + agent_onnx_model.set_action_means_only(action_space) + + _set_heuristic("model", agents_inference) + + +func _initialize_demo_recording(): + if agent_demo_record: + expert_demo_save_path = agent_demo_record.expert_demo_save_path + assert( + not expert_demo_save_path.is_empty(), + "Expert demo save path set in %s is empty." % agent_demo_record.get_path() + ) + + InputMap.add_action("RemoveLastDemoEpisode") + InputMap.action_add_event( + "RemoveLastDemoEpisode", agent_demo_record.remove_last_episode_key + ) + current_demo_trajectory.resize(2) + current_demo_trajectory[0] = [] + current_demo_trajectory[1] = [] + agent_demo_record.heuristic = "demo_record" + + +func _physics_process(_delta): + # two modes, human control, agent control + # pause tree, send obs, get actions, set actions, unpause tree + + _demo_record_process() + + if n_action_steps % action_repeat != 0: + n_action_steps += 1 + return + + n_action_steps += 1 + + _training_process() + _inference_process() + _heuristic_process() + + +func _training_process(): + if connected: + get_tree().set_pause(true) + + var obs = _get_obs_from_agents(agents_training) + var info = _get_info_from_agents(agents_training) + + if just_reset: + just_reset = false + + var reply = {"type": "reset", "obs": obs, "info": info} + _send_dict_as_json_message(reply) + # this should go straight to getting the action and setting it checked the agent, no need to perform one phyics tick + get_tree().set_pause(false) + return + + if need_to_send_obs: + need_to_send_obs = false + var reward = _get_reward_from_agents() + var done = _get_done_from_agents() + #_reset_agents_if_done() # this ensures the new observation is from the next env instance : NEEDS REFACTOR + + var reply = {"type": "step", "obs": obs, "reward": reward, "done": done, "info": info} + _send_dict_as_json_message(reply) + + var handled = handle_message() + + +func _inference_process(): + if agents_inference.size() > 0: + var obs: Array = _get_obs_from_agents(agents_inference) + var actions = [] + + for agent_id in range(0, agents_inference.size()): + var model: ONNXModel = agents_inference[agent_id].onnx_model + var action = model.run_inference(obs[agent_id]["obs"], 1.0) + var action_dict = _extract_action_dict( + action["output"], _action_space_inference[agent_id], model.action_means_only + ) + actions.append(action_dict) + + _set_agent_actions(actions, agents_inference) + _reset_agents_if_done(agents_inference) + get_tree().set_pause(false) + + +func _demo_record_process(): + if not agent_demo_record: + return + + if Input.is_action_just_pressed("RemoveLastDemoEpisode"): + print("[Sync script][Demo recorder] Removing last recorded episode.") + demo_trajectories.remove_at(demo_trajectories.size() - 1) + print("Remaining episode count: %d" % demo_trajectories.size()) + + if n_action_steps % agent_demo_record.action_repeat != 0: + return + + var obs_dict: Dictionary = agent_demo_record.get_obs() + + # Get the current obs from the agent + assert( + obs_dict.has("obs"), + "Demo recorder needs an 'obs' key in get_obs() returned dictionary to record obs from." + ) + current_demo_trajectory[0].append(obs_dict.obs) + + # Get the action applied for the current obs from the agent + agent_demo_record.set_action() + var acts = agent_demo_record.get_action() + + var terminal = agent_demo_record.get_done() + # Record actions only for non-terminal states + if terminal: + agent_demo_record.set_done_false() + else: + current_demo_trajectory[1].append(acts) + + if terminal: + #current_demo_trajectory[2].append(true) + demo_trajectories.append(current_demo_trajectory.duplicate(true)) + print("[Sync script][Demo recorder] Recorded episode count: %d" % demo_trajectories.size()) + current_demo_trajectory[0].clear() + current_demo_trajectory[1].clear() + + +func _heuristic_process(): + for agent in agents_heuristic: + _reset_agents_if_done(agents_heuristic) + + +func _extract_action_dict(action_array: Array, action_space: Dictionary, action_means_only: bool): + var index = 0 + var result = {} + for key in action_space.keys(): + var size = action_space[key]["size"] + var action_type = action_space[key]["action_type"] + if action_type == "discrete": + var largest_logit: float = -INF # Value of the largest logit for this action in the actions array + var largest_logit_idx: int = 0 # Index of the largest logit for this action in the actions array + for logit_idx in range(0, size): + var logit_value = action_array[index + logit_idx] + if logit_value > largest_logit: + largest_logit = logit_value + largest_logit_idx = logit_idx + result[key] = largest_logit_idx # Index of the largest logit is the discrete action value + index += size + elif action_type == "continuous": + # For continous actions, we only take the action mean values + result[key] = clamp_array(action_array.slice(index, index + size), -1.0, 1.0) + if action_means_only: + index += size # model only outputs action means, so we move index by size + else: + index += size * 2 # model outputs logstd after action mean, we skip the logstd part + + else: + assert( + false, + ( + 'Only "discrete" and "continuous" action types supported. Found: %s action type set.' + % action_type + ) + ) + + return result + + +## For AIControllers that inherit mode from sync, sets the correct mode. +func _set_agent_mode(agent: Node): + var agent_inherits_mode: bool = agent.control_mode == agent.ControlModes.INHERIT_FROM_SYNC + + if agent_inherits_mode: + match control_mode: + ControlModes.HUMAN: + agent.control_mode = agent.ControlModes.HUMAN + ControlModes.TRAINING: + agent.control_mode = agent.ControlModes.TRAINING + ControlModes.ONNX_INFERENCE: + agent.control_mode = agent.ControlModes.ONNX_INFERENCE + + +func _get_agents(): + all_agents = get_tree().get_nodes_in_group("AGENT") + for agent in all_agents: + _set_agent_mode(agent) + + if agent.control_mode == agent.ControlModes.TRAINING: + agents_training.append(agent) + elif agent.control_mode == agent.ControlModes.ONNX_INFERENCE: + agents_inference.append(agent) + elif agent.control_mode == agent.ControlModes.HUMAN: + agents_heuristic.append(agent) + elif agent.control_mode == agent.ControlModes.RECORD_EXPERT_DEMOS: + assert( + not agent_demo_record, + "Currently only a single AIController can be used for recording expert demos." + ) + agent_demo_record = agent + + var training_agent_count = agents_training.size() + agents_training_policy_names.resize(training_agent_count) + for i in range(0, training_agent_count): + agents_training_policy_names[i] = agents_training[i].policy_name + + +func _set_heuristic(heuristic, agents: Array): + for agent in agents: + agent.set_heuristic(heuristic) + + +func _handshake(): + print("performing handshake") + + var json_dict = _get_dict_json_message() + assert(json_dict["type"] == "handshake") + var major_version = json_dict["major_version"] + var minor_version = json_dict["minor_version"] + if major_version != MAJOR_VERSION: + print("WARNING: major verison mismatch ", major_version, " ", MAJOR_VERSION) + if minor_version != MINOR_VERSION: + print("WARNING: minor verison mismatch ", minor_version, " ", MINOR_VERSION) + + print("handshake complete") + + +func _get_dict_json_message(): + # returns a dictionary from of the most recent message + # this is not waiting + while stream.get_available_bytes() == 0: + stream.poll() + if stream.get_status() != 2: + print("server disconnected status, closing") + get_tree().quit() + return null + + OS.delay_usec(10) + + var message = stream.get_string() + var json_data = JSON.parse_string(message) + + return json_data + + +func _send_dict_as_json_message(dict): + stream.put_string(JSON.stringify(dict, "", false)) + + +func _send_env_info(): + var json_dict = _get_dict_json_message() + assert(json_dict["type"] == "env_info") + + var message = { + "type": "env_info", + "observation_space": _obs_space_training, + "action_space": _action_space_training, + "n_agents": len(agents_training), + "agent_policy_names": agents_training_policy_names + } + _send_dict_as_json_message(message) + + +func connect_to_server(): + print("Waiting for one second to allow server to start") + OS.delay_msec(1000) + print("trying to connect to server") + stream = StreamPeerTCP.new() + + # "localhost" was not working on windows VM, had to use the IP + var ip = "127.0.0.1" + var port = _get_port() + var connect = stream.connect_to_host(ip, port) + stream.set_no_delay(true) # TODO check if this improves performance or not + stream.poll() + # Fetch the status until it is either connected (2) or failed to connect (3) + while stream.get_status() < 2: + stream.poll() + return stream.get_status() == 2 + + +func _get_args(): + print("getting command line arguments") + var arguments = {} + for argument in OS.get_cmdline_args(): + print(argument) + if argument.find("=") > -1: + var key_value = argument.split("=") + arguments[key_value[0].lstrip("--")] = key_value[1] + else: + # Options without an argument will be present in the dictionary, + # with the value set to an empty string. + arguments[argument.lstrip("--")] = "" + + return arguments + + +func _get_speedup(): + print(args) + return args.get("speedup", str(speed_up)).to_float() + + +func _get_port(): + return args.get("port", DEFAULT_PORT).to_int() + + +func _set_seed(): + var _seed = args.get("env_seed", DEFAULT_SEED).to_int() + seed(_seed) + + +func _set_action_repeat(): + action_repeat = args.get("action_repeat", str(action_repeat)).to_int() + + +func disconnect_from_server(): + stream.disconnect_from_host() + + +func handle_message() -> bool: + # get json message: reset, step, close + var message = _get_dict_json_message() + if message["type"] == "close": + print("received close message, closing game") + get_tree().quit() + get_tree().set_pause(false) + return true + + if message["type"] == "reset": + print("resetting all agents") + _reset_agents() + just_reset = true + get_tree().set_pause(false) + #print("resetting forcing draw") +# RenderingServer.force_draw() +# var obs = _get_obs_from_agents() +# print("obs ", obs) +# var reply = { +# "type": "reset", +# "obs": obs +# } +# _send_dict_as_json_message(reply) + return true + + if message["type"] == "call": + var method = message["method"] + var returns = _call_method_on_agents(method) + var reply = {"type": "call", "returns": returns} + print("calling method from Python") + _send_dict_as_json_message(reply) + return handle_message() + + if message["type"] == "action": + var action = message["action"] + _set_agent_actions(action, agents_training) + need_to_send_obs = true + get_tree().set_pause(false) + return true + + print("message was not handled") + return false + + +func _call_method_on_agents(method): + var returns = [] + for agent in all_agents: + returns.append(agent.call(method)) + + return returns + + +func _reset_agents_if_done(agents = all_agents): + for agent in agents: + if agent.get_done(): + agent.set_done_false() + + +func _reset_agents(agents = all_agents): + for agent in agents: + agent.needs_reset = true + #agent.reset() + + +func _get_obs_from_agents(agents: Array = all_agents): + var obs = [] + for agent in agents: + obs.append(agent.get_obs()) + return obs + + +func _get_reward_from_agents(agents: Array = agents_training): + var rewards = [] + for agent in agents: + rewards.append(agent.get_reward()) + agent.zero_reward() + return rewards + + +func _get_info_from_agents(agents: Array = all_agents): + var info = [] + for agent in agents: + info.append(agent.get_info()) + return info + + +func _get_done_from_agents(agents: Array = agents_training): + var dones = [] + for agent in agents: + var done = agent.get_done() + if done: + agent.set_done_false() + dones.append(done) + return dones + + +func _set_agent_actions(actions, agents: Array = all_agents): + for i in range(len(actions)): + agents[i].set_action(actions[i]) + + +func clamp_array(arr: Array, min: float, max: float): + var output: Array = [] + for a in arr: + output.append(clamp(a, min, max)) + return output + + +## Save recorded export demos on window exit (Close game window instead of "Stop" button in Godot Editor) +func _notification(what): + if demo_trajectories.size() == 0 or expert_demo_save_path.is_empty(): + return + + if what == NOTIFICATION_PREDELETE: + var json_string = JSON.stringify(demo_trajectories, "", false) + var file = FileAccess.open(expert_demo_save_path, FileAccess.WRITE) + + if not file: + var error: Error = FileAccess.get_open_error() + assert(not error, "There was an error opening the file: %d" % error) + + file.store_line(json_string) + var error = file.get_error() + assert(not error, "There was an error after trying to write to the file: %d" % error) diff --git a/examples/2DCoopBallChallenge/addons/godot_rl_agents/sync.gd.uid b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sync.gd.uid new file mode 100755 index 0000000..3b71256 --- /dev/null +++ b/examples/2DCoopBallChallenge/addons/godot_rl_agents/sync.gd.uid @@ -0,0 +1 @@ +uid://dkxvjlyxwlu3t diff --git a/examples/2DCoopBallChallenge/assets/goal.png b/examples/2DCoopBallChallenge/assets/goal.png new file mode 100755 index 0000000000000000000000000000000000000000..5e6fb705e98be92b7d1fe7cf29c76ed78eb6e1a7 GIT binary patch literal 5757 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 literal 0 HcmV?d00001 diff --git a/examples/2DCoopBallChallenge/assets/player/jump/Player1Jump1.png b/examples/2DCoopBallChallenge/assets/player/jump/Player1Jump1.png new file mode 100755 index 0000000000000000000000000000000000000000..9a1719b97398c805aadf5ca28975642b1c3b1a4e GIT binary patch literal 3474 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 pQtpzOcZl;QFdov#bU*5fyiSxoe^?vmrxm( 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 Cn=v5( literal 0 HcmV?d00001 diff --git a/examples/2DCoopBallChallenge/assets/player/jump/Player1Jump3.png b/examples/2DCoopBallChallenge/assets/player/jump/Player1Jump3.png new file mode 100755 index 0000000000000000000000000000000000000000..79cecf17fea04e2b2ce81979be9b78afd55bb73b GIT binary patch literal 3811 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 literal 0 HcmV?d00001 diff --git a/examples/2DCoopBallChallenge/assets/player/move/Player-2.png b/examples/2DCoopBallChallenge/assets/player/move/Player-2.png new file mode 100755 index 0000000000000000000000000000000000000000..e750568ae37c15c16fd1b9af5bd03ba2a7fee326 GIT binary patch literal 3727 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 V72qkczda@au(pv_sfKOX{{TNGE4csw literal 0 HcmV?d00001 diff --git a/examples/2DCoopBallChallenge/assets/player/move/Player-3.png b/examples/2DCoopBallChallenge/assets/player/move/Player-3.png new file mode 100755 index 0000000000000000000000000000000000000000..f9b894bb642937d92d02d61be5be1af200c00100 GIT binary patch literal 3763 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 LG=NoU+C}~cS}P*Z literal 0 HcmV?d00001 diff --git a/examples/2DCoopBallChallenge/assets/tilesheet.png b/examples/2DCoopBallChallenge/assets/tilesheet.png new file mode 100755 index 0000000000000000000000000000000000000000..ee1352df4114dfd3eba1c5a0ed0836752cd8861c GIT binary patch literal 35040 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 diff --git a/examples/2DCoopBallChallenge/license.md b/examples/2DCoopBallChallenge/license.md new file mode 100755 index 0000000..5a2bf82 --- /dev/null +++ b/examples/2DCoopBallChallenge/license.md @@ -0,0 +1,5 @@ +2DCoopBallChallenge Environment made by Ivan Dodic (https://github.com/Ivan-267) + +The following license is only for the graphical assets in the folder "assets", specifically .png files: +Author: Ivan Dodic (https://github.com/Ivan-267), +License: https://creativecommons.org/licenses/by/4.0/ diff --git a/examples/2DCoopBallChallenge/onnx/model.onnx b/examples/2DCoopBallChallenge/onnx/model.onnx new file mode 100755 index 0000000000000000000000000000000000000000..fa23d3cf10984bc3a94ad53199f445ca1f470f19 GIT binary patch literal 35684 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 literal 0 HcmV?d00001 diff --git a/examples/2DCoopBallChallenge/project.godot b/examples/2DCoopBallChallenge/project.godot new file mode 100755 index 0000000..e626a33 --- /dev/null +++ b/examples/2DCoopBallChallenge/project.godot @@ -0,0 +1,72 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="2DCoopBallChallenge" +run/main_scene="uid://b6fkgmq1op7kd" +config/features=PackedStringArray("4.4", "C#", "Forward Plus") +config/icon="res://icon.svg" + +[display] + +window/size/viewport_width=1920 +window/size/viewport_height=1080 + +[dotnet] + +project/assembly_name="Platform2D" + +[editor_plugins] + +enabled=PackedStringArray("res://addons/godot_rl_agents/plugin.cfg") + +[global_group] + +AGENT2="" +AGENT3="" +AGENT4="" +AGENT1="" +AGENT5="" +AGENT6="" + +[input] + +move_left={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +move_right={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +jump={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194326,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +kick_ball={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null) +] +} + +[physics] + +common/max_physics_steps_per_frame=32 + +[rendering] + +environment/defaults/default_clear_color=Color(0, 0, 0, 1) diff --git a/examples/2DCoopBallChallenge/readme.md b/examples/2DCoopBallChallenge/readme.md new file mode 100755 index 0000000..f90a1c7 --- /dev/null +++ b/examples/2DCoopBallChallenge/readme.md @@ -0,0 +1,21 @@ +# 2DCoopBallChallenge environment + + +## Goal: + +## Observations: + +## Running inference: + +If you’d just like to test the env using the pre-trained onnx model, open `[SceneName]` in Godot, then press `F6`. + +## Training: + +There’s an included onnx file that was trained with https://github.com/edbeeching/godot_rl_agents/blob/main/examples/stable_baselines3_example.py + +CL arguments used (also onnx export and model saving was used, enable as needed, add `env_path` too to set the exported executable of the platform): + +```python +``` + +Stats from the training session (success rate only): diff --git a/examples/2DCoopBallChallenge/scenes/ball/ball.tscn b/examples/2DCoopBallChallenge/scenes/ball/ball.tscn new file mode 100755 index 0000000..993e109 --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/ball/ball.tscn @@ -0,0 +1,50 @@ +[gd_scene load_steps=7 format=3 uid="uid://wdjkbtjihjoq"] + +[ext_resource type="Script" uid="uid://8xjlwdi087jf" path="res://scenes/training_scene/ball.gd" id="1_nv6q1"] +[ext_resource type="Script" uid="uid://b8bbgclt0ufcp" path="res://addons/godot_rl_agents/rewards/ApproachNodeReward2D.gd" id="2_stoaj"] +[ext_resource type="Script" uid="uid://cyrvpwvwcjebl" path="res://addons/godot_rl_agents/sensors/sensors_2d/PositionSensor2D.gd" id="3_stoaj"] + +[sub_resource type="SphereMesh" id="SphereMesh_dgtlv"] +radius = 50.0 +height = 100.0 + +[sub_resource type="CircleShape2D" id="CircleShape2D_jl3mj"] +radius = 40.0 + +[sub_resource type="CircleShape2D" id="CircleShape2D_3kmt7"] +radius = 100.0 + +[node name="Ball" type="CharacterBody2D" node_paths=PackedStringArray("approach_goal_reward")] +collision_layer = 4 +collision_mask = 9 +motion_mode = 1 +wall_min_slide_angle = 0.0 +platform_on_leave = 2 +script = ExtResource("1_nv6q1") +approach_goal_reward = NodePath("ApproachGoalReward") + +[node name="MeshInstance2D" type="MeshInstance2D" parent="."] +mesh = SubResource("SphereMesh_dgtlv") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +shape = SubResource("CircleShape2D_jl3mj") + +[node name="Area2D" type="Area2D" parent="."] +process_mode = 1 +collision_layer = 0 +collision_mask = 19 + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"] +shape = SubResource("CircleShape2D_3kmt7") + +[node name="ApproachGoalReward" type="Node2D" parent="."] +script = ExtResource("2_stoaj") +reward_scale = 0.1 +metadata/_custom_type_script = "uid://b8bbgclt0ufcp" + +[node name="PositionSensor2D" type="Node2D" parent="."] +script = ExtResource("3_stoaj") +debug_lines = false +metadata/_custom_type_script = "uid://cyrvpwvwcjebl" + +[connection signal="body_shape_entered" from="Area2D" to="." method="_on_area_2d_body_shape_entered"] diff --git a/examples/2DCoopBallChallenge/scenes/game_scene/all_game_scenes.tscn b/examples/2DCoopBallChallenge/scenes/game_scene/all_game_scenes.tscn new file mode 100755 index 0000000..2bc490c --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/game_scene/all_game_scenes.tscn @@ -0,0 +1,16 @@ +[gd_scene load_steps=4 format=3 uid="uid://dl1ldlvoh3s4n"] + +[ext_resource type="PackedScene" uid="uid://e3y017d7w6rb" path="res://scenes/game_scene/game_scene_3.tscn" id="2_b1s8e"] +[ext_resource type="PackedScene" uid="uid://p8ny2i03ct7i" path="res://scenes/game_scene/game_scene_1.tscn" id="2_uymmq"] +[ext_resource type="PackedScene" uid="uid://d1ax4bj7gdem6" path="res://scenes/game_scene/game_scene_2.tscn" id="3_ik6hd"] + +[node name="AllGameScenes" type="Node2D"] + +[node name="GameScene1" parent="." instance=ExtResource("2_uymmq")] +position = Vector2(-2908, -12000) + +[node name="GameScene2" parent="." instance=ExtResource("3_ik6hd")] +position = Vector2(-2908, -1468) + +[node name="GameScene3" parent="." instance=ExtResource("2_b1s8e")] +position = Vector2(-2908, 12000) diff --git a/examples/2DCoopBallChallenge/scenes/game_scene/game_manager.gd b/examples/2DCoopBallChallenge/scenes/game_scene/game_manager.gd new file mode 100755 index 0000000..ffdcaec --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/game_scene/game_manager.gd @@ -0,0 +1,43 @@ +extends Node2D +class_name GameManager + +@export var players: Players +@export var ball: Ball +@export var goal: Goal + + +func _ready(): + end_episode(0) + + +## If more steps passed than set in AI Controlled reset_after +func episode_timed_out(): + episode_failed() + + +func episode_failed(): + end_episode(-10.0) + + +func episode_success(): + end_episode(10.0, true) + + +func player_hit_ball(player): + var index = players.players.find(player) + assert(index != -1, "Player that hit ball is not in the players array") + for i in players.players.size(): + if index == i: + players.players[i].hit_ball() + else: + players.players[i].clear_hit_ball() + + +func on_ball_goal_reached(): + episode_success() + + +func end_episode(reward, success := false): + players.end_episode(reward, success) + ball.reset() + goal.move_to_next_position() diff --git a/examples/2DCoopBallChallenge/scenes/game_scene/game_manager.gd.uid b/examples/2DCoopBallChallenge/scenes/game_scene/game_manager.gd.uid new file mode 100755 index 0000000..ff4eb4d --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/game_scene/game_manager.gd.uid @@ -0,0 +1 @@ +uid://4u78ls38rhtw diff --git a/examples/2DCoopBallChallenge/scenes/game_scene/game_scene_1.tscn b/examples/2DCoopBallChallenge/scenes/game_scene/game_scene_1.tscn new file mode 100755 index 0000000..310b534 --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/game_scene/game_scene_1.tscn @@ -0,0 +1,41 @@ +[gd_scene load_steps=4 format=4 uid="uid://p8ny2i03ct7i"] + +[ext_resource type="PackedScene" uid="uid://ds7ud4f3aglol" path="res://scenes/game_scene/game_scene_base.tscn" id="1_a6u74"] +[ext_resource type="Script" uid="uid://1dbj4cknn23h" path="res://scenes/game_scene/goal_position_manager.gd" id="2_2e0g5"] +[ext_resource type="Texture2D" uid="uid://bbpqyprxxe2fk" path="res://assets/goal.png" id="3_udn15"] + +[node name="GameScene1" instance=ExtResource("1_a6u74")] + +[node name="TileMapLayer" parent="." index="0"] +tile_map_data = PackedByteArray("AAAAAA8AAAABAAAAAAABAA8AAAABAAAAAAACAA8AAAABAAAAAAADAA8AAAABAAAAAAAEAA8AAAABAAAAAAAFAA8AAAABAAAAAAAGAA8AAAABAAAAAAAHAA8AAAABAAAAAAAIAA8AAAABAAAAAAAJAA8AAAABAAAAAAAKAA8AAAABAAAAAAALAA8AAAABAAAAAAAMAA8AAAABAAAAAAAcAP//AAABAAEAAAAbAP//AAABAAEAAAAaAP//AAABAAEAAAAZAP//AAABAAEAAAAYAP//AAABAAEAAAAXAP//AAABAAEAAAAWAP//AAABAAEAAAAVAP//AAABAAEAAAAUAP//AAABAAEAAAATAP//AAABAAEAAAASAP//AAABAAEAAAARAP//AAABAAEAAAAQAP//AAABAAEAAAAPAP//AAABAAEAAAAOAP//AAABAAEAAAANAP//AAABAAEAAAAMAP//AAABAAEAAAALAP//AAABAAEAAAAKAP//AAABAAEAAAAJAP//AAABAAEAAAAIAP//AAABAAEAAAAHAP//AAABAAEAAAAGAP//AAABAAEAAAAFAP//AAABAAEAAAAEAP//AAABAAEAAAADAP//AAABAAEAAAACAP//AAABAAEAAAABAP//AAABAAEAAAAAAP//AAABAAEAAAD/////AAABAAEAAAD//wAAAAABAAEAAAD//wEAAAABAAEAAAD//wIAAAABAAEAAAD//wMAAAABAAEAAAD//wQAAAABAAEAAAD//wUAAAABAAEAAAD//wYAAAABAAEAAAD//wcAAAABAAEAAAD//wgAAAABAAEAAAD//wkAAAABAAEAAAD//woAAAABAAEAAAD//wsAAAABAAEAAAD//wwAAAABAAEAAAD//w0AAAABAAEAAAD//w4AAAABAAEAAAD//w8AAAABAAEAAAANAA8AAAABAAAAAAAOAA8AAAABAAAAAAAPAA8AAAABAAAAAAAQAA8AAAABAAAAAAARAA8AAAABAAAAAAASAA8AAAABAAAAAAATAA8AAAABAAAAAAAUAA8AAAABAAAAAAAVAA8AAAABAAAAAAAWAA8AAAABAAAAAAAXAA8AAAABAAAAAAAYAA8AAAABAAAAAAAZAA8AAAABAAAAAAAaAA8AAAABAAAAAAAbAA8AAAABAAAAAAAcAA8AAAABAAAAAAAdAA8AAAABAAAAAAAeAA8AAAABAAAAAAAfAA8AAAABAAAAAAAgAA8AAAABAAAAAAAhAA8AAAABAAAAAAAiAA8AAAABAAAAAAAjAA8AAAABAAAAAAAkAA8AAAABAAAAAAAlAA8AAAABAAAAAAAmAA8AAAABAAAAAAAnAA8AAAABAAEAAAAdAP//AAABAAEAAAAeAP//AAABAAEAAAAfAP//AAABAAEAAAAgAP//AAABAAEAAAAhAP//AAABAAEAAAAiAP//AAABAAEAAAAjAP//AAABAAEAAAAkAP//AAABAAEAAAAlAP//AAABAAEAAAAmAP//AAABAAEAAAAnAP//AAABAAEAAAAnAA0AAAABAAEAAAAnAA4AAAABAAEAAAAnAAAAAAABAAEAAAAnAAEAAAABAAEAAAAnAAIAAAABAAEAAAAnAAMAAAABAAEAAAAnAAQAAAABAAEAAAAnAAUAAAABAAEAAAAnAAYAAAABAAEAAAAnAAcAAAABAAEAAAAnAAgAAAABAAEAAAAnAAkAAAABAAEAAAAnAAoAAAABAAEAAAAnAAsAAAABAAEAAAAnAAwAAAABAAEAAAA=") + +[node name="Ball" parent="." index="1"] +position = Vector2(1965, 435) + +[node name="Goal" parent="." index="2" node_paths=PackedStringArray("goal_positions")] +position = Vector2(618, 2232) +goal_positions = NodePath("../GoalPositions") + +[node name="GoalPositions" type="Node2D" parent="." index="3"] +position = Vector2(3119, 2239) +script = ExtResource("2_2e0g5") + +[node name="Sprite2D3" type="Sprite2D" parent="GoalPositions" index="0"] +modulate = Color(1, 1, 1, 0.14902) +position = Vector2(-2500, -66.79) +scale = Vector2(3, 3) +texture = ExtResource("3_udn15") + +[node name="Sprite2D4" type="Sprite2D" parent="GoalPositions" index="1"] +modulate = Color(1, 1, 1, 0.14902) +position = Vector2(2500, -66.79) +scale = Vector2(3, 3) +texture = ExtResource("3_udn15") + +[node name="Players" parent="." index="4"] +position = Vector2(3098, 2304) + +[editable path="Ball"] +[editable path="Players"] +[editable path="Players/Player"] +[editable path="Players/Player2"] diff --git a/examples/2DCoopBallChallenge/scenes/game_scene/game_scene_2.tscn b/examples/2DCoopBallChallenge/scenes/game_scene/game_scene_2.tscn new file mode 100755 index 0000000..262ab01 --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/game_scene/game_scene_2.tscn @@ -0,0 +1,41 @@ +[gd_scene load_steps=4 format=4 uid="uid://d1ax4bj7gdem6"] + +[ext_resource type="PackedScene" uid="uid://ds7ud4f3aglol" path="res://scenes/game_scene/game_scene_base.tscn" id="1_3or3n"] +[ext_resource type="Script" uid="uid://1dbj4cknn23h" path="res://scenes/game_scene/goal_position_manager.gd" id="2_sew6x"] +[ext_resource type="Texture2D" uid="uid://bbpqyprxxe2fk" path="res://assets/goal.png" id="3_h6xdb"] + +[node name="GameScene2" instance=ExtResource("1_3or3n")] + +[node name="TileMapLayer" parent="." index="0"] +tile_map_data = PackedByteArray("AAAAAA8AAAABAAAAAAABAA8AAAABAAAAAAACAA8AAAABAAAAAAADAA8AAAABAAAAAAAEAA8AAAABAAAAAAAFAA8AAAABAAEAAAAGAA8AAAABAAEAAAAHAA8AAAABAAEAAAAIAA8AAAABAAEAAAAJAA8AAAABAAEAAAAKAA8AAAABAAEAAAALAA8AAAABAAEAAAAMAA8AAAABAAEAAAAcAP//AAABAAEAAAAbAP//AAABAAEAAAAaAP//AAABAAEAAAAZAP//AAABAAEAAAAYAP//AAABAAEAAAAXAP//AAABAAEAAAAWAP//AAABAAEAAAAVAP//AAABAAEAAAAUAP//AAABAAEAAAATAP//AAABAAEAAAASAP//AAABAAEAAAARAP//AAABAAEAAAAQAP//AAABAAEAAAAPAP//AAABAAEAAAAOAP//AAABAAEAAAANAP//AAABAAEAAAAMAP//AAABAAEAAAALAP//AAABAAEAAAAKAP//AAABAAEAAAAJAP//AAABAAEAAAAIAP//AAABAAEAAAAHAP//AAABAAEAAAAGAP//AAABAAEAAAAFAP//AAABAAEAAAAEAP//AAABAAEAAAADAP//AAABAAEAAAACAP//AAABAAEAAAABAP//AAABAAEAAAAAAP//AAABAAEAAAD/////AAABAAEAAAD//wAAAAABAAEAAAD//wEAAAABAAEAAAD//wIAAAABAAEAAAD//wMAAAABAAEAAAD//wQAAAABAAEAAAD//wUAAAABAAEAAAD//wYAAAABAAEAAAD//wcAAAABAAEAAAD//wgAAAABAAEAAAD//wkAAAABAAEAAAD//woAAAABAAEAAAD//wsAAAABAAEAAAD//wwAAAABAAEAAAD//w0AAAABAAEAAAD//w4AAAABAAEAAAD//w8AAAABAAEAAAANAA8AAAABAAEAAAAOAA8AAAABAAEAAAAPAA8AAAABAAEAAAAQAA8AAAABAAEAAAARAA8AAAABAAEAAAASAA8AAAABAAEAAAATAA8AAAABAAEAAAAUAA8AAAABAAEAAAAVAA8AAAABAAEAAAAWAA8AAAABAAEAAAAXAA8AAAABAAEAAAAYAA8AAAABAAEAAAAZAA8AAAABAAEAAAAaAA8AAAABAAEAAAAbAA8AAAABAAEAAAAcAA8AAAABAAEAAAAdAA8AAAABAAEAAAAeAA8AAAABAAEAAAAfAA8AAAABAAEAAAAgAA8AAAABAAEAAAAhAA8AAAABAAEAAAAiAA8AAAABAAAAAAAjAA8AAAABAAAAAAAkAA8AAAABAAAAAAAlAA8AAAABAAAAAAAmAA8AAAABAAAAAAAnAA8AAAABAAEAAAAdAP//AAABAAEAAAAeAP//AAABAAEAAAAfAP//AAABAAEAAAAgAP//AAABAAEAAAAhAP//AAABAAEAAAAiAP//AAABAAEAAAAjAP//AAABAAEAAAAkAP//AAABAAEAAAAlAP//AAABAAEAAAAmAP//AAABAAEAAAAnAP//AAABAAEAAAAnAA0AAAABAAEAAAAnAA4AAAABAAEAAAAnAAAAAAABAAEAAAAnAAEAAAABAAEAAAAnAAIAAAABAAEAAAAnAAMAAAABAAEAAAAnAAQAAAABAAEAAAAnAAUAAAABAAEAAAAnAAYAAAABAAEAAAAnAAcAAAABAAEAAAAnAAgAAAABAAEAAAAnAAkAAAABAAEAAAAnAAoAAAABAAEAAAAnAAsAAAABAAEAAAAnAAwAAAABAAEAAAAPAAkAAAABAAAAAAAQAAkAAAABAAAAAAARAAkAAAABAAAAAAASAAkAAAABAAAAAAATAAkAAAABAAAAAAAUAAkAAAABAAAAAAAVAAkAAAABAAAAAAAWAAkAAAABAAAAAAAXAAkAAAABAAAAAAAPAAoAAAABAAEAAAAPAAsAAAABAAEAAAAPAAwAAAABAAEAAAAPAA0AAAABAAEAAAAPAA4AAAABAAEAAAAQAAoAAAABAAEAAAAQAAsAAAABAAEAAAAQAAwAAAABAAEAAAAQAA0AAAABAAEAAAAQAA4AAAABAAEAAAARAAoAAAABAAEAAAARAAsAAAABAAEAAAARAAwAAAABAAEAAAARAA0AAAABAAEAAAARAA4AAAABAAEAAAASAAoAAAABAAEAAAASAAsAAAABAAEAAAASAAwAAAABAAEAAAASAA0AAAABAAEAAAASAA4AAAABAAEAAAATAAoAAAABAAEAAAATAAsAAAABAAEAAAATAAwAAAABAAEAAAATAA0AAAABAAEAAAATAA4AAAABAAEAAAAUAAoAAAABAAEAAAAUAAsAAAABAAEAAAAUAAwAAAABAAEAAAAUAA0AAAABAAEAAAAUAA4AAAABAAEAAAAVAAoAAAABAAEAAAAVAAsAAAABAAEAAAAVAAwAAAABAAEAAAAVAA0AAAABAAEAAAAVAA4AAAABAAEAAAAWAAoAAAABAAEAAAAWAAsAAAABAAEAAAAWAAwAAAABAAEAAAAWAA0AAAABAAEAAAAWAA4AAAABAAEAAAAXAAoAAAABAAEAAAAXAAsAAAABAAEAAAAXAAwAAAABAAEAAAAXAA0AAAABAAEAAAAXAA4AAAABAAEAAAANAAoAAAABAAAAAAAOAAoAAAABAAAAAAALAAsAAAABAAAAAAAMAAsAAAABAAAAAAAJAAwAAAABAAAAAAAKAAwAAAABAAAAAAAHAA0AAAABAAAAAAAIAA0AAAABAAAAAAAFAA4AAAABAAAAAAAGAA4AAAABAAAAAAAYAAoAAAABAAAAAAAZAAoAAAABAAAAAAAaAAsAAAABAAAAAAAbAAsAAAABAAAAAAAcAAwAAAABAAAAAAAdAAwAAAABAAAAAAAeAA0AAAABAAAAAAAfAA0AAAABAAAAAAAgAA4AAAABAAAAAAAhAA4AAAABAAAAAAAYAAsAAAABAAEAAAAYAAwAAAABAAEAAAAYAA0AAAABAAEAAAAYAA4AAAABAAEAAAAZAAsAAAABAAEAAAAZAAwAAAABAAEAAAAZAA0AAAABAAEAAAAZAA4AAAABAAEAAAAaAAwAAAABAAEAAAAaAA0AAAABAAEAAAAaAA4AAAABAAEAAAAbAAwAAAABAAEAAAAbAA0AAAABAAEAAAAbAA4AAAABAAEAAAAcAA0AAAABAAEAAAAcAA4AAAABAAEAAAAdAA0AAAABAAEAAAAdAA4AAAABAAEAAAAeAA4AAAABAAEAAAAfAA4AAAABAAEAAAANAAsAAAABAAEAAAAOAAsAAAABAAEAAAALAAwAAAABAAEAAAAMAAwAAAABAAEAAAANAAwAAAABAAEAAAAOAAwAAAABAAEAAAAJAA0AAAABAAEAAAAKAA0AAAABAAEAAAALAA0AAAABAAEAAAAMAA0AAAABAAEAAAANAA0AAAABAAEAAAAOAA0AAAABAAEAAAAHAA4AAAABAAEAAAAIAA4AAAABAAEAAAAJAA4AAAABAAEAAAAKAA4AAAABAAEAAAALAA4AAAABAAEAAAAMAA4AAAABAAEAAAANAA4AAAABAAEAAAAOAA4AAAABAAEAAAA=") + +[node name="Ball" parent="." index="1"] +position = Vector2(3179, 642) + +[node name="Goal" parent="." index="2" node_paths=PackedStringArray("goal_positions")] +position = Vector2(580, 2131) +goal_positions = NodePath("../GoalPositions") + +[node name="GoalPositions" type="Node2D" parent="." index="3"] +position = Vector2(3122.5, 2239) +script = ExtResource("2_sew6x") + +[node name="Sprite2D3" type="Sprite2D" parent="GoalPositions" index="0"] +modulate = Color(1, 1, 1, 0.14902) +position = Vector2(-2750, -73.775) +scale = Vector2(3, 3) +texture = ExtResource("3_h6xdb") + +[node name="Sprite2D4" type="Sprite2D" parent="GoalPositions" index="1"] +modulate = Color(1, 1, 1, 0.14902) +position = Vector2(2750, -73.775) +scale = Vector2(3, 3) +texture = ExtResource("3_h6xdb") + +[node name="Players" parent="." index="4"] +position = Vector2(2952, 1304) + +[editable path="Ball"] +[editable path="Players"] +[editable path="Players/Player"] +[editable path="Players/Player2"] diff --git a/examples/2DCoopBallChallenge/scenes/game_scene/game_scene_3.tscn b/examples/2DCoopBallChallenge/scenes/game_scene/game_scene_3.tscn new file mode 100755 index 0000000..c689977 --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/game_scene/game_scene_3.tscn @@ -0,0 +1,41 @@ +[gd_scene load_steps=4 format=4 uid="uid://e3y017d7w6rb"] + +[ext_resource type="PackedScene" uid="uid://ds7ud4f3aglol" path="res://scenes/game_scene/game_scene_base.tscn" id="1_4gcc6"] +[ext_resource type="Script" uid="uid://1dbj4cknn23h" path="res://scenes/game_scene/goal_position_manager.gd" id="2_xduj0"] +[ext_resource type="Texture2D" uid="uid://bbpqyprxxe2fk" path="res://assets/goal.png" id="3_i7o85"] + +[node name="GameScene3" instance=ExtResource("1_4gcc6")] + +[node name="TileMapLayer" parent="." index="0"] +tile_map_data = PackedByteArray("AAAAAA8AAAABAAEAAAABAA8AAAABAAEAAAACAA8AAAABAAEAAAADAA8AAAABAAEAAAAEAA8AAAABAAEAAAAFAA8AAAABAAEAAAAGAA8AAAABAAEAAAAHAA8AAAABAAEAAAAIAA8AAAABAAEAAAAJAA8AAAABAAEAAAAKAA8AAAABAAEAAAALAA8AAAABAAEAAAAMAA8AAAABAAEAAAAcAP//AAABAAEAAAAbAP//AAABAAEAAAAaAP//AAABAAEAAAAZAP//AAABAAEAAAAYAP//AAABAAEAAAAXAP//AAABAAEAAAAWAP//AAABAAEAAAAVAP//AAABAAEAAAAUAP//AAABAAEAAAATAP//AAABAAEAAAASAP//AAABAAEAAAARAP//AAABAAEAAAAQAP//AAABAAEAAAAPAP//AAABAAEAAAAOAP//AAABAAEAAAANAP//AAABAAEAAAAMAP//AAABAAEAAAALAP//AAABAAEAAAAKAP//AAABAAEAAAAJAP//AAABAAEAAAAIAP//AAABAAEAAAAHAP//AAABAAEAAAAGAP//AAABAAEAAAAFAP//AAABAAEAAAAEAP//AAABAAEAAAADAP//AAABAAEAAAACAP//AAABAAEAAAABAP//AAABAAEAAAAAAP//AAABAAEAAAD/////AAABAAEAAAD//wAAAAABAAEAAAD//wEAAAABAAEAAAD//wIAAAABAAEAAAD//wMAAAABAAEAAAD//wQAAAABAAEAAAD//wUAAAABAAEAAAD//wYAAAABAAEAAAD//wcAAAABAAEAAAD//wgAAAABAAEAAAD//wkAAAABAAEAAAD//woAAAABAAEAAAD//wsAAAABAAEAAAD//wwAAAABAAEAAAD//w0AAAABAAEAAAD//w4AAAABAAEAAAD//w8AAAABAAEAAAANAA8AAAABAAEAAAAOAA8AAAABAAEAAAAPAA8AAAABAAEAAAAQAA8AAAABAAEAAAARAA8AAAABAAEAAAASAA8AAAABAAEAAAATAA8AAAABAAEAAAAUAA8AAAABAAEAAAAVAA8AAAABAAEAAAAWAA8AAAABAAEAAAAXAA8AAAABAAEAAAAYAA8AAAABAAEAAAAZAA8AAAABAAEAAAAaAA8AAAABAAEAAAAbAA8AAAABAAEAAAAcAA8AAAABAAEAAAAdAA8AAAABAAEAAAAeAA8AAAABAAEAAAAfAA8AAAABAAEAAAAgAA8AAAABAAEAAAAhAA8AAAABAAEAAAAiAA8AAAABAAEAAAAjAA8AAAABAAEAAAAkAA8AAAABAAEAAAAlAA8AAAABAAEAAAAmAA8AAAABAAEAAAAnAA8AAAABAAEAAAAdAP//AAABAAEAAAAeAP//AAABAAEAAAAfAP//AAABAAEAAAAgAP//AAABAAEAAAAhAP//AAABAAEAAAAiAP//AAABAAEAAAAjAP//AAABAAEAAAAkAP//AAABAAEAAAAlAP//AAABAAEAAAAmAP//AAABAAEAAAAnAP//AAABAAEAAAAXAA4AAAABAAAAAAAnAA0AAAABAAEAAAAnAA4AAAABAAEAAAAPAA4AAAABAAAAAAAnAAAAAAABAAEAAAAnAAEAAAABAAEAAAAnAAIAAAABAAEAAAAnAAMAAAABAAEAAAAnAAQAAAABAAEAAAAnAAUAAAABAAEAAAAnAAYAAAABAAEAAAAnAAcAAAABAAEAAAAnAAgAAAABAAEAAAAnAAkAAAABAAEAAAAnAAoAAAABAAEAAAAnAAsAAAABAAEAAAAnAAwAAAABAAEAAAAQAA4AAAABAAAAAAARAA4AAAABAAAAAAASAA4AAAABAAAAAAATAA4AAAABAAAAAAAUAA4AAAABAAAAAAAVAA4AAAABAAAAAAAWAA4AAAABAAAAAAAYAA4AAAABAAAAAAAZAA4AAAABAAAAAAAaAA0AAAABAAAAAAAaAA4AAAABAAEAAAAbAA0AAAABAAAAAAAbAA4AAAABAAEAAAAcAAwAAAABAAAAAAAcAA0AAAABAAEAAAAcAA4AAAABAAEAAAAdAAwAAAABAAAAAAAdAA0AAAABAAEAAAAdAA4AAAABAAEAAAAeAAsAAAABAAAAAAAeAAwAAAABAAEAAAAeAA0AAAABAAEAAAAeAA4AAAABAAEAAAAfAAsAAAABAAAAAAAfAAwAAAABAAEAAAAfAA0AAAABAAEAAAAfAA4AAAABAAEAAAAgAAsAAAABAAEAAAAgAAwAAAABAAEAAAAgAA0AAAABAAEAAAAgAA4AAAABAAEAAAAhAAsAAAABAAEAAAAhAAwAAAABAAEAAAAhAA0AAAABAAEAAAAhAA4AAAABAAEAAAAiAAkAAAABAAAAAAAiAAoAAAABAAEAAAAiAAsAAAABAAEAAAAiAAwAAAABAAEAAAAiAA0AAAABAAEAAAAiAA4AAAABAAEAAAAjAAkAAAABAAAAAAAjAAoAAAABAAEAAAAjAAsAAAABAAEAAAAjAAwAAAABAAEAAAAjAA0AAAABAAEAAAAjAA4AAAABAAEAAAAkAAkAAAABAAAAAAAkAAoAAAABAAEAAAAkAAsAAAABAAEAAAAkAAwAAAABAAEAAAAkAA0AAAABAAEAAAAkAA4AAAABAAEAAAAlAAkAAAABAAAAAAAlAAoAAAABAAEAAAAlAAsAAAABAAEAAAAlAAwAAAABAAEAAAAlAA0AAAABAAEAAAAlAA4AAAABAAEAAAAmAAkAAAABAAAAAAAmAAoAAAABAAEAAAAmAAsAAAABAAEAAAAmAAwAAAABAAEAAAAmAA0AAAABAAEAAAAmAA4AAAABAAEAAAANAA4AAAABAAAAAAAOAA4AAAABAAAAAAALAA0AAAABAAAAAAALAA4AAAABAAEAAAAMAA0AAAABAAAAAAAMAA4AAAABAAEAAAAJAAwAAAABAAAAAAAJAA0AAAABAAEAAAAJAA4AAAABAAEAAAAKAAwAAAABAAAAAAAKAA0AAAABAAEAAAAKAA4AAAABAAEAAAAHAAsAAAABAAAAAAAHAAwAAAABAAEAAAAHAA0AAAABAAEAAAAHAA4AAAABAAEAAAAIAAsAAAABAAAAAAAIAAwAAAABAAEAAAAIAA0AAAABAAEAAAAIAA4AAAABAAEAAAAAAAkAAAABAAAAAAAAAAoAAAABAAEAAAAAAAsAAAABAAEAAAAAAAwAAAABAAEAAAAAAA0AAAABAAEAAAAAAA4AAAABAAEAAAABAAkAAAABAAAAAAABAAoAAAABAAEAAAABAAsAAAABAAEAAAABAAwAAAABAAEAAAABAA0AAAABAAEAAAABAA4AAAABAAEAAAACAAkAAAABAAAAAAACAAoAAAABAAEAAAACAAsAAAABAAEAAAACAAwAAAABAAEAAAACAA0AAAABAAEAAAACAA4AAAABAAEAAAADAAkAAAABAAAAAAADAAoAAAABAAEAAAADAAsAAAABAAEAAAADAAwAAAABAAEAAAADAA0AAAABAAEAAAADAA4AAAABAAEAAAAEAAkAAAABAAAAAAAEAAoAAAABAAEAAAAEAAsAAAABAAEAAAAEAAwAAAABAAEAAAAEAA0AAAABAAEAAAAEAA4AAAABAAEAAAAFAAoAAAABAAAAAAAFAAsAAAABAAEAAAAFAAwAAAABAAEAAAAFAA0AAAABAAEAAAAFAA4AAAABAAEAAAAGAAoAAAABAAAAAAAGAAsAAAABAAEAAAAGAAwAAAABAAEAAAAGAA0AAAABAAEAAAAGAA4AAAABAAEAAAAgAAoAAAABAAAAAAAhAAoAAAABAAAAAAA=") + +[node name="Ball" parent="." index="1"] +position = Vector2(3179, 642) + +[node name="Goal" parent="." index="2" node_paths=PackedStringArray("goal_positions")] +position = Vector2(500, 1212) +goal_positions = NodePath("../GoalPositions") + +[node name="GoalPositions" type="Node2D" parent="." index="3"] +position = Vector2(3114.5, 1214.09) +script = ExtResource("2_xduj0") + +[node name="Sprite2D3" type="Sprite2D" parent="GoalPositions" index="0"] +modulate = Color(1, 1, 1, 0.14902) +position = Vector2(-2750, 0) +scale = Vector2(3, 3) +texture = ExtResource("3_i7o85") + +[node name="Sprite2D4" type="Sprite2D" parent="GoalPositions" index="1"] +modulate = Color(1, 1, 1, 0.14902) +position = Vector2(2750, 0) +scale = Vector2(3, 3) +texture = ExtResource("3_i7o85") + +[node name="Players" parent="." index="4"] +position = Vector2(2952, 1304) + +[editable path="Ball"] +[editable path="Players"] +[editable path="Players/Player"] +[editable path="Players/Player2"] diff --git a/examples/2DCoopBallChallenge/scenes/game_scene/game_scene_base.tscn b/examples/2DCoopBallChallenge/scenes/game_scene/game_scene_base.tscn new file mode 100755 index 0000000..002b744 --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/game_scene/game_scene_base.tscn @@ -0,0 +1,65 @@ +[gd_scene load_steps=7 format=4 uid="uid://ds7ud4f3aglol"] + +[ext_resource type="Script" uid="uid://4u78ls38rhtw" path="res://scenes/game_scene/game_manager.gd" id="1_8bc8d"] +[ext_resource type="TileSet" uid="uid://sdmrwh4xd6qj" path="res://scenes/tileset/tileset.tres" id="2_qcps1"] +[ext_resource type="Script" uid="uid://375ynh2lk7vl" path="res://scenes/tilemap/tile_map_layer.gd" id="3_he3s8"] +[ext_resource type="PackedScene" uid="uid://wdjkbtjihjoq" path="res://scenes/ball/ball.tscn" id="4_hv6x3"] +[ext_resource type="PackedScene" uid="uid://ddxv6uu5j055x" path="res://scenes/goal/goal.tscn" id="6_08cpv"] +[ext_resource type="PackedScene" uid="uid://d2qsl7semlkyv" path="res://scenes/player/players.tscn" id="9_m56ji"] + +[node name="GameSceneBase" type="Node2D" node_paths=PackedStringArray("players", "ball", "goal")] +script = ExtResource("1_8bc8d") +players = NodePath("Players") +ball = NodePath("Ball") +goal = NodePath("Goal") + +[node name="TileMapLayer" type="TileMapLayer" parent="."] +tile_map_data = PackedByteArray("AAAAAA8AAAABAAEAAAABAA8AAAABAAEAAAACAA8AAAABAAEAAAADAA8AAAABAAEAAAAEAA8AAAABAAEAAAAFAA8AAAABAAEAAAAGAA8AAAABAAEAAAAHAA8AAAABAAEAAAAIAA8AAAABAAEAAAAJAA8AAAABAAEAAAAKAA8AAAABAAEAAAALAA8AAAABAAEAAAAMAA8AAAABAAEAAAAcAP//AAABAAEAAAAbAP//AAABAAEAAAAaAP//AAABAAEAAAAZAP//AAABAAEAAAAYAP//AAABAAEAAAAXAP//AAABAAEAAAAWAP//AAABAAEAAAAVAP//AAABAAEAAAAUAP//AAABAAEAAAATAP//AAABAAEAAAASAP//AAABAAEAAAARAP//AAABAAEAAAAQAP//AAABAAEAAAAPAP//AAABAAEAAAAOAP//AAABAAEAAAANAP//AAABAAEAAAAMAP//AAABAAEAAAALAP//AAABAAEAAAAKAP//AAABAAEAAAAJAP//AAABAAEAAAAIAP//AAABAAEAAAAHAP//AAABAAEAAAAGAP//AAABAAEAAAAFAP//AAABAAEAAAAEAP//AAABAAEAAAADAP//AAABAAEAAAACAP//AAABAAEAAAABAP//AAABAAEAAAAAAP//AAABAAEAAAD/////AAABAAEAAAD//wAAAAABAAEAAAD//wEAAAABAAEAAAD//wIAAAABAAEAAAD//wMAAAABAAEAAAD//wQAAAABAAEAAAD//wUAAAABAAEAAAD//wYAAAABAAEAAAD//wcAAAABAAEAAAD//wgAAAABAAEAAAD//wkAAAABAAEAAAD//woAAAABAAEAAAD//wsAAAABAAEAAAD//wwAAAABAAEAAAD//w0AAAABAAEAAAD//w4AAAABAAEAAAD//w8AAAABAAEAAAANAA8AAAABAAEAAAAOAA8AAAABAAEAAAAPAA8AAAABAAEAAAAQAA8AAAABAAEAAAARAA8AAAABAAEAAAASAA8AAAABAAEAAAATAA8AAAABAAEAAAAUAA8AAAABAAEAAAAVAA8AAAABAAEAAAAWAA8AAAABAAEAAAAXAA8AAAABAAEAAAAYAA8AAAABAAEAAAAZAA8AAAABAAEAAAAaAA8AAAABAAEAAAAbAA8AAAABAAEAAAAcAA8AAAABAAEAAAAdAA8AAAABAAEAAAAeAA8AAAABAAEAAAAfAA8AAAABAAEAAAAgAA8AAAABAAEAAAAhAA8AAAABAAEAAAAiAA8AAAABAAEAAAAjAA8AAAABAAEAAAAkAA8AAAABAAEAAAAlAA8AAAABAAEAAAAmAA8AAAABAAEAAAAnAA8AAAABAAEAAAAoAA8AAAABAAEAAAApAA8AAAABAAEAAAAqAA8AAAABAAEAAAArAA8AAAABAAEAAAAsAA8AAAABAAEAAAAtAA8AAAABAAEAAAAuAA8AAAABAAEAAAAvAA8AAAABAAEAAAAwAA8AAAABAAEAAAAxAA8AAAABAAEAAAAdAP//AAABAAEAAAAeAP//AAABAAEAAAAfAP//AAABAAEAAAAgAP//AAABAAEAAAAhAP//AAABAAEAAAAiAP//AAABAAEAAAAjAP//AAABAAEAAAAkAP//AAABAAEAAAAlAP//AAABAAEAAAAmAP//AAABAAEAAAAnAP//AAABAAEAAAAoAP//AAABAAEAAAApAP//AAABAAEAAAAqAP//AAABAAEAAAArAP//AAABAAEAAAAsAP//AAABAAEAAAAtAP//AAABAAEAAAAuAP//AAABAAEAAAAvAP//AAABAAEAAAAwAP//AAABAAEAAAAxAP//AAABAAEAAAAyAA8AAAABAAEAAAAyAA4AAAABAAEAAAAyAA0AAAABAAEAAAAyAAwAAAABAAEAAAAyAAsAAAABAAEAAAAyAAoAAAABAAEAAAAyAAkAAAABAAEAAAAyAAgAAAABAAEAAAAyAAcAAAABAAEAAAAyAAYAAAABAAEAAAAyAAUAAAABAAEAAAAyAAQAAAABAAEAAAAyAAMAAAABAAEAAAAyAAIAAAABAAEAAAAyAAEAAAABAAEAAAAyAAAAAAABAAEAAAAyAP//AAABAAEAAAA=") +tile_set = ExtResource("2_qcps1") +navigation_enabled = false +script = ExtResource("3_he3s8") + +[node name="Ball" parent="." node_paths=PackedStringArray("game_manager") instance=ExtResource("4_hv6x3")] +position = Vector2(3909, 126) +game_manager = NodePath("..") + +[node name="ApproachGoalReward" parent="Ball" index="3" node_paths=PackedStringArray("target_node")] +target_node = NodePath("../../Goal") +reward_scale = 0.0 + +[node name="PositionSensor2D" parent="Ball" index="4" node_paths=PackedStringArray("objects_to_observe")] +objects_to_observe = [NodePath("../../Goal")] +max_distance = 1000.0 +use_separate_direction = true + +[node name="Goal" parent="." instance=ExtResource("6_08cpv")] +position = Vector2(2390, 1917) + +[node name="Players" parent="." node_paths=PackedStringArray("game_manager") instance=ExtResource("9_m56ji")] +position = Vector2(3845, 1699) +game_manager = NodePath("..") + +[node name="PositionSensor2D" parent="Players/Player" index="5" node_paths=PackedStringArray("objects_to_observe")] +objects_to_observe = [NodePath("../../../Ball"), NodePath("../../../Goal"), NodePath("../../Player2")] +max_distance = 2500.0 +use_separate_direction = true + +[node name="PositionSensor2D" parent="Players/Player2" index="5" node_paths=PackedStringArray("objects_to_observe")] +objects_to_observe = [NodePath("../../../Ball"), NodePath("../../../Goal"), NodePath("../../Player")] +max_distance = 2500.0 +use_separate_direction = true + +[node name="VelocitySensor2D" parent="Players" index="2" node_paths=PackedStringArray("objects_to_observe")] +objects_to_observe = [NodePath("../Player"), NodePath("../Player2"), NodePath("../../Ball")] +max_velocity = 500 + +[node name="AIController2D" parent="Players" index="3" node_paths=PackedStringArray("sensors")] +sensors = [NodePath("../Player/RaycastSensorPlatform"), NodePath("../Player/RaycastSensorGround"), NodePath("../Player/PositionSensor2D"), NodePath("../Player2/RaycastSensorPlatform"), NodePath("../Player2/RaycastSensorGround"), NodePath("../Player2/PositionSensor2D"), NodePath("../VelocitySensor2D"), NodePath("../../Ball/PositionSensor2D")] +reset_after = 1500 + +[connection signal="body_entered" from="Goal" to="Ball" method="_on_goal_body_entered"] + +[editable path="Ball"] +[editable path="Players"] +[editable path="Players/Player"] +[editable path="Players/Player2"] diff --git a/examples/2DCoopBallChallenge/scenes/game_scene/goal.gd b/examples/2DCoopBallChallenge/scenes/game_scene/goal.gd new file mode 100755 index 0000000..6dbbd49 --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/game_scene/goal.gd @@ -0,0 +1,33 @@ +extends Area2D +class_name Goal + +@export var goal_positions: GoalPositions +var current_position_index := 0 + +func _ready() -> void: + if not goal_positions.is_node_ready(): + await goal_positions.ready + move_to_random_position() + + +## Moves the goal to next position, and wraps around +func move_to_next_position(): + await get_tree().physics_frame # Prevents the goal from moving before the ball fully resets + current_position_index = (current_position_index + 1) % goal_positions.goal_positions.size() + global_position = goal_positions.goal_positions[current_position_index].global_position + + +func move_to_previous_position(): + await get_tree().physics_frame # Prevents the goal from moving before the ball fully resets + current_position_index = max(current_position_index - 1, 0) + global_position = goal_positions.goal_positions[current_position_index].global_position + + +func move_to_first_position(): + await get_tree().physics_frame # Prevents the goal from moving before the ball fully resets + global_position = goal_positions.goal_positions[0].global_position + + +func move_to_random_position(): + await get_tree().physics_frame # Prevents the goal from moving before the ball fully resets + global_position = goal_positions.goal_positions.pick_random().global_position diff --git a/examples/2DCoopBallChallenge/scenes/game_scene/goal.gd.uid b/examples/2DCoopBallChallenge/scenes/game_scene/goal.gd.uid new file mode 100755 index 0000000..667f3b3 --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/game_scene/goal.gd.uid @@ -0,0 +1 @@ +uid://dy75ik1x5kg8m diff --git a/examples/2DCoopBallChallenge/scenes/game_scene/goal_position_manager.gd b/examples/2DCoopBallChallenge/scenes/game_scene/goal_position_manager.gd new file mode 100755 index 0000000..1259a6f --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/game_scene/goal_position_manager.gd @@ -0,0 +1,4 @@ +extends Node2D +class_name GoalPositions + +@onready var goal_positions = get_children() diff --git a/examples/2DCoopBallChallenge/scenes/game_scene/goal_position_manager.gd.uid b/examples/2DCoopBallChallenge/scenes/game_scene/goal_position_manager.gd.uid new file mode 100755 index 0000000..e156575 --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/game_scene/goal_position_manager.gd.uid @@ -0,0 +1 @@ +uid://1dbj4cknn23h diff --git a/examples/2DCoopBallChallenge/scenes/game_scene/spawn_positions.gd.uid b/examples/2DCoopBallChallenge/scenes/game_scene/spawn_positions.gd.uid new file mode 100755 index 0000000..504e9c9 --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/game_scene/spawn_positions.gd.uid @@ -0,0 +1 @@ +uid://c8cyks5v7dfbk diff --git a/examples/2DCoopBallChallenge/scenes/goal/goal.tscn b/examples/2DCoopBallChallenge/scenes/goal/goal.tscn new file mode 100755 index 0000000..c0bea52 --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/goal/goal.tscn @@ -0,0 +1,22 @@ +[gd_scene load_steps=4 format=3 uid="uid://ddxv6uu5j055x"] + +[ext_resource type="Script" uid="uid://dy75ik1x5kg8m" path="res://scenes/game_scene/goal.gd" id="1_r84s2"] +[ext_resource type="Texture2D" uid="uid://bbpqyprxxe2fk" path="res://assets/goal.png" id="2_0ivff"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_3kmt7"] +size = Vector2(80, 81.6) + +[node name="Goal" type="Area2D"] +collision_layer = 0 +collision_mask = 4 +monitorable = false +script = ExtResource("1_r84s2") + +[node name="Sprite2D" type="Sprite2D" parent="."] +scale = Vector2(3, 3) +texture = ExtResource("2_0ivff") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +position = Vector2(1.77636e-15, 0) +scale = Vector2(5, 5) +shape = SubResource("RectangleShape2D_3kmt7") diff --git a/examples/2DCoopBallChallenge/scenes/goal/goal_positions.tscn b/examples/2DCoopBallChallenge/scenes/goal/goal_positions.tscn new file mode 100755 index 0000000..24690e0 --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/goal/goal_positions.tscn @@ -0,0 +1,55 @@ +[gd_scene load_steps=3 format=3 uid="uid://lc8300re8fgd"] + +[ext_resource type="Script" uid="uid://1dbj4cknn23h" path="res://scenes/game_scene/goal_position_manager.gd" id="1_halm4"] +[ext_resource type="Texture2D" uid="uid://bbpqyprxxe2fk" path="res://assets/goal.png" id="2_y5yss"] + +[node name="GoalPositions" type="Node2D"] +script = ExtResource("1_halm4") + +[node name="Sprite2D3" type="Sprite2D" parent="."] +modulate = Color(1, 1, 1, 0.14902) +position = Vector2(407, 303) +scale = Vector2(2, 2) +texture = ExtResource("2_y5yss") + +[node name="Sprite2D4" type="Sprite2D" parent="."] +modulate = Color(1, 1, 1, 0.14902) +position = Vector2(3584, 313) +scale = Vector2(2, 2) +texture = ExtResource("2_y5yss") + +[node name="Sprite2D5" type="Sprite2D" parent="."] +modulate = Color(1, 1, 1, 0.14902) +position = Vector2(-244, 317) +scale = Vector2(2, 2) +texture = ExtResource("2_y5yss") + +[node name="Sprite2D6" type="Sprite2D" parent="."] +modulate = Color(1, 1, 1, 0.14902) +position = Vector2(4223, 313) +scale = Vector2(2, 2) +texture = ExtResource("2_y5yss") + +[node name="Sprite2D7" type="Sprite2D" parent="."] +modulate = Color(1, 1, 1, 0.14902) +position = Vector2(-873, 327) +scale = Vector2(2, 2) +texture = ExtResource("2_y5yss") + +[node name="Sprite2D8" type="Sprite2D" parent="."] +modulate = Color(1, 1, 1, 0.14902) +position = Vector2(4878, 322) +scale = Vector2(2, 2) +texture = ExtResource("2_y5yss") + +[node name="Sprite2D9" type="Sprite2D" parent="."] +modulate = Color(1, 1, 1, 0.14902) +position = Vector2(-1666, -307) +scale = Vector2(2, 2) +texture = ExtResource("2_y5yss") + +[node name="Sprite2D10" type="Sprite2D" parent="."] +modulate = Color(1, 1, 1, 0.14902) +position = Vector2(5675, -324) +scale = Vector2(2, 2) +texture = ExtResource("2_y5yss") diff --git a/examples/2DCoopBallChallenge/scenes/player/VelocitySensor2D.gd b/examples/2DCoopBallChallenge/scenes/player/VelocitySensor2D.gd new file mode 100755 index 0000000..b37f67d --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/player/VelocitySensor2D.gd @@ -0,0 +1,19 @@ +extends ISensor2D + +## Reports character body velocities +## Note: velocity is in global reference, not transformed +## (as the players in this env do not rotate, it should be sufficient) + +@export var objects_to_observe: Array[CharacterBody2D] +## Max expected velocity used for normalization (larger values will be clipped) +@export var max_velocity := 1500 + + +func get_observation(): + var observations: Array[float] + + for obj in objects_to_observe: + var velocity := obj.get_real_velocity().limit_length(max_velocity) / max_velocity + observations.append_array([velocity.x, velocity.y]) + + return observations diff --git a/examples/2DCoopBallChallenge/scenes/player/VelocitySensor2D.gd.uid b/examples/2DCoopBallChallenge/scenes/player/VelocitySensor2D.gd.uid new file mode 100755 index 0000000..9eea306 --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/player/VelocitySensor2D.gd.uid @@ -0,0 +1 @@ +uid://bu0wvfxl80g6b diff --git a/examples/2DCoopBallChallenge/scenes/player/player.gd b/examples/2DCoopBallChallenge/scenes/player/player.gd new file mode 100755 index 0000000..f6f8e5a --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/player/player.gd @@ -0,0 +1,77 @@ +extends CharacterBody2D +class_name Player + +@export var speed := 2400.0 +@export var jump_velocity := -1400.0 +@export var ai_controller: PlayerAIController + +@onready var animated_sprite: AnimatedSprite2D = $AnimatedSprite2D +@onready var initial_transform := transform + +var requested_movement: float +var requested_jump: bool + +var last_to_hit_ball: bool + +func hit_ball(): + last_to_hit_ball = true + $CanHitBallIndicator.visible = false + + +func clear_hit_ball(): + last_to_hit_ball = false + $CanHitBallIndicator.visible = true + + +func _physics_process(delta: float) -> void: + handle_movement(delta) + + +func handle_movement(delta: float): + apply_gravity(delta) + + # Controls (human or AI controlled) + var direction: float + if ai_controller.control_mode == AIController2D.ControlModes.HUMAN: + direction = Input.get_axis("move_left", "move_right") + requested_jump = Input.is_action_pressed("jump") + else: + direction = requested_movement + + # Horizontal movement + velocity.x = direction * speed + if velocity.x: + animated_sprite.flip_h = velocity.x < 0 + if is_on_floor(): + animated_sprite.animation = "move" + animated_sprite.play() + + # Jump + if requested_jump and is_on_floor(): + velocity.y = jump_velocity + requested_jump = false + animated_sprite.animation = "jump" + animated_sprite.play() + + # Stop animation if not moving + if velocity.length_squared() < 0.01 and is_on_floor(): + animated_sprite.animation = "move" + animated_sprite.stop() + + move_and_slide() + + +func apply_gravity(delta) -> void: + if not is_on_floor(): + velocity += Vector2.DOWN * 14000 * delta + + +func reset() -> void: + transform = initial_transform + + var state := PhysicsServer2D.body_get_direct_state(get_rid()) + state.transform = global_transform + velocity = Vector2.ZERO + requested_movement = 0 + requested_jump = 0 + clear_hit_ball() diff --git a/examples/2DCoopBallChallenge/scenes/player/player.gd.uid b/examples/2DCoopBallChallenge/scenes/player/player.gd.uid new file mode 100755 index 0000000..454838d --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/player/player.gd.uid @@ -0,0 +1 @@ +uid://b60kdidvpybwh diff --git a/examples/2DCoopBallChallenge/scenes/player/player.tscn b/examples/2DCoopBallChallenge/scenes/player/player.tscn new file mode 100755 index 0000000..90154c1 --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/player/player.tscn @@ -0,0 +1,94 @@ +[gd_scene load_steps=12 format=3 uid="uid://dyobkmccrdtji"] + +[ext_resource type="Script" uid="uid://b60kdidvpybwh" path="res://scenes/player/player.gd" id="1_uuo8p"] +[ext_resource type="Texture2D" uid="uid://bapc1vq7fr0av" path="res://assets/player/jump/Player1Jump1.png" id="2_6wykd"] +[ext_resource type="Texture2D" uid="uid://bmraxeklbi6h0" path="res://assets/player/move/Player-1.png" id="2_t2oqs"] +[ext_resource type="Texture2D" uid="uid://33reagv1bqpr" path="res://assets/player/jump/Player1Jump2.png" id="3_0ssda"] +[ext_resource type="Texture2D" uid="uid://wxq1h605mn72" path="res://assets/player/move/Player-2.png" id="3_y4ujj"] +[ext_resource type="Texture2D" uid="uid://c7wxqpsnaqfpb" path="res://assets/player/move/Player-3.png" id="4_iy4ba"] +[ext_resource type="Texture2D" uid="uid://dqwycbviv5ag2" path="res://assets/player/jump/Player1Jump3.png" id="4_ngojv"] +[ext_resource type="Script" uid="uid://dar4d1peo5076" path="res://addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd" id="6_ybkrn"] +[ext_resource type="Script" uid="uid://cyrvpwvwcjebl" path="res://addons/godot_rl_agents/sensors/sensors_2d/PositionSensor2D.gd" id="9_fm80t"] + +[sub_resource type="SpriteFrames" id="SpriteFrames_5tff7"] +animations = [{ +"frames": [{ +"duration": 1.0, +"texture": ExtResource("2_6wykd") +}, { +"duration": 1.0, +"texture": ExtResource("3_0ssda") +}, { +"duration": 1.0, +"texture": ExtResource("4_ngojv") +}], +"loop": true, +"name": &"jump", +"speed": 6.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": ExtResource("2_t2oqs") +}, { +"duration": 1.0, +"texture": ExtResource("3_y4ujj") +}, { +"duration": 1.0, +"texture": ExtResource("4_iy4ba") +}], +"loop": true, +"name": &"move", +"speed": 6.0 +}] + +[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_jl23j"] +radius = 70.7 +height = 183.6 + +[node name="Player" type="CharacterBody2D"] +collision_layer = 2 +collision_mask = 11 +script = ExtResource("1_uuo8p") +speed = 1400.0 +jump_velocity = -3900.0 + +[node name="CanHitBallIndicator" type="Label" parent="."] +offset_left = -37.0 +offset_top = -430.0 +offset_right = 36.0 +offset_bottom = -81.0 +theme_override_colors/font_color = Color(1, 1, 1, 1) +theme_override_colors/font_shadow_color = Color(0, 0, 0, 1) +theme_override_colors/font_outline_color = Color(0, 0, 0, 1) +theme_override_constants/outline_size = 4 +theme_override_constants/shadow_outline_size = 5 +theme_override_font_sizes/font_size = 256 +text = "." +horizontal_alignment = 1 + +[node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="."] +scale = Vector2(1.5, 1.5) +sprite_frames = SubResource("SpriteFrames_5tff7") +animation = &"move" +autoplay = "move" + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +shape = SubResource("CapsuleShape2D_jl23j") + +[node name="RaycastSensorPlatform" type="Node2D" parent="."] +rotation = 1.5708 +script = ExtResource("6_ybkrn") +n_rays = 8.0 +ray_length = 4000 + +[node name="RaycastSensorGround" type="Node2D" parent="."] +rotation = 1.5708 +script = ExtResource("6_ybkrn") +collision_mask = 8 +n_rays = 8.0 +ray_length = 4000 + +[node name="PositionSensor2D" type="Node2D" parent="."] +visible = false +script = ExtResource("9_fm80t") +debug_lines = false diff --git a/examples/2DCoopBallChallenge/scenes/player/player_ai_controller.gd b/examples/2DCoopBallChallenge/scenes/player/player_ai_controller.gd new file mode 100755 index 0000000..798856b --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/player/player_ai_controller.gd @@ -0,0 +1,79 @@ +extends AIController2D +class_name PlayerAIController + +@export var players: Players +@export var sensors: Array[Node2D] + +var is_success := false + + +func _ready(): + super._ready() + + +func _physics_process(_delta): + n_steps += 1 + if needs_reset: + reset() + + if n_steps > reset_after: + players.game_manager.episode_timed_out() + + +func end_episode(final_reward := 0.0, success := false) -> void: + is_success = success + reward += final_reward + done = true + reset() + + +func reset(): + old_obs.clear() + super.reset() + + +func get_info() -> Dictionary: + if done: + return {"is_success": is_success} + return {} + + +var old_obs: PackedFloat32Array + +func get_obs() -> Dictionary: + var obs: PackedFloat32Array + + for sensor in sensors: + obs.append_array(sensor.get_observation()) + + for player in players.players: + ( + obs + . append_array( + [ + float(player.is_on_floor()), + float(player.last_to_hit_ball) + ] + ) + ) + return {"obs": obs} + + +func get_reward() -> float: + reward += players.game_manager.ball.get_approach_goal_reward() + return reward + + +func get_action_space() -> Dictionary: + return { + "actions_p1": {"size": 2, "action_type": "continuous"}, + "actions_p2": {"size": 2, "action_type": "continuous"}, + } + + +func set_action(action: Dictionary) -> void: + players.players[0].requested_movement = action.actions_p1[0] + players.players[0].requested_jump = action.actions_p1[1] > 0.8 + + players.players[1].requested_movement = action.actions_p2[0] + players.players[1].requested_jump = action.actions_p2[1] > 0.8 diff --git a/examples/2DCoopBallChallenge/scenes/player/player_ai_controller.gd.uid b/examples/2DCoopBallChallenge/scenes/player/player_ai_controller.gd.uid new file mode 100755 index 0000000..5b2d74a --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/player/player_ai_controller.gd.uid @@ -0,0 +1 @@ +uid://dlkdrp0sajie diff --git a/examples/2DCoopBallChallenge/scenes/player/players.gd b/examples/2DCoopBallChallenge/scenes/player/players.gd new file mode 100755 index 0000000..993feec --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/player/players.gd @@ -0,0 +1,20 @@ +extends Node2D +class_name Players + +@export var game_manager: GameManager +@export var players: Array[Player] +@export var ai_controller: PlayerAIController + + +func get_random_player(): + return players.pick_random() + + +func end_episode(reward, success := false): + ai_controller.end_episode(reward, success) + for player in players: + player.reset() + + +func add_player_reward(reward: float): + ai_controller.reward += reward diff --git a/examples/2DCoopBallChallenge/scenes/player/players.gd.uid b/examples/2DCoopBallChallenge/scenes/player/players.gd.uid new file mode 100755 index 0000000..8ae906b --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/player/players.gd.uid @@ -0,0 +1 @@ +uid://c5bbhwxv1im5u diff --git a/examples/2DCoopBallChallenge/scenes/player/players.tscn b/examples/2DCoopBallChallenge/scenes/player/players.tscn new file mode 100755 index 0000000..6089a4e --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/player/players.tscn @@ -0,0 +1,31 @@ +[gd_scene load_steps=5 format=3 uid="uid://d2qsl7semlkyv"] + +[ext_resource type="PackedScene" uid="uid://dyobkmccrdtji" path="res://scenes/player/player.tscn" id="1_cr4op"] +[ext_resource type="Script" uid="uid://c5bbhwxv1im5u" path="res://scenes/player/players.gd" id="1_kutnq"] +[ext_resource type="Script" uid="uid://bu0wvfxl80g6b" path="res://scenes/player/VelocitySensor2D.gd" id="3_6l7rq"] +[ext_resource type="Script" uid="uid://dlkdrp0sajie" path="res://scenes/player/player_ai_controller.gd" id="11_eplxt"] + +[node name="Players" type="Node2D" node_paths=PackedStringArray("players", "ai_controller")] +script = ExtResource("1_kutnq") +players = [NodePath("Player"), NodePath("Player2")] +ai_controller = NodePath("AIController2D") + +[node name="Player" parent="." node_paths=PackedStringArray("ai_controller") instance=ExtResource("1_cr4op")] +ai_controller = NodePath("../AIController2D") + +[node name="Player2" parent="." node_paths=PackedStringArray("ai_controller") instance=ExtResource("1_cr4op")] +position = Vector2(300, 0) +ai_controller = NodePath("../AIController2D") + +[node name="VelocitySensor2D" type="Node2D" parent="." node_paths=PackedStringArray("objects_to_observe")] +script = ExtResource("3_6l7rq") +objects_to_observe = [NodePath("../Player"), NodePath("../Player2"), null] + +[node name="AIController2D" type="Node2D" parent="." node_paths=PackedStringArray("players", "sensors")] +script = ExtResource("11_eplxt") +players = NodePath("..") +sensors = [NodePath("../Player/RaycastSensorPlatform"), NodePath("../Player/RaycastSensorGround"), NodePath("../Player/PositionSensor2D"), NodePath("../Player2/RaycastSensorPlatform"), NodePath("../Player2/RaycastSensorGround"), NodePath("../Player2/PositionSensor2D"), NodePath("../VelocitySensor2D")] +reset_after = 5000 + +[editable path="Player"] +[editable path="Player2"] diff --git a/examples/2DCoopBallChallenge/scenes/tilemap/tile_map_layer.gd b/examples/2DCoopBallChallenge/scenes/tilemap/tile_map_layer.gd new file mode 100755 index 0000000..81d4598 --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/tilemap/tile_map_layer.gd @@ -0,0 +1,14 @@ +extends TileMapLayer +class_name MapManager + +## Maps tile names to tileset atlas coordinates +class Tiles: + const PLATFORM_LEFT_EDGE = Vector2i(0, 0) + const PLATFORM_MIDDLE = Vector2i(1, 0) + const PLATFORM_RIGHT_EDGE = Vector2i(2, 0) + const GROUND = Vector2i(0, 1) # currently not used + const GROUND_2 = Vector2i(1, 1) # currently not used + const SPIKES = Vector2i(2, 1) + const COIN = Vector2i(0, 2) + const GOAL = Vector2i(1, 2) + const SPAWN = Vector2i(2, 2) diff --git a/examples/2DCoopBallChallenge/scenes/tilemap/tile_map_layer.gd.uid b/examples/2DCoopBallChallenge/scenes/tilemap/tile_map_layer.gd.uid new file mode 100755 index 0000000..a29a52c --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/tilemap/tile_map_layer.gd.uid @@ -0,0 +1 @@ +uid://375ynh2lk7vl diff --git a/examples/2DCoopBallChallenge/scenes/tileset/tileset.tres b/examples/2DCoopBallChallenge/scenes/tileset/tileset.tres new file mode 100755 index 0000000..4bde33b --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/tileset/tileset.tres @@ -0,0 +1,32 @@ +[gd_resource type="TileSet" load_steps=3 format=3 uid="uid://sdmrwh4xd6qj"] + +[ext_resource type="Texture2D" uid="uid://c26la8ahcrqow" path="res://assets/tilesheet.png" id="1_xi302"] + +[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_oky61"] +texture = ExtResource("1_xi302") +texture_region_size = Vector2i(160, 160) +0:0/0 = 0 +0:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-80, -80, 80, -80, 80, 80, -80, 80) +1:0/0 = 0 +1:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-80, -80, 80, -80, 80, 80, -80, 80) +2:0/0 = 0 +2:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-80, -80, 80, -80, 80, 80, -80, 80) +0:1/0 = 0 +0:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-80, -80, 80, -80, 80, 80, -80, 80) +1:1/0 = 0 +1:1/0/physics_layer_1/polygon_0/points = PackedVector2Array(-80, -80, 80, -80, 80, 80, -80, 80) +2:1/0 = 0 +2:1/0/physics_layer_2/polygon_0/points = PackedVector2Array(-80, 80, -50.9091, -7.27273, 50.9091, -7.27273, 80, 80) +0:2/0 = 0 +0:2/0/physics_layer_1/polygon_0/points = PackedVector2Array(-21.8182, -21.8182, 21.8182, -21.8182, 21.8182, 21.8182, -21.8182, 21.8182, -21.8182, 21.8182) +1:2/0 = 0 +1:2/0/physics_layer_3/polygon_0/points = PackedVector2Array(-50.9091, -50.9091, -7.27273, 80, 7.27273, 80, 50.9091, -50.9091) +2:2/0 = 0 + +[resource] +tile_size = Vector2i(160, 160) +physics_layer_0/collision_layer = 1 +physics_layer_1/collision_layer = 8 +physics_layer_2/collision_layer = 8 +physics_layer_3/collision_layer = 16 +sources/0 = SubResource("TileSetAtlasSource_oky61") diff --git a/examples/2DCoopBallChallenge/scenes/training_scene/ball.gd b/examples/2DCoopBallChallenge/scenes/training_scene/ball.gd new file mode 100755 index 0000000..2444f97 --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/training_scene/ball.gd @@ -0,0 +1,97 @@ +extends CharacterBody2D +class_name Ball + +@export var game_manager: GameManager +@export var approach_goal_reward: ApproachNodeReward2D + +@onready var initial_transform = transform + +var pre_slide_velocity: Vector2 +var drag: float = 1.5 +var last_player_collided: Player + + +func get_approach_goal_reward(): + return approach_goal_reward.get_reward() + + +func _physics_process(delta: float) -> void: + pre_slide_velocity = velocity + velocity.x /= 1 + (drag * delta) + velocity.y /= 1 + (drag * delta) + velocity.x = clampf(velocity.x, -2500, 2500) + velocity.y = clampf(velocity.y, -2500, 2500) + + move_and_slide() + bounce_on_ground_collision() + apply_gravity(delta) + + +func apply_gravity(delta) -> void: + velocity += Vector2.DOWN * 2500.0 * delta + + +func bounce_on_ground_collision(): + for i in get_slide_collision_count(): + var last_coll = get_slide_collision(i) + if last_coll: + var collider := last_coll.get_collider() + if collider is MapManager: + velocity = pre_slide_velocity.bounce(last_coll.get_normal()) + + +func reset(): + approach_goal_reward.reset() + last_player_collided = null + transform = initial_transform + global_position = game_manager.players.get_random_player().global_position + Vector2.UP * 2000 + position.y = max(70, position.y) + velocity = Vector2.ZERO + + +var platform_collision_count: int = 0 + + +func episode_failed(): + game_manager.episode_failed() + + +func _on_area_2d_body_shape_entered( + body_rid: RID, collider: Node2D, _body_shape_index: int, _local_shape_index: int +) -> void: + if collider is Player: + if collider == last_player_collided: + episode_failed() + else: + var move = collider.global_position.direction_to(global_position) * 85 + if not test_move(global_transform, move): + global_position += move + velocity = ( + (collider.global_position.direction_to(global_position)) + * ( + 1000 + + collider.get_real_velocity().dot( + collider.global_position.direction_to(global_position) + ) + ) + ) + game_manager.player_hit_ball(collider) + last_player_collided = collider + + elif collider is MapManager: + var coords = collider.get_coords_for_body_rid(body_rid) + if ( + collider.get_cell_atlas_coords(coords) == MapManager.Tiles.PLATFORM_LEFT_EDGE + or collider.get_cell_atlas_coords(coords) == MapManager.Tiles.PLATFORM_MIDDLE + or collider.get_cell_atlas_coords(coords) == MapManager.Tiles.PLATFORM_RIGHT_EDGE + ): + if platform_collision_count == 0: + platform_collision_count += 1 + await get_tree().physics_frame + episode_failed() + platform_collision_count = 0 + + +func _on_goal_body_entered(body: Node2D) -> void: + if body == self: + game_manager.on_ball_goal_reached() diff --git a/examples/2DCoopBallChallenge/scenes/training_scene/ball.gd.uid b/examples/2DCoopBallChallenge/scenes/training_scene/ball.gd.uid new file mode 100755 index 0000000..d2538a3 --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/training_scene/ball.gd.uid @@ -0,0 +1 @@ +uid://8xjlwdi087jf diff --git a/examples/2DCoopBallChallenge/scenes/training_scene/camera_2d.gd b/examples/2DCoopBallChallenge/scenes/training_scene/camera_2d.gd new file mode 100755 index 0000000..a97c745 --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/training_scene/camera_2d.gd @@ -0,0 +1,5 @@ +extends Camera2D + + +func _ready() -> void: + make_current() diff --git a/examples/2DCoopBallChallenge/scenes/training_scene/camera_2d.gd.uid b/examples/2DCoopBallChallenge/scenes/training_scene/camera_2d.gd.uid new file mode 100755 index 0000000..3e4e6ea --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/training_scene/camera_2d.gd.uid @@ -0,0 +1 @@ +uid://bqt62hhqmt7bv diff --git a/examples/2DCoopBallChallenge/scenes/training_scene/inference_scene.tscn b/examples/2DCoopBallChallenge/scenes/training_scene/inference_scene.tscn new file mode 100755 index 0000000..5b5b2c6 --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/training_scene/inference_scene.tscn @@ -0,0 +1,65 @@ +[gd_scene load_steps=7 format=3 uid="uid://c2baxuisewykf"] + +[ext_resource type="PackedScene" uid="uid://dl1ldlvoh3s4n" path="res://scenes/game_scene/all_game_scenes.tscn" id="1_h4anc"] +[ext_resource type="Script" uid="uid://bqt62hhqmt7bv" path="res://scenes/training_scene/camera_2d.gd" id="3_h4anc"] +[ext_resource type="Script" uid="uid://dkxvjlyxwlu3t" path="res://addons/godot_rl_agents/sync.gd" id="3_m4b8y"] + +[sub_resource type="Animation" id="Animation_m4b8y"] +length = 0.001 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:position") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [Vector2(200, -10722)] +} + +[sub_resource type="Animation" id="Animation_h4anc"] +resource_name = "new_animation" +length = 30.0 +loop_mode = 2 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:position") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 6, 10, 16, 20), +"transitions": PackedFloat32Array(1, 0.5, -2, 0.5, -2), +"update": 0, +"values": [Vector2(200, -10722), Vector2(206.521, -10722), Vector2(207, -299), Vector2(207, -299), Vector2(207, 13167)] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_8edvk"] +_data = { +&"RESET": SubResource("Animation_m4b8y"), +&"new_animation": SubResource("Animation_h4anc") +} + +[node name="InferenceScene" type="Node2D"] + +[node name="Camera2D" type="Camera2D" parent="."] +position = Vector2(200, -10722) +zoom = Vector2(0.257, 0.257) +script = ExtResource("3_h4anc") + +[node name="AnimationPlayer" type="AnimationPlayer" parent="Camera2D"] +libraries = { +&"": SubResource("AnimationLibrary_8edvk") +} +autoplay = "new_animation" +speed_scale = 0.5 + +[node name="AllGameScenes" parent="." instance=ExtResource("1_h4anc")] + +[node name="Sync" type="Node" parent="."] +script = ExtResource("3_m4b8y") +control_mode = 2 +action_repeat = 2 +onnx_model_path = "onnx/model.onnx" diff --git a/examples/2DCoopBallChallenge/scenes/training_scene/training_scene.tscn b/examples/2DCoopBallChallenge/scenes/training_scene/training_scene.tscn new file mode 100755 index 0000000..5bce5d4 --- /dev/null +++ b/examples/2DCoopBallChallenge/scenes/training_scene/training_scene.tscn @@ -0,0 +1,204 @@ +[gd_scene load_steps=4 format=3 uid="uid://b6fkgmq1op7kd"] + +[ext_resource type="Script" uid="uid://bqt62hhqmt7bv" path="res://scenes/training_scene/camera_2d.gd" id="2_milqm"] +[ext_resource type="Script" uid="uid://dkxvjlyxwlu3t" path="res://addons/godot_rl_agents/sync.gd" id="3_n8d1s"] +[ext_resource type="PackedScene" uid="uid://dl1ldlvoh3s4n" path="res://scenes/game_scene/all_game_scenes.tscn" id="4_n8d1s"] + +[node name="TrainingScene" type="Node2D"] + +[node name="Camera2D" type="Camera2D" parent="."] +position = Vector2(4166, 2894) +zoom = Vector2(0.107, 0.107) +script = ExtResource("2_milqm") + +[node name="Sync" type="Node" parent="."] +script = ExtResource("3_n8d1s") +action_repeat = 2 + +[node name="AllGameScenes" parent="." instance=ExtResource("4_n8d1s")] + +[node name="SubViewport" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport2" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport2" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport3" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport3" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport4" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport4" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport5" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport5" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport6" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport6" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport7" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport7" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport8" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport8" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport9" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport9" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport10" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport10" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport11" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport11" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport12" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport12" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport13" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport13" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport14" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport14" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport15" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport15" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport16" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport16" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport17" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport17" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport18" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport18" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport19" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport19" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport20" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport20" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport21" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport21" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport22" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport22" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport23" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport23" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport24" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport24" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport25" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport25" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport26" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport26" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport27" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport27" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport28" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport28" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport29" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport29" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport30" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport30" instance=ExtResource("4_n8d1s")] + +[node name="SubViewport31" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AllGameScenes" parent="SubViewport31" instance=ExtResource("4_n8d1s")] From ee0576bca2e99aeef8ca6c3db4f6275a0464926f Mon Sep 17 00:00:00 2001 From: Ivan-267 <61947090+Ivan-267@users.noreply.github.com> Date: Tue, 20 May 2025 04:59:17 +0200 Subject: [PATCH 2/2] Update readme.md --- examples/2DCoopBallChallenge/readme.md | 76 +++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/examples/2DCoopBallChallenge/readme.md b/examples/2DCoopBallChallenge/readme.md index f90a1c7..53db47a 100755 --- a/examples/2DCoopBallChallenge/readme.md +++ b/examples/2DCoopBallChallenge/readme.md @@ -2,20 +2,94 @@ ## Goal: +- The ball needs to reach the goal, it must not fall and/or hit any ground tile with grass ("platform" tile), otherwise a negative reward is given (ground tiles the left-right and top sections of the maps are allowed). +- Only one player is allowed to hit the ball at a time, so cooperation / taking turns is required. +- There is a time out period as well with a negative reward if reached. ## Observations: +```gdscript +func get_obs() -> Dictionary: + var obs: PackedFloat32Array + + for sensor in sensors: + obs.append_array(sensor.get_observation()) + + for player in players.players: + ( + obs + . append_array( + [ + float(player.is_on_floor()), + float(player.last_to_hit_ball) + ] + ) + ) + return {"obs": obs} +``` + +The data collected from sensor includes: +- 2 Raycast sensors from each player, one for ground tiles (that the ball can collide with), and other for the "platform" tiles. +- Each player has a position sensor that tracks relative positions to: ball, goal, and other player. +- There's an additional position sensor that tracks the relative position of the goal from the ball. +- There's a velocity sensor that tracks velocities of both players and the ball. ## Running inference: -If you’d just like to test the env using the pre-trained onnx model, open `[SceneName]` in Godot, then press `F6`. +If you’d just like to test the env using the pre-trained onnx model, open `res://scenes/training_scene/inference_scene.tscn` in Godot, then press `F6`. ## Training: There’s an included onnx file that was trained with https://github.com/edbeeching/godot_rl_agents/blob/main/examples/stable_baselines3_example.py +Hyperparams used (you can modify the script to use these): +```python +if args.resume_model_path is None: + learning_rate = 0.00008 if not args.linear_lr_schedule else linear_schedule(0.0001) + + model: PPO = PPO( + "MlpPolicy", + env, + verbose=2, + n_steps=32, + batch_size=32 * env.num_envs, + n_epochs=80, + target_kl=0.01, + learning_rate=learning_rate, + tensorboard_log=args.experiment_dir, + ) +``` + +Note that while `MlpPolicy` was used which requires further change to: +`env = SBGSingleObsEnv( + env_path=args.env_path, show_window=args.viz, seed=args.seed, n_parallel=args.n_parallel, speedup=args.speedup, +)` + +and: + +`export_model_as_onnx(model, str(path_onnx), use_obs_array=True)` + +This is not necessary, you can just use: +`"MultiInputPolicy"` instead, then you don't need the two changes above and it shouldn't affect the performance, but still the original setting is included above for reference. + CL arguments used (also onnx export and model saving was used, enable as needed, add `env_path` too to set the exported executable of the platform): ```python +--onnx_export_path=model.onnx +--timesteps=150_000_000 +--env_path=EXPORTED ENV PATH HERE +--n_parallel=4 +--speedup=16 +--experiment_name=exp ``` +Additionally, `--save_checkpoint_frequency=1_000_000` was used, you can optionally add it if you'd like to have checkpoints as the agent trains. + +Further note: +It's possible that training was done using a slight modification of the sync node: +` stream.set_no_delay(false) # TODO check if this improves performance or not ` +it might have been set to `false` instead of the default `true` (to which I returned when using Python inference on Linux, as I noticed with false it seemed laggier), +however this shouldn't affect the training results and you shouldn't need to modify it. + Stats from the training session (success rate only): + +![success rate](https://github.com/user-attachments/assets/a134beeb-b383-48af-97a1-8e30195cd72e)