gfolf2/src/ui/3d/projectile_arc/projectile_arc.gd
2024-12-17 15:23:27 -07:00

214 lines
6.7 KiB
GDScript

class_name ProjectileArc extends Node3D
## Visually project the arc of a projectile through space.
##
## If this node has any children, they will be positioned wherever the projection ends.
## Constant attrition factor for putting collisions
## The actual velocity attrition is dependent on lots of factors
#const PUTT_ATTRITION := 0.75 # rough?
const PUTT_ATTRITION := 0.8325 # green?
## Initial speed of the projectile, in m/s.
## The projectile's initial direction vector is the negative Z direction relative to this node.
@export var initial_speed := 1.0
@export_category("Projection")
## Time between projection steps, in seconds.
@export var time_step := 0.2
## Maximum number of steps to predict before stopping.
@export var max_steps := 50
## Ticks between each projection update. 0 means update every tick.
@export var ticks_per_update := 0
## If enabled, project a linear putt instead of an arcing shot
@export var putt_projection := false
@export_category("Collision & Physics")
## Enables collision checking. Projection will end at the point where a collision is detected.
## Uses continuous collision detection.
@export var check_collision := true
## Mask for collision checking.
@export_flags_3d_physics var collision_mask := 1 | 2
## Bodies excluded from collision checking.
## This should probably include the ball!
@export var excluded_bodies: Array[CollisionObject3D] = []
## Enables checking local gravity at each point along the trajectory.
## If disabled, global gravity will be used instead.
@export var check_gravity := true
## Improves performance by caching gravity at each point along the projection.
## This can cause problems if there is a moving gravity field.
@export var cache_gravity := true
## Linear damping factor of the shot.
@export var linear_damp := 0.0
var _tick_counter := 0
var _debug_points: Array[Vector3] = []
var _gravity_cache: Array[Vector3] = []
var _cached_pos: Vector3
var _cached_vel: Vector3
@onready var polygon: CSGPolygon3D = %Polygon
@onready var path: Path3D = %Path
@onready var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
@onready var gravity_vec: Vector3 = ProjectSettings.get_setting("physics/3d/default_gravity_vector")
@onready var debug_draw: Control = %DebugDraw
func set_ball(ball: GameBall) -> void:
global_position = ball.global_position
linear_damp = ball.linear_damp
func _process(_delta: float) -> void:
if not visible:
# Don't bother if we're not visible
return
# Short-circuit if we need to wait more ticks
if _tick_counter > 0:
_tick_counter -= 1
return
_tick_counter = ticks_per_update
_debug_points = []
# Rebuild path curve
path.global_basis = Basis.IDENTITY
path.curve.clear_points()
var space := get_world_3d().direct_space_state
var excluded_rid: Array[RID] = []
excluded_rid.assign(excluded_bodies.map(func(k: CollisionObject3D) -> RID: return k.get_rid()))
var pos := global_position
var vel := -global_basis.z * initial_speed
if not cache_gravity or pos != _cached_pos or vel != _cached_vel:
_gravity_cache = []
_cached_pos = pos
_cached_vel = vel
var final_normal: Vector3
for t in range(0, max_steps):
# TODO: smooth curve with bezier handles
path.curve.add_point(pos - global_position)
# Get local gravity if enabled
var local_gravity := gravity * gravity_vec
if check_gravity and Game.settings.projection_gravity:
if t >= len(_gravity_cache):
_gravity_cache.append(_get_gravity(pos))
local_gravity = _gravity_cache[t]
# Integrate projectile path
var next_pos := pos + vel * time_step + 0.5 * local_gravity * time_step * time_step
vel += local_gravity * time_step
vel *= maxf(1 - linear_damp * time_step, 0)
# Collision
if check_collision and Game.settings.projection_collisions:
var ray_params := PhysicsRayQueryParameters3D.create(
pos, next_pos, collision_mask, excluded_rid
)
var collision := space.intersect_ray(ray_params)
if collision:
# Set current position to collision point, so it will be added to the path
pos = collision["position"]
_debug_points.append(pos)
if putt_projection:
@warning_ignore("unsafe_cast")
var norm: Vector3 = (collision["normal"] as Vector3).normalized()
next_pos = pos + norm * 0.05
vel = PUTT_ATTRITION * (vel - 2 * norm * vel.dot(norm))
else:
# End projection!
final_normal = collision["normal"]
break
pos = next_pos
# Add terminal point (possibly collision point)
path.curve.add_point(pos - global_position)
var child_basis := Basis.IDENTITY
if final_normal:
var up := final_normal.normalized()
var forward := Vector3(up.y, -up.x, 0)
var right := up.cross(forward).normalized()
forward = right.cross(up).normalized()
child_basis = Basis(right, up, forward)
# Reposition any children
for n: Node in get_children():
if n is Node3D and n != path:
var node_3d: Node3D = n
node_3d.global_position = pos
node_3d.global_basis = child_basis
(%DebugDraw as CanvasItem).queue_redraw()
func _get_gravity(point: Vector3) -> Vector3:
# Start with global gravity
var local_gravity := gravity * gravity_vec
# TODO this is awful, surely there has to be a better way than this!!!
# Get areas at point
var point_params := PhysicsPointQueryParameters3D.new()
point_params.collide_with_areas = true
point_params.collide_with_bodies = false
point_params.collision_mask = collision_mask
point_params.position = point
var collisions := get_world_3d().direct_space_state.intersect_point(point_params)
var gravity_areas: Array[Area3D] = []
@warning_ignore("unsafe_cast")
gravity_areas.assign(
collisions.map(func(d: Dictionary) -> Area3D: return d["collider"] as Area3D)
)
gravity_areas.sort_custom(func(a: Area3D, b: Area3D) -> bool: return a.priority < b.priority)
# Iteratively integrate gravity
for area: Area3D in gravity_areas:
var point_local := point - area.global_position
var area_gravity: Vector3
if area.gravity_point:
var v := area.transform * area.gravity_direction - point_local
if area.gravity_point_unit_distance > 0:
var v_sq := v.length_squared()
if v_sq > 0:
area_gravity = (
v.normalized()
* area.gravity
* pow(area.gravity_point_unit_distance, 2)
/ v_sq
)
else:
area_gravity = Vector3.ZERO
else:
area_gravity = v.normalized() * area.gravity
else:
area_gravity = area.gravity * area.gravity_direction
match area.gravity_space_override:
Area3D.SPACE_OVERRIDE_COMBINE, Area3D.SPACE_OVERRIDE_COMBINE_REPLACE:
local_gravity += area_gravity
Area3D.SPACE_OVERRIDE_REPLACE_COMBINE, Area3D.SPACE_OVERRIDE_REPLACE:
local_gravity = area_gravity
Area3D.SPACE_OVERRIDE_COMBINE_REPLACE, Area3D.SPACE_OVERRIDE_REPLACE:
break
return local_gravity
func _on_visibility_changed() -> void:
# Force update as soon as visible
_tick_counter = 0