class_name Ball extends RigidBody3D # Physics ball. Floor gravity is untouched (gravity_scale in ball.tscn); this # only adds the wall/ceiling "surface pull" (see ArenaBoundary.get_surface_pull) # so the ball can cling near a wall for dribbling or hang against the ceiling # for ceiling shots, plus a safety speed clamp (the ball previously had none). @export_group("Surface Pull") @export var wall_pull_strength = 4.0 # Weaker than the ship's — assist, not adherence @export var wall_pull_range = 2.0 @export var ceiling_pull_strength = 5.5 # Stays below the ball's effective gravity (0.8 * 9.8) @export var ceiling_pull_range = 2.0 # Kept close to ship_observations.gd's BALL_SPEED_SCALE (30.0) so this feature # doesn't push ball velocity further out of the range trained policies expect. const MAX_SPEED := 32.0 var _boundary: ArenaBoundary var _trail: GPUParticles3D func _ready() -> void: _boundary = get_tree().get_first_node_in_group("arena_boundary") if DisplayServer.get_name() == "headless": # _integrate_forces remains active; only the render-side trail updater is # disabled across the many parallel RL environments. set_physics_process(false) else: _build_trail() func _physics_process(_delta: float) -> void: if _trail: var speed_ratio := clampf(linear_velocity.length() / MAX_SPEED, 0.0, 1.0) _trail.emitting = speed_ratio > 0.12 _trail.amount_ratio = smoothstep(0.12, 1.0, speed_ratio) func _build_trail() -> void: var mat := StandardMaterial3D.new() mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED mat.billboard_mode = BaseMaterial3D.BILLBOARD_ENABLED mat.albedo_color = Color(0.55, 0.85, 1.0, 0.42) mat.emission_enabled = true mat.emission = Color(0.35, 0.72, 1.0) mat.emission_energy_multiplier = 1.8 var quad := QuadMesh.new() quad.size = Vector2(0.22, 0.22) quad.material = mat var process := ParticleProcessMaterial.new() process.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_SPHERE process.emission_sphere_radius = 0.35 process.gravity = Vector3.ZERO process.scale_min = 0.35 process.scale_max = 1.0 _trail = GPUParticles3D.new() _trail.name = "BallTrail" _trail.amount = 48 _trail.lifetime = 0.48 _trail.local_coords = false _trail.process_material = process _trail.draw_pass_1 = quad _trail.visibility_aabb = AABB(Vector3(-18, -18, -18), Vector3(36, 36, 36)) _trail.emitting = false add_child(_trail) func _integrate_forces(state: PhysicsDirectBodyState3D) -> void: if _boundary: var pull := _boundary.get_surface_pull( global_position, wall_pull_strength, wall_pull_range, ceiling_pull_strength, ceiling_pull_range ) state.apply_central_force(pull * mass) if state.linear_velocity.length() > MAX_SPEED: state.linear_velocity = state.linear_velocity.normalized() * MAX_SPEED