【Unity】动作游戏开发实战详细分析-16-敌人AI设计

【Unity】动作游戏开发实战详细分析-16-敌人AI设计


基本思想

本文来实现简单的敌人AI,使用协程来开发AI。如果想要使用行为树插件可自行学习使用。

代码实现

敌人的目标信息结构

用于存储所有的敌人可攻击的目标对象信息,包括游戏对象以及仇恨值。

并提供只读的目标列表,通过函数进行注册与反注册

public struct EnemyTargetInfo//目标信息结构
{
  public GameObject GameObject { get; set; }//目标
  public float Hatred { get; set; }//仇恨值
}
public class EnemyTargets
{
  static EnemyTargets mInstance;
  public static EnemyTargets Instance { get { return mInstance ?? (mInstance = new EnemyTargets()); } }

  readonly List<EnemyTargetInfo> mTargetList = new List<EnemyTargetInfo>();
  public IReadOnlyList<EnemyTargetInfo> TargetList { get { return mTargetList; } }//目标列表


  public void RegistTarget(GameObject target, float hatred)//注册目标
  {
    mTargetList.Add(new EnemyTargetInfo() { GameObject = target, Hatred = hatred });
  }

  public void UnregistTarget(GameObject target)//反注册目标
  {
    mTargetList.RemoveAll(m => m.GameObject == target);
  }
}

敌人共享数据段

用于所有敌人AI共享数据

public class EasyEnemyShareMemory
{
  static EasyEnemyShareMemory mInstance;//单例
  public static EasyEnemyShareMemory Instance { get { return mInstance ?? (mInstance = new EasyEnemyShareMemory()); } }

  int mAttackerCount;
  public int AttackCount { get { return mAttackerCount; } }//当前攻击者计数


  public void NoticeAttack()//通知攻击
  {
    mAttackerCount++;
  }

  public void EndOfAttack()//结束攻击
  {
    mAttackerCount--;
  }
}

简单的敌人AI

基本字段

  • 激活范围
  • 攻击范围
  • 移动速度
  • 当前的行为协程
  • 当前目标

行为逻辑说明:

游戏开始时,开启等待协程,原地等待,并不断检测是否有目标进入激活范围。如果进入激活范围,则停止当前协程,并进入激活协程,在激活协程中,通过共享数据段新增数据,并等待攻击协程。通过攻击协程进行攻击指令的触发,不断循环。

public class EasyEnemy : MonoBehaviour
{
  public Animator animator;
  public float activeRange = 8f;//激活范围
  public float attackRange = 2f;//攻击范围
  public float speed = 20f;//移动速度
  Coroutine mBehaviourCoroutine;//主协程
  GameObject mCurrentTarget;//当前目标


  void Start()
  {
    mBehaviourCoroutine = StartCoroutine(StandByBehaviour());
  }
  void Hit()
  {

  }

  IEnumerator StandByBehaviour()
  {
    var whileFlag = true;
    while (whileFlag)
    {
      for (int i = 0, iMax = EnemyTargets.Instance.TargetList.Count; i < iMax; i++)
      {
        var target = EnemyTargets.Instance.TargetList[i];
        if (IsInActiveRange(target.GameObject.transform))//有目标进入激活范围
        {
          StopCoroutine(mBehaviourCoroutine);//关闭当前协程
          mBehaviourCoroutine = StartCoroutine(ActiveBehaviour());//进入激活行为
          whileFlag = false;
          break;
        }
      }
      yield return null;
    }
  }

  IEnumerator ActiveBehaviour()
  {
    animator.SetBool("Moving", false);
    UpdateTarget();//更新目标,仇恨值筛选

    while (mCurrentTarget != null && IsInActiveRange(mCurrentTarget.transform))//确保目标没有离开
    {
      if (EasyEnemyShareMemory.Instance.AttackCount < 1)//攻击数量检测
      {
        EasyEnemyShareMemory.Instance.NoticeAttack();//增加攻击者统计
        yield return AttackBehaviour(mCurrentTarget.transform);//攻击
        EasyEnemyShareMemory.Instance.EndOfAttack();//减少攻击者统计
      }

      yield return null;
    }
    StartCoroutine(StandByBehaviour());//结束后回到待机行为
  }

