From 0be4ed79efcdc71586460ca6dc34cfb0f875b883 Mon Sep 17 00:00:00 2001 From: Ivan-267 <61947090+Ivan-267@users.noreply.github.com> Date: Tue, 7 Jan 2025 21:47:15 +0100 Subject: [PATCH 1/8] Create readme.md --- examples/Ship2D/readme.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 examples/Ship2D/readme.md diff --git a/examples/Ship2D/readme.md b/examples/Ship2D/readme.md new file mode 100644 index 0000000..3ce2261 --- /dev/null +++ b/examples/Ship2D/readme.md @@ -0,0 +1 @@ +### Ship2D readme goes here From cdaf44e6a07d85eb992561d9d148731b81915fba Mon Sep 17 00:00:00 2001 From: Ivan-267 <61947090+Ivan-267@users.noreply.github.com> Date: Tue, 7 Jan 2025 23:18:39 +0100 Subject: [PATCH 2/8] Update readme.md --- examples/Ship2D/readme.md | 59 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/examples/Ship2D/readme.md b/examples/Ship2D/readme.md index 3ce2261..062773c 100644 --- a/examples/Ship2D/readme.md +++ b/examples/Ship2D/readme.md @@ -1 +1,58 @@ -### Ship2D readme goes here +# Ship2D environment + + +## Goal: +The ship needs to avoid asteroids for as long as possible. +The agent is trained on two scenarios, one where shooting the asteroids is possible, and one with shooting disabled. + +## Observations: +Note: `raycast_stack_frames` is set to 2. +```gdscript +func get_obs() -> Dictionary: + var obs: Array[float] + + var raycast_obs: Array[float] = get_raycast_obs() + + if raycast_stack_frames > 1: + if obs_stack.is_empty(): + obs_stack.resize(1) + obs_stack.fill(raycast_obs) + + obs_stack.remove_at(0) + obs_stack.append(raycast_obs) + # + for obs_array in obs_stack: + obs.append_array(obs_array) + else: + obs.append_array(raycast_obs) + + obs.append(float(player.can_shoot)) + obs.append(clampf(player.time_since_ball_spawned / player.ball_fire_interval_seconds, 0, 1.0)) + return {"obs": obs} +``` + +## Actions: +```python +func get_action_space() -> Dictionary: + return { + "move": {"size": 1, "action_type": "continuous"}, + "shoot": {"size": 1, "action_type": "continuous"}, + } +``` + +## Running inference: + +If you’d just like to test the env using the pre-trained onnx model, open `res://scenes/test_scene/test_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 + +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 +TODO +``` + +Stats from the training session: +TODO From 31340675e984a38bb121cae4b6d5a8a3d049fec1 Mon Sep 17 00:00:00 2001 From: Ivan-267 <61947090+Ivan-267@users.noreply.github.com> Date: Wed, 8 Jan 2025 23:33:12 +0100 Subject: [PATCH 3/8] Update readme.md Updated to the newest obs/actions/training stats, some updates to the env include (there were a few more adjustments): - Switched to discrete actions - Added velocity to obs - Removed ray-cast stacking ability as it didn't affect the results notably - Fixed an issue with removing objects / memory that caused FPS to drop significantly during longer training --- examples/Ship2D/readme.md | 71 +++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/examples/Ship2D/readme.md b/examples/Ship2D/readme.md index 062773c..a9d63c4 100644 --- a/examples/Ship2D/readme.md +++ b/examples/Ship2D/readme.md @@ -1,4 +1,4 @@ -# Ship2D environment +# Ship2D example environment ## Goal: @@ -6,28 +6,29 @@ The ship needs to avoid asteroids for as long as possible. The agent is trained on two scenarios, one where shooting the asteroids is possible, and one with shooting disabled. ## Observations: -Note: `raycast_stack_frames` is set to 2. ```gdscript +func get_raycast_obs() -> Array[float]: + var obs: Array[float] + for sensor in raycast_sensors: + obs.append_array(sensor.get_observation()) + return obs + + func get_obs() -> Dictionary: var obs: Array[float] var raycast_obs: Array[float] = get_raycast_obs() - if raycast_stack_frames > 1: - if obs_stack.is_empty(): - obs_stack.resize(1) - obs_stack.fill(raycast_obs) - - obs_stack.remove_at(0) - obs_stack.append(raycast_obs) - # - for obs_array in obs_stack: - obs.append_array(obs_array) - else: - obs.append_array(raycast_obs) - - obs.append(float(player.can_shoot)) - obs.append(clampf(player.time_since_ball_spawned / player.ball_fire_interval_seconds, 0, 1.0)) + obs.append_array(raycast_obs) + + var velocity_x = clampf(player.get_real_velocity().x / 3000, -1.0, 1.0) + + obs.append_array([ + float(player.can_shoot), + clampf(player.time_since_ball_spawned / player.ball_fire_interval_seconds, 0, 1.0), + velocity_x + ]) + return {"obs": obs} ``` @@ -35,8 +36,8 @@ func get_obs() -> Dictionary: ```python func get_action_space() -> Dictionary: return { - "move": {"size": 1, "action_type": "continuous"}, - "shoot": {"size": 1, "action_type": "continuous"}, + "move": {"size": 3, "action_type": "discrete"}, + "shoot": {"size": 2, "action_type": "discrete"}, } ``` @@ -44,15 +45,41 @@ func get_action_space() -> Dictionary: If you’d just like to test the env using the pre-trained onnx model, open `res://scenes/test_scene/test_scene.tscn` in Godot, then press `F6`. +You can control the shooting ability of the ship by turning on or off the `override can shoot always enabled` property of `BaseGameScene` in the inspector. + +![image](https://github.com/user-attachments/assets/f61b4e1d-108f-4157-9089-ae5345675e22) + ## 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 +There’s an included onnx file that was trained using SB3. + +The hyperparameters in the example script were adjusted: + +```Python + learning_rate = 0.0003 if not args.linear_lr_schedule else linear_schedule(0.0003) + model: PPO = PPO( + "MultiInputPolicy", + env, + verbose=2, + n_steps=128, + n_epochs=3, + tensorboard_log=args.experiment_dir, + learning_rate=learning_rate, + target_kl=0.02 + ) +``` 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 -TODO +--env_path=path_to_exported_game_executable +--speedup=16 +--n_parallel=4 +--onnx_export_path=model.onnx +--timesteps=30_000_000 +--linear_lr_schedule ``` Stats from the training session: -TODO +![training session rewards](https://github.com/user-attachments/assets/986fac4f-fcf9-470b-8809-c6018228ce43) + From afc0e8064164a17bb4cb41b16300a36abf68a7a2 Mon Sep 17 00:00:00 2001 From: Ivan-267 <61947090+Ivan-267@users.noreply.github.com> Date: Wed, 8 Jan 2025 23:44:57 +0100 Subject: [PATCH 4/8] Uploads env files --- .gitignore | 1 + examples/Ship2D/.gitattributes | 2 + examples/Ship2D/.gitignore | 3 + .../controller/ai_controller_2d.gd | 130 ++++ .../controller/ai_controller_3d.gd | 130 ++++ .../addons/godot_rl_agents/godot_rl_agents.gd | 16 + .../Ship2D/addons/godot_rl_agents/icon.png | Bin 0 -> 198 bytes .../onnx/csharp/ONNXInference.cs | 109 ++++ .../onnx/csharp/SessionConfigurator.cs | 131 ++++ .../onnx/csharp/docs/ONNXInference.xml | 31 + .../onnx/csharp/docs/SessionConfigurator.xml | 29 + .../onnx/wrapper/ONNX_wrapper.gd | 54 ++ .../Ship2D/addons/godot_rl_agents/plugin.cfg | 7 + .../sensors_2d/ExampleRaycastSensor2D.tscn | 48 ++ .../sensors/sensors_2d/GridSensor2D.gd | 235 +++++++ .../sensors/sensors_2d/ISensor2D.gd | 25 + .../sensors/sensors_2d/RaycastSensor2D.gd | 118 ++++ .../sensors/sensors_2d/RaycastSensor2D.tscn | 7 + .../sensors_3d/ExampleRaycastSensor3D.tscn | 6 + .../sensors/sensors_3d/GridSensor3D.gd | 258 ++++++++ .../sensors/sensors_3d/ISensor3D.gd | 25 + .../sensors/sensors_3d/RGBCameraSensor3D.gd | 63 ++ .../sensors/sensors_3d/RGBCameraSensor3D.tscn | 35 ++ .../sensors/sensors_3d/RaycastSensor3D.gd | 185 ++++++ .../sensors/sensors_3d/RaycastSensor3D.tscn | 27 + .../Ship2D/addons/godot_rl_agents/sync.gd | 594 ++++++++++++++++++ examples/Ship2D/assets/Background.png | Bin 0 -> 194564 bytes examples/Ship2D/assets/Ship.png | Bin 0 -> 12697 bytes examples/Ship2D/assets/asset_license.md | 3 + examples/Ship2D/assets/asteroid.png | Bin 0 -> 5450 bytes examples/Ship2D/assets/asteroid2.png | Bin 0 -> 5385 bytes examples/Ship2D/assets/asteroid3.png | Bin 0 -> 5194 bytes examples/Ship2D/assets/asteroid4.png | Bin 0 -> 5672 bytes examples/Ship2D/assets/asteroid5.png | Bin 0 -> 5662 bytes examples/Ship2D/assets/asteroid6.png | Bin 0 -> 5585 bytes examples/Ship2D/assets/ball.svg | 72 +++ examples/Ship2D/icon.svg | 1 + examples/Ship2D/model.onnx | Bin 0 -> 25306 bytes examples/Ship2D/project.godot | 64 ++ examples/Ship2D/readme.md | 1 + examples/Ship2D/scenes/asteroid/asteroid.gd | 80 +++ examples/Ship2D/scenes/asteroid/asteroid.tscn | 90 +++ .../scenes/asteroid/asteroid_spawner.gd | 40 ++ .../scenes/asteroid/asteroid_spawner.tscn | 8 + examples/Ship2D/scenes/ball/ball.gd | 45 ++ examples/Ship2D/scenes/ball/ball.tscn | 22 + .../scenes/game_scenes/base_game_scene.gd | 21 + .../scenes/game_scenes/base_game_scene.tscn | 51 ++ .../scenes/game_scenes/spawned_objects.gd | 9 + examples/Ship2D/scenes/ship/ship.gd | 84 +++ examples/Ship2D/scenes/ship/ship.tscn | 52 ++ .../Ship2D/scenes/ship/ship_ai_controller.gd | 75 +++ .../Ship2D/scenes/test_scene/test_scene.tscn | 18 + .../scenes/train_scene/training_scene.tscn | 43 ++ examples/Ship2D/ships2d.csproj | 11 + examples/Ship2D/ships2d.sln | 19 + 56 files changed, 3078 insertions(+) create mode 100644 examples/Ship2D/.gitattributes create mode 100644 examples/Ship2D/.gitignore create mode 100644 examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_2d.gd create mode 100644 examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_3d.gd create mode 100644 examples/Ship2D/addons/godot_rl_agents/godot_rl_agents.gd create mode 100644 examples/Ship2D/addons/godot_rl_agents/icon.png create mode 100644 examples/Ship2D/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs create mode 100644 examples/Ship2D/addons/godot_rl_agents/onnx/csharp/SessionConfigurator.cs create mode 100644 examples/Ship2D/addons/godot_rl_agents/onnx/csharp/docs/ONNXInference.xml create mode 100644 examples/Ship2D/addons/godot_rl_agents/onnx/csharp/docs/SessionConfigurator.xml create mode 100644 examples/Ship2D/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd create mode 100644 examples/Ship2D/addons/godot_rl_agents/plugin.cfg create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/ExampleRaycastSensor2D.tscn create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/GridSensor2D.gd create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/ISensor2D.gd create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.tscn create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/ExampleRaycastSensor3D.tscn create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/GridSensor3D.gd create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/ISensor3D.gd create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.tscn create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.gd create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.tscn create mode 100644 examples/Ship2D/addons/godot_rl_agents/sync.gd create mode 100644 examples/Ship2D/assets/Background.png create mode 100644 examples/Ship2D/assets/Ship.png create mode 100644 examples/Ship2D/assets/asset_license.md create mode 100644 examples/Ship2D/assets/asteroid.png create mode 100644 examples/Ship2D/assets/asteroid2.png create mode 100644 examples/Ship2D/assets/asteroid3.png create mode 100644 examples/Ship2D/assets/asteroid4.png create mode 100644 examples/Ship2D/assets/asteroid5.png create mode 100644 examples/Ship2D/assets/asteroid6.png create mode 100644 examples/Ship2D/assets/ball.svg create mode 100644 examples/Ship2D/icon.svg create mode 100644 examples/Ship2D/model.onnx create mode 100644 examples/Ship2D/project.godot create mode 100644 examples/Ship2D/scenes/asteroid/asteroid.gd create mode 100644 examples/Ship2D/scenes/asteroid/asteroid.tscn create mode 100644 examples/Ship2D/scenes/asteroid/asteroid_spawner.gd create mode 100644 examples/Ship2D/scenes/asteroid/asteroid_spawner.tscn create mode 100644 examples/Ship2D/scenes/ball/ball.gd create mode 100644 examples/Ship2D/scenes/ball/ball.tscn create mode 100644 examples/Ship2D/scenes/game_scenes/base_game_scene.gd create mode 100644 examples/Ship2D/scenes/game_scenes/base_game_scene.tscn create mode 100644 examples/Ship2D/scenes/game_scenes/spawned_objects.gd create mode 100644 examples/Ship2D/scenes/ship/ship.gd create mode 100644 examples/Ship2D/scenes/ship/ship.tscn create mode 100644 examples/Ship2D/scenes/ship/ship_ai_controller.gd create mode 100644 examples/Ship2D/scenes/test_scene/test_scene.tscn create mode 100644 examples/Ship2D/scenes/train_scene/training_scene.tscn create mode 100644 examples/Ship2D/ships2d.csproj create mode 100644 examples/Ship2D/ships2d.sln diff --git a/.gitignore b/.gitignore index ddcbc2f..b5e9975 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ builds/ # Blender *.blend1 +*.tmp diff --git a/examples/Ship2D/.gitattributes b/examples/Ship2D/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/examples/Ship2D/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/examples/Ship2D/.gitignore b/examples/Ship2D/.gitignore new file mode 100644 index 0000000..0af181c --- /dev/null +++ b/examples/Ship2D/.gitignore @@ -0,0 +1,3 @@ +# Godot 4+ specific ignores +.godot/ +/android/ diff --git a/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_2d.gd b/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_2d.gd new file mode 100644 index 0000000..5247135 --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_2d.gd @@ -0,0 +1,130 @@ +extends Node2D +class_name AIController2D + +enum ControlModes { INHERIT_FROM_SYNC, HUMAN, TRAINING, ONNX_INFERENCE, RECORD_EXPERT_DEMOS } ## Inherit setting from sync node ## Test the environment manually ## Train a model ## Load a pretrained model using an .onnx file ## 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/Ship2D/addons/godot_rl_agents/controller/ai_controller_3d.gd b/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_3d.gd new file mode 100644 index 0000000..4984b18 --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_3d.gd @@ -0,0 +1,130 @@ +extends Node3D +class_name AIController3D + +enum ControlModes { INHERIT_FROM_SYNC, HUMAN, TRAINING, ONNX_INFERENCE, RECORD_EXPERT_DEMOS } ## Inherit setting from sync node ## Test the environment manually ## Train a model ## Load a pretrained model using an .onnx file ## 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/Ship2D/addons/godot_rl_agents/godot_rl_agents.gd b/examples/Ship2D/addons/godot_rl_agents/godot_rl_agents.gd new file mode 100644 index 0000000..e4fe136 --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/icon.png b/examples/Ship2D/addons/godot_rl_agents/icon.png new file mode 100644 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/Ship2D/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs b/examples/Ship2D/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs new file mode 100644 index 0000000..6dcfa18 --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/onnx/csharp/SessionConfigurator.cs b/examples/Ship2D/addons/godot_rl_agents/onnx/csharp/SessionConfigurator.cs new file mode 100644 index 0000000..ad7a41c --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/onnx/csharp/docs/ONNXInference.xml b/examples/Ship2D/addons/godot_rl_agents/onnx/csharp/docs/ONNXInference.xml new file mode 100644 index 0000000..91b07d6 --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/onnx/csharp/docs/SessionConfigurator.xml b/examples/Ship2D/addons/godot_rl_agents/onnx/csharp/docs/SessionConfigurator.xml new file mode 100644 index 0000000..f160c02 --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd b/examples/Ship2D/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd new file mode 100644 index 0000000..d201f0c --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd @@ -0,0 +1,54 @@ +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/Ship2D/addons/godot_rl_agents/plugin.cfg b/examples/Ship2D/addons/godot_rl_agents/plugin.cfg new file mode 100644 index 0000000..b1bc988 --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/ExampleRaycastSensor2D.tscn b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/ExampleRaycastSensor2D.tscn new file mode 100644 index 0000000..5edb6c7 --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/GridSensor2D.gd b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/GridSensor2D.gd new file mode 100644 index 0000000..48b132e --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/ISensor2D.gd b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/ISensor2D.gd new file mode 100644 index 0000000..67669a1 --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd new file mode 100644 index 0000000..9bb54ed --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.tscn b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.tscn new file mode 100644 index 0000000..5ca402c --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/ExampleRaycastSensor3D.tscn b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/ExampleRaycastSensor3D.tscn new file mode 100644 index 0000000..a8057c7 --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/GridSensor3D.gd b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/GridSensor3D.gd new file mode 100644 index 0000000..24de9a4 --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/ISensor3D.gd b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/ISensor3D.gd new file mode 100644 index 0000000..aca3c2d --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd new file mode 100644 index 0000000..96dfb6a --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.tscn b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.tscn new file mode 100644 index 0000000..d58649c --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.gd b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.gd new file mode 100644 index 0000000..1357529 --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.tscn b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.tscn new file mode 100644 index 0000000..35f9796 --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/sync.gd b/examples/Ship2D/addons/godot_rl_agents/sync.gd new file mode 100644 index 0000000..68c6581 --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/sync.gd @@ -0,0 +1,594 @@ +extends Node +class_name Sync + +# --fixed-fps 2000 --disable-render-loop + +enum ControlModes { HUMAN, TRAINING, ONNX_INFERENCE } ## Test the environment manually ## Train a model ## 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 # Value of the largest logit for this action in the actions array + var largest_logit_idx: int # 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/Ship2D/assets/Background.png b/examples/Ship2D/assets/Background.png new file mode 100644 index 0000000000000000000000000000000000000000..82950690948bb40fe523cb2d26df551c8ec0b129 GIT binary patch literal 194564 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 L%yB*^5JvwGhgawg literal 0 HcmV?d00001 diff --git a/examples/Ship2D/assets/Ship.png b/examples/Ship2D/assets/Ship.png new file mode 100644 index 0000000000000000000000000000000000000000..b31ba7135752207622382b90708cc1aae9371ceb GIT binary patch literal 12697 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%0DOyQ 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 EKNYe$g#Z8m literal 0 HcmV?d00001 diff --git a/examples/Ship2D/assets/asteroid5.png b/examples/Ship2D/assets/asteroid5.png new file mode 100644 index 0000000000000000000000000000000000000000..a8ba77504f039bc2a11e85f2ff97dabc053d81ae GIT binary patch literal 5662 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 M2m^SXj(z0+0UK_3TmS$7 literal 0 HcmV?d00001 diff --git a/examples/Ship2D/assets/ball.svg b/examples/Ship2D/assets/ball.svg new file mode 100644 index 0000000..a16e335 --- /dev/null +++ b/examples/Ship2D/assets/ball.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + diff --git a/examples/Ship2D/icon.svg b/examples/Ship2D/icon.svg new file mode 100644 index 0000000..9d8b7fa --- /dev/null +++ b/examples/Ship2D/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/Ship2D/model.onnx b/examples/Ship2D/model.onnx new file mode 100644 index 0000000000000000000000000000000000000000..55d0d79dc8020bd0b80930fd51ef548b76076f51 GIT binary patch literal 25306 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 x$x6tKTBY+}>FO%7%KS2g|8tqAuI&Ftx-0*8+eq void: + var rand_scale := randf_range(0.7, 2) + scale = Vector2(rand_scale, rand_scale) + randomize_speed() + enable_random_sprite() + + +func _physics_process(delta: float) -> void: + process_movement(delta) + process_remove_on_out_of_bounds() + + _active_sprite.rotate(_sprite_rotation * delta) + + +## Moves the asteroid +func process_movement(delta): + position.x += _sideways_speed * delta + position.y += _downward_speed * delta + + +## Removes the asteroid if outside of the playing area +## (each instance that is not destroyed by ship or another asteroid should be removed by this method) +func process_remove_on_out_of_bounds(): + if position.y > 2000: + queue_free() + + +func enable_random_sprite(): + var sprite = _sprites.pick_random() + _active_sprite = sprite + _active_sprite.visible = true + + +## Randomizes the speed of the asteroid +func randomize_speed(): + _downward_speed = randf_range(downward_speed_min, downward_speed_max) + _sideways_speed = randf_range(-0.1, 0.1) * _downward_speed + + +## Destroys the asteroid, leaving a particle effect +var destroying: bool +func destroy(): + if destroying: return + particles.emitting = true + _active_sprite.visible = false + set_deferred("process_mode", PROCESS_MODE_DISABLED) + destroying = true + await particles.finished + queue_free() + + +func _on_particles_finished(): + queue_free() + + +## Handles collision with physics bodies (for now, only player/ship) +func _on_body_entered(body: PhysicsBody2D) -> void: + if body is Ship: + body.hit_by_asteroid() + destroy() + + +## Handles collisions with other areas (for now, specifically other asteroids) +func _on_area_entered(area: Area2D) -> void: + destroy() diff --git a/examples/Ship2D/scenes/asteroid/asteroid.tscn b/examples/Ship2D/scenes/asteroid/asteroid.tscn new file mode 100644 index 0000000..70eaa36 --- /dev/null +++ b/examples/Ship2D/scenes/asteroid/asteroid.tscn @@ -0,0 +1,90 @@ +[gd_scene load_steps=14 format=3 uid="uid://da7uefbrja8u0"] + +[ext_resource type="Script" path="res://scenes/asteroid/asteroid.gd" id="1_i2wen"] +[ext_resource type="Texture2D" uid="uid://c3uv7bhj0icto" path="res://assets/asteroid.png" id="2_uoiax"] +[ext_resource type="Texture2D" uid="uid://cjber4yoanul1" path="res://assets/asteroid2.png" id="3_danrt"] +[ext_resource type="Texture2D" uid="uid://drmjmr725556o" path="res://assets/asteroid3.png" id="4_ab212"] +[ext_resource type="Texture2D" uid="uid://b020rbym6vtor" path="res://assets/asteroid4.png" id="5_7s632"] +[ext_resource type="Texture2D" uid="uid://b5jhhj8ch1o3i" path="res://assets/asteroid5.png" id="6_du6mb"] +[ext_resource type="Texture2D" uid="uid://b05hpaculnwb5" path="res://assets/asteroid6.png" id="7_61yp0"] + +[sub_resource type="Curve" id="Curve_g1nht"] +bake_resolution = 10 +_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0] +point_count = 2 + +[sub_resource type="CurveTexture" id="CurveTexture_k1m6u"] +curve = SubResource("Curve_g1nht") + +[sub_resource type="Curve" id="Curve_s457i"] +max_value = 10.0 +bake_resolution = 10 +_data = [Vector2(0, 3.23649), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0] +point_count = 2 + +[sub_resource type="CurveTexture" id="CurveTexture_r5po8"] +curve = SubResource("Curve_s457i") + +[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_2m1fr"] +particle_flag_disable_z = true +angle_min = 1.07288e-05 +angle_max = 360.0 +angular_velocity_min = -50.0 +angular_velocity_max = 50.0 +radial_velocity_min = 113.77 +radial_velocity_max = 113.77 +radial_velocity_curve = SubResource("CurveTexture_r5po8") +gravity = Vector3(0, 0, 0) +scale_min = 0.01 +scale_max = 0.1 +color = Color(1, 1, 1, 0.737255) +alpha_curve = SubResource("CurveTexture_k1m6u") + +[sub_resource type="CircleShape2D" id="CircleShape2D_kuteu"] +radius = 53.6 + +[node name="Asteroid" type="Area2D"] +collision_layer = 2 +collision_mask = 514 +script = ExtResource("1_i2wen") + +[node name="GPUParticles2D" type="GPUParticles2D" parent="."] +process_mode = 1 +emitting = false +amount = 2 +process_material = SubResource("ParticleProcessMaterial_2m1fr") +texture = ExtResource("3_danrt") +one_shot = true +explosiveness = 1.0 + +[node name="Sprites" type="Node2D" parent="."] + +[node name="Sprite2D" type="Sprite2D" parent="Sprites"] +visible = false +texture = ExtResource("2_uoiax") + +[node name="Sprite2D2" type="Sprite2D" parent="Sprites"] +visible = false +texture = ExtResource("3_danrt") + +[node name="Sprite2D3" type="Sprite2D" parent="Sprites"] +visible = false +texture = ExtResource("4_ab212") + +[node name="Sprite2D4" type="Sprite2D" parent="Sprites"] +visible = false +texture = ExtResource("5_7s632") + +[node name="Sprite2D5" type="Sprite2D" parent="Sprites"] +visible = false +texture = ExtResource("6_du6mb") + +[node name="Sprite2D6" type="Sprite2D" parent="Sprites"] +visible = false +texture = ExtResource("7_61yp0") + +[node name="CollisionShape2D2" type="CollisionShape2D" parent="."] +shape = SubResource("CircleShape2D_kuteu") + +[connection signal="area_entered" from="." to="." method="_on_area_entered"] +[connection signal="body_entered" from="." to="." method="_on_body_entered"] diff --git a/examples/Ship2D/scenes/asteroid/asteroid_spawner.gd b/examples/Ship2D/scenes/asteroid/asteroid_spawner.gd new file mode 100644 index 0000000..364948e --- /dev/null +++ b/examples/Ship2D/scenes/asteroid/asteroid_spawner.gd @@ -0,0 +1,40 @@ +extends Node + +@export var ship: Ship +@export var asteroid_scene: PackedScene + +## Used for grouping all spawned instances for easy removal when needed +@export var spawned_objects: SpawnedObjects + +@export var asteroid_spawn_interval_min := 0.2 +@export var asteroid_spawn_interval_max := 0.5 +var _asteroid_spawn_interval: float +var _asteroid_spawn_timer: float + + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + return + reset_spawn_timer() + + +# Called every frame. 'delta' is the elapsed time since the previous frame. +func _physics_process(delta: float) -> void: + _asteroid_spawn_timer += delta + + if _asteroid_spawn_timer > _asteroid_spawn_interval: + spawn_asteroid(randi_range(1, 2)) + reset_spawn_timer() + + +func reset_spawn_timer() -> void: + _asteroid_spawn_interval = randf_range(asteroid_spawn_interval_min, asteroid_spawn_interval_max) + _asteroid_spawn_timer = 0 + + +func spawn_asteroid(count: int = 1): + for i in count: + var asteroid := asteroid_scene.instantiate() as Asteroid + spawned_objects.add_child(asteroid) + asteroid.position = Vector2(randf_range(0, 1920), -500) + asteroid.randomize_speed() diff --git a/examples/Ship2D/scenes/asteroid/asteroid_spawner.tscn b/examples/Ship2D/scenes/asteroid/asteroid_spawner.tscn new file mode 100644 index 0000000..45c8e0b --- /dev/null +++ b/examples/Ship2D/scenes/asteroid/asteroid_spawner.tscn @@ -0,0 +1,8 @@ +[gd_scene load_steps=3 format=3 uid="uid://bwx014jyttag4"] + +[ext_resource type="Script" path="res://scenes/asteroid/asteroid_spawner.gd" id="1_7njbd"] +[ext_resource type="PackedScene" uid="uid://da7uefbrja8u0" path="res://scenes/asteroid/asteroid.tscn" id="2_j1kme"] + +[node name="AsteroidSpawner" type="Node"] +script = ExtResource("1_7njbd") +asteroid_scene = ExtResource("2_j1kme") diff --git a/examples/Ship2D/scenes/ball/ball.gd b/examples/Ship2D/scenes/ball/ball.gd new file mode 100644 index 0000000..6c4db8c --- /dev/null +++ b/examples/Ship2D/scenes/ball/ball.gd @@ -0,0 +1,45 @@ +extends Area2D +class_name Ball + +## Removes this instance after n seconds, if no other code removes it before that +@export var remove_after_seconds: int = 10 + +## Tracks how long this instance exists +var seconds: float + +## Linear velocity for movement, initially set by other nodes +var linear_velocity: Vector2 + +## The ship that spawned this instance +var spawned_by: Ship + + +func _physics_process(delta: float) -> void: + process_remove_after_timeout(delta) + process_movement(delta) + + +## Moves the asteroid +func process_movement(delta): + position += linear_velocity * delta + + +## Removes the asteroid after `remove_after_seconds`, this is a safeguard +## in case the instance is not already removed by collisions or other code +func process_remove_after_timeout(delta): + seconds += delta + if seconds > remove_after_seconds: + queue_free() + + +## Handles collisions with other physics bodies (for now, only walls) +func _on_body_entered(body: Node) -> void: + queue_free() + + +## Handles collisions with other instances, and asteroids +func _on_area_entered(area: Area2D) -> void: + if area is Asteroid: + spawned_by.hit_an_asteroid() + area.destroy() + queue_free() diff --git a/examples/Ship2D/scenes/ball/ball.tscn b/examples/Ship2D/scenes/ball/ball.tscn new file mode 100644 index 0000000..5972905 --- /dev/null +++ b/examples/Ship2D/scenes/ball/ball.tscn @@ -0,0 +1,22 @@ +[gd_scene load_steps=4 format=3 uid="uid://dl6nxothvtqua"] + +[ext_resource type="Script" path="res://scenes/ball/ball.gd" id="1_ovmhm"] +[ext_resource type="Texture2D" uid="uid://bqprtmb25w6u5" path="res://assets/ball.svg" id="2_1r82m"] + +[sub_resource type="CircleShape2D" id="CircleShape2D_t02dy"] +radius = 17.0 + +[node name="Ball" type="Area2D"] +collision_layer = 16 +collision_mask = 3 +script = ExtResource("1_ovmhm") + +[node name="Sprite2D" type="Sprite2D" parent="."] +scale = Vector2(0.33, 0.33) +texture = ExtResource("2_1r82m") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +shape = SubResource("CircleShape2D_t02dy") + +[connection signal="area_entered" from="." to="." method="_on_area_entered"] +[connection signal="body_entered" from="." to="." method="_on_body_entered"] diff --git a/examples/Ship2D/scenes/game_scenes/base_game_scene.gd b/examples/Ship2D/scenes/game_scenes/base_game_scene.gd new file mode 100644 index 0000000..7e612a2 --- /dev/null +++ b/examples/Ship2D/scenes/game_scenes/base_game_scene.gd @@ -0,0 +1,21 @@ +extends Node2D +class_name BaseGameScene + +## Used to differentiate envs +static var env_id: int = 0 + +@export var ship: Ship + +## By default, this is enabled for every 2nd env +## but you can override it for testing here +@export var override_can_shoot_always_enabled: bool = false + + +func _ready() -> void: + if override_can_shoot_always_enabled: + ship.can_shoot = 1 + return + + ## Enables ship shooting capability for every 2nd env only + ship.can_shoot = bool(env_id % 2) + BaseGameScene.env_id += 1 diff --git a/examples/Ship2D/scenes/game_scenes/base_game_scene.tscn b/examples/Ship2D/scenes/game_scenes/base_game_scene.tscn new file mode 100644 index 0000000..e580e61 --- /dev/null +++ b/examples/Ship2D/scenes/game_scenes/base_game_scene.tscn @@ -0,0 +1,51 @@ +[gd_scene load_steps=8 format=3 uid="uid://bswnfo2l0387l"] + +[ext_resource type="Script" path="res://scenes/game_scenes/base_game_scene.gd" id="1_jgp3q"] +[ext_resource type="Texture2D" uid="uid://dl8pqumuikotr" path="res://assets/Background.png" id="1_yv3j2"] +[ext_resource type="PackedScene" uid="uid://giklicruf2u" path="res://scenes/ship/ship.tscn" id="2_vqieq"] +[ext_resource type="Script" path="res://scenes/game_scenes/spawned_objects.gd" id="4_o3sdh"] +[ext_resource type="PackedScene" uid="uid://bwx014jyttag4" path="res://scenes/asteroid/asteroid_spawner.tscn" id="5_8kb5v"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_rsqge"] +size = Vector2(3920, 1000) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_cjlw4"] +size = Vector2(1000, 2000) + +[node name="BaseGameScene" type="Node2D" node_paths=PackedStringArray("ship")] +script = ExtResource("1_jgp3q") +ship = NodePath("Ship") + +[node name="TextureRect" type="TextureRect" parent="."] +offset_right = 1920.0 +offset_bottom = 1080.0 +texture = ExtResource("1_yv3j2") + +[node name="Ship" parent="." node_paths=PackedStringArray("spawned_objects") instance=ExtResource("2_vqieq")] +position = Vector2(969, 999) +spawned_objects = NodePath("../SpawnedObjects") + +[node name="Boundaries" type="StaticBody2D" parent="."] + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Boundaries"] +position = Vector2(960, -500) +shape = SubResource("RectangleShape2D_rsqge") + +[node name="CollisionShape2D3" type="CollisionShape2D" parent="Boundaries"] +position = Vector2(-500, 540) +shape = SubResource("RectangleShape2D_cjlw4") + +[node name="CollisionShape2D4" type="CollisionShape2D" parent="Boundaries"] +position = Vector2(2420, 540) +shape = SubResource("RectangleShape2D_cjlw4") + +[node name="CollisionShape2D2" type="CollisionShape2D" parent="Boundaries"] +position = Vector2(960, 1580) +shape = SubResource("RectangleShape2D_rsqge") + +[node name="SpawnedObjects" type="Node2D" parent="."] +script = ExtResource("4_o3sdh") + +[node name="AsteroidSpawner" parent="." node_paths=PackedStringArray("ship", "spawned_objects") instance=ExtResource("5_8kb5v")] +ship = NodePath("../Ship") +spawned_objects = NodePath("../SpawnedObjects") diff --git a/examples/Ship2D/scenes/game_scenes/spawned_objects.gd b/examples/Ship2D/scenes/game_scenes/spawned_objects.gd new file mode 100644 index 0000000..7b0cf4e --- /dev/null +++ b/examples/Ship2D/scenes/game_scenes/spawned_objects.gd @@ -0,0 +1,9 @@ +extends Node2D +class_name SpawnedObjects + + +## Used as a container for spawned objects that need to be removed when resetting the level +## (for now, we do not remove asteroids) +func remove_all_spawned_items(): + for node in get_children(): + node.queue_free() diff --git a/examples/Ship2D/scenes/ship/ship.gd b/examples/Ship2D/scenes/ship/ship.gd new file mode 100644 index 0000000..a23c074 --- /dev/null +++ b/examples/Ship2D/scenes/ship/ship.gd @@ -0,0 +1,84 @@ +extends CharacterBody2D +class_name Ship + +@export var ball_scene: PackedScene +@export var ai_controller: ShipAIController +## Used for grouping all spawned instances for easy removal +@export var spawned_objects: SpawnedObjects +@export var ship_acceleration: float = 100000 +@export var ball_velocity: float = 2500 +@export var ball_fire_interval_seconds: float = 0.1 + +@onready var _initial_transform := global_transform + +var can_shoot: bool +var time_since_ball_spawned: float +var requested_movement: float +var requested_shoot: float + +var _time_survived: float + + +func _ready() -> void: + reposition_player() + + +func _physics_process(delta: float) -> void: + var direction: float + + _time_survived += delta + requested_movement = signf(requested_movement) + + direction = requested_movement + + velocity += global_transform.x * direction * ship_acceleration * delta + velocity = velocity.move_toward(Vector2.ZERO, ship_acceleration * 0.95 * delta) + + move_and_slide() + + time_since_ball_spawned += delta + if can_shoot and requested_shoot: + handle_shoot() + + +func handle_shoot(): + if time_since_ball_spawned > ball_fire_interval_seconds: + var ball = ball_scene.instantiate() as Ball + spawned_objects.add_child(ball) + ball.global_position = global_position + ball.linear_velocity = ((-global_transform.y.normalized()) * ball_velocity) + ball.spawned_by = self + time_since_ball_spawned = 0 + + +func game_over(final_reward := 0.0) -> void: + _time_survived = 0 + + # Uncomment if you want to remove all spawned objects on reset + # currently we keep all objects around + #spawned_objects.remove_all_spawned_items() + + ai_controller.end_episode(final_reward) + + # Uncomment if you want to reposition the player on reset + #reposition_player() + velocity = Vector2.ZERO + + +func reposition_player(): + global_transform = _initial_transform + position.x = randf_range(100, 1800) + + +## Called by an asteroid instance that hits the ship +func hit_by_asteroid() -> void: + # Check that the ship has survived for longer than n seconds + # this adds a "protection period" after getting hit by an asteroid + if _time_survived > 1.0: + game_over(-1.0) + _time_survived = 0 + + +## Called by a ball instance that has hit an asteroid +func hit_an_asteroid(): + ai_controller.reward += 0.1 diff --git a/examples/Ship2D/scenes/ship/ship.tscn b/examples/Ship2D/scenes/ship/ship.tscn new file mode 100644 index 0000000..0044e7a --- /dev/null +++ b/examples/Ship2D/scenes/ship/ship.tscn @@ -0,0 +1,52 @@ +[gd_scene load_steps=8 format=3 uid="uid://giklicruf2u"] + +[ext_resource type="Texture2D" uid="uid://c8oedbtmvqww" path="res://assets/Ship.png" id="1_dm7gt"] +[ext_resource type="Script" path="res://scenes/ship/ship.gd" id="1_hjjh5"] +[ext_resource type="PackedScene" uid="uid://dl6nxothvtqua" path="res://scenes/ball/ball.tscn" id="2_18p7p"] +[ext_resource type="Script" path="res://scenes/ship/ship_ai_controller.gd" id="3_ijd3m"] +[ext_resource type="Script" path="res://addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd" id="5_xq2sa"] + +[sub_resource type="CircleShape2D" id="CircleShape2D_450bq"] +radius = 41.72 + +[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_fpp2b"] +radius = 30.2 +height = 181.6 + +[node name="Ship" type="CharacterBody2D" node_paths=PackedStringArray("ai_controller")] +collision_layer = 512 +script = ExtResource("1_hjjh5") +ball_scene = ExtResource("2_18p7p") +ai_controller = NodePath("AIController2D") + +[node name="Sprite2D" type="Sprite2D" parent="."] +texture = ExtResource("1_dm7gt") + +[node name="AIController2D" type="Node2D" parent="." node_paths=PackedStringArray("player", "raycast_sensors")] +script = ExtResource("3_ijd3m") +player = NodePath("..") +raycast_sensors = [NodePath("RaycastSensorWall"), NodePath("RaycastSensorAsteroid")] +reset_after = 512 + +[node name="RaycastSensorWall" type="Node2D" parent="AIController2D"] +rotation = 1.5708 +script = ExtResource("5_xq2sa") +n_rays = 2.0 +ray_length = 2000 + +[node name="RaycastSensorAsteroid" type="Node2D" parent="AIController2D"] +rotation = -1.5708 +script = ExtResource("5_xq2sa") +collision_mask = 2 +collide_with_areas = true +collide_with_bodies = false +ray_length = 2000 +cone_width = 185.0 + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +shape = SubResource("CircleShape2D_450bq") + +[node name="CollisionShape2D2" type="CollisionShape2D" parent="."] +position = Vector2(0, 11.285) +rotation = 1.5708 +shape = SubResource("CapsuleShape2D_fpp2b") diff --git a/examples/Ship2D/scenes/ship/ship_ai_controller.gd b/examples/Ship2D/scenes/ship/ship_ai_controller.gd new file mode 100644 index 0000000..672e797 --- /dev/null +++ b/examples/Ship2D/scenes/ship/ship_ai_controller.gd @@ -0,0 +1,75 @@ +extends AIController2D +class_name ShipAIController + +@export var player: Ship +@export var raycast_sensors: Array[Node2D] + + +## Overriden to remove resetting after n steps +func _physics_process(_delta): + n_steps += 1 + if needs_reset: + reset() + + if heuristic == "human": + set_action() + + +func end_episode(final_reward := 0.0) -> void: + reward += final_reward + done = true + reset() + + +func reset(): + super.reset() + + +func get_raycast_obs() -> Array[float]: + var obs: Array[float] + for sensor in raycast_sensors: + obs.append_array(sensor.get_observation()) + return obs + + +func get_obs() -> Dictionary: + var obs: Array[float] + + var raycast_obs: Array[float] = get_raycast_obs() + + obs.append_array(raycast_obs) + + var velocity_x = clampf(player.get_real_velocity().x / 3000, -1.0, 1.0) + + obs.append_array([ + float(player.can_shoot), + clampf(player.time_since_ball_spawned / player.ball_fire_interval_seconds, 0, 1.0), + velocity_x + ]) + + return {"obs": obs} + + +func get_reward() -> float: + return reward + + +func get_action_space() -> Dictionary: + return { + "move": {"size": 3, "action_type": "discrete"}, + "shoot": {"size": 2, "action_type": "discrete"}, + } + + +func set_action(_action = null) -> void: + var move_dir: float + var shoot: float + if _action: + move_dir = _action.move - 1 + shoot = bool(_action.shoot) + else: + move_dir = Input.get_axis("move_left", "move_right") + shoot = Input.is_action_pressed("shoot") + + player.requested_movement = move_dir + player.requested_shoot = shoot diff --git a/examples/Ship2D/scenes/test_scene/test_scene.tscn b/examples/Ship2D/scenes/test_scene/test_scene.tscn new file mode 100644 index 0000000..edc8255 --- /dev/null +++ b/examples/Ship2D/scenes/test_scene/test_scene.tscn @@ -0,0 +1,18 @@ +[gd_scene load_steps=3 format=3 uid="uid://d35htadfm3qip"] + +[ext_resource type="Script" path="res://addons/godot_rl_agents/sync.gd" id="2_0dvr4"] +[ext_resource type="PackedScene" uid="uid://bswnfo2l0387l" path="res://scenes/game_scenes/base_game_scene.tscn" id="3_3wx3y"] + +[node name="TestScene" type="Node2D"] + +[node name="BaseGameScene" parent="." instance=ExtResource("3_3wx3y")] +override_can_shoot_always_enabled = true + +[node name="Camera2D" type="Camera2D" parent="."] +position = Vector2(960, 540) + +[node name="Sync" type="Node" parent="."] +script = ExtResource("2_0dvr4") +control_mode = 2 +action_repeat = 4 +onnx_model_path = "model.onnx" diff --git a/examples/Ship2D/scenes/train_scene/training_scene.tscn b/examples/Ship2D/scenes/train_scene/training_scene.tscn new file mode 100644 index 0000000..a392084 --- /dev/null +++ b/examples/Ship2D/scenes/train_scene/training_scene.tscn @@ -0,0 +1,43 @@ +[gd_scene load_steps=3 format=3 uid="uid://ji4spgoalcj6"] + +[ext_resource type="Script" path="res://addons/godot_rl_agents/sync.gd" id="2_vhkae"] +[ext_resource type="PackedScene" uid="uid://bswnfo2l0387l" path="res://scenes/game_scenes/base_game_scene.tscn" id="3_jqd0t"] + +[node name="TrainingScene" type="Node2D"] + +[node name="BaseGameScene" parent="." instance=ExtResource("3_jqd0t")] + +[node name="SubViewport" type="SubViewport" parent="."] + +[node name="BaseGameScene" parent="SubViewport" instance=ExtResource("3_jqd0t")] + +[node name="SubViewport2" type="SubViewport" parent="."] + +[node name="BaseGameScene" parent="SubViewport2" instance=ExtResource("3_jqd0t")] + +[node name="SubViewport3" type="SubViewport" parent="."] + +[node name="BaseGameScene" parent="SubViewport3" instance=ExtResource("3_jqd0t")] + +[node name="SubViewport4" type="SubViewport" parent="."] + +[node name="BaseGameScene" parent="SubViewport4" instance=ExtResource("3_jqd0t")] + +[node name="SubViewport5" type="SubViewport" parent="."] + +[node name="BaseGameScene" parent="SubViewport5" instance=ExtResource("3_jqd0t")] + +[node name="SubViewport6" type="SubViewport" parent="."] + +[node name="BaseGameScene" parent="SubViewport6" instance=ExtResource("3_jqd0t")] + +[node name="SubViewport7" type="SubViewport" parent="."] + +[node name="BaseGameScene" parent="SubViewport7" instance=ExtResource("3_jqd0t")] + +[node name="Camera2D" type="Camera2D" parent="."] + +[node name="Sync" type="Node" parent="."] +script = ExtResource("2_vhkae") +action_repeat = 4 +speed_up = 16.0 diff --git a/examples/Ship2D/ships2d.csproj b/examples/Ship2D/ships2d.csproj new file mode 100644 index 0000000..4a8fc76 --- /dev/null +++ b/examples/Ship2D/ships2d.csproj @@ -0,0 +1,11 @@ + + + net8.0 + net7.0 + net8.0 + true + + + + + \ No newline at end of file diff --git a/examples/Ship2D/ships2d.sln b/examples/Ship2D/ships2d.sln new file mode 100644 index 0000000..8d11a05 --- /dev/null +++ b/examples/Ship2D/ships2d.sln @@ -0,0 +1,19 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2012 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ships1v12D", "Ships1v12D.csproj", "{9B4B9FB6-68BB-4358-A6B9-30F46B2A26E5}" +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 + {9B4B9FB6-68BB-4358-A6B9-30F46B2A26E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B4B9FB6-68BB-4358-A6B9-30F46B2A26E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B4B9FB6-68BB-4358-A6B9-30F46B2A26E5}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU + {9B4B9FB6-68BB-4358-A6B9-30F46B2A26E5}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU + {9B4B9FB6-68BB-4358-A6B9-30F46B2A26E5}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU + {9B4B9FB6-68BB-4358-A6B9-30F46B2A26E5}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU + EndGlobalSection +EndGlobal From 3c1e2171cbf38f580739aa8874ef409f95deb2e6 Mon Sep 17 00:00:00 2001 From: Ivan-267 <61947090+Ivan-267@users.noreply.github.com> Date: Mon, 20 Oct 2025 22:34:43 +0200 Subject: [PATCH 5/8] Updates Ship2D (adds boss level + small refactor) --- .../controller/ai_controller_2d.gd.uid | 1 + .../controller/ai_controller_3d.gd.uid | 1 + .../godot_rl_agents/godot_rl_agents.gd.uid | 1 + .../onnx/csharp/ONNXInference.cs.uid | 1 + .../onnx/csharp/SessionConfigurator.cs.uid | 1 + .../onnx/wrapper/ONNX_wrapper.gd.uid | 1 + .../sensors_2d/ExampleRaycastSensor2D.tscn | 2 +- .../sensors/sensors_2d/GridSensor2D.gd.uid | 1 + .../sensors/sensors_2d/ISensor2D.gd.uid | 1 + .../sensors/sensors_2d/RaycastSensor2D.gd.uid | 1 + .../sensors/sensors_2d/RaycastSensor2D.tscn | 2 +- .../sensors/sensors_3d/GridSensor3D.gd.uid | 1 + .../sensors/sensors_3d/ISensor3D.gd.uid | 1 + .../sensors_3d/RGBCameraSensor3D.gd.uid | 1 + .../sensors/sensors_3d/RGBCameraSensor3D.tscn | 2 +- .../sensors/sensors_3d/RaycastSensor3D.gd.uid | 1 + .../sensors/sensors_3d/RaycastSensor3D.tscn | 2 +- .../Ship2D/addons/godot_rl_agents/sync.gd.uid | 1 + examples/Ship2D/assets/ShipEnemy.png | Bin 0 -> 12969 bytes examples/Ship2D/assets/ball_green.svg | 72 +++++++++ examples/Ship2D/assets/ball_red.svg | 72 +++++++++ examples/Ship2D/project.godot | 10 +- examples/Ship2D/scenes/asteroid/asteroid.gd | 13 +- .../Ship2D/scenes/asteroid/asteroid.gd.uid | 1 + examples/Ship2D/scenes/asteroid/asteroid.tscn | 18 +-- .../scenes/asteroid/asteroid_spawner.gd | 2 +- .../scenes/asteroid/asteroid_spawner.gd.uid | 1 + .../scenes/asteroid/asteroid_spawner.tscn | 2 +- .../asteroid_game_scene.gd | 21 +++ .../asteroid_game_scene.gd.uid | 1 + .../asteroid_game_scene.tscn | 80 ++++++++++ .../boundaries_asteroid_sensor.gd | 3 + .../boundaries_asteroid_sensor.gd.uid | 1 + .../boss_game_scene/boss_game_scene.gd | 21 +++ .../boss_game_scene/boss_game_scene.gd.uid | 1 + .../boss_game_scene/boss_game_scene.tscn | 80 ++++++++++ .../boundaries_enemy_ship_sensor.gd | 3 + .../boundaries_enemy_ship_sensor.gd.uid | 1 + .../scenes/game_scenes/spawned_objects.gd.uid | 1 + examples/Ship2D/scenes/projectiles/ball.gd | 48 ++++++ .../Ship2D/scenes/projectiles/ball.gd.uid | 1 + .../scenes/projectiles/projectile_enemy.tscn | 22 +++ .../scenes/projectiles/projectile_player.tscn | 22 +++ examples/Ship2D/scenes/ship/ball_sensor.gd | 36 +++++ .../Ship2D/scenes/ship/ball_sensor.gd.uid | 1 + examples/Ship2D/scenes/ship/ship.gd | 40 +++-- examples/Ship2D/scenes/ship/ship.gd.uid | 1 + examples/Ship2D/scenes/ship/ship.tscn | 36 ++++- .../Ship2D/scenes/ship/ship_ai_controller.gd | 14 +- .../scenes/ship/ship_ai_controller.gd.uid | 1 + .../Ship2D/scenes/ship_enemy_boss/boss.gd | 95 ++++++++++++ .../Ship2D/scenes/ship_enemy_boss/boss.gd.uid | 1 + .../Ship2D/scenes/ship_enemy_boss/boss.tscn | 146 ++++++++++++++++++ .../scenes/ship_enemy_boss/health_bar.gd | 22 +++ .../scenes/ship_enemy_boss/health_bar.gd.uid | 1 + .../scenes/test_scene/test_boss_scene.tscn | 17 ++ .../Ship2D/scenes/test_scene/test_scene.tscn | 4 +- .../train_scene/training_scene_asteroids.tscn | 45 ++++++ .../train_scene/training_scene_boss.tscn | 53 +++++++ .../train_scene/training_scene_mixed.tscn | 53 +++++++ examples/Ship2D/ships2d.csproj | 4 +- 61 files changed, 1033 insertions(+), 56 deletions(-) create mode 100644 examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_2d.gd.uid create mode 100644 examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_3d.gd.uid create mode 100644 examples/Ship2D/addons/godot_rl_agents/godot_rl_agents.gd.uid create mode 100644 examples/Ship2D/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs.uid create mode 100644 examples/Ship2D/addons/godot_rl_agents/onnx/csharp/SessionConfigurator.cs.uid create mode 100644 examples/Ship2D/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd.uid create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/GridSensor2D.gd.uid create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/ISensor2D.gd.uid create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd.uid create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/GridSensor3D.gd.uid create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/ISensor3D.gd.uid create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd.uid create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.gd.uid create mode 100644 examples/Ship2D/addons/godot_rl_agents/sync.gd.uid create mode 100644 examples/Ship2D/assets/ShipEnemy.png create mode 100644 examples/Ship2D/assets/ball_green.svg create mode 100644 examples/Ship2D/assets/ball_red.svg create mode 100644 examples/Ship2D/scenes/asteroid/asteroid.gd.uid create mode 100644 examples/Ship2D/scenes/asteroid/asteroid_spawner.gd.uid create mode 100644 examples/Ship2D/scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.gd create mode 100644 examples/Ship2D/scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.gd.uid create mode 100644 examples/Ship2D/scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.tscn create mode 100644 examples/Ship2D/scenes/game_scenes/asteroid_game_scene/boundaries_asteroid_sensor.gd create mode 100644 examples/Ship2D/scenes/game_scenes/asteroid_game_scene/boundaries_asteroid_sensor.gd.uid create mode 100644 examples/Ship2D/scenes/game_scenes/boss_game_scene/boss_game_scene.gd create mode 100644 examples/Ship2D/scenes/game_scenes/boss_game_scene/boss_game_scene.gd.uid create mode 100644 examples/Ship2D/scenes/game_scenes/boss_game_scene/boss_game_scene.tscn create mode 100644 examples/Ship2D/scenes/game_scenes/boss_game_scene/boundaries_enemy_ship_sensor.gd create mode 100644 examples/Ship2D/scenes/game_scenes/boss_game_scene/boundaries_enemy_ship_sensor.gd.uid create mode 100644 examples/Ship2D/scenes/game_scenes/spawned_objects.gd.uid create mode 100644 examples/Ship2D/scenes/projectiles/ball.gd create mode 100644 examples/Ship2D/scenes/projectiles/ball.gd.uid create mode 100644 examples/Ship2D/scenes/projectiles/projectile_enemy.tscn create mode 100644 examples/Ship2D/scenes/projectiles/projectile_player.tscn create mode 100644 examples/Ship2D/scenes/ship/ball_sensor.gd create mode 100644 examples/Ship2D/scenes/ship/ball_sensor.gd.uid create mode 100644 examples/Ship2D/scenes/ship/ship.gd.uid create mode 100644 examples/Ship2D/scenes/ship/ship_ai_controller.gd.uid create mode 100644 examples/Ship2D/scenes/ship_enemy_boss/boss.gd create mode 100644 examples/Ship2D/scenes/ship_enemy_boss/boss.gd.uid create mode 100644 examples/Ship2D/scenes/ship_enemy_boss/boss.tscn create mode 100644 examples/Ship2D/scenes/ship_enemy_boss/health_bar.gd create mode 100644 examples/Ship2D/scenes/ship_enemy_boss/health_bar.gd.uid create mode 100644 examples/Ship2D/scenes/test_scene/test_boss_scene.tscn create mode 100644 examples/Ship2D/scenes/train_scene/training_scene_asteroids.tscn create mode 100644 examples/Ship2D/scenes/train_scene/training_scene_boss.tscn create mode 100644 examples/Ship2D/scenes/train_scene/training_scene_mixed.tscn diff --git a/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_2d.gd.uid b/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_2d.gd.uid new file mode 100644 index 0000000..4f457c9 --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_2d.gd.uid @@ -0,0 +1 @@ +uid://c121dwd5k2m4i diff --git a/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_3d.gd.uid b/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_3d.gd.uid new file mode 100644 index 0000000..f5b0c44 --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_3d.gd.uid @@ -0,0 +1 @@ +uid://cop2ay1taoscq diff --git a/examples/Ship2D/addons/godot_rl_agents/godot_rl_agents.gd.uid b/examples/Ship2D/addons/godot_rl_agents/godot_rl_agents.gd.uid new file mode 100644 index 0000000..50466f7 --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/godot_rl_agents.gd.uid @@ -0,0 +1 @@ +uid://b18rfkoomxxad diff --git a/examples/Ship2D/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs.uid b/examples/Ship2D/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs.uid new file mode 100644 index 0000000..c871364 --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs.uid @@ -0,0 +1 @@ +uid://c0n546jt6p4wg diff --git a/examples/Ship2D/addons/godot_rl_agents/onnx/csharp/SessionConfigurator.cs.uid b/examples/Ship2D/addons/godot_rl_agents/onnx/csharp/SessionConfigurator.cs.uid new file mode 100644 index 0000000..5749449 --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/onnx/csharp/SessionConfigurator.cs.uid @@ -0,0 +1 @@ +uid://c5p5v4ds4vq6i diff --git a/examples/Ship2D/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd.uid b/examples/Ship2D/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd.uid new file mode 100644 index 0000000..5117eee --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd.uid @@ -0,0 +1 @@ +uid://dl3hfghbv75lh diff --git a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/ExampleRaycastSensor2D.tscn b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/ExampleRaycastSensor2D.tscn index 5edb6c7..a203360 100644 --- a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/ExampleRaycastSensor2D.tscn +++ b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/ExampleRaycastSensor2D.tscn @@ -1,6 +1,6 @@ [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"] +[ext_resource type="Script" uid="uid://dbdksstuow56x" path="res://addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd" id="1"] [sub_resource type="GDScript" id="2"] script/source = "extends Node2D diff --git a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/GridSensor2D.gd.uid b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/GridSensor2D.gd.uid new file mode 100644 index 0000000..de00b51 --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/GridSensor2D.gd.uid @@ -0,0 +1 @@ +uid://de4ygcq4turad diff --git a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/ISensor2D.gd.uid b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/ISensor2D.gd.uid new file mode 100644 index 0000000..ba6d861 --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/ISensor2D.gd.uid @@ -0,0 +1 @@ +uid://g1hq3jnyoum7 diff --git a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd.uid b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd.uid new file mode 100644 index 0000000..5f9e6a8 --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd.uid @@ -0,0 +1 @@ +uid://dbdksstuow56x diff --git a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.tscn b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.tscn index 5ca402c..a485ee1 100644 --- a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.tscn +++ b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.tscn @@ -1,6 +1,6 @@ [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"] +[ext_resource type="Script" uid="uid://dbdksstuow56x" path="res://addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd" id="1"] [node name="RaycastSensor2D" type="Node2D"] script = ExtResource("1") diff --git a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/GridSensor3D.gd.uid b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/GridSensor3D.gd.uid new file mode 100644 index 0000000..2faea95 --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/GridSensor3D.gd.uid @@ -0,0 +1 @@ +uid://pe8q1u14ylyc diff --git a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/ISensor3D.gd.uid b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/ISensor3D.gd.uid new file mode 100644 index 0000000..823e83a --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/ISensor3D.gd.uid @@ -0,0 +1 @@ +uid://l4ly8hep0va0 diff --git a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd.uid b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd.uid new file mode 100644 index 0000000..65eaa89 --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd.uid @@ -0,0 +1 @@ +uid://bvk2trprdg2q5 diff --git a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.tscn b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.tscn index d58649c..4eef45a 100644 --- a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.tscn +++ b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.tscn @@ -1,6 +1,6 @@ [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"] +[ext_resource type="Script" uid="uid://bvk2trprdg2q5" path="res://addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd" id="1"] [sub_resource type="ViewportTexture" id="ViewportTexture_y72s3"] viewport_path = NodePath("SubViewport") diff --git a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.gd.uid b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.gd.uid new file mode 100644 index 0000000..a7a0139 --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.gd.uid @@ -0,0 +1 @@ +uid://cqnmnaoemdk8f diff --git a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.tscn b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.tscn index 35f9796..be02b34 100644 --- a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.tscn +++ b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.tscn @@ -1,6 +1,6 @@ [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"] +[ext_resource type="Script" uid="uid://cqnmnaoemdk8f" path="res://addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.gd" id="1"] [node name="RaycastSensor3D" type="Node3D"] script = ExtResource("1") diff --git a/examples/Ship2D/addons/godot_rl_agents/sync.gd.uid b/examples/Ship2D/addons/godot_rl_agents/sync.gd.uid new file mode 100644 index 0000000..f662c1d --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/sync.gd.uid @@ -0,0 +1 @@ +uid://dwyruq0ulo56c diff --git a/examples/Ship2D/assets/ShipEnemy.png b/examples/Ship2D/assets/ShipEnemy.png new file mode 100644 index 0000000000000000000000000000000000000000..e44d9b3ab611fd23a4ec8a1f4a4ffc7d796d5795 GIT binary patch literal 12969 zcmV;aGFHurP)EX>4Tx04R}tl08VnKp2MKrb?w$9qb_DkfDl$T~x%eRIvyaN?V~-2a`+xph-iL z;^HW{799LptU9+0Yt2!cN#?t+t|iZqU!3sG7%QcR?2Kknflar`NA$>gel zkz)ZhsE`~#_#6E0ty!F&a+5-Fp!dbLKgNK-F3@V&_V=-Ew@v{6GjOGK{Iw=9_epxA zqeYH@!ENB;x}(W^z~v4w{G>~UBE#Ld)?#SecgTg_e{HgKfeKT#rmM*lmGxA07*naRCwC$y?Kxv*L~;t`Mu0K`##WU zTqJk^5D)PdC6Tr)jj4kKx+QT8d*pLhj@8()9(gSvJ2Ac#wnO7X6Ek7$$gvuYJswYt zZDB%!0D=t5vP7+{n}#>U33vDUs7xKl*-h{#S=-3lau9v~krbHGX91>jk0?bjZD_~Bz4 zcW`}+Y}|tNDSP+sy+=fT3U~mx{xVK=T2&u0#{9R#hYx>y~RDP$>MGhBk>`+}mh@4Uk{avTxtM4+8%YxVqhbt|6ar%tFeMoNJ`$8Yv0{jv)vv4uazcHdP5U z>sh)ytNmV}-7Eb33nKEXhYuhA(nd>cfPCG{efQnhmrkcY1^isAeUgsM^(4*tu7sIN z7>WWaMif;w;homndv7(QX|*Z};#pF8&yM6Oc6%kzzBZThS!2w{4jnp_+h~~$kgpee z-wJ#lxVDLuuw4l`H;^(XvI$uf5i6=HB31-NRipj1iRPC0B&vw28Wj;$HASDEOU2;& zyl1yGYbOzTNmc*lLk~Ul)J6+!fc&z`*x1;IRQ0n!wsBim!kiz;n&;DwqW z!6Kp|*wHG$)rNRh6~t75u8OFLpeli4tYQ=~s%i=WJs0x+O(kDPnp|E%Re$lwkt2V% z(NY^AUn_%u4ESW@eR722mTvdSzLdEjU>#8_s@52>MzBk49sxY0?Bz@j5~02dbX7H0 zG1jV45fi8w1v66&wp}dRTLY`93Q$YV!&t<`SOXw zhY$buM$0WP&c-ch3h>_nKHiAPp^WqV)xGZHDMKmY;5kNo*WkM%Ubx10jrfkJ@5JkB z9f2EtcLd*v1db8k2tl}rHE7@%u?}jD!5R^31QkIY!McLipLWg$7K)x7Mx>UcesuTl z-5fi1>}wk>xB>E&0RAh$pVihG5j)!LKDM>nIhruyyMpHke!_^CFyUv{NWAu4!B4uz zbI|C!`fdpBAYQ89qo+Z_F@Zs?2@TdNz@x>Ah#F91M0-+ZN{lJwE6a@1ckbD<=iISl z$DY_|$ql*EwCvrx_q`%=q_*>mh~1i%Z*J~#&KMzZBldL#UkF*)5h8#Rmq0}RxPl$^b^yyKBa9x1=k+t=NIIFw4IPc#C&KV^-LZWr_3p!v&8=mjT)wziDm@uga^K9j6ns_3iZ~|p zs<>jXf#MoOeT((2;tC)FR@u>Qo-2B`Z=s^K!K%Am*Zpna_coY(LxdLDf>I zc=Hnr^Iz&U#czd-9m*2ViW?|yVAb^%H_+<$gpas>pb1YU`TUIco|3PF4f_1fBS(%r zv*BHNgO)q*yfe|;+j|(6{LV(CvU|Y&YRZ+O8+uTDQ{x(p+=QMh8r6yK8sk^2$)+=d zAKu*Cdso6Sjxh!!f@1_D(Gt}e5hVOAj0jOoh=k*$`!d;^7Cad=w*0IyCeVnmWvR1B z%ufmu21*e`=yK)4Oi8cPrFL~h7V|qM<0DuH$eUdEO*>-$FI?laJ9AD`R+hk zCS4+#TW^w!a7{3hR+EE0TI{W2Z&v)*sV4H(vI`z z&rcmYcI-Q}Pz&wVBcf@>amM(U+bRrjXT>>BQ`lOQ5e&(Jl2$xvP_DF;9y#xc6EqsM*hr8)oIx@2bz8e@J1 z_{0X2zd_0eKls5R&+}dey5hRNq?{Y=a%L16wAK!bkP;C@V=b)Jnqr#Q)hHaGZv44Zf%(6q7jSR$n$ckP~zLCC!*h@>TTCpm#ca=*ZL)7YXU@)6zQzljSyHuYJ(RHcTJT25%WvDl0 z8V6krS!@5|gCG3h&>OA2-!M%6zylBLb{yw@z`Ip-Z|(oylMn_o4o1L86oNDo$K5eT zP-{u}6}p2G-F}g-pv=gPchKA08yc212Bi@}jETK9NaQ_PrpZ-NLV2R9p(a;FBaI$J z(q(RThT?PIBk!bGbkgMAGzBL`(Ims)5ec6FHe_|HU==QvEQ=Le-{2w^cuGXR3jE{3 z!os7k_d(LHTafSHzuzeo3b#3qGY0Ha)jMNgHQsRTjom#|_85CSn+F%%_1&drc>Gb~Dyikk}M3sClziG0uq^2S?L zRMl@7V;-{B{;$_#D!}Us+sj+*{7a1c0f`0(L>-f$tl!oUuK-~+(^APDXg zkrjcRwjRA+o(st(dn_T!W}C;svLqy^cVpD>K^S!e-z3O%_eQ!S)a~6@T?;4|CMWsW zum3uQVv#@i!$0D#yY8ZI*A2{{cp*H4hQDP|`mTQysO8L=Gko;l{0hG3@%z8`d)#>A z4Rl|99SdhpR$VFS-hT3K5@UU&6+hJhH~icu9tpL0H9m%0b1#!oj}w{AOeK5U-L|WM zj{+YZA0I!as(){-{oHFUP2e>H^8NewXNtw*gCg>wAPC;w31biVd9KY*a!v69+eFWPImF(q1li%Q;*%!#U8BS(4^HO#*XVb&@Vp=`@b_2g_ zjQQQMv9V`GgAivD={`bHCo*)SRw^FI}lOoc$f(~JOd4}tX7r3@C z$$*!y@?F_U)Ui0gpj09tXdT#V*Hwm(eCCLtZX)D<8+u+)r93}NFf_#Nx82IS-uYwX za*N#mzV~4RpW@6kw(8(sdO{Rurpfg6arax^!n@x2PKw0>KmIS?MPMz(`Pr~zML@TJ zx(TRA)m0gH%J6!rTZbJp{M?VnBugNlO!ea#m!$RS^NZY?JH@TJQ+VQVDzk}`+0DF^ z9i?bm^2kl9`Uj5Ve0*$d?4N+Y%H?ujc(ruwPX_4BWC3u(y_H*;AZg zcm6!xeyQ%s9sh$bW=Hw_@LhFM$01bw9AZ_%&DX;Phi$Uzn_3OKtyC z!qFb*k^>y?+RD-HZ4}Ik56*eu?}5L0wdwa)4aoQJ-|v*m<-O75neU-p@>mS)!g+QV zCg}D{%Rt?@{_?<0Jl1nf^%Q-0>@-CZ*gBv)q9h`~I}xa34~vNm(sJ)6`mVn@%rK0D zJ;sp8WEtFZGku#!F^)rFexA7(o~E)eSA7c9{tqgGlg=`5!!2|T4TCBRmo71L^aqp| z=0ngEMnD6r%%6CU{9eq@Wc>A0Iz7c-NSIoW|${Y{rTH-r+Dw|GxZ>E zTt^=QF`UY5;%Ig&FJ(umm=y)UQzG(dRsA1_4<9bP;X!`?{r7KjUH2D(e+}$xx9^a* z$c>94odOwmRhzSEZzF!UL$C}(mOrAUO9Lf2M1hKEY2HdebG4`wk zgdmUEd|H!C)add-KhG~se6=Zyw*$Nr_!62?1ScI7Wl7@dvFM;0#?rJATc9y5PF?WVtg$Y74Z*{<~-9DA*sbd@m#h?TTP6X;0>g(x8_ZuvkJtp#|PcooSM!A8uz z1n4M06tjD@)B@f_|94R1eYue6WoPA5eI&L4)Y1mwSd2i!L{Huj8&{K>+?!rwjm=%fBCfjj3F0(bJ-Yp?y8APD{z_+?;gv;9)m z&NECD+Nz8?pNbZNVfp`c-)s4Cg~96KtrM0sILOEcNmVjI!D?)8i8Dok=) z?j*f_k@-XyMW?B;Is!bHN~M1OO>cS=TefU@>eQ)IR}}1Wg~8ojE|-51_zhLv-fF+i zm3eN-z03{yvm|V=>}ED&?Pd+pip{^&cLU$-xvu(qSptTQM*>R%4ZSMSXQ!sFs>vfg z9vh#Pt^u+;t|uM^RLyy*=CH6NRjAKUtdlD>>>axpZN%Ujh4VG)0M;snb0GW1|rtugn!s+xdPxf5H$?WEK z`s9p={D)L3^_c?)4isNAa^?Q}@9%Y8_t#6M(no>*ra!EfJ^6FomOI60d2R`)EhNdR zL0_|dCQ(1Py(8eOgjyRL&%7D!zf`A$1Cb-%jgAeO(X)yJsDuF?6Fm)V%@ZbwfC1*w30u9+bQ0kc5CuV60Jt`5vYrp$7V3&p{?wp&gcF6cz>SRlU zWRWJ41+9w}SL=-`JBw3XU7TV%(araJuHpHf9nGZ74pse6rBdnNj*pN3aVnMiivtG^ zR9>}mW&i&D$?LDb{uhlgp9l6rO$4H9Nc!B8dzpPxk8@*w$x9LgqgH-&Qg0gMO3bRY z+rQeoyUx$sCARv|j@60OiqI&)61P~mTFUbF5@}3#e<(|8qQM zf86x;_Wp$L`v-xak1n{bWNn4J=3nB0smIw>m?WisRX5iT66h4%b@dHeyb<*O(02nt zO}M&Sg3u^rX{3BrSj3~}Yg$XICTaNDchg8$UzOD~_Q=!#yqWe6Vpgt6qm4Cq%+%H@ ze$9UI+E?%v7r?7ThGqopSbJC9hxG|sUi3MBQWR^N+=}6yxfho|{iI%5trpacnms;O zLIbv!E^%w_6kS1ui^)E`xw`6YNz~|a6Mc88dOfUj zBW>c z!}5GWt?S(ZQJ#kwYy{?5i>rn~88@7)3P%LrDq@vD!bf4CU?lPanWX{EsExF~)-Yb} zK;{VGEcwf;BYbM~c@TZ}HMAK54Gp^(+_6mYVtrl%{8~111Vrnd)CH5EF9@Q5RK3e^ zDR9-IzLqAN9Mh=R>%WB3^{$1NxGi^zCws5qdwshaqp1S?q~kdI#>U3}Ae~Ns^1y)u z!7CwGWZ%AhKW~gV1iYj9hPOli3 zN4u}DyKCJNG@*>hBQ7;>u#zQ7qwyj_U>r2l6S@ppT4EPs9HG=mlZP%q>J3N*zpBB* zjHzITEvYRidbgz5B~b0jCb+_ZD9!YrXn`)~)19Lw)16 zWzwL@{>|zfl&$3%Zdg1^#kfo*`xgYyPwYNk%oiL{*fs!N$^IIar)*l!p;W~&&wJz4+wf=EaGs?e7V7KZc zul1M58a?*Ps45lDBQbj+lqoipSd}P0#f#Q~NHrSkFU4xxmfx8A8l!%$BR$=yy<6(> z6*PILbuVQ{d35N_mM{yN*d`DKP>CkAEPx-|m5@tkFo;GYL?;JKcw zn>{?HMC7Lq9Xj;YHG_O?Z0ru;bHH_t2kl-w%R6SCB@SKyP(?b|Xb8PW*Z1oq|;^(R^&2PT0AbG1#^vTy6*H z(3q|L9g8sKbTQ?2F=H~!IN>tuWSEr{Ig_Fyuf7R@MU@_{&}}OW*&>5Mk%6GdfGsi- z6d3h$q+76bC;sep0qP0>rUj_mb)08=uIB$6x*5Bim_D~<%a))0%x6C1wFUXu*w}9a ze^lo=LAiVOD0j>ruZuXcN*3>^BdqsRVHHxSoxi?}L(ZhgIVlz-Nm*P<;*ggF72{GA zmog4TlL%dxn7TZTqPW-^>ociAH8>Xg?z8HnKw5oL8j#X}ta>D^Pl|wy`gGe$71-TD zxz3rfCOy60pl;S_I_yjxcLv~mYLI^zxsyVJQL3tcch_Ba{mjQd{&CwBK$GRLT#0tONHqT=f?kjELacKkuD=^?%rN>$T zycYIt^&4wFpDN_tG!Kowl}ioNYKX|69XfRAx0VHi#>dC+Rn-I3OahwqOT2&L+iWh+ zwPnOL0s1mod@b^*(}-)UAXQ_oO{Y2T%g#ab1y;!2UPXXqrB1eDeY2p?Dt}|eZL-GW zqCW}ibKT;3&Si!h8U5Zjz3ELqIC}Kx^TdGuzz06iud07r8yrdn6~-pM#b9M&RX^VD zX1BYs(&k32_Bd&CTw`Zw1^SK+`!;3`SxWUL)v$SPOrI@C#$3TwNksi zg6-W5e`)KnZ}L0zdy9>-1l+T_?{-P`%fNZp$%y&wp7xxj~j zbUc>PQH$>+$hld1{2%(18LvqB`|g|g$4odUOIiZ@+<=S|=93fx!)%b?Wv7=<^uC>6 z%{|7Ql?xr6chY3}RM%}h=MFC0_MOCL4y1Q+qd&vH$$y{CwzGTvcw#eu-g_55f-6d& z@#u2HRD@yxv%X8tr1+bjTe#qMvw!iqSIvFaD>z0!5C@Bp)S#N9uBr*wb$`m)y?gh+ z2X@9FR}s!|d%Ov<>Ma98En}6%;g5RXPDxxgxn*waE^9|(EsV!`Qxc@-kmkwcDA!eHnu0{zfJawgUR@(& zuhc<0IX35JIng!B(ViV)T5e0*Kv31|Twpk&_Lg&2s|XY6K_=3Jd}Ht?MoTkXUzp&! z`~-b9EYTK-Z4A%0+gbov+ZeO8;FA^VWt=f9ui%ub2R{Q2{6^S zN~kJ9G(b82?nk#Pp*9>8-6{wK?Dl3zt4~o9oGd2UWx{c?o*y70cz!_9^O-CeE(Yo9 z|EEm=*$TYc2E2`iUdJ6_h4Zxn`o&})C$pP5k=e|}^uUTzsL#uDeeMj84eY69^C?$V zj{@75qC=MVPJEY=o8m5wtc#%zBt|fq5 z0=scu%!WrqGoZF~YiZ5Y35~9(==dAU7rDE1j_;$w^k-v zMmgjEH?1B&n}3%7+I=hM{S22D0)||VE;pPXrVt3zUWU94V|;HbpX0{LR9g>4tH@|G z4@xKO->M_fK}RT?1gBF&oXm`HGP{{YH?ty5C#}!U!X&%qPqU>s&8hA!jsEguGCn^3 z>#F+6xN=i@j`yGadNn6nfRdBqRQDE6c5UHodZ;PGH!EF!iJhgXa4AmFZOgAnf>y^N zdzPk~c(`j9|B$_|*}NbCYz-FqiTv~2Ub)cG1fvC9{-FO}&N|^7jNQhk+p(mK#Z$wA zZ&a{$y_3cb#hHH2K zx3X{FzHK7%5;ZBcKXUOYZp@u->b0mi30}@_=0x{aPG>eXn?Ivv8T1y}R+(jc=@L6C zvkX;oOA~nV3V^yzDgi*jBzZcunF*(xX}1ei7zzs9RGH#tZ>k!JYy>n-qK*(IWqmID zCcc)ws_7dXT(!Kje43xkzd&03Wly43Gs`OB0PSa!o5Em_+eHDFUR&mRf0jE+=Seh- zz+d-45o^dPHPI7eZ4LhCbr?i&nO7TUW10x?Xchd7hn#LMYTTuSw=%7v7*6}FaU z*jAZiYh{itl{va><+1_19y0*I>L(U$Wk)PZf}@F{FqK!*q||47kmKgc6kY1Iv`0Ff zPFY)QTZz5tfNj^>5fou6(Zl&|4oZxj`vyHV<2F}{SeD!rr_;ln&TQgz zc7#PIy=s4Ge^6vgWsWW7Ikr{i*z7H=D05)#1D)G}x)WIf?LS|RKe#EYw=+)HkiiI@ z&5_a$^gf2^L^l(OUM7?_AaBFuZGq3qGt#!J zFPa3CZVwj|JY0K1`Dhq5a&#=8b!;a!Ksi3m*n9vsWc_+;y zqjz&Dwajn+SBDQD{&35-7#|=1Q&s)Vx{E`(bLJ?w&%IckEQk((zLK8KyJ;p;158Gb zx5-q0yEl%liXr-gBK^^Du)b&+h?f4KNXB}rk|eT%ENLb+$emy@s8=It0}naJ;Y21rxU)A>}N97Uws_r zI`EgSwj?#6KPb{=D|7{AdTcoyQl?Aadu)ZQt&q}yOk^Ogmc3=I0Pld+OX5-yhk_(n zG)W33$)ZWJXj0Yh`Dj^mQskU;-AilNtGH?z@^XxLIY!EJZ1Lt9DbM3te_e;>5ji^^ z@889f{kxVKv8s9~lgaFFoOGyV8;p;S|0`AfOCZzu$hm3ZW$vDRfkgCn;7$e$UyhMj zHVLMZp$@#5=w&L|%XFfL;uS6GVLrD@;#GxYL_)#zVT#{G*bx%eugF%5!l?66=IW~DYhARsU z_<5w^r2>~@%(gnmM0${~4d23CvZ>7dY%Z7k1i{fAzRTKa+#1Ad+X zzd(Pq9`7O1S zAMa7sPXWKsZ2OktCEhZ7lx?L;?OA#qK)sTISi3!MQY^R`7MwKmZkB~ee=fKg=9~;U zC%ta7P`y@)qn4haM4v6u=NIV-O7sRrdi^5ZL5aSgNOw?P&dMFQDK7`~t$^=~!!x}* zd9wd{O3mLtblh6|p(96*Jl@tm==27Sjg9>n@c+JHt1&9&|ZkQ!I(-7%QS-);3^@IkvYDue)6WIY7>ygj^ zrzX>L&2Gk(qN$hZhU~b$W}7dDV?8@~ynk2Ae*+(;#a!pETCUBX z=eD_*>T+S$3)HK5_4rDFZuy>^mE44~_M5!oP%~|RjIIFw%k{5!Dpl43?u)5Dp6FIf4oX$q$y6%!sRIWN zEM8V%YCRide0=@6)0Dm8Mxl;y%siK z6To$CL83LW`ekN3HU)KTC5pp|>?p^2c5o&=yiz9ODOLS+CX@NhfddET*W*535fXaO zd*0KPPN#oHRsT=mmUjCltj{%t3tU^6Jylj2Gj&8=hlV&14$m#Sj zFK0KmdTCX&^1O(A&KUFG4jw#s>XpRX4?TRhckkXCj4|WD`+>Kv#PD&+*a}+<(`+fv zGFqNxq%vO(lB|b;+36q%S>w&tb?5+>!{i+qq$13?-AtwjIG-Bed}f#hH@lkVeFFHR zh#b6Rs) zKOQYNul9Pgeu?4A0-L-AhRgE|R~G2^@^#_!wi0C(L)xyyja-#J*VP%MIXA;pqL)j_ zUM{8jm`e7tkmy>=T!trA^~=CNsp^-G962)kYQxfNCJk?Ve0;O2)`EWLTF&EYz)*RC zp(qc!-^(-LEz;{3>T*I_>hN|qa~<#IYUdMd#^e@ZF%hO9&nLRVp=_yMrjxyt*Pe@i zNkqP?s$X^-=gS8V9(?K5j-A(Ru4$A~a4+yS5xGZI-%OimYbSE2#|!hP`zs;nyQ37; zo}ffFC|8kpr5U|b23LfFlOpG4$U7+(-3&Q58|K(2x>#_tYZ_qOP+kJQ1$-+Af^Xh^ z_uWs|zI5ufP+m6))%*AF?=F|iZ&B5IfVTm610(BoexsJIpiGxvq}wl&2`Z$lN7{O% z8a^jA%o>C+o6x9j3)*-V1B*~LE)|oYY!Xp=Z-R;wey%uiYH$eXf|FX+JSH8K9PlLY zEfM*q<2c{`!WX`9@wJYP*Y&7@2OoTJi|_lls_Lz(dYg#c3hcfd#=o4Bsu?0zsY%WK zA(d0WQ_=FY<2c_xc<|r}_+ekx2FTZg@iV1T>1GkRQB`*XyCUH40*7WlR$bZOXY;0^;M7COMw;N*yRdrBR2Sj8L z7y#n$86ckVCSB7xdLyZ1fjGm?Bh29dD2Yf>Rp)>?U{*wCfmv(qtTARzM5clB$z<}} fdS&Uo-pl_FAnc`|6`Ip^00000NkvXXu0mjffYqJI literal 0 HcmV?d00001 diff --git a/examples/Ship2D/assets/ball_green.svg b/examples/Ship2D/assets/ball_green.svg new file mode 100644 index 0000000..a16e335 --- /dev/null +++ b/examples/Ship2D/assets/ball_green.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + diff --git a/examples/Ship2D/assets/ball_red.svg b/examples/Ship2D/assets/ball_red.svg new file mode 100644 index 0000000..26dcb16 --- /dev/null +++ b/examples/Ship2D/assets/ball_red.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + diff --git a/examples/Ship2D/project.godot b/examples/Ship2D/project.godot index f34838b..ee89e2e 100644 --- a/examples/Ship2D/project.godot +++ b/examples/Ship2D/project.godot @@ -11,8 +11,8 @@ config_version=5 [application] config/name="Ships2D" -run/main_scene="res://scenes/train_scene/training_scene.tscn" -config/features=PackedStringArray("4.3", "C#", "Forward Plus") +run/main_scene="uid://2uwyckkppxmy" +config/features=PackedStringArray("4.5", "C#", "Forward Plus") config/icon="res://icon.svg" [display] @@ -55,10 +55,12 @@ move_right={ 2d_physics/layer_1="Wall" 2d_physics/layer_2="Asteroid" -2d_physics/layer_3="EnemyShip" +2d_physics/layer_5="ProjectilePlayer" +2d_physics/layer_6="ProjectileEnemy" 2d_physics/layer_10="Ship" +2d_physics/layer_11="EnemyShipBoss" [physics] -common/max_physics_steps_per_frame=16 +common/max_physics_steps_per_frame=100 2d/default_gravity=0.0 diff --git a/examples/Ship2D/scenes/asteroid/asteroid.gd b/examples/Ship2D/scenes/asteroid/asteroid.gd index 21eadab..9851641 100644 --- a/examples/Ship2D/scenes/asteroid/asteroid.gd +++ b/examples/Ship2D/scenes/asteroid/asteroid.gd @@ -52,10 +52,18 @@ func randomize_speed(): _sideways_speed = randf_range(-0.1, 0.1) * _downward_speed +## Called when an asteroid is hit +func hit(): + destroy() + + ## Destroys the asteroid, leaving a particle effect var destroying: bool + + func destroy(): - if destroying: return + if destroying: + return particles.emitting = true _active_sprite.visible = false set_deferred("process_mode", PROCESS_MODE_DISABLED) @@ -77,4 +85,5 @@ func _on_body_entered(body: PhysicsBody2D) -> void: ## Handles collisions with other areas (for now, specifically other asteroids) func _on_area_entered(area: Area2D) -> void: - destroy() + if area is Asteroid: + destroy() diff --git a/examples/Ship2D/scenes/asteroid/asteroid.gd.uid b/examples/Ship2D/scenes/asteroid/asteroid.gd.uid new file mode 100644 index 0000000..362b083 --- /dev/null +++ b/examples/Ship2D/scenes/asteroid/asteroid.gd.uid @@ -0,0 +1 @@ +uid://cs372i2hmtx2s diff --git a/examples/Ship2D/scenes/asteroid/asteroid.tscn b/examples/Ship2D/scenes/asteroid/asteroid.tscn index 70eaa36..7dd8342 100644 --- a/examples/Ship2D/scenes/asteroid/asteroid.tscn +++ b/examples/Ship2D/scenes/asteroid/asteroid.tscn @@ -1,12 +1,12 @@ [gd_scene load_steps=14 format=3 uid="uid://da7uefbrja8u0"] -[ext_resource type="Script" path="res://scenes/asteroid/asteroid.gd" id="1_i2wen"] -[ext_resource type="Texture2D" uid="uid://c3uv7bhj0icto" path="res://assets/asteroid.png" id="2_uoiax"] -[ext_resource type="Texture2D" uid="uid://cjber4yoanul1" path="res://assets/asteroid2.png" id="3_danrt"] -[ext_resource type="Texture2D" uid="uid://drmjmr725556o" path="res://assets/asteroid3.png" id="4_ab212"] -[ext_resource type="Texture2D" uid="uid://b020rbym6vtor" path="res://assets/asteroid4.png" id="5_7s632"] -[ext_resource type="Texture2D" uid="uid://b5jhhj8ch1o3i" path="res://assets/asteroid5.png" id="6_du6mb"] -[ext_resource type="Texture2D" uid="uid://b05hpaculnwb5" path="res://assets/asteroid6.png" id="7_61yp0"] +[ext_resource type="Script" uid="uid://cs372i2hmtx2s" path="res://scenes/asteroid/asteroid.gd" id="1_i2wen"] +[ext_resource type="Texture2D" uid="uid://mjpxgn86nl23" path="res://assets/asteroid.png" id="2_uoiax"] +[ext_resource type="Texture2D" uid="uid://cqb6wvr02rka5" path="res://assets/asteroid2.png" id="3_danrt"] +[ext_resource type="Texture2D" uid="uid://rbv5tcfk0whb" path="res://assets/asteroid3.png" id="4_ab212"] +[ext_resource type="Texture2D" uid="uid://b0rw8ilj1ut2l" path="res://assets/asteroid4.png" id="5_7s632"] +[ext_resource type="Texture2D" uid="uid://c551m3175eapd" path="res://assets/asteroid5.png" id="6_du6mb"] +[ext_resource type="Texture2D" uid="uid://8oebebu1var" path="res://assets/asteroid6.png" id="7_61yp0"] [sub_resource type="Curve" id="Curve_g1nht"] bake_resolution = 10 @@ -17,7 +17,7 @@ point_count = 2 curve = SubResource("Curve_g1nht") [sub_resource type="Curve" id="Curve_s457i"] -max_value = 10.0 +_limits = [0.0, 10.0, 0.0, 1.0] bake_resolution = 10 _data = [Vector2(0, 3.23649), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0] point_count = 2 @@ -52,10 +52,10 @@ script = ExtResource("1_i2wen") process_mode = 1 emitting = false amount = 2 -process_material = SubResource("ParticleProcessMaterial_2m1fr") texture = ExtResource("3_danrt") one_shot = true explosiveness = 1.0 +process_material = SubResource("ParticleProcessMaterial_2m1fr") [node name="Sprites" type="Node2D" parent="."] diff --git a/examples/Ship2D/scenes/asteroid/asteroid_spawner.gd b/examples/Ship2D/scenes/asteroid/asteroid_spawner.gd index 364948e..5fd0d4b 100644 --- a/examples/Ship2D/scenes/asteroid/asteroid_spawner.gd +++ b/examples/Ship2D/scenes/asteroid/asteroid_spawner.gd @@ -1,6 +1,6 @@ extends Node -@export var ship: Ship +#@export var ship: Ship @export var asteroid_scene: PackedScene ## Used for grouping all spawned instances for easy removal when needed diff --git a/examples/Ship2D/scenes/asteroid/asteroid_spawner.gd.uid b/examples/Ship2D/scenes/asteroid/asteroid_spawner.gd.uid new file mode 100644 index 0000000..6f58a2f --- /dev/null +++ b/examples/Ship2D/scenes/asteroid/asteroid_spawner.gd.uid @@ -0,0 +1 @@ +uid://dk5nk5s2shpni diff --git a/examples/Ship2D/scenes/asteroid/asteroid_spawner.tscn b/examples/Ship2D/scenes/asteroid/asteroid_spawner.tscn index 45c8e0b..256fc47 100644 --- a/examples/Ship2D/scenes/asteroid/asteroid_spawner.tscn +++ b/examples/Ship2D/scenes/asteroid/asteroid_spawner.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=3 format=3 uid="uid://bwx014jyttag4"] -[ext_resource type="Script" path="res://scenes/asteroid/asteroid_spawner.gd" id="1_7njbd"] +[ext_resource type="Script" uid="uid://dk5nk5s2shpni" path="res://scenes/asteroid/asteroid_spawner.gd" id="1_7njbd"] [ext_resource type="PackedScene" uid="uid://da7uefbrja8u0" path="res://scenes/asteroid/asteroid.tscn" id="2_j1kme"] [node name="AsteroidSpawner" type="Node"] diff --git a/examples/Ship2D/scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.gd b/examples/Ship2D/scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.gd new file mode 100644 index 0000000..91c4623 --- /dev/null +++ b/examples/Ship2D/scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.gd @@ -0,0 +1,21 @@ +extends Node2D +class_name AsteroidGameScene + +## Used to differentiate envs +static var env_id: int = 0 + +@export var ship: Ship + +## By default, this is enabled for every 2nd env +## but you can override it for testing here +@export var override_can_shoot_always_enabled: bool = false + + +func _ready() -> void: + if override_can_shoot_always_enabled: + ship.can_shoot = 1 + return + + ## Enables ship shooting capability for every 2nd env only + ship.can_shoot = bool(env_id % 2) + AsteroidGameScene.env_id += 1 diff --git a/examples/Ship2D/scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.gd.uid b/examples/Ship2D/scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.gd.uid new file mode 100644 index 0000000..cf64cba --- /dev/null +++ b/examples/Ship2D/scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.gd.uid @@ -0,0 +1 @@ +uid://bteax4826v3aj diff --git a/examples/Ship2D/scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.tscn b/examples/Ship2D/scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.tscn new file mode 100644 index 0000000..90a51fb --- /dev/null +++ b/examples/Ship2D/scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.tscn @@ -0,0 +1,80 @@ +[gd_scene load_steps=11 format=3 uid="uid://bswnfo2l0387l"] + +[ext_resource type="Script" uid="uid://bteax4826v3aj" path="res://scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.gd" id="1_jgp3q"] +[ext_resource type="Texture2D" uid="uid://btmg1n0pa2h0d" path="res://assets/Background.png" id="1_yv3j2"] +[ext_resource type="PackedScene" uid="uid://giklicruf2u" path="res://scenes/ship/ship.tscn" id="2_vqieq"] +[ext_resource type="Script" uid="uid://bp4cqv2mxebik" path="res://scenes/game_scenes/spawned_objects.gd" id="4_o3sdh"] +[ext_resource type="PackedScene" uid="uid://bwx014jyttag4" path="res://scenes/asteroid/asteroid_spawner.tscn" id="5_8kb5v"] +[ext_resource type="Script" uid="uid://cmd1i3qvcx5r3" path="res://scenes/game_scenes/asteroid_game_scene/boundaries_asteroid_sensor.gd" id="6_0c2m8"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_1m5vw"] +size = Vector2(3920, 1000) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_4rtxq"] +size = Vector2(1000, 2000) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_rsqge"] +size = Vector2(3920, 1000) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_cjlw4"] +size = Vector2(1000, 2000) + +[node name="AsteroidGameScene" type="Node2D" node_paths=PackedStringArray("ship")] +script = ExtResource("1_jgp3q") +ship = NodePath("Ship") + +[node name="TextureRect" type="TextureRect" parent="."] +offset_right = 1920.0 +offset_bottom = 1080.0 +texture = ExtResource("1_yv3j2") + +[node name="Ship" parent="." node_paths=PackedStringArray("spawned_objects") instance=ExtResource("2_vqieq")] +position = Vector2(969, 999) +spawned_objects = NodePath("../SpawnedObjects") + +[node name="BoundariesAsteroidSensor" type="Area2D" parent="."] +collision_layer = 2 +collision_mask = 0 +monitoring = false +monitorable = false +script = ExtResource("6_0c2m8") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="BoundariesAsteroidSensor"] +position = Vector2(960, -500) +shape = SubResource("RectangleShape2D_1m5vw") + +[node name="CollisionShape2D3" type="CollisionShape2D" parent="BoundariesAsteroidSensor"] +position = Vector2(-500, 540) +shape = SubResource("RectangleShape2D_4rtxq") + +[node name="CollisionShape2D4" type="CollisionShape2D" parent="BoundariesAsteroidSensor"] +position = Vector2(2420, 540) +shape = SubResource("RectangleShape2D_4rtxq") + +[node name="CollisionShape2D2" type="CollisionShape2D" parent="BoundariesAsteroidSensor"] +position = Vector2(960, 1580) +shape = SubResource("RectangleShape2D_1m5vw") + +[node name="Boundaries" type="StaticBody2D" parent="."] + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Boundaries"] +position = Vector2(960, -500) +shape = SubResource("RectangleShape2D_rsqge") + +[node name="CollisionShape2D3" type="CollisionShape2D" parent="Boundaries"] +position = Vector2(-500, 540) +shape = SubResource("RectangleShape2D_cjlw4") + +[node name="CollisionShape2D4" type="CollisionShape2D" parent="Boundaries"] +position = Vector2(2420, 540) +shape = SubResource("RectangleShape2D_cjlw4") + +[node name="CollisionShape2D2" type="CollisionShape2D" parent="Boundaries"] +position = Vector2(960, 1580) +shape = SubResource("RectangleShape2D_rsqge") + +[node name="SpawnedObjects" type="Node2D" parent="."] +script = ExtResource("4_o3sdh") + +[node name="AsteroidSpawner" parent="." node_paths=PackedStringArray("spawned_objects") instance=ExtResource("5_8kb5v")] +spawned_objects = NodePath("../SpawnedObjects") diff --git a/examples/Ship2D/scenes/game_scenes/asteroid_game_scene/boundaries_asteroid_sensor.gd b/examples/Ship2D/scenes/game_scenes/asteroid_game_scene/boundaries_asteroid_sensor.gd new file mode 100644 index 0000000..c660769 --- /dev/null +++ b/examples/Ship2D/scenes/game_scenes/asteroid_game_scene/boundaries_asteroid_sensor.gd @@ -0,0 +1,3 @@ +extends Area2D + +## Prevents player raycast from seeing any asteroids outside the screen diff --git a/examples/Ship2D/scenes/game_scenes/asteroid_game_scene/boundaries_asteroid_sensor.gd.uid b/examples/Ship2D/scenes/game_scenes/asteroid_game_scene/boundaries_asteroid_sensor.gd.uid new file mode 100644 index 0000000..e6efb9c --- /dev/null +++ b/examples/Ship2D/scenes/game_scenes/asteroid_game_scene/boundaries_asteroid_sensor.gd.uid @@ -0,0 +1 @@ +uid://cmd1i3qvcx5r3 diff --git a/examples/Ship2D/scenes/game_scenes/boss_game_scene/boss_game_scene.gd b/examples/Ship2D/scenes/game_scenes/boss_game_scene/boss_game_scene.gd new file mode 100644 index 0000000..1dc5159 --- /dev/null +++ b/examples/Ship2D/scenes/game_scenes/boss_game_scene/boss_game_scene.gd @@ -0,0 +1,21 @@ +extends Node2D +class_name BossGameScene + +## Used to differentiate envs +static var env_id: int = 0 + +@export var ship: Ship + +## By default, this is enabled for every 2nd env +## but you can override it for testing here +@export var override_can_shoot_always_enabled: bool = false + + +func _ready() -> void: + if override_can_shoot_always_enabled: + ship.can_shoot = 1 + return + + ## Enables ship shooting capability for every 2nd env only + ship.can_shoot = bool(env_id % 2) + BossGameScene.env_id += 1 diff --git a/examples/Ship2D/scenes/game_scenes/boss_game_scene/boss_game_scene.gd.uid b/examples/Ship2D/scenes/game_scenes/boss_game_scene/boss_game_scene.gd.uid new file mode 100644 index 0000000..4d92da0 --- /dev/null +++ b/examples/Ship2D/scenes/game_scenes/boss_game_scene/boss_game_scene.gd.uid @@ -0,0 +1 @@ +uid://bplsqbdweadcr diff --git a/examples/Ship2D/scenes/game_scenes/boss_game_scene/boss_game_scene.tscn b/examples/Ship2D/scenes/game_scenes/boss_game_scene/boss_game_scene.tscn new file mode 100644 index 0000000..3194e35 --- /dev/null +++ b/examples/Ship2D/scenes/game_scenes/boss_game_scene/boss_game_scene.tscn @@ -0,0 +1,80 @@ +[gd_scene load_steps=10 format=3 uid="uid://cav0l7p55yibs"] + +[ext_resource type="Script" uid="uid://bplsqbdweadcr" path="res://scenes/game_scenes/boss_game_scene/boss_game_scene.gd" id="1_qiwy6"] +[ext_resource type="Texture2D" uid="uid://btmg1n0pa2h0d" path="res://assets/Background.png" id="2_gnyax"] +[ext_resource type="Script" uid="uid://b3g1xqywe8fr" path="res://scenes/game_scenes/boss_game_scene/boundaries_enemy_ship_sensor.gd" id="3_cjdmt"] +[ext_resource type="Script" uid="uid://bp4cqv2mxebik" path="res://scenes/game_scenes/spawned_objects.gd" id="4_kaqct"] +[ext_resource type="PackedScene" uid="uid://df1c2tmkk6e5i" path="res://scenes/ship_enemy_boss/boss.tscn" id="6_al25m"] +[ext_resource type="PackedScene" uid="uid://dd8ua86so56af" path="res://scenes/projectiles/projectile_enemy.tscn" id="6_cjdmt"] +[ext_resource type="PackedScene" uid="uid://giklicruf2u" path="res://scenes/ship/ship.tscn" id="6_gnyax"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_rsqge"] +size = Vector2(3920, 1000) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_cjlw4"] +size = Vector2(1000, 2000) + +[node name="BossGameScene" type="Node2D" node_paths=PackedStringArray("ship")] +script = ExtResource("1_qiwy6") +ship = NodePath("Ship") +override_can_shoot_always_enabled = true + +[node name="TextureRect" type="TextureRect" parent="."] +offset_right = 1920.0 +offset_bottom = 1080.0 +texture = ExtResource("2_gnyax") + +[node name="Boundaries" type="StaticBody2D" parent="."] +visible = false + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Boundaries"] +position = Vector2(960, -500) +shape = SubResource("RectangleShape2D_rsqge") + +[node name="CollisionShape2D3" type="CollisionShape2D" parent="Boundaries"] +position = Vector2(-500, 540) +shape = SubResource("RectangleShape2D_cjlw4") + +[node name="CollisionShape2D4" type="CollisionShape2D" parent="Boundaries"] +position = Vector2(2420, 540) +shape = SubResource("RectangleShape2D_cjlw4") + +[node name="CollisionShape2D2" type="CollisionShape2D" parent="Boundaries"] +position = Vector2(960, 1580) +shape = SubResource("RectangleShape2D_rsqge") + +[node name="BoundariesEnemyShipSensor" type="Area2D" parent="."] +collision_layer = 1024 +collision_mask = 0 +monitoring = false +monitorable = false +script = ExtResource("3_cjdmt") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="BoundariesEnemyShipSensor"] +position = Vector2(960, -500) +shape = SubResource("RectangleShape2D_rsqge") + +[node name="CollisionShape2D3" type="CollisionShape2D" parent="BoundariesEnemyShipSensor"] +position = Vector2(-500, 540) +shape = SubResource("RectangleShape2D_cjlw4") + +[node name="CollisionShape2D4" type="CollisionShape2D" parent="BoundariesEnemyShipSensor"] +position = Vector2(2420, 540) +shape = SubResource("RectangleShape2D_cjlw4") + +[node name="CollisionShape2D2" type="CollisionShape2D" parent="BoundariesEnemyShipSensor"] +position = Vector2(960, 1580) +shape = SubResource("RectangleShape2D_rsqge") + +[node name="SpawnedObjects" type="Node2D" parent="."] +script = ExtResource("4_kaqct") + +[node name="Ship" parent="." node_paths=PackedStringArray("boss_ship", "spawned_objects") instance=ExtResource("6_gnyax")] +position = Vector2(960, 956) +boss_ship = NodePath("../EnemyShipBoss") +spawned_objects = NodePath("../SpawnedObjects") + +[node name="EnemyShipBoss" parent="." node_paths=PackedStringArray("player_ship", "spawned_objects") instance=ExtResource("6_al25m")] +player_ship = NodePath("../Ship") +spawned_objects = NodePath("../SpawnedObjects") +ball_scene = ExtResource("6_cjdmt") diff --git a/examples/Ship2D/scenes/game_scenes/boss_game_scene/boundaries_enemy_ship_sensor.gd b/examples/Ship2D/scenes/game_scenes/boss_game_scene/boundaries_enemy_ship_sensor.gd new file mode 100644 index 0000000..365fa87 --- /dev/null +++ b/examples/Ship2D/scenes/game_scenes/boss_game_scene/boundaries_enemy_ship_sensor.gd @@ -0,0 +1,3 @@ +extends Area2D + +## Prevents player raycast from seeing any enemy ship(s) outside the screen diff --git a/examples/Ship2D/scenes/game_scenes/boss_game_scene/boundaries_enemy_ship_sensor.gd.uid b/examples/Ship2D/scenes/game_scenes/boss_game_scene/boundaries_enemy_ship_sensor.gd.uid new file mode 100644 index 0000000..d50d983 --- /dev/null +++ b/examples/Ship2D/scenes/game_scenes/boss_game_scene/boundaries_enemy_ship_sensor.gd.uid @@ -0,0 +1 @@ +uid://b3g1xqywe8fr diff --git a/examples/Ship2D/scenes/game_scenes/spawned_objects.gd.uid b/examples/Ship2D/scenes/game_scenes/spawned_objects.gd.uid new file mode 100644 index 0000000..d7bc15e --- /dev/null +++ b/examples/Ship2D/scenes/game_scenes/spawned_objects.gd.uid @@ -0,0 +1 @@ +uid://bp4cqv2mxebik diff --git a/examples/Ship2D/scenes/projectiles/ball.gd b/examples/Ship2D/scenes/projectiles/ball.gd new file mode 100644 index 0000000..b06dcea --- /dev/null +++ b/examples/Ship2D/scenes/projectiles/ball.gd @@ -0,0 +1,48 @@ +extends Area2D +class_name Projectile + +## Removes this instance after n seconds, if no other code removes it before that +@export var remove_after_seconds: int = 10 + +## Tracks how long this instance exists +var seconds: float + +## Linear velocity for movement, initially set by other nodes +var linear_velocity: Vector2 + +## The ship that spawned this instance +var spawned_by: Node2D + + +func _physics_process(delta: float) -> void: + process_remove_after_timeout(delta) + process_movement(delta) + + +## Moves the asteroid +func process_movement(delta): + position += linear_velocity * delta + + +## Removes the asteroid after `remove_after_seconds`, this is a safeguard +## in case the instance is not already removed by collisions or other code +func process_remove_after_timeout(delta): + seconds += delta + if seconds > remove_after_seconds: + queue_free() + + +## Handles collisions with other physics bodies (walls and player ship) +func _on_body_entered(body: Node) -> void: + if body is Ship: + body.hit() + queue_free() + + +## Handles collisions with other instances and the boss ship +func _on_area_entered(area: Area2D) -> void: + if area is Asteroid: + spawned_by.hit_an_asteroid() + if area.has_method("hit"): + area.hit() + queue_free() diff --git a/examples/Ship2D/scenes/projectiles/ball.gd.uid b/examples/Ship2D/scenes/projectiles/ball.gd.uid new file mode 100644 index 0000000..2d380c9 --- /dev/null +++ b/examples/Ship2D/scenes/projectiles/ball.gd.uid @@ -0,0 +1 @@ +uid://8aa5kw4xcr4m diff --git a/examples/Ship2D/scenes/projectiles/projectile_enemy.tscn b/examples/Ship2D/scenes/projectiles/projectile_enemy.tscn new file mode 100644 index 0000000..b84df4d --- /dev/null +++ b/examples/Ship2D/scenes/projectiles/projectile_enemy.tscn @@ -0,0 +1,22 @@ +[gd_scene load_steps=4 format=3 uid="uid://dd8ua86so56af"] + +[ext_resource type="Script" uid="uid://8aa5kw4xcr4m" path="res://scenes/projectiles/ball.gd" id="1_5d7ua"] +[ext_resource type="Texture2D" uid="uid://ya6rcpktvvvp" path="res://assets/ball_red.svg" id="2_5d7ua"] + +[sub_resource type="CircleShape2D" id="CircleShape2D_t02dy"] +radius = 17.0 + +[node name="ProjectileEnemy" type="Area2D"] +collision_layer = 32 +collision_mask = 513 +script = ExtResource("1_5d7ua") + +[node name="Sprite2D" type="Sprite2D" parent="."] +scale = Vector2(0.33, 0.33) +texture = ExtResource("2_5d7ua") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +shape = SubResource("CircleShape2D_t02dy") + +[connection signal="area_entered" from="." to="." method="_on_area_entered"] +[connection signal="body_entered" from="." to="." method="_on_body_entered"] diff --git a/examples/Ship2D/scenes/projectiles/projectile_player.tscn b/examples/Ship2D/scenes/projectiles/projectile_player.tscn new file mode 100644 index 0000000..8b0506b --- /dev/null +++ b/examples/Ship2D/scenes/projectiles/projectile_player.tscn @@ -0,0 +1,22 @@ +[gd_scene load_steps=4 format=3 uid="uid://dl6nxothvtqua"] + +[ext_resource type="Script" uid="uid://8aa5kw4xcr4m" path="res://scenes/projectiles/ball.gd" id="1_ovmhm"] +[ext_resource type="Texture2D" uid="uid://cnfr52i2fr3yg" path="res://assets/ball_green.svg" id="2_1r82m"] + +[sub_resource type="CircleShape2D" id="CircleShape2D_t02dy"] +radius = 17.0 + +[node name="ProjectilePlayer" type="Area2D"] +collision_layer = 16 +collision_mask = 1027 +script = ExtResource("1_ovmhm") + +[node name="Sprite2D" type="Sprite2D" parent="."] +scale = Vector2(0.33, 0.33) +texture = ExtResource("2_1r82m") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +shape = SubResource("CircleShape2D_t02dy") + +[connection signal="area_entered" from="." to="." method="_on_area_entered"] +[connection signal="body_entered" from="." to="." method="_on_body_entered"] diff --git a/examples/Ship2D/scenes/ship/ball_sensor.gd b/examples/Ship2D/scenes/ship/ball_sensor.gd new file mode 100644 index 0000000..72d8516 --- /dev/null +++ b/examples/Ship2D/scenes/ship/ball_sensor.gd @@ -0,0 +1,36 @@ +extends ShapeCast2D +class_name ProjectileSensor + +## Adds position to n nearest projectile objects detected +## maximum amount of objects included is set by max_results + + +func get_observation(): + var detected_projectiles: Array[Projectile] + for i in get_collision_count(): + var collider := get_collider(i) + if collider is Projectile: + detected_projectiles.append(collider) + + detected_projectiles.sort_custom( + func(a: Projectile, b: Projectile): + return ( + a.global_position.distance_squared_to(global_position) + < b.global_position.distance_squared_to(global_position) + ) + ) + + var obs: Array[float] + for projectile_idx in max_results: + var projectile: Projectile + if projectile_idx < detected_projectiles.size(): + projectile = detected_projectiles[projectile_idx] + + var projectile_obs := [0.0, 0.0, 0.0] + if projectile: + var relative := to_local(projectile.global_position) + var direction := relative.normalized() + var distance := clampf(relative.length() / shape.radius, 0.0, 1.0) + projectile_obs = [direction.x, direction.y, distance] + obs.append_array(projectile_obs) + return obs diff --git a/examples/Ship2D/scenes/ship/ball_sensor.gd.uid b/examples/Ship2D/scenes/ship/ball_sensor.gd.uid new file mode 100644 index 0000000..1350d7a --- /dev/null +++ b/examples/Ship2D/scenes/ship/ball_sensor.gd.uid @@ -0,0 +1 @@ +uid://bn2wd3ur67von diff --git a/examples/Ship2D/scenes/ship/ship.gd b/examples/Ship2D/scenes/ship/ship.gd index a23c074..953b370 100644 --- a/examples/Ship2D/scenes/ship/ship.gd +++ b/examples/Ship2D/scenes/ship/ship.gd @@ -1,18 +1,19 @@ extends CharacterBody2D class_name Ship -@export var ball_scene: PackedScene +@export var boss_ship: ShipEnemyBoss +@export var projectile_scene: PackedScene @export var ai_controller: ShipAIController ## Used for grouping all spawned instances for easy removal @export var spawned_objects: SpawnedObjects @export var ship_acceleration: float = 100000 -@export var ball_velocity: float = 2500 -@export var ball_fire_interval_seconds: float = 0.1 +@export var projectile_velocity: float = 2500 +@export var projectile_fire_interval_seconds: float = 0.1 @onready var _initial_transform := global_transform var can_shoot: bool -var time_since_ball_spawned: float +var time_since_projectile_spawned: float var requested_movement: float var requested_shoot: float @@ -36,19 +37,19 @@ func _physics_process(delta: float) -> void: move_and_slide() - time_since_ball_spawned += delta + time_since_projectile_spawned += delta if can_shoot and requested_shoot: handle_shoot() func handle_shoot(): - if time_since_ball_spawned > ball_fire_interval_seconds: - var ball = ball_scene.instantiate() as Ball - spawned_objects.add_child(ball) - ball.global_position = global_position - ball.linear_velocity = ((-global_transform.y.normalized()) * ball_velocity) - ball.spawned_by = self - time_since_ball_spawned = 0 + if time_since_projectile_spawned > projectile_fire_interval_seconds: + var projectile = projectile_scene.instantiate() as Projectile + spawned_objects.add_child(projectile) + projectile.global_position = global_position - global_transform.orthonormalized().y * 100 + projectile.linear_velocity = ((-global_transform.y.normalized()) * projectile_velocity) + projectile.spawned_by = self + time_since_projectile_spawned = 0 func game_over(final_reward := 0.0) -> void: @@ -56,18 +57,19 @@ func game_over(final_reward := 0.0) -> void: # Uncomment if you want to remove all spawned objects on reset # currently we keep all objects around - #spawned_objects.remove_all_spawned_items() + spawned_objects.remove_all_spawned_items() ai_controller.end_episode(final_reward) # Uncomment if you want to reposition the player on reset - #reposition_player() + reposition_player() velocity = Vector2.ZERO func reposition_player(): global_transform = _initial_transform - position.x = randf_range(100, 1800) + if boss_ship: # Might be moved to a scene manager in future updates + boss_ship.reset() ## Called by an asteroid instance that hits the ship @@ -79,6 +81,12 @@ func hit_by_asteroid() -> void: _time_survived = 0 -## Called by a ball instance that has hit an asteroid +## Called by a projectile instance that has hit an asteroid func hit_an_asteroid(): ai_controller.reward += 0.1 + + +## Called by a projectile instance that hit the ship +func hit(): + #print("Player hit by boss") + game_over(-10.0) diff --git a/examples/Ship2D/scenes/ship/ship.gd.uid b/examples/Ship2D/scenes/ship/ship.gd.uid new file mode 100644 index 0000000..133f3c9 --- /dev/null +++ b/examples/Ship2D/scenes/ship/ship.gd.uid @@ -0,0 +1 @@ +uid://8ytlelvwj7m2 diff --git a/examples/Ship2D/scenes/ship/ship.tscn b/examples/Ship2D/scenes/ship/ship.tscn index 0044e7a..7baf449 100644 --- a/examples/Ship2D/scenes/ship/ship.tscn +++ b/examples/Ship2D/scenes/ship/ship.tscn @@ -1,10 +1,13 @@ -[gd_scene load_steps=8 format=3 uid="uid://giklicruf2u"] +[gd_scene load_steps=9 format=3 uid="uid://giklicruf2u"] -[ext_resource type="Texture2D" uid="uid://c8oedbtmvqww" path="res://assets/Ship.png" id="1_dm7gt"] -[ext_resource type="Script" path="res://scenes/ship/ship.gd" id="1_hjjh5"] -[ext_resource type="PackedScene" uid="uid://dl6nxothvtqua" path="res://scenes/ball/ball.tscn" id="2_18p7p"] -[ext_resource type="Script" path="res://scenes/ship/ship_ai_controller.gd" id="3_ijd3m"] -[ext_resource type="Script" path="res://addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd" id="5_xq2sa"] +[ext_resource type="Texture2D" uid="uid://bihmrnhpofem3" path="res://assets/Ship.png" id="1_dm7gt"] +[ext_resource type="Script" uid="uid://8ytlelvwj7m2" path="res://scenes/ship/ship.gd" id="1_hjjh5"] +[ext_resource type="Script" uid="uid://ckmria3yip4c" path="res://scenes/ship/ship_ai_controller.gd" id="3_ijd3m"] +[ext_resource type="Script" uid="uid://dbdksstuow56x" path="res://addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd" id="5_xq2sa"] +[ext_resource type="Script" uid="uid://bn2wd3ur67von" path="res://scenes/ship/ball_sensor.gd" id="6_dh1m8"] + +[sub_resource type="CircleShape2D" id="CircleShape2D_7nsyl"] +radius = 1000.0 [sub_resource type="CircleShape2D" id="CircleShape2D_450bq"] radius = 41.72 @@ -16,7 +19,6 @@ height = 181.6 [node name="Ship" type="CharacterBody2D" node_paths=PackedStringArray("ai_controller")] collision_layer = 512 script = ExtResource("1_hjjh5") -ball_scene = ExtResource("2_18p7p") ai_controller = NodePath("AIController2D") [node name="Sprite2D" type="Sprite2D" parent="."] @@ -25,7 +27,7 @@ texture = ExtResource("1_dm7gt") [node name="AIController2D" type="Node2D" parent="." node_paths=PackedStringArray("player", "raycast_sensors")] script = ExtResource("3_ijd3m") player = NodePath("..") -raycast_sensors = [NodePath("RaycastSensorWall"), NodePath("RaycastSensorAsteroid")] +raycast_sensors = [NodePath("RaycastSensorWall"), NodePath("RaycastSensorAsteroid"), NodePath("RaycastSensorEnemyShip"), NodePath("EnemyProjectileSensor")] reset_after = 512 [node name="RaycastSensorWall" type="Node2D" parent="AIController2D"] @@ -43,6 +45,24 @@ collide_with_bodies = false ray_length = 2000 cone_width = 185.0 +[node name="RaycastSensorEnemyShip" type="Node2D" parent="AIController2D"] +rotation = -1.5708 +script = ExtResource("5_xq2sa") +collision_mask = 1024 +collide_with_areas = true +collide_with_bodies = false +ray_length = 2000 +cone_width = 185.0 + +[node name="EnemyProjectileSensor" type="ShapeCast2D" parent="AIController2D"] +visible = false +shape = SubResource("CircleShape2D_7nsyl") +max_results = 5 +collision_mask = 32 +collide_with_areas = true +collide_with_bodies = false +script = ExtResource("6_dh1m8") + [node name="CollisionShape2D" type="CollisionShape2D" parent="."] shape = SubResource("CircleShape2D_450bq") diff --git a/examples/Ship2D/scenes/ship/ship_ai_controller.gd b/examples/Ship2D/scenes/ship/ship_ai_controller.gd index 672e797..5dbf738 100644 --- a/examples/Ship2D/scenes/ship/ship_ai_controller.gd +++ b/examples/Ship2D/scenes/ship/ship_ai_controller.gd @@ -40,12 +40,14 @@ func get_obs() -> Dictionary: obs.append_array(raycast_obs) var velocity_x = clampf(player.get_real_velocity().x / 3000, -1.0, 1.0) - - obs.append_array([ - float(player.can_shoot), - clampf(player.time_since_ball_spawned / player.ball_fire_interval_seconds, 0, 1.0), - velocity_x - ]) + + obs.append_array( + [ + float(player.can_shoot), + clampf(player.time_since_ball_spawned / player.ball_fire_interval_seconds, 0, 1.0), + velocity_x + ] + ) return {"obs": obs} diff --git a/examples/Ship2D/scenes/ship/ship_ai_controller.gd.uid b/examples/Ship2D/scenes/ship/ship_ai_controller.gd.uid new file mode 100644 index 0000000..6b9c02c --- /dev/null +++ b/examples/Ship2D/scenes/ship/ship_ai_controller.gd.uid @@ -0,0 +1 @@ +uid://ckmria3yip4c diff --git a/examples/Ship2D/scenes/ship_enemy_boss/boss.gd b/examples/Ship2D/scenes/ship_enemy_boss/boss.gd new file mode 100644 index 0000000..b1821bd --- /dev/null +++ b/examples/Ship2D/scenes/ship_enemy_boss/boss.gd @@ -0,0 +1,95 @@ +extends Area2D +class_name ShipEnemyBoss + +@export var player_ship: Ship +## Used for grouping all spawned instances for easy removal +@export var spawned_objects: SpawnedObjects +@export var health_bar: HealthBar +## How much damage to receive if hit (by player projectile only for now) +@export var damage_taken_per_hit := 0.45 +@export var projectile_scene: PackedScene +## How many projectiles to spawn at once (in sequence) +@export var proj_spawn_count_sequence := [1, 3, 2, 4, 3, 5] +## The cone width of the spawned projectiles +@export var fire_cone_width := PI / 1.25 +## Time between spawning projectiles +@export var fire_interval := 1.38 +## Velocity of the spawned projectiles +@export var proj_velocity: float = 1000 +## The animation (used for different movement sequences of the boss) +@export var animation: AnimationPlayer + +@onready var _initial_transform = transform +var _proj_spawn_count_sequence_index := 0 + +var _fire_timer: float = 0 +var _hp := 100.0: + set = set_hp + + +func _physics_process(delta: float) -> void: + handle_shoot(delta) + + +func handle_shoot(delta): + _fire_timer += delta + if _fire_timer > fire_interval: + var spawn_count = proj_spawn_count_sequence[_proj_spawn_count_sequence_index] + shoot(spawn_count, fire_cone_width, true) + _proj_spawn_count_sequence_index += 1 + _proj_spawn_count_sequence_index %= proj_spawn_count_sequence.size() + _fire_timer = 0 + + +## Spawns projs +func shoot(spawn_count: int, cone_width: float, center_at_player: bool = false): + var step := cone_width / spawn_count + var start_angle := -(cone_width / 2.0) + (step / 2.0) + + for i in spawn_count: + var projectile = projectile_scene.instantiate() as Projectile + spawned_objects.add_child(projectile) + + projectile.global_position = global_position + + var projectile_direction: Vector2 + if center_at_player: + projectile_direction = projectile.global_position.direction_to( + player_ship.global_position + ) + else: + projectile_direction = Vector2.DOWN + + projectile.global_position += projectile_direction * 50.0 + projectile.linear_velocity = projectile_direction * proj_velocity + + var angle := start_angle + i * step + projectile.linear_velocity = projectile.linear_velocity.rotated(angle) + projectile.spawned_by = self + + +## Called by a projectile instance that hit the ship +func hit(): + _hp -= damage_taken_per_hit + if _hp < 0.1: + reset() + player_ship.ai_controller.reward += 0.1 + + +func set_hp(value): + _hp = value + health_bar.set_health(_hp) + + +func reset(): + animation.stop() + animation.play("animation") + _hp = 100 + _fire_timer = 0 + _proj_spawn_count_sequence_index = 0 + reposition() + + +func reposition(): + if _initial_transform: + transform = _initial_transform diff --git a/examples/Ship2D/scenes/ship_enemy_boss/boss.gd.uid b/examples/Ship2D/scenes/ship_enemy_boss/boss.gd.uid new file mode 100644 index 0000000..ae05e2d --- /dev/null +++ b/examples/Ship2D/scenes/ship_enemy_boss/boss.gd.uid @@ -0,0 +1 @@ +uid://dgq6ji1e4ow70 diff --git a/examples/Ship2D/scenes/ship_enemy_boss/boss.tscn b/examples/Ship2D/scenes/ship_enemy_boss/boss.tscn new file mode 100644 index 0000000..1949ebd --- /dev/null +++ b/examples/Ship2D/scenes/ship_enemy_boss/boss.tscn @@ -0,0 +1,146 @@ +[gd_scene load_steps=10 format=3 uid="uid://df1c2tmkk6e5i"] + +[ext_resource type="Script" uid="uid://dgq6ji1e4ow70" path="res://scenes/ship_enemy_boss/boss.gd" id="1_74po8"] +[ext_resource type="Texture2D" uid="uid://x0fs4oaimipy" path="res://assets/ShipEnemy.png" id="2_fbos3"] +[ext_resource type="Script" uid="uid://bjn1r547iu6tq" path="res://scenes/ship_enemy_boss/health_bar.gd" id="3_fbos3"] + +[sub_resource type="CircleShape2D" id="CircleShape2D_450bq"] +radius = 67.57 + +[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_fpp2b"] +radius = 74.71498 +height = 371.99994 + +[sub_resource type="Animation" id="Animation_cjdmt"] +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(960, 241)] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath(".:rotation") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [0.0] +} + +[sub_resource type="Animation" id="Animation_al25m"] +resource_name = "animation" +length = 5.0 +step = 0.0333333 +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, 1, 2, 3, 4, 5), +"transitions": PackedFloat32Array(-2, -2, -2, -2, -2, -2), +"update": 0, +"values": [Vector2(960, 187.73), Vector2(-240, 237.73), Vector2(960, 187.73), Vector2(960, 187.73), Vector2(2160, 237.73), Vector2(960, 187.73)] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath(".:rotation") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0, 1, 2, 3, 4, 5), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1), +"update": 0, +"values": [0.0, -0.17453292519943295, 0.17453292519943295, -0.17453292519943295, 0.17453292519943295, 0.0] +} + +[sub_resource type="Animation" id="Animation_i3xlu"] +resource_name = "animation2" +length = 10.0 +step = 0.0333333 +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, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10), +"transitions": PackedFloat32Array(-2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2), +"update": 0, +"values": [Vector2(960, 187.73), Vector2(960, -199.415), Vector2(460, -199.415), Vector2(460, 187.73), Vector2(460, 187.73), Vector2(1460, 187.73), Vector2(1460, 187.73), Vector2(1460, -199.415), Vector2(960, -199.415), Vector2(960, 187.73), Vector2(960, 187.73)] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath(".:rotation") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1), +"update": 0, +"values": [0.0, -0.17453292519943295, 0.17453292519943295, -0.17453292519943295, 0.17453292519943295, -0.17453292519943295, 0.17453292519943295, -0.17453292519943295, 0.17453292519943295, -0.17453292519943295, 0.17453292519943295] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_cjdmt"] +_data = { +&"RESET": SubResource("Animation_cjdmt"), +&"animation": SubResource("Animation_al25m"), +&"animation2": SubResource("Animation_i3xlu") +} + +[node name="EnemyShipBoss" type="Area2D" node_paths=PackedStringArray("health_bar", "animation")] +position = Vector2(960, 241) +collision_layer = 1024 +script = ExtResource("1_74po8") +health_bar = NodePath("HealthBar") +animation = NodePath("AnimationPlayer") + +[node name="Sprite2D" type="Sprite2D" parent="."] +scale = Vector2(2, 2) +texture = ExtResource("2_fbos3") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +position = Vector2(0, -26.455) +shape = SubResource("CircleShape2D_450bq") + +[node name="CollisionShape2D2" type="CollisionShape2D" parent="."] +position = Vector2(0, 11.285) +rotation = 1.5708 +shape = SubResource("CapsuleShape2D_fpp2b") + +[node name="HealthBar" type="ProgressBar" parent="."] +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -150.0 +offset_top = -138.5 +offset_right = 150.0 +offset_bottom = -111.5 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("3_fbos3") + +[node name="AnimationPlayer" type="AnimationPlayer" parent="."] +libraries = { +&"": SubResource("AnimationLibrary_cjdmt") +} +speed_scale = 0.5 +next/animation = &"animation2" +next/animation2 = &"animation" diff --git a/examples/Ship2D/scenes/ship_enemy_boss/health_bar.gd b/examples/Ship2D/scenes/ship_enemy_boss/health_bar.gd new file mode 100644 index 0000000..074274d --- /dev/null +++ b/examples/Ship2D/scenes/ship_enemy_boss/health_bar.gd @@ -0,0 +1,22 @@ +extends ProgressBar +class_name HealthBar + +var stylebox: StyleBoxFlat +@export var full_hp_color = Color.GREEN +@export var empty_hp_color = Color.RED +var current_color: Color = full_hp_color + + +func _ready() -> void: + stylebox = StyleBoxFlat.new() + add_theme_color_override("font_color", Color.BLACK) + add_theme_stylebox_override("background", stylebox) + + +func set_health(health := 100.0): + if not is_node_ready(): + await ready + + value = health + current_color = full_hp_color.lerp(empty_hp_color, (100 - health) / 100.0) + stylebox.bg_color = current_color diff --git a/examples/Ship2D/scenes/ship_enemy_boss/health_bar.gd.uid b/examples/Ship2D/scenes/ship_enemy_boss/health_bar.gd.uid new file mode 100644 index 0000000..7978c85 --- /dev/null +++ b/examples/Ship2D/scenes/ship_enemy_boss/health_bar.gd.uid @@ -0,0 +1 @@ +uid://bjn1r547iu6tq diff --git a/examples/Ship2D/scenes/test_scene/test_boss_scene.tscn b/examples/Ship2D/scenes/test_scene/test_boss_scene.tscn new file mode 100644 index 0000000..2f45e36 --- /dev/null +++ b/examples/Ship2D/scenes/test_scene/test_boss_scene.tscn @@ -0,0 +1,17 @@ +[gd_scene load_steps=3 format=3 uid="uid://dmumxjvt7qg0s"] + +[ext_resource type="PackedScene" uid="uid://cav0l7p55yibs" path="res://scenes/game_scenes/boss_game_scene/boss_game_scene.tscn" id="1_37srg"] +[ext_resource type="Script" uid="uid://dwyruq0ulo56c" path="res://addons/godot_rl_agents/sync.gd" id="2_lgab8"] + +[node name="TestScene" type="Node2D"] + +[node name="BossGameScene" parent="." instance=ExtResource("1_37srg")] + +[node name="Camera2D" type="Camera2D" parent="."] +position = Vector2(960, 540) + +[node name="Sync" type="Node" parent="."] +script = ExtResource("2_lgab8") +control_mode = 0 +action_repeat = 4 +onnx_model_path = "model.onnx" diff --git a/examples/Ship2D/scenes/test_scene/test_scene.tscn b/examples/Ship2D/scenes/test_scene/test_scene.tscn index edc8255..4da821c 100644 --- a/examples/Ship2D/scenes/test_scene/test_scene.tscn +++ b/examples/Ship2D/scenes/test_scene/test_scene.tscn @@ -1,7 +1,7 @@ [gd_scene load_steps=3 format=3 uid="uid://d35htadfm3qip"] -[ext_resource type="Script" path="res://addons/godot_rl_agents/sync.gd" id="2_0dvr4"] -[ext_resource type="PackedScene" uid="uid://bswnfo2l0387l" path="res://scenes/game_scenes/base_game_scene.tscn" id="3_3wx3y"] +[ext_resource type="Script" uid="uid://dwyruq0ulo56c" path="res://addons/godot_rl_agents/sync.gd" id="2_0dvr4"] +[ext_resource type="PackedScene" uid="uid://bswnfo2l0387l" path="res://scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.tscn" id="3_3wx3y"] [node name="TestScene" type="Node2D"] diff --git a/examples/Ship2D/scenes/train_scene/training_scene_asteroids.tscn b/examples/Ship2D/scenes/train_scene/training_scene_asteroids.tscn new file mode 100644 index 0000000..a9ec9ff --- /dev/null +++ b/examples/Ship2D/scenes/train_scene/training_scene_asteroids.tscn @@ -0,0 +1,45 @@ +[gd_scene load_steps=3 format=3 uid="uid://ji4spgoalcj6"] + +[ext_resource type="PackedScene" uid="uid://bswnfo2l0387l" path="res://scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.tscn" id="1_qvwyi"] +[ext_resource type="Script" uid="uid://dwyruq0ulo56c" path="res://addons/godot_rl_agents/sync.gd" id="2_rgi60"] + +[node name="TrainingScene" type="Node2D"] + +[node name="BaseGameScene" parent="." instance=ExtResource("1_qvwyi")] +override_can_shoot_always_enabled = true + +[node name="SubViewport" type="SubViewport" parent="."] + +[node name="BaseGameScene" parent="SubViewport" instance=ExtResource("1_qvwyi")] + +[node name="SubViewport2" type="SubViewport" parent="."] + +[node name="BaseGameScene" parent="SubViewport2" instance=ExtResource("1_qvwyi")] +override_can_shoot_always_enabled = true + +[node name="SubViewport3" type="SubViewport" parent="."] + +[node name="BaseGameScene" parent="SubViewport3" instance=ExtResource("1_qvwyi")] + +[node name="SubViewport4" type="SubViewport" parent="."] + +[node name="BaseGameScene" parent="SubViewport4" instance=ExtResource("1_qvwyi")] +override_can_shoot_always_enabled = true + +[node name="SubViewport5" type="SubViewport" parent="."] + +[node name="BaseGameScene" parent="SubViewport5" instance=ExtResource("1_qvwyi")] + +[node name="SubViewport6" type="SubViewport" parent="."] + +[node name="BaseGameScene" parent="SubViewport6" instance=ExtResource("1_qvwyi")] +override_can_shoot_always_enabled = true + +[node name="SubViewport7" type="SubViewport" parent="."] + +[node name="BaseGameScene" parent="SubViewport7" instance=ExtResource("1_qvwyi")] + +[node name="Sync" type="Node" parent="."] +script = ExtResource("2_rgi60") +action_repeat = 4 +speed_up = 16.0 diff --git a/examples/Ship2D/scenes/train_scene/training_scene_boss.tscn b/examples/Ship2D/scenes/train_scene/training_scene_boss.tscn new file mode 100644 index 0000000..ff98442 --- /dev/null +++ b/examples/Ship2D/scenes/train_scene/training_scene_boss.tscn @@ -0,0 +1,53 @@ +[gd_scene load_steps=3 format=3 uid="uid://2uwyckkppxmy"] + +[ext_resource type="PackedScene" uid="uid://cav0l7p55yibs" path="res://scenes/game_scenes/boss_game_scene/boss_game_scene.tscn" id="1_bi1es"] +[ext_resource type="Script" uid="uid://dwyruq0ulo56c" path="res://addons/godot_rl_agents/sync.gd" id="2_xghqd"] + +[node name="TrainingSceneBoss" type="Node2D"] + +[node name="BossGameScene" parent="." instance=ExtResource("1_bi1es")] +override_can_shoot_always_enabled = false + +[node name="SubViewport" type="SubViewport" parent="."] +canvas_cull_mask = 4294967294 + +[node name="BossGameScene" parent="SubViewport" instance=ExtResource("1_bi1es")] +override_can_shoot_always_enabled = false + +[node name="SubViewport2" type="SubViewport" parent="."] +canvas_cull_mask = 4294967294 + +[node name="BossGameScene" parent="SubViewport2" instance=ExtResource("1_bi1es")] + +[node name="SubViewport3" type="SubViewport" parent="."] +canvas_cull_mask = 4294967294 + +[node name="BossGameScene" parent="SubViewport3" instance=ExtResource("1_bi1es")] +override_can_shoot_always_enabled = false + +[node name="SubViewport4" type="SubViewport" parent="."] +canvas_cull_mask = 4294967294 + +[node name="BossGameScene" parent="SubViewport4" instance=ExtResource("1_bi1es")] + +[node name="SubViewport5" type="SubViewport" parent="."] +canvas_cull_mask = 4294967294 + +[node name="BossGameScene" parent="SubViewport5" instance=ExtResource("1_bi1es")] +override_can_shoot_always_enabled = false + +[node name="SubViewport6" type="SubViewport" parent="."] +canvas_cull_mask = 4294967294 + +[node name="BossGameScene" parent="SubViewport6" instance=ExtResource("1_bi1es")] + +[node name="SubViewport7" type="SubViewport" parent="."] +canvas_cull_mask = 4294967294 + +[node name="BossGameScene" parent="SubViewport7" instance=ExtResource("1_bi1es")] +override_can_shoot_always_enabled = false + +[node name="Sync" type="Node" parent="."] +script = ExtResource("2_xghqd") +action_repeat = 4 +speed_up = 16.0 diff --git a/examples/Ship2D/scenes/train_scene/training_scene_mixed.tscn b/examples/Ship2D/scenes/train_scene/training_scene_mixed.tscn new file mode 100644 index 0000000..9864337 --- /dev/null +++ b/examples/Ship2D/scenes/train_scene/training_scene_mixed.tscn @@ -0,0 +1,53 @@ +[gd_scene load_steps=4 format=3 uid="uid://cwobnl1dd05uq"] + +[ext_resource type="PackedScene" uid="uid://cav0l7p55yibs" path="res://scenes/game_scenes/boss_game_scene/boss_game_scene.tscn" id="1_8q6pp"] +[ext_resource type="Script" uid="uid://dwyruq0ulo56c" path="res://addons/godot_rl_agents/sync.gd" id="2_ccsrb"] +[ext_resource type="PackedScene" uid="uid://bswnfo2l0387l" path="res://scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.tscn" id="3_ccsrb"] + +[node name="TrainingSceneMixed" type="Node2D"] + +[node name="Sync" type="Node" parent="."] +script = ExtResource("2_ccsrb") +action_repeat = 4 +speed_up = 16.0 + +[node name="BossGameScene" parent="." instance=ExtResource("1_8q6pp")] + +[node name="AsteroidScenes" type="Node" parent="."] + +[node name="SubViewport" type="SubViewport" parent="AsteroidScenes"] + +[node name="BaseGameScene" parent="AsteroidScenes/SubViewport" instance=ExtResource("3_ccsrb")] + +[node name="SubViewport2" type="SubViewport" parent="AsteroidScenes"] + +[node name="BaseGameScene" parent="AsteroidScenes/SubViewport2" instance=ExtResource("3_ccsrb")] +override_can_shoot_always_enabled = true + +[node name="SubViewport3" type="SubViewport" parent="AsteroidScenes"] + +[node name="BaseGameScene" parent="AsteroidScenes/SubViewport3" instance=ExtResource("3_ccsrb")] + +[node name="SubViewport4" type="SubViewport" parent="AsteroidScenes"] + +[node name="BaseGameScene" parent="AsteroidScenes/SubViewport4" instance=ExtResource("3_ccsrb")] +override_can_shoot_always_enabled = true + +[node name="BossScenes" type="Node" parent="."] + +[node name="SubViewport" type="SubViewport" parent="BossScenes"] +canvas_cull_mask = 4294967294 + +[node name="BossGameScene" parent="BossScenes/SubViewport" instance=ExtResource("1_8q6pp")] +override_can_shoot_always_enabled = false + +[node name="SubViewport2" type="SubViewport" parent="BossScenes"] +canvas_cull_mask = 4294967294 + +[node name="BossGameScene" parent="BossScenes/SubViewport2" instance=ExtResource("1_8q6pp")] + +[node name="SubViewport3" type="SubViewport" parent="BossScenes"] +canvas_cull_mask = 4294967294 + +[node name="BossGameScene" parent="BossScenes/SubViewport3" instance=ExtResource("1_8q6pp")] +override_can_shoot_always_enabled = false diff --git a/examples/Ship2D/ships2d.csproj b/examples/Ship2D/ships2d.csproj index 4a8fc76..01706d4 100644 --- a/examples/Ship2D/ships2d.csproj +++ b/examples/Ship2D/ships2d.csproj @@ -1,8 +1,6 @@ - + net8.0 - net7.0 - net8.0 true From 66bd278b0e98d33ca9c2689f50f3112facd5b0bf Mon Sep 17 00:00:00 2001 From: Ivan-267 <61947090+Ivan-267@users.noreply.github.com> Date: Sat, 25 Oct 2025 09:07:51 +0200 Subject: [PATCH 6/8] onnx model updated, other small updates --- .../controller/ai_controller_2d.gd | 8 +- .../controller/ai_controller_2d.gd.uid | 2 +- .../controller/ai_controller_3d.gd | 8 +- .../controller/ai_controller_3d.gd.uid | 2 +- .../onnx/csharp/ONNXInference.cs | 56 +++++++------ .../onnx/csharp/ONNXInference.cs.uid | 2 +- .../onnx/wrapper/ONNX_wrapper.gd | 9 +- .../onnx/wrapper/ONNX_wrapper.gd.uid | 2 +- .../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 | 2 +- .../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.tscn | 2 +- .../sensors/sensors_3d/PositionSensor3D.gd | 79 ++++++++++++++++++ .../sensors_3d/PositionSensor3D.gd.uid | 1 + .../sensors/sensors_3d/RGBCameraSensor3D.gd | 6 +- .../sensors_3d/RGBCameraSensor3D.gd.uid | 2 +- .../sensors/sensors_3d/RGBCameraSensor3D.tscn | 2 +- .../sensors/sensors_3d/RaycastSensor3D.tscn | 2 +- .../Ship2D/addons/godot_rl_agents/sync.gd | 35 +++++++- .../Ship2D/addons/godot_rl_agents/sync.gd.uid | 2 +- examples/Ship2D/model.onnx | Bin 25306 -> 35802 bytes examples/Ship2D/project.godot | 2 +- .../scenes/asteroid/asteroid_spawner.gd | 6 -- .../asteroid_game_scene.gd | 21 +---- .../scenes/game_scenes/base_game_scene.gd.uid | 1 + .../boss_game_scene/boss_game_scene.gd | 21 +---- .../boss_game_scene/boss_game_scene.tscn | 3 +- .../Ship2D/scenes/projectiles/projectile.gd | 48 +++++++++++ .../scenes/projectiles/projectile.gd.uid | 1 + .../scenes/projectiles/projectile_enemy.tscn | 2 +- .../scenes/projectiles/projectile_player.tscn | 2 +- .../Ship2D/scenes/ship/projectile_sensor.gd | 42 ++++++++++ .../scenes/ship/projectile_sensor.gd.uid | 1 + examples/Ship2D/scenes/ship/ship.gd | 30 ++++--- examples/Ship2D/scenes/ship/ship.tscn | 7 +- .../Ship2D/scenes/ship/ship_ai_controller.gd | 4 +- .../Ship2D/scenes/ship_enemy_boss/boss.gd | 3 +- .../test_scene/test_scene_asteroids.tscn | 20 +++++ .../scenes/test_scene/test_scene_boss.tscn | 20 +++++ .../train_scene/training_scene_asteroids.tscn | 22 ++++- .../train_scene/training_scene_boss.tscn | 22 +++-- .../train_scene/training_scene_mixed.tscn | 66 +++++++++++---- 53 files changed, 692 insertions(+), 138 deletions(-) create mode 100644 examples/Ship2D/addons/godot_rl_agents/rewards/ApproachNodeReward2D.gd create mode 100644 examples/Ship2D/addons/godot_rl_agents/rewards/ApproachNodeReward2D.gd.uid create mode 100644 examples/Ship2D/addons/godot_rl_agents/rewards/ApproachNodeReward3D.gd create mode 100644 examples/Ship2D/addons/godot_rl_agents/rewards/ApproachNodeReward3D.gd.uid create mode 100644 examples/Ship2D/addons/godot_rl_agents/rewards/RewardFunction2D.gd create mode 100644 examples/Ship2D/addons/godot_rl_agents/rewards/RewardFunction2D.gd.uid create mode 100644 examples/Ship2D/addons/godot_rl_agents/rewards/RewardFunction3D.gd create mode 100644 examples/Ship2D/addons/godot_rl_agents/rewards/RewardFunction3D.gd.uid create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/PositionSensor2D.gd create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/PositionSensor2D.gd.uid create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RGBCameraSensor2D.gd create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RGBCameraSensor2D.gd.uid create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RGBCameraSensor2D.tscn create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/PositionSensor3D.gd create mode 100644 examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/PositionSensor3D.gd.uid create mode 100644 examples/Ship2D/scenes/game_scenes/base_game_scene.gd.uid create mode 100644 examples/Ship2D/scenes/projectiles/projectile.gd create mode 100644 examples/Ship2D/scenes/projectiles/projectile.gd.uid create mode 100644 examples/Ship2D/scenes/ship/projectile_sensor.gd create mode 100644 examples/Ship2D/scenes/ship/projectile_sensor.gd.uid create mode 100644 examples/Ship2D/scenes/test_scene/test_scene_asteroids.tscn create mode 100644 examples/Ship2D/scenes/test_scene/test_scene_boss.tscn diff --git a/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_2d.gd b/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_2d.gd index 5247135..06d928b 100644 --- a/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_2d.gd +++ b/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_2d.gd @@ -1,7 +1,13 @@ extends Node2D class_name AIController2D -enum ControlModes { INHERIT_FROM_SYNC, HUMAN, TRAINING, ONNX_INFERENCE, RECORD_EXPERT_DEMOS } ## Inherit setting from sync node ## Test the environment manually ## Train a model ## Load a pretrained model using an .onnx file ## Record observations and actions for expert demonstrations +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 := "" diff --git a/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_2d.gd.uid b/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_2d.gd.uid index 4f457c9..c41de21 100644 --- a/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_2d.gd.uid +++ b/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_2d.gd.uid @@ -1 +1 @@ -uid://c121dwd5k2m4i +uid://j72k8nt8vkha diff --git a/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_3d.gd b/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_3d.gd index 4984b18..61a0529 100644 --- a/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_3d.gd +++ b/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_3d.gd @@ -1,7 +1,13 @@ extends Node3D class_name AIController3D -enum ControlModes { INHERIT_FROM_SYNC, HUMAN, TRAINING, ONNX_INFERENCE, RECORD_EXPERT_DEMOS } ## Inherit setting from sync node ## Test the environment manually ## Train a model ## Load a pretrained model using an .onnx file ## Record observations and actions for expert demonstrations +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 := "" diff --git a/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_3d.gd.uid b/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_3d.gd.uid index f5b0c44..3345e9d 100644 --- a/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_3d.gd.uid +++ b/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_3d.gd.uid @@ -1 +1 @@ -uid://cop2ay1taoscq +uid://bs4t5ytckrlem diff --git a/examples/Ship2D/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs b/examples/Ship2D/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs index 6dcfa18..ec845ed 100644 --- a/examples/Ship2D/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs +++ b/examples/Ship2D/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs @@ -19,48 +19,54 @@ public partial class ONNXInference : GodotObject private SessionOptions SessionOpt; - ///

