feat(mobs): fish schools + jellyfish + shark + spawner with distance-based life cycle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
123
scripts/mobs/FishSchool.gd
Normal file
123
scripts/mobs/FishSchool.gd
Normal file
@@ -0,0 +1,123 @@
|
||||
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
|
||||
|
||||
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()
|
||||
|
||||
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) * delta * 2.0
|
||||
|
||||
# --- Global cohesion: return to school center ---
|
||||
var center_offset: Vector3 = fish.position # positions are local to school node
|
||||
if center_offset.length() > school_radius:
|
||||
vel += -center_offset.normalized() * 1.5 * delta * 4.0
|
||||
|
||||
# --- Player avoidance ---
|
||||
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)
|
||||
if dist_to_player < 3.0:
|
||||
var flee: Vector3 = (world_fish_pos - _player.global_position).normalized()
|
||||
vel += flee * 4.0 * delta * (3.0 - 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
|
||||
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
|
||||
Reference in New Issue
Block a user