前言
欢迎来到【制作100个Unity游戏】系列!本系列将引导您一步步学习如何使用Unity开发各种类型的游戏。在这第24篇中,我们将探索如何用unity制作一个3D动物AI生态系统游戏,我会附带项目源码,以便你们更好理解它。
添加捕食者
修改AnimalState,为了方便我就直接贴出全部代码吧,大概就是在原基础上添加了追逐状态和事件处理
using System.Collections; using UnityEngine; using UnityEngine.AI; // 定义动物状态的枚举类型 public enum AnimalState { Idle, // 空闲状态 Moving, // 移动状态 Chase // 追逐状态 } // 必须附加到具有 NavMeshAgent 组件的游戏对象上 [RequireComponent(typeof(NavMeshAgent))] public class Animal : MonoBehaviour { [Header("动物一次移动的距离"), SerializeField] private float wanderDistance = 50f; [Header("动物的行走速度"), SerializeField] private float walkSpeed = 5f; [Header("动物行走的最大时间"), SerializeField] private float maxWalkTime = 6f; [Header("动物休息的时间"), SerializeField] private float idleTime = 5f; [Header("奔跑速度"), SerializeField] private float runSpeed = 8f; [Header("生命值"), SerializeField] private int health = 10; protected NavMeshAgent navMeshAgent; // 存储 NavMeshAgent 组件的引用 protected AnimalState currentState = AnimalState.Idle; // 存储当前动物的状态,默认为 Idle private void Start() { InitialiseAnimal(); // 初始化动物 } // 初始化动物 protected virtual void InitialiseAnimal() { navMeshAgent = GetComponent<NavMeshAgent>(); // 获取 NavMeshAgent 组件的引用 navMeshAgent.speed = walkSpeed; // 设置动物的行走速度 currentState = AnimalState.Idle; // 将当前状态设置为 Idle UpdateState(); // 更新动物的状态 } // 更新动物的状态 protected virtual void UpdateState() { switch (currentState) { case AnimalState.Idle: // 如果当前状态是 Idle HandleIdleState(); // 处理空闲状态 break; case AnimalState.Moving: // 如果当前状态是 Moving HandleMovingState(); // 处理移动状态 break; case AnimalState.Chase: // 如果当前状态是 Chase HandleChaseState(); // 处理追逐状态 break; } } // 获取距离起点一定距离内的随机 NavMesh 位置 protected Vector3 GetRandomNavMeshPosition(Vector3 origin, float distance) { for (int i = 0; i < 5; i++) { Vector3 randomDirection = Random.insideUnitSphere * distance; // 在球形区域内生成一个随机方向 randomDirection += origin; // 将随机方向与起点相加,计算出随机点的位置 NavMeshHit navMeshHit; if (NavMesh.SamplePosition(randomDirection, out navMeshHit, distance, NavMesh.AllAreas)) { // 如果找到了 NavMesh 上的可行走位置,返回该位置 return navMeshHit.position; } } return origin; } protected virtual void HandleChaseState() { StopAllCoroutines(); } // 处理空闲状态 protected virtual void HandleIdleState() { StartCoroutine(WaitToMove()); // 等待一段时间后转换到移动状态 } // 等待一段时间后转换到移动状态 private IEnumerator WaitToMove() { float waitTime = Random.Range(idleTime / 2, idleTime * 2); // 随机生成等待时间 yield return new WaitForSeconds(waitTime); // 等待一段时间 Vector3 randomDestination = GetRandomNavMeshPosition(transform.position, wanderDistance); // 获取随机位置 navMeshAgent.SetDestination(randomDestination); // 设置 NavMeshAgent 的目标位置 SetState(AnimalState.Moving); // 将状态设置为 Moving } // 处理移动状态 protected virtual void HandleMovingState() { StartCoroutine(WaitToReachDestination()); // 等待到达目的地后转换到空闲状态 } // 等待到达目的地后转换到空闲状态 private IEnumerator WaitToReachDestination() { float startTime = Time.time; // 记录开始移动的时间 while (navMeshAgent.pathPending || navMeshAgent.remainingDistance > navMeshAgent.stoppingDistance && navMeshAgent.isActiveAndEnabled) // 当还未到达目的地时循环 { if (Time.time - startTime >= maxWalkTime) // 如果超过了最大行走时间 { navMeshAgent.ResetPath(); // 重置路径 SetState(AnimalState.Idle); // 将状态设置为 Idle yield break; // 结束函数的执行 } CheckChaseConditions(); // 检查是否满足追逐条件 yield return null; // 等待下一帧 } // 到达目的地后将状态设置为 Idle SetState(AnimalState.Idle); } // 将动物的状态设置为指定的状态 protected void SetState(AnimalState newState) { if (currentState == newState) // 如果新状态与当前状态相同,直接返回 { return; } currentState = newState; // 更新当前状态 OnStateChanged(newState); // 触发状态改变事件 } // 状态改变事件的处理函数 protected virtual void OnStateChanged(AnimalState newState) { if (newState == AnimalState.Moving) navMeshAgent.speed = walkSpeed; if (newState == AnimalState.Chase) navMeshAgent.speed = runSpeed; UpdateState(); } // 接收到伤害时的处理函数 public virtual void RecieveDamage(int damage) { health -= damage; if (health <= 0) Die(); } // 死亡时的处理函数 protected virtual void Die() { StopAllCoroutines(); Destroy(gameObject); } // 检查是否满足追逐条件的函数 protected virtual void CheckChaseConditions() { } }
新增Prey,基础Animal,定义猎物的行为脚本
using System.Collections; using UnityEngine; // 表示一种猎物动物 public class Prey : Animal { [SerializeField, Header("检测范围")] private float detectionRange = 10f; [SerializeField, Header("逃离的最大距离")] private float escapeMaxDistance = 80f; private Predator currentPredator = null; // 当前追逐的捕食者 // 警报猎物,传入捕食者对象 public void AlertPrey(Predator predator) { SetState(AnimalState.Chase); // 设置状态为追逐 currentPredator = predator; // 设置当前捕食者 StartCoroutine(RunFromPredator()); // 开始逃离捕食者 } // 逃离捕食者的协程 private IEnumerator RunFromPredator() { // 等待捕食者进入检测范围 while (currentPredator == null || Vector3.Distance(transform.position, currentPredator.transform.position) > detectionRange) { yield return null; } // 捕食者进入检测范围,开始逃离 while (currentPredator != null && Vector3.Distance(transform.position, currentPredator.transform.position) <= detectionRange) { RunAwayFromPredator(); // 逃离捕食者 yield return null; } // 捕食者超出范围,向最终位置逃离并回到空闲状态 if (!navMeshAgent.pathPending && navMeshAgent.remainingDistance > navMeshAgent.stoppingDistance) { yield return null; } SetState(AnimalState.Idle); // 设置状态为空闲 } // 逃离捕食者的方法 private void RunAwayFromPredator() { if (navMeshAgent != null && navMeshAgent.isActiveAndEnabled) { if (!navMeshAgent.pathPending && navMeshAgent.remainingDistance < navMeshAgent.stoppingDistance) { // 计算逃离方向,即当前位置与捕食者位置的反向向量 Vector3 runDirection = transform.position - currentPredator.transform.position; // 根据逃离方向和逃离的最大距离,计算逃离的目标位置 Vector3 escapeDestination = transform.position + runDirection.normalized * (escapeMaxDistance * 2); // 设置导航代理的目标位置为随机的逃离目标位置 navMeshAgent.SetDestination(GetRandomNavMeshPosition(escapeDestination, escapeMaxDistance)); } } } // 处理猎物死亡的方法 protected override void Die() { StopAllCoroutines(); base.Die(); } // 在场景视图中绘制检测范围的方法 private void OnDrawGizmos() { Gizmos.color = Color.green; Gizmos.DrawWireSphere(transform.position, detectionRange); } }
新增Predator,同样继承Animal,定义捕食者的行为脚本
using System.Collections; using UnityEngine; // 表示一种捕食者动物 public class Predator : Animal { [SerializeField, Header("检测范围,用于检测猎物")] private float detectionRange = 20f; [SerializeField, Header("追逐的最长时间")] private float maxChaseTime = 10f; [SerializeField, Header("咬伤伤害值")] private int biteDamage = 3; [SerializeField, Header("咬伤冷却时间")] private float biteCooldown = 1f; private Prey currentChaseTarget; // 当前追逐的猎物 // 检查追逐条件 protected override void CheckChaseConditions() { if (currentChaseTarget) return; // 获取检测范围内的所有 collider,存储到数组中 Collider[] colliders = new Collider[10]; int numColliders = Physics.OverlapSphereNonAlloc(transform.position, detectionRange, colliders); for (int i = 0; i < numColliders; i++) { // 如果该 collider 绑定的游戏物体上有 Prey 组件,则该游戏物体为猎物 Prey prey = colliders[i].GetComponent<Prey>(); if (prey != null) { StartChase(prey); // 开始追逐该猎物 return; } } currentChaseTarget = null; } // 开始追逐指定的猎物 private void StartChase(Prey prey) { currentChaseTarget = prey; SetState(AnimalState.Chase); // 设置状态为追逐状态 } // 处理追逐状态 protected override void HandleChaseState() { if (currentChaseTarget != null) { currentChaseTarget.AlertPrey(this); // 提醒猎物有捕食者正在追逐它 StartCoroutine(ChasePrey()); // 开始追逐猎物的协程 } else { SetState(AnimalState.Idle); // 如果没有猎物,则回到空闲状态 } } // 追逐猎物的协程 private IEnumerator ChasePrey() { float startTime = Time.time; while (currentChaseTarget != null && Vector3.Distance(transform.position, currentChaseTarget.transform.position) > navMeshAgent.stoppingDistance) { if (Time.time - startTime >= maxChaseTime || currentChaseTarget == null) { StopChase(); // 如果超时或者目标不存在,则停止追逐 yield break; } SetState(AnimalState.Chase); // 设置状态为追逐状态 navMeshAgent.SetDestination(currentChaseTarget.transform.position); // 设置目标位置为猎物的位置 yield return null; } // 如果猎物还存在,则对猎物造成伤害 if (currentChaseTarget) { currentChaseTarget.RecieveDamage(biteDamage); } yield return new WaitForSeconds(biteCooldown); // 等待咬伤冷却时间 currentChaseTarget = null; HandleChaseState(); // 继续处理追逐状态 CheckChaseConditions(); // 检查是否可以继续追逐其他猎物 } // 停止追逐 private void StopChase() { navMeshAgent.ResetPath(); currentChaseTarget = null; SetState(AnimalState.Moving); // 设置状态为移动状态 } // 绘制检测范围的 gizmos private void OnDrawGizmos() { Gizmos.color = Color.red; Gizmos.DrawWireSphere(transform.position, detectionRange); } }
挂载新脚本,并配置参数,这里可以设置捕猎者速度大于动物,确保可以狩猎成功
记得给动物添加碰撞体,不然捕猎者无法检测到动物发起攻击
同样修改动物和捕猎者导航网格停止距离的值,这里我设置为3
注意
:导航网格停止距离的值默认为0,如果不设置会影响动物的逃跑功能和捕猎者的咬伤功能
羊参数配置
狼参数配置
效果
动画控制
修改Animal
private Animator animator; animator = GetComponentInChildren<Animator>(); // 状态改变事件的处理函数 protected virtual void OnStateChanged(AnimalState newState) { animator?.CrossFadeInFixedTime(newState.ToString(), 0.5f); //... }
看看CrossFadeInFixedTime官方文档解释,实现一个淡入淡出的动画效果
效果
源码
源码在最后一期