mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-18 11:22:01 +00:00
Compare commits
5 Commits
c3329b3280
...
379ef9910e
| Author | SHA1 | Date | |
|---|---|---|---|
| 379ef9910e | |||
| 85f96eb15e | |||
| 328831df1f | |||
| 14a8907038 | |||
| 2772087123 |
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
name: godot
|
||||||
|
description: Runs all godot-mcp tool interactions (reading/editing scenes, running the project, in-game verification, screenshots, debug output) on Haiku to keep verbose Godot tool outputs out of the main model's context and reduce cost. Give it a concrete, self-contained task and tell it exactly what to report back.
|
||||||
|
tools: mcp__godot-mcp__*, Read, Grep, Glob, Bash
|
||||||
|
model: haiku
|
||||||
|
---
|
||||||
|
|
||||||
|
You operate the Godot project at `Game/` (repo root: the parent directory) via the godot-mcp tools. Prefer godot-mcp tools over shell commands or manual file parsing for anything involving scenes, nodes, scripts, the input map, or the running game.
|
||||||
|
|
||||||
|
Ground rules:
|
||||||
|
- The Godot executable is at `/Applications/Godot.app/Contents/MacOS/Godot` if you need the CLI (e.g. headless runs); otherwise use the MCP tools.
|
||||||
|
- `get_debug_output` and `stop_project` can return enormous logs — never dump them into your report. Grep/filter for the relevant lines and quote only those.
|
||||||
|
- When verifying gameplay, follow the existing patterns: interact via input actions (`game_key_press` with action names from `project.godot`), inspect state with `game_get_property`/`game_get_scene_tree`, and check `game_get_errors`.
|
||||||
|
- Always stop a running project before finishing unless told otherwise.
|
||||||
|
- Report back exactly what was asked: the outcome, key evidence (short quotes, property values), and any errors — not raw tool output.
|
||||||
@@ -1 +1,8 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# RL training artifacts (training/ code is committed; outputs are not)
|
||||||
|
training/.venv/
|
||||||
|
training/logs/
|
||||||
|
training/checkpoints/
|
||||||
|
training/smoke_run.log
|
||||||
|
training/__pycache__/
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "mcp/godot-mcp"]
|
||||||
|
path = mcp/godot-mcp
|
||||||
|
url = https://github.com/tugcantopaloglu/godot-mcp.git
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"godot-mcp": {
|
||||||
|
"command": "node",
|
||||||
|
"args": ["mcp/godot-mcp/build/index.js"],
|
||||||
|
"env": {
|
||||||
|
"GODOT_PATH": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
Important rule: never create co-authored commits. Never mention Claude in commits.
|
||||||
|
|
||||||
|
## Project overview
|
||||||
|
|
||||||
|
Cosmic Clash is an open-source, physics-based "vehicle soccer" game (a spiritual successor to Rocket League) built in Godot 4.7, using space ships instead of cars. The project is GDScript/Godot only right now — the "C# backend" mentioned in README.md is planned but not yet started. There is no server-side code; the MVP is local-only play against bots.
|
||||||
|
|
||||||
|
Because the gameplay concept (vehicle soccer) can't be copyrighted but specific expression can, all code/art/assets must be original — this is why the project uses Godot instead of Unreal/Unity and ships instead of cars. Keep this in mind when writing code or pulling in assets: don't port or closely mirror Rocket League's actual implementation.
|
||||||
|
|
||||||
|
## Godot MCP server
|
||||||
|
|
||||||
|
This repo vendors [godot-mcp](https://github.com/tugcantopaloglu/godot-mcp) as a git submodule at `mcp/godot-mcp` and registers it in `.mcp.json`. **Prefer the godot-mcp tools over manual file edits or shell commands** when the task involves inspecting or modifying the Godot project — reading/editing scenes, nodes, scripts, running the project, or interacting with a live Godot editor/runtime instance. It understands Godot's scene tree and `.tscn`/`.gd` structures directly, which is more reliable than hand-parsing them.
|
||||||
|
|
||||||
|
Setup after cloning (submodules aren't checked out by default):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git submodule update --init --recursive
|
||||||
|
cd mcp/godot-mcp
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
`GODOT_PATH` (env var in `.mcp.json`) is left blank to auto-detect the Godot executable; set it explicitly if auto-detection fails on your machine.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
There is no build step, linter, or automated test suite for the GDScript project itself — Godot projects run directly from source.
|
||||||
|
|
||||||
|
- **Open the project**: open `Game/` as a project in the Godot 4.7 editor, or run `godot --path Game` from the repo root.
|
||||||
|
- **Run the game**: press Play in the editor, or `godot --path Game res://scenes/main_menu.tscn`.
|
||||||
|
- **Headless smoke test** (RL/CI precondition — the game must run without rendering): `godot --headless --path Game res://scenes/free_play.tscn`.
|
||||||
|
- The `mcp/godot-mcp` submodule is a separate Node/TypeScript project with its own `npm install` / `npm run build` (see above) — it is tooling, not part of the game itself.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The structure was deliberately chosen so an RL-trained AI opponent and, later, multiplayer bolt on without rework (see `TODO.md` for the deferred work). The three load-bearing seams are the controller abstraction, the arena/game-mode split, and code-driven spawning.
|
||||||
|
|
||||||
|
- **Scene flow**: `scenes/main_menu.tscn` (`main_menu.gd`, one handler per mode) → `scenes/free_play.tscn` (practice: no timer, R resets ball) or `scenes/match.tscn` (150s timer, per-team score, kickoff resets). Esc returns to the menu from either mode.
|
||||||
|
- **Controller seam (do not bypass)**: `Ship` (`scripts/ship.gd`, `RigidBody3D`) never reads `Input`. Each physics tick, `_integrate_forces` pulls one `ShipAction` (`scripts/ship_action.gd`: thrust `Vector3`, rotation `Vector3`, turbo `bool`, each axis -1..1) from its `ShipController` child (`scripts/ship_controller.gd`, base returns a zero action). `PlayerShipController` reads input actions; a future `AIShipController` (RL policy) or network-replication controller implements the same `get_action()` interface. A ship with no controller is inert but simulated. The ShipAction shape *is* the future RL action space — change it deliberately.
|
||||||
|
- **Arena vs game mode**: `scenes/arena_01.tscn` (`scripts/arena.gd`, group `"arena"`) is a stateless stadium — a setting (space-platform floor, starfield sky, lighting), an enclosing `Boundary` (instance of `objects/arena_boundary.tscn`: floor/walls/ceiling colliders), two `Goal` instances (team 0 and 1), `BallSpawn` and `SpawnsTeam0/1` Marker3Ds — queried via `get_ball_spawn()`/`get_ship_spawns(team)`/`get_goals()`. All arenas are a standard size: they instance the shared `arena_boundary.tscn`, and `scripts/arena_boundary.gd` (`ArenaBoundary`) holds the canonical play-volume constants (inner x ±12, z ±18, height 12, goal lines z ±17) that field-size logic must derive from instead of restating numbers. Game modes extend `GameMode` (`scripts/game_mode.gd`, group `"game"`): the mode's scene contains an Arena + HUD, and the mode spawns ball/ships/controllers/camera **in code** (`spawn_ship(team, index, controller)` etc.) so ship counts and controller mixes stay flexible. `free_play.gd` and `match_mode.gd` override `_start()` and `_on_goal_scored(conceding_team)`.
|
||||||
|
- **Goals are dumb sensors**: `scripts/goal.gd` (`Area3D`, group `"goal"`, `@export team`) only emits `goal_scored(team)` when a body in group `"ball"` enters; `GameMode` debounces it (`_handle_goal_scored`) and modes decide consequences. Never put scoring/reset logic in the goal.
|
||||||
|
- **Ship physics**: all movement is force/torque-based (`_integrate_forces`), not kinematic — inputs become world-space forces/torques relative to ship orientation, with manual drag and speed clamps per tick. Physics formulas are commented inline; see `FLIGHT_MANUAL.md` for the player-facing flight model. Physics properties (mass, inertia, friction material) live in `objects/ship.tscn`, not in `_ready` overrides — keep the scene truthful; RL tuning depends on it.
|
||||||
|
- **Camera** (`scenes/ship_camera_rig.tscn`, `scripts/ship_camera.gd`, group `"ship_camera"`) is spawned by the game mode and given a `target` ship — ships have no camera/HUD dependency, so headless RL runs work (`godot --headless`).
|
||||||
|
- **HUD / telemetry pattern**: `Ship` emits flight data via signals only when values change past thresholds (`_last_*` fields, `*_THRESHOLD` constants). `HUDController` (`scripts/HUDController.gd` on `scenes/HUD.tscn`, instanced by each mode's scene) discovers the ship, camera rig, and game mode via groups (`"ship"`, `"ship_camera"`, `"game"`), connects to signals, and only updates labels — no polling. Follow this discovery-by-group + signal-push pattern for new instruments or cross-node communication, not hardcoded `get_node` paths or per-frame polling.
|
||||||
|
- **Input actions** are defined in `Game/project.godot` under `[input]` (`move_forward`, `turn_left`, `turbo`, `reset_ball`, etc.) and read only by `PlayerShipController` (plus mode-level `_unhandled_input` for `reset_ball`/`ui_cancel`) — add new controls there rather than hardcoding key checks.
|
||||||
|
- Physics engine is Jolt (`Game/project.godot`, `[physics] 3d/physics_engine="Jolt Physics"`).
|
||||||
|
|
||||||
|
## Reinforcement learning / AI bots
|
||||||
|
|
||||||
|
See `TRAINING.md` for the full workflow (training, exporting, evaluating, difficulty tiers). Architecture summary:
|
||||||
|
|
||||||
|
- `scenes/training.tscn` (`scripts/training_mode.gd`, extends `GameMode`) is the headless self-play environment: two ships driven by `RLShipController`s, with `ShipAIController` (extends the vendored plugin's `AIController3D`) as the only class touching godot_rl types. The plugin is vendored (not a submodule) at `Game/addons/godot_rl_agents` — see its `VENDORED.md`; its C#/ONNX files are unused.
|
||||||
|
- `scripts/ship_observations.gd` is the shared observation builder used by both training and in-game inference — never fork or diverge these two paths. Team 1's observations are mirrored (180° about Y) so one policy plays both sides.
|
||||||
|
- In-game bots: `scripts/ai_ship_controller.gd` (a `ShipController`) runs the exported policy JSON via `scripts/policy_network.gd` (pure-GDScript MLP) — no .NET/ONNX/Python at runtime. Models live in `Game/bots/`; Match mode's `bot_model_path`/`bot_reaction_ticks`/`bot_action_noise` exports configure the opponent.
|
||||||
|
- Python side lives in `training/` (venv, not committed): `train.py` (SB3 PPO, launches parallel headless Godot instances from source), `export_policy.py` (checkpoint → JSON with parity check), `evaluate.py` (head-to-head eval, appends `training/eval_history.json`).
|
||||||
|
- The flattened action space is Box(7): thrust xyz, rotation xyz, turbo (>0 = on) — this is `ShipAction` verbatim; change either only deliberately and together.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Edward Beeching
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Vendored from https://github.com/edbeeching/godot_rl_agents_plugin
|
||||||
|
commit 998c357a0cd09b37f40a36d70c7867fc9f682338 (2026-06-13), MIT license (see LICENSE).
|
||||||
|
The onnx/csharp files are unused (require the .NET Godot build); in-game inference uses scripts/policy_network.gd instead.
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dgfbh3y7s07g7
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://qm8wpg7ccydk
|
||||||
@@ -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")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bdnkavfpf6ge6
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 198 B |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bxy3je5atsh68"
|
||||||
|
path="res://.godot/imported/icon.png-45a871b53434e556222f5901d598ab34.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://addons/godot_rl_agents/icon.png"
|
||||||
|
dest_files=["res://.godot/imported/icon.png-45a871b53434e556222f5901d598ab34.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
using Godot;
|
||||||
|
using Microsoft.ML.OnnxRuntime;
|
||||||
|
using Microsoft.ML.OnnxRuntime.Tensors;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace GodotONNX
|
||||||
|
{
|
||||||
|
/// <include file='docs/ONNXInference.xml' path='docs/members[@name="ONNXInference"]/ONNXInference/*'/>
|
||||||
|
public partial class ONNXInference : GodotObject
|
||||||
|
{
|
||||||
|
|
||||||
|
private InferenceSession session;
|
||||||
|
/// <summary>
|
||||||
|
/// Path to the ONNX model. Use Initialize to change it.
|
||||||
|
/// </summary>
|
||||||
|
private string modelPath;
|
||||||
|
private int batchSize;
|
||||||
|
|
||||||
|
private SessionOptions SessionOpt;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// init function
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Path"></param>
|
||||||
|
/// <param name="BatchSize"></param>
|
||||||
|
/// <returns>Returns the output size of the model</returns>
|
||||||
|
public int Initialize(string Path, int BatchSize)
|
||||||
|
{
|
||||||
|
modelPath = Path;
|
||||||
|
batchSize = BatchSize;
|
||||||
|
SessionOpt = SessionConfigurator.MakeConfiguredSessionOptions();
|
||||||
|
session = LoadModel(modelPath);
|
||||||
|
return session.OutputMetadata["output"].Dimensions[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <include file='docs/ONNXInference.xml' path='docs/members[@name="ONNXInference"]/Run/*'/>
|
||||||
|
public Godot.Collections.Dictionary<string, Godot.Collections.Array<float>> RunInference(Godot.Collections.Dictionary<string, Godot.Collections.Array<float>> 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>
|
||||||
|
{
|
||||||
|
NamedOnnxValue.CreateFromTensor("state_ins", new DenseTensor<float>(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<float>(obsData, new int[] { batchSize, subObs.Count }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IReadOnlyCollection<string> outputNames = new List<string> { "output", "state_outs" }; //ONNX is sensible to these names, as well as the input names
|
||||||
|
|
||||||
|
IDisposableReadOnlyCollection<DisposableNamedOnnxValue> 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<float> to Variant, so we have to convert it to an array or something
|
||||||
|
Godot.Collections.Dictionary<string, Godot.Collections.Array<float>> output = new Godot.Collections.Dictionary<string, Godot.Collections.Array<float>>();
|
||||||
|
DisposableNamedOnnxValue output1 = results.First();
|
||||||
|
DisposableNamedOnnxValue output2 = results.Last();
|
||||||
|
Godot.Collections.Array<float> output1Array = new Godot.Collections.Array<float>();
|
||||||
|
Godot.Collections.Array<float> output2Array = new Godot.Collections.Array<float>();
|
||||||
|
|
||||||
|
foreach (float f in output1.AsEnumerable<float>())
|
||||||
|
{
|
||||||
|
output1Array.Add(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (float f in output2.AsEnumerable<float>())
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
/// <include file='docs/ONNXInference.xml' path='docs/members[@name="ONNXInference"]/Load/*'/>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
using Godot;
|
||||||
|
using Microsoft.ML.OnnxRuntime;
|
||||||
|
|
||||||
|
namespace GodotONNX
|
||||||
|
{
|
||||||
|
/// <include file='docs/SessionConfigurator.xml' path='docs/members[@name="SessionConfigurator"]/SessionConfigurator/*'/>
|
||||||
|
|
||||||
|
public static class SessionConfigurator
|
||||||
|
{
|
||||||
|
public enum ComputeName
|
||||||
|
{
|
||||||
|
CUDA,
|
||||||
|
ROCm,
|
||||||
|
DirectML,
|
||||||
|
CoreML,
|
||||||
|
CPU
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <include file='docs/SessionConfigurator.xml' path='docs/members[@name="SessionConfigurator"]/GetSessionOptions/*'/>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <include file='docs/SessionConfigurator.xml' path='docs/members[@name="SessionConfigurator"]/SystemCheck/*'/>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <include file='docs/SessionConfigurator.xml' path='docs/members[@name="SessionConfigurator"]/ComputeCheck/*'/>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<docs>
|
||||||
|
<members name="ONNXInference">
|
||||||
|
<ONNXInference>
|
||||||
|
<summary>
|
||||||
|
The main <c>ONNXInference</c> Class that handles the inference process.
|
||||||
|
</summary>
|
||||||
|
</ONNXInference>
|
||||||
|
<Initialize>
|
||||||
|
<summary>
|
||||||
|
Starts the inference process.
|
||||||
|
</summary>
|
||||||
|
<param name="Path">Path to the ONNX model, expects a path inside resources.</param>
|
||||||
|
<param name="BatchSize">How many observations will the model recieve.</param>
|
||||||
|
</Initialize>
|
||||||
|
<Run>
|
||||||
|
<summary>
|
||||||
|
Runs the given input through the model and returns the output.
|
||||||
|
</summary>
|
||||||
|
<param name="obs">Dictionary containing all observations.</param>
|
||||||
|
<param name="state_ins">How many different agents are creating these observations.</param>
|
||||||
|
<returns>A Dictionary of arrays, containing instructions based on the observations.</returns>
|
||||||
|
</Run>
|
||||||
|
<Load>
|
||||||
|
<summary>
|
||||||
|
Loads the given model into the inference process, using the best Execution provider available.
|
||||||
|
</summary>
|
||||||
|
<param name="Path">Path to the ONNX model, expects a path inside resources.</param>
|
||||||
|
<returns>InferenceSession ready to run.</returns>
|
||||||
|
</Load>
|
||||||
|
</members>
|
||||||
|
</docs>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<docs>
|
||||||
|
<members name="SessionConfigurator">
|
||||||
|
<SessionConfigurator>
|
||||||
|
<summary>
|
||||||
|
The main <c>SessionConfigurator</c> Class that handles the execution options and providers for the inference process.
|
||||||
|
</summary>
|
||||||
|
</SessionConfigurator>
|
||||||
|
<GetSessionOptions>
|
||||||
|
<summary>
|
||||||
|
Creates a SessionOptions with all available execution providers.
|
||||||
|
</summary>
|
||||||
|
<returns>SessionOptions with all available execution providers.</returns>
|
||||||
|
</GetSessionOptions>
|
||||||
|
<SystemCheck>
|
||||||
|
<summary>
|
||||||
|
Appends any execution provider available in the current system.
|
||||||
|
</summary>
|
||||||
|
<remarks>
|
||||||
|
This function is mainly verbose for tracking implementation progress of different compute APIs.
|
||||||
|
</remarks>
|
||||||
|
</SystemCheck>
|
||||||
|
<ComputeCheck>
|
||||||
|
<summary>
|
||||||
|
Checks for available GPUs.
|
||||||
|
</summary>
|
||||||
|
<returns>An integer identifier for each compute platform.</returns>
|
||||||
|
</ComputeCheck>
|
||||||
|
</members>
|
||||||
|
</docs>
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c35ckkxpe764s
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://2jcows6svnje
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bpgeoecqatvwi
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
extends Node2D
|
||||||
|
class_name RewardFunction2D
|
||||||
|
|
||||||
|
|
||||||
|
func get_reward():
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func reset():
|
||||||
|
return
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://52jl48u122l8
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
extends Node3D
|
||||||
|
class_name RewardFunction3D
|
||||||
|
|
||||||
|
|
||||||
|
func get_reward():
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func reset():
|
||||||
|
return
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bwqq2ytgnepid
|
||||||
@@ -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")
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ctfloulhiht0v
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dwhx5ltv86b4e
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dvny3m75wta1e
|
||||||
@@ -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]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c8f8s63q3yyu1
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
@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 := false:
|
||||||
|
get:
|
||||||
|
return debug_draw
|
||||||
|
set(value):
|
||||||
|
debug_draw = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
var _angles = []
|
||||||
|
var rays := []
|
||||||
|
|
||||||
|
|
||||||
|
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():
|
||||||
|
for ray in get_children():
|
||||||
|
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)))
|
||||||
|
)
|
||||||
|
if debug_draw:
|
||||||
|
ray.enabled = true
|
||||||
|
else:
|
||||||
|
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)
|
||||||
|
ray.set_owner(get_tree().edited_scene_root)
|
||||||
|
ray.set_name("node_" + str(i))
|
||||||
|
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:
|
||||||
|
if not debug_draw:
|
||||||
|
ray.enabled = true
|
||||||
|
ray.force_raycast_update()
|
||||||
|
var distance = _get_raycast_distance(ray)
|
||||||
|
result.append(distance)
|
||||||
|
if not debug_draw:
|
||||||
|
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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c0eh0jjfthdgw
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://me8mehqmblq8
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b6dvaob0xndoh
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cew2a213sw1q
|
||||||
@@ -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]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://6e38006xhqf0
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
@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
|
||||||
|
|
||||||
|
@export var debug_draw := false:
|
||||||
|
get:
|
||||||
|
return debug_draw
|
||||||
|
set(value):
|
||||||
|
debug_draw = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if debug_draw:
|
||||||
|
ray.enabled = true
|
||||||
|
else:
|
||||||
|
ray.enabled = false
|
||||||
|
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)
|
||||||
|
ray.set_name("node_" + str(i) + " " + str(j))
|
||||||
|
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:
|
||||||
|
if not debug_draw:
|
||||||
|
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))
|
||||||
|
if not debug_draw:
|
||||||
|
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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://glop37kjpwci
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b4vk6sql5v1jl
|
||||||
@@ -15,9 +15,13 @@ dest_files=["res://.godot/imported/gold_ball.blend-c2b8b4e1fb4c47cd0c20f77e5daf3
|
|||||||
|
|
||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
nodes/use_node_type_suffixes=true
|
nodes/use_node_type_suffixes=true
|
||||||
meshes/ensure_tangents=true
|
meshes/ensure_tangents=true
|
||||||
meshes/generate_lods=true
|
meshes/generate_lods=true
|
||||||
@@ -32,6 +36,9 @@ animation/trimming=false
|
|||||||
animation/remove_immutable_tracks=true
|
animation/remove_immutable_tracks=true
|
||||||
animation/import_rest_as_RESET=false
|
animation/import_rest_as_RESET=false
|
||||||
import_script/path=""
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
_subresources={}
|
_subresources={}
|
||||||
blender/nodes/visible=0
|
blender/nodes/visible=0
|
||||||
blender/nodes/active_collection_only=false
|
blender/nodes/active_collection_only=false
|
||||||
@@ -39,10 +46,11 @@ blender/nodes/punctual_lights=true
|
|||||||
blender/nodes/cameras=true
|
blender/nodes/cameras=true
|
||||||
blender/nodes/custom_properties=true
|
blender/nodes/custom_properties=true
|
||||||
blender/nodes/modifiers=1
|
blender/nodes/modifiers=1
|
||||||
blender/meshes/colors=false
|
blender/meshes/vertex_colors=2
|
||||||
blender/meshes/uvs=true
|
blender/meshes/uvs=true
|
||||||
blender/meshes/normals=true
|
blender/meshes/normals=true
|
||||||
blender/meshes/export_geometry_nodes_instances=false
|
blender/meshes/export_geometry_nodes_instances=false
|
||||||
|
blender/meshes/gpu_instances=false
|
||||||
blender/meshes/tangents=true
|
blender/meshes/tangents=true
|
||||||
blender/meshes/skins=2
|
blender/meshes/skins=2
|
||||||
blender/meshes/export_bones_deforming_mesh_only=false
|
blender/meshes/export_bones_deforming_mesh_only=false
|
||||||
@@ -51,3 +59,5 @@ blender/materials/export_materials=1
|
|||||||
blender/animation/limit_playback=true
|
blender/animation/limit_playback=true
|
||||||
blender/animation/always_sample=true
|
blender/animation/always_sample=true
|
||||||
blender/animation/group_tracks=true
|
blender/animation/group_tracks=true
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 MiB |
@@ -1,35 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
uid="uid://cqubdkwj11v1i"
|
|
||||||
path.s3tc="res://.godot/imported/Grass.jpg-6a640b9c5e86d03c7318eda5c14b9c10.s3tc.ctex"
|
|
||||||
metadata={
|
|
||||||
"imported_formats": ["s3tc_bptc"],
|
|
||||||
"vram_texture": true
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://assets/models/Grass.jpg"
|
|
||||||
dest_files=["res://.godot/imported/Grass.jpg-6a640b9c5e86d03c7318eda5c14b9c10.s3tc.ctex"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
compress/mode=2
|
|
||||||
compress/high_quality=false
|
|
||||||
compress/lossy_quality=0.7
|
|
||||||
compress/hdr_compression=1
|
|
||||||
compress/normal_map=0
|
|
||||||
compress/channel_pack=0
|
|
||||||
mipmaps/generate=true
|
|
||||||
mipmaps/limit=-1
|
|
||||||
roughness/mode=0
|
|
||||||
roughness/src_normal=""
|
|
||||||
process/fix_alpha_border=true
|
|
||||||
process/premult_alpha=false
|
|
||||||
process/normal_map_invert_y=false
|
|
||||||
process/hdr_as_srgb=false
|
|
||||||
process/hdr_clamp_exposure=false
|
|
||||||
process/size_limit=0
|
|
||||||
detect_3d/compress_to=0
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# Blender MTL File: 'None'
|
|
||||||
# Material Count: 1
|
|
||||||
|
|
||||||
newmtl Grass
|
|
||||||
Ns 0.000000
|
|
||||||
Ka 1.000000 1.000000 1.000000
|
|
||||||
Kd 0.800000 0.800000 0.800000
|
|
||||||
Ks 0.500000 0.500000 0.500000
|
|
||||||
Ke 0.000000 0.000000 0.000000
|
|
||||||
Ni 1.450000
|
|
||||||
d 1.000000
|
|
||||||
illum 2
|
|
||||||
map_Kd C:\\Users\\Daniel\\Desktop\\Godot Projects\\OpenWorldGame\\Procedural Generation\\Textures\\Grass.jpg
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,25 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="wavefront_obj"
|
|
||||||
importer_version=1
|
|
||||||
type="Mesh"
|
|
||||||
uid="uid://cndcxj6gf0smr"
|
|
||||||
path="res://.godot/imported/Terrain.obj-180d9e542e76d4bc65ef6f60023d2068.mesh"
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
files=["res://.godot/imported/Terrain.obj-180d9e542e76d4bc65ef6f60023d2068.mesh"]
|
|
||||||
|
|
||||||
source_file="res://assets/models/Terrain.obj"
|
|
||||||
dest_files=["res://.godot/imported/Terrain.obj-180d9e542e76d4bc65ef6f60023d2068.mesh", "res://.godot/imported/Terrain.obj-180d9e542e76d4bc65ef6f60023d2068.mesh"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
generate_tangents=true
|
|
||||||
generate_lods=true
|
|
||||||
generate_shadow_mesh=true
|
|
||||||
generate_lightmap_uv2=false
|
|
||||||
generate_lightmap_uv2_texel_size=0.2
|
|
||||||
scale_mesh=Vector3(1, 1, 1)
|
|
||||||
offset_mesh=Vector3(0, 0, 0)
|
|
||||||
force_disable_mesh_compression=false
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[gd_resource type="StandardMaterial3D" load_steps=2 format=3 uid="uid://drm3clvqpis42"]
|
|
||||||
|
|
||||||
[ext_resource type="Texture2D" uid="uid://cqubdkwj11v1i" path="res://assets/models/Grass.jpg" id="1_imo8j"]
|
|
||||||
|
|
||||||
[resource]
|
|
||||||
albedo_texture = ExtResource("1_imo8j")
|
|
||||||
@@ -15,9 +15,13 @@ dest_files=["res://.godot/imported/gold_ball.glb-704aa0b9ad2b63d580121e20c448248
|
|||||||
|
|
||||||
nodes/root_type="Node3D"
|
nodes/root_type="Node3D"
|
||||||
nodes/root_name="Scene Root"
|
nodes/root_name="Scene Root"
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
nodes/use_node_type_suffixes=true
|
nodes/use_node_type_suffixes=true
|
||||||
meshes/ensure_tangents=true
|
meshes/ensure_tangents=true
|
||||||
meshes/generate_lods=true
|
meshes/generate_lods=true
|
||||||
@@ -32,6 +36,9 @@ animation/trimming=false
|
|||||||
animation/remove_immutable_tracks=true
|
animation/remove_immutable_tracks=true
|
||||||
animation/import_rest_as_RESET=false
|
animation/import_rest_as_RESET=false
|
||||||
import_script/path=""
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
_subresources={
|
_subresources={
|
||||||
"meshes": {
|
"meshes": {
|
||||||
"gold_ball_Sphere": {
|
"gold_ball_Sphere": {
|
||||||
@@ -41,10 +48,12 @@ _subresources={
|
|||||||
"lods/normal_merge_angle": 60.0,
|
"lods/normal_merge_angle": 60.0,
|
||||||
"lods/normal_split_angle": 25.0,
|
"lods/normal_split_angle": 25.0,
|
||||||
"save_to_file/enabled": true,
|
"save_to_file/enabled": true,
|
||||||
|
"save_to_file/fallback_path": "res://assets/models/gold_ball.res",
|
||||||
"save_to_file/make_streamable": "",
|
"save_to_file/make_streamable": "",
|
||||||
"save_to_file/path": "res://assets/models/gold_ball.res"
|
"save_to_file/path": "uid://ucw1lyo43yi4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
gltf/naming_version=0
|
gltf/naming_version=0
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.cte
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
[gd_scene load_steps=11 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/arena_boundary.gd" id="1_bndry"]
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_floor"]
|
||||||
|
albedo_color = Color(0.08, 0.09, 0.12, 1)
|
||||||
|
metallic = 0.3
|
||||||
|
roughness = 0.6
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_field"]
|
||||||
|
transparency = 1
|
||||||
|
shading_mode = 0
|
||||||
|
albedo_color = Color(0, 0.368627, 1, 0.08)
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="BoxMesh_floor"]
|
||||||
|
material = SubResource("StandardMaterial3D_floor")
|
||||||
|
size = Vector3(28, 1, 40)
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="BoxMesh_ceiling"]
|
||||||
|
material = SubResource("StandardMaterial3D_field")
|
||||||
|
size = Vector3(28, 1, 40)
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="BoxMesh_side_wall"]
|
||||||
|
material = SubResource("StandardMaterial3D_field")
|
||||||
|
size = Vector3(1, 13, 38)
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="BoxMesh_end_wall"]
|
||||||
|
material = SubResource("StandardMaterial3D_field")
|
||||||
|
size = Vector3(26, 13, 1)
|
||||||
|
|
||||||
|
[sub_resource type="BoxShape3D" id="BoxShape3D_slab"]
|
||||||
|
size = Vector3(28, 1, 40)
|
||||||
|
|
||||||
|
[sub_resource type="BoxShape3D" id="BoxShape3D_side_wall"]
|
||||||
|
size = Vector3(1, 13, 38)
|
||||||
|
|
||||||
|
[sub_resource type="BoxShape3D" id="BoxShape3D_end_wall"]
|
||||||
|
size = Vector3(26, 13, 1)
|
||||||
|
|
||||||
|
[node name="ArenaBoundary" type="StaticBody3D"]
|
||||||
|
script = ExtResource("1_bndry")
|
||||||
|
|
||||||
|
[node name="FloorShape" type="CollisionShape3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.5, 0)
|
||||||
|
shape = SubResource("BoxShape3D_slab")
|
||||||
|
|
||||||
|
[node name="FloorMesh" type="MeshInstance3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.5, 0)
|
||||||
|
mesh = SubResource("BoxMesh_floor")
|
||||||
|
|
||||||
|
[node name="WallPosXShape" type="CollisionShape3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 12.5, 5.5, 0)
|
||||||
|
shape = SubResource("BoxShape3D_side_wall")
|
||||||
|
|
||||||
|
[node name="WallPosXMesh" type="MeshInstance3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 12.5, 5.5, 0)
|
||||||
|
cast_shadow = 0
|
||||||
|
mesh = SubResource("BoxMesh_side_wall")
|
||||||
|
|
||||||
|
[node name="WallNegXShape" type="CollisionShape3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -12.5, 5.5, 0)
|
||||||
|
shape = SubResource("BoxShape3D_side_wall")
|
||||||
|
|
||||||
|
[node name="WallNegXMesh" type="MeshInstance3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -12.5, 5.5, 0)
|
||||||
|
cast_shadow = 0
|
||||||
|
mesh = SubResource("BoxMesh_side_wall")
|
||||||
|
|
||||||
|
[node name="WallPosZShape" type="CollisionShape3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5.5, 18.5)
|
||||||
|
shape = SubResource("BoxShape3D_end_wall")
|
||||||
|
|
||||||
|
[node name="WallPosZMesh" type="MeshInstance3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5.5, 18.5)
|
||||||
|
cast_shadow = 0
|
||||||
|
mesh = SubResource("BoxMesh_end_wall")
|
||||||
|
|
||||||
|
[node name="WallNegZShape" type="CollisionShape3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5.5, -18.5)
|
||||||
|
shape = SubResource("BoxShape3D_end_wall")
|
||||||
|
|
||||||
|
[node name="WallNegZMesh" type="MeshInstance3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5.5, -18.5)
|
||||||
|
cast_shadow = 0
|
||||||
|
mesh = SubResource("BoxMesh_end_wall")
|
||||||
|
|
||||||
|
[node name="CeilingShape" type="CollisionShape3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 12.5, 0)
|
||||||
|
shape = SubResource("BoxShape3D_slab")
|
||||||
|
|
||||||
|
[node name="CeilingMesh" type="MeshInstance3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 12.5, 0)
|
||||||
|
cast_shadow = 0
|
||||||
|
mesh = SubResource("BoxMesh_ceiling")
|
||||||
@@ -8,9 +8,10 @@
|
|||||||
bounce = 0.8
|
bounce = 0.8
|
||||||
friction = 0.3
|
friction = 0.3
|
||||||
|
|
||||||
[node name="Ball" type="RigidBody3D"]
|
[node name="Ball" type="RigidBody3D" groups=["ball"]]
|
||||||
mass = 3
|
mass = 3
|
||||||
physics_material_override = SubResource("PhysicsMaterial_ball")
|
physics_material_override = SubResource("PhysicsMaterial_ball")
|
||||||
|
continuous_cd = true
|
||||||
inertia = Vector3(3, 3, 3)
|
inertia = Vector3(3, 3, 3)
|
||||||
gravity_scale = 0.8
|
gravity_scale = 0.8
|
||||||
linear_damp = 0.1
|
linear_damp = 0.1
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[gd_scene load_steps=5 format=3 uid="uid://cofdcxo5170rs"]
|
[gd_scene load_steps=5 format=3 uid="uid://cofdcxo5170rs"]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://scripts/goal2.gd" id="1_v8ikr"]
|
[ext_resource type="Script" path="res://scripts/goal.gd" id="1_v8ikr"]
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ptddx"]
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ptddx"]
|
||||||
transparency = 1
|
transparency = 1
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
[gd_scene load_steps=5 format=3 uid="uid://p07epxnh8wwp"]
|
[gd_scene load_steps=5 format=3 uid="uid://p07epxnh8wwp"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://dyq1n7q1bqjps" path="res://scripts/ship.gd" id="1_efag7"]
|
[ext_resource type="Script" uid="uid://dyq1n7q1bqjps" path="res://scripts/ship.gd" id="1_efag7"]
|
||||||
[ext_resource type="PackedScene" uid="uid://c8kak2l3m4n5" path="res://scenes/HUD.tscn" id="2_hud_scene"]
|
|
||||||
|
[sub_resource type="PhysicsMaterial" id="PhysicsMaterial_ship"]
|
||||||
|
friction = 0.1
|
||||||
|
bounce = 0.2
|
||||||
|
|
||||||
[sub_resource type="BoxMesh" id="BoxMesh_efag7"]
|
[sub_resource type="BoxMesh" id="BoxMesh_efag7"]
|
||||||
size = Vector3(1, 1, 4)
|
size = Vector3(1, 1, 4)
|
||||||
@@ -10,8 +13,9 @@ size = Vector3(1, 1, 4)
|
|||||||
size = Vector3(1, 1, 4)
|
size = Vector3(1, 1, 4)
|
||||||
|
|
||||||
[node name="Ship" type="RigidBody3D"]
|
[node name="Ship" type="RigidBody3D"]
|
||||||
mass = 10.0
|
mass = 5.0
|
||||||
gravity_scale = 10.0
|
physics_material_override = SubResource("PhysicsMaterial_ship")
|
||||||
|
inertia = Vector3(1, 1, 1)
|
||||||
script = ExtResource("1_efag7")
|
script = ExtResource("1_efag7")
|
||||||
|
|
||||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
||||||
@@ -19,8 +23,3 @@ mesh = SubResource("BoxMesh_efag7")
|
|||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||||
shape = SubResource("BoxShape3D_dsjou")
|
shape = SubResource("BoxShape3D_dsjou")
|
||||||
|
|
||||||
[node name="Camera3D" type="Camera3D" parent="."]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 0.867366, 0.497671, 0, -0.497671, 0.867366, 0, 2.04336, 3.31556)
|
|
||||||
|
|
||||||
[node name="HUD" parent="." instance=ExtResource("2_hud_scene")]
|
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
[gd_scene load_steps=5 format=3 uid="uid://dnra1buk328d"]
|
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://byy2mu4mxdlgl" path="res://scripts/Vehicle.gd" id="1_5snef"]
|
|
||||||
|
|
||||||
[sub_resource type="BoxShape3D" id="BoxShape3D_0otfk"]
|
|
||||||
size = Vector3(0.5, 0.5, 1)
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_hrmfk"]
|
|
||||||
albedo_color = Color(0.670588, 0, 0, 1)
|
|
||||||
|
|
||||||
[sub_resource type="BoxMesh" id="BoxMesh_uwum7"]
|
|
||||||
material = SubResource("StandardMaterial3D_hrmfk")
|
|
||||||
size = Vector3(0.5, 0.5, 1)
|
|
||||||
|
|
||||||
[node name="Vehicle" type="RigidBody3D"]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.08165e-12, 0.5, 2)
|
|
||||||
mass = 10.0
|
|
||||||
gravity_scale = 10.0
|
|
||||||
lock_rotation = true
|
|
||||||
script = ExtResource("1_5snef")
|
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
|
||||||
shape = SubResource("BoxShape3D_0otfk")
|
|
||||||
|
|
||||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
|
||||||
mesh = SubResource("BoxMesh_uwum7")
|
|
||||||
|
|
||||||
[node name="Camera3D" type="Camera3D" parent="."]
|
|
||||||
transform = Transform3D(1, -4.68079e-16, 3.27752e-16, 3.27752e-16, 0.939693, 0.34202, -4.68079e-16, -0.34202, 0.939693, 2.08165e-12, 0.8, 1.2)
|
|
||||||
current = true
|
|
||||||
+17
-6
@@ -8,15 +8,23 @@
|
|||||||
|
|
||||||
config_version=5
|
config_version=5
|
||||||
|
|
||||||
|
[animation]
|
||||||
|
|
||||||
|
compatibility/default_parent_skeleton_in_mesh_instance_3d=true
|
||||||
|
|
||||||
[application]
|
[application]
|
||||||
|
|
||||||
config/name="Cosmic Clash"
|
config/name="Cosmic Clash"
|
||||||
config/description="A fast-paced, physics-based sports game set in space. From Raymond Studios."
|
config/description="A fast-paced, physics-based sports game set in space. From Raymond Studios."
|
||||||
config/version="0.0.1"
|
config/version="0.0.1"
|
||||||
run/main_scene="uid://bcq14356s3e2i"
|
run/main_scene="uid://bcq14356s3e2i"
|
||||||
config/features=PackedStringArray("4.4", "Forward Plus")
|
config/features=PackedStringArray("4.7", "Forward Plus")
|
||||||
config/icon="res://icon.svg"
|
config/icon="res://icon.svg"
|
||||||
|
|
||||||
|
[editor_plugins]
|
||||||
|
|
||||||
|
enabled=PackedStringArray("res://addons/godot_rl_agents/plugin.cfg")
|
||||||
|
|
||||||
[display]
|
[display]
|
||||||
|
|
||||||
window/size/viewport_width=1920
|
window/size/viewport_width=1920
|
||||||
@@ -27,6 +35,8 @@ window/stretch/aspect="expand"
|
|||||||
|
|
||||||
[input]
|
[input]
|
||||||
|
|
||||||
|
reset_ball={"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":82,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)]}
|
||||||
|
|
||||||
move_forward={
|
move_forward={
|
||||||
"deadzone": 0.2,
|
"deadzone": 0.2,
|
||||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null)
|
"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":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null)
|
||||||
@@ -47,11 +57,6 @@ move_right={
|
|||||||
"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)
|
"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)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
move_backward={
|
|
||||||
"deadzone": 0.2,
|
|
||||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
move_up={
|
move_up={
|
||||||
"deadzone": 0.2,
|
"deadzone": 0.2,
|
||||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null)
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null)
|
||||||
@@ -108,3 +113,9 @@ roll_right={
|
|||||||
[physics]
|
[physics]
|
||||||
|
|
||||||
3d/physics_engine="Jolt Physics"
|
3d/physics_engine="Jolt Physics"
|
||||||
|
|
||||||
|
[autoload]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
|||||||
[gd_scene load_steps=3 format=3 uid="uid://c8kak2l3m4n5"]
|
[gd_scene load_steps=3 format=3 uid="uid://c8kak2l3m4n5"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://bx9j8k7l6m5n" path="res://scripts/HUDController.gd" id="1_hud_controller"]
|
[ext_resource type="Script" uid="uid://du7y176h5aaq4" path="res://scripts/HUDController.gd" id="1_hud_controller"]
|
||||||
|
|
||||||
[sub_resource type="LabelSettings" id="LabelSettings_hud"]
|
[sub_resource type="LabelSettings" id="LabelSettings_hud"]
|
||||||
font_size = 32
|
font_size = 32
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
[gd_scene load_steps=8 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/arena.gd" id="1_iywne"]
|
||||||
|
[ext_resource type="PackedScene" path="res://objects/arena_boundary.tscn" id="2_bndry"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://cofdcxo5170rs" path="res://objects/goal.tscn" id="6_p57ef"]
|
||||||
|
|
||||||
|
[sub_resource type="Shader" id="Shader_stars"]
|
||||||
|
code = "shader_type sky;
|
||||||
|
|
||||||
|
// Procedural starfield: sparse hash-based stars over near-black space, with a
|
||||||
|
// faint blue nebula glow toward the horizon.
|
||||||
|
void sky() {
|
||||||
|
vec3 dir = EYEDIR;
|
||||||
|
vec3 col = vec3(0.01, 0.012, 0.02);
|
||||||
|
col += vec3(0.02, 0.03, 0.08) * pow(1.0 - abs(dir.y), 3.0);
|
||||||
|
vec3 cell = floor(dir * 300.0);
|
||||||
|
float h = fract(sin(dot(cell, vec3(12.9898, 78.233, 45.164))) * 43758.5453);
|
||||||
|
if (h > 0.998) {
|
||||||
|
vec3 in_cell = fract(dir * 300.0) - 0.5;
|
||||||
|
float star = smoothstep(0.4, 0.0, length(in_cell));
|
||||||
|
col += vec3(star * (0.5 + fract(h * 999.0)));
|
||||||
|
}
|
||||||
|
COLOR = col;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
[sub_resource type="ShaderMaterial" id="ShaderMaterial_stars"]
|
||||||
|
shader = SubResource("Shader_stars")
|
||||||
|
|
||||||
|
[sub_resource type="Sky" id="Sky_space"]
|
||||||
|
sky_material = SubResource("ShaderMaterial_stars")
|
||||||
|
|
||||||
|
[sub_resource type="Environment" id="Environment_space"]
|
||||||
|
background_mode = 2
|
||||||
|
sky = SubResource("Sky_space")
|
||||||
|
ambient_light_source = 2
|
||||||
|
ambient_light_color = Color(0.55, 0.6, 0.75, 1)
|
||||||
|
ambient_light_energy = 0.4
|
||||||
|
|
||||||
|
[node name="Arena" type="Node3D"]
|
||||||
|
script = ExtResource("1_iywne")
|
||||||
|
|
||||||
|
[node name="Boundary" parent="." instance=ExtResource("2_bndry")]
|
||||||
|
|
||||||
|
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
|
||||||
|
transform = Transform3D(0.866025, -0.383022, 0.321394, 0, 0.642788, 0.766044, -0.5, -0.663414, 0.55667, 0, 11, 0)
|
||||||
|
shadow_enabled = true
|
||||||
|
|
||||||
|
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
||||||
|
environment = SubResource("Environment_space")
|
||||||
|
|
||||||
|
[node name="GoalTeam0" parent="." instance=ExtResource("6_p57ef")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.79, 17)
|
||||||
|
|
||||||
|
[node name="GoalTeam1" parent="." instance=ExtResource("6_p57ef")]
|
||||||
|
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0.79, -17)
|
||||||
|
team = 1
|
||||||
|
|
||||||
|
[node name="BallSpawn" type="Marker3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
|
||||||
|
|
||||||
|
[node name="SpawnsTeam0" type="Node3D" parent="."]
|
||||||
|
|
||||||
|
[node name="Spawn1" type="Marker3D" parent="SpawnsTeam0"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.8, 13.5)
|
||||||
|
|
||||||
|
[node name="SpawnsTeam1" type="Node3D" parent="."]
|
||||||
|
|
||||||
|
[node name="Spawn1" type="Marker3D" parent="SpawnsTeam1"]
|
||||||
|
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 2.8, -13.5)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[gd_scene load_steps=4 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/free_play.gd" id="1_fp"]
|
||||||
|
[ext_resource type="PackedScene" path="res://scenes/arena_01.tscn" id="2_fp"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://c8kak2l3m4n5" path="res://scenes/HUD.tscn" id="3_fp"]
|
||||||
|
|
||||||
|
[node name="FreePlay" type="Node3D"]
|
||||||
|
script = ExtResource("1_fp")
|
||||||
|
|
||||||
|
[node name="Arena" parent="." instance=ExtResource("2_fp")]
|
||||||
|
|
||||||
|
[node name="HUD" parent="." instance=ExtResource("3_fp")]
|
||||||
+35
-36
@@ -1,44 +1,43 @@
|
|||||||
[gd_scene load_steps=3 format=3 uid="uid://bcq14356s3e2i"]
|
[gd_scene load_steps=2 format=3 uid="uid://bcq14356s3e2i"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://c1tfuurttt8ft" path="res://scripts/main_menu_play_button.gd" id="1_28flt"]
|
[ext_resource type="Script" path="res://scripts/main_menu.gd" id="1_menu"]
|
||||||
|
|
||||||
[sub_resource type="LabelSettings" id="LabelSettings_erv1k"]
|
[node name="MainMenu" type="Control"]
|
||||||
|
layout_mode = 3
|
||||||
[node name="MainMenu" type="Node2D"]
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
[node name="Label" type="Label" parent="."]
|
anchor_bottom = 1.0
|
||||||
anchors_preset = 8
|
|
||||||
anchor_left = 0.5
|
|
||||||
anchor_top = 0.5
|
|
||||||
anchor_right = 0.5
|
|
||||||
anchor_bottom = 0.5
|
|
||||||
offset_left = -51.0
|
|
||||||
offset_top = -11.0
|
|
||||||
offset_right = 253.0
|
|
||||||
offset_bottom = 109.0
|
|
||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
size_flags_horizontal = 4
|
script = ExtResource("1_menu")
|
||||||
|
|
||||||
|
[node name="CenterContainer" type="CenterContainer" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 24
|
||||||
|
|
||||||
|
[node name="TitleLabel" type="Label" parent="CenterContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 48
|
||||||
text = "Cosmic Clash"
|
text = "Cosmic Clash"
|
||||||
label_settings = SubResource("LabelSettings_erv1k")
|
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
vertical_alignment = 1
|
|
||||||
|
|
||||||
[node name="Button" type="Button" parent="."]
|
[node name="FreePlayButton" type="Button" parent="CenterContainer/VBoxContainer"]
|
||||||
anchors_preset = 8
|
custom_minimum_size = Vector2(330, 60)
|
||||||
anchor_left = 0.5
|
layout_mode = 2
|
||||||
anchor_top = 0.5
|
text = "Free Play"
|
||||||
anchor_right = 0.5
|
|
||||||
anchor_bottom = 0.5
|
|
||||||
offset_left = 347.0
|
|
||||||
offset_top = 233.0
|
|
||||||
offset_right = 676.0
|
|
||||||
offset_bottom = 373.0
|
|
||||||
grow_horizontal = 2
|
|
||||||
grow_vertical = 2
|
|
||||||
size_flags_horizontal = 4
|
|
||||||
size_flags_vertical = 4
|
|
||||||
text = "Play"
|
|
||||||
script = ExtResource("1_28flt")
|
|
||||||
|
|
||||||
[connection signal="pressed" from="Button" to="Button" method="_on_pressed"]
|
[node name="MatchButton" type="Button" parent="CenterContainer/VBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(330, 60)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Match"
|
||||||
|
|
||||||
|
[connection signal="pressed" from="CenterContainer/VBoxContainer/FreePlayButton" to="." method="_on_free_play_pressed"]
|
||||||
|
[connection signal="pressed" from="CenterContainer/VBoxContainer/MatchButton" to="." method="_on_match_pressed"]
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[gd_scene load_steps=4 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/match_mode.gd" id="1_m"]
|
||||||
|
[ext_resource type="PackedScene" path="res://scenes/arena_01.tscn" id="2_m"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://c8kak2l3m4n5" path="res://scenes/HUD.tscn" id="3_m"]
|
||||||
|
|
||||||
|
[node name="Match" type="Node3D"]
|
||||||
|
script = ExtResource("1_m")
|
||||||
|
bot_model_path = "res://bots/rookie.json"
|
||||||
|
|
||||||
|
[node name="Arena" parent="." instance=ExtResource("2_m")]
|
||||||
|
|
||||||
|
[node name="HUD" parent="." instance=ExtResource("3_m")]
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/ship_camera.gd" id="1_rig"]
|
||||||
|
|
||||||
|
[node name="ShipCameraRig" type="Node3D"]
|
||||||
|
script = ExtResource("1_rig")
|
||||||
|
|
||||||
|
[node name="Camera3D" type="Camera3D" parent="."]
|
||||||
|
current = true
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[gd_scene load_steps=4 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/training_mode.gd" id="1_tr"]
|
||||||
|
[ext_resource type="PackedScene" path="res://scenes/arena_01.tscn" id="2_tr"]
|
||||||
|
[ext_resource type="Script" path="res://addons/godot_rl_agents/sync.gd" id="3_tr"]
|
||||||
|
|
||||||
|
[node name="Training" type="Node3D"]
|
||||||
|
script = ExtResource("1_tr")
|
||||||
|
|
||||||
|
[node name="Arena" parent="." instance=ExtResource("2_tr")]
|
||||||
|
|
||||||
|
[node name="Sync" type="Node" parent="."]
|
||||||
|
script = ExtResource("3_tr")
|
||||||
|
action_repeat = 8
|
||||||
|
speed_up = 8.0
|
||||||
@@ -25,22 +25,26 @@ func _initialize_hud():
|
|||||||
# Find the ship
|
# Find the ship
|
||||||
ship = get_tree().get_first_node_in_group("ship")
|
ship = get_tree().get_first_node_in_group("ship")
|
||||||
if not ship:
|
if not ship:
|
||||||
# Try to find it as our parent (ship contains this HUD)
|
|
||||||
var parent_node = get_parent()
|
|
||||||
if parent_node and parent_node.is_in_group("ship"):
|
|
||||||
ship = parent_node
|
|
||||||
else:
|
|
||||||
push_error("HUDController: No ship found in 'ship' group")
|
push_error("HUDController: No ship found in 'ship' group")
|
||||||
return
|
return
|
||||||
|
|
||||||
print("HUDController: Found ship: ", ship.name)
|
print("HUDController: Found ship: ", ship.name)
|
||||||
_connect_ship_signals()
|
_connect_ship_signals()
|
||||||
|
|
||||||
# Connect to game manager's timer signal
|
# Camera mode comes from the camera rig, not the ship
|
||||||
|
var camera_rig = get_tree().get_first_node_in_group("ship_camera")
|
||||||
|
if camera_rig and camera_rig.has_signal("camera_mode_changed"):
|
||||||
|
camera_rig.camera_mode_changed.connect(_on_ship_camera_mode_changed)
|
||||||
|
|
||||||
|
# Connect to game manager's timer signal; modes without a timer
|
||||||
|
# (e.g. free play) just don't show one
|
||||||
var game_manager = get_tree().get_first_node_in_group("game")
|
var game_manager = get_tree().get_first_node_in_group("game")
|
||||||
if game_manager and game_manager.has_signal("timer_updated"):
|
var has_timer = game_manager and game_manager.has_signal("timer_updated")
|
||||||
|
if has_timer:
|
||||||
game_manager.timer_updated.connect(_on_timer_updated)
|
game_manager.timer_updated.connect(_on_timer_updated)
|
||||||
print("HUDController: Connected to game timer")
|
print("HUDController: Connected to game timer")
|
||||||
|
if timer_label and is_instance_valid(timer_label):
|
||||||
|
timer_label.visible = has_timer
|
||||||
|
|
||||||
func _connect_ship_signals():
|
func _connect_ship_signals():
|
||||||
# Connect ship signals to label update methods
|
# Connect ship signals to label update methods
|
||||||
@@ -51,8 +55,6 @@ func _connect_ship_signals():
|
|||||||
ship.altitude_changed.connect(_on_ship_altitude_changed)
|
ship.altitude_changed.connect(_on_ship_altitude_changed)
|
||||||
if ship.has_signal("angular_velocity_changed"):
|
if ship.has_signal("angular_velocity_changed"):
|
||||||
ship.angular_velocity_changed.connect(_on_ship_angular_velocity_changed)
|
ship.angular_velocity_changed.connect(_on_ship_angular_velocity_changed)
|
||||||
if ship.has_signal("camera_mode_changed"):
|
|
||||||
ship.camera_mode_changed.connect(_on_ship_camera_mode_changed)
|
|
||||||
if ship.has_signal("attitude_changed"):
|
if ship.has_signal("attitude_changed"):
|
||||||
ship.attitude_changed.connect(_on_ship_attitude_changed)
|
ship.attitude_changed.connect(_on_ship_attitude_changed)
|
||||||
if ship.has_signal("heading_changed"):
|
if ship.has_signal("heading_changed"):
|
||||||
@@ -79,7 +81,7 @@ func _on_ship_camera_mode_changed(is_ball_cam: bool):
|
|||||||
var camera_mode = "Ball Cam" if is_ball_cam else "Ship Cam"
|
var camera_mode = "Ball Cam" if is_ball_cam else "Ship Cam"
|
||||||
camera_mode_label.text = "Camera: %s" % camera_mode
|
camera_mode_label.text = "Camera: %s" % camera_mode
|
||||||
|
|
||||||
func _on_ship_attitude_changed(pitch: float, roll: float, yaw: float):
|
func _on_ship_attitude_changed(pitch: float, roll: float, _yaw: float):
|
||||||
if attitude_label and is_instance_valid(attitude_label):
|
if attitude_label and is_instance_valid(attitude_label):
|
||||||
attitude_label.text = "Pitch: %.0f° Roll: %.0f°" % [pitch, roll]
|
attitude_label.text = "Pitch: %.0f° Roll: %.0f°" % [pitch, roll]
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
uid://scnbnslvru0b
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
extends RigidBody3D
|
|
||||||
|
|
||||||
# Speed variable can be adjusted by subclasses
|
|
||||||
var speed = 10.0
|
|
||||||
|
|
||||||
func _integrate_forces(state):
|
|
||||||
# input_vector represents the movement input relative to the vehicle
|
|
||||||
var input_vector = get_input_vector()
|
|
||||||
|
|
||||||
# Scale the input_vector by speed
|
|
||||||
input_vector = input_vector.normalized() * speed
|
|
||||||
|
|
||||||
# Set the linear velocity based on the input
|
|
||||||
state.linear_velocity = input_vector
|
|
||||||
|
|
||||||
# Get the input vector from subclasses
|
|
||||||
func get_input_vector() -> Vector3:
|
|
||||||
var input_vector = Vector3.ZERO
|
|
||||||
|
|
||||||
# Subclasses will implement this function to provide their input mappings
|
|
||||||
return input_vector
|
|
||||||
|
|
||||||
# Called when the node enters the scene tree for the first time.
|
|
||||||
func _ready():
|
|
||||||
pass # Replace with function body.
|
|
||||||
|
|
||||||
|
|
||||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
|
||||||
func _process(delta):
|
|
||||||
pass
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://byy2mu4mxdlgl
|
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
class_name AIShipController
|
||||||
|
extends ShipController
|
||||||
|
|
||||||
|
# Drives a ship from a trained self-play policy (see TRAINING.md). Builds the
|
||||||
|
# same canonical observation as training (ShipObservations) and runs the
|
||||||
|
# policy MLP in GDScript (PolicyNetwork) — the shipped bot has no Python,
|
||||||
|
# .NET, or network dependency.
|
||||||
|
#
|
||||||
|
# Difficulty is (model, reaction_ticks, action_noise): weaker checkpoints make
|
||||||
|
# easier bots outright, and the two knobs handicap a given model further —
|
||||||
|
# slower reactions and noisier execution. Models live in res://bots/.
|
||||||
|
|
||||||
|
@export_file("*.json") var model_path: String = ""
|
||||||
|
# Decide a new action every N physics ticks, holding the last one between
|
||||||
|
# decisions. 8 matches the training action_repeat; larger = slower reactions.
|
||||||
|
@export_range(1, 60) var reaction_ticks: int = 8
|
||||||
|
# Uniform noise magnitude added to each action axis (0 = play at full skill).
|
||||||
|
@export_range(0.0, 1.0) var action_noise: float = 0.0
|
||||||
|
|
||||||
|
var _policy: PolicyNetwork
|
||||||
|
var _action := ShipAction.new()
|
||||||
|
var _ticks_until_decision := 0
|
||||||
|
|
||||||
|
var _ship: Ship
|
||||||
|
var _opponent: Ship
|
||||||
|
var _ball: RigidBody3D
|
||||||
|
var _attack_goal_position: Vector3
|
||||||
|
var _scene_refs_ready := false
|
||||||
|
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
if not model_path.is_empty():
|
||||||
|
_policy = PolicyNetwork.load_from_file(model_path)
|
||||||
|
|
||||||
|
|
||||||
|
func get_action() -> ShipAction:
|
||||||
|
if _policy == null:
|
||||||
|
return _action # unloaded model: behaves like the inert placeholder
|
||||||
|
if not _scene_refs_ready and not _discover_scene_refs():
|
||||||
|
return _action
|
||||||
|
|
||||||
|
_ticks_until_decision -= 1
|
||||||
|
if _ticks_until_decision <= 0:
|
||||||
|
_ticks_until_decision = reaction_ticks
|
||||||
|
_decide()
|
||||||
|
return _action
|
||||||
|
|
||||||
|
|
||||||
|
func _decide() -> void:
|
||||||
|
var obs := ShipObservations.build(_ship, _opponent, _ball, _attack_goal_position)
|
||||||
|
var out := _policy.forward(obs)
|
||||||
|
# Output layout matches the flattened training action space (Box(7)):
|
||||||
|
# thrust xyz, rotation xyz, turbo (> 0 means on).
|
||||||
|
_action.thrust = Vector3(
|
||||||
|
_axis(out[0]),
|
||||||
|
_axis(out[1]),
|
||||||
|
_axis(out[2])
|
||||||
|
)
|
||||||
|
_action.rotation = Vector3(
|
||||||
|
_axis(out[3]),
|
||||||
|
_axis(out[4]),
|
||||||
|
_axis(out[5])
|
||||||
|
)
|
||||||
|
_action.turbo = out[6] > 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _axis(value: float) -> float:
|
||||||
|
if action_noise > 0.0:
|
||||||
|
value += randf_range(-action_noise, action_noise)
|
||||||
|
return clampf(value, -1.0, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
# Find ship/ball/opponent/goal once everything is spawned. ShipAction axes
|
||||||
|
# are body-frame so only observations need team context (ShipObservations).
|
||||||
|
func _discover_scene_refs() -> bool:
|
||||||
|
_ship = get_parent() as Ship
|
||||||
|
if _ship == null or not is_inside_tree():
|
||||||
|
return false
|
||||||
|
_ball = get_tree().get_first_node_in_group("ball")
|
||||||
|
for node in get_tree().get_nodes_in_group("ship"):
|
||||||
|
if node != _ship:
|
||||||
|
_opponent = node
|
||||||
|
break
|
||||||
|
for goal in get_tree().get_nodes_in_group("goal"):
|
||||||
|
if goal.team == 1 - _ship.team:
|
||||||
|
_attack_goal_position = goal.global_position
|
||||||
|
if _ball == null:
|
||||||
|
return false
|
||||||
|
_scene_refs_ready = true
|
||||||
|
return true
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://4emuhiolrkb2
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
class_name Arena
|
||||||
|
extends Node3D
|
||||||
|
|
||||||
|
# A reusable stadium: terrain, lighting, environment, two team goals, and
|
||||||
|
# spawn markers. An arena holds no rules and no state — game modes query it
|
||||||
|
# for spawn transforms and goals, then spawn ships/ball themselves.
|
||||||
|
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
add_to_group("arena")
|
||||||
|
|
||||||
|
|
||||||
|
func get_ball_spawn() -> Transform3D:
|
||||||
|
return $BallSpawn.global_transform
|
||||||
|
|
||||||
|
|
||||||
|
func get_ship_spawns(team: int) -> Array[Transform3D]:
|
||||||
|
var spawns: Array[Transform3D] = []
|
||||||
|
var container := get_node_or_null("SpawnsTeam%d" % team)
|
||||||
|
if container:
|
||||||
|
for child in container.get_children():
|
||||||
|
if child is Marker3D:
|
||||||
|
spawns.append(child.global_transform)
|
||||||
|
return spawns
|
||||||
|
|
||||||
|
|
||||||
|
func get_goals() -> Array[Goal]:
|
||||||
|
var goals: Array[Goal] = []
|
||||||
|
for child in get_children():
|
||||||
|
if child is Goal:
|
||||||
|
goals.append(child)
|
||||||
|
return goals
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c1kq2m6gnwjxo
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
class_name ArenaBoundary
|
||||||
|
extends StaticBody3D
|
||||||
|
|
||||||
|
# The standard arena play volume (inner faces of the enclosure). Every arena
|
||||||
|
# instances objects/arena_boundary.tscn so all arenas share one size; code
|
||||||
|
# that needs field dimensions derives them from these constants rather than
|
||||||
|
# restating numbers.
|
||||||
|
const INNER_HALF_X := 12.0
|
||||||
|
const INNER_HALF_Z := 18.0
|
||||||
|
const INNER_HEIGHT := 12.0
|
||||||
|
# Goal-centre distance from arena centre; the end walls sit 1 m behind, so a
|
||||||
|
# ball pinned against them still overlaps the goal sensor.
|
||||||
|
const GOAL_LINE_Z := 17.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://f32otkpo3lvr
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
extends GameMode
|
||||||
|
|
||||||
|
# Free Play: one player ship, one ball, no timer, no score — practice like
|
||||||
|
# Rocket League's free play. R resets the ball, Esc returns to the menu.
|
||||||
|
|
||||||
|
|
||||||
|
func _start() -> void:
|
||||||
|
spawn_ball()
|
||||||
|
var player_ship := spawn_ship(0, 0, PlayerShipController.new())
|
||||||
|
spawn_camera_rig(player_ship)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_goal_scored(conceding_team: int) -> void:
|
||||||
|
print("Goal! (into team %d's goal)" % conceding_team)
|
||||||
|
reset_ball()
|
||||||
|
|
||||||
|
|
||||||
|
func _unhandled_input(event):
|
||||||
|
if event.is_action_pressed("reset_ball"):
|
||||||
|
reset_ball()
|
||||||
|
else:
|
||||||
|
super(event)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://coirkjf1pbhi7
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
extends Node3D
|
|
||||||
|
|
||||||
@onready var game_timer: Timer = get_node("Timer")
|
|
||||||
signal timer_updated(minutes: int, seconds: int)
|
|
||||||
|
|
||||||
func _ready():
|
|
||||||
# Add to group for discovery by HUD
|
|
||||||
add_to_group("game")
|
|
||||||
|
|
||||||
# Set timer to 2 minutes 30 seconds (150 seconds)
|
|
||||||
game_timer.wait_time = 150.0
|
|
||||||
game_timer.one_shot = true # Timer runs once
|
|
||||||
game_timer.start()
|
|
||||||
|
|
||||||
func _process(_delta):
|
|
||||||
if game_timer.time_left > 0:
|
|
||||||
var time_left = game_timer.time_left
|
|
||||||
var minutes = int(time_left) / 60
|
|
||||||
var seconds = int(time_left) % 60
|
|
||||||
timer_updated.emit(minutes, seconds)
|
|
||||||
else:
|
|
||||||
print("Timer finished!")
|
|
||||||
|
|
||||||
func _on_timer_timeout() -> void:
|
|
||||||
if game_timer.time_left == 0:
|
|
||||||
get_tree().change_scene_to_file("res://scenes/main_menu.tscn")
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://bpxm8ge52w5g8
|
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
class_name GameMode
|
||||||
|
extends Node3D
|
||||||
|
|
||||||
|
# Base for game modes (Free Play, Match; later Vs-AI and multiplayer).
|
||||||
|
# A mode's scene contains an Arena (the stadium) and a HUD; the mode itself
|
||||||
|
# spawns the ball, ships, controllers, and camera in code — variable ship
|
||||||
|
# counts with mixed controller types (player/AI/network) is exactly what
|
||||||
|
# future modes need. Subclasses override _start() and _on_goal_scored().
|
||||||
|
|
||||||
|
@export var ship_scene: PackedScene = preload("res://objects/ship.tscn")
|
||||||
|
@export var ball_scene: PackedScene = preload("res://objects/ball.tscn")
|
||||||
|
|
||||||
|
const CAMERA_RIG_SCENE = preload("res://scenes/ship_camera_rig.tscn")
|
||||||
|
const MAIN_MENU_SCENE_PATH = "res://scenes/main_menu.tscn"
|
||||||
|
|
||||||
|
var arena: Arena
|
||||||
|
var ball: RigidBody3D
|
||||||
|
var ships: Array[Ship] = []
|
||||||
|
var _ship_spawn_transforms := {}
|
||||||
|
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
# Group lets the HUD discover the game mode for timer/score signals
|
||||||
|
add_to_group("game")
|
||||||
|
for child in get_children():
|
||||||
|
if child is Arena:
|
||||||
|
arena = child
|
||||||
|
break
|
||||||
|
if not arena:
|
||||||
|
push_error("GameMode: scene has no Arena child")
|
||||||
|
return
|
||||||
|
for goal in arena.get_goals():
|
||||||
|
goal.goal_scored.connect(_handle_goal_scored)
|
||||||
|
_start()
|
||||||
|
|
||||||
|
|
||||||
|
# Virtual: subclasses spawn their ball/ships/camera here.
|
||||||
|
func _start() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Virtual: the ball entered the goal owned (conceded) by `_conceding_team`.
|
||||||
|
func _on_goal_scored(_conceding_team: int) -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Debounce: a fast ball can re-trigger the goal area before the deferred
|
||||||
|
# reset teleports it away, which would double-count the goal.
|
||||||
|
var _goal_cooldown := false
|
||||||
|
|
||||||
|
func _handle_goal_scored(conceding_team: int) -> void:
|
||||||
|
if _goal_cooldown:
|
||||||
|
return
|
||||||
|
_goal_cooldown = true
|
||||||
|
get_tree().create_timer(0.5).timeout.connect(func(): _goal_cooldown = false)
|
||||||
|
_on_goal_scored(conceding_team)
|
||||||
|
|
||||||
|
|
||||||
|
func spawn_ball() -> RigidBody3D:
|
||||||
|
ball = ball_scene.instantiate()
|
||||||
|
add_child(ball)
|
||||||
|
ball.global_transform = arena.get_ball_spawn()
|
||||||
|
return ball
|
||||||
|
|
||||||
|
|
||||||
|
func spawn_ship(team: int, spawn_index: int = 0, controller: ShipController = null) -> Ship:
|
||||||
|
var ship: Ship = ship_scene.instantiate()
|
||||||
|
ship.name = "ShipTeam%d_%d" % [team, ships.size()]
|
||||||
|
add_child(ship)
|
||||||
|
var spawns := arena.get_ship_spawns(team)
|
||||||
|
var spawn_transform := spawns[spawn_index] if spawn_index < spawns.size() else Transform3D.IDENTITY
|
||||||
|
ship.global_transform = spawn_transform
|
||||||
|
ship.team = team
|
||||||
|
if controller:
|
||||||
|
ship.set_controller(controller)
|
||||||
|
ships.append(ship)
|
||||||
|
_ship_spawn_transforms[ship] = spawn_transform
|
||||||
|
return ship
|
||||||
|
|
||||||
|
|
||||||
|
func spawn_camera_rig(target: Ship) -> ShipCameraRig:
|
||||||
|
var rig: ShipCameraRig = CAMERA_RIG_SCENE.instantiate()
|
||||||
|
add_child(rig)
|
||||||
|
rig.target = target
|
||||||
|
return rig
|
||||||
|
|
||||||
|
|
||||||
|
func reset_ball() -> void:
|
||||||
|
if is_instance_valid(ball):
|
||||||
|
_reset_body(ball, arena.get_ball_spawn())
|
||||||
|
|
||||||
|
|
||||||
|
func reset_ships() -> void:
|
||||||
|
for ship in ships:
|
||||||
|
if is_instance_valid(ship):
|
||||||
|
_reset_body(ship, _ship_spawn_transforms[ship])
|
||||||
|
|
||||||
|
|
||||||
|
func _reset_body(body: RigidBody3D, to: Transform3D) -> void:
|
||||||
|
# Deferred: a RigidBody3D transform can't be set mid-physics-step
|
||||||
|
body.set_deferred("global_transform", to)
|
||||||
|
body.set_deferred("linear_velocity", Vector3.ZERO)
|
||||||
|
body.set_deferred("angular_velocity", Vector3.ZERO)
|
||||||
|
|
||||||
|
|
||||||
|
func _unhandled_input(event):
|
||||||
|
if event.is_action_pressed("ui_cancel"):
|
||||||
|
get_tree().change_scene_to_file(MAIN_MENU_SCENE_PATH)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c6qkhqup6h0pk
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
class_name Goal
|
||||||
|
extends Area3D
|
||||||
|
|
||||||
|
# A goal is a dumb sensor: it detects the ball crossing its plane and emits
|
||||||
|
# goal_scored. The game mode owns all consequences (score, resets). Two of
|
||||||
|
# these live in each arena, one per team.
|
||||||
|
|
||||||
|
# The team that concedes when the ball enters this goal.
|
||||||
|
@export var team: int = 0
|
||||||
|
|
||||||
|
signal goal_scored(team: int)
|
||||||
|
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
# Group lets AI controllers and game modes discover goals
|
||||||
|
add_to_group("goal")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_body_entered(body):
|
||||||
|
if body.is_in_group("ball"):
|
||||||
|
goal_scored.emit(team)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user