- /// init function - /// - /// - /// - /// Returns the output size of the model - public int Initialize(string Path, int BatchSize) + /// + /// 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]; - } + SessionOpt = SessionConfigurator.MakeConfiguredSessionOptions(); + session = LoadModel(modelPath); + return session.OutputMetadata["output"].Dimensions[1]; + } /// - public Godot.Collections.Dictionary> RunInference(Godot.Collections.Array obs, int state_ins) + public Godot.Collections.Dictionary> RunInference(Godot.Collections.Dictionary> 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 + //Expects a tensor of shape [batch_size, input_size] type float for any output of the agents observation dictionary 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++) + var modelInputsList = new List { - span[i] = obs[i]; + NamedOnnxValue.CreateFromTensor("state_ins", new DenseTensor(new float[] { state_ins }, new int[] { batchSize })) + }; + foreach (var key in obs.Keys) + { + var subObs = obs[key]; + // Fill the input tensors for each key of the observation + // create span of observation from specific inputSize + var obsData = new float[subObs.Count]; //There's probably a better way to do this + for (int i = 0; i < subObs.Count; i++) + { + obsData[i] = subObs[i]; + } + modelInputsList.Add( + NamedOnnxValue.CreateFromTensor(key, new DenseTensor(obsData, new int[] { batchSize, subObs.Count })) + ); } - 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; + IDisposableReadOnlyCollection results; //We do not use "using" here so we get a better exception explaination later try { - results = session.Run(inputs, outputNames); + results = session.Run(modelInputsList, outputNames); } catch (OnnxRuntimeException e) { diff --git a/examples/Ship2D/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs.uid b/examples/Ship2D/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs.uid index c871364..e701913 100644 --- a/examples/Ship2D/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs.uid +++ b/examples/Ship2D/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs.uid @@ -1 +1 @@ -uid://c0n546jt6p4wg +uid://dlwgh87fsvfk7 diff --git a/examples/Ship2D/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd b/examples/Ship2D/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd index d201f0c..7d29a03 100644 --- a/examples/Ship2D/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd +++ b/examples/Ship2D/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd @@ -15,17 +15,15 @@ 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: +# requires the observations as an Dictionary and the state_ins as an int +# returns a Dictionary containing the action the model takes. +func run_inference(obs: Dictionary, state_ins: int) -> Dictionary: if inferencer == null: printerr("Inferencer not initialized") return {} @@ -37,7 +35,6 @@ func _notification(what): 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 diff --git a/examples/Ship2D/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd.uid b/examples/Ship2D/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd.uid index 5117eee..5fbb2c4 100644 --- a/examples/Ship2D/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd.uid +++ b/examples/Ship2D/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd.uid @@ -1 +1 @@ -uid://dl3hfghbv75lh +uid://c5moyrpqt6o13 diff --git a/examples/Ship2D/addons/godot_rl_agents/rewards/ApproachNodeReward2D.gd b/examples/Ship2D/addons/godot_rl_agents/rewards/ApproachNodeReward2D.gd new file mode 100644 index 0000000..c4aab2a --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/rewards/ApproachNodeReward2D.gd.uid b/examples/Ship2D/addons/godot_rl_agents/rewards/ApproachNodeReward2D.gd.uid new file mode 100644 index 0000000..45aeb4d --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/rewards/ApproachNodeReward2D.gd.uid @@ -0,0 +1 @@ +uid://d0wljq0650h6d diff --git a/examples/Ship2D/addons/godot_rl_agents/rewards/ApproachNodeReward3D.gd b/examples/Ship2D/addons/godot_rl_agents/rewards/ApproachNodeReward3D.gd new file mode 100644 index 0000000..d83fb4a --- /dev/null +++ b/examples/Ship2D/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_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/Ship2D/addons/godot_rl_agents/rewards/ApproachNodeReward3D.gd.uid b/examples/Ship2D/addons/godot_rl_agents/rewards/ApproachNodeReward3D.gd.uid new file mode 100644 index 0000000..bc4f1fc --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/rewards/ApproachNodeReward3D.gd.uid @@ -0,0 +1 @@ +uid://u0v82fqc1qu1 diff --git a/examples/Ship2D/addons/godot_rl_agents/rewards/RewardFunction2D.gd b/examples/Ship2D/addons/godot_rl_agents/rewards/RewardFunction2D.gd new file mode 100644 index 0000000..0285f78 --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/rewards/RewardFunction2D.gd.uid b/examples/Ship2D/addons/godot_rl_agents/rewards/RewardFunction2D.gd.uid new file mode 100644 index 0000000..ebb6a0b --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/rewards/RewardFunction2D.gd.uid @@ -0,0 +1 @@ +uid://t4n8u2axlhy diff --git a/examples/Ship2D/addons/godot_rl_agents/rewards/RewardFunction3D.gd b/examples/Ship2D/addons/godot_rl_agents/rewards/RewardFunction3D.gd new file mode 100644 index 0000000..87da5f9 --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/rewards/RewardFunction3D.gd.uid b/examples/Ship2D/addons/godot_rl_agents/rewards/RewardFunction3D.gd.uid new file mode 100644 index 0000000..d2f87f4 --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/rewards/RewardFunction3D.gd.uid @@ -0,0 +1 @@ +uid://08atf5btym8i diff --git a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/ExampleRaycastSensor2D.tscn b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/ExampleRaycastSensor2D.tscn index a203360..5edb6c7 100644 --- a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/ExampleRaycastSensor2D.tscn +++ b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/ExampleRaycastSensor2D.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=5 format=3 uid="uid://ddeq7mn1ealyc"] -[ext_resource type="Script" uid="uid://dbdksstuow56x" path="res://addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd" id="1"] +[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 diff --git a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/PositionSensor2D.gd b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/PositionSensor2D.gd new file mode 100644 index 0000000..34ed435 --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/PositionSensor2D.gd.uid b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/PositionSensor2D.gd.uid new file mode 100644 index 0000000..282581c --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/PositionSensor2D.gd.uid @@ -0,0 +1 @@ +uid://chtm8dxxej22l diff --git a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RGBCameraSensor2D.gd b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RGBCameraSensor2D.gd new file mode 100644 index 0000000..3afe39c --- /dev/null +++ b/examples/Ship2D/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 := Vector2i(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 := Vector2i(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 := Vector2i(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/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RGBCameraSensor2D.gd.uid b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RGBCameraSensor2D.gd.uid new file mode 100644 index 0000000..3400a4d --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RGBCameraSensor2D.gd.uid @@ -0,0 +1 @@ +uid://dfeteba88enyi diff --git a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RGBCameraSensor2D.tscn b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RGBCameraSensor2D.tscn new file mode 100644 index 0000000..94ab778 --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.tscn b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.tscn index a485ee1..5ca402c 100644 --- a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.tscn +++ b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=2 format=3 uid="uid://drvfihk5esgmv"] -[ext_resource type="Script" uid="uid://dbdksstuow56x" path="res://addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd" id="1"] +[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") diff --git a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/PositionSensor3D.gd b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/PositionSensor3D.gd new file mode 100644 index 0000000..85fcf03 --- /dev/null +++ b/examples/Ship2D/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/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/PositionSensor3D.gd.uid b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/PositionSensor3D.gd.uid new file mode 100644 index 0000000..06afefd --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/PositionSensor3D.gd.uid @@ -0,0 +1 @@ +uid://drlm33po47650 diff --git a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd index 96dfb6a..c263637 100644 --- a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd +++ b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd @@ -7,11 +7,11 @@ var camera_pixels = null @onready var sub_viewport := $SubViewport as SubViewport @onready var displayed_image: ImageTexture -@export var render_image_resolution := Vector2(36, 36) +@export var render_image_resolution := Vector2i(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 var displayed_image_scale_factor := Vector2i(8, 8) @export_group("Downscale image options") ## Enable to downscale the rendered image before sending the obs. @@ -19,7 +19,7 @@ var camera_pixels = null ## 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) +@export var resized_image_resolution := Vector2i(36, 36) func _ready(): diff --git a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd.uid b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd.uid index 65eaa89..1a8ab8d 100644 --- a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd.uid +++ b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd.uid @@ -1 +1 @@ -uid://bvk2trprdg2q5 +uid://dwd553usk3fed diff --git a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.tscn b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.tscn index 4eef45a..d58649c 100644 --- a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.tscn +++ b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=3 format=3 uid="uid://baaywi3arsl2m"] -[ext_resource type="Script" uid="uid://bvk2trprdg2q5" path="res://addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd" id="1"] +[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") diff --git a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.tscn b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.tscn index be02b34..35f9796 100644 --- a/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.tscn +++ b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=2 format=3 uid="uid://b803cbh1fmy66"] -[ext_resource type="Script" uid="uid://cqnmnaoemdk8f" path="res://addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.gd" id="1"] +[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") diff --git a/examples/Ship2D/addons/godot_rl_agents/sync.gd b/examples/Ship2D/addons/godot_rl_agents/sync.gd index 68c6581..8428976 100644 --- a/examples/Ship2D/addons/godot_rl_agents/sync.gd +++ b/examples/Ship2D/addons/godot_rl_agents/sync.gd @@ -3,7 +3,11 @@ class_name Sync # --fixed-fps 2000 --disable-render-loop -enum ControlModes { HUMAN, TRAINING, ONNX_INFERENCE } ## Test the environment manually ## Train a model ## Load a pretrained model using an .onnx file +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 @@ -11,6 +15,8 @@ enum ControlModes { HUMAN, TRAINING, ONNX_INFERENCE } ## Test the environment m @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 := "" +## Whether the inference will be deterministic (NOTE: Only applies to discrete actions in onnx inference mode) +@export var deterministic_inference := true # Onnx model stored for each requested path var onnx_models: Dictionary @@ -226,7 +232,7 @@ func _inference_process(): 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 = model.run_inference(obs[agent_id], 1.0) var action_dict = _extract_action_dict( action["output"], _action_space_inference[agent_id], model.action_means_only ) @@ -288,15 +294,36 @@ func _extract_action_dict(action_array: Array, action_space: Dictionary, action_ 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 # Value of the largest logit for this action in the actions array + var largest_logit: float = -INF # Value of the largest logit for this action in the actions array var largest_logit_idx: int # 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 + if deterministic_inference: + result[key] = largest_logit_idx # Index of the largest logit is the discrete action value + else: + var exp_logit_sum: float # Sum of exp of each logit + var exp_logits: Array[float] + + for logit_idx in range(0, size): + # Normalize using the max logit to add stability in case a logit would be huge after exp + exp_logits.append(exp(action_array[index + logit_idx] - largest_logit)) + exp_logit_sum += exp_logits[logit_idx] + + # Choose a random number, will be used to select an action + var random_value = randf_range(0, exp_logit_sum) + + # Select the first index at which the sum is larger than the random number + var sum: float + for exp_logit_idx in exp_logits.size(): + sum += exp_logits[exp_logit_idx] + if sum > random_value: + result[key] = exp_logit_idx + break index += size elif action_type == "continuous": # For continous actions, we only take the action mean values diff --git a/examples/Ship2D/addons/godot_rl_agents/sync.gd.uid b/examples/Ship2D/addons/godot_rl_agents/sync.gd.uid index f662c1d..4aa605d 100644 --- a/examples/Ship2D/addons/godot_rl_agents/sync.gd.uid +++ b/examples/Ship2D/addons/godot_rl_agents/sync.gd.uid @@ -1 +1 @@ -uid://dwyruq0ulo56c +uid://dsq7jynbrdcgu diff --git a/examples/Ship2D/model.onnx b/examples/Ship2D/model.onnx index 55d0d79dc8020bd0b80930fd51ef548b76076f51..c8ff6185bbd06ef43df9c00cc2d83be0cf270371 100644 GIT binary patch literal 35802 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 x$x6tKTBY+}>FO%7%KS2g|8tqAuI&Ftx-0*8+eq void: - return - reset_spawn_timer() - - # Called every frame. 'delta' is the elapsed time since the previous frame. func _physics_process(delta: float) -> void: _asteroid_spawn_timer += delta diff --git a/examples/Ship2D/scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.gd b/examples/Ship2D/scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.gd index 91c4623..e7ca7a9 100644 --- a/examples/Ship2D/scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.gd +++ b/examples/Ship2D/scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.gd @@ -1,21 +1,2 @@ -extends Node2D +extends BaseGameScene class_name AsteroidGameScene - -## Used to differentiate envs -static var env_id: int = 0 - -@export var ship: Ship - -## By default, this is enabled for every 2nd env -## but you can override it for testing here -@export var override_can_shoot_always_enabled: bool = false - - -func _ready() -> void: - if override_can_shoot_always_enabled: - ship.can_shoot = 1 - return - - ## Enables ship shooting capability for every 2nd env only - ship.can_shoot = bool(env_id % 2) - AsteroidGameScene.env_id += 1 diff --git a/examples/Ship2D/scenes/game_scenes/base_game_scene.gd.uid b/examples/Ship2D/scenes/game_scenes/base_game_scene.gd.uid new file mode 100644 index 0000000..a3fd31a --- /dev/null +++ b/examples/Ship2D/scenes/game_scenes/base_game_scene.gd.uid @@ -0,0 +1 @@ +uid://bhpyfqqdtsper diff --git a/examples/Ship2D/scenes/game_scenes/boss_game_scene/boss_game_scene.gd b/examples/Ship2D/scenes/game_scenes/boss_game_scene/boss_game_scene.gd index 1dc5159..ad07432 100644 --- a/examples/Ship2D/scenes/game_scenes/boss_game_scene/boss_game_scene.gd +++ b/examples/Ship2D/scenes/game_scenes/boss_game_scene/boss_game_scene.gd @@ -1,21 +1,2 @@ -extends Node2D +extends BaseGameScene class_name BossGameScene - -## Used to differentiate envs -static var env_id: int = 0 - -@export var ship: Ship - -## By default, this is enabled for every 2nd env -## but you can override it for testing here -@export var override_can_shoot_always_enabled: bool = false - - -func _ready() -> void: - if override_can_shoot_always_enabled: - ship.can_shoot = 1 - return - - ## Enables ship shooting capability for every 2nd env only - ship.can_shoot = bool(env_id % 2) - BossGameScene.env_id += 1 diff --git a/examples/Ship2D/scenes/game_scenes/boss_game_scene/boss_game_scene.tscn b/examples/Ship2D/scenes/game_scenes/boss_game_scene/boss_game_scene.tscn index 3194e35..c8615f2 100644 --- a/examples/Ship2D/scenes/game_scenes/boss_game_scene/boss_game_scene.tscn +++ b/examples/Ship2D/scenes/game_scenes/boss_game_scene/boss_game_scene.tscn @@ -17,7 +17,6 @@ size = Vector2(1000, 2000) [node name="BossGameScene" type="Node2D" node_paths=PackedStringArray("ship")] script = ExtResource("1_qiwy6") ship = NodePath("Ship") -override_can_shoot_always_enabled = true [node name="TextureRect" type="TextureRect" parent="."] offset_right = 1920.0 @@ -77,4 +76,4 @@ spawned_objects = NodePath("../SpawnedObjects") [node name="EnemyShipBoss" parent="." node_paths=PackedStringArray("player_ship", "spawned_objects") instance=ExtResource("6_al25m")] player_ship = NodePath("../Ship") spawned_objects = NodePath("../SpawnedObjects") -ball_scene = ExtResource("6_cjdmt") +projectile_scene = ExtResource("6_cjdmt") diff --git a/examples/Ship2D/scenes/projectiles/projectile.gd b/examples/Ship2D/scenes/projectiles/projectile.gd new file mode 100644 index 0000000..b06dcea --- /dev/null +++ b/examples/Ship2D/scenes/projectiles/projectile.gd @@ -0,0 +1,48 @@ +extends Area2D +class_name Projectile + +## Removes this instance after n seconds, if no other code removes it before that +@export var remove_after_seconds: int = 10 + +## Tracks how long this instance exists +var seconds: float + +## Linear velocity for movement, initially set by other nodes +var linear_velocity: Vector2 + +## The ship that spawned this instance +var spawned_by: Node2D + + +func _physics_process(delta: float) -> void: + process_remove_after_timeout(delta) + process_movement(delta) + + +## Moves the asteroid +func process_movement(delta): + position += linear_velocity * delta + + +## Removes the asteroid after `remove_after_seconds`, this is a safeguard +## in case the instance is not already removed by collisions or other code +func process_remove_after_timeout(delta): + seconds += delta + if seconds > remove_after_seconds: + queue_free() + + +## Handles collisions with other physics bodies (walls and player ship) +func _on_body_entered(body: Node) -> void: + if body is Ship: + body.hit() + queue_free() + + +## Handles collisions with other instances and the boss ship +func _on_area_entered(area: Area2D) -> void: + if area is Asteroid: + spawned_by.hit_an_asteroid() + if area.has_method("hit"): + area.hit() + queue_free() diff --git a/examples/Ship2D/scenes/projectiles/projectile.gd.uid b/examples/Ship2D/scenes/projectiles/projectile.gd.uid new file mode 100644 index 0000000..2d380c9 --- /dev/null +++ b/examples/Ship2D/scenes/projectiles/projectile.gd.uid @@ -0,0 +1 @@ +uid://8aa5kw4xcr4m diff --git a/examples/Ship2D/scenes/projectiles/projectile_enemy.tscn b/examples/Ship2D/scenes/projectiles/projectile_enemy.tscn index b84df4d..07c95eb 100644 --- a/examples/Ship2D/scenes/projectiles/projectile_enemy.tscn +++ b/examples/Ship2D/scenes/projectiles/projectile_enemy.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=4 format=3 uid="uid://dd8ua86so56af"] -[ext_resource type="Script" uid="uid://8aa5kw4xcr4m" path="res://scenes/projectiles/ball.gd" id="1_5d7ua"] +[ext_resource type="Script" uid="uid://8aa5kw4xcr4m" path="res://scenes/projectiles/projectile.gd" id="1_5d7ua"] [ext_resource type="Texture2D" uid="uid://ya6rcpktvvvp" path="res://assets/ball_red.svg" id="2_5d7ua"] [sub_resource type="CircleShape2D" id="CircleShape2D_t02dy"] diff --git a/examples/Ship2D/scenes/projectiles/projectile_player.tscn b/examples/Ship2D/scenes/projectiles/projectile_player.tscn index 8b0506b..9566c78 100644 --- a/examples/Ship2D/scenes/projectiles/projectile_player.tscn +++ b/examples/Ship2D/scenes/projectiles/projectile_player.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=4 format=3 uid="uid://dl6nxothvtqua"] -[ext_resource type="Script" uid="uid://8aa5kw4xcr4m" path="res://scenes/projectiles/ball.gd" id="1_ovmhm"] +[ext_resource type="Script" uid="uid://8aa5kw4xcr4m" path="res://scenes/projectiles/projectile.gd" id="1_ovmhm"] [ext_resource type="Texture2D" uid="uid://cnfr52i2fr3yg" path="res://assets/ball_green.svg" id="2_1r82m"] [sub_resource type="CircleShape2D" id="CircleShape2D_t02dy"] diff --git a/examples/Ship2D/scenes/ship/projectile_sensor.gd b/examples/Ship2D/scenes/ship/projectile_sensor.gd new file mode 100644 index 0000000..f0273b7 --- /dev/null +++ b/examples/Ship2D/scenes/ship/projectile_sensor.gd @@ -0,0 +1,42 @@ +extends ShapeCast2D +class_name ProjectileSensor + +## Adds position to n nearest projectile objects detected +## maximum amount of objects included is set by max_results + + +func get_observation(): + force_shapecast_update() + + var detected_projectiles: Array[Projectile] + for i in get_collision_count(): + var collider := get_collider(i) + if collider is Projectile: + detected_projectiles.append(collider) + + detected_projectiles.sort_custom( + func(a: Projectile, b: Projectile): + return ( + a.global_position.distance_squared_to(global_position) + < b.global_position.distance_squared_to(global_position) + ) + ) + + var obs: Array[float] + for projectile_idx in max_results: + var projectile: Projectile + if projectile_idx < detected_projectiles.size(): + projectile = detected_projectiles[projectile_idx] + + var projectile_obs := [0.0, 0.0, 0.0, 0.0, 0.0] + #var projectile_obs := [0.0, 0.0, 0.0] + if projectile: + var relative := to_local(projectile.global_position) + var direction := relative.normalized() + var distance := clampf(relative.length() / shape.radius, 0.0, 1.0) + var rel_vel = projectile.linear_velocity + #rel_vel = rel_vel.rotated(-projectile.global_position.direction_to(global_position).angle()) + rel_vel = rel_vel.normalized() # Keep only the directional info + projectile_obs = [direction.x, direction.y, distance, rel_vel.x, rel_vel.y] + obs.append_array(projectile_obs) + return obs diff --git a/examples/Ship2D/scenes/ship/projectile_sensor.gd.uid b/examples/Ship2D/scenes/ship/projectile_sensor.gd.uid new file mode 100644 index 0000000..1350d7a --- /dev/null +++ b/examples/Ship2D/scenes/ship/projectile_sensor.gd.uid @@ -0,0 +1 @@ +uid://bn2wd3ur67von diff --git a/examples/Ship2D/scenes/ship/ship.gd b/examples/Ship2D/scenes/ship/ship.gd index 953b370..2b31552 100644 --- a/examples/Ship2D/scenes/ship/ship.gd +++ b/examples/Ship2D/scenes/ship/ship.gd @@ -54,14 +54,8 @@ func handle_shoot(): func game_over(final_reward := 0.0) -> void: _time_survived = 0 - - # Uncomment if you want to remove all spawned objects on reset - # currently we keep all objects around spawned_objects.remove_all_spawned_items() - ai_controller.end_episode(final_reward) - - # Uncomment if you want to reposition the player on reset reposition_player() velocity = Vector2.ZERO @@ -75,10 +69,9 @@ func reposition_player(): ## Called by an asteroid instance that hits the ship func hit_by_asteroid() -> void: # Check that the ship has survived for longer than n seconds - # this adds a "protection period" after getting hit by an asteroid + # this adds a "protection period" after getting hit if _time_survived > 1.0: - game_over(-1.0) - _time_survived = 0 + game_over(-5.0) ## Called by a projectile instance that has hit an asteroid @@ -86,7 +79,22 @@ func hit_an_asteroid(): ai_controller.reward += 0.1 +## Called by enemy boss ship that was hit +func hit_enemy_boss_ship(): + ai_controller.reward += 0.1 + + +func defeated_boss_ship(): + game_over(0.0) + + ## Called by a projectile instance that hit the ship +## (currently that means the enemy boss ship has hit the player ship) func hit(): - #print("Player hit by boss") - game_over(-10.0) + # Check that the ship has survived for longer than n seconds + # this adds a "protection period" after getting hit + if _time_survived > 1.0: + game_over(-50.0) + print("hit_by_boss") + else: + print("hit before 1sec") diff --git a/examples/Ship2D/scenes/ship/ship.tscn b/examples/Ship2D/scenes/ship/ship.tscn index 7baf449..97111e5 100644 --- a/examples/Ship2D/scenes/ship/ship.tscn +++ b/examples/Ship2D/scenes/ship/ship.tscn @@ -1,10 +1,11 @@ -[gd_scene load_steps=9 format=3 uid="uid://giklicruf2u"] +[gd_scene load_steps=10 format=3 uid="uid://giklicruf2u"] [ext_resource type="Texture2D" uid="uid://bihmrnhpofem3" path="res://assets/Ship.png" id="1_dm7gt"] [ext_resource type="Script" uid="uid://8ytlelvwj7m2" path="res://scenes/ship/ship.gd" id="1_hjjh5"] +[ext_resource type="PackedScene" uid="uid://dl6nxothvtqua" path="res://scenes/projectiles/projectile_player.tscn" id="2_18p7p"] [ext_resource type="Script" uid="uid://ckmria3yip4c" path="res://scenes/ship/ship_ai_controller.gd" id="3_ijd3m"] [ext_resource type="Script" uid="uid://dbdksstuow56x" path="res://addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd" id="5_xq2sa"] -[ext_resource type="Script" uid="uid://bn2wd3ur67von" path="res://scenes/ship/ball_sensor.gd" id="6_dh1m8"] +[ext_resource type="Script" uid="uid://bn2wd3ur67von" path="res://scenes/ship/projectile_sensor.gd" id="6_dh1m8"] [sub_resource type="CircleShape2D" id="CircleShape2D_7nsyl"] radius = 1000.0 @@ -19,6 +20,7 @@ height = 181.6 [node name="Ship" type="CharacterBody2D" node_paths=PackedStringArray("ai_controller")] collision_layer = 512 script = ExtResource("1_hjjh5") +projectile_scene = ExtResource("2_18p7p") ai_controller = NodePath("AIController2D") [node name="Sprite2D" type="Sprite2D" parent="."] @@ -56,6 +58,7 @@ cone_width = 185.0 [node name="EnemyProjectileSensor" type="ShapeCast2D" parent="AIController2D"] visible = false +enabled = false shape = SubResource("CircleShape2D_7nsyl") max_results = 5 collision_mask = 32 diff --git a/examples/Ship2D/scenes/ship/ship_ai_controller.gd b/examples/Ship2D/scenes/ship/ship_ai_controller.gd index 5dbf738..8ad1718 100644 --- a/examples/Ship2D/scenes/ship/ship_ai_controller.gd +++ b/examples/Ship2D/scenes/ship/ship_ai_controller.gd @@ -32,6 +32,7 @@ func get_raycast_obs() -> Array[float]: return obs +#var previous_raycast_obs: Array func get_obs() -> Dictionary: var obs: Array[float] @@ -44,7 +45,8 @@ func get_obs() -> Dictionary: obs.append_array( [ float(player.can_shoot), - clampf(player.time_since_ball_spawned / player.ball_fire_interval_seconds, 0, 1.0), + clampf(player.time_since_projectile_spawned / + player.projectile_fire_interval_seconds, 0, 1.0), velocity_x ] ) diff --git a/examples/Ship2D/scenes/ship_enemy_boss/boss.gd b/examples/Ship2D/scenes/ship_enemy_boss/boss.gd index b1821bd..b324799 100644 --- a/examples/Ship2D/scenes/ship_enemy_boss/boss.gd +++ b/examples/Ship2D/scenes/ship_enemy_boss/boss.gd @@ -73,7 +73,8 @@ func hit(): _hp -= damage_taken_per_hit if _hp < 0.1: reset() - player_ship.ai_controller.reward += 0.1 + player_ship.defeated_boss_ship() + player_ship.hit_enemy_boss_ship() func set_hp(value): diff --git a/examples/Ship2D/scenes/test_scene/test_scene_asteroids.tscn b/examples/Ship2D/scenes/test_scene/test_scene_asteroids.tscn new file mode 100644 index 0000000..c0aa857 --- /dev/null +++ b/examples/Ship2D/scenes/test_scene/test_scene_asteroids.tscn @@ -0,0 +1,20 @@ +[gd_scene load_steps=3 format=3 uid="uid://d35htadfm3qip"] + +[ext_resource type="PackedScene" uid="uid://bswnfo2l0387l" path="res://scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.tscn" id="1_230it"] +[ext_resource type="Script" uid="uid://dsq7jynbrdcgu" path="res://addons/godot_rl_agents/sync.gd" id="2_uhjbv"] + +[node name="TestScene" type="Node2D"] + +[node name="AsteroidGameScene" parent="." instance=ExtResource("1_230it")] +override_can_shoot_always_enabled = true + +[node name="Camera2D" type="Camera2D" parent="."] +position = Vector2(960, 540) + +[node name="Sync" type="Node" parent="."] +script = ExtResource("2_uhjbv") +control_mode = 2 +action_repeat = 4 +speed_up = 3.0 +onnx_model_path = "model.onnx" +deterministic_inference = false diff --git a/examples/Ship2D/scenes/test_scene/test_scene_boss.tscn b/examples/Ship2D/scenes/test_scene/test_scene_boss.tscn new file mode 100644 index 0000000..b481000 --- /dev/null +++ b/examples/Ship2D/scenes/test_scene/test_scene_boss.tscn @@ -0,0 +1,20 @@ +[gd_scene load_steps=3 format=3 uid="uid://dmumxjvt7qg0s"] + +[ext_resource type="PackedScene" uid="uid://cav0l7p55yibs" path="res://scenes/game_scenes/boss_game_scene/boss_game_scene.tscn" id="1_3prfu"] +[ext_resource type="Script" uid="uid://dsq7jynbrdcgu" path="res://addons/godot_rl_agents/sync.gd" id="2_qmijl"] + +[node name="TestScene" type="Node2D"] + +[node name="BossGameScene" parent="." instance=ExtResource("1_3prfu")] +override_can_shoot_always_enabled = true + +[node name="Camera2D" type="Camera2D" parent="."] +position = Vector2(960, 540) + +[node name="Sync" type="Node" parent="."] +script = ExtResource("2_qmijl") +control_mode = 2 +action_repeat = 4 +speed_up = 100.0 +onnx_model_path = "model.onnx" +deterministic_inference = false diff --git a/examples/Ship2D/scenes/train_scene/training_scene_asteroids.tscn b/examples/Ship2D/scenes/train_scene/training_scene_asteroids.tscn index a9ec9ff..994e49d 100644 --- a/examples/Ship2D/scenes/train_scene/training_scene_asteroids.tscn +++ b/examples/Ship2D/scenes/train_scene/training_scene_asteroids.tscn @@ -1,7 +1,7 @@ [gd_scene load_steps=3 format=3 uid="uid://ji4spgoalcj6"] [ext_resource type="PackedScene" uid="uid://bswnfo2l0387l" path="res://scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.tscn" id="1_qvwyi"] -[ext_resource type="Script" uid="uid://dwyruq0ulo56c" path="res://addons/godot_rl_agents/sync.gd" id="2_rgi60"] +[ext_resource type="Script" uid="uid://dsq7jynbrdcgu" path="res://addons/godot_rl_agents/sync.gd" id="2_rgi60"] [node name="TrainingScene" type="Node2D"] @@ -9,36 +9,56 @@ override_can_shoot_always_enabled = true [node name="SubViewport" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 [node name="BaseGameScene" parent="SubViewport" instance=ExtResource("1_qvwyi")] [node name="SubViewport2" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 [node name="BaseGameScene" parent="SubViewport2" instance=ExtResource("1_qvwyi")] override_can_shoot_always_enabled = true [node name="SubViewport3" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 [node name="BaseGameScene" parent="SubViewport3" instance=ExtResource("1_qvwyi")] [node name="SubViewport4" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 [node name="BaseGameScene" parent="SubViewport4" instance=ExtResource("1_qvwyi")] override_can_shoot_always_enabled = true [node name="SubViewport5" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 [node name="BaseGameScene" parent="SubViewport5" instance=ExtResource("1_qvwyi")] [node name="SubViewport6" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 [node name="BaseGameScene" parent="SubViewport6" instance=ExtResource("1_qvwyi")] override_can_shoot_always_enabled = true [node name="SubViewport7" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 [node name="BaseGameScene" parent="SubViewport7" instance=ExtResource("1_qvwyi")] +[node name="SubViewport8" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="BaseGameScene" parent="SubViewport8" instance=ExtResource("1_qvwyi")] + [node name="Sync" type="Node" parent="."] script = ExtResource("2_rgi60") action_repeat = 4 diff --git a/examples/Ship2D/scenes/train_scene/training_scene_boss.tscn b/examples/Ship2D/scenes/train_scene/training_scene_boss.tscn index ff98442..b7d5839 100644 --- a/examples/Ship2D/scenes/train_scene/training_scene_boss.tscn +++ b/examples/Ship2D/scenes/train_scene/training_scene_boss.tscn @@ -1,53 +1,61 @@ [gd_scene load_steps=3 format=3 uid="uid://2uwyckkppxmy"] [ext_resource type="PackedScene" uid="uid://cav0l7p55yibs" path="res://scenes/game_scenes/boss_game_scene/boss_game_scene.tscn" id="1_bi1es"] -[ext_resource type="Script" uid="uid://dwyruq0ulo56c" path="res://addons/godot_rl_agents/sync.gd" id="2_xghqd"] +[ext_resource type="Script" uid="uid://dsq7jynbrdcgu" path="res://addons/godot_rl_agents/sync.gd" id="2_xghqd"] [node name="TrainingSceneBoss" type="Node2D"] [node name="BossGameScene" parent="." instance=ExtResource("1_bi1es")] -override_can_shoot_always_enabled = false +override_can_shoot_always_enabled = true [node name="SubViewport" type="SubViewport" parent="."] +disable_3d = true canvas_cull_mask = 4294967294 [node name="BossGameScene" parent="SubViewport" instance=ExtResource("1_bi1es")] -override_can_shoot_always_enabled = false [node name="SubViewport2" type="SubViewport" parent="."] +disable_3d = true canvas_cull_mask = 4294967294 [node name="BossGameScene" parent="SubViewport2" instance=ExtResource("1_bi1es")] [node name="SubViewport3" type="SubViewport" parent="."] +disable_3d = true canvas_cull_mask = 4294967294 [node name="BossGameScene" parent="SubViewport3" instance=ExtResource("1_bi1es")] -override_can_shoot_always_enabled = false [node name="SubViewport4" type="SubViewport" parent="."] +disable_3d = true canvas_cull_mask = 4294967294 [node name="BossGameScene" parent="SubViewport4" instance=ExtResource("1_bi1es")] [node name="SubViewport5" type="SubViewport" parent="."] +disable_3d = true canvas_cull_mask = 4294967294 [node name="BossGameScene" parent="SubViewport5" instance=ExtResource("1_bi1es")] -override_can_shoot_always_enabled = false [node name="SubViewport6" type="SubViewport" parent="."] +disable_3d = true canvas_cull_mask = 4294967294 [node name="BossGameScene" parent="SubViewport6" instance=ExtResource("1_bi1es")] [node name="SubViewport7" type="SubViewport" parent="."] +disable_3d = true canvas_cull_mask = 4294967294 [node name="BossGameScene" parent="SubViewport7" instance=ExtResource("1_bi1es")] -override_can_shoot_always_enabled = false + +[node name="SubViewport8" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="BossGameScene" parent="SubViewport8" instance=ExtResource("1_bi1es")] [node name="Sync" type="Node" parent="."] script = ExtResource("2_xghqd") action_repeat = 4 -speed_up = 16.0 diff --git a/examples/Ship2D/scenes/train_scene/training_scene_mixed.tscn b/examples/Ship2D/scenes/train_scene/training_scene_mixed.tscn index 9864337..82b2019 100644 --- a/examples/Ship2D/scenes/train_scene/training_scene_mixed.tscn +++ b/examples/Ship2D/scenes/train_scene/training_scene_mixed.tscn @@ -1,53 +1,91 @@ [gd_scene load_steps=4 format=3 uid="uid://cwobnl1dd05uq"] [ext_resource type="PackedScene" uid="uid://cav0l7p55yibs" path="res://scenes/game_scenes/boss_game_scene/boss_game_scene.tscn" id="1_8q6pp"] -[ext_resource type="Script" uid="uid://dwyruq0ulo56c" path="res://addons/godot_rl_agents/sync.gd" id="2_ccsrb"] +[ext_resource type="Script" uid="uid://dsq7jynbrdcgu" path="res://addons/godot_rl_agents/sync.gd" id="2_ccsrb"] [ext_resource type="PackedScene" uid="uid://bswnfo2l0387l" path="res://scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.tscn" id="3_ccsrb"] [node name="TrainingSceneMixed" type="Node2D"] -[node name="Sync" type="Node" parent="."] -script = ExtResource("2_ccsrb") -action_repeat = 4 -speed_up = 16.0 - [node name="BossGameScene" parent="." instance=ExtResource("1_8q6pp")] +override_can_shoot_always_enabled = true [node name="AsteroidScenes" type="Node" parent="."] [node name="SubViewport" type="SubViewport" parent="AsteroidScenes"] +disable_3d = true +canvas_cull_mask = 4294967294 -[node name="BaseGameScene" parent="AsteroidScenes/SubViewport" instance=ExtResource("3_ccsrb")] +[node name="AsteroidGameScene" parent="AsteroidScenes/SubViewport" instance=ExtResource("3_ccsrb")] [node name="SubViewport2" type="SubViewport" parent="AsteroidScenes"] +disable_3d = true +canvas_cull_mask = 4294967294 -[node name="BaseGameScene" parent="AsteroidScenes/SubViewport2" instance=ExtResource("3_ccsrb")] -override_can_shoot_always_enabled = true +[node name="AsteroidGameScene" parent="AsteroidScenes/SubViewport2" instance=ExtResource("3_ccsrb")] [node name="SubViewport3" type="SubViewport" parent="AsteroidScenes"] +disable_3d = true +canvas_cull_mask = 4294967294 -[node name="BaseGameScene" parent="AsteroidScenes/SubViewport3" instance=ExtResource("3_ccsrb")] +[node name="AsteroidGameScene" parent="AsteroidScenes/SubViewport3" instance=ExtResource("3_ccsrb")] [node name="SubViewport4" type="SubViewport" parent="AsteroidScenes"] +disable_3d = true +canvas_cull_mask = 4294967294 -[node name="BaseGameScene" parent="AsteroidScenes/SubViewport4" instance=ExtResource("3_ccsrb")] -override_can_shoot_always_enabled = true +[node name="AsteroidGameScene" parent="AsteroidScenes/SubViewport4" instance=ExtResource("3_ccsrb")] + +[node name="SubViewport5" type="SubViewport" parent="AsteroidScenes"] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AsteroidGameScene" parent="AsteroidScenes/SubViewport5" instance=ExtResource("3_ccsrb")] + +[node name="SubViewport6" type="SubViewport" parent="AsteroidScenes"] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="AsteroidGameScene" parent="AsteroidScenes/SubViewport6" instance=ExtResource("3_ccsrb")] [node name="BossScenes" type="Node" parent="."] [node name="SubViewport" type="SubViewport" parent="BossScenes"] +disable_3d = true canvas_cull_mask = 4294967294 [node name="BossGameScene" parent="BossScenes/SubViewport" instance=ExtResource("1_8q6pp")] -override_can_shoot_always_enabled = false [node name="SubViewport2" type="SubViewport" parent="BossScenes"] +disable_3d = true canvas_cull_mask = 4294967294 [node name="BossGameScene" parent="BossScenes/SubViewport2" instance=ExtResource("1_8q6pp")] [node name="SubViewport3" type="SubViewport" parent="BossScenes"] +disable_3d = true canvas_cull_mask = 4294967294 [node name="BossGameScene" parent="BossScenes/SubViewport3" instance=ExtResource("1_8q6pp")] -override_can_shoot_always_enabled = false + +[node name="SubViewport4" type="SubViewport" parent="BossScenes"] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="BossGameScene" parent="BossScenes/SubViewport4" instance=ExtResource("1_8q6pp")] + +[node name="SubViewport5" type="SubViewport" parent="BossScenes"] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="BossGameScene" parent="BossScenes/SubViewport5" instance=ExtResource("1_8q6pp")] + +[node name="SubViewport6" type="SubViewport" parent="BossScenes"] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="BossGameScene" parent="BossScenes/SubViewport6" instance=ExtResource("1_8q6pp")] + +[node name="Sync" type="Node" parent="."] +script = ExtResource("2_ccsrb") +action_repeat = 4 +speed_up = 16.0 From ce5e264f28f598d710009f3bec070f635ac871b5 Mon Sep 17 00:00:00 2001 From: Ivan-267 <61947090+Ivan-267@users.noreply.github.com> Date: Sat, 25 Oct 2025 09:18:00 +0200 Subject: [PATCH 7/8] Adjust inference speed, remove debug text --- examples/Ship2D/scenes/ship/ship.gd | 3 --- examples/Ship2D/scenes/test_scene/test_scene_asteroids.tscn | 1 - examples/Ship2D/scenes/test_scene/test_scene_boss.tscn | 1 - 3 files changed, 5 deletions(-) diff --git a/examples/Ship2D/scenes/ship/ship.gd b/examples/Ship2D/scenes/ship/ship.gd index 2b31552..e86fc6c 100644 --- a/examples/Ship2D/scenes/ship/ship.gd +++ b/examples/Ship2D/scenes/ship/ship.gd @@ -95,6 +95,3 @@ func hit(): # this adds a "protection period" after getting hit if _time_survived > 1.0: game_over(-50.0) - print("hit_by_boss") - else: - print("hit before 1sec") diff --git a/examples/Ship2D/scenes/test_scene/test_scene_asteroids.tscn b/examples/Ship2D/scenes/test_scene/test_scene_asteroids.tscn index c0aa857..4325896 100644 --- a/examples/Ship2D/scenes/test_scene/test_scene_asteroids.tscn +++ b/examples/Ship2D/scenes/test_scene/test_scene_asteroids.tscn @@ -15,6 +15,5 @@ position = Vector2(960, 540) script = ExtResource("2_uhjbv") control_mode = 2 action_repeat = 4 -speed_up = 3.0 onnx_model_path = "model.onnx" deterministic_inference = false diff --git a/examples/Ship2D/scenes/test_scene/test_scene_boss.tscn b/examples/Ship2D/scenes/test_scene/test_scene_boss.tscn index b481000..cfd35ff 100644 --- a/examples/Ship2D/scenes/test_scene/test_scene_boss.tscn +++ b/examples/Ship2D/scenes/test_scene/test_scene_boss.tscn @@ -15,6 +15,5 @@ position = Vector2(960, 540) script = ExtResource("2_qmijl") control_mode = 2 action_repeat = 4 -speed_up = 100.0 onnx_model_path = "model.onnx" deterministic_inference = false From f37089418e723309370b79e6ce947e0c69dd74d8 Mon Sep 17 00:00:00 2001 From: Ivan-267 <61947090+Ivan-267@users.noreply.github.com> Date: Sun, 26 Oct 2025 05:41:58 +0100 Subject: [PATCH 8/8] Enhance Ship2D readme with updates and corrections Updated the readme with new scene links, improved observation details, and adjusted hyperparameters for training. --- examples/Ship2D/readme.md | 52 +++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/examples/Ship2D/readme.md b/examples/Ship2D/readme.md index 099faae..c4107c7 100644 --- a/examples/Ship2D/readme.md +++ b/examples/Ship2D/readme.md @@ -1,9 +1,12 @@ # Ship2D example environment +### Boss scene: +https://github.com/user-attachments/assets/72856bb0-a935-44a5-b2a5-9ef524d56a8a -## Goal: -The ship needs to avoid asteroids for as long as possible. -The agent is trained on two scenarios, one where shooting the asteroids is possible, and one with shooting disabled. +### Asteroid scene: +https://github.com/user-attachments/assets/22d36378-885b-4d4c-848e-f7fd90970bbe + +For both scenes, the agent is trained with two variations, player ship shooting enabled and disabled (in which case avoidance is the main goal). ## Observations: ```gdscript @@ -14,6 +17,7 @@ func get_raycast_obs() -> Array[float]: return obs +#var previous_raycast_obs: Array func get_obs() -> Dictionary: var obs: Array[float] @@ -22,15 +26,19 @@ func get_obs() -> Dictionary: obs.append_array(raycast_obs) var velocity_x = clampf(player.get_real_velocity().x / 3000, -1.0, 1.0) - - obs.append_array([ - float(player.can_shoot), - clampf(player.time_since_ball_spawned / player.ball_fire_interval_seconds, 0, 1.0), - velocity_x - ]) + + obs.append_array( + [ + float(player.can_shoot), + clampf(player.time_since_projectile_spawned / + player.projectile_fire_interval_seconds, 0, 1.0), + velocity_x + ] + ) return {"obs": obs} ``` +Note: In this updated version, there is also a shapecast based sensor for detecting bullets fired by the boss ship among the `raycast_sensors`, even though it's not technically a raycast sensor. ## Actions: ```python @@ -43,7 +51,7 @@ func get_action_space() -> Dictionary: ## Running inference: -If you’d just like to test the env using the pre-trained onnx model, open `res://scenes/test_scene/test_scene.tscn` in Godot, then press `F6`. +If you’d just like to test the env using the pre-trained onnx model, open a test scene found in `res://scenes/test_scene` in Godot, then press `F6`. You can control the shooting ability of the ship by turning on or off the `override can shoot always enabled` property of `BaseGameScene` in the inspector. @@ -56,16 +64,18 @@ There’s an included onnx file that was trained using SB3. The hyperparameters in the example script were adjusted: ```Python - learning_rate = 0.0003 if not args.linear_lr_schedule else linear_schedule(0.0003) + learning_rate = 0.0002 if not args.linear_lr_schedule else linear_schedule(0.0003) model: PPO = PPO( "MultiInputPolicy", env, verbose=2, - n_steps=128, - n_epochs=3, - tensorboard_log=args.experiment_dir, + n_steps=2048, + n_epochs=25, + batch_size=512 * env.num_envs, + target_kl=0.006, + stats_window_size=250, learning_rate=learning_rate, - target_kl=0.02 + tensorboard_log=args.experiment_dir, ) ``` @@ -73,14 +83,14 @@ CL arguments used (also onnx export and model saving was used, enable as needed, ```python --env_path=path_to_exported_game_executable ---speedup=16 ---n_parallel=4 +--speedup=100 +--n_parallel=1 --onnx_export_path=model.onnx --timesteps=30_000_000 ---linear_lr_schedule ``` - +Note: training was manually stopped after ~20m steps. Stats from the training session: -![training session rewards](https://github.com/user-attachments/assets/986fac4f-fcf9-470b-8809-c6018228ce43) -This environment was made by [Ivan-267](https://github.com/Ivan-267). \ No newline at end of file +training_stats + +This environment was made by [Ivan-267](https://github.com/Ivan-267).