【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);
...
}