原创

cocos小游戏实战-03-FSM有限状态机


FSM有限状态机

Marionette 动画系统文档:https://docs.cocos.com/creator/manual/zh/animation/marionette/

https://web03-1252477692.cos.ap-guangzhou.myqcloud.com/blog/images/QQ%E6%88%AA%E5%9B%BE20220710211957.png

状态机实现人物各方向动画切换

Base/StateMachine.ts

状态机核心抽象类

import { _decorator, Component, Animation, SpriteFrame } from 'cc'
import { FSM_PARAMS_TYPE_ENUM } from '../Enum'
import State from '../Base/State'
import { SubStateMachine } from './SubStateMachine'
const { ccclass, property } = _decorator

type ParamsValueType = boolean | number

export interface IParamsValue {
  type: FSM_PARAMS_TYPE_ENUM
  value: ParamsValueType
}
export const initParamsTrigger = {
  type: FSM_PARAMS_TYPE_ENUM.TRIGGER,
  value: false,
}

export const initParamsNumber = {
  type: FSM_PARAMS_TYPE_ENUM.NUMBER,
  value: 0,
}

@ccclass('StateMachine')
export abstract class StateMachine extends Component {
  // 当前选中的动画
  private _currentState: State | SubStateMachine = null
  // 参数列表
  params: Map<string, IParamsValue> = new Map()
  // 状态机列表
  stateMachines: Map<string, State | SubStateMachine> = new Map()
  // 动画组件
  animationComponent: Animation
  // 需要等待加载的列表
  waitingList: Array<Promise<SpriteFrame[]>> = []

  get currentState() {
    return this._currentState
  }

  set currentState(newState) {
    this._currentState = newState
    this._currentState.run()
  }

  getParams(paramsName: string) {
    if (this.params.has(paramsName)) {
      return this.params.get(paramsName).value
    }
  }
  setParams(paramsName: string, value: ParamsValueType) {
    if (this.params.has(paramsName)) {
      this.params.get(paramsName).value = value
      this.run()
      this.resetTrigger()
    }
  }

  resetTrigger() {
    for (const [_, value] of this.params) {
      if (value.type === FSM_PARAMS_TYPE_ENUM.TRIGGER) {
        value.value = false
      }
    }
  }

  abstract run(): void
  abstract init(): void
}

Base/State.ts

播放状态机当前动画 行动类

/**
 * 需要知道animationClip
 * 需要播放动画能力animation
 */
import { AnimationClip, animation, Sprite, SpriteFrame } from 'cc'
import ResourceManager from '../Runtime/ResourceManager'
import { StateMachine } from './StateMachine'

const ANIMATION_SPEED = 1 / 8 // 1秒8帧
export default class State {
  animationClip: AnimationClip
  constructor(
    private fsm: StateMachine,
    private path: string,
    private wrapMode: AnimationClip.WrapMode = AnimationClip.WrapMode.Normal,
  ) {
    this.init()
  }
  // 渲染人物
  async init() {
    // 加载资源文件夹
    const promise = ResourceManager.Instance.loadDir(this.path)
    this.fsm.waitingList.push(promise)
    const spriteFrames = await promise
    this.animationClip = new AnimationClip()
    // 设置名称,目的为了判断取消
    this.animationClip.name = this.path
    // 创建一个对象轨道
    const track = new animation.ObjectTrack()
    // 添加轨道路径为Sprite组件
    track.path = new animation.TrackPath().toComponent(Sprite).toProperty('spriteFrame')

    const frames: Array<[number, SpriteFrame]> = spriteFrames.map((item, index) => [index * ANIMATION_SPEED, item])

    // 设置一条通道channel的关键帧
    track.channel.curve.assignSorted(frames)
    // 最后将轨道添加到动画剪辑以应用
    this.animationClip.addTrack(track)
    // 整个动画剪辑的周期 帧数*帧率
    this.animationClip.duration = frames.length * ANIMATION_SPEED
    // 循环播放
    this.animationClip.wrapMode = this.wrapMode
  }
  run() {
    // 设置动画,defaultClip,并且播放
    this.fsm.animationComponent.defaultClip = this.animationClip
    this.fsm.animationComponent.play()
  }
}

