若想取得战争的胜利,必先控好兵线。
———— 麦克阿瑟
是时候让敌人经历一下我们兵线的洗礼。
———— 拿破仑
在LOL对局中,职业选手对兵线的控制可以说是达到了“运筹帷幄之中,决胜千里之外”。其实普通玩家只要控好兵线,在对线中一样可以打开优势。
这一篇会介绍,Moba游戏的小兵AI系统在游戏中的推进规律,相信看完此篇,就可以像我一样,怒升两个段位。(黑铁5——黑铁3)
小兵AI系统非常简单!我们会用到这几点技术:
构建易扩展的AI基类和数据结构
有限状态机FSM控制小兵AI行为状态
NavMesh自动寻路系统控制小兵移动
缓存池CatchTool 控制小兵动态创建和销毁
……
直接上代码…………………………………………
第一步,定义AI系统基类,基类中定义多个AI必须的行为类
public class AI : MonoBehaviour
{
/// <summary>
/// AI自身对象
/// </summary>
public GameObject obj;
/// <summary>
/// 自身ID
/// </summary>
public int AIID;
/// <summary>
/// 名字
/// </summary>
public string Name;
/// <summary>
/// 唯一识别ID
/// </summary>
public int UniqueID;
/// <summary>
/// AI类型
/// </summary>
public AIType AIType;
/// <summary>
/// 阵营
/// </summary>
public CampEnum CampEnum;
/// <summary>
/// 是否存活
/// </summary>
public bool isAlive;
/// <summary>
/// AI皮肤
/// </summary>
public AISkin skin = new AISkin();
/// <summary>
/// AI属性
/// </summary>
public AIAttribute attribute = new AIAttribute();
/// <summary>
/// AI属性变化事件
/// </summary>
public AttributeEvent attributeEvent = new AttributeEvent();
/// <summary>
/// AI成长属性
/// </summary>
public AIGrowUp growUp = new AIGrowUp();
/// <summary>
/// 播放动画
/// </summary>
public AIAnimation aiAnimation = new AIAnimation();
AI小兵派生类:
/// <summary>
/// 小兵AI派生类
/// </summary>
public class SoldierAI : AI
{
/// <summary>
/// 自动寻路组件
/// </summary>
public AINaveMesh aINaveMesh;
/// <summary>
/// 小兵状态机组件
/// </summary>
public SoldierBehaviour soldierBehaviour;
/// <summary>
/// 小兵侦察距离检测类
/// </summary>
public SoldierDetectionCollider soldierDetectionCollider;
/// <summary>
/// 小兵战斗距离检测类
/// </summary>
public SoldierAttackCollider soldierAttackCollider;
/// <summary>
/// 小兵技能
/// </summary>
public SoldierSkill soldierSkill;
/// <summary>
/// 小兵UI画布
/// </summary>
public SoldierCanvas soldierCanvas;
/// <summary>
/// 小兵类型
/// </summary>
public string SoldierType;
/// <summary>
/// 是否死亡
/// </summary>
private bool isDeath = false;
定义小兵AI管理器类,用于管理小兵AI的创建、销毁、属性成长等等
/// <summary>
/// 小兵管理器类
/// </summary>
public class SoldierManager : MonoSingle<SoldierManager>
{
/// <summary>
/// 小兵加载波次
/// </summary>
private int Frequency = 0;
/// <summary>
/// 加载小兵间隔
/// </summary>
private float LoadInterval = 0;
/// <summary>
/// 倒计时
/// </summary>
private float Countdown = 1f;
/// <summary>
/// 间隔时间
/// </summary>
private float IntervalTime = 30f;
/// <summary>
/// 判定是否开始小兵系统
/// </summary>
private bool isStart = false;
/// <summary>
///
/// </summary>
private TerrainEnum GameTerrain;
/// <summary>
/// 对象池小兵预制体
/// </summary>
public Dictionary<string, SoldierAI> SoldierPrefabs = new Dictionary<string, SoldierAI>();
//小兵临时对象
public Queue<SoldierAI> Temp_SoldierMelee_Blue = new Queue<SoldierAI>();
public Queue<SoldierAI> Temp_SoldierRemote_Blue = new Queue<SoldierAI>();
public Queue<SoldierAI> Temp_GunTruck_Blue = new Queue<SoldierAI>();
public Queue<SoldierAI> Temp_SoldierMelee_Red = new Queue<SoldierAI>();
public Queue<SoldierAI> Temp_SoldierRemote_Red = new Queue<SoldierAI>();
public Queue<SoldierAI> Temp_GunTruck_Red = new Queue<SoldierAI>();
/// <summary>
/// 所有小兵容器
/// Key:小兵唯一识别符
/// Value:小兵AI
/// </summary>
public Dictionary<int, SoldierAI> Soldiers_Dic = new Dictionary<int, SoldierAI>();
/// <summary>
/// 是否刷新小兵
/// </summary>
private bool isRefresh = false;
/// <summary>
/// 当前刷新帧
/// </summary>
private float refreshCDNow = 0;
/// <summary>
/// 每个小兵刷新间隔
/// </summary>
private float refreshCD = 0.7f;
/// <summary>
///
/// </summary>
private int refreshNumber = 0;
/// <summary>
/// 小兵类型列表
/// </summary>
private string[] refreshList = new string[7];
定义小兵缓存池,并创建小兵缓存
//根据游戏模式加载小兵缓存池
int SoldierRemote = 0;
int SoldierMelee = 0;
int GunTruck = 0;
switch (GameTerrain)
{
case TerrainEnum.Gorge:
SoldierRemote = 27;
SoldierMelee = 27;
GunTruck = 9;
break;
case TerrainEnum.Bridge:
SoldierRemote = 9;
SoldierMelee = 9;
GunTruck = 3;
break;
case TerrainEnum.Jungle:
SoldierRemote = 27;
SoldierMelee = 27;
GunTruck = 9;
break;
default:
break;
}
//创建小兵缓存池
ToolManager.objectPool_C.Init_Public("SoldierRemote_Blue", PoolParentType.Model);
ToolManager.objectPool_C.SetPool_Public("SoldierRemote_Blue", SoldierPrefabs["SoldierRemote_Blue"].obj, SoldierRemote);
ToolManager.objectPool_C.Init_Public("SoldierMelee_Blue", PoolParentType.Model);
ToolManager.objectPool_C.SetPool_Public("SoldierMelee_Blue", SoldierPrefabs["SoldierMelee_Blue"].obj, SoldierMelee);
ToolManager.objectPool_C.Init_Public("GunTruck_Blue", PoolParentType.Model);
ToolManager.objectPool_C.SetPool_Public("GunTruck_Blue", SoldierPrefabs["GunTruck_Blue"].obj, GunTruck);
ToolManager.objectPool_C.Init_Public("SoldierRemote_Red", PoolParentType.Model);
ToolManager.objectPool_C.SetPool_Public("SoldierRemote_Red", SoldierPrefabs["SoldierRemote_Red"].obj, SoldierRemote);
ToolManager.objectPool_C.Init_Public("SoldierMelee_Red", PoolParentType.Model);
ToolManager.objectPool_C.SetPool_Public("SoldierMelee_Red", SoldierPrefabs["SoldierMelee_Red"].obj, SoldierMelee);
ToolManager.objectPool_C.Init_Public("GunTruck_Red", PoolParentType.Model);
ToolManager.objectPool_C.SetPool_Public("GunTruck_Red", SoldierPrefabs["GunTruck_Red"].obj, GunTruck);
第二步,和场景类似,将小兵模型预制体打包成ab包,供加载使用
打包ab包方法:
/// <summary>
/// 编译资源
/// </summary>
/// <param name="targetPath">目标位置</param>
/// <param name="prefabs">预制体</param>
/// <param name="scenes">场景</param>
/// <param name="buildTarget">目标平台</param>
[MenuItem("开始打包/打包Obj")]
public static void BuildingObj()
{
AssetBundleManifest prs = null;
try
{
UnityEngine.Object[] selects = Selection.GetFiltered(typeof(UnityEngine.Object), SelectionMode.DeepAssets);
//添加资源
foreach (UnityEngine.Object obj in selects)
{
List<AssetBundleBuild> prefabBuilds = new List<AssetBundleBuild>(); //预制体资源
AssetBundleBuild build = new AssetBundleBuild();
build.assetBundleName = obj.name + ".assetBundle";
string assetPath = AssetDatabase.GetAssetPath(obj);
build.assetNames = new string[] { assetPath };
prefabBuilds.Add(build);
string prefabPath = Environment.CurrentDirectory + "/DownLoad/Assetbundle/" + obj.name;
if (!Directory.Exists(prefabPath)) Directory.CreateDirectory(prefabPath);
try
{
prs = BuildPipeline.BuildAssetBundles(prefabPath, prefabBuilds.ToArray(), BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows64);
}
catch (System.Exception ex)
{
Debug.Log("预制体 打包 失败:" + ex.ToString());
}
}
}
catch (UnityException e)
{
Debug.Log("预制体 打包 失败:" + e.ToString());
prs = null;
}
if (prs != null)
{
Debug.Log("预制体 打包 成功");
}
else
{
Debug.Log("预制体 打包 失败");
}
//刷新编辑器(不写的话要手动刷新,否则打包的资源不能及时在Project视图内显示)
AssetDatabase.Refresh();
}
加载ab包方法:
UnityWebRequest www = UnityWebRequestAssetBundle.GetAssetBundle(URL);
www.SendWebRequest();
while (!www.isDone)
{
yield return null;
}
yield return www;
assetBundle = DownloadHandlerAssetBundle.GetContent(www);
if (assetBundle != null)
{
GameObject[] objects = assetBundle.LoadAllAssets<GameObject>();
yield return new WaitForEndOfFrame();
GameObject model;
if (objects[0])
{
try
{
model = Instantiate(objects[0]);
action(model);
}
catch (System.Exception EX)
{
iDebug.YiYan("克隆模型物体失败!" + EX, DebugColor.red);
model = GameObject.CreatePrimitive(PrimitiveType.Cube);
action(model);
}
}
else
{
iDebug.YiYan("AB包为空!", DebugColor.red);
model = GameObject.CreatePrimitive(PrimitiveType.Cube);
action(model);
}
}
else
{
iDebug.YiYan("AB包为空", DebugColor.red);
}
if (assetBundle != null)
{
assetBundle.Unload(false);
}
www.Dispose();
*第三步,小兵AI之间自动检测战斗系统
这个是AI系统中比较重要的环节,双方小兵按照规定移动轨迹走到线上,然后开始自动战斗,其中AI状态大概分为:
自动寻路状态 对线状态 攻击状态 防守状态
死亡状态 停止状态 追击状态 强制攻击状态
小兵的行为应该在遇到对应变化时适时切换这几种状态,而切换的方法则要用到设计模式中常用的模式:状态模式,也就是FSM有限状态机,代码如下:
状态机基类:
/// <summary>
/// 状态机基类
/// </summary>
public class BaseState
{
/// <summary>
/// 状态名称
/// </summary>
private string m_StateName = "BaseState";
public string StateName
{
get { return m_StateName; }
set { m_StateName = value; }
}
/// <summary>
/// 控制器
/// </summary>
protected BaseStateController m_Controller = null;
/// <summary>
/// AI行为逻辑
/// </summary>
public StateBehaviour m_Behaviour = null;
/// <summary>
/// 构造函数——建造者
/// </summary>
/// <param name="Controller"></param>
public BaseState(BaseStateController Controller, StateBehaviour behaviour = null)
{
m_Controller = Controller;
if (behaviour != null)
m_Behaviour = behaviour;
}
/// <summary>
/// 状态开始
/// </summary>
public virtual void StateBegin() { }
/// <summary>
/// 状态更新
/// </summary>
public virtual void StateUpdate() { }
/// <summary>
/// 状态结束
/// </summary>
public virtual void StateEnd() { }
/// <summary>
/// 输出当前状态
/// </summary>
/// <returns></returns>
public override string ToString()
{
return string.Format("[StateName = {0}]", StateName);
}
}
状态机控制器类:
/// <summary>
/// 状态机控制器类
/// </summary>
public class BaseStateController
{
/// <summary>
/// 切换的当前状态
/// </summary>
private BaseState m_State;
/// <summary>
/// 是否已经开始执行当前状态
/// </summary>
private bool m_bRunBegin = false;
/// <summary>
/// 构造
/// </summary>
public BaseStateController() { }
/// <summary>
/// 状态机操作方法——执行设置状态方法,执行结束状态方法
/// 1、执行上一个状态的结束方法
/// 2、设置新的状态
/// </summary>
/// <param name="State"></param>
/// <param name="Value"></param>
public void SetState(BaseState State)
{
m_bRunBegin = false;
//通知前一个状态结束
if (m_State != null)
{
m_State.StateEnd();
}
m_State = State;
}
/// <summary>
/// 状态机操作方法——执行开始状态方法
/// </summary>
public void StateBegin()
{
if (m_State != null && m_bRunBegin == false)
{
m_State.StateBegin();
m_bRunBegin = true;
}
}
/// <summary>
/// 状态机操作方法——执行更新状态方法
/// </summary>
public void StateUpdate()
{
if (m_State != null && m_bRunBegin == true)
{
m_State.StateUpdate();
}
}
}
状态机控制器:
public class StateBehaviour : MonoBehaviour
{
/// <summary>
/// ID
/// </summary>
public int ID;
/// <summary>
/// 名字
/// </summary>
public string Name;
/// <summary>
/// 状态集合
/// </summary>
public Dictionary<string, BaseState> StateDic;
/// <summary>
/// 状态机控制器
/// </summary>
public BaseStateController StateController;
public virtual void Awake()
{
StateDic = new Dictionary<string, BaseState>();
StateController = new BaseStateController();
}
public virtual void Start()
{
}
public virtual void Update()
{
StateController.StateUpdate();
}
/// <summary>
/// 初始化状态机
/// </summary>
public virtual void Init(int id, string name)
{
ID = id;
Name = name;
}
/// <summary>
/// 设置状态
/// </summary>
/// <param name="state"></param>
public virtual void SetState(string state)
{
StateController.SetState(StateDic[state]);
StateController.StateBegin();
}
}
小兵AI状态机管理器
public class SoldierBehaviour : StateBehaviour
{
/// <summary>
/// 本身物体
/// </summary>
public GameObject Local;
/// <summary>
/// 目标物体
/// </summary>
public GameObject Target;
/// <summary>
/// 自身AI对象
/// </summary>
public SoldierAI AI;
public override void Awake()
{
base.Awake();
StateDic.Add("SoldierState_Alignment", new SoldierState_Alignment(StateController, this));
StateDic.Add("SoldierState_Attack", new SoldierState_Attack(StateController, this));
StateDic.Add("SoldierState_CrazyAttack", new SoldierState_CrazyAttack(StateController, this));
StateDic.Add("SoldierState_Defense", new SoldierState_Defense(StateController, this));
StateDic.Add("SoldierState_Death", new SoldierState_Death(StateController, this));
StateDic.Add("SoldierState_Pathfinding", new SoldierState_Pathfinding(StateController, this));
StateDic.Add("SoldierState_Pursuit", new SoldierState_Pursuit(StateController, this));
StateDic.Add("SoldierState_Stop", new SoldierState_Stop(StateController, this));
}
public override void Start()
{
base.Start();
}
public override void Update()
{
base.Update();
}
/// <summary>
/// 初始化状态机
/// </summary>
public void Init(int id, string name, SoldierAI ai)
{
base.Init(id, name);
AI = ai;
// iDebug.YiYan("小兵状态机初始化");
}
/// <summary>
///
/// </summary>
/// <param name="name"></param>
public void SetGameState(string name)
{
SetState(name);
}
/// <summary>
/// 结束系统
/// </summary>
public void End()
{
Target = null;
}
}
然后,就是在小兵进行途中,在小兵不同的状态下写好当前状态应该做的行为,和判定切换状态的条件行为,举个例子:
小兵在自动寻路状态下在兵线上走,突然攻击预警范围内有敌人出现,则自动切换到追击状态。
等走到攻击范围内,则切换到攻击状态,开始攻击敌人。
当敌人离开攻击范围但尚未离开预警范围,则继续切换到追击状态。
当敌人离开预警范围,则切换到自动寻路状态。
自动寻路状态代码:
/// <summary>
/// 状态开始
/// </summary>
public override void StateBegin()
{
// iDebug.YiYan("开始State:SoldierState_Pathfinding" + soldierBehaviour.AI.obj.GetInstanceID());
//播放行走动画
//获取寻路目标
soldierBehaviour.AI.aINaveMesh.SetState(true);
soldierBehaviour.AI.aINaveMesh.SetFinalTarget();
}
/// <summary>
/// 状态更新
/// </summary>
public override void StateUpdate()
{
//判断自身区域触发器是否有敌人
if (soldierBehaviour.AI.soldierDetectionCollider.EnemyList.Count > 0)
{
soldierBehaviour.AI.aINaveMesh.Target = soldierBehaviour.AI.soldierDetectionCollider.EnemyList[0];
soldierBehaviour.SetState("SoldierState_Pursuit");
}
}
追击状态代码:
/// <summary>
/// 状态开始
/// </summary>
public override void StateBegin()
{
// iDebug.YiYan("开始State:SoldierState_Pursuit" + soldierBehaviour.AI.obj.GetInstanceID());
//播放行走动画
if (soldierBehaviour.AI.aINaveMesh.Target != null && soldierBehaviour.AI.soldierDetectionCollider.EnemyList.Contains(soldierBehaviour.AI.aINaveMesh.Target))
{
soldierBehaviour.AI.aINaveMesh.SetDestinationTarget(soldierBehaviour.AI.aINaveMesh.Target.obj);
}
}
/// <summary>
/// 状态更新
/// </summary>
public override void StateUpdate()
{
//判断自身区域触发器是否有敌人
if (soldierBehaviour.AI.soldierAttackCollider.EnemyList.Contains(soldierBehaviour.AI.aINaveMesh.Target))
{
soldierBehaviour.AI.aINaveMesh.SetState(false);
soldierBehaviour.SetState("SoldierState_Attack");
}
if (!soldierBehaviour.AI.soldierDetectionCollider.EnemyList.Contains(soldierBehaviour.AI.aINaveMesh.Target))
{
soldierBehaviour.SetState("SoldierState_Pathfinding");
}
}
攻击状态代码:
/// <summary>
/// 状态开始
/// </summary>
public override void StateBegin()
{
// iDebug.YiYan("开始State:SoldierState_Attack" + soldierBehaviour.AI.obj.GetInstanceID());
if (soldierBehaviour.AI.aINaveMesh.Target != null && soldierBehaviour.AI.soldierAttackCollider.EnemyList.Contains(soldierBehaviour.AI.aINaveMesh.Target))
{
soldierBehaviour.AI.soldierSkill.SetTarget(soldierBehaviour.AI, soldierBehaviour.AI.aINaveMesh.Target);
soldierBehaviour.AI.soldierSkill.AutoAttack(true);
}
}
/// <summary>
/// 状态更新
/// </summary>
public override void StateUpdate()
{
//持续对目标释放技能
//播放攻击动画
if (!soldierBehaviour.AI.soldierAttackCollider.EnemyList.Contains(soldierBehaviour.AI.aINaveMesh.Target) && soldierBehaviour.AI.soldierDetectionCollider.EnemyList.Contains(soldierBehaviour.AI.aINaveMesh.Target))
{
soldierBehaviour.AI.aINaveMesh.SetState(true);
soldierBehaviour.SetState("SoldierState_Pursuit");
}
else if (!soldierBehaviour.AI.soldierDetectionCollider.EnemyList.Contains(soldierBehaviour.AI.aINaveMesh.Target)|| soldierBehaviour.AI.aINaveMesh.Target.attribute.attributeValueNow.HP <= 0)
{
soldierBehaviour.AI.aINaveMesh.SetFinalTarget();
soldierBehaviour.AI.aINaveMesh.SetState(true);
soldierBehaviour.SetState("SoldierState_Pathfinding");
// iDebug.YiYan("切换目标");
}
else
{
// iDebug.YiYan("不断攻击");
}
}
判定效果就像这样~~~ :
是不是非常简单易懂(笑脸+手动狗头~~)
最后,放一段小兵对线的效果:
(为了连贯性,所以把UI界面和资源导入的效果都录入了)
这里有的小伙伴会有疑问了,小兵的战斗数值和防御塔的属性数值是多少,如何设置呢?
哈~ 当然是在数据库中,由服务器统一分发了
数值就动态读取,然后赋予到AI基类里即可,后面的篇章会介绍技能系统和战斗计算,那时候再详细介绍数值调用的详细逻辑吧
↓↓↓↓↓↓
文末福利: