技能位移一般在动画素材中包含,但如果不包含,就需要在程序中实现

技能位移作用于Controller类,类似于PlayerController,但是有不一样的速度

修改Controller父类

在Controller父类中添加skillMove的bool变量,和skillMoveSpeed的float变量,并添加SetSkillMove公共方法

1
2
3
4
5
6
7
8
protected bool skillMove = false;
protected float skillMoveSpeed = 0f;

public void SetSkillMoveState(bool move,float skillSpeed=0f)
{
skillMove = move;
skillMoveSpeed = skillSpeed;
}

修改PlayerController

PlayerController中检测skillMove变量,并添加SerSkillMove私有方法产生移动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void Update()
{
//...
if (skillMove)
{
SetSkillMove();
//摄像机跟随
SetCam();
}
}
private void SetSkillMove()
{
characterController.Move(transform.forward * Time.deltaTime * skillMoveSpeed);
}

位移技能配置

从上面的代码可知,我们需要知道一个技能移动速度等信息,这种信息需要在游戏中不断修改和配置,所以我们可以先设定一个配置文件

因为这个配置文件可能会随时修改,所以我们先命名为skillmove_format_v1

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<root>
<item ID="">
<moveTime></moveTime>
<moveDis></moveDis>
</item>
<item ID="">
<moveTime></moveTime>
<moveDis></moveDis>
</item>
</root>

在Excel中编辑后,导出时取名为skillmove.xml

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<item ID="1011">
<moveTime>80</moveTime>
<moveDis>5.5</moveDis>
</item>
<item/>
</root>

位移技能的ID是“1011”,是为了能和ID为“101”的技能联系起来

moveTime单位是毫秒

为了让技能配置和技能位移配置联系起来,我们修改之前的skill_format.xml

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

添加了skillMove这个Node,来指定一个skill配置对应的skillMove配置

重新导出的skill.xml

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>
<skillMove>1011</skillMove>
</item>
</root>

应用技能位移

技能位移逻辑在Controller内,我们要调用它就需要先在EntityBase内声明对应方法

1
2
3
4
5
6
7
public virtual void SetSkillMoveState(bool move,float speed = 0f)
{
if(controller != null)
{
controller.SetSkillMoveState(move, speed);
}
}

然后再在SkillMgr里面根据当前skill的配置数据确定此技能是否需要位移并且执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
        public void AttackEffect(EntityBase entity,int skillID)
{
SkillCfg skillCfg = resSvc.GetSkillCfg(skillID);

entity.SetAction(skillCfg.aniAction);
entity.SetFX(skillCfg.fx, skillCfg.skillTime);
//+++++
SkillMoveCfg skillMoveCfg = resSvc.GetSkillMoveCfg(skillCfg.skillMove);
float speed = skillMoveCfg.moveDis / (skillMoveCfg.moveTime / 1000f);//先计算speed
entity.SetSkillMoveState(true, speed);
timerSvc.AddTimeTask(tid =>
{
entity.SetSkillMoveState(false);
}, skillMoveCfg.moveTime);//根据时间关闭
//+++++
timerSvc.AddTimeTask(tid =>
{
entity.Idle();
}, skillCfg.skillTime);
}

此代码还需要修改

运行时更新配置

ResSvc作为一个单例,在其内部缓存的各个配置字典仅此一份,其他的模块也只用这一份数据,所以我们可以在ResSvc内部提供一个方法,用来重新初始化字典,这样我们可以在运行时修改配置的xml数据,然后在游戏内点击某个按钮刷新数据字典,数据就实时更新了,这种方式用来调试技能配置参数。

能这样做是因为配置文件xml是文本文件,修改后不会被编译。在程序中外部文本流被关闭后就只对内存中的数据进行操作。我们修改完外部文本,只需要再读取一次刷新内存数据即可。

ResCfg增加API

1
2
3
4
5
6
7
8
public void ResetSkillCfgs()
{
skillCfgDataDic.Clear();
skillMoveCfgDataDic.Clear();
InitSkillCfg(PathDefine.SkillCfgPath);
InitSkillMoveCfg(PathDefine.SkillMoveCfgPath);
PECommon.Log("Reset Skill Cfgs");
}

PlayerCtrlWnd增加API

1
2
3
4
public void ClickResetCfgs()
{
resSvc.ResetSkillCfgs();
}

技能位移延迟

我们还需要给位移技能增加延迟,希望隔一段时间位移

