好久没有更新我的博客啦!今天提供三种方法类描述一下我们的敌人AI状态如何完成的哦!
我个人习惯呢!开发项目从难到易的去开发项目。所以写博客时一般把最难的写在最前面哦!因为最难的你都会啦!其余的不更是So Easy !各位读者要习惯啦!当然了你也可以从后往前阅读哦!不多说了,进入主题吧!
方法一:使用FSM有限状态机完成
我们都知道现在的人工智能都很强大的,作为一名程序员,这是你必须会的哦!今天我们就来讲讲敌人的AI的人工智能吧!先来张图片吧!有了图以后才能更好的开发游戏嘛!不知道大家在开发游戏项目中是否是这样呢?
下面呢!我们使用一个坦克的案例来来编写一个属于我们自己的状态机。
坦克要求很简单的。有身子有炮杆就行。
首先创建一个FSM基类:
using UnityEngine;
using System.Collections;
public class FSM : MonoBehaviour
{
protected Transform playerTransform;//玩家坦克
protected Vector3 destPos;
protected GameObject[] pointList;
protected float shootRate;//敌人发射子弹的频率
protected float elapsedTime;//发射的时间计时器
public Transform turrent { get; set; }//敌人炮塔的位置
public Transform bulletSpawnPoint { get; set; }//敌人炮弹的位置
/// <summary>
/// 初始化方法
/// </summary>
protected virtual void Initialize() { }
/// <summary>
/// 更新方法
/// </summary>
protected virtual void FSMUpdate() { }
/// <summary>
/// 固定帧率的更新方法
/// </summary>
protected virtual void FSMFixedUpdate() { }
void Start()
{
Initialize();
}
void Update()
{
FSMUpdate();
}
void FixedUpdate()
{
FSMFixedUpdate();
}
}
然后来一个类来继承父类FSM。
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public enum Transition
{
None = 0,
SawPlayer,
ReacherPlayer,
LoatPlayer,
NoHealth,
}
public enum FSMStateID
{
None = 0,
Patroling,
Chaseing,
Attacking,
Dead,
}
public class AdvanceFSM : FSM
{
private List<FSMState> fsmStates;
private FSMState currentState;
public FSMState CurrentState
{
get
{
return currentState;
}
}
private FSMStateID currentStateID;
public FSMStateID CurrentStateID
{
get
{
return currentStateID;
}
}
public AdvanceFSM()
{
fsmStates = new List<FSMState>();
}
public void AddFSMState(FSMState fsmState)
{
if (fsmState == null)
{
Debug.LogError("FSM ERROR : Null reference is not allowed !");
return;
}
if (fsmStates.Count == 0)
{
fsmStates.Add(fsmState);
currentState = fsmState;
currentStateID = fsmState.ID;
}
//遍历集合中所有的状态
//防止添加重复的状态,那么我们就不再添加了;
foreach (FSMState state in fsmStates)
{
if (state.ID == fsmState.ID)
{
Debug.LogWarning("FSM ERROR :Trying to add a state was already indise the list !");
return;
}
}
fsmStates.Add(fsmState);
}
public void DeleteState(FSMStateID fsmState)
{
if (fsmState == FSMStateID.None)
{
Debug.LogError("FSM ERROR : Null reference is not allowed !");
return;
}
foreach (FSMState state in fsmStates)
{
if (state.ID == fsmState)
{
fsmStates.Remove(state);
return;
}
}
}
public void PreformTransition(Transition trans)
{
if (trans == Transition.None)
{
Debug.LogError("FSM ERROR : Null transition is not allowed");
return;
}
FSMStateID id = currentState.GetOutState(trans);
if (id == FSMStateID.None)
{
Debug.LogWarning("FSM ERROR :Current state does not have a target state for this transition ");
return;
}
currentStateID = id;
foreach (FSMState state in fsmStates)
{
if (state.ID == CurrentStateID)
{
currentState = state;
break;
}
}
}
}
接着来一个具体的坦克类。
using UnityEngine;
using System.Collections;
public class NPCTankCntroller : AdvanceFSM
{
public GameObject bullet;
public int health;
protected override void Initialize()
{
health = 100;
GameObject objPlayer = GameObject.FindGameObjectWithTag("Player");
playerTransform = objPlayer.transform;
turrent = gameObject.transform.GetChild(0).transform;
bulletSpawnPoint = turrent.GetChild(0).transform;
shootRate = 2.0f;
elapsedTime = 0.0f;
ConstructFSM();
}
private void ConstructFSM()
{
pointList = GameObject.FindGameObjectsWithTag("WandarPoint");
//做一个GameObject和Transform类型的转换
Transform[] waypoints = new Transform[pointList.Length];
int i = 0;
foreach (GameObject item in pointList)
{
waypoints[i] = item.transform;
i++;
}
PatorlState patrol = new PatorlState(waypoints);
//看见玩家转化为追逐状态
patrol.AddTransition(Transition.SawPlayer, FSMStateID.Chaseing);
patrol.AddTransition(Transition.ReacherPlayer, FSMStateID.Attacking);
patrol.AddTransition(Transition.NoHealth, FSMStateID.Dead);
ChaseState chase = new ChaseState(waypoints);
chase.AddTransition(Transition.LoatPlayer, FSMStateID.Patroling);
chase.AddTransition(Transition.ReacherPlayer, FSMStateID.Attacking);
chase.AddTransition(Transition.NoHealth, FSMStateID.Dead);
AttackState attack = new AttackState(waypoints);
attack.AddTransition(Transition.LoatPlayer, FSMStateID.Patroling);
attack.AddTransition(Transition.SawPlayer, FSMStateID.Chaseing);
attack.AddTransition(Transition.NoHealth, FSMStateID.Dead);
DeadState dead = new DeadState();
AddFSMState(patrol);
AddFSMState(chase);
AddFSMState(attack);
AddFSMState(dead);
}
protected override void FSMFixedUpdate()
{
CurrentState.Reason(playerTransform, transform);
CurrentState.Act(playerTransform, transform);
}
protected override void FSMUpdate()
{
elapsedTime += Time.deltaTime;
}
public void SetTransition(Transition t)
{
PreformTransition(t);
}
public void ShootBullet()
{
if (elapsedTime >= shootRate)
{
Instantiate(bullet, bulletSpawnPoint.position, bulletSpawnPoint.rotation);
elapsedTime = 0.0f;
}
}
private void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("Bullet"))
{
health -= 50;
}
if (health <= 0)
{
SetTransition(Transition.NoHealth);
Explode();
}
}
private void Explode()
{
float rndX = Random.Range(10.0f, 30.0f);
float rndZ = Random.Range(10.0f, 30.0f);
for (int i = 0; i < 3; i++)
{
//Rigidbody.AddExplosionForce 添加爆炸力 应用一个力到刚体来模拟爆炸效果。
//爆炸力
//球体的中心。
//球体的半径,在半径内才有爆炸效果。
//调节爆炸出现的位置,使其看起来像掀起对象。
GetComponent<Rigidbody>().AddExplosionForce(100.0f, transform.position - new Vector3(rndX, 10.0f, rndZ), 40.0f, 10.0f);
//Transform.TransformDirection 变换方向 变换方向从局部坐标转换到世界坐标。
//这个操作不会受到变换的缩放和位置的影响。返回的向量与direction有同样的长度。
GetComponent<Rigidbody>().velocity = transform.TransformDirection(new Vector3(rndX, 20.0f, rndZ));
}
Destroy(gameObject, 1.5f);
}
}
下面来一个状态的基类:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public abstract class FSMState
{
protected Dictionary<Transition, FSMStateID> map = new Dictionary<Transition, FSMStateID>();
protected FSMStateID stateID;
public FSMStateID ID
{
get
{
return stateID;
}
}
protected Vector3 destPos;//巡逻的具体目标点
protected Transform[] waypoints;//巡逻的四个点
protected float curSpeed;//坦克行进的速度
protected float curRotSpeed;//坦克转弯的速度
/// <summary>
/// 进行状态改变的方法
/// </summary>
/// <param name="player"></param>
/// <param name="npc"></param>
public abstract void Reason(Transform player, Transform npc);
/// <summary>
/// 当坦克处于当前状态是要发起的行为
/// </summary>
/// <param name="player"></param>
/// <param name="npc"></param>
public abstract void Act(Transform player, Transform npc);
public void AddTransition(Transition transition, FSMStateID id)
{
if (transition == Transition.None || id == FSMStateID.None)
{
Debug.LogError("FSMState ERROR: Null transition not allowed ");
return;
}
if (map.ContainsKey(transition))
{
Debug.LogWarning("FSMState ERROR:transition is already inside the map");
}
map.Add(transition, id);
Debug.Log("Addea: " + transition + "WithID" + id);
}
public void DeleteTransition(Transition trans)
{
if (trans == Transition.None)
{
Debug.LogError("FSMState ERROR: Null transition not allowed ");
return;
}
if (map.ContainsKey(trans))
{
map.Remove(trans);
return;
}
Debug.Log("FSMState ERROR :Transition passed was not no this state List");
}
public FSMStateID GetOutState(Transition trans)
{
if (trans == Transition.None)
{
Debug.LogError("FSMState ERROR :NullTransition is not allowed ");
return FSMStateID.None;
}
if (map.ContainsKey(trans))
{
return map[trans];
}
Debug.LogWarning("FSMState ERROR :" + trans + "Transition passed to the state was not on the Dictionary! ");
return FSMStateID.None;
}
/// <summary>
/// 寻找巡逻的目标点
/// </summary>
protected void FindNextPoint()
{
int rndIndex = Random.Range(0, waypoints.Length);
destPos = waypoints[rndIndex].transform.position;
}
/// <summary>
/// 是否在循环范围
/// </summary>
/// <param name="transform"></param>坦克的位置
/// <param name="pos"></param>pos目标点的位置(玩家坦克的位置)
/// <returns></returns>
protected bool IsInCurrentRange(Transform trans, Vector3 pos)
{
float xPos = Mathf.Abs(pos.x - trans.position.x);
float zPos = Mathf.Abs(pos.z - trans.position.z);
if (xPos <= 50 && zPos <= 50)
{
return true;
}
else
{
return false;
}
}
}
下面就是具体的各个状态类了:
using UnityEngine;
using System.Collections;
using System;
public class PatorlState : FSMState
{
public PatorlState(Transform[] wp)
{
waypoints = wp;
stateID = FSMStateID.Patroling;
curRotSpeed = 1.0f;
curSpeed = 100.0f;
}
public override void Act(Transform player, Transform npc)
{
if (Vector3.Distance(npc.position, destPos) <= 100.0f)
{
FindNextPoint();
}
Quaternion targetRotation = Quaternion.LookRotation(destPos - npc.position);
npc.rotation = Quaternion.Slerp(npc.rotation, targetRotation, Time.deltaTime * curRotSpeed);
npc.transform.Translate(Vector3.forward * Time.deltaTime * curSpeed);
}
public override void Reason(Transform player, Transform npc)
{
float dist = Vector3.Distance(npc.position, player.position);
if (dist < 200.0f)
{
npc.GetComponent<NPCTankCntroller>().SetTransition(Transition.ReacherPlayer);
}
else if (dist < 300.0f)
{
Debug.Log("Switch to Chase State!");
npc.GetComponent<NPCTankCntroller>().SetTransition(Transition.SawPlayer);
}
}
}
using UnityEngine;
using System.Collections;
using System;
public class ChaseState : FSMState
{
public ChaseState(Transform[] wp)
{
waypoints = wp;
stateID = FSMStateID.Chaseing;
curRotSpeed = 1.0f;
curSpeed = 100.0f;
FindNextPoint();
}
public override void Act(Transform player, Transform npc)
{
destPos = player.transform.position;
Quaternion targetPoisition = Quaternion.LookRotation(player.position - npc.position);
npc.rotation = Quaternion.Slerp(npc.rotation, targetPoisition, Time.deltaTime * curRotSpeed);
npc.transform.Translate(Vector3.forward * curSpeed * Time.deltaTime);
}
public override void Reason(Transform player, Transform npc)
{
destPos = player.position;
float dist = Vector3.Distance(npc.position ,destPos);
if (dist <200.0f)
{
npc.GetComponent<NPCTankCntroller>().SetTransition(Transition.ReacherPlayer);
}
if (dist >=300f)
{
npc.GetComponent<NPCTankCntroller>().SetTransition(Transition.LoatPlayer);
}
}
}
using UnityEngine;
using System.Collections;
using System;
public class AttackState : FSMState
{
public AttackState(Transform[] wp)
{
waypoints = wp;
stateID = FSMStateID.Attacking;
curRotSpeed = 1.0f;
curSpeed = 100.0f;
FindNextPoint();
}
public override void Act(Transform player, Transform npc)
{
destPos = player.position;
Transform turrent = npc.GetComponent<NPCTankCntroller>().turrent;
Quaternion turrentRotation = Quaternion.LookRotation(destPos - turrent.position);
turrent.rotation = Quaternion.Slerp(turrent.rotation, turrentRotation, Time.deltaTime * curRotSpeed);
npc.GetComponent<NPCTankCntroller>().ShootBullet();
}
public override void Reason(Transform player, Transform npc)
{
float dist = Vector3.Distance(npc.position, player.position);
if (dist >= 200 && dist < 300)
{
npc.GetComponent<NPCTankCntroller>().SetTransition(Transition.SawPlayer);
}
else if (dist >= 300.0f)
{
npc.GetComponent<NPCTankCntroller>().SetTransition(Transition.LoatPlayer);
}
//if (npc.GetComponent<NPCTankCntroller>().health <= 0)
//{
// npc.GetComponent<NPCTankCntroller>().SetTransition(Transition.NoHealth);
//}
}
}
using UnityEngine;
using System.Collections;
using System;
public class DeadState : FSMState
{
public DeadState()
{
stateID = FSMStateID.Dead;
}
public override void Act(Transform player, Transform npc)
{
//DO Nothing
}
public override void Reason(Transform player, Transform npc)
{
//DO Nothing
}
}
哎!代码确实有点多啊!都不想往下写了!但是作为一名程序员这又算神马呢?。。。
方法二:使用协成程序完成
使用协成程序来完成敌人与玩家的AI。
案例需求:敌人自动在目标点自动巡逻。发现玩家时去追逐玩家,当玩家跑的太快时,继续执行巡逻任务。
案例场景:四个巡逻点。(空物体标记)一个玩家小球。一个敌人(Cube代替。)
思路提供:我们都知道协同是我们unity中用来执行另一段进程的任务。就像多线程一样的。可惜我们的unity并不支持多线程哦!
那么我们就要思考了在巡逻中什么时候使用协程呢?首先我们到达巡逻点是,待一会。发现敌人时开启另一段线程来处理发现敌人的事情。
代码编译:
首先给我们的四个标记点添加一个脚本,用来提供标记的。脚本内容不必复杂。一个变量搞定!
using UnityEngine;
using System.Collections;
public class Point : MonoBehaviour
{
public Point nextPoint;
}
然后我们来写核心的代码哦!
using UnityEngine;
using System.Collections;
public class MoveFindEnemy : MonoBehaviour
{
private Point[] points;
private Point currentPoint;
private Transform enemy;
public float speed = 5f;
private int index = 0;
// Use this for initialization
void Start()
{
enemy = GameObject.Find("Enemy").GetComponent<Transform>();
points = GameObject.Find("Ponits").GetComponentsInChildren<Point>();
for (int i = 0; i < points.Length; i++)
{
if (i == points.Length - 1)
{
points[i].nextPoint = points[0];
}
else
{
points[i].nextPoint = points[i + 1];
}
}
index = Random.Range(0, 4);
currentPoint = points[index];
StartCoroutine(Move());
}
private IEnumerator Move()
{
while (true)
{
if (Vector3.Distance(enemy.transform.position, transform.position) < 10.0f)
{
yield return StartCoroutine(FollowEnemy());
}
if (Vector3.Distance(currentPoint.transform.position, transform.position) < 1.0f)
{
currentPoint = currentPoint.nextPoint;
yield return new WaitForSeconds(2f);
}
Vector3 v = currentPoint.transform.position;
v.y = transform.position.y;
transform.LookAt(v);
transform.Translate(Vector3.forward * speed * Time.deltaTime);
yield return new WaitForFixedUpdate();
}
}
private IEnumerator FollowEnemy()
{
while (true)
{
if (Vector3.Distance(enemy.position, transform.position) < 1.0f)
{
Debug.Log("追到你了!!!");
yield return new WaitForSeconds(2f);
}
if (Vector3.Distance(enemy.position, transform.position) > 10.0f)
{
Debug.Log("追不到你了!!!");
yield break;
}
transform.LookAt(enemy);
transform.Translate(Vector3.forward * speed * Time.deltaTime);
yield return new WaitForEndOfFrame();
}
}
}
方法二还是比较简单的吧!下面还有更简单的哦!往下看哦!加油哦!
方法三:使用BehaviorDesigner 插件来完成
1.首先安装BehaviorDesigner插件,(插件作为一名程序员应该都会有办法弄到的哦!这里就不提供啦!)然后安装它的一个扩展包,Behavior Designer - Movement_Pack。这样你的BehaviorDesigner编译器中就会有了一个Movement了。
这个主要利用unity中自带的Navigation组件。
2.下面通过小案例来讲解:
首先创建一个平面,调整合适的大小,然后创建一个胶囊体作为主角。
然后创建两个空物体作为要巡逻的点。
接着给胶囊体添加Behaviour Tree组件。
然后进入到编辑面板。
然后在Task中添加一个巡逻的状态。
然后将巡逻点添加上去。设置一下速度等参数。
最后烘焙一下导航组件和给敌人添加上Nav Mesh Agent 组件。
运行时可以看见敌人在巡逻点之间来回巡逻的。
3.接着我们继续完善这个案例。
当我们的主角在看见任何一个敌人时都会有追击的状态。
那么我们就必须先判断有没有看到,看到之后再追,否则继续巡逻。
在Conditionals中有一个这样的行为。CanseeObject状态。
添加上了以后会发现scene视窗中出现一个扇形的视野。
可以自己修改扇形的参数。
接着传递敌人:
下面就是如何把看见的敌人传递给我们要追的状态(另外一个Seek)呢?(即实现先看再追的过程)这时就需要一个全局变量了!
添加变量
然后通过我们自定义的变量去赋值
然后给他们添加一个序列任务。
然后做如下连接:
再将Sequence 改成这样表示可以打断右边的执行
然后运行如下:
未看见敌人。
看见敌人并追逐敌人。
追到敌人自己就会停下。那是因为自己的行为树所有状态已经完成了!要知道行为树中状态都只会执行一次的!
参数说明:
Slef表示可以中断比自己层的运行。
Lower Priority 表示可以中断比自己优先级低的。
Both:表示两种情况都会中断的。
一般情况下行为树的执行都是从左到右执行的。不会发生中断的。
.。。。。。。
写了好久,截图都累死了!希望我的付出会对疑惑中的你有点帮助哦!!!
三种方法,当然了实现的效果都差不了多少。但是作为程序员的我建议的能自己写的就代码实现吧!毕竟作为程序员的我们都是代码的搬运工嘛!还有这次代码并没有写好多的注释,不是写,而是写注释太累了哦!相信大家都能看的懂的哦!加油啦!最后感谢大家的阅读哦!