第十二章:怪物系统
怪物功能是交互的重要部分,涉及到任务系统、人物状态系统等等,设计起来也较为复杂。
首先将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的指定、属性值以及攻击距离(大狼体积较大,攻击距离过小会导致无法正常攻击)