一、有限状态机

一、状态基类

在创建一个FSM的有限状态机的缩写脚本


例:比如枚举这个状态,现在不确定是给敌人还是玩家,那么就写一个枚举的基类

在这里先创建了三个抽象方法,进行状态的切换;

并且这是一个状态基类,不需要挂载和继承Mono

using System;
/// <summary>
/// 状态对象基类
/// 之后所有状态都继承这个基类
/// Idle,Walk,Attack
/// </summary>
public abstract class StateBase 
{
	//当前状态对象 代表的枚举状态
	public Enum StateType;
    //进入
    public abstract void OnEnter();
	//更新
	public abstract void OnUpdate();
	//退出
	public abstract void OnExit();
}

初始化函数中传入这个枚举的变量

这是首次的初始化

至此,这个状态基类完成


二、玩家结构说明

三、玩家输入层与音效层

1、输入层

这个输入层的代码是一个类,不需要继承

使用水平和纵轴,不需要再重新定义,直接lambo表达式解决

public class Player_Input 
{
    public float Horizontal {  get=>Input.GetAxis("Horizontal"); }
    public float Vertical { get => Input.GetAxis("Vertical"); }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player_Input 
{
    private KeyCode runKeyCode = KeyCode.LeftShift;
    private KeyCode attackKeyCode=KeyCode.J;
    public float Horizontal {  get=>Input.GetAxis("Horizontal"); }
    public float Vertical { get => Input.GetAxis("Vertical"); }

    //按键持续按下状态
    public bool GetKey(KeyCode key)
    {
        return Input.GetKey(key);
    }
    //按键按下瞬间
    public bool GetKeyDown(KeyCode key)
    {
        return Input.GetKeyDown(key);
    }
	//获取当前Run按键有没有持续按下中
	public bool GetRunKey()//判断持续按下的按键
	{
        return  GetKey(runKeyCode);
	}
	//获取当前Attack按键有没有持续按下中
	public bool GetAttackKeyDown()
    {
        return GetKeyDown(runKeyCode);
    }
}

2、音效层

注:这里的audio有一个波浪线

原因是FSM继承的Mono里面有废弃的api,这里意思是询问你是否用了一个新的audio,而不是mono的api
所以前面加上一个new即可

public class Player_Audio 
{
    private AudioSource audioSource;

    public Player_Audio(AudioSource audioSource)
    {
        this.audioSource = audioSource;
    }

    //播放指定的音效
    public void PlayAudio(AudioClip audioClip)
    {
        audioSource.PlayOneShot(audioClip);
    }
}

3、脚本控制

第一

新建一个空物体,坐标归置为0,改名为Player

将模型作为子物体

这样做的好处是:功能实现在父物体上,子物体可以随时更换模型

第二

新建一个脚本“玩家控制”

先继承FSM,实现抽象方法

然后对音效和输入进行一个初始化;这里的音效需要传入一个自身的组件,因为他是一个构造函数

public class Player_Controller : FSMController
{
	public override Enum CurrentState { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }

	private Player_Input input;
	private new Player_Audio audio;
	private void Start()
	{
		input=new Player_Input();
		audio = new Player_Audio(GetComponent<AudioSource>());
		
	}
}

至此,玩家的音效和输入层结束


四、角色移动动画设置

为玩家的AnimatorController配置基于动画混合树的移动动画

(一)调整动画资源

由于这个动画镜像之后,左右不相同

所以复制一份动画,并进行相应的调整,为右侧动画即可

先来左边:取消他的镜像Mirror,按照初始动画设置

将偏移值设置为20

之后又边:偏移值则为-20,不使用镜像即可调整

注:这里最好把动画的Y轴全部勾选

(二)动画混合树

先建一个混合树

这里的类型选择2D的自由方向

新建一个混合树,会自动添加一个Blend参数,这里并不需要,直接删除

添加一个动画片段

添加两个参数,这里使用中文不会有任何影响


个人理解:动画混合树,就是对多个动画的一种平滑过渡,依靠的是数值

举个例子:WS前后移动,中间有一个动画状态为静止,那么按下W会从0~1的数值增长,对应的动画也进行平滑的过渡。

前移加上奔跑制作方法:向前的移动数值是1,而奔跑可能是1.5或者2,那么将奔跑动画的数值设置为2即可。在代码中,让这个W的值为1时在加一个1,即可实现奔跑效果

(三)混合树的制作

这里我取消了根运动

在混合树中添加第一个状态(待机),不需要按下任何按键,所以XY是0;

在混合树中添加第二个状态(前进),需要按下W,所以Y轴是1时,完全是前进动画

在混合树中添加第三个状态(后退),需要按下S,所以Y轴是-1时,完全是后退动画

在混合树中添加第四个状态(向左),需要按下A,所以X轴是-1时,完全是向左动画

在混合树中添加第五个状态(向右),需要按下D,所以X轴是-1时,完全是向右动画

在混合树中添加第六个状态(奔跑),需要长按A,所以当X轴是1加上一个1时,完全是奔跑动画


五、角色移动状态实现

1、实现人物移动未同步动画

这里的PlayMove来持有playcontroller,所以把它设置为了属性

这是第一步

下面是第二步

public enum PlayerState
{
	//移动
	Player_Move,
}

public class Player_Controller : FSMController
{
	
