generated from krampus/template-godot4
214 lines
6.7 KiB
GDScript
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
|