技能位移一般在动画素材中包含,但如果不包含,就需要在程序中实现
技能位移作用于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); 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; 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;
|
在SkillMgr
的AttackEffect
方法中将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); }
|
在StateAttack
的Exit
生命周期中将canControl设为true
1 2 3 4 5
| public void Exit(EntityBase entity, params object[] args) { entity.canControl = true; entity.SetAction(Constants.ActionDefault); }
|
在BattleMgr
的SetSelfPlayerMoveDir
方法内判断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; } 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;
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); } }
|