feat: add critter herd behavior

This commit is contained in:
Alexa Amundson
2025-11-25 14:50:20 -06:00
parent ffc2842bd0
commit fe6f35597b
3 changed files with 217 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
using UnityEngine;
namespace BlackRoad.Worldbuilder.Life
{
/// <summary>
/// Simple movement agent for critters. Handles forward walking and
/// blends in optional steering from a HerdMember component when present.
/// </summary>
[RequireComponent(typeof(CharacterController))]
public class CritterAgent : MonoBehaviour
{
[Header("Movement")]
[SerializeField] private float walkSpeed = 2f;
[SerializeField] private float turnSpeed = 120f;
private Vector3 _velocity;
private CharacterController _controller;
private HerdMember _herdMember;
public Vector3 Velocity => _velocity;
private void Awake()
{
_controller = GetComponent<CharacterController>();
_herdMember = GetComponent<HerdMember>();
}
private void Update()
{
WalkRoutine();
}
/// <summary>
/// Sets the heading the critter should look toward.
/// </summary>
/// <param name="direction">Desired facing direction.</param>
public void SetHeading(Vector3 direction)
{
if (direction.sqrMagnitude < 0.0001f) return;
direction.y = 0f;
Quaternion target = Quaternion.LookRotation(direction.normalized, Vector3.up);
transform.rotation = Quaternion.RotateTowards(transform.rotation, target, turnSpeed * Time.deltaTime);
}
/// <summary>
/// Walk forward while optionally blending in herd steering.
/// </summary>
private void WalkRoutine()
{
Vector3 moveDir = transform.forward;
// Optional herd steering
if (_herdMember != null && _herdMember.SteerOffset.sqrMagnitude > 0.0001f)
{
// Blend forward direction with herd steering
moveDir = (moveDir + _herdMember.SteerOffset).normalized;
}
// Apply move
Vector3 move = moveDir * walkSpeed;
_velocity.x = move.x;
_velocity.z = move.z;
if (_controller != null)
{
_controller.SimpleMove(_velocity);
}
else
{
transform.position += _velocity * Time.deltaTime;
}
}
}
}

View File

@@ -0,0 +1,61 @@
using System.Collections.Generic;
using UnityEngine;
namespace BlackRoad.Worldbuilder.Life
{
/// <summary>
/// Defines a herd center and radius. Herd members will try
/// to stay roughly within this zone.
/// </summary>
public class CritterHerd : MonoBehaviour
{
[Header("Herd Settings")]
[SerializeField] private float herdRadius = 30f;
[SerializeField] private float cohesionWeight = 1f;
[SerializeField] private float separationDistance = 2f;
[SerializeField] private float separationWeight = 1.5f;
private readonly List<HerdMember> _members = new List<HerdMember>();
public float HerdRadius => herdRadius;
public float CohesionWeight => cohesionWeight;
public float SeparationDistance => separationDistance;
public float SeparationWeight => separationWeight;
public Vector3 Center
{
get
{
if (_members.Count == 0) return transform.position;
Vector3 sum = Vector3.zero;
foreach (var m in _members)
{
if (m != null)
sum += m.transform.position;
}
return sum / _members.Count;
}
}
public IReadOnlyList<HerdMember> Members => _members;
public void Register(HerdMember member)
{
if (member != null && !_members.Contains(member))
_members.Add(member);
}
public void Unregister(HerdMember member)
{
if (member != null)
_members.Remove(member);
}
private void OnDrawGizmosSelected()
{
Gizmos.color = new Color(1f, 1f, 0.5f, 0.3f);
Gizmos.DrawWireSphere(transform.position, herdRadius);
}
}
}

View File

@@ -0,0 +1,81 @@
using UnityEngine;
namespace BlackRoad.Worldbuilder.Life
{
/// <summary>
/// Attached to critters that belong to a herd.
/// Provides a steering offset (cohesion + separation) that
/// can be used by movement logic (e.g. CritterAgent).
/// </summary>
[RequireComponent(typeof(CritterAgent))]
public class HerdMember : MonoBehaviour
{
[SerializeField] private CritterHerd herd;
[SerializeField] private float steerWeight = 0.6f;
public Vector3 SteerOffset { get; private set; }
private void OnEnable()
{
if (herd != null)
herd.Register(this);
}
private void OnDisable()
{
if (herd != null)
herd.Unregister(this);
}
private void LateUpdate()
{
if (herd == null)
{
SteerOffset = Vector3.zero;
return;
}
var members = herd.Members;
if (members == null || members.Count == 0)
{
SteerOffset = Vector3.zero;
return;
}
Vector3 cohesion = Vector3.zero;
Vector3 separation = Vector3.zero;
int neighborCount = 0;
foreach (var m in members)
{
if (m == null || m == this) continue;
Vector3 toNeighbor = m.transform.position - transform.position;
float dist = toNeighbor.magnitude;
// Cohesion: move toward herd center
cohesion += m.transform.position;
neighborCount++;
// Separation: avoid crowding
if (dist < herd.SeparationDistance && dist > 0.001f)
{
separation -= toNeighbor / dist; // push away
}
}
if (neighborCount > 0)
{
cohesion /= neighborCount;
cohesion = (cohesion - transform.position).normalized * herd.CohesionWeight;
}
separation *= herd.SeparationWeight;
Vector3 combined = cohesion + separation;
combined.y = 0f; // stay horizontal
SteerOffset = combined.normalized * steerWeight;
}
}
}