当前位置:   article > 正文

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

unity moba

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

                                                             ———— 麦克阿瑟

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

                                                             ———— 拿破仑

 在LOL对局中,职业选手对兵线的控制可以说是达到了“运筹帷幄之中,决胜千里之外”。其实普通玩家只要控好兵线,在对线中一样可以打开优势。

这一篇会介绍,Moba游戏的小兵AI系统在游戏中的推进规律,相信看完此篇,就可以像我一样,怒升两个段位。(黑铁5——黑铁3

小兵AI系统非常简单!我们会用到这几点技术:

构建易扩展的AI基类和数据结构

有限状态机FSM控制小兵AI行为状态

NavMesh自动寻路系统控制小兵移动

缓存池CatchTool 控制小兵动态创建和销毁

……

直接上代码…………………………………………

第一步,定义AI系统基类,基类中定义多个AI必须的行为类

  1. public class AI : MonoBehaviour
  2. {
  3. /// <summary>
  4. /// AI自身对象
  5. /// </summary>
  6. public GameObject obj;
  7. /// <summary>
  8. /// 自身ID
  9. /// </summary>
  10. public int AIID;
  11. /// <summary>
  12. /// 名字
  13. /// </summary>
  14. public string Name;
  15. /// <summary>
  16. /// 唯一识别ID
  17. /// </summary>
  18. public int UniqueID;
  19. /// <summary>
  20. /// AI类型
  21. /// </summary>
  22. public AIType AIType;
  23. /// <summary>
  24. /// 阵营
  25. /// </summary>
  26. public CampEnum CampEnum;
  27. /// <summary>
  28. /// 是否存活
  29. /// </summary>
  30. public bool isAlive;
  31. /// <summary>
  32. /// AI皮肤
  33. /// </summary>
  34. public AISkin skin = new AISkin();
  35. /// <summary>
  36. /// AI属性
  37. /// </summary>
  38. public AIAttribute attribute = new AIAttribute();
  39. /// <summary>
  40. /// AI属性变化事件
  41. /// </summary>
  42. public AttributeEvent attributeEvent = new AttributeEvent();
  43. /// <summary>
  44. /// AI成长属性
  45. /// </summary>
  46. public AIGrowUp growUp = new AIGrowUp();
  47. /// <summary>
  48. /// 播放动画
  49. /// </summary>
  50. public AIAnimation aiAnimation = new AIAnimation();

AI小兵派生类:

  1. /// <summary>
  2. /// 小兵AI派生类
  3. /// </summary>
  4. public class SoldierAI : AI
  5. {
  6. /// <summary>
  7. /// 自动寻路组件
  8. /// </summary>
  9. public AINaveMesh aINaveMesh;
  10. /// <summary>
  11. /// 小兵状态机组件
  12. /// </summary>
  13. public SoldierBehaviour soldierBehaviour;
  14. /// <summary>
  15. /// 小兵侦察距离检测类
  16. /// </summary>
  17. public SoldierDetectionCollider soldierDetectionCollider;
  18. /// <summary>
  19. /// 小兵战斗距离检测类
  20. /// </summary>
  21. public SoldierAttackCollider soldierAttackCollider;
  22. /// <summary>
  23. /// 小兵技能
  24. /// </summary>
  25. public SoldierSkill soldierSkill;
  26. /// <summary>
  27. /// 小兵UI画布
  28. /// </summary>
  29. public SoldierCanvas soldierCanvas;
  30. /// <summary>
  31. /// 小兵类型
  32. /// </summary>
  33. public string SoldierType;
  34. /// <summary>
  35. /// 是否死亡
  36. /// </summary>
  37. private bool isDeath = false;

定义小兵AI管理器类,用于管理小兵AI的创建、销毁、属性成长等等

  1. /// <summary>
  2. /// 小兵管理器类
  3. /// </summary>
  4. public class SoldierManager : MonoSingle<SoldierManager>
  5. {
  6. /// <summary>
  7. /// 小兵加载波次
  8. /// </summary>
  9. private int Frequency = 0;
  10. /// <summary>
  11. /// 加载小兵间隔
  12. /// </summary>
  13. private float LoadInterval = 0;
  14. /// <summary>
  15. /// 倒计时
  16. /// </summary>
  17. private float Countdown = 1f;
  18. /// <summary>
  19. /// 间隔时间
  20. /// </summary>
  21. private float IntervalTime = 30f;
  22. /// <summary>
  23. /// 判定是否开始小兵系统
  24. /// </summary>
  25. private bool isStart = false;
  26. /// <summary>
  27. ///
  28. /// </summary>
  29. private TerrainEnum GameTerrain;
  30. /// <summary>
  31. /// 对象池小兵预制体
  32. /// </summary>
  33. public Dictionary<string, SoldierAI> SoldierPrefabs = new Dictionary<string, SoldierAI>();
  34. //小兵临时对象
  35. public Queue<SoldierAI> Temp_SoldierMelee_Blue = new Queue<SoldierAI>();
  36. public Queue<SoldierAI> Temp_SoldierRemote_Blue = new Queue<SoldierAI>();
  37. public Queue<SoldierAI> Temp_GunTruck_Blue = new Queue<SoldierAI>();
  38. public Queue<SoldierAI> Temp_SoldierMelee_Red = new Queue<SoldierAI>();
  39. public Queue<SoldierAI> Temp_SoldierRemote_Red = new Queue<SoldierAI>();
  40. public Queue<SoldierAI> Temp_GunTruck_Red = new Queue<SoldierAI>();
  41. /// <summary>
  42. /// 所有小兵容器
  43. /// Key:小兵唯一识别符
  44. /// Value:小兵AI
  45. /// </summary>
  46. public Dictionary<int, SoldierAI> Soldiers_Dic = new Dictionary<int, SoldierAI>();
  47. /// <summary>
  48. /// 是否刷新小兵
  49. /// </summary>
  50. private bool isRefresh = false;
  51. /// <summary>
  52. /// 当前刷新帧
  53. /// </summary>
  54. private float refreshCDNow = 0;
  55. /// <summary>
  56. /// 每个小兵刷新间隔
  57. /// </summary>
  58. private float refreshCD = 0.7f;
  59. /// <summary>
  60. ///
  61. /// </summary>
  62. private int refreshNumber = 0;
  63. /// <summary>
  64. /// 小兵类型列表
  65. /// </summary>
  66. private string[] refreshList = new string[7];

定义小兵缓存池,并创建小兵缓存

  1. //根据游戏模式加载小兵缓存池
  2. int SoldierRemote = 0;
  3. int SoldierMelee = 0;
  4. int GunTruck = 0;
  5. switch (GameTerrain)
  6. {
  7. case TerrainEnum.Gorge:
  8. SoldierRemote = 27;
  9. SoldierMelee = 27;
  10. GunTruck = 9;
  11. break;
  12. case TerrainEnum.Bridge:
  13. SoldierRemote = 9;
  14. SoldierMelee = 9;
  15. GunTruck = 3;
  16. break;
  17. case TerrainEnum.Jungle:
  18. SoldierRemote = 27;
  19. SoldierMelee = 27;
  20. GunTruck = 9;
  21. break;
  22. default:
  23. break;
  24. }
  25. //创建小兵缓存池
  26. ToolManager.objectPool_C.Init_Public("SoldierRemote_Blue", PoolParentType.Model);
  27. ToolManager.objectPool_C.SetPool_Public("SoldierRemote_Blue", SoldierPrefabs["SoldierRemote_Blue"].obj, SoldierRemote);
  28. ToolManager.objectPool_C.Init_Public("SoldierMelee_Blue", PoolParentType.Model);
  29. ToolManager.objectPool_C.SetPool_Public("SoldierMelee_Blue", SoldierPrefabs["SoldierMelee_Blue"].obj, SoldierMelee);
  30. ToolManager.objectPool_C.Init_Public("GunTruck_Blue", PoolParentType.Model);
  31. ToolManager.objectPool_C.SetPool_Public("GunTruck_Blue", SoldierPrefabs["GunTruck_Blue"].obj, GunTruck);
  32. ToolManager.objectPool_C.Init_Public("SoldierRemote_Red", PoolParentType.Model);
  33. ToolManager.objectPool_C.SetPool_Public("SoldierRemote_Red", SoldierPrefabs["SoldierRemote_Red"].obj, SoldierRemote);
  34. ToolManager.objectPool_C.Init_Public("SoldierMelee_Red", PoolParentType.Model);
  35. ToolManager.objectPool_C.SetPool_Public("SoldierMelee_Red", SoldierPrefabs["SoldierMelee_Red"].obj, SoldierMelee);
  36. ToolManager.objectPool_C.Init_Public("GunTruck_Red", PoolParentType.Model);
  37. ToolManager.objectPool_C.SetPool_Public("GunTruck_Red", SoldierPrefabs["GunTruck_Red"].obj, GunTruck);

第二步,和场景类似,将小兵模型预制体打包成ab包,供加载使用

 打包ab包方法:

  1. /// <summary>
  2. /// 编译资源
  3. /// </summary>
  4. /// <param name="targetPath">目标位置</param>
  5. /// <param name="prefabs">预制体</param>
  6. /// <param name="scenes">场景</param>
  7. /// <param name="buildTarget">目标平台</param>
  8. [MenuItem("开始打包/打包Obj")]
  9. public static void BuildingObj()
  10. {
  11. AssetBundleManifest prs = null;
  12. try
  13. {
  14. UnityEngine.Object[] selects = Selection.GetFiltered(typeof(UnityEngine.Object), SelectionMode.DeepAssets);
  15. //添加资源
  16. foreach (UnityEngine.Object obj in selects)
  17. {
  18. List<AssetBundleBuild> prefabBuilds = new List<AssetBundleBuild>(); //预制体资源
  19. AssetBundleBuild build = new AssetBundleBuild();
  20. build.assetBundleName = obj.name + ".assetBundle";
  21. string assetPath = AssetDatabase.GetAssetPath(obj);
  22. build.assetNames = new string[] { assetPath };
  23. prefabBuilds.Add(build);
  24. string prefabPath = Environment.CurrentDirectory + "/DownLoad/Assetbundle/" + obj.name;
  25. if (!Directory.Exists(prefabPath)) Directory.CreateDirectory(prefabPath);
  26. try
  27. {
  28. prs = BuildPipeline.BuildAssetBundles(prefabPath, prefabBuilds.ToArray(), BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows64);
  29. }
  30. catch (System.Exception ex)
  31. {
  32. Debug.Log("预制体 打包 失败:" + ex.ToString());
  33. }
  34. }
  35. }
  36. catch (UnityException e)
  37. {
  38. Debug.Log("预制体 打包 失败:" + e.ToString());
  39. prs = null;
  40. }
  41. if (prs != null)
  42. {
  43. Debug.Log("预制体 打包 成功");
  44. }
  45. else
  46. {
  47. Debug.Log("预制体 打包 失败");
  48. }
  49. //刷新编辑器(不写的话要手动刷新,否则打包的资源不能及时在Project视图内显示)
  50. AssetDatabase.Refresh();
  51. }

 加载ab包方法:

  1. UnityWebRequest www = UnityWebRequestAssetBundle.GetAssetBundle(URL);
  2. www.SendWebRequest();
  3. while (!www.isDone)
  4. {
  5. yield return null;
  6. }
  7. yield return www;
  8. assetBundle = DownloadHandlerAssetBundle.GetContent(www);
  9. if (assetBundle != null)
  10. {
  11. GameObject[] objects = assetBundle.LoadAllAssets<GameObject>();
  12. yield return new WaitForEndOfFrame();
  13. GameObject model;
  14. if (objects[0])
  15. {
  16. try
  17. {
  18. model = Instantiate(objects[0]);
  19. action(model);
  20. }
  21. catch (System.Exception EX)
  22. {
  23. iDebug.YiYan("克隆模型物体失败!" + EX, DebugColor.red);
  24. model = GameObject.CreatePrimitive(PrimitiveType.Cube);
  25. action(model);
  26. }
  27. }
  28. else
  29. {
  30. iDebug.YiYan("AB包为空!", DebugColor.red);
  31. model = GameObject.CreatePrimitive(PrimitiveType.Cube);
  32. action(model);
  33. }
  34. }
  35. else
  36. {
  37. iDebug.YiYan("AB包为空", DebugColor.red);
  38. }
  39. if (assetBundle != null)
  40. {
  41. assetBundle.Unload(false);
  42. }
  43. www.Dispose();

*第三步,小兵AI之间自动检测战斗系统

这个是AI系统中比较重要的环节,双方小兵按照规定移动轨迹走到线上,然后开始自动战斗,其中AI状态大概分为:

自动寻路状态           对线状态          攻击状态            防守状态                

死亡状态             停止状态             追击状态             强制攻击状态


小兵的行为应该在遇到对应变化时适时切换这几种状态,而切换的方法则要用到设计模式中常用的模式:状态模式,也就是FSM有限状态机,代码如下:

状态机基类:

  1. /// <summary>
  2. /// 状态机基类
  3. /// </summary>
  4. public class BaseState
  5. {
  6. /// <summary>
  7. /// 状态名称
  8. /// </summary>
  9. private string m_StateName = "BaseState";
  10. public string StateName
  11. {
  12. get { return m_StateName; }
  13. set { m_StateName = value; }
  14. }
  15. /// <summary>
  16. /// 控制器
  17. /// </summary>
  18. protected BaseStateController m_Controller = null;
  19. /// <summary>
  20. /// AI行为逻辑
  21. /// </summary>
  22. public StateBehaviour m_Behaviour = null;
  23. /// <summary>
  24. /// 构造函数——建造者
  25. /// </summary>
  26. /// <param name="Controller"></param>
  27. public BaseState(BaseStateController Controller, StateBehaviour behaviour = null)
  28. {
  29. m_Controller = Controller;
  30. if (behaviour != null)
  31. m_Behaviour = behaviour;
  32. }
  33. /// <summary>
  34. /// 状态开始
  35. /// </summary>
  36. public virtual void StateBegin() { }
  37. /// <summary>
  38. /// 状态更新
  39. /// </summary>
  40. public virtual void StateUpdate() { }
  41. /// <summary>
  42. /// 状态结束
  43. /// </summary>
  44. public virtual void StateEnd() { }
  45. /// <summary>
  46. /// 输出当前状态
  47. /// </summary>
  48. /// <returns></returns>
  49. public override string ToString()
  50. {
  51. return string.Format("[StateName = {0}]", StateName);
  52. }
  53. }

状态机控制器类:

  1. /// <summary>
  2. /// 状态机控制器类
  3. /// </summary>
  4. public class BaseStateController
  5. {
  6. /// <summary>
  7. /// 切换的当前状态
  8. /// </summary>
  9. private BaseState m_State;
  10. /// <summary>
  11. /// 是否已经开始执行当前状态
  12. /// </summary>
  13. private bool m_bRunBegin = false;
  14. /// <summary>
  15. /// 构造
  16. /// </summary>
  17. public BaseStateController() { }
  18. /// <summary>
  19. /// 状态机操作方法——执行设置状态方法,执行结束状态方法
  20. /// 1、执行上一个状态的结束方法
  21. /// 2、设置新的状态
  22. /// </summary>
  23. /// <param name="State"></param>
  24. /// <param name="Value"></param>
  25. public void SetState(BaseState State)
  26. {
  27. m_bRunBegin = false;
  28. //通知前一个状态结束
  29. if (m_State != null)
  30. {
  31. m_State.StateEnd();
  32. }
  33. m_State = State;
  34. }
  35. /// <summary>
  36. /// 状态机操作方法——执行开始状态方法
  37. /// </summary>
  38. public void StateBegin()
  39. {
  40. if (m_State != null && m_bRunBegin == false)
  41. {
  42. m_State.StateBegin();
  43. m_bRunBegin = true;
  44. }
  45. }
  46. /// <summary>
  47. /// 状态机操作方法——执行更新状态方法
  48. /// </summary>
  49. public void StateUpdate()
  50. {
  51. if (m_State != null && m_bRunBegin == true)
  52. {
  53. m_State.StateUpdate();
  54. }
  55. }
  56. }

状态机控制器:

  1. public class StateBehaviour : MonoBehaviour
  2. {
  3. /// <summary>
  4. /// ID
  5. /// </summary>
  6. public int ID;
  7. /// <summary>
  8. /// 名字
  9. /// </summary>
  10. public string Name;
  11. /// <summary>
  12. /// 状态集合
  13. /// </summary>
  14. public Dictionary<string, BaseState> StateDic;
  15. /// <summary>
  16. /// 状态机控制器
  17. /// </summary>
  18. public BaseStateController StateController;
  19. public virtual void Awake()
  20. {
  21. StateDic = new Dictionary<string, BaseState>();
  22. StateController = new BaseStateController();
  23. }
  24. public virtual void Start()
  25. {
  26. }
  27. public virtual void Update()
  28. {
  29. StateController.StateUpdate();
  30. }
  31. /// <summary>
  32. /// 初始化状态机
  33. /// </summary>
  34. public virtual void Init(int id, string name)
  35. {
  36. ID = id;
  37. Name = name;
  38. }
  39. /// <summary>
  40. /// 设置状态
  41. /// </summary>
  42. /// <param name="state"></param>
  43. public virtual void SetState(string state)
  44. {
  45. StateController.SetState(StateDic[state]);
  46. StateController.StateBegin();
  47. }
  48. }

小兵AI状态机管理器

  1. public class SoldierBehaviour : StateBehaviour
  2. {
  3. /// <summary>
  4. /// 本身物体
  5. /// </summary>
  6. public GameObject Local;
  7. /// <summary>
  8. /// 目标物体
  9. /// </summary>
  10. public GameObject Target;
  11. /// <summary>
  12. /// 自身AI对象
  13. /// </summary>
  14. public SoldierAI AI;
  15. public override void Awake()
  16. {
  17. base.Awake();
  18. StateDic.Add("SoldierState_Alignment", new SoldierState_Alignment(StateController, this));
  19. StateDic.Add("SoldierState_Attack", new SoldierState_Attack(StateController, this));
  20. StateDic.Add("SoldierState_CrazyAttack", new SoldierState_CrazyAttack(StateController, this));
  21. StateDic.Add("SoldierState_Defense", new SoldierState_Defense(StateController, this));
  22. StateDic.Add("SoldierState_Death", new SoldierState_Death(StateController, this));
  23. StateDic.Add("SoldierState_Pathfinding", new SoldierState_Pathfinding(StateController, this));
  24. StateDic.Add("SoldierState_Pursuit", new SoldierState_Pursuit(StateController, this));
  25. StateDic.Add("SoldierState_Stop", new SoldierState_Stop(StateController, this));
  26. }
  27. public override void Start()
  28. {
  29. base.Start();
  30. }
  31. public override void Update()
  32. {
  33. base.Update();
  34. }
  35. /// <summary>
  36. /// 初始化状态机
  37. /// </summary>
  38. public void Init(int id, string name, SoldierAI ai)
  39. {
  40. base.Init(id, name);
  41. AI = ai;
  42. // iDebug.YiYan("小兵状态机初始化");
  43. }
  44. /// <summary>
  45. ///
  46. /// </summary>
  47. /// <param name="name"></param>
  48. public void SetGameState(string name)
  49. {
  50. SetState(name);
  51. }
  52. /// <summary>
  53. /// 结束系统
  54. /// </summary>
  55. public void End()
  56. {
  57. Target = null;
  58. }
  59. }

然后,就是在小兵进行途中,在小兵不同的状态下写好当前状态应该做的行为,和判定切换状态的条件行为,举个例子:

小兵在自动寻路状态下在兵线上走,突然攻击预警范围内有敌人出现,则自动切换到追击状态。

等走到攻击范围内,则切换到攻击状态,开始攻击敌人。

当敌人离开攻击范围但尚未离开预警范围,则继续切换到追击状态。

当敌人离开预警范围,则切换到自动寻路状态。

自动寻路状态代码:

  1. /// <summary>
  2. /// 状态开始
  3. /// </summary>
  4. public override void StateBegin()
  5. {
  6. // iDebug.YiYan("开始State:SoldierState_Pathfinding" + soldierBehaviour.AI.obj.GetInstanceID());
  7. //播放行走动画
  8. //获取寻路目标
  9. soldierBehaviour.AI.aINaveMesh.SetState(true);
  10. soldierBehaviour.AI.aINaveMesh.SetFinalTarget();
  11. }
  12. /// <summary>
  13. /// 状态更新
  14. /// </summary>
  15. public override void StateUpdate()
  16. {
  17. //判断自身区域触发器是否有敌人
  18. if (soldierBehaviour.AI.soldierDetectionCollider.EnemyList.Count > 0)
  19. {
  20. soldierBehaviour.AI.aINaveMesh.Target = soldierBehaviour.AI.soldierDetectionCollider.EnemyList[0];
  21. soldierBehaviour.SetState("SoldierState_Pursuit");
  22. }
  23. }

追击状态代码:

  1. /// <summary>
  2. /// 状态开始
  3. /// </summary>
  4. public override void StateBegin()
  5. {
  6. // iDebug.YiYan("开始State:SoldierState_Pursuit" + soldierBehaviour.AI.obj.GetInstanceID());
  7. //播放行走动画
  8. if (soldierBehaviour.AI.aINaveMesh.Target != null && soldierBehaviour.AI.soldierDetectionCollider.EnemyList.Contains(soldierBehaviour.AI.aINaveMesh.Target))
  9. {
  10. soldierBehaviour.AI.aINaveMesh.SetDestinationTarget(soldierBehaviour.AI.aINaveMesh.Target.obj);
  11. }
  12. }
  13. /// <summary>
  14. /// 状态更新
  15. /// </summary>
  16. public override void StateUpdate()
  17. {
  18. //判断自身区域触发器是否有敌人
  19. if (soldierBehaviour.AI.soldierAttackCollider.EnemyList.Contains(soldierBehaviour.AI.aINaveMesh.Target))
  20. {
  21. soldierBehaviour.AI.aINaveMesh.SetState(false);
  22. soldierBehaviour.SetState("SoldierState_Attack");
  23. }
  24. if (!soldierBehaviour.AI.soldierDetectionCollider.EnemyList.Contains(soldierBehaviour.AI.aINaveMesh.Target))
  25. {
  26. soldierBehaviour.SetState("SoldierState_Pathfinding");
  27. }
  28. }

攻击状态代码:

  1. /// <summary>
  2. /// 状态开始
  3. /// </summary>
  4. public override void StateBegin()
  5. {
  6. // iDebug.YiYan("开始State:SoldierState_Attack" + soldierBehaviour.AI.obj.GetInstanceID());
  7. if (soldierBehaviour.AI.aINaveMesh.Target != null && soldierBehaviour.AI.soldierAttackCollider.EnemyList.Contains(soldierBehaviour.AI.aINaveMesh.Target))
  8. {
  9. soldierBehaviour.AI.soldierSkill.SetTarget(soldierBehaviour.AI, soldierBehaviour.AI.aINaveMesh.Target);
  10. soldierBehaviour.AI.soldierSkill.AutoAttack(true);
  11. }
  12. }
  13. /// <summary>
  14. /// 状态更新
  15. /// </summary>
  16. public override void StateUpdate()
  17. {
  18. //持续对目标释放技能
  19. //播放攻击动画
  20. if (!soldierBehaviour.AI.soldierAttackCollider.EnemyList.Contains(soldierBehaviour.AI.aINaveMesh.Target) && soldierBehaviour.AI.soldierDetectionCollider.EnemyList.Contains(soldierBehaviour.AI.aINaveMesh.Target))
  21. {
  22. soldierBehaviour.AI.aINaveMesh.SetState(true);
  23. soldierBehaviour.SetState("SoldierState_Pursuit");
  24. }
  25. else if (!soldierBehaviour.AI.soldierDetectionCollider.EnemyList.Contains(soldierBehaviour.AI.aINaveMesh.Target)|| soldierBehaviour.AI.aINaveMesh.Target.attribute.attributeValueNow.HP <= 0)
  26. {
  27. soldierBehaviour.AI.aINaveMesh.SetFinalTarget();
  28. soldierBehaviour.AI.aINaveMesh.SetState(true);
  29. soldierBehaviour.SetState("SoldierState_Pathfinding");
  30. // iDebug.YiYan("切换目标");
  31. }
  32. else
  33. {
  34. // iDebug.YiYan("不断攻击");
  35. }
  36. }

判定效果就像这样~~~ :

是不是非常简单易懂(笑脸+手动狗头~~)

最后,放一段小兵对线的效果:

(为了连贯性,所以把UI界面和资源导入的效果都录入了) 

 这里有的小伙伴会有疑问了,小兵的战斗数值和防御塔的属性数值是多少,如何设置呢?

哈~ 当然是在数据库中,由服务器统一分发了

 数值就动态读取,然后赋予到AI基类里即可,后面的篇章会介绍技能系统和战斗计算,那时候再详细介绍数值调用的详细逻辑吧

↓↓↓↓↓↓

文末福利:

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/872894
推荐阅读
相关标签
  

闽ICP备14008679号