Unity3D RPG Core | 16 攻击属性
继上一节完成角色属性参数之后,这一节需要完成角色的攻击参数。并且完善史莱姆的攻击逻辑。
1. 创建 ScriptableObject 脚本/资产文件
和角色属性参数如出一辙,我们首先创建角色攻击参数的 ScriptableObject 脚本。如代码清单 1 所示,我们定义了攻击距离、技能距离、冷却时间、最小/最大攻击力、暴击加成倍率以及暴击几率。
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
- [CreateAssetMenu(fileName = "Attack Data", menuName = "ScriptableObject/Attack Data")]
- public class AttackDataScriptableObject : ScriptableObject
- {
- public float m_attackRange;
- public float m_skillRange;
- public float m_coolDown;
- public int m_minDamage;
- public int m_maxDamage;
- public float m_criticalMultiplier;
- public float m_criticalChance;
- }
同样的,在 Unity Assets 窗体中创建对应的 ScriptableObject 资产文件。并如图 1 所示,在 Inspector 窗体中指定各个属性的值。

同样的,如代码清单 2 所示,我们编写相应的访问脚本。视频中是把 AttackDataScriptableObject 直接放在先前的角色参数访问脚本中,共有一个访问。但是感觉有点奇怪了,觉得既然都分离开了,就彻底一点;而且视频中原先角色属性参数用访问器,现在攻击参数就直接访问了,看着不统一。
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
- public class AttackData : MonoBehaviour
- {
- [SerializeField]
- private AttackDataScriptableObject m_attackData = null;
- public float AttackRange
- {
- get { return m_attackData != null ? m_attackData.m_attackRange : 0; }
- set { if (m_attackData != null) m_attackData.m_attackRange = value; }
- }
- public float SkillRange
- {
- get { return m_attackData != null ? m_attackData.m_skillRange : 0; }
- set { if (m_attackData != null) m_attackData.m_skillRange = value; }
- }
- public float CoolDown
- {
- get { return m_attackData != null ? m_attackData.m_coolDown : 0; }
- set { if (m_attackData != null) m_attackData.m_coolDown = value; }
- }
- public int MinDamage
- {
- get { return m_attackData != null ? m_attackData.m_minDamage : 0; }
- set { if (m_attackData != null) m_attackData.m_minDamage = value; }
- }
- public int MaxDamage
- {
- get { return m_attackData != null ? m_attackData.m_maxDamage : 0; }
- set { if (m_attackData != null) m_attackData.m_maxDamage = value; }
- }
- public float CriticalMultiplier
- {
- get { return m_attackData != null ? m_attackData.m_criticalMultiplier : 0; }
- set { if (m_attackData != null) m_attackData.m_criticalMultiplier = value; }
- }
- public float CriticalChance
- {
- get { return m_attackData != null ? m_attackData.m_criticalChance : 0; }
- set { if (m_attackData != null) m_attackData.m_criticalChance = value; }
- }
- }
视频中没有这样做。视频中是把 AttackDataScriptableObject 直接放在先前的角色参数访问脚本中,共用一个访问脚本。
1.1 玩家角色使用攻击属性
我们将访问脚本挂载在玩家人物对象上之后,就可以如代码清单 3 一样访问攻击参数了(第 28 行获取到脚本组件)。是否还记得之前第 9 行中的人物攻击距离是硬编码写死的,现在我们把它更改为 AttackRange 参数。
- public class PlayerController : MonoBehaviour
- {
- CharacterData m_characterData;
- AttackData m_attackData;
- IEnumerator CoroutineAttackEnemy(GameObject obj)
- {
- transform.LookAt(obj.transform);
- while (Vector3.Distance(obj.transform.position, transform.position) > m_attackData.AttackRange)
- {
- m_navMeshAgent.destination = obj.transform.position;
- yield return null;
- }
- m_navMeshAgent.isStopped = true;
- if (m_attackCoolTime <= 0)
- {
- m_animator.SetTrigger("Attack");
- m_attackCoolTime = 0.5f;
- }
- }
- void Awake()
- {
- m_navMeshAgent = GetComponent<NavMeshAgent>();
- m_animator = GetComponent<Animator>();
- m_characterData = GetComponent<CharacterData>();
- m_attackData = GetComponent<AttackData>();
- }
- }
2. 实现敌人攻击逻辑
在为史莱姆创建好攻击属性资产文件,并将相关访问脚本附加到对象上之后,我们开始完善史莱姆的攻击逻辑。
整体的逻辑是,追击着敌人直到在攻击范围之内,停下来攻击玩家。首先是否达到攻击范围可以这样判断:
- bool IsPlayerInAttackRange(GameObject player)
- {
- return Vector3.Distance(player.transform.position, transform.position) <= m_attackData.AttackRange;
- }
史莱姆的攻击也分为普通攻击和暴击,我们先在动画控制器中将攻击对应的动画,以及触发条件设置好。
如图 2 所示,我们在待机状态的基础上设置两个攻击动画,左边是普通攻击的动画,右边是暴击动画。同时创建触发变量 Attack,并创建布尔变量 CriticalHit 区分是否暴击。

