在诸如街霸、拳皇等格斗游戏中,搓招指的是玩家通过在短时间内连续输入特定的指令来释放角色的招式(比如右下右拳释放升龙拳)

那么如何通过状态机来实现搓招呢?

我们可以让每个招式都持有一个状态机,把这个招式要求的输入指令作为状态机的状态而存在。依然以升龙拳为例,我们可以把升龙拳的4个指令视为4个状态,每个状态都对应一种输入指令,在玩家输入指定指令后,切换到下一个指令的状态(如果超时或输入的指令不是特定指令则切换回初始状态),在最后一个指令状态中来处理招式释放的逻辑,并切换回初始状态

首先从一个十分简单的状态机开始,新建一个Fsm文件夹,在其中新建2个类文件

代码非常简单:

/// <summary>
/// 状态基类
/// </summary>
public abstract class FsmState {/// <summary>/// 状态ID/// </summary>public int stateID;protected FsmState(int stateID){this.stateID = stateID;}public abstract void OnEnter(Fsm fsm);public abstract void OnUpdate(Fsm fsm,float deltaTime);public abstract void OnLeave(Fsm fsm);/// <summary>///  切换状态/// </summary>public void ChangeState<TState>(int stateID,Fsm fsm) where TState : FsmState{fsm.ChangeState(stateID);}
}
/// <summary>
/// 状态机
/// </summary>
public class Fsm{/// <summary>/// 状态字典/// </summary>private Dictionary<int, FsmState> states = new Dictionary<int, FsmState>();/// <summary>/// 当前状态/// </summary>private FsmState currentState;/// <summary>/// 添加状态/// </summary>public void AddState(FsmState state){if (!states.ContainsKey(state.stateID)){states.Add(state.stateID, state);}}/// <summary>/// 切换状态/// </summary>public void ChangeState(int stateID) {if (states.ContainsKey(stateID)){FsmState state = states[stateID];currentState.OnLeave(this);currentState = state;currentState.OnEnter(this);}}/// <summary>/// 开始状态机/// </summary>public void Start(int stateID){if (states.ContainsKey(stateID)){FsmState state = states[stateID];currentState = state;currentState.OnEnter(this);}}/// <summary>/// 轮询状态机/// </summary>public void Update(float deltaTime){currentState.OnUpdate(this, deltaTime);}
}

状态机的轮询将交给持有该状态机的招式类调用,而招式的轮询最终会由持有该招式的Player类(继承MonoBehaviour)在Update中进行

接下来新建一个Skill文件夹,在其中新建一个代表招式指令状态的InstructionsState文件,继承FsmState类

为其添加要用到的字段,并在构造方法里进行初始化

