角色的技能动画都用一个数值来表示,在程序中通过设定数值来直接切换对应的技能动画。

Tip

在动画的Transition中,“Has Exit Time”表示从当前状态转到下一状态需要在动画过渡部分播放完成后才转换,如果取消勾选,那么当前的

Transition需要指定一个Parameter,否则将无法完成状态的转换。

我们直接添加一个int值作为Parameter,命名为Action,默认为-1,接下来,所有的技能动画都通过这个值来设置。

把动画过渡的“Has Exit Time”取消勾选后,动画还是有过渡的,我们这时可以把Transition Duration的值设为0。

动画过渡设置

使用下方的代码来改变和重置“Action”的值:

1
2
3
4
5
6
7
8
9
10
11
public void ClickSkill1Btn()
{
playerAnimator.SetInteger("Action", 1);

StartCoroutine(Delay());
}
IEnumerator Delay()
{
yield return new WaitForSeconds(0.3f);//等待的时间只要在Skill1动画播放的时长之内就好
playerAnimator.SetInteger("Action", -1);
}

我们需要一个协程来将Action的值重置为-1,否则角色的技能动画会一直循环播放。

只要我们在触发技能的Transition里面关闭了“Can Transition To Self”,反复释放技能就不会反复播放技能动画

特效

在特效的Prefab中,将各个Particle和Audio Source都放在了一起,全部都勾选了Play on awake。这样每个特效都在激活时都会全部播放并且包含了声音。

特效的种类是多种多样的,在教程中,将所有特效的Prefab都设为了Player Prefab的子物体,在运行时激活即可。在实际游戏中,可能会有多个人物公用一个特效,需要配合资源系统加载,并且配合对象池优化。

技能配置

用于技能配置的xml模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<root>
<item ID="">
<skillName></skillName>
<skillTime></skillTime>
<aniAction></aniAction>
<fx></fx>
</item>
<item ID="">
<skillName></skillName>
<skillTime></skillTime>
<aniAction></aniAction>
<fx></fx>
</item>
</root>

技能配置的例子

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<item ID="101">
<skillName>穿刺</skillName>
<skillTime>900</skillTime>
<aniAction>1</aniAction>
<fx>dagger_skill1</fx>
</item>
<item/>
</root>

状态类传参

技能属于表现部分,所以Controller里面设置动画Action,在它的上层,通过Entity来调用这个Controller,而在Entity的上层,需要State类或者BattleMgr来调用Entity,不过BattleMgr大多数情况下仅用来调用Entity的状态转换API,具体的Entity逻辑需要State类来调用

我们在设计时,角色不管使用什么技能,都属于“Attack”状态,所以我们不能在状态类里设定entity的Action, 为此我们需要让状态类可以传参

修改IState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace DarknessWarGodLearning
{
public interface IState
{
void Enter(EntityBase entity,params object[] args);
void Process(EntityBase entity,params object[] args);
void Exit(EntityBase entity, params object[] args);
}

public enum AniState
{
None,
Idle,
Move,
Attack,
}
}

修改它后,修改我们的状态类StateIdleStateMoveStateAttack即可。

修改EntityBase和StateMgr

状态类修改了,状态类的参数需要传递给Entity,Entity的方法必须要包含参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void Move()
{
stateMgr.ChangeStates(this, AniState.Move,null);
}

public void Idle()
{
stateMgr.ChangeStates(this,AniState.Idle,null);
}
public void Attack(int skillID)
{
stateMgr.ChangeStates(this, AniState.Attack,skillID);
}

当然,StateMgrChangeStates也需要修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void ChangeStates(EntityBase entity,AniState targetState,params object[] args)
{
if(entity.currentAniState == targetState) { return; }

if(fsm.ContainsKey(targetState))
{
if(entity.currentAniState != AniState.None)
{
fsm[entity.currentAniState].Exit(entity,args);

}
fsm[targetState].Enter(entity,args);
fsm[targetState].Process(entity, args);
}
}