更新配置文件skillmove.xml,增加了delayTime,表示多长时间后开启位移技能

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<item ID="1011">
<delayTime>300</delayTime>
<moveTime>80</moveTime>
<moveDis>5.5</moveDis>
</item>
<item ID="1012">
<delayTime>300</delayTime>
<moveTime>80</moveTime>
<moveDis>5.5</moveDis>
</item>
</root>

配置文件更新流程和代码更新流程略

修改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
27
28
29
30
31
32
        public void AttackEffect(EntityBase entity,int skillID)
{
SkillCfg skillCfg = resSvc.GetSkillCfg(skillID);

entity.SetAction(skillCfg.aniAction);
entity.SetFX(skillCfg.fx, skillCfg.skillTime);

SkillMoveCfg skillMoveCfg = resSvc.GetSkillMoveCfg(skillCfg.skillMove);
float speed = skillMoveCfg.moveDis / (skillMoveCfg.moveTime / 1000f);
//++++
if(skillMoveCfg.delayTime > 0)//确定技能是否需要延迟
{
timerSvc.AddTimeTask(tid =>
{
entity.SetSkillMoveState(true, speed);
},skillMoveCfg.delayTime);
}
else
{
entity.SetSkillMoveState(true, speed);
}

timerSvc.AddTimeTask(tid =>
{
entity.SetSkillMoveState(false);
}, skillMoveCfg.moveTime + skillMoveCfg.delayTime);//将技能延迟时间也加上
//++++
timerSvc.AddTimeTask(tid =>
{
entity.Idle();
}, skillCfg.skillTime);
}

技能分阶段位移

再更新一波skill.xml配置文件,将之前的skillMove改为skillMoveList

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>
<skillMoveList>1011|1012</skillMoveList>
</item>
</root>

我们将skillMoveList解析成列表,一个技能可能对应多个skillMove,所以这也是为什么skillMove需要单独写一个配置文件。

再修改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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public void AttackEffect(EntityBase entity,int skillID)
{
SkillCfg skillCfg = resSvc.GetSkillCfg(skillID);

entity.SetAction(skillCfg.aniAction);
entity.SetFX(skillCfg.fx, skillCfg.skillTime);

List<int> skillMoveList = skillCfg.skillMoveList;

int sum = 0;//使用sum控制时间,多个moveskill的开启时间累加一下
for (int i = 0; i < skillMoveList.Count; i++)
{

SkillMoveCfg skillMoveCfg = resSvc.GetSkillMoveCfg(skillCfg.skillMoveList[i]);
float speed = skillMoveCfg.moveDis / (skillMoveCfg.moveTime / 1000f);

sum += skillMoveCfg.delayTime;
if(sum > 0)
{
timerSvc.AddTimeTask(tid =>
{
entity.SetSkillMoveState(true, speed);
},sum);
}
else
{
entity.SetSkillMoveState(true, speed);
}
sum += skillMoveCfg.moveTime;
timerSvc.AddTimeTask(tid =>
{
entity.SetSkillMoveState(false);
}, sum);
}


timerSvc.AddTimeTask(tid =>
{
entity.Idle();
}, skillCfg.skillTime);
}

一个技能启动时,可能会开启多个计时器,有些计时器负责技能位移,有个计时器负责技能结束后重置状态,注意负责技能位移的计时器delayTime和moveTime加起来不能超过重置状态计时器的skillTime

过滤技能释放时的方向控制

先把之前的skill.xml配置内的“skillMoveList”设为“1011”,“skillName”改为“突袭”。

skillmove.xml配置内删掉整个“item1012”的配置

当我们使用位移技能时,应该锁定轮盘,停止对轮盘的响应。因为对轮盘的控制是UI发出的,所以我们在Entity层面声明“canControl”这个bool变量来控制

EntityBase内添加“canControl”bool变量

1
public bool canControl = true;

SkillMgrAttackEffect方法中将canControl设为false,之所以在这里设是因为所有的技能都会调用到这个方法

1
2
3
4
5
6
7
8
9
10
11
12
public void AttackEffect(EntityBase entity,int skillID)
{
//...

entity.canControl = false;//++++

timerSvc.AddTimeTask(tid =>
{
entity.Idle();
}, skillCfg.skillTime);
}

StateAttackExit生命周期中将canControl设为true

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

