feat: add critter herd behavior
This commit is contained in:
75
Assets/Scripts/Life/CritterAgent.cs
Normal file
75
Assets/Scripts/Life/CritterAgent.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
61
Assets/Scripts/Life/CritterHerd.cs
Normal file
61
Assets/Scripts/Life/CritterHerd.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
Assets/Scripts/Life/HerdMember.cs
Normal file
81
Assets/Scripts/Life/HerdMember.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user