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