BattleMgrSetSelfPlayerMoveDir方法内判断canControl变量,如果为false则return

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void SetSelfPlayerMoveDir(Vector2 dir)
{
if (!entitySelfPlayer.canControl) return;

//设置玩家移动
if(dir == Vector2.zero)
{
entitySelfPlayer.Idle();
}
else
{
entitySelfPlayer.Move();
entitySelfPlayer.SetDir(dir);
}
}

到目前的修改还有问题,如果玩家在移动角色的过程中释放技能,角色从Move状态直接转换为attack状态,在Controller父类中,isMove变量没有设为false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected bool isMove = false;
private Vector2 dir = Vector2.zero;
public Vector2 Dir
{
get => dir; set
{
if (value == Vector2.zero)
{
isMove = false;//isMove只有在dir为zero时才会为false
}
else
{
isMove = true;
}
dir = value;
}
}

直接在skillMgr里将dir归零

1
2
3
4
5
6
7
8
9
10
11
12
public void AttackEffect(EntityBase entity,int skillID)
{
//...

entity.canControl = false;//++++
entity.SetDir(Vector2.zero);

timerSvc.AddTimeTask(tid =>
{
entity.Idle();
}, skillCfg.skillTime);
}

Idle状态自动检测

当玩家一直按住UI轮盘向一个方向不动的情况下,释放技能后,角色并不能继续移动。

这是因为UGUI的OnDrag调用逻辑是,只有UI在拖拽时才会被调用,玩家释放技能会强制将Controller父类的dir重置为zero,此时UI轮盘不动就不会重置dir。

要解决此问题,我们需要将释放技能前的轮盘dir缓存起来,技能结束后根据缓存的轮盘dir来判断角色是否需要继续移动。

轮盘Dir和Controller的dir是不同的,前者记录的是玩家操作的Dir,后者才是真正驱动角色移动的dir

修改PlayerCtrlWnd

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
27
public Vector2 currentDir;
public void RegisterTouchEvents()
{
//...
OnPointerUp(imgTouch.gameObject, evt =>
{
imgDirBg.transform.position = defaultPos;
SetActive(imgDirPoint, false);
imgDirPoint.transform.position = Vector2.zero;
currentDir = Vector2.zero;//+++
BattleSys.Instance.SetMoveDir(currentDir);//+++
});
OnPointerDrag(imgTouch.gameObject, evt =>
{
Vector2 dir = evt.position - startPos;
float length = dir.magnitude;
if (length > pointDis)
{
Vector2 clampDir = Vector2.ClampMagnitude(dir, pointDis);
imgDirPoint.transform.position = startPos + clampDir;
}
else imgDirPoint.transform.position = evt.position;
currentDir = dir.normalized;//+++
BattleSys.Instance.SetMoveDir(currentDir);//+++
});
}

这样轮盘Dir就缓存好了,我们在BattleSys中添加GetDirInput方法来将此值转出去

1
2
3
4
public Vector2 GetDirInput()
{
return playerCtrlWnd.currentDir;
}

我们再在BattleMgr中转一下,这是因为在系统设计中BattleSys不能被即时创建的逻辑实体调用

1
2
3
4
public Vector2 GetDirInput()
{
return BattleSys.Instance.GetDirInput();
}

EntityBase修改

我们最终在Entity中获取UI轮盘缓存的Dir,修改EntityBase

1
2
3
4
5
6
public BattleMgr battleMgr = null;//别忘了在BattleMgr中注入

public virtual Vector2 GetDirInput()
{
return Vector2.zero;
}

因为UI轮盘缓存的Dir只有玩家的Entity能用到,所以在EntityBase中直接返回zero

修改EntityPlayer

1
2
3
4
5
6
7
8
9
10
11
12
using UnityEngine;

namespace DarknessWarGodLearning
{
public class EntityPlayer : EntityBase
{
public override Vector2 GetDirInput()
{
return battleMgr.GetDirInput();
}
}
}

修改StateIdle

我们的位移技能结束后,会进入Idle状态,我们可以在StateIdle这个类里面的Process周期中判断UI轮盘缓存的Dir,再转入Move状态

1
2
3
4
5
6
7
8
9
10
11
12
public void Process(EntityBase entity, params object[] args)
{
if (entity.GetDirInput() != Vector2.zero)
{
entity.Move();
entity.SetDir(entity.GetDirInput());
}
else
{
entity.SetBlend(Constants.BlendIdle);
}
}