目录
声明
16:AttackData 攻击属性
17:Execute Attack 实现攻击数值计算
18:Guard & Dead 守卫状态和死亡状态
19:泛型单例模式 Singleton
20:Observer Pattern 接口实现观察者模式的订阅和广播
本教程学习均来自U3D中文课堂麦扣老师
将人物攻击的数值也写成ScriptObject,在Scripts文件夹中创建一个与攻击相关的文件夹Combat,创建脚本AttactData_SO:写一些基本的数值
AttactData_SO:
[CreateAssetMenu(fileName = "New Attack",menuName ="Attack/Attack Data")]
public class AttactData_SO : ScriptableObject
{
public float attackRange;//攻击距离
public float skillRange;//远程攻击距离
public float coolDown;//冷却时间
public float minDamge;//最小攻击数值
public float maxDamge;//最大攻击数值
public float criticalMultiplier;//暴击加成
public float criticalChance;//暴击率
}
创建PlayerBaseAttack Data ,设置数值
创建好了需要在CharacterStats代码中进行调用,这样人物可以读到攻击数值:
创建 AttactData_SO变量即可: public AttactData_SO attactData;
在PlayerController中补充未做的修改攻击范围参数:
//修改攻击范围参数
while(Vector3.Distance(attackTarget.transform.position,transform.position)>characterStats.attactData.attackRange)
{
agent.destination = attackTarget.transform.position;
yield return null;
}
在EnemyController中完善Chase状态:在攻击范围内则攻击
先定义CharacterStats 变量, private CharacterStats characterStats;
在Awake中初始化:characterStats = GetComponent
写2个方法判断是否在攻击范围内:
bool TargetInAttackRange()//是否能进行近距离攻击
{
if (attackTarget != null)
return Vector3.Distance(attackTarget.transform.position, transform.position) <= characterStats.attactData.attackRange;
else
return false;
}
bool TargetInSkillRange()//是否能进行远距离攻击
{
if (attackTarget != null)
return Vector3.Distance(attackTarget.transform.position, transform.position) <= characterStats.attactData.skillRange;
else
return false;
}
定义攻击时间间隔: private float lastAttackTime;
在Update中进行计时: lastAttackTime -= Time.deltaTime;
//TODO: 在攻击范围内则攻击
if(TargetInAttackRange()||TargetInSkillRange())//在攻击范围内
{
isFollow = false;
agent.isStopped = true;
if(lastAttackTime < 0)//攻击
{
lastAttackTime = characterStats.attactData.coolDown;
//暴击判断
}
}
进行暴击判断:在CharacterStats创建一个布尔值判断是否暴击:public bool isCritical;上一行加上 [HideInInspector]让它不在Inspector面板显示
回到EnemyController:
进行暴击判断:
//暴击判断
characterStats.isCritical = Random.value < characterStats.attactData.criticalChance;
写一个执行攻击的方法:
void Attack()//执行攻击
{
transform.LookAt(attackTarget.transform);//看着攻击目标
if(TargetInAttackRange())
{
//近身攻击动画
}
if(TargetInSkillRange())
{
//技能攻击动画
}
}
为Slime添加Attack Data:
添加攻击动画:
敌人跑到人物Player面前要停止移动:回到IdleBattle的状态下,然后进行暴击攻击和普通攻击的判断:
void SwitchAnimation()//切换动画
{
anim.SetBool("Walk",isWalk);
anim.SetBool("Chase",isChase);
anim.SetBool("Follow",isFollow);
anim.SetBool("Critical", characterStats.isCritical);
}
void Attack()//执行攻击
{
transform.LookAt(attackTarget.transform);//看着攻击目标
if(TargetInAttackRange())
{
//近身攻击动画
anim.SetTrigger("Attack");
}
if(TargetInSkillRange())
{
//技能攻击动画
anim.SetTrigger("Skill");
}
}
现在敌人可以攻击了,但是拉托敌人后敌人不动了,要把 agent.isStopped = false;加上让敌人可以继续移动
if(!FoundPlayer())
{
//拉托回到上一个状态
isFollow = false;
if(remainLookAtTime > 0)
{
agent.destination = transform.position;
remainLookAtTime -= Time.deltaTime;
}
else if(isGuard)
{
enemyStates = EnemyStates.GUARD;
}
else
{
enemyStates = EnemyStates.PATROL;
}
}
else
{
isFollow = true;
agent.isStopped = false;
agent.destination = attackTarget.transform.position;//追Player
}
添加Player暴击动画:
代码当中保持同步:
//Attack
if(lastAttackTime < 0)
{
anim.SetTrigger("Attack");
anim.SetBool("Critical", characterStats.isCritical);
//重置冷却时间
lastAttackTime = characterStats.attactData.coolDown;
}
接下来就可以写受伤的计算公式了,受伤部分的计算放在CharacterStats里面,因为这里面读取了最基本的每一个人物的数值,
#region Character Combat
public void TakeDamage(CharacterStats attacker, CharacterStats defener)
{
int damage = Mathf.Max(attacker.CurrentDamage() - defener.CurrentDefence,0);//保证伤害不会是负值
CurrentHealth = Mathf.Max(CurrentHealth - damage, 0);//保证血量不会是负值
//TODO:Update UI
//TODO:经验Update
}
private int CurrentDamage()//当前伤害
{
float coreDamage = UnityEngine.Random.Range(attactData.minDamge, attactData.maxDamge);//核心伤害
if(isCritical)//暴击
{
coreDamage *= attactData.criticalMultiplier;
}
return (int)coreDamage;
}
#endregion
在动画的位置执行一个事件,事件来调用一个函数方法来计算它们2个之间的生命,
先在PlayerController中补充暴击布尔值代码:
private void EventAttack(GameObject target)
{
if(target != null)
{
attackTarget = target;
characterStats.isCritical = UnityEngine.Random.value < characterStats.attactData.criticalChance;
StartCoroutine(MoveToAttackTarget());//协程:攻击敌人
}
}
写Hit事件:
//Animation Event
void Hit()
{
var targetStats = attackTarget.GetComponent();//临时变量
targetStats.TakeDamage(characterStats,targetStats);
}
添加事件:
同样,在EnemyController中添加事件方法:
//Animation Event
void Hit()
{
if(attackTarget != null)
{
var targetStats = attackTarget.GetComponent();//临时变量
targetStats.TakeDamage(characterStats, targetStats);
}
}
发现敌人的动画都是Read——Only的
如何解决FBX导入动画无法编辑的问题:将FBX文件里的动画Ctrl+D复制一份出来并更换掉之前的就可以了
现在就可以造成伤害了
ScriptableObject一个特性就是在你打包好了游戏后只要游戏不退出,ScriptableObject也会一直保留你的数据,所以每次试玩结束后要记得把数值该回去
进入EnemyController当中写一下另外的几个状态:
case EnemyStates.GUARD:
isChase = false;
if(transform.position != guardPos)
{
isWalk = true;
agent.isStopped = false;
agent.destination = guardPos;//回到站桩点
if(Vector3.SqrMagnitude(guardPos - transform.position)<= agent.stoppingDistance)//判断两点之间的距离
{
isWalk = false;
}
}
要使得Slime回去能转向回原来的方向,创建一个四元数变量来存储原来的rotation
private Quaternion guardRotation;//初始旋转角度
在Awake中初始化: guardRotation = transform.rotation;
缓慢转回原来的角度:
case EnemyStates.GUARD:
isChase = false;
if(transform.position != guardPos)
{
isWalk = true;
agent.isStopped = false;
agent.destination = guardPos;//回到站桩点
if(Vector3.SqrMagnitude(guardPos - transform.position)<= agent.stoppingDistance)//判断两点之间的距离
{
isWalk = false;
transform.rotation = Quaternion.Lerp(transform.rotation, guardRotation, 0.01f);
}
}
添加死亡动画:
为Slime创建一个Death Layer,
先来制作Enemy死亡的有关部分:
创建bool变量 bool isDead;
Update中实时监测:
if(characterStats.CurrentHealth == 0)
{
isDead = true;//死亡
}
SwitchAnimation()切换动画方法中加入 anim.SetBool("Death", isDead);
void SwitchStates() //切换状态
{
if (isDead)//死亡
enemyStates = EnemyStates.DEAD;
//如果发现Player,切换为CHASE
else if(FoundPlayer())
{
enemyStates = EnemyStates.CHASE;
}
。。。
补充死亡状态:
case EnemyStates.DEAD:
agent.enabled = false;
Destroy(gameObject, 2f);
break;
现在就可以杀死敌人了:但是敌人死了之后还能攻击,我们可以消除敌人的Collider
定义 private Collider coll;
Awake初始化: coll = GetComponent
case EnemyStates.DEAD:
coll.enabled = false;
agent.enabled = false;
Destroy(gameObject, 2f);
break;
现在就可以了
下面设置Player的死亡状态: private bool isDead;
Update: isDead = characterStats.CurrentHealth == 0;
private void SwitchAnimation()//实时切换动画
{
anim.SetFloat("Speed", agent.velocity.sqrMagnitude); //sqrMagnitude将velocity转换为浮点数值
anim.SetBool("Death", isDead);
}
就完成死亡状态了;
public class GameManager : MonoBehaviour
{
public CharacterStats playerStats;
public void RigisterPlayer(CharacterStats player)//反向注册
{
playerStats = player;
}
}
打开Singleton:
泛型类的创建方法:在一个类名后面接上一个尖括号,尖括号里通常写的是它的类型
对MonoBehaviour做一个约束,代表它是Singleton的一个类型:
public class Singleton: MonoBehaviour where T: Singleton
{
}
这是通常泛型单例的写法,
然后可以将MouseManager当中的初始的Awake方法和创建static静态变量的方法都挪到Singleton里面来写
Singleton:
public class Singleton: MonoBehaviour where T: Singleton
{
private static T instance;
public static T Instance
{
get{ return instance; }
}
protected virtual void Awake()
{
if(instance != null)
{
Destroy(gameObject);
}
else
{
instance = (T)this;
}
}
public static bool IsInitialized//判断当前单例模式是否已经初始化生成了
{
get { return instance != null; }
}
protected virtual void OnDestroy()//销毁单例
{
if(instance == this)
{
instance = null;
}
}
}
改一下MouseManager和GameManager:
public class MouseManager : Singleton
public class GameManager : Singleton
这样就能直接用了。
接下回到PlayerController注册:
private void Start()
{
。。。
GameManager.Instance.RigisterPlayer(characterStats);//注册GameManager
}
创建脚本使用第一个接口IEndGameObserver:当结束游戏给这些观察者们来调用的方法
public interface IEndGameObserver
{
}
我们要实现的就是接口需要调用的函数方法,在这里我们只写方法的定义,而不写方法里面的实现,也就是接口当中我们只写每一个使用了这个接口的代码一定要调用的一些函数
比如:
public interface IEndGameObserver
{
void EndNotify();//结束游戏的广播
}
public class EnemyController : MonoBehaviour,IEndGameObserver
修补接口可以看到:所有调用了这个接口都一定要有这个函数方法在代码当中
每一个调用接口的代码才写函数里面具体的方法
那么怎么做观察者模式的订阅和广播呢?以后我们会生成多个不同的敌人,每一个敌人都有这个接口,那么我们可以创建一个列表在GameManager当中去收集所有加载了这个接口的函数方法,那就代表它是一个敌人,需要订阅我的结束游戏的广播
在GameManager中创建一个接口类型的列表:用注册的方式让这些观察者主动添加到我们列表当中,敌人生成的时候添加到列表,死亡的时候从列表删除
public class GameManager : Singleton
{
public CharacterStats playerStats;
List endGameObservers = new List();//观察者列表
public void RigisterPlayer(CharacterStats player)//反向注册
{
playerStats = player;
}
public void AddObserver(IEndGameObserver observer)//让观察者主动添加到列表
{
endGameObservers.Add(observer);
}
public void RemoveObserver(IEndGameObserver observer)//让观察者移除列表
{
endGameObservers.Remove(observer);
}
}
EnemyController:
private void OnEnable()
{
GameManager.Instance.AddObserver(this);//让观察者主动添加到列表
}
private void OnDisable()//销毁完成之后执行
{
GameManager.Instance.RemoveObserver(this);//让观察者移除列表
}
在GameManager中实现广播:
public void NotifyObservers()//向所有的观察者广播
{
foreach(var observer in endGameObservers)
{
observer.EndNotify();
}
}
当Player死亡的时候调用这个功能广播:
PlayerController:
private void Update()
{
isDead = characterStats.CurrentHealth == 0;
if(isDead)
{
GameManager.Instance.NotifyObservers();//广播
}
SwitchAnimation();//实时切换动画
lastAttackTime -= Time.deltaTime;
}
最后在EnemyController当中实现广播内容:
添加Slime胜利动画在Victory Layer上
现在就实现实现 Player 死亡敌人集体欢呼胜利了