mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
70 lines
2.7 KiB
GDScript
70 lines
2.7 KiB
GDScript
class_name ShipObservations
|
|
extends RefCounted
|
|
|
|
# Canonical, team-relative observation builder. Shared by training
|
|
# (ShipAIController) and in-game inference (AIShipController) so a trained
|
|
# policy sees byte-identical inputs in both contexts — do not fork this logic.
|
|
#
|
|
# Self-play trick: observations for team 1 are rotated 180° about Y
|
|
# (x → -x, z → -z), so every ship perceives itself attacking toward -Z
|
|
# regardless of which side it spawned on. One policy can then play both teams.
|
|
# The same rotation must be inverted when interpreting actions (see canon —
|
|
# it is its own inverse).
|
|
|
|
# Normalization scales. Arena bounds: goals at z ≈ ±15.56, ship spawns at
|
|
# z = ±12; positions are soft-normalized to roughly [-1, 1].
|
|
const POSITION_SCALE := Vector3(20.0, 10.0, 20.0)
|
|
const BALL_SPEED_SCALE := 30.0
|
|
const GOAL_DISTANCE_SCALE := 40.0
|
|
|
|
# Number of floats build() returns; the policy input size.
|
|
const SIZE := 31
|
|
|
|
|
|
# 180° rotation about Y for team 1; identity for team 0. A proper rotation
|
|
# (preserves handedness), and its own inverse — used for both observations
|
|
# and mapping canonical-frame actions back to world intent.
|
|
static func canon(v: Vector3, team: int) -> Vector3:
|
|
return v if team == 0 else Vector3(-v.x, v.y, -v.z)
|
|
|
|
|
|
# attack_goal_position: centre of the goal this ship is trying to score in
|
|
# (the goal whose `team` == the opponent's team).
|
|
static func build(ship: Ship, opponent: Ship, ball: RigidBody3D, attack_goal_position: Vector3) -> Array:
|
|
var team := ship.team
|
|
var obs := []
|
|
|
|
# Own kinematics
|
|
_append(obs, canon(ship.global_position, team) / POSITION_SCALE)
|
|
_append(obs, canon(-ship.global_transform.basis.z, team)) # forward
|
|
_append(obs, canon(ship.global_transform.basis.y, team)) # up
|
|
_append(obs, canon(ship.linear_velocity, team) / ship.max_speed)
|
|
_append(obs, canon(ship.angular_velocity, team) / ship.max_angular_speed)
|
|
|
|
# Ball, relative to self
|
|
var ball_rel := ball.global_position - ship.global_position
|
|
_append(obs, canon(ball_rel, team) / POSITION_SCALE)
|
|
_append(obs, canon(ball.linear_velocity, team) / BALL_SPEED_SCALE)
|
|
|
|
# Opponent, relative to self (zeros if absent, e.g. a 1-ship drill)
|
|
if is_instance_valid(opponent):
|
|
var opp_rel := opponent.global_position - ship.global_position
|
|
_append(obs, canon(opp_rel, team) / POSITION_SCALE)
|
|
_append(obs, canon(opponent.linear_velocity, team) / ship.max_speed)
|
|
else:
|
|
_append(obs, Vector3.ZERO)
|
|
_append(obs, Vector3.ZERO)
|
|
|
|
# Goal we are attacking, relative to self
|
|
var goal_rel := attack_goal_position - ship.global_position
|
|
_append(obs, canon(goal_rel, team) / POSITION_SCALE)
|
|
obs.append(goal_rel.length() / GOAL_DISTANCE_SCALE)
|
|
|
|
return obs
|
|
|
|
|
|
static func _append(obs: Array, v: Vector3) -> void:
|
|
obs.append(v.x)
|
|
obs.append(v.y)
|
|
obs.append(v.z)
|