17.游戏场景-怪物逻辑
17.1 状态机准备
创建怪物的Animator文件在基础动画层添加出生,待机,跑步死亡动画。注意选择的动画只是有动作而不要有实际的移动,因为实际的移动是之后通过导航代理实现的。设置好过渡和一些参数。记得要把死亡状态的要把能够切换自己取消。创建攻击和受伤层。人形遮罩可以复用之前的,只受到上半身影响。因为是新的层所以设置一个默认的空状态。创建攻击状态和受伤状态并添加过渡。攻击状态可使用子状态机。子状态机设置两个攻击动画,代表左右手各攻击一次后才算完整的攻击状态。攻击和受过渡条件要非死亡状态才行,不然死亡后也可能播放攻击动画。
对于其他怪物的动画使用动画覆盖器,拖拽对应怪物的动画
创建多个怪物预制体,在预制体添加Animator组件,不要关联。之后会准备配置文件数据通过代码关联各个怪物的Animator文件。这样四个怪物和四个动画就能产生16种排列组合。
17.2 数据准备
创建excel表,配置怪物的id,怪物预制体路径,怪物动画文件路径,攻击力,移动速度,选择速度,血量,攻击间隔。去网站上转成json。取名MonsterInfo.json存储到StreamAssets文件夹下。
[
{"id":1,"res":"Monster/Z1","animator":"Animator/Monster/Aggresive","atk":5,"moveSpeed":100,"roundSpeed":100,"hp":10,"atkOffset":0.8},
{"id":2,"res":"Monster/Z2","animator":"Animator/Monster/Broken","atk":5,"moveSpeed":50,"roundSpeed":100,"hp":10,"atkOffset":1.2},
{"id":3,"res":"Monster/Z3","animator":"Animator/Monster/Calm","atk":5,"moveSpeed":60,"roundSpeed":100,"hp":10,"atkOffset":1},
{"id":4,"res":"Monster/Z4","animator":"Animator/Monster/Crawl","atk":5,"moveSpeed":100,"roundSpeed":100,"hp":10,"atkOffset":0.5},
{"id":5,"res":"Monster/Z2","animator":"Animator/Monster/Aggresive","atk":5,"moveSpeed":110,"roundSpeed":100,"hp":10,"atkOffset":0.8},
{"id":6,"res":"Monster/Z3","animator":"Animator/Monster/Crawl","atk":5,"moveSpeed":100,"roundSpeed":100,"hp":10,"atkOffset":0.8}
]
创建怪物数据脚本MonsterInfo,变量名要和配置表一一对应
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MonsterInfo
{
public int id;
public string res;
public string animator;
public int atk;
public int moveSpeed;
public int roundSpeed;
public int hp;
public float atkOffset;
}
在GameDataMgr脚本中,关联一个MonsterInfo列表。在构造函数中用Json数据管理器初始化读取。
/// <summary>
/// 专门用来管理数据的类
/// </summary>
public class GameDataMgr
{
private static GameDataMgr instance = new GameDataMgr();
public static GameDataMgr Instance => instance;
//所有的怪物数据
public List<MonsterInfo> monsterInfoList;
private GameDataMgr()
{
//读取怪物数据
monsterInfoList = JsonMgr.Instance.LoadData<List<MonsterInfo>>("MonsterInfo");
}
}
17.3 逻辑处理
创建一个怪物对象类MonsterObject脚本,继承MonoBehaviour。挂载到怪物预制体上。给各个怪物预制体添加寻路组件和胶囊碰撞器。添加胶囊碰撞器是因为之后要收到玩家伤害。
public class MonsterObject : MonoBehaviour
声明动画组件和寻路组件变量。声明当前怪物信息数据,血量,死亡标识,上次攻击时间变量。在Awake的时候拿到动画组件和寻路组件。
// 动画相关
private Animator animator;
// 位移相关 寻路组件
private NavMeshAgent agent;
// 一些不变的基础数据
private MonsterInfo monsterInfo;
// 当前血量
private int hp;
// 怪物是否死亡
public bool isDead = false;
// 上一次攻击的时间
private float frontTime;
void Awake()
{
agent = this.GetComponent<NavMeshAgent>(); // 获取当前物体上的NavMeshAgent组件
animator = this.GetComponent<Animator>(); // 获取当前物体上的Animator组件
}
创建提供给外部初始化怪物的信息方法InitInfo(MonsterInfo info)。将传入的MonsterInfo对象赋值给私有变量monsterInfo,并根据怪物信息中的配置设置动画控制器、血量、移动速度和旋转速度。
// 初始化
public void InitInfo(MonsterInfo info)
{
monsterInfo = info; // 将传入的怪物信息赋值给monsterInfo
// 状态机加载
animator.runtimeAnimatorController = Resources.Load<RuntimeAnimatorController>(info.animator);
// 要变的当前血量
hp = info.hp;
// 速度和加速度赋值 之所以赋值一样 是希望没有 明显的加速运动 而是一个匀速运动 初始化
agent.speed = agent.acceleration = info.moveSpeed;
// 旋转速度
agent.angularSpeed = info.roundSpeed;
}
创建受伤方法Wound(int dmg),外部传入伤害。对血量进行减少并设置受伤动画参数,设置受伤参数播放受伤动画。判断血量是否小于0,走死亡逻辑或者播放音效逻辑,这段逻辑待补充。
// 受伤
public void Wound(int dmg)
{
hp -= dmg; // 减少血量
animator.SetTrigger("Wound"); // 播放受伤动画
if (hp <= 0)
{
// 死亡
}
else
{
// 播放音效
}
}
创建死亡方法Dead。修改死亡标识为true,代理组件中停止移动标识也为true。设置死亡动画参数播放死亡动画。播放音效和杀死怪物加钱逻辑待补充。
// 死亡
public void Dead()
{
isDead = true;
agent.isStopped = true; // 停止移动
animator.SetBool("Dead", true); // 播放死亡动画
// 播放音效
// 加钱——我们之后通过关卡管理类 来管理游戏中的对象 通过它来让玩家加钱
}
在各个怪物的死亡动画播放完之后,添加死亡后要执行的事件。在死亡动画播放完后应该移除怪物对象,这段逻辑之后有了关卡管理器再处理。
// 死亡动画播放完毕后 会调用的事件方法
public void DeadEvent()
{
// 死亡动画播放完毕后移除对象
// 之后有了关卡管理器再来处理
}
在各个怪物出生动画播放完之后,应该添加出生后要执行的事件。出生结束后,通过设置代理组件的目标位置让怪物朝目标点移动。设置跑步动画参数来播放跑步动画。
// 出生过后再移动
// 移动——寻路组件
public void BornOver()
{
// 出生结束后 再让怪物朝目标点移动
agent.SetDestination(MainTowerObject.Instance.transform.position);
// 播放移动动画
animator.SetBool("Run", true);
}
Update中写攻击逻辑。假如怪物死亡就直接返回。只要代理组件的速度不为0,就设置跑步动画参数为true来播放跑步动画。检测离保护区域主塔点的位置小于5米并且当前时间减上次攻击时间大于攻击间隔,就重置上次攻击时间为当前时间,设置攻击动画参数来播放动画。
// 攻击
void Update()
{
if (isDead)
return;
// 根据速度 来决定动画播放什么
animator.SetBool("Run", agent.velocity != Vector3.zero);
// 检测和目标点达到移动条件时 就攻击
if (Vector3.Distance(this.transform.position, MainTowerObject.Instance.transform.position) < 5 &&
Time.time - frontTime >= monsterInfo.atkOffset)
{
// 记录这次攻击时的时间
frontTime = Time.time;
animator.SetTrigger("Atk");
}
}
在各个怪物的攻击时添加攻击事件。通过范围检测,在怪物面前1m创建一个半径一米的球进行检测,只检测主塔层。主塔层MainTower要进行添加判断是否碰撞到了主塔对象,调用保护区受伤方法,传入怪物攻击力让保护区受伤。
// 伤害检测
public void AtkEvent()
{
// 范围检测 进行伤害判断
Collider[] colliders = Physics.OverlapSphere(this.transform.position + this.transform.forward + this.transform.up, 1, 1 << LayerMask.NameToLayer("MainTower"));
for (int i = 0; i < colliders.Length; i++)
{
if (MainTowerObject.Instance.gameObject == colliders[i].gameObject)
{
// 让保护区域受到伤害
MainTowerObject.Instance.Wound(monsterInfo.atk);
}
}
}
注意寻路组件上的停止参数要设置的大一些,不然一直达不到保护区域点会一直跑
注意怪物预制体所有子物体都改成Monster层。
17.4 代码
MonsterInfo
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MonsterInfo
{
public int id;
public string res;
public string animator;
public int atk;
public int moveSpeed;
public int roundSpeed;
public int hp;
public float atkOffset;
}
GameDataMgr
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 专门用来管理数据的类
/// </summary>
public class GameDataMgr
{
private static GameDataMgr instance = new GameDataMgr();
public static GameDataMgr Instance => instance;
//记录选择的角色数据 用于之后在游戏场景中创建
public RoleInfo nowSelRole;
//音效相关数据
public MusicData musicData;
//玩家相关数据
public PlayerData playerData;
//所有的角色数据
public List<RoleInfo> roleInfoList;
//所有的场景数据
public List<SceneInfo> sceneInfoList;
//所有的怪物数据
public List<MonsterInfo> monsterInfoList;
private GameDataMgr()
{
//初始化一些默认数据
musicData = JsonMgr.Instance.LoadData<MusicData>("MusicData");
//获取初始化玩家数据
playerData = JsonMgr.Instance.LoadData<PlayerData>("PlayerData");
//读取角色数据
roleInfoList = JsonMgr.Instance.LoadData<List<RoleInfo>>("RoleInfo");
//读取场景数据
sceneInfoList = JsonMgr.Instance.LoadData<List<SceneInfo>>("SceneInfo");
//读取怪物数据
monsterInfoList = JsonMgr.Instance.LoadData<List<MonsterInfo>>("MonsterInfo");
}
/// <summary>
/// 存储音效数据
/// </summary>
public void SaveMusicData()
{
JsonMgr.Instance.SaveData(musicData, "MusicData");
}
/// <summary>
/// 存储玩家数据
/// </summary>
public void SavePlayerData()
{
JsonMgr.Instance.SaveData(playerData, "PlayerData");
}
}
MonsterObject
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class MonsterObject : MonoBehaviour
{
// 动画相关
private Animator animator;
// 位移相关 寻路组件
private NavMeshAgent agent;
// 一些不变的基础数据
private MonsterInfo monsterInfo;
// 当前血量
private int hp;
// 怪物是否死亡
public bool isDead = false;
// 上一次攻击的时间
private float frontTime;
void Awake()
{
agent = this.GetComponent<NavMeshAgent>(); // 获取当前物体上的NavMeshAgent组件
animator = this.GetComponent<Animator>(); // 获取当前物体上的Animator组件
}
// 初始化
public void InitInfo(MonsterInfo info)
{
monsterInfo = info; // 将传入的怪物信息赋值给monsterInfo
// 状态机加载
animator.runtimeAnimatorController = Resources.Load<RuntimeAnimatorController>(info.animator);
// 要变的当前血量
hp = info.hp;
// 速度和加速度赋值 之所以赋值一样 是希望没有 明显的加速运动 而是一个匀速运动 初始化
agent.speed = agent.acceleration = info.moveSpeed;
// 旋转速度
agent.angularSpeed = info.roundSpeed;
}
// 受伤
public void Wound(int dmg)
{
hp -= dmg; // 减少血量
animator.SetTrigger("Wound"); // 播放受伤动画
if (hp <= 0)
{
// 死亡
}
else
{
// 播放音效
}
}
// 死亡
public void Dead()
{
isDead = true;
agent.isStopped = true; // 停止移动
animator.SetBool("Dead", true); // 播放死亡动画
// 播放音效
// 加钱——我们之后通过关卡管理类 来管理游戏中的对象 通过它来让玩家加钱
}
// 死亡动画播放完毕后 会调用的事件方法
public void DeadEvent()
{
// 死亡动画播放完毕后移除对象
// 之后有了关卡管理器再来处理
}
// 出生过后再移动
// 移动——寻路组件
public void BornOver()
{
// 出生结束后 再让怪物朝目标点移动
agent.SetDestination(MainTowerObject.Instance.transform.position);
// 播放移动动画
animator.SetBool("Run", true);
}
// 攻击
void Update()
{
if (isDead)
return;
// 根据速度 来决定动画播放什么
animator.SetBool("Run", agent.velocity != Vector3.zero);
// 检测和目标点达到移动条件时 就攻击
if (Vector3.Distance(this.transform.position, MainTowerObject.Instance.transform.position) < 5 &&
Time.time - frontTime >= monsterInfo.atkOffset)
{
// 记录这次攻击时的时间
frontTime = Time.time;
animator.SetTrigger("Atk");
}
}
// 伤害检测
public void AtkEvent()
{
// 范围检测 进行伤害判断
Collider[] colliders = Physics.OverlapSphere(this.transform.position + this.transform.forward + this.transform.up, 1, 1 << LayerMask.NameToLayer("MainTower"));
for (int i = 0; i < colliders.Length; i++)
{
if (MainTowerObject.Instance.gameObject == colliders[i].gameObject)
{
// 让保护区域受到伤害
MainTowerObject.Instance.Wound(monsterInfo.atk);
}
}
}
}
MonsterObject.json
[
{"id":1,"res":"Monster/Z1","animator":"Animator/Monster/Aggresive","atk":5,"moveSpeed":100,"roundSpeed":100,"hp":10,"atkOffset":0.8},
{"id":2,"res":"Monster/Z2","animator":"Animator/Monster/Broken","atk":5,"moveSpeed":50,"roundSpeed":100,"hp":10,"atkOffset":1.2},
{"id":3,"res":"Monster/Z3","animator":"Animator/Monster/Calm","atk":5,"moveSpeed":60,"roundSpeed":100,"hp":10,"atkOffset":1},
{"id":4,"res":"Monster/Z4","animator":"Animator/Monster/Crawl","atk":5,"moveSpeed":100,"roundSpeed":100,"hp":10,"atkOffset":0.5},
{"id":5,"res":"Monster/Z2","animator":"Animator/Monster/Aggresive","atk":5,"moveSpeed":110,"roundSpeed":100,"hp":10,"atkOffset":0.8},
{"id":6,"res":"Monster/Z3","animator":"Animator/Monster/Crawl","atk":5,"moveSpeed":100,"roundSpeed":100,"hp":10,"atkOffset":0.8}
]
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com