  IEnumerator AttackBehaviour(Transform target)
  {
    var flag = true;//确认攻击的flag
    while (!IsInAttackRange(target))
    {//若不在攻击范围则追踪
      if (!IsInActiveRange(target))//若逃离则跳出
      {
        flag = false;
        break;
      }
      var to = (target.position - transform.position);
      to = Vector3.ProjectOnPlane(to, Physics.gravity.normalized).normalized;
      transform.position += to * speed * Time.deltaTime;//执行移动
      transform.forward = to;//更新方向
      animator.SetBool("Moving", true);//更新动画
      yield return null;
    }
    animator.SetBool("Moving", false);
    if (flag)
    {
      animator.SetTrigger("AttackTrigger");//执行攻击
      animator.Update(0);
      const string IDLE_TAG = "Idle";
      yield return new WaitUntil(() => !animator.IsInTransition(0) && animator.GetCurrentAnimatorStateInfo(0).IsTag(IDLE_TAG));
      //动画回到有空闲标签的状态,则退出攻击行为。
    }
  }

  void UpdateTarget()//更新当前目标
  {
    var maxHatred = -1f;
    for (int i = 0, iMax = EnemyTargets.Instance.TargetList.Count; i < iMax; i++)
    {
      var currentTarget = EnemyTargets.Instance.TargetList[i];
      if (currentTarget.Hatred > maxHatred)//筛选最大仇恨值的目标
      {
        if (IsInActiveRange(currentTarget.GameObject.transform))//是否在激活范围
        {
          mCurrentTarget = currentTarget.GameObject;
          maxHatred = currentTarget.Hatred;//更新目标
        }
      }
    }
  }

  //判断激活范围
  bool IsInActiveRange(Transform target)//是否在激活范围内
  {
    return Vector3.Distance(transform.position, target.position) <= activeRange;
  }
  //判断攻击范围
  bool IsInAttackRange(Transform target)//是否在攻击范围内
  {
    return Vector3.Distance(transform.position, target.position) <= attackRange;
  }

  //绘制激活范围
  void OnDrawGizmos()
  {
    Color color = Gizmos.color;
    Gizmos.color = Color.white;
    Gizmos.DrawWireSphere(transform.position, activeRange);
    Gizmos.color = Color.red;
    Gizmos.DrawWireSphere(transform.position, attackRange);
    Gizmos.color = color;
  }
}

可控的随机行为

这里提供一些示例的随机代码

float Random01_Fall()
{
  var r1 = Random.value;
  return Random.Range(r1, 1);
}

float Random01_Rise()
{
  var r1 = Random.value;
  return Random.Range(0, 1 - r1);
}

//模拟正态分布
float Random01_Arc(float averageOffset = 0,float alpha = 2f)
{
  var r1 = Random.value;
  var t1 = Mathf.Lerp(0, 1, r1);
  var t2 = Mathf.Lerp(1, 0, r1);
  var tFinal = Mathf.Lerp(t1, t2, r1) * alpha;
  return Mathf.Lerp(r1, 0.5f, tFinal) + averageOffset;
}
//不会与上一次重复
int EliminateRepeatRandom(int last,int min,int max)
{
  var current = Random.Range(min, max);
  if (last == current)
  {
    return (current + (int)Mathf.Sign(Random.value) * Random.Range(min + 1, max - 1)) % max;
  }
  else
    return current;
}

场景信息获取

AI编写时,经常有需要获取场景信息的需求,例如BOSS飞到场地中央释放技能等等。

之前,我们的角色都是通过SpawnPoint创建,因此,我们只需要通过SpawnPoint绑定场景的配置信息即可。

我们添加一个接口

所以实现了该接口的目标都会实现发送Spawn场景信息

public interface ISpawnPointCallBack
{
    void OnSpawn(SpawnPoint sender);
}

修改SpawnPoint的Spawn函数,并进行回调执行

protected virtual void Spawn()
{
  ...
  var spawnPointCallback = mSpawnedGO.GetComponent<ISpawnPointCallBack>();
  if (spawnPointCallback != null)
    spawnPointCallback.OnSpawn(this);
  ...
}

猜你喜欢

转载自blog.csdn.net/qq_52324195/article/details/125877825