Files
CosmicClash/Game/scripts/ship_observations.gd
T

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)