(一)ScriptableObject的基本功能
ScriptableObject是unity提供的一种数据配置存储基类,常用于数据持久化、数据配置和数据复用等处。先使用ScriptableObject创建CharacterData_SO来作为最基本的数值,完成后即可在由CreateAssetMenu标注所创建出的Player Data中编辑数值(可以视作模板和具体配置文件的关系)。如:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName ="New Data",menuName ="Character Stats/Data")]//此行标注会在unity-Asset界面的create选项中添加一条
public class CharacterData_SO : ScriptableObject
{
[Header("Stats Info")]
public int maxHealth;
public int currentHealth;
public int maxDefence;
public int currentDefence;
}
要实现其中数据的读取和变化等功能,需要另写脚本控制。其中使用property形式来获取ScriptableObject中的变量。如:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CharacterStas : MonoBehaviour
{
public CharacterData_SO characterData;//ScriptableObject的目标数据对象
#region Read from CharacterData_SO
public int MaxHealth//以property的形式获取CharacterData_SO中的数据
{
get
{
if (characterData != null)
{
return characterData.maxHealth;
}
else
{
return 0;
}
}
set
{
characterData.maxHealth = value;
}
}
public int CurrentHealth
{
get
{
if (characterData != null)
{
return characterData.currentHealth;
}
else
{
return 0;
}
}
set
{
characterData.currentHealth = value;
}
}
public int MaxDefence
{
get
{
if (characterData != null)
{
return characterData.maxDefence;
}
else
{
return 0;
}
}
set
{
characterData.maxDefence = value;
}
}
public int CurrentDefence
{
get
{
if (characterData != null)
{
return characterData.maxDefence;
}
else
{
return 0;
}
}
set
{
characterData.maxDefence = value;
}
}
#endregion
}
接下来将CharacterStats脚本添加到玩家和敌人上,并读取设置好的对应data文件,这样在对象挂载的其他脚本中就可以使用这些数据了,如:
public class PlayerController : MonoBehaviour
{
private CharacterStats characterStats;//玩家数据
private void Awake()
{
characterStats = GetComponent<CharacterStats>();//获得玩家数据
}
}
(二)设置攻击属性AttackData
像上面的CharacterData_SO一样创建攻击数据的ScriptableObject和对应的Assert文件,并在CharacterStats中获得攻击数值(只要实例化类即可,不用创建属性),并将先前任务中需要使用这些数值的地方进行修改,如攻击范围,冷却时间等,这样后续由于切换武器等形式导致的参数改变就可以在数据文件中统一进行。
接下来实现敌人的攻击效果。先在EnemyController中实现近战和远程的玩家是否在攻击范围内的判断。技能冷却的计时器可以直接在update中计时,因为其独立于角色行动。暴击的判断则可以写在CharacterStats类中,在需要时进行调用,其中使用[HideInInspector]标注可以让需要被其他类调用但却不希望在unity中被看到的功能在Inspector窗口中隐藏起来。最后实现敌人的攻击方法,包括攻击动画和数值。
注意动画控制器中trigger作为一次性的bool值,会在设为true后自动返回false,Bool参数通常用于表示持久性状态,Trigger参数通常用于触发一次性事件或状态转换。Bool参数适合用于表示角色的当前状态(如站立、行走),而Trigger参数适合用于表示瞬时事件(如攻击、跳跃)。
最后设置好数值和脚本的位置,就可以实现攻击属性于基于其的攻击效果了。
(三)实现攻击计算
先完善一下玩家的攻击,为玩家也制作暴击的动画,方式和slime的一样。
接下来在CharacterStats.cs,即角色数据操控类中去编写对攻击计算的实现。在这个类中实现方法如下:
#region character combat
public void TakeAttack(CharacterStats attacker,CharacterStats defener)//1对2进行攻击
{
int damage = Mathf.Max(attacker.CurrentDamage() - defener.CurrentDefence,0);//由攻击者的伤害减去被攻击者的防御,当为负数时取0
CurrentHealth = Mathf.Max(CurrentHealth - damage, 0);//被攻击,血量下降且不低于0
//TODO:update ui
//TODO:expirences update
}
private int CurrentDamage()
{
float coreDamage = UnityEngine.Random.Range(attackData.minDamage, attackData.maxDamage);
if(isCritical)//判断是否暴击
{
coreDamage = coreDamage * attackData.criticalMultiplier;
Debug.Log("Critical!" + coreDamage);//test
}
return (int)coreDamage;
}
#endregion
在unity中打开animation界面,查看攻击代码应当在动画的何时执行。当动画执行到合适的时间时触发一个事件,由事件调用方法来实现攻击数值计算。先在PlayerController中创建Hit方法来执行攻击方法,并在之前的EventAttack攻击事件方法中判断是否暴击。完成后在unity-Animation中为玩家的两个Attack动画添加事件与方法Hit(如果动画都是只读的,那就把原本资源包里打包的动画文件ctrl+d复制出来单独存放使用)。完成后再做调整,即可完成攻击数值实现
(四)实现敌人的Guard模式和Dead模式
Guard模式需要判断敌人是否出于守卫点,如果不在就需要将其导航回去。计算离导航点的距离时可以采用Vector3.SqrMagnitude(guardPosition-transform.position)方法获得两个三维向量的差值,这种方法比Distance开销更小。通过事前记录守卫坐标和角度来还原守卫状态。unity中角度被存储为Quaternion四元数,如果想实现转向原方向,可采用Lerp方法来缓慢改变参数。Guard模式部分代码如下:
case EnemyStates.GUARD:
isChase = false;
if(transform.position!=guardPosition)
{
isWalk = true;
agent.isStopped = false;
agent.destination = guardPosition;
if (Vector3.SqrMagnitude(guardPosition - transform.position) < agent.stoppingDistance)
{
isWalk = false;
transform.rotation = Quaternion.Lerp(transform.rotation,guardRotation,0.1f);//实现缓慢旋转,第三个参数控制旋转速度,越接近1f越快
}
}
break;
Dead模式则于敌人的CurrentHealth有关,首先在Animator中添加一层表示受伤和死亡的动画,关于Animator-Layer除了快捷代表上一层的所有状态外的具体作用可参阅该文章。注意死亡动画需要取消勾选“can transition to self”防止反复循环死亡动画本身。然后在DEAD状态中关闭不希望生效的组件(如agent和collider来防止死亡动画中的移动和被攻击),最后销毁敌人对象。
对玩家对象同理,但后续玩家死亡后需要更多处理。