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