149 lines
4.7 KiB
GDScript
149 lines
4.7 KiB
GDScript
extends Node3D
|
|
|
|
@export var fish_count: int = 12
|
|
@export var school_radius: float = 4.0
|
|
@export var swim_speed: float = 2.5
|
|
|
|
var _fish_meshes: Array[MeshInstance3D] = []
|
|
var _fish_velocities: Array[Vector3] = []
|
|
var _time: float = 0.0
|
|
var _player: CharacterBody3D = null
|
|
var _boid_update_timer: float = 0.0
|
|
|
|
func _ready() -> void:
|
|
_find_player()
|
|
_spawn_fish()
|
|
|
|
|
|
func _find_player() -> void:
|
|
var players := get_tree().get_nodes_in_group("player")
|
|
if players.size() > 0:
|
|
_player = players[0] as CharacterBody3D
|
|
|
|
|
|
func _spawn_fish() -> void:
|
|
var mat := StandardMaterial3D.new()
|
|
mat.albedo_color = Color(0.75, 0.82, 0.95)
|
|
mat.metallic = 0.7
|
|
mat.roughness = 0.3
|
|
|
|
for i: int in range(fish_count):
|
|
var mesh_inst := MeshInstance3D.new()
|
|
var cyl := CylinderMesh.new()
|
|
cyl.top_radius = 0.05
|
|
cyl.bottom_radius = 0.15
|
|
cyl.height = 0.4
|
|
mesh_inst.mesh = cyl
|
|
mesh_inst.material_override = mat
|
|
|
|
# Random position in sphere
|
|
var offset := Vector3(
|
|
randf_range(-school_radius, school_radius),
|
|
randf_range(-school_radius * 0.5, school_radius * 0.5),
|
|
randf_range(-school_radius, school_radius)
|
|
)
|
|
mesh_inst.position = offset
|
|
# Rotate mesh so cone points forward (cylinder is vertical by default)
|
|
mesh_inst.rotation.z = PI * 0.5
|
|
|
|
add_child(mesh_inst)
|
|
_fish_meshes.append(mesh_inst)
|
|
|
|
var rand_vel := Vector3(
|
|
randf_range(-1.0, 1.0),
|
|
randf_range(-0.3, 0.3),
|
|
randf_range(-1.0, 1.0)
|
|
).normalized() * swim_speed
|
|
_fish_velocities.append(rand_vel)
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
_time += delta
|
|
|
|
if _player == null:
|
|
_find_player()
|
|
|
|
# Throttle boid calculations to 10 Hz
|
|
_boid_update_timer += delta
|
|
if _boid_update_timer >= 0.1:
|
|
_update_boids(_boid_update_timer)
|
|
_boid_update_timer = 0.0
|
|
|
|
# Apply velocities every frame for smooth movement
|
|
_apply_velocities(delta)
|
|
|
|
|
|
func _update_boids(dt: float) -> void:
|
|
for i: int in range(_fish_meshes.size()):
|
|
var fish: MeshInstance3D = _fish_meshes[i]
|
|
var vel: Vector3 = _fish_velocities[i]
|
|
|
|
# --- Boids: cohesion + alignment with 3 nearest neighbors ---
|
|
var avg_pos: Vector3 = Vector3.ZERO
|
|
var avg_vel: Vector3 = Vector3.ZERO
|
|
var neighbor_count: int = 0
|
|
|
|
# Sort by distance to find 3 nearest
|
|
var distances: Array = []
|
|
for j: int in range(_fish_meshes.size()):
|
|
if j == i:
|
|
continue
|
|
var d: float = fish.position.distance_to(_fish_meshes[j].position)
|
|
distances.append({"idx": j, "dist": d})
|
|
distances.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a["dist"] < b["dist"])
|
|
|
|
for k: int in range(min(3, distances.size())):
|
|
var ni: int = distances[k]["idx"]
|
|
avg_pos += _fish_meshes[ni].position
|
|
avg_vel += _fish_velocities[ni]
|
|
neighbor_count += 1
|
|
|
|
if neighbor_count > 0:
|
|
avg_pos /= float(neighbor_count)
|
|
avg_vel /= float(neighbor_count)
|
|
var cohesion_force: Vector3 = (avg_pos - fish.position).normalized() * 0.5
|
|
var alignment_force: Vector3 = avg_vel.normalized() * 0.3
|
|
vel += (cohesion_force + alignment_force) * dt * 2.0
|
|
|
|
# --- Global cohesion: return to school center ---
|
|
var center_offset: Vector3 = fish.position
|
|
if center_offset.length() > school_radius:
|
|
vel += -center_offset.normalized() * 1.5 * dt * 4.0
|
|
|
|
# --- Player avoidance (boosted player = bigger + faster flee) ---
|
|
if is_instance_valid(_player):
|
|
var world_fish_pos: Vector3 = global_position + fish.position
|
|
var dist_to_player: float = world_fish_pos.distance_to(_player.global_position)
|
|
var player_boosting: bool = _player.get("is_boosting") if "is_boosting" in _player else false
|
|
var flee_radius: float = 6.0 if player_boosting else 3.0
|
|
var flee_force: float = 9.0 if player_boosting else 4.0
|
|
if dist_to_player < flee_radius:
|
|
var flee: Vector3 = (world_fish_pos - _player.global_position).normalized()
|
|
# Only flee if player is moving toward the school
|
|
var player_vel: Vector3 = _player.velocity if "velocity" in _player else Vector3.ZERO
|
|
var toward: bool = player_vel.dot((world_fish_pos - _player.global_position).normalized()) > 0.2
|
|
if toward or player_boosting:
|
|
vel += flee * flee_force * dt * (flee_radius - dist_to_player)
|
|
|
|
# Clamp speed
|
|
if vel.length() > swim_speed * 1.5:
|
|
vel = vel.normalized() * swim_speed * 1.5
|
|
if vel.length() < 0.3:
|
|
vel = vel.normalized() * 0.3
|
|
|
|
_fish_velocities[i] = vel
|
|
|
|
|
|
func _apply_velocities(delta: float) -> void:
|
|
for i: int in range(_fish_meshes.size()):
|
|
var fish: MeshInstance3D = _fish_meshes[i]
|
|
var vel: Vector3 = _fish_velocities[i]
|
|
fish.position += vel * delta
|
|
|
|
# Rotate mesh to face velocity direction
|
|
if vel.length_squared() > 0.001:
|
|
var look_pos: Vector3 = fish.position + vel.normalized()
|
|
fish.look_at(look_pos, Vector3.UP)
|
|
# Compensate for the mesh's local rotation offset
|
|
fish.rotation.z += PI * 0.5
|