第十二章:怪物系统
怪物功能是交互的重要部分,涉及到任务系统、人物状态系统等等,设计起来也较为复杂。
首先将Model中的小狼拖入场景,命名为WolfBaby,并添加动画信息
12.1 小狼的状态切换和移动
为其添加一个脚本WolfBaby以控制其行为
using UnityEngine; using System.Collections; public enum WolfBabyState{ //几种枚举状态,对应怪物的不同形态 Idle, Walk, Attack, Death } public class WolfBaby : MonoBehaviour { public WolfBabyState state = WolfBabyState.Idle; //默认动画为站立 public string aniName_death; //对应不同的动画 public string aniName_walk; public string aniName_idle; public string currentAniName; //当前播放的动画 public float changeTime = 3; //动画改变的时间间隔 public float timer = 0; //定时器 void Awake() { currentAniName = aniName_idle; } void Update() { if (state == WolfBabyState.Death) //根据state判断当前的状态,并做出相应操作 { animation.CrossFade(aniName_death); } else if(state == WolfBabyState.Attack) //为攻击状态 { //todo,对应下文的AutoAttack } else //巡逻状态 { animation.CrossFade(currentAniName); timer += Time.deltaTime; 定时器开始工作 if(timer >= changeTime) //当定时器大于3秒时 { timer = 0; RandomState(); //随机生成一种动画 animation.CrossFade(currentAniName); } } } void RandomState() { int value = Random.Range (0, 2); if (value == 0) { currentAniName = aniName_idle; } else { currentAniName = aniName_walk; } } }
可以看到小狼有两种不同的动画,每隔3秒改变一次,但在Walk状态下的小狼无法移动,需要改进
因此添加一个Character Controller,用以控制小狼的移动,并在state == WolfBabyState.Walk的时候调用SimpleMove()控制移动。
private CharacterController cc; public float speed = 0.5f; void Awake() { cc = this.GetComponent<CharacterController> (); } if(currentAniName == aniName_walk) { cc.SimpleMove(transform.forward * speed); }
此时的小狼只会朝一个方向进行移动,因此我们修改RandomState()中的设置,在切换到Walk状态时给小狼一个随机的方向。
void RandomState() { int value = Random.Range (0, 2); if (value == 0) { currentAniName = aniName_idle; } else { if(currentAniName != aniName_walk) //当value为1时,即下一个播放状态为Walk,但当前状态仍为Idle的时候,改变小狼朝向的角度 { transform.Rotate(transform.up * Random.Range(0,361)); //随机一个角度,改变朝向 } currentAniName = aniName_walk; } }
12.2 小狼遭受攻击
处理完移动行为后,接下来还有自动攻击,被攻击等功能,先处理被攻击功能。为小狼创建一个血量。
public int hp = 100; //怪物血量 public float missRate = 0.2; //怪物闪避率 public void BeDamaged(int attackValue) { int value = Random.Range (0f, 1f); //生成一个0~1之间的随机数,与missRate比较,若小于,则产生miss if (value > this.missRate) { this.hp -= attackValue; //扣除血量 if(hp <= 0) { state = WolfBabyState.Death; //播放死亡动画,2秒后销毁 Destroy(this.gameObject,2); } } }
这样达到了扣血的目的,但被攻击时的效果需要直观地显示出来,以更好地提示用户,因此我们通过Skinned Mesh Renderer组建控制怪物颜色的改变,在WolfBaby中定义两种颜色,对应普通状态和受击时的颜色。
我们定义一般状态的颜色normalColor,并通过协程控制颜色的改变,起到伤害的颜色效果
private Color normalColor; //存储原始的颜色,当被击效果结束后可以返回原样 private GameObject wolfBody; void Awake() { wolfBody = transform.Find ("Wolf_Baby").gameObject; //访问到控制颜色的子物体Wolf_Baby normalColor = wolfBody.renderer.material.color; } IEnumerator ShowWolfRed() //通过协程代替计时器,更加简单,协程的概念见https://blog.csdn.net/jasonwang18/article/details/55519165 { wolfBody.renderer.material.color = Color.red; yield return new WaitForSeconds (1f); wolfBody.renderer.material.color = normalColor; }
并在受到伤害时调用StartCoroutine(ShowWolfRed())即可
12.3 MISS效果
攻击怪物时加入一个闪避效果可以提高游戏体验,因此我们为Miss效果添加一个AudioClip,实现提示效果,在if (value > this.missRate)时,添加
AudioSource.PlayClipAtPoint(missSound,transform.position);
导入HUD Text创建Miss效果,将HUD Text放到UI root下,
它包含的UIFollow Target用以跟随主角或怪物,文本HUD Text用以显示Miss或扣血效果。在UI root下创建一个Invisible Widget,命名为HUDTextParent,并为其创建一个脚本HUDTextParent,并设置为单例模式。在每个物体创建时新增一个HUD Text
之后在WolfBaby中添加一个Empty物体,用以存放HUD Text,命名为WolfHUDtext,并将WolfBaby做成一个Prefab
private GameObject wolfHUDTextGO; //WolfBaby下的HUD Text private GameObject HUDTextGO; //UI root下的HUDTextParent下的HUD Text public GameObject HUDTextPrefab; //HUD Text的prefab,直接导入即可 private HUDText showText; //HUDTextGO下的Text信息,控制显示 private UIFollowTarget followTarget; //HUDTextGO下的位置信息,控制位置 void Awake() { wolfHUDTextGO = transform.Find ("WolfHUDText").gameObject; } void Start() { HUDTextGO = GameObject.Instantiate (HUDTextPrefab, Vector3.zero, Quaternion.identity) as GameObject; //HUDTextParent下的HUD Text由Prefab得到 HUDTextGO.transform.parent = HUDTextParent._instance.gameObject.transform; //并将这一Prefab作为HUDTextParent的子类 showText = HUDTextGO.GetComponent<HUDText> (); //取得文本和位置信息 followTarget = HUDTextGO.GetComponent<UIFollowTarget> (); followTarget.target = wolfHUDTextGO.transform; //followTarget中的位置跟随小狼的移动 followTarget.gameCamera = Camera.main; //followTarget中的Camera为main Camera followTarget.uiCamera = UICamera.current.GetComponent<Camera> (); //followTarget中的UICamera为current Camera }
初始化完成后,在Miss时进行测试
public void BeDamaged(int attackValue) { if (value > this.missRate) { } else { AudioSource.PlayClipAtPoint(missSound,transform.position); showText.Add("Miss",Color.gray,1); //添加显示的文本、颜色和时间 } }
我们添加一个方法,作为模拟攻击测试。在Update()中,通过按下“A”键起到模拟的作用
if (Input.GetKeyDown (KeyCode.A)) { BeDamaged(1); }
被攻击时,小狼会变为红色,但miss文字的显示有问题(上图灰色部分所示,字体过于巨大)
问题出现在
HUDTextGO = GameObject.Instantiate (HUDTextPrefab, Vector3.zero, Quaternion.identity) as GameObject; HUDTextGO.transform.parent = HUDTextParent._instance.gameObject.transform;
即创建HUDText物体时的问题,我们用NGUITool创建可以避免这一情况。
即
HUDTextGO = NGUITools.AddChild (HUDTextParent._instance.gameObject, HUDTextPrefab);
结果如下。
在怪物被杀死时,我们要销毁WolfBaby,并且销毁HUDText,在WolfBaby脚本中添加
if (hp <= 0) { state = WolfBabyState.Death; Destroy (this.gameObject, 2); GameObject.Destroy(HUDTextGO); }
12.4 敌人的自动攻击部分
自动攻击的设计关系到AI的智商,这里只涉及基本的AI操作,一些高端的“拉怪”操作不在考虑范围内。。。
12.4.1 自动攻击逻辑
当state == WolfBabyState.Attack时,我们需要让小狼自动攻击。攻击包括下面几个属性
public int attackValue; //攻击伤害 public string aniName_normalAttack; //正常攻击 public string aniName_crazyAttack; //疯狂攻击,提升attackRate,即攻击速率 public string aniName_nowAttack; //当前攻击的种类 public float normalAttackTime; //普通攻击消耗时间 public float crazyAttackTime; //疯狂攻击消耗时间 public float crazyAttackRate; //疯狂攻击触发的概率 public int attackRate = 1; //攻击速率,默认为1秒1次 public float attackTimer = 0; //攻击的计时器,决定attackRate public Transform target; //攻击目标,当触发BeDamage函数时获取目标
属性如下
自动攻击的逻辑为
- 当人物与小狼的距离小于可攻击距离时,进行攻击 (distance < acceptAttackDistance ,攻击)
- 当人物与小狼的距离大于可攻击距离并且小于最大攻击距离时,移动到最小距离之内,再攻击 (acceptAttackDistance < distance < maxAcceptAttackDistance,,移动再攻击)
- 当人物与小狼距离大于最大攻击距离时,返回巡逻状态 (distance > maxAcceptAttackDistance ,取消攻击状态)
因此对函数AutoAttack()地设置如下
public float minAttackDistance = 2f; public float maxAttackDistance = 5f; void AutoAttack() { if (target != null) //取得目标 { float distance = Vector3.Distance(target.position,transform.position); //计算距离 if(distance > maxAttackDistance) //大于最大攻击距离时,切换到巡逻状态 { target = null; state = WolfBabyState.Idle; } else if(distance <= minAttackDistance) //小于最小攻击距离时,攻击 { } else //介于最小与最大攻击距离时,移动到攻击距离内再攻击 { transform.LookAt(target); cc.SimpleMove(transform.forward * speed); animation.CrossFade(aniName_walk); } } else { state = WolfBabyState.Idle; } }
12.4.2 攻击行为的切换与播放
攻击行为可以拆分成两部分:攻击和距离下一次攻击开始的休息时间。因此我们将攻击状态分为3种:普通攻击、疯狂攻击以及攻击的休息间隔
先考虑distance <= minAttackDistance的情况
else if(distance <= minAttackDistance) { attackTimer += Time.deltaTime; //计时器开启 animation.CrossFade(aniName_nowAttack); //先播放当前攻击动画 if(aniName_nowAttack == aniName_normalAttack) //判断当前攻击动画的种类 { if(attackTimer >= normalAttackTime) //大于播放时间后,造成伤害 { //todo,造成伤害 animation.CrossFade(aniName_idle); } } else if(aniName_nowAttack == aniName_crazyAttack) { if(attackTimer >= crazyAttackTime) { //todo animation.CrossFade(aniName_idle); } } if(attackTimer > (1f/attackRate)) //如果大于攻击休息间隔时,随机一种攻击动画并重置计时器,实现一个攻击的循环 { RandomAttack(); attackTimer = 0; } } void RandomAttack() { float value = Random.Range (0f, 1f); if (value > crazyAttackRate) { aniName_nowAttack = aniName_normalAttack; } else { aniName_nowAttack = aniName_crazyAttack; } }
这样就实现了攻击动画和攻击行为的循环,但暂时没有取得target目标,target要在角色对小狼造成伤害后将主角信息传递给小狼,之后进行补充。
ps:(5月3日补充)中型狼和Boss狼的创建。
中型狼和大型BOSS狼
中型狼和大型狼的素材都在RPG——>Model——>Model Enemy之中,拖入场景之中
与小狼类似,我们为其添加Animation动画和角色控制器,我们使用WolfBaby的脚本,稍作修改即可。
主要涉及修改部分:Animation动画、自身gameObject的指定、属性值以及攻击距离(大狼体积较大,攻击距离过小会导致无法正常攻击)