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..06d928b --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_2d.gd @@ -0,0 +1,136 @@ +extends Node2D +class_name AIController2D + +enum ControlModes { + INHERIT_FROM_SYNC, ## Inherit setting from sync node + HUMAN, ## Test the environment manually + TRAINING, ## Train a model + ONNX_INFERENCE, ## Load a pretrained model using an .onnx file + RECORD_EXPERT_DEMOS ## Record observations and actions for expert demonstrations +} +@export var control_mode: ControlModes = ControlModes.INHERIT_FROM_SYNC +## The path to a trained .onnx model file to use for inference (overrides the path set in sync node). +@export var onnx_model_path := "" +## Once the number of steps has passed, the flag 'needs_reset' will be set to 'true' for this instance. +@export var reset_after := 1000 + +@export_group("Record expert demos mode options") +## Path where the demos will be saved. The file can later be used for imitation learning. +@export var expert_demo_save_path: String +## The action that erases the last recorded episode from the currently recorded data. +@export var remove_last_episode_key: InputEvent +## Action will be repeated for n frames. Will introduce control lag if larger than 1. +## Can be used to ensure that action_repeat on inference and training matches +## the recorded demonstrations. +@export var action_repeat: int = 1 + +@export_group("Multi-policy mode options") +## Allows you to set certain agents to use different policies. +## Changing has no effect with default SB3 training. Works with Rllib example. +## Tutorial: https://github.com/edbeeching/godot_rl_agents/blob/main/docs/TRAINING_MULTIPLE_POLICIES.md +@export var policy_name: String = "shared_policy" + +var onnx_model: ONNXModel + +var heuristic := "human" +var done := false +var reward := 0.0 +var n_steps := 0 +var needs_reset := false + +var _player: Node2D + + +func _ready(): + add_to_group("AGENT") + + +func init(player: Node2D): + _player = player + + +#region Methods that need implementing using the "extend script" option in Godot +func get_obs() -> Dictionary: + assert(false, "the get_obs method is not implemented when extending from ai_controller") + return {"obs": []} + + +func get_reward() -> float: + assert(false, "the get_reward method is not implemented when extending from ai_controller") + return 0.0 + + +func get_action_space() -> Dictionary: + assert( + false, "the get_action_space method is not implemented when extending from ai_controller" + ) + return { + "example_actions_continous": {"size": 2, "action_type": "continuous"}, + "example_actions_discrete": {"size": 2, "action_type": "discrete"}, + } + + +func set_action(action) -> void: + assert(false, "the set_action method is not implemented when extending from ai_controller") + + +#endregion + + +#region Methods that sometimes need implementing using the "extend script" option in Godot +# Only needed if you are recording expert demos with this AIController +func get_action() -> Array: + assert( + false, + "the get_action method is not implemented in extended AIController but demo_recorder is used" + ) + return [] + + +# For providing additional info (e.g. `is_success` for SB3 training) +func get_info() -> Dictionary: + return {} + + +#endregion + + +func _physics_process(delta): + n_steps += 1 + if n_steps > reset_after: + needs_reset = true + + +func get_obs_space(): + # may need overriding if the obs space is complex + var obs = get_obs() + return { + "obs": {"size": [len(obs["obs"])], "space": "box"}, + } + + +func reset(): + n_steps = 0 + needs_reset = false + + +func reset_if_done(): + if done: + reset() + + +func set_heuristic(h): + # sets the heuristic from "human" or "model" nothing to change here + heuristic = h + + +func get_done(): + return done + + +func set_done_false(): + done = false + + +func zero_reward(): + reward = 0.0 diff --git a/examples/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..c41de21 --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_2d.gd.uid @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000..61a0529 --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_3d.gd @@ -0,0 +1,136 @@ +extends Node3D +class_name AIController3D + +enum ControlModes { + INHERIT_FROM_SYNC, ## Inherit setting from sync node + HUMAN, ## Test the environment manually + TRAINING, ## Train a model + ONNX_INFERENCE, ## Load a pretrained model using an .onnx file + RECORD_EXPERT_DEMOS ## Record observations and actions for expert demonstrations +} +@export var control_mode: ControlModes = ControlModes.INHERIT_FROM_SYNC +## The path to a trained .onnx model file to use for inference (overrides the path set in sync node). +@export var onnx_model_path := "" +## Once the number of steps has passed, the flag 'needs_reset' will be set to 'true' for this instance. +@export var reset_after := 1000 + +@export_group("Record expert demos mode options") +## Path where the demos will be saved. The file can later be used for imitation learning. +@export var expert_demo_save_path: String +## The action that erases the last recorded episode from the currently recorded data. +@export var remove_last_episode_key: InputEvent +## Action will be repeated for n frames. Will introduce control lag if larger than 1. +## Can be used to ensure that action_repeat on inference and training matches +## the recorded demonstrations. +@export var action_repeat: int = 1 + +@export_group("Multi-policy mode options") +## Allows you to set certain agents to use different policies. +## Changing has no effect with default SB3 training. Works with Rllib example. +## Tutorial: https://github.com/edbeeching/godot_rl_agents/blob/main/docs/TRAINING_MULTIPLE_POLICIES.md +@export var policy_name: String = "shared_policy" + +var onnx_model: ONNXModel + +var heuristic := "human" +var done := false +var reward := 0.0 +var n_steps := 0 +var needs_reset := false + +var _player: Node3D + + +func _ready(): + add_to_group("AGENT") + + +func init(player: Node3D): + _player = player + + +#region Methods that need implementing using the "extend script" option in Godot +func get_obs() -> Dictionary: + assert(false, "the get_obs method is not implemented when extending from ai_controller") + return {"obs": []} + + +func get_reward() -> float: + assert(false, "the get_reward method is not implemented when extending from ai_controller") + return 0.0 + + +func get_action_space() -> Dictionary: + assert( + false, "the get_action_space method is not implemented when extending from ai_controller" + ) + return { + "example_actions_continous": {"size": 2, "action_type": "continuous"}, + "example_actions_discrete": {"size": 2, "action_type": "discrete"}, + } + + +func set_action(action) -> void: + assert(false, "the set_action method is not implemented when extending from ai_controller") + + +#endregion + + +#region Methods that sometimes need implementing using the "extend script" option in Godot +# Only needed if you are recording expert demos with this AIController +func get_action() -> Array: + assert( + false, + "the get_action method is not implemented in extended AIController but demo_recorder is used" + ) + return [] + + +# For providing additional info (e.g. `is_success` for SB3 training) +func get_info() -> Dictionary: + return {} + + +#endregion + + +func _physics_process(delta): + n_steps += 1 + if n_steps > reset_after: + needs_reset = true + + +func get_obs_space(): + # may need overriding if the obs space is complex + var obs = get_obs() + return { + "obs": {"size": [len(obs["obs"])], "space": "box"}, + } + + +func reset(): + n_steps = 0 + needs_reset = false + + +func reset_if_done(): + if done: + reset() + + +func set_heuristic(h): + # sets the heuristic from "human" or "model" nothing to change here + heuristic = h + + +func get_done(): + return done + + +func set_done_false(): + done = false + + +func zero_reward(): + reward = 0.0 diff --git a/examples/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..3345e9d --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/controller/ai_controller_3d.gd.uid @@ -0,0 +1 @@ +uid://bs4t5ytckrlem 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/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/icon.png b/examples/Ship2D/addons/godot_rl_agents/icon.png new file mode 100644 index 0000000..fd8190e Binary files /dev/null and b/examples/Ship2D/addons/godot_rl_agents/icon.png differ 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..ec845ed --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs @@ -0,0 +1,115 @@ +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.Dictionary> obs, int state_ins) + { + //Current model: Any (Godot Rl Agents) + //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 + + var modelInputsList = new List + { + 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 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(modelInputsList, 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/ONNXInference.cs.uid b/examples/Ship2D/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs.uid new file mode 100644 index 0000000..e701913 --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs.uid @@ -0,0 +1 @@ +uid://dlwgh87fsvfk7 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/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/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..7d29a03 --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd @@ -0,0 +1,51 @@ +extends Resource +class_name ONNXModel +var inferencer_script = load("res://addons/godot_rl_agents/onnx/csharp/ONNXInference.cs") + +var inferencer = null + +## How many action values the model outputs +var action_output_size: int + +## Used to differentiate models +## that only output continuous action mean (e.g. sb3, cleanrl export) +## versus models that output mean and logstd (e.g. rllib export) +var action_means_only: bool + +## Whether action_means_value has been set already for this model +var action_means_only_set: bool + +# Must provide the path to the model and the batch size +func _init(model_path, batch_size): + inferencer = inferencer_script.new() + action_output_size = inferencer.Initialize(model_path, batch_size) + +# This function is the one that will be called from the game, +# requires the 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 {} + 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/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..5fbb2c4 --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd.uid @@ -0,0 +1 @@ +uid://c5moyrpqt6o13 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/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 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/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 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/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/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.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.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 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/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 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/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/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 new file mode 100644 index 0000000..c263637 --- /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 := 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(): + 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.gd.uid b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd.uid new file mode 100644 index 0000000..1a8ab8d --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd.uid @@ -0,0 +1 @@ +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 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.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 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..8428976 --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/sync.gd @@ -0,0 +1,621 @@ +extends Node +class_name Sync + +# --fixed-fps 2000 --disable-render-loop + +enum ControlModes { + HUMAN, ## Test the environment manually + TRAINING, ## Train a model + ONNX_INFERENCE ## Load a pretrained model using an .onnx file +} +@export var control_mode: ControlModes = ControlModes.TRAINING +## Action will be repeated for n frames (Godot physics steps). +@export_range(1, 10, 1, "or_greater") var action_repeat := 8 +## Speeds up the physics in the environment to enable faster training. +@export_range(0, 10, 0.1, "or_greater") var speed_up := 1.0 +## The path to a trained .onnx model file to use for inference (only needed for the 'Onnx Inference' control mode). +@export var onnx_model_path := "" +## 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 + +@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], 1.0) + var action_dict = _extract_action_dict( + action["output"], _action_space_inference[agent_id], model.action_means_only + ) + actions.append(action_dict) + + _set_agent_actions(actions, agents_inference) + _reset_agents_if_done(agents_inference) + get_tree().set_pause(false) + + +func _demo_record_process(): + if not agent_demo_record: + return + + if Input.is_action_just_pressed("RemoveLastDemoEpisode"): + print("[Sync script][Demo recorder] Removing last recorded episode.") + demo_trajectories.remove_at(demo_trajectories.size() - 1) + print("Remaining episode count: %d" % demo_trajectories.size()) + + if n_action_steps % agent_demo_record.action_repeat != 0: + return + + var obs_dict: Dictionary = agent_demo_record.get_obs() + + # Get the current obs from the agent + assert( + obs_dict.has("obs"), + "Demo recorder needs an 'obs' key in get_obs() returned dictionary to record obs from." + ) + current_demo_trajectory[0].append(obs_dict.obs) + + # Get the action applied for the current obs from the agent + agent_demo_record.set_action() + var acts = agent_demo_record.get_action() + + var terminal = agent_demo_record.get_done() + # Record actions only for non-terminal states + if terminal: + agent_demo_record.set_done_false() + else: + current_demo_trajectory[1].append(acts) + + if terminal: + #current_demo_trajectory[2].append(true) + demo_trajectories.append(current_demo_trajectory.duplicate(true)) + print("[Sync script][Demo recorder] Recorded episode count: %d" % demo_trajectories.size()) + current_demo_trajectory[0].clear() + current_demo_trajectory[1].clear() + + +func _heuristic_process(): + for agent in agents_heuristic: + _reset_agents_if_done(agents_heuristic) + + +func _extract_action_dict(action_array: Array, action_space: Dictionary, action_means_only: bool): + var index = 0 + var result = {} + for key in action_space.keys(): + var size = action_space[key]["size"] + var action_type = action_space[key]["action_type"] + + if action_type == "discrete": + var largest_logit: float = -INF # Value of the largest logit for this action in the actions array + var largest_logit_idx: int # 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 + 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 + 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/addons/godot_rl_agents/sync.gd.uid b/examples/Ship2D/addons/godot_rl_agents/sync.gd.uid new file mode 100644 index 0000000..4aa605d --- /dev/null +++ b/examples/Ship2D/addons/godot_rl_agents/sync.gd.uid @@ -0,0 +1 @@ +uid://dsq7jynbrdcgu diff --git a/examples/Ship2D/assets/Background.png b/examples/Ship2D/assets/Background.png new file mode 100644 index 0000000..8295069 Binary files /dev/null and b/examples/Ship2D/assets/Background.png differ diff --git a/examples/Ship2D/assets/Ship.png b/examples/Ship2D/assets/Ship.png new file mode 100644 index 0000000..b31ba71 Binary files /dev/null and b/examples/Ship2D/assets/Ship.png differ diff --git a/examples/Ship2D/assets/ShipEnemy.png b/examples/Ship2D/assets/ShipEnemy.png new file mode 100644 index 0000000..e44d9b3 Binary files /dev/null and b/examples/Ship2D/assets/ShipEnemy.png differ diff --git a/examples/Ship2D/assets/asset_license.md b/examples/Ship2D/assets/asset_license.md new file mode 100644 index 0000000..90267aa --- /dev/null +++ b/examples/Ship2D/assets/asset_license.md @@ -0,0 +1,3 @@ +The following license is only for the graphical assets in the folder "assets", specifically .png and .svg files: +Author: Ivan Dodic (https://github.com/Ivan-267), +License: https://creativecommons.org/licenses/by/4.0/ \ No newline at end of file diff --git a/examples/Ship2D/assets/asteroid.png b/examples/Ship2D/assets/asteroid.png new file mode 100644 index 0000000..425f87c Binary files /dev/null and b/examples/Ship2D/assets/asteroid.png differ diff --git a/examples/Ship2D/assets/asteroid2.png b/examples/Ship2D/assets/asteroid2.png new file mode 100644 index 0000000..282f83b Binary files /dev/null and b/examples/Ship2D/assets/asteroid2.png differ diff --git a/examples/Ship2D/assets/asteroid3.png b/examples/Ship2D/assets/asteroid3.png new file mode 100644 index 0000000..3159a2c Binary files /dev/null and b/examples/Ship2D/assets/asteroid3.png differ diff --git a/examples/Ship2D/assets/asteroid4.png b/examples/Ship2D/assets/asteroid4.png new file mode 100644 index 0000000..d16b84d Binary files /dev/null and b/examples/Ship2D/assets/asteroid4.png differ diff --git a/examples/Ship2D/assets/asteroid5.png b/examples/Ship2D/assets/asteroid5.png new file mode 100644 index 0000000..a8ba775 Binary files /dev/null and b/examples/Ship2D/assets/asteroid5.png differ diff --git a/examples/Ship2D/assets/asteroid6.png b/examples/Ship2D/assets/asteroid6.png new file mode 100644 index 0000000..2c31bc6 Binary files /dev/null and b/examples/Ship2D/assets/asteroid6.png differ 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/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/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 0000000..c8ff618 Binary files /dev/null and b/examples/Ship2D/model.onnx differ diff --git a/examples/Ship2D/project.godot b/examples/Ship2D/project.godot new file mode 100644 index 0000000..f4d3d52 --- /dev/null +++ b/examples/Ship2D/project.godot @@ -0,0 +1,66 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Ships2D" +run/main_scene="uid://cwobnl1dd05uq" +config/features=PackedStringArray("4.5", "C#", "Forward Plus") +config/icon="res://icon.svg" + +[display] + +window/size/viewport_width=1920 +window/size/viewport_height=1080 +window/size/window_width_override=960 +window/size/window_height_override=540 +window/stretch/mode="viewport" + +[dotnet] + +project/assembly_name="ships2d" + +[editor_plugins] + +enabled=PackedStringArray() + +[input] + +shoot={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null) +] +} +move_left={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +move_right={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} + +[layer_names] + +2d_physics/layer_1="Wall" +2d_physics/layer_2="Asteroid" +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=100 +2d/default_gravity=0.0 diff --git a/examples/Ship2D/readme.md b/examples/Ship2D/readme.md new file mode 100644 index 0000000..c4107c7 --- /dev/null +++ b/examples/Ship2D/readme.md @@ -0,0 +1,96 @@ +# Ship2D example environment + +### Boss scene: +https://github.com/user-attachments/assets/72856bb0-a935-44a5-b2a5-9ef524d56a8a + +### 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 +func get_raycast_obs() -> Array[float]: + var obs: Array[float] + for sensor in raycast_sensors: + obs.append_array(sensor.get_observation()) + return obs + + +#var previous_raycast_obs: Array +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_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 +func get_action_space() -> Dictionary: + return { + "move": {"size": 3, "action_type": "discrete"}, + "shoot": {"size": 2, "action_type": "discrete"}, + } +``` + +## Running inference: + +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. + +![image](https://github.com/user-attachments/assets/f61b4e1d-108f-4157-9089-ae5345675e22) + +## Training: + +There’s an included onnx file that was trained using SB3. + +The hyperparameters in the example script were adjusted: + +```Python + learning_rate = 0.0002 if not args.linear_lr_schedule else linear_schedule(0.0003) + model: PPO = PPO( + "MultiInputPolicy", + env, + verbose=2, + n_steps=2048, + n_epochs=25, + batch_size=512 * env.num_envs, + target_kl=0.006, + stats_window_size=250, + learning_rate=learning_rate, + tensorboard_log=args.experiment_dir, + ) +``` + +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 +--env_path=path_to_exported_game_executable +--speedup=100 +--n_parallel=1 +--onnx_export_path=model.onnx +--timesteps=30_000_000 +``` +Note: training was manually stopped after ~20m steps. +Stats from the training session: + +training_stats + +This environment was made by [Ivan-267](https://github.com/Ivan-267). diff --git a/examples/Ship2D/scenes/asteroid/asteroid.gd b/examples/Ship2D/scenes/asteroid/asteroid.gd new file mode 100644 index 0000000..9851641 --- /dev/null +++ b/examples/Ship2D/scenes/asteroid/asteroid.gd @@ -0,0 +1,89 @@ +extends Area2D +class_name Asteroid + +@export var downward_speed_min := 500.0 +@export var downward_speed_max := 900.0 + +var _downward_speed: float +var _sideways_speed: float +var _active_sprite: Sprite2D +var _sprite_rotation = randf_range(-5, 5) + +@onready var particles := $GPUParticles2D +@onready var _sprites: Array = $Sprites.get_children() + + +func _ready() -> 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 + + +## Called when an asteroid is hit +func hit(): + destroy() + + +## 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: + 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 new file mode 100644 index 0000000..7dd8342 --- /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" 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 +_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"] +_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 + +[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 +texture = ExtResource("3_danrt") +one_shot = true +explosiveness = 1.0 +process_material = SubResource("ParticleProcessMaterial_2m1fr") + +[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..4905b4e --- /dev/null +++ b/examples/Ship2D/scenes/asteroid/asteroid_spawner.gd @@ -0,0 +1,34 @@ +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 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.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 new file mode 100644 index 0000000..256fc47 --- /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" 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"] +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/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..e7ca7a9 --- /dev/null +++ b/examples/Ship2D/scenes/game_scenes/asteroid_game_scene/asteroid_game_scene.gd @@ -0,0 +1,2 @@ +extends BaseGameScene +class_name AsteroidGameScene 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/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.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/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/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..ad07432 --- /dev/null +++ b/examples/Ship2D/scenes/game_scenes/boss_game_scene/boss_game_scene.gd @@ -0,0 +1,2 @@ +extends BaseGameScene +class_name BossGameScene 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..c8615f2 --- /dev/null +++ b/examples/Ship2D/scenes/game_scenes/boss_game_scene/boss_game_scene.tscn @@ -0,0 +1,79 @@ +[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") + +[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") +projectile_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 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/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.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 new file mode 100644 index 0000000..07c95eb --- /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/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"] +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..9566c78 --- /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/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"] +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/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 new file mode 100644 index 0000000..e86fc6c --- /dev/null +++ b/examples/Ship2D/scenes/ship/ship.gd @@ -0,0 +1,97 @@ +extends CharacterBody2D +class_name Ship + +@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 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_projectile_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_projectile_spawned += delta + if can_shoot and requested_shoot: + handle_shoot() + + +func handle_shoot(): + 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: + _time_survived = 0 + spawned_objects.remove_all_spawned_items() + ai_controller.end_episode(final_reward) + reposition_player() + velocity = Vector2.ZERO + + +func reposition_player(): + global_transform = _initial_transform + 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 +func hit_by_asteroid() -> void: + # 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(-5.0) + + +## Called by a projectile instance that has hit an asteroid +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(): + # 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) 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 new file mode 100644 index 0000000..97111e5 --- /dev/null +++ b/examples/Ship2D/scenes/ship/ship.tscn @@ -0,0 +1,75 @@ +[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/projectile_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 + +[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") +projectile_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"), NodePath("RaycastSensorEnemyShip"), NodePath("EnemyProjectileSensor")] +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="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 +enabled = 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") + +[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..8ad1718 --- /dev/null +++ b/examples/Ship2D/scenes/ship/ship_ai_controller.gd @@ -0,0 +1,79 @@ +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 + + +#var previous_raycast_obs: Array +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_projectile_spawned / + player.projectile_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/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..b324799 --- /dev/null +++ b/examples/Ship2D/scenes/ship_enemy_boss/boss.gd @@ -0,0 +1,96 @@ +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.defeated_boss_ship() + player_ship.hit_enemy_boss_ship() + + +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 new file mode 100644 index 0000000..4da821c --- /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" 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"] + +[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/test_scene/test_scene_asteroids.tscn b/examples/Ship2D/scenes/test_scene/test_scene_asteroids.tscn new file mode 100644 index 0000000..4325896 --- /dev/null +++ b/examples/Ship2D/scenes/test_scene/test_scene_asteroids.tscn @@ -0,0 +1,19 @@ +[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 +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..cfd35ff --- /dev/null +++ b/examples/Ship2D/scenes/test_scene/test_scene_boss.tscn @@ -0,0 +1,19 @@ +[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 +onnx_model_path = "model.onnx" +deterministic_inference = false 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/scenes/train_scene/training_scene_asteroids.tscn b/examples/Ship2D/scenes/train_scene/training_scene_asteroids.tscn new file mode 100644 index 0000000..994e49d --- /dev/null +++ b/examples/Ship2D/scenes/train_scene/training_scene_asteroids.tscn @@ -0,0 +1,65 @@ +[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://dsq7jynbrdcgu" 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="."] +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 +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..b7d5839 --- /dev/null +++ b/examples/Ship2D/scenes/train_scene/training_scene_boss.tscn @@ -0,0 +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://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 = true + +[node name="SubViewport" type="SubViewport" parent="."] +disable_3d = true +canvas_cull_mask = 4294967294 + +[node name="BossGameScene" parent="SubViewport" instance=ExtResource("1_bi1es")] + +[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")] + +[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")] + +[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")] + +[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 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..82b2019 --- /dev/null +++ b/examples/Ship2D/scenes/train_scene/training_scene_mixed.tscn @@ -0,0 +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://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="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="AsteroidGameScene" parent="AsteroidScenes/SubViewport" instance=ExtResource("3_ccsrb")] + +[node name="SubViewport2" type="SubViewport" parent="AsteroidScenes"] +disable_3d = true +canvas_cull_mask = 4294967294 + +[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="AsteroidGameScene" parent="AsteroidScenes/SubViewport3" instance=ExtResource("3_ccsrb")] + +[node name="SubViewport4" type="SubViewport" parent="AsteroidScenes"] +disable_3d = true +canvas_cull_mask = 4294967294 + +[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")] + +[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")] + +[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 diff --git a/examples/Ship2D/ships2d.csproj b/examples/Ship2D/ships2d.csproj new file mode 100644 index 0000000..01706d4 --- /dev/null +++ b/examples/Ship2D/ships2d.csproj @@ -0,0 +1,9 @@ + + + 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