	public override Enum CurrentState { get => playerState; set => playerState=(PlayerState)value; }
	private PlayerState playerState;
	public Player_Input input { get; private set; }
	public new Player_Audio audio { get; private set; }
	public CharacterController characterController { get; private set; }
	private void Start()
	{
		input=new Player_Input();
		audio = new Player_Audio(GetComponent<AudioSource>());
		characterController = GetComponent<CharacterController>();
		//默认是移动状态
		ChangeState(PlayerState.Player_Move);
	}
}

这是第一步的代码

注:这里的SimpleMove方法传入的ying'ga'shi'yi

public class Player_Move : StateBase
{
	public Player_Controller player;
	private float moveSpeed = 90;
	private float rotateSpeed = 90;
	public override void Init(FSMController controller, Enum stateType)
	{
		base.Init(controller, stateType);
		player=controller as Player_Controller;
	}
	public override void OnUpdate()
	{
		float h = player.input.Horizontal;
		float v = player.input.Vertical;
		Move(h, v);
	}
	private void Move(float h,float v)
	{
		//移动
		Vector3 dir= new Vector3(0,0,h);
		dir=player.transform.TransformDirection(dir);
		player.characterController.SimpleMove(dir*moveSpeed);
		//旋转
		Vector3 rot = new Vector3(0,v,0);
		player.transform.Rotate(rot*Time.deltaTime*rotateSpeed);
		//todo:同步模型动画
	}
	public override void OnEnter()
	{
		
	}
	public override void OnExit()
	{
		
	}
}

2、实现人物移动并同步动画

新建代码用于模型控制

在这个代码中包含武器、动画、特效等

将这个代码拖拽给人物模型

先持有玩家控制的脚本以及动画组件

并对变量进行初始化

这里是一个意思

然后更新相关的移动参数 ,设置动画参数,实现关联

//动画、武器层、刀光效果
public class Player_Model : MonoBehaviour
{
	private Player_Controller player;
	private Animator animator;
	
	public void Init(Player_Controller player)
	{
		this.player = player;
		animator = GetComponent<Animator>();
	}
	//更新移动相关参数
	public void UpdateMovePar(float x,float y)
	{
		animator.SetFloat("左右", x);
		animator.SetFloat("前后", y);
	}
}

然后再玩家控制脚本中,添加这个属性 ;并在开始时初始化

在玩家控制脚本中,实现玩家的shift奔跑效果

先定义变量奔跑动画过渡时间、移动速度、旋转速度

添加一个bool的属性,可以只得到,不设置: 当玩家按下w和shift时,重新赋值移动速度,并返回这个bool变量

在update中,定义局部变量水平和纵轴

如果处于奔跑状态,并且过渡时间小于1,那么过渡时间加到1

如果出于非奔跑状态并且过渡时间大于零,那么过渡时间慢慢减到0

如果没有移动,过渡时间大于零了,那么过渡时间会逐渐至0

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player_Move : StateBase
{
	public Player_Controller player;
	private float runTransition = 0;
	private float moveSpeed = 90;
	private float rotateSpeed = 90;

	private bool isRun
	{
		get
		{
			bool temp= player.input.GetRunKey() && player.input.Vertical > 0;
			if (temp) moveSpeed = 200f;
			else moveSpeed = 100f;
			return temp;
		}
		
	}
	public override void Init(FSMController controller, Enum stateType)
	{
		base.Init(controller, stateType);
		player=controller as Player_Controller;
	}
	public override void OnUpdate()
	{
		float h = player.input.Horizontal;
		float v = player.input.Vertical;
		if (v >= 0)
		{
			if (isRun && runTransition < 1) runTransition += Time.deltaTime / 2;//慢慢加到1
			else if (!isRun && runTransition > 0) runTransition -= Time.deltaTime / 2;//慢慢减到0
		}
		else if (runTransition > 0) runTransition -= Time.deltaTime / 2;
		
        Move(h, v+runTransition);
	}
	private void Move(float h,float v)
	{
		//移动
		Vector3 dir= new Vector3(0,0,v);
		dir=player.transform.TransformDirection(dir);
		player.characterController.SimpleMove(dir *Time.deltaTime * moveSpeed);
		//旋转
		Vector3 rot = new Vector3(0,h,0);
		player.transform.Rotate(rot*Time.deltaTime*rotateSpeed);
		//同步模型动画
		player.model.UpdateMovePar(h, v);
	}
	public override void OnEnter()
	{
		
	}
	public override void OnExit()
	{
		
	}
}

六、虚拟相机Cinemachne

常规方法是:虚拟摄像机始终跟随玩家,但一些游戏设定,例如:屏幕晃动,摄像机消失等
这些效果,操作虚拟摄像机较为复杂

所以,在玩家下面创建一个空物体,让虚拟摄像机跟随这个空物体,效果也实现在这个空物体上

摄像机的绑定选择始终看向目标

X Y Z的平滑值设置设置为0.5

七、状态机重构

1、Enum的Bug

Enum他是一个Class的引用类型,比较的是值而不是引用

所以这里永远不会相等

这里其实不相等的

修改为Equals()进行比较,只考虑值而不考虑引用

2、状态机去除enum

因为这里的Enum不知道是玩家的还是怪物的,他是一个具体的类型

所以在这里重构为泛型

把当前代码中的所有Enum都改成T泛型

此时会有大量报错

同时要确定stateBase的类型


这个代码继承了Fsm,而Fsm是一个泛型,所以在派生类中传入PlayState的枚举类型

那么基类的泛型T就会接收到这个枚举类型

以后的怪物状态等,也使用这个方法

这就是泛型的应用


这里的报错就是枚举类型和泛型类型无法比较,使用强转划不来

此时确定StateBase的类型

将所有的stateBase基类都加上泛型

3、优化循环

这里的循环,每一个集合里面的对象,但是对象如果有大量的,就会走一步卡一步,在这里消耗性能,所以在这里使用字典

把集合改成了字典,这样就可以每次获取,不需要查找

那他的Key就是T(玩家状态或怪物状态)

接下来优化代码中的反射

原因是:使用反射对性能的消耗很大

增加一个泛型K,并且是泛型约束

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值