/// <summary>
/// 招式指令状态
/// </summary>
public class InstructionsState : FsmState
{/// <summary>/// 输入等待时间/// </summary>private float inputWaitTime;/// <summary>/// 计时器/// </summary>private float timer;/// <summary>/// 招式指令状态对应的按键指令/// </summary>private KeyCode keyCode;/// <summary>/// 指令状态要执行的方法/// </summary>private UnityAction action;/// <summary>/// 最大招式指令状态ID/// </summary>private int maxStateID;public InstructionsState(int stateID, float inputWaitTime, KeyCode keyCode, UnityAction action, int maxStateID): base(stateID){this.inputWaitTime = inputWaitTime;timer = 0;this.keyCode = keyCode;this.action = action;this.maxStateID = maxStateID;}}

这个类的重点在于状态的轮询方法

public override void OnUpdate(Fsm fsm, float deltaTime){timer += deltaTime;if (timer >= inputWaitTime){//指令输入等待时间耗尽,重置指令状态timer = 0;fsm.ChangeState(1);}//按下任意按键if (Input.anyKeyDown){//重置计时器timer = 0;//按下了指令状态的对应按键if (Input.GetKeyDown(keyCode)){Debug.Log("玩家按下了" + keyCode.ToString());//执行该指令要执行的方法if (action != null){action();}//最后一个指令状态if (stateID == maxStateID){//重置指令状态fsm.ChangeState(1);}else{//不是最后一个指令状态,切换到下一个指令状态fsm.ChangeState(stateID + 1);}}//未按下指令状态的对应指令,重置指令状态else{fsm.ChangeState(1);}}}

在指令状态轮询时,有3种可能的情况需要切换回初始状态

1.玩家在限定时间内未输入

2.玩家的输入不是该指令状态对应的指令

3.该指令状态是最后一个指令状态

在指令状态编写完毕后,我们需要编写一个招式数据类,在其中使用一个招式ID与指令数组的字典保存数据

/// <summary>
/// 招式数据
/// </summary>
public static class SkillData{/// <summary>/// 招式ID与指令的字典/// </summary>public static Dictionary<int, int[]> instructions = new Dictionary<int, int[]>();static SkillData(){//100-D 115-S 106-Jinstructions.Add(1, new int[] { 100, 115 ,100 ,106});instructions.Add(2, new int[] { 115, 100, 106 });}}

指令数组中存储的是指令对应按键的KeyCode枚举所对应的整数

这里的字典数据可以根据需求从文件中读取,为了方便就先直接写到静态构造方法里

接着同样是在Skill文件夹下,新建一个代表招式基类的Skill类,为其添加需要的字段与方法,并在构造方法里进行初始化

/// <summary>
/// 招式基类
/// </summary>
public class Skill
{/// <summary>/// 招式ID/// </summary>protected int skillID;/// <summary>/// 招式指令的状态机/// </summary>protected Fsm fsm = new Fsm();/// <summary>/// 招式持有者/// </summary>protected Player player;/// <summary>/// 最大指令状态ID/// </summary>protected int maxSkillStateID;/// <summary>/// 指令状态ID/// </summary>protected int stateID = 0;public Skill(Player player){this.player = player;skillID = GetSkillID();Init();fsm.Start(1);}/// <summary>/// 初始化/// </summary>protected void Init(){//从字典中读取到招式指令数据,然后根据数据添加指令状态if (SkillData.instructions.ContainsKey(skillID)){int[] instructions = null;SkillData.instructions.TryGetValue(skillID, out instructions);maxSkillStateID = instructions.Length;for (int i = 0; i < instructions.Length; i++){if (i == instructions.Length - 1){//最后一个指令需要执行招式的出招处理AddInstructionsState((KeyCode)instructions[i], SkillFight);}else{AddInstructionsState((KeyCode)instructions[i]);}}}}/// <summary>/// 招式的出招处理(使用模板方法模式,延迟给子类实现)/// </summary>protected virtual void SkillFight(){player.ResetSkill();}/// <summary>/// 获取招式ID(使用模板方法模式,延迟给子类实现)/// </summary>protected virtual int GetSkillID(){return -1;}/// <summary>/// 添加指令状态/// </summary>/// <param name="keyCode">指令对应的按键</param>/// <param name="action">指令对应的方法</param>/// <param name="inputWaitTime">指令的输入等待时间</param>protected void AddInstructionsState(KeyCode keyCode, UnityAction action = null, float inputWaitTime = 0.5f){fsm.AddState(new InstructionsState(++stateID, inputWaitTime, keyCode, action, maxSkillStateID));}}

在上面的代码里调用了两个虚方法GetSkillID与SkillFight,这两个方法都需要由子类进行重写

接下来为该类添加状态机的重置与轮询方法

    /// <summary>/// 指令状态机重置/// </summary>public void Reset(){fsm.ChangeState(1);}/// <summary>/// 轮询指令状态机/// </summary>public void Update(float deltaTime){fsm.Update(deltaTime);}

这里重置状态机的方法是由持有该招式的Player类来调用的,其意义在于,我们在通过输入指令成功出招后,需要让其他可能也响应了部分指令的招式回到初始状态,以免出现玩家在释放了一个招式后继续操作却无法释放或意外释放另一个招式的情况

现在招式类已经完成,可以开始编写Player脚本了

新建Player脚本,在其中使用一个列表维护该Player的所有招式,并添加招式的轮询与重置

/// <summary>
/// 可以搓招的玩家类
/// </summary>
public class Player : MonoBehaviour
{/// <summary>/// 所有招式的列表/// </summary>private List<Skill> skills = new List<Skill>();void Update(){//轮询招式列表foreach (Skill skill in skills){skill.Update(Time.deltaTime);}}/// <summary>/// 重置所有招式/// </summary>public void ResetSkill(){foreach (Skill skill in skills){skill.Reset();}Debug.Log("所有招式都被重置了");}
}

先到这里,想要往Skills里添加招式需要等到实现了具体的招式类以后

以升龙拳和气功波为例,在Skill文件夹下新建FirePunch类,继承Skill类,重写GetSkillID与出招逻辑的方法

/// <summary>
/// 升龙拳
/// </summary>
public class FirePunch : Skill{public FirePunch(Player player) : base(player){}protected override int GetSkillID(){return 1;}protected override void SkillFight(){base.SkillFight();Debug.Log("升龙拳!");}}

整个类都非常简单,我们以同样简单的方式来编写KiBlast类

/// <summary>
/// 气功波
/// </summary>
public class KiBlast : Skill
{public KiBlast(Player player) : base(player){}protected override int GetSkillID(){return 2;}protected override void SkillFight(){base.SkillFight();Debug.Log("气功波!");}}

OK,现在让我们回到Player类里,在Start方法中为Player添加招式

 void Start(){//添加招式 添加顺序决定搓招优先级skills.Add(new FirePunch(this));skills.Add(new KiBlast(this));}

这里优先级的意义在于,如果两个招式中,指令较短的那个招式重合了指令较长的招式(比如在上面编写的两个招式指令升龙拳-DSDJ与气功波-SDJ),那么Player将优先释放在列表中靠前的那个招式

现在可以开始测试我们的搓招系统了,在场景中新建一个游戏物体,将Player脚本挂载上去,运行Unity,快速输入DSDJ

在测试中可以看到,第二次按键输入的D被Log了两次,这是因为我们在按下一次D以后再按S时,升龙拳与气功波同时响应了S的输入,进行了状态切换,升龙拳的第3个指令状态与气功波的第2个指令状态对应的指令都是D,所以第二次输入的D被Log了两次

因为招式优先级的关系,即使我们在输入S后输入过SDJ,搓出的也是升龙拳而不是气功波,如果在添加招式时让气功波排在升龙拳前面,那么我们搓出的就是气功波了(而且将无论如何都搓不出升龙拳,这点读者可以自行测试)

基于有限状态机在Unity3D中实现的简单搓招系统相关推荐

  1. Unity3D中的布娃娃(ragdoll)系统

    在FPS或者TPS游戏中,玩家死亡时会像"布娃娃"一样死去,也就是说,角色死亡是,不会执行事先设定的动画,而是实现自然坐下或倒地的效果,用来提升游戏的真实性. 布娃娃系统只适用于具 ...

  2. unity3d中ScriptingBackend选择mono和il2cpp的区别

    unity3d中ScriptingBackend选择mono和il2cpp的区别 在iOS和Android上,在Player Settings中选择mono或il2cpp脚本后端.要更改脚本后端,请转 ...

  3. 道LOT--史上最简单的物联网系统

    github地址为 https://github.com/Supermax197/tao-lot 道LOT基于树莓派,是史上最简单的物联网系统之一. 道LOT基于springboot,自动打TCP隧道 ...

  4. Keras之ML~P:基于Keras中建立的简单的二分类问题的神经网络模型(根据200个数据样本预测新的5个样本)——概率预测

    Keras之ML~P:基于Keras中建立的简单的二分类问题的神经网络模型(根据200个数据样本预测新的5个样本)--概率预测 目录 输出结果 核心代码 输出结果 核心代码 # -*- coding: ...

  5. Keras之ML~P:基于Keras中建立的简单的二分类问题的神经网络模型(根据200个数据样本预测新的5+1个样本)——类别预测

    Keras之ML~P:基于Keras中建立的简单的二分类问题的神经网络模型(根据200个数据样本预测新的5+1个样本)--类别预测 目录 输出结果 核心代码 输出结果 核心代码 # -*- codin ...

  6. Neo4j离线环境搭建与基于python中py2neo的简单操作

    Neo4j离线环境搭建与基于python中py2neo的简单操作 1 安装与配置 1.1 Neo4j安装 1.2 python操作环境配置 2 Neo4j操作 2.1 创建: 创建点 创建点边: 2. ...

  7. 基于C#中的Trace实现一个简单的日志系统

      最近在做的项目进入中期阶段,因为在基本框架结构确定以后,现阶段工作重心开始转变为具体业务逻辑的实现,在这个过程中我认为主要有两点,即保证逻辑代码的正确性和容错性.确定需求文档中隐性需求和逻辑缺陷. ...

  8. Unity3D中简单地应用玻璃材质

    最近在学习Unity3D.发现Unity3D中的玻璃效果要用到Shader才能实现.虽然简单学习了一下能看懂Shader的结构了,但要自己写一个实现自己想要的效果的Shader暂时还无能为力.这个要么 ...

  9. Unity3D中实现简单的电影模式框架

    Unity3D中实现简单的电影模式框架 游戏中,经常会有这样的需求,即播放一段电影,给玩家更好的体验.比如摄像机朝向某两个NPC,两个NPC在那里交谈之类的. 在用Unity3D制作游戏的过程中,也经 ...

最新文章

  1. Python第三周 学习笔记(2)
  2. php读取远程二进制文件,php 读取二进制文件
  3. Java的新项目学成在线笔记-day10(三)
  4. 实体类(VO,DO,DTO)的划分
  5. python csv读取数据 去掉标题-Python读csv文件去掉一列后再写入新的文件实例
  6. linux 删旧内核,Ubuntu 删除旧内核的方法
  7. python语言print函数_Python 的 print 函数
  8. ❤️六W字《计算机基础知识》(一)(建议收藏)❤️
  9. 不可不知的站群外推方法与技巧
  10. android复习第二天------布局
  11. JS中获取地址栏中的参数
  12. 微信自动抢红包软件被判赔 475 万;日本科学家打破网速全球纪录;JavaScript蝉联最受欢迎编程语言|极客头条...
  13. 关于大规模录入的数据流转
  14. 网络历史之金融投资三剑客03
  15. 企业级安全攻防三:身份认证,只有账号密码吗?
  16. 使用Kali linux生成木马入侵局域网安卓手机
  17. 【电脑使用】硬盘无法引导进入系统,无法退出BIOS
  18. 大数据发展前沿 期末总结复习
  19. alv oo sap 多个_OO ALV 全屏显示
  20. 计算机无法格式化分区,电脑硬盘无法格式化也无法分区怎么办?

热门文章

  1. 任务栏不能显示声音图标(电脑运行有声音)
  2. idea安装与使用Translat遇到的问题
  3. Photoshop中钢笔工具
  4. Mybatis中大于,小于,不等于等特殊符号的写法
  5. B站小迪安全笔记第六天-加密算法
  6. php 自有文章同步微博
  7. JavaScript属性的获取、设置和移除还有自定义属性
  8. pl\sql直接不使用update语句修改数据
  9. 《小常识-21》什么是扫码支付
  10. word 设置标题和自动编号