gfolf2/src/ui/3d/projectile_arc/projectile_arc.gd

190 lines
6.0 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
## 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
var _tick_counter := 0
var _debug_points: Array[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 _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
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:
local_gravity = _get_gravity(pos)
# Integrate projectile path
vel += local_gravity * time_step
var next_pos := pos + vel * time_step
# 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:
# TODO: `point` may need to be local
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