B4:Unity制作Moba类游戏——小兵AI系统

           若想取得战争的胜利,必先控好兵线。

                                                             ———— 麦克阿瑟

           是时候让敌人经历一下我们兵线的洗礼。

                                                             ———— 拿破仑

 在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基类里即可,后面的篇章会介绍技能系统和战斗计算,那时候再详细介绍数值调用的详细逻辑吧

↓↓↓↓↓↓

文末福利:

猜你喜欢

转载自blog.csdn.net/qq_25325511/article/details/128304125