/Base/SubStateMachine.ts

状态机抽象类,状态机记录所有状态的动画状态,并展示当前需要展示的动画

import State from '../Base/State'
import { StateMachine } from './StateMachine'

export abstract class SubStateMachine {
  constructor(public fsm: StateMachine) {}
  // 当前选中的动画
  private _currentState: State = null
  // 状态机列表
  stateMachines: Map<string, State> = new Map()

  get currentState() {
    return this._currentState
  }

  set currentState(newState) {
    this._currentState = newState
    this._currentState.run()
  }

  abstract run(): void
}

Base/DirectionSubStateMachine.ts

只是为了抽离run方法,在Idle | Turn SubStateManager,转向动画和方向动画播放

import { _decorator } from 'cc'
import { SubStateMachine } from './SubStateMachine'
import { DIRECTION_ORDER_ENUM, PARAMS_NAME_ENUM } from '../Enum'
const { ccclass, property } = _decorator

@ccclass('DirectionSubStateMachine')
export class DirectionSubStateMachine extends SubStateMachine {
  run(): void {
    const value = this.fsm.getParams(PARAMS_NAME_ENUM.DIRECTION)
    this.currentState = this.stateMachines.get(DIRECTION_ORDER_ENUM[value as number])
  }
}

Scripts/Player/IdleSubStateMachine.ts(TurnSubStateMachine.ts同理)

继承状态机,并给状态机添加 上下左右 动画 状态

import { _decorator, AnimationClip } from 'cc'
import { StateMachine } from '../../Base/StateMachine'
import { DIRECTION_ENUM } from '../../Enum'
import State from '../../Base/State'
import { DirectionSubStateMachine } from '../../Base/DirectionSubStateMachine'
const { ccclass } = _decorator

const BASE_URL = 'texture/player/idle'

@ccclass('IdleSubStateMachine')
export class IdleSubStateMachine extends DirectionSubStateMachine {
  constructor(fsm: StateMachine) {
    super(fsm)
    // 人物动画,无限播放
    this.stateMachines.set(DIRECTION_ENUM.TOP, new State(fsm, `${BASE_URL}/top`, AnimationClip.WrapMode.Loop))
    this.stateMachines.set(DIRECTION_ENUM.BOTTOM, new State(fsm, `${BASE_URL}/bottom`, AnimationClip.WrapMode.Loop))
    this.stateMachines.set(DIRECTION_ENUM.LEFT, new State(fsm, `${BASE_URL}/left`, AnimationClip.WrapMode.Loop))
    this.stateMachines.set(DIRECTION_ENUM.RIGHT, new State(fsm, `${BASE_URL}/right`, AnimationClip.WrapMode.Loop))
  }
}

assets/Scripts/Player/PlayerStateMachine.ts

初始化状态机,为状态机添加一些初始动画

import { _decorator, Animation } from 'cc'
import { FSM_PARAMS_TYPE_ENUM, PARAMS_NAME_ENUM } from '../../Enum'
import State from '../../Base/State'
import { initParamsNumber, initParamsTrigger, StateMachine } from '../../Base/StateMachine'
import { IdleSubStateMachine } from './IdleSubStateMachine'
import { TurnLeftSubStateMachine } from './TurnLeftSubStateMachine'
const { ccclass, property } = _decorator

@ccclass('PlayerStateMachine')
export class PlayerStateMachine extends StateMachine {
  resetTrigger() {
    for (const [_, value] of this.params) {
      if (value.type === FSM_PARAMS_TYPE_ENUM.TRIGGER) {
        value.value = false
      }
    }
  }