技能管理器

技能的表现是多种多样的,我们需要一个单独的管理器来处理各种技能的表现 ,这自然属于表现层,需要Entity调用

首先在EntityBase里面声名SkillMgr的引用,然后在BattleMgr里面注入

EntityBase

1
2
3
4
5
6
7
public SkillMgr skillMgr = null;

//调用SkillMgr的API
public virtual void AttackEffect(int skillID)
{
skillMgr.AttackEffect(this, skillID);
}

BattleMgr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void LoadPlayer(MapCfg mapCfg)
{
GameObject player = resSvc.LoadPrefab(PathDefine.AssassinBattlePlayerPrefab);
player.transform.position = mapCfg.playerBornPos;
player.transform.localEulerAngles= mapCfg.playerBornRota;
player.transform.localScale = Vector3.one;

entitySelfPlayer = new EntityPlayer
{
stateMgr = stateMgr,
skillMgr = skillMgr//+++
};

PlayerController playerCtrl = player.GetComponent<PlayerController>();
playerCtrl.Init();
entitySelfPlayer.controller = playerCtrl;
}

SkillMgr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using UnityEngine;

namespace DarknessWarGodLearning
{
public class SkillMgr : MonoBehaviour
{
private ResSvc resSvc;
public void InitMgr()
{
resSvc = ResSvc.Instance;
PECommon.Log("Init SkillMgr Done");
}
/// <summary>
/// 技能效果表现
/// </summary>
/// <param name="attacker">技能发起者</param>
/// <param name="skillID">技能ID</param>
public void AttackEffect(EntityBase attacker,int skillID)
{
SkillCfg skillCfg = resSvc.GetSkillCfg(skillID);

attacker.SetAction(skillCfg.aniAction);
attacker.SetFX(skillCfg.fx, skillCfg.skillTime);
}
}
}

我们需要StateAttack状态类调用技能:

1
2
3
4
public void Process(EntityBase entity, params object[] args)
{
entity.AttackEffect((int)args[0]);
}

状态与动画、特效重置

特效重置

特效属于表现层,在Controller父类里面声明用于缓存特效的字典和启动特效的方法:

1
2
3
4
5
6
7
8
9
10
11
protected Dictionary<string,GameObject> fxDic = new Dictionary<string,GameObject>();
protected TimerSvc timerSvc;

public virtual void Init()
{
timerSvc = TimerSvc.Instance;
}
public virtual void SetFX(string name, float destroy)
{

}

在PlayerController子类里面初始化特效字典,添加特效重置的Time Task

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[SerializeField] GameObject daggeratk1fx;
public override void Init()
{
base.Init();
//...
fxDic.Add(daggeratk1fx.name, daggeratk1fx);
}
public override void SetFX(string name, float destroy)
{
GameObject go;
if(fxDic.TryGetValue(name, out go))
{
go.SetActive(true);
timerSvc.AddTimeTask((tid) =>
{
go.SetActive(false);
}, destroy);
}
}

状态重置

角色在Attack状态完成之后,返回Idle状态,我们在SkillMgr中引用计时器,来完成状态重置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private TimerSvc timerSvc;
public void InitMgr()
{
//...
timerSvc = TimerSvc.Instance;
}

public void AttackEffect(EntityBase entity,int skillID)
{
SkillCfg skillCfg = resSvc.GetSkillCfg(skillID);

entity.SetAction(skillCfg.aniAction);
entity.SetFX(skillCfg.fx, skillCfg.skillTime);
timerSvc.AddTimeTask(tid =>
{
entity.Idle();//状态重置
}, skillCfg.skillTime);
}

动画重置

角色动画可能会被打断,所以不能在计时任务里面重置,我们在StateAttackExit生命周期里对它进行重置

1
2
3
4
public void Exit(EntityBase entity, params object[] args)
{
entity.SetAction(Constants.ActionDefault);
}