342 lines
9.4 KiB
GDScript
342 lines
9.4 KiB
GDScript
class_name ObjectMotionComponent
|
|
extends Node
|
|
## Component providing procedural movement (Rotate, Bob/Float, Waypoint paths)
|
|
## and interactive triggers for placed world objects.
|
|
## Supports platform velocity for player character riding and multiplayer timestamp sync.
|
|
|
|
signal motion_started
|
|
signal motion_stopped
|
|
signal waypoint_reached(index: int)
|
|
|
|
enum MotionType { NONE, ROTATE, BOB, WAYPOINT }
|
|
enum TriggerMode { ALWAYS, PROXIMITY, INTERACT, SIGNAL }
|
|
enum LoopMode { CYCLE, PING_PONG, ONCE }
|
|
|
|
var motion_type: String = "none"
|
|
var trigger_mode: String = "always"
|
|
var loop_mode: String = "cycle"
|
|
|
|
# Rotation parameters
|
|
var rotation_axis: Vector3 = Vector3.UP
|
|
var rotation_speed: float = 45.0 # degrees per second
|
|
|
|
# Bob / Float parameters
|
|
var bob_axis: Vector3 = Vector3.UP
|
|
var bob_amplitude: float = 0.5
|
|
var bob_frequency: float = 1.0
|
|
|
|
# Waypoint parameters
|
|
var waypoints: Array[Vector3] = []
|
|
var move_speed: float = 2.0
|
|
var pause_time: float = 0.0
|
|
|
|
# Trigger parameters
|
|
var trigger_radius: float = 3.5
|
|
var is_active: bool = true
|
|
|
|
# Runtime state
|
|
var _target_node: Node3D = null
|
|
var _initial_pos: Vector3 = Vector3.ZERO
|
|
var _initial_basis: Basis = Basis.IDENTITY
|
|
var _time: float = 0.0
|
|
var _current_waypoint_idx: int = 0
|
|
var _waypoint_forward: bool = true
|
|
var _pause_timer: float = 0.0
|
|
var linear_velocity: Vector3 = Vector3.ZERO
|
|
var angular_velocity: Vector3 = Vector3.ZERO
|
|
|
|
|
|
func _ready() -> void:
|
|
add_to_group("moving_objects")
|
|
if _target_node == null:
|
|
_target_node = get_parent() as Node3D
|
|
if _target_node != null:
|
|
_initial_pos = _target_node.position
|
|
_initial_basis = _target_node.basis
|
|
if trigger_mode != "always":
|
|
is_active = false
|
|
|
|
|
|
func setup(p_target: Node3D, config: Dictionary = {}) -> void:
|
|
_target_node = p_target
|
|
if _target_node != null:
|
|
_initial_pos = _target_node.position
|
|
_initial_basis = _target_node.basis
|
|
_time = 0.0
|
|
_pause_timer = 0.0
|
|
_current_waypoint_idx = 0
|
|
_waypoint_forward = true
|
|
linear_velocity = Vector3.ZERO
|
|
angular_velocity = Vector3.ZERO
|
|
if not config.is_empty():
|
|
load_from_dict(config)
|
|
|
|
|
|
func _physics_process(delta: float) -> void:
|
|
if _target_node == null or delta <= 0.0:
|
|
return
|
|
|
|
if trigger_mode.to_lower() == "proximity":
|
|
_process_proximity_check()
|
|
|
|
if not is_active:
|
|
linear_velocity = Vector3.ZERO
|
|
angular_velocity = Vector3.ZERO
|
|
_apply_velocities_to_colliders()
|
|
return
|
|
|
|
_time += delta
|
|
var prev_pos := _target_node.position
|
|
|
|
match motion_type.to_lower():
|
|
"rotate":
|
|
_process_rotate(delta)
|
|
"bob", "oscillate":
|
|
_process_bob(delta)
|
|
"waypoint", "patrol":
|
|
_process_waypoint(delta)
|
|
|
|
linear_velocity = (_target_node.position - prev_pos) / delta
|
|
_apply_velocities_to_colliders()
|
|
|
|
|
|
func _process_proximity_check() -> void:
|
|
if _target_node == null:
|
|
return
|
|
var target_pos := _target_node.global_position if _target_node.is_inside_tree() else _target_node.position
|
|
var players := get_tree().get_nodes_in_group("players")
|
|
var nearby := false
|
|
for p in players:
|
|
if p is Node3D:
|
|
var p_pos := (p as Node3D).global_position if (p as Node3D).is_inside_tree() else (p as Node3D).position
|
|
if p_pos.distance_to(target_pos) <= trigger_radius:
|
|
nearby = true
|
|
break
|
|
if nearby and not is_active:
|
|
start_motion()
|
|
elif not nearby and is_active:
|
|
stop_motion()
|
|
|
|
|
|
func check_proximity_to_position(pos: Vector3) -> bool:
|
|
if _target_node == null:
|
|
return false
|
|
var target_pos := _target_node.global_position if _target_node.is_inside_tree() else _target_node.position
|
|
var dist := target_pos.distance_to(pos)
|
|
var in_range := dist <= trigger_radius
|
|
if in_range and not is_active:
|
|
start_motion()
|
|
elif not in_range and is_active:
|
|
stop_motion()
|
|
return in_range
|
|
|
|
|
|
func on_interact() -> void:
|
|
if trigger_mode.to_lower() == "interact":
|
|
toggle_motion()
|
|
|
|
|
|
func on_signal(_sig_name: String = "") -> void:
|
|
if trigger_mode.to_lower() == "signal":
|
|
toggle_motion()
|
|
|
|
|
|
func _process_rotate(delta: float) -> void:
|
|
var rad_speed := deg_to_rad(rotation_speed)
|
|
var rot_delta := rad_speed * delta
|
|
_target_node.rotate(rotation_axis.normalized(), rot_delta)
|
|
angular_velocity = rotation_axis.normalized() * rad_speed
|
|
|
|
|
|
func _process_bob(_delta: float) -> void:
|
|
var offset: float = sin(_time * bob_frequency * TAU) * bob_amplitude
|
|
_target_node.position = _initial_pos + (bob_axis.normalized() * offset)
|
|
|
|
|
|
func _process_waypoint(delta: float) -> void:
|
|
if waypoints.is_empty():
|
|
return
|
|
|
|
if _pause_timer > 0.0:
|
|
_pause_timer -= delta
|
|
return
|
|
|
|
var target_wp := waypoints[_current_waypoint_idx]
|
|
var current_pos := _target_node.position
|
|
var to_target := target_wp - current_pos
|
|
var dist := to_target.length()
|
|
|
|
if dist < 0.001:
|
|
_advance_waypoint()
|
|
target_wp = waypoints[_current_waypoint_idx]
|
|
to_target = target_wp - current_pos
|
|
dist = to_target.length()
|
|
|
|
var step := move_speed * delta
|
|
|
|
if dist <= step:
|
|
_target_node.position = target_wp
|
|
waypoint_reached.emit(_current_waypoint_idx)
|
|
_pause_timer = pause_time
|
|
_advance_waypoint()
|
|
else:
|
|
_target_node.position += to_target.normalized() * step
|
|
|
|
|
|
func _advance_waypoint() -> void:
|
|
match loop_mode.to_lower():
|
|
"cycle":
|
|
_current_waypoint_idx = (_current_waypoint_idx + 1) % waypoints.size()
|
|
"ping_pong":
|
|
if _waypoint_forward:
|
|
if _current_waypoint_idx >= waypoints.size() - 1:
|
|
_waypoint_forward = false
|
|
_current_waypoint_idx = maxi(0, waypoints.size() - 2)
|
|
else:
|
|
_current_waypoint_idx += 1
|
|
else:
|
|
if _current_waypoint_idx <= 0:
|
|
_waypoint_forward = true
|
|
_current_waypoint_idx = mini(1, waypoints.size() - 1)
|
|
else:
|
|
_current_waypoint_idx -= 1
|
|
"once":
|
|
if _current_waypoint_idx < waypoints.size() - 1:
|
|
_current_waypoint_idx += 1
|
|
else:
|
|
is_active = false
|
|
motion_stopped.emit()
|
|
|
|
|
|
func _apply_velocities_to_colliders() -> void:
|
|
if _target_node == null:
|
|
return
|
|
var sb := _target_node.find_child("collision", true, false) as StaticBody3D
|
|
if sb == null:
|
|
sb = _target_node.find_child("*", true, false) as StaticBody3D
|
|
if sb != null:
|
|
sb.constant_linear_velocity = linear_velocity
|
|
sb.constant_angular_velocity = angular_velocity
|
|
|
|
|
|
func start_motion() -> void:
|
|
is_active = true
|
|
motion_started.emit()
|
|
|
|
|
|
func stop_motion() -> void:
|
|
is_active = false
|
|
linear_velocity = Vector3.ZERO
|
|
angular_velocity = Vector3.ZERO
|
|
_apply_velocities_to_colliders()
|
|
motion_stopped.emit()
|
|
|
|
|
|
func toggle_motion() -> void:
|
|
if is_active:
|
|
stop_motion()
|
|
else:
|
|
start_motion()
|
|
|
|
|
|
func reverse_motion() -> void:
|
|
rotation_speed = -rotation_speed
|
|
_waypoint_forward = not _waypoint_forward
|
|
|
|
|
|
func sync_to_timestamp(server_time: float) -> void:
|
|
_time = server_time
|
|
if _target_node == null:
|
|
return
|
|
match motion_type.to_lower():
|
|
"bob", "oscillate":
|
|
var offset: float = sin(_time * bob_frequency * TAU) * bob_amplitude
|
|
_target_node.position = _initial_pos + (bob_axis.normalized() * offset)
|
|
"rotate":
|
|
var angle: float = deg_to_rad(rotation_speed) * server_time
|
|
_target_node.basis = _initial_basis.rotated(rotation_axis.normalized(), angle)
|
|
"waypoint", "patrol":
|
|
if waypoints.size() >= 2 and move_speed > 0.0:
|
|
_sync_waypoints(server_time)
|
|
|
|
|
|
func _sync_waypoints(t: float) -> void:
|
|
var total_dur := 0.0
|
|
var seg_durs: Array[float] = []
|
|
var count := waypoints.size()
|
|
var seg_count := count if loop_mode == "cycle" else count - 1
|
|
for i in range(seg_count):
|
|
var p0: Vector3 = waypoints[i]
|
|
var p1: Vector3 = waypoints[(i + 1) % count]
|
|
var d := p0.distance_to(p1)
|
|
var dur := (d / move_speed) + pause_time
|
|
seg_durs.append(dur)
|
|
total_dur += dur
|
|
if total_dur <= 0.0:
|
|
return
|
|
var progress := fmod(t, total_dur)
|
|
var accumulated := 0.0
|
|
for i in range(seg_count):
|
|
var dur := seg_durs[i]
|
|
if progress <= accumulated + dur:
|
|
var seg_t := (progress - accumulated) / maxf(0.001, (dur - pause_time))
|
|
seg_t = clampf(seg_t, 0.0, 1.0)
|
|
var p0: Vector3 = waypoints[i]
|
|
var p1: Vector3 = waypoints[(i + 1) % count]
|
|
_target_node.position = p0.lerp(p1, seg_t)
|
|
_current_waypoint_idx = (i + 1) % count
|
|
break
|
|
accumulated += dur
|
|
|
|
|
|
func to_dict() -> Dictionary:
|
|
var wps_serialized: Array = []
|
|
for wp in waypoints:
|
|
wps_serialized.append([wp.x, wp.y, wp.z])
|
|
return {
|
|
"motion_type": motion_type,
|
|
"trigger_mode": trigger_mode,
|
|
"loop_mode": loop_mode,
|
|
"rotation_axis": [rotation_axis.x, rotation_axis.y, rotation_axis.z],
|
|
"rotation_speed": rotation_speed,
|
|
"bob_axis": [bob_axis.x, bob_axis.y, bob_axis.z],
|
|
"bob_amplitude": bob_amplitude,
|
|
"bob_frequency": bob_frequency,
|
|
"move_speed": move_speed,
|
|
"pause_time": pause_time,
|
|
"waypoints": wps_serialized,
|
|
"trigger_radius": trigger_radius,
|
|
"is_active": is_active,
|
|
}
|
|
|
|
|
|
func load_from_dict(d: Dictionary) -> void:
|
|
motion_type = str(d.get("motion_type", "none"))
|
|
trigger_mode = str(d.get("trigger_mode", "always"))
|
|
loop_mode = str(d.get("loop_mode", "cycle"))
|
|
rotation_speed = float(d.get("rotation_speed", 45.0))
|
|
bob_amplitude = float(d.get("bob_amplitude", 0.5))
|
|
bob_frequency = float(d.get("bob_frequency", 1.0))
|
|
move_speed = float(d.get("move_speed", 2.0))
|
|
pause_time = float(d.get("pause_time", 0.0))
|
|
trigger_radius = float(d.get("trigger_radius", 3.5))
|
|
is_active = bool(d.get("is_active", trigger_mode == "always"))
|
|
|
|
if d.has("rotation_axis"):
|
|
var a = d["rotation_axis"]
|
|
if a is Array and a.size() >= 3:
|
|
rotation_axis = Vector3(a[0], a[1], a[2])
|
|
|
|
if d.has("bob_axis"):
|
|
var b = d["bob_axis"]
|
|
if b is Array and b.size() >= 3:
|
|
bob_axis = Vector3(b[0], b[1], b[2])
|
|
|
|
if d.has("waypoints"):
|
|
waypoints.clear()
|
|
for wp in d["waypoints"]:
|
|
if wp is Array and wp.size() >= 3:
|
|
waypoints.append(Vector3(wp[0], wp[1], wp[2]))
|
|
elif wp is Vector3:
|
|
waypoints.append(wp)
|