  // 初始化参数
  initParams() {
    this.params.set(PARAMS_NAME_ENUM.IDLE, initParamsTrigger)
    this.params.set(PARAMS_NAME_ENUM.TURN_LEFT, initParamsTrigger)
    this.params.set(PARAMS_NAME_ENUM.DIRECTION, initParamsNumber)
  }

  // 初始化状态机
  initStateMachine() {
    // 人物动画,无限播放
    this.stateMachines.set(PARAMS_NAME_ENUM.IDLE, new IdleSubStateMachine(this))
    // 左转动画,播放一次
    this.stateMachines.set(PARAMS_NAME_ENUM.TURN_LEFT, new TurnLeftSubStateMachine(this))
  }

  // 初始化动画
  initAnimationEvent() {
    this.animationComponent.on(Animation.EventType.FINISHED, () => {
      // 执行完动画需要恢复默认idle动画的白名单
      const whiteList = ['turn']
      const name = this.animationComponent.defaultClip.name
      if (whiteList.some(v => name.includes(v))) {
        this.currentState = this.stateMachines.get(PARAMS_NAME_ENUM.IDLE)
      }
    })
  }

  async init() {
    // 添加动画组件
    this.animationComponent = this.addComponent(Animation)
    this.initParams()
    this.initStateMachine()
    this.initAnimationEvent()
    // 确保资源资源加载
    await Promise.all(this.waitingList)
  }
  run() {
    switch (this.currentState) {
      case this.stateMachines.get(PARAMS_NAME_ENUM.TURN_LEFT):
      case this.stateMachines.get(PARAMS_NAME_ENUM.IDLE):
        if (this.params.get(PARAMS_NAME_ENUM.TURN_LEFT).value) {
          this.currentState = this.stateMachines.get(PARAMS_NAME_ENUM.TURN_LEFT)
        } else if (this.params.get(PARAMS_NAME_ENUM.IDLE).value) {
          this.currentState = this.stateMachines.get(PARAMS_NAME_ENUM.IDLE)
        } else {
          // 为了触发子状态机的改变
          this.currentState = this.currentState
        }
        break
      default:
        this.currentState = this.stateMachines.get(PARAMS_NAME_ENUM.IDLE)
    }
  }
}

Base/EntityManager.ts

抽离一些数据,给下面继承用

import { _decorator, Component, Sprite, UITransform } from 'cc'
import { DIRECTION_ENUM, DIRECTION_ORDER_ENUM, ENTITY_STATE_ENUM, PARAMS_NAME_ENUM } from '../Enum'
import { IEntity } from '../Levels'
import { PlayerStateMachine } from '../Scripts/Player/PlayerStateMachine'
import { TILE_HEIGHT, TILE_WIDTH } from '../Scripts/Tile/TileManager'

const { ccclass, property } = _decorator

@ccclass('EntityManager')
export class EntityManager extends Component {
  // 坐标
  x: number = 0
  y: number = 0
  // 状态机
  fsm: PlayerStateMachine
  // 人物当前方向
  private _direction: DIRECTION_ENUM
  // 人物当前状态
  private _state: ENTITY_STATE_ENUM

  get direction() {
    return this._direction
  }
  set direction(newDirection) {
    this._direction = newDirection
    // 设置fsm化动画
    this.fsm.setParams(PARAMS_NAME_ENUM.DIRECTION, DIRECTION_ORDER_ENUM[newDirection])
  }
  get state() {
    return this._state
  }
  set state(newState) {
    this._state = newState
    this.fsm.setParams(this.state, true)
  }

  async init(params: IEntity) {
    // 渲染人物
    const sprite = this.addComponent(Sprite)
    sprite.sizeMode = Sprite.SizeMode.CUSTOM
    const transform = this.getComponent(UITransform)
    transform.setContentSize(TILE_WIDTH * 4, TILE_HEIGHT * 4)

    // this.fsm = this.addComponent(PlayerStateMachine)
    // await this.fsm.init()
    this.x = params.x
    this.y = params.y
    this.direction = params.direction
    // 设置fsm化动画
    this.state = params.state

    // 设置初始方向
    this.direction = DIRECTION_ENUM.TOP
  }
  update() {
    // 更新移动位置,由于人物占4个格子居中需要偏移
    this.node.setPosition(this.x * TILE_WIDTH - TILE_WIDTH * 1.5, -this.y * TILE_HEIGHT + TILE_HEIGHT * 1.5)
  }
}