一切准备就绪之后,写代码实现攻击逻辑。如代码清单 4 所示,首先获取到属性的访问脚本变量(第 15、16 行)。攻击逻辑在原先的追击逻辑上完善。如果追击到攻击范围之内(第 77 行,IsPlayerInAttackRange),则不播放追击动画,设置 Following 变量为 false(第 79 行),并停止运动(第 80 行)。
和人物攻击一样,敌人攻击也有冷却时间,如果可以攻击,则首先计算此次攻击是否暴击。判断的方法是使用 Random.value,Random.value 会产生 (0,1) 之间的随机数,正好和我们定义暴击率范围是一致的。
攻击的逻辑看到 Attack() 函数(第 115 至 123 行),首先朝向玩家,然后设置 CriticalHit 变量决定是否暴击,接着设置 Attack 变量触发攻击动画。
最后不要忘记让角色不攻击时恢复运动(第 68 行)。
- public class EnemyController : MonoBehaviour
- {
- CharacterData m_characterData;
- AttackData m_attackData;
- float m_attackCoolTime = 0;
- void Awake()
- {
- m_navMeshAgent = GetComponent<NavMeshAgent>();
- m_animator = GetComponent<Animator>();
- m_defaultSpeed = m_navMeshAgent.speed;
- m_initPosition = transform.position;
- m_characterData = GetComponent<CharacterData>();
- m_attackData = GetComponent<AttackData>();
- }
- void SwitchState()
- {
- GameObject attackObject = null;
- switch (m_enemyStates)
- {
- case EnemyStates.GUARD:
- if (FindPlayer() != null)
- {
- Debug.Log("HasFoundPlayer");
- m_enemyStates = EnemyStates.PURSUIT;
- }
- break;
- case EnemyStates.PATROL:
- m_navMeshAgent.isStopped = false;
- if (FindPlayer() != null)
- {
- Debug.Log("HasFoundPlayer");
- m_enemyStates = EnemyStates.PURSUIT;
- break;
- }
- m_navMeshAgent.speed = m_defaultSpeed / 1.5f;
- m_animatorState.isPursuing = false;
- //Debug.Log("dist=" + Vector3.Distance(m_patrolDestPoint, transform.position));
- if (Vector3.Distance(m_patrolDestPoint, transform.position) <= m_navMeshAgent.stoppingDistance)
- {
- m_navMeshAgent.destination = transform.position;
- m_animatorState.isWalking = false;
- m_animatorState.isSensing = true;
- if (m_patrolStoppingRemainTime > 0)
- {
- m_patrolStoppingRemainTime -= Time.deltaTime;
- }
- else
- {
- m_patrolStoppingRemainTime = m_patrolStoppingTime;
- m_patrolDestPoint = GetNewPatrolDestPoint();
- }
- }
- else
- {
- m_animatorState.isWalking = true;
- m_animatorState.isSensing = false;
- m_navMeshAgent.destination = m_patrolDestPoint;
- }
- break;
- case EnemyStates.PURSUIT:
- m_navMeshAgent.isStopped = false;
- m_navMeshAgent.speed = m_defaultSpeed;
- m_animatorState.isWalking = false;
- m_animatorState.isSensing = false;
- m_animatorState.isPursuing = true;
- if ((attackObject = FindPlayer()) != null)
- {
- m_animatorState.isFollowing = true;
- m_navMeshAgent.destination = attackObject.transform.position;
- if (IsPlayerInAttackRange(attackObject))
- {
- m_animatorState.isFollowing = false;
- m_navMeshAgent.isStopped = true;
- if (m_attackCoolTime < 0)
- {
- m_attackCoolTime = m_attackData.CoolDown;
- m_attackData.isCriticalHit = Random.value < m_attackData.CriticalChance;
- Attack(attackObject);
- }
- }
- }
- else
- {
- m_animatorState.isFollowing = false;
- m_navMeshAgent.destination = transform.position;
- if (m_patrolStoppingRemainTime > 0)
- {
- m_patrolStoppingRemainTime -= Time.deltaTime;
- }
- else
- {
- m_patrolStoppingRemainTime = m_patrolStoppingTime;
- if (m_enemyType == EnemyType.PATROL)
- m_enemyStates = EnemyStates.PATROL;
- else
- m_enemyStates = EnemyStates.GUARD;
- }
- }
- break;
- case EnemyStates.DEATH:
- break;
- }
- }
- void Attack(GameObject player)
- {
- transform.LookAt(player.transform);
- if (IsPlayerInAttackRange(player))
- {
- m_animator.SetBool("CriticalHit", m_attackData.isCriticalHit);
- m_animator.SetTrigger("Attack");
- }
- }
- bool IsPlayerInAttackRange(GameObject player)
- {
- return Vector3.Distance(player.transform.position, transform.position) <= m_attackData.AttackRange;
- }
- }
最终实现的效果如图 3 所示,可以看到史莱姆有普通攻击和暴击。
