Files
dauphincraft/scripts/mobs/FishSchool.gd

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