Scripts/Player/PlayerManager.ts

人物移动,改变状态,触发状态机更新

import { _decorator } from 'cc'
import { CONTROLLER_ENUM, DIRECTION_ENUM, ENTITY_STATE_ENUM, ENTITY_TYPE_ENUM, EVENT_ENUM } from '../../Enum'
import EventManager from '../../Runtime/EventManager'
import { PlayerStateMachine } from './PlayerStateMachine'
import { EntityManager } from '../../Base/EntityManager'

const { ccclass, property } = _decorator

@ccclass('PlayerManager')
export class PlayerManager extends EntityManager {
  // 目标坐标
  targetX: number = 0
  targetY: number = 0
  // 速度
  private readonly sped = 1 / 10

  async init() {
    this.fsm = this.addComponent(PlayerStateMachine)
    await this.fsm.init()

    super.init({
      x: 0,
      y: 0,
      type: ENTITY_TYPE_ENUM.PLAYER,
      direction: DIRECTION_ENUM.TOP, // 设置初始方向
      state: ENTITY_STATE_ENUM.IDLE, // 设置fsm化动画
    })

    EventManager.Instance.on(EVENT_ENUM.PLAY_CTRL, this.move, this)
  }
  update() {
    // 更新人物移动坐标数据
    this.updateXY()
    super.update()
  }

  // 更新xy,让xy无限趋近于targetX targetY
  updateXY() {
    if (this.targetX < this.x) {
      this.x -= this.sped
    } else if (this.targetX > this.x) {
      this.x += this.sped
    }

    if (this.targetY < this.y) {
      this.y -= this.sped
    } else if (this.targetY > this.y) {
      this.y += this.sped
    }

    if (Math.abs(this.targetY - this.y) <= 0.1 && Math.abs(this.targetX - this.x) <= 0.1) {
      this.x = this.targetX
      this.y = this.targetY
    }
  }

  // 人物移动
  move(inputDirection: CONTROLLER_ENUM) {
    switch (inputDirection) {
      case CONTROLLER_ENUM.BOTTOM:
        this.targetY += 1
        break
      case CONTROLLER_ENUM.LEFT:
        this.targetX -= 1
        break
      case CONTROLLER_ENUM.TOP:
        this.targetY -= 1
        break
      case CONTROLLER_ENUM.RIGHT:
        this.targetX += 1
        break
      case CONTROLLER_ENUM.TURN_LEFT:
        // 旋转方向
        switch (this.direction) {
          case DIRECTION_ENUM.TOP:
            this.direction = DIRECTION_ENUM.LEFT
            break
          case DIRECTION_ENUM.LEFT:
            this.direction = DIRECTION_ENUM.BOTTOM
            break
          case DIRECTION_ENUM.BOTTOM:
            this.direction = DIRECTION_ENUM.RIGHT
            break
          case DIRECTION_ENUM.RIGHT:
            this.direction = DIRECTION_ENUM.TOP
            break
        }
        this.state = ENTITY_STATE_ENUM.IDLE
        break
      case CONTROLLER_ENUM.TURN_RIGHT:
        break
    }
  }
}

https://web03-1252477692.cos.ap-guangzhou.myqcloud.com/blog/images/QQ%E6%88%AA%E5%9B%BE20220710225808.png

本节源码地址:

https://gitee.com/yuan30/cramped-room-of-death/tree/day3/

Cocos
  • 作者:零三(联系作者)
  • 最后更新时间:2022-07-10 23:20
  • 版权声明:自由转载-非商用-非衍生-保持署名
  • 转载声明:来源地址 https://web03.cn