状态机--状态机5,关于战斗兵种的多状态

本文探讨了在游戏战斗中如何设计一个支持多状态的角色状态机,包括主状态和子状态的概念。主状态如待机、行走、攻击等互斥,而子状态如中毒、变羊等可叠加。状态机在切换状态时需要考虑子状态对主状态切换的影响,例如某些子状态可能阻止角色执行特定行为。通过这种方式,实现了角色状态的灵活管理和扩展。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在前面一篇《状态机4,关于战斗中负责兵种状态切换的状态机》中实现了一个简单的状态机

在最后抛出了为什么说简单的问题,因为在一个复杂的战斗中,一个角色很少说只有一个状态,

或者说根本不可能,多状态是合理的需求,在之前实现的状态机中,角色同时只能有一个状态,

要么行走状态,要么冰冻状态,从代码逻辑上看,一个新的状态进来当前的状态会被顶替掉,


来看需求:一个角色的攻击的过程中中了一个中毒buff,在一段时间内会持续掉血,这种情况下

角色会同时有两个状态,攻击状态和中毒状态,中毒状态不会顶替攻击状态,在中毒状态下角色

也可以切换到行走状态,冰冻状态等等,那么从以上的需求来看,


我们可以把状态分为两种类型:主状态子状态

主状态:如待机状态,行走状态,攻击状态,冰冻状态等等,它们的特点是相互这间互斥,

角色在同一时刻只能处于其中一种状态,新的主状态进来会顶替当前的主状态

子状态:如中毒状态,变羊状态,隐身状态等等,它们的特点是相互之间独立,且不会顶替

掉主状态,如角色可以同时处于行走状态,中毒状态,隐身状态


经过上面的总结,我们把上一篇文章里提到的状态做下分类,并加入一些新的状态

主状态:待机状态,行走状态,攻击状态,冰冻状态,眩晕状态

子状态:中毒状态,网捕状态,变羊状态


总结完之后,大概说下新的状态机的原理,大部分跟之前的简单状态机一样,不同的地方在于切换

状态的时候,需要做一些额外的逻辑处理,

1.如果新进来的状态是子状态,那么直接新增一个子状态,并直接返回不做其他处理,

子状态可以独立添加和移除

2.如果新进来的状态是主状态,那么先检查一下角色身上的子状态,为什么要做检查,

下面说一下一些情况就能明白,

假如角色现在处于攻击状态,在此状态下中了一个网捕buff,网捕buff的作用是让角色

不能行走,带有网捕状态的角色只能呆在原地做攻击,待机等等行为,

再假如角色现在处于行走状态,在此状态下中了一个变羊的buff,变羊buff的作用是让角色

变成羊的模型,带有变羊状态的角色不能使用技能,不能攻击,其他如行走,冰冻是可以的


通过上面的例子可以总结出来,子状态的存在会影响到主状态的切换,那么我们的状态切换

规则就分三步:

1.如果新进来的状态是子状态,那么直接添加

2.如果新进来的状态是主状态,那么分下面几步:

如果是相同的状态,则直接返回

如果不是相同的状态,那么检查自身的子状态,是否有影响到主状态的切换的子状态

如果没有影响到主状态切换的子状态,那么检查是否有可切换的主状态


下面来看代码:

StateId.lua:状态id,没什么大的区别,只是新加了几个新的状态id,子状态从500开始,做区分

local StateId = {}

StateId.unKnown = -1
StateId.idle = 1 --待机
StateId.walk = 2 --行走
StateId.attack = 3 --攻击
StateId.frozen = 4 --冰冻
StateId.vertigo = 5 --眩晕

StateId.subStateStartId = 500 --子状态开始id
StateId.poisoning = 501 --中毒
StateId.netCatch = 502 --网捕
StateId.sheep = 503 --羊

StateId.name = {}
StateId.name[StateId.idle] = "待机状态"
StateId.name[StateId.walk] = "行走状态"
StateId.name[StateId.attack] = "攻击状态"
StateId.name[StateId.frozen] = "冰冻状态"
StateId.name[StateId.vertigo] = "眩晕状态"
StateId.name[StateId.poisoning] = "中毒状态"
StateId.name[StateId.netCatch] = "网捕状态"
StateId.name[StateId.sheep] = "羊状态"

return StateId


StateRuleConfig.lua:这个跟之前的有些区别,分为可切换的主状态和互斥的子状态

拿一个出来说一下,假设当前角色处于攻击状态(3)和网捕状态(502),要切换掉行走状态(2),

那么新进来的状态是主状态,要先判断是否有影响主状态切换的子状态,行走状态的id是2,那么取出

来的配置是:StateRuleConfig[2] = {majorStateCanToggle = {1, 3, 4, 5}, mutexSubState = {502}}

此时角色身上有网捕状态(502),在互斥配置中的相同的id,那么无法切换到行走状态


StateRuleConfig = {}

--状态id:待机状态(1),行走状态(2),攻击状态(3),冰冻状态(4),眩晕状态(5),中毒(501),网捕(502),羊(503)
--majorStateCanToggle:可切换的主状态
--mutexSubState:互斥子状态,

StateRuleConfig[1] = {majorStateCanToggle = {2, 3, 4, 5}, mutexSubState = {}}
StateRuleConfig[2] = {majorStateCanToggle = {1, 3, 4, 5}, mutexSubState = {502}}
StateRuleConfig[3] = {majorStateCanToggle = {1, 2, 4, 5}, mutexSubState = {503}}
StateRuleConfig[4] = {majorStateCanToggle = {1}, mutexSubState = {}}
StateRuleConfig[5] = {majorStateCanToggle = {1}, mutexSubState = {}}

return StateRuleConfig


StateRule.lua:状态切换规则,跟之前的版本有些不同,新加了互斥子状态的检查

local StateId = require("app.edition6.state.StateId")

local StateRule = class("StateRule")

function StateRule:ctor()

end

function StateRule:bindStateRuleConfig(tStateRuleConfig)
    self.tStateRuleConfig = tStateRuleConfig
end

function StateRule:isCanToggle(iCurrentStateId, iNewStateId, tSubState)
    --是否是相同的主状态
    if self:isSameState(iCurrentStateId, iNewStateId) then return false end
    --角色身上是否有与主状态互斥的子状态
    local bHaveMutexSubState, iMutexSubStateId = self:isHaveMutexSubState(iNewStateId, tSubState)
    --如果有互斥的子状态,将互斥的子状态的id返回,方便调试
    if bHaveMutexSubState then return false, iMutexSubStateId end
    --是否有可切换的主状态
    return self:isCanToggleToNewState(iCurrentStateId, iNewStateId)
end

function StateRule:isSubState(iNewStateId)
    return iNewStateId > StateId.subStateStartId
end

--是否跟当前状态相同
function StateRule:isSameState(iCurrentStateId, iNewStateId)
    return iCurrentStateId == iNewStateId
end

--能否切换到新状态
function StateRule:isCanToggleToNewState(iCurrentStateId, iNewStateId)
    local tCurrentStateRule = self.tStateRuleConfig[iCurrentStateId].majorStateCanToggle
    if tCurrentStateRule == nil then return false end

    for _, iStateId in pairs(tCurrentStateRule) do
        if iStateId == iNewStateId then
            return true
        end
    end
    return false
end

--是否有角色身上带有互斥子状态
function StateRule:isHaveMutexSubState(iNewStateId, tSubState)
    local tMutexSubStateRule = self.tStateRuleConfig[iNewStateId].mutexSubState
    if tMutexSubStateRule == nil then return false end

    if #tMutexSubStateRule == 0 then return false end

    for _, iMutexSubStateId in pairs(tMutexSubStateRule) do
        if self:isHaveSubState(tSubState, iMutexSubStateId) then
            return true, iMutexSubStateId
        end
    end
    return false
end

--是否有指定的子状态
function StateRule:isHaveSubState(tSubState, iSubStateId)
    for _, subState in pairs(tSubState) do
        if subState:getStateId() == iSubStateId then
            return true
        end
    end
    return false
end

return StateRule


上面我们把状态分为主状态和子状态,那么对应的将具体状态做区分

StateType.lua:状态类型,这个比较简单

local StateType = {}

StateType.unKnown = -1 
StateType.major = 1 --主状态
StateType.sub = 2 --子状态

return StateType


StateBase.lua:状态基类,加了两个接口,比较简单,不多说

local StateId = require("app.edition6.state.StateId")
local StateType = require("app.edition6.state.StateType")
local StateBase = class("StateBase")

function StateBase:ctor()
    self:setStateId(StateId.unKnown)
    self:setStateType(StateType.unKnown)
end

function StateBase:bindSoldier(soldier)
    self.soldier = soldier
end

--进入状态
function StateBase:onEnter(parms)
    
end

--退出状态
function StateBase:onExit()

end

function StateBase:update(dt)

end

function StateBase:setStateId(iStateId)
    self.iStateId = iStateId
end

function StateBase:getStateId()
    return self.iStateId
end

function StateBase:setStateType(iStateType)
    self.iStateType = iStateType
end

function StateBase:getStateType()
    return self.iStateType
end

return StateBase


MajorStateBase.lua:主状态基类,继承状态基类,比较简单,这样分也是有点用处的,下一篇再讲

local StateBase = require("app.edition6.state.StateBase")
local StateType = require("app.edition6.state.StateType")

--主状态
local MajorStateBase = class("MajorStateBase", function()
    return StateBase.new()
end)

function MajorStateBase:ctor()
    self:setStateType(StateType.major)
end

return MajorStateBase



SubStateBase.lua:子状态基类

local StateBase = require("app.edition6.state.StateBase")
local StateType = require("app.edition6.state.StateType")

--主状态
local SubStateBase = class("SubStateBase", function()
    return StateBase.new()
end)

function SubStateBase:ctor()
    self:setStateType(StateType.sub)
end

return SubStateBase

StateCreator.lua:状态创建器,只是新加了些状态而已

local StateId = require("app.edition6.state.StateId")

--主状态
local IdleState = require("app.edition6.state.majorState.IdleState")
local WalkState = require("app.edition6.state.majorState.WalkState")
local AttackState = require("app.edition6.state.majorState.AttackState")
local FrozenState = require("app.edition6.state.majorState.FrozenState")
local VertigoState = require("app.edition6.state.majorState.VertigoState")


--子状态
local PoisoningState = require("app.edition6.state.subState.PoisoningState")
local SheepState = require("app.edition6.state.subState.SheepState")
local NetCatchState = require("app.edition6.state.subState.NetCatchState")

local StateCreator = class("StateCreator")

function StateCreator:ctor()

end

function StateCreator:createState(iStateId)
    local state = nil
    if iStateId == StateId.idle then
        state = IdleState.new()
    elseif iStateId == StateId.walk then
        state = WalkState.new()
    elseif iStateId == StateId.attack then
        state = AttackState.new()
    elseif iStateId == StateId.frozen then
        state = FrozenState.new()
    elseif iStateId == StateId.vertigo then
        state = VertigoState.new()
    elseif iStateId == StateId.poisoning then
        state = PoisoningState.new()
    elseif iStateId == StateId.sheep then
        state = SheepState.new()
    elseif iStateId == StateId.netCatch then
        state = NetCatchState.new()
    end
    return state
end

return StateCreator


IdleState.lua:待机状态(主状态),跟之前唯一的区别是继承主状态MajorStateBase,其他的主状态如攻击状态,行走状态类似

local MajorStateBase = require("app.edition6.state.MajorStateBase")
local StateId = require("app.edition6.state.StateId")

local IdleState = class("IdleState", function()
    return MajorStateBase.new()
end)

function IdleState:ctor()
    self:setStateId(StateId.idle)
end

function IdleState:onEnter(parms)
    print("进入待机状态")
end

function IdleState:onExit()
    print("退出待机状态")
end

return IdleState

PoisoningState.lua:中毒状态(子状态),继承SubStateBase,其他的子状态如变羊状态,网捕状态类似

local SubStateBase = require("app.edition6.state.SubStateBase")
local StateId = require("app.edition6.state.StateId")

local PoisoningState = class("PoisoningState", function()
    return SubStateBase.new()
end)

function PoisoningState:ctor()
    self:setStateId(StateId.poisoning)
end

function PoisoningState:onEnter(parms)
    print("进入中毒状态")
end

function PoisoningState:onExit()
    print("退出中毒状态")
end

return PoisoningState


StateMachine.lua:状态机,新增了addSubState()和removeSubState()和insertSubState()接口,removeSubState()

在兵种类中被调用,在toggleState()中做了修改,加入了子状态的逻辑判断

local StateId = require("app.edition6.state.StateId")

local StateMachine = class("StateMachine")

function StateMachine:ctor()
    self.tSubState = {}
end

--绑定角色对象
function StateMachine:bindSoldier(soldier)
    self.soldier = soldier
    self:initDefaultState()
end

--初始化默认状态
function StateMachine:initDefaultState()
    local defaultState = self:createState(StateId.idle)
    self:setCurrentState(defaultState)
    print("初始化默认状态为 ( " .. StateId.name[defaultState:getStateId()] .. " )")
end

--绑定状态切换规则
function StateMachine:bindStateRule(stateRule)
    self.stateRule = stateRule
end

function StateMachine:bindStateCreator(stateCretor)
    self.stateCretor = stateCretor
end

--设置当前状态
function StateMachine:setCurrentState(state)
    self.currentState = state
end

function StateMachine:getCurrentState()
    return self.currentState
end

function StateMachine:getCurrentStateId()
    local iCurrentStateId = StateId.unKnown
    if self.currentState ~= nil then
        iCurrentStateId = self.currentState:getStateId()
    end
    return iCurrentStateId
end

function StateMachine:toggleState(iNewStateId, parms)
    --是否是子状态,如果是,则直接加到子状态列表
    local bSubState = self.stateRule:isSubState(iNewStateId)
    if bSubState then
        self:addSubState(iNewStateId, parms)
    else
        self:toggleMajorState(iNewStateId, parms)
    end
end

--切换主状态
function StateMachine:toggleMajorState(iNewStateId, parms)
    --不能切换,直接返回
    local iCurrentStateId = self:getCurrentStateId()
    local bCanToggleMajorState, iMutexSubStateId = self.stateRule:isCanToggle(iCurrentStateId, iNewStateId, self.tSubState)
    if not bCanToggleMajorState then 
        self.soldier:toggleStateFail(iCurrentStateId, iNewStateId, iMutexSubStateId)
        return 
    end

    local newState = self:createState(iNewStateId)
    --没有对应的状态,直接返回
    if newState == nil then 
        self.soldier:toggleStateFail(iCurrentStateId, iNewStateId)
        return 
    end

    --退出当前状态
    self:getCurrentState():onExit()
    self:setCurrentState(newState)
    --进入新状态
    newState:onEnter(parms)
end

--新增子状态
function StateMachine:addSubState(iNewStateId, parms)
    local subState = self:createState(iNewStateId)
    --没有对应的状态,直接返回
    if subState == nil then 
        self.soldier:addSubStateFail(iNewStateId)
        return 
    end
    subState:onEnter(parms)
    self:insertSubState(subState)
end

function StateMachine:insertSubState(subState)
    table.insert(self.tSubState, subState)
end

function StateMachine:removeSubState(iSubStateId)
    for _, subState in pairs(self.tSubState) do
        if subState:getStateId() == iSubStateId then
            subState:onExit()
            table.removebyvalue(self.tSubState, subState, true)
        end
    end
end

--创建状态
function StateMachine:createState(iNewStateId)
    local newState = self.stateCretor:createState(iNewStateId)
    if newState ~= nil then
        newState:bindSoldier(self.soldier)
    end
    return newState
end

function StateMachine:isState(iStateId)
    return self:getCurrentStateId() == iStateId
end

return StateMachine


Soldier.lua:兵种类,新加了一个removeSubState()接口,传入状态id,委托状态机移除子状态,为什么

有些XXXEnd()里面是调用onIdle(),而有些是removeSubState(),因为在设计状态时我们已经区别出哪些

是主状态,哪些是子状态,而主状态是切换,被顶替的特性,子状态的特点是直接添加或移除,所以调用不同

local StateId = require("app.edition6.state.StateId")
local Soldier = class("Soldier")

function Soldier:ctor()

end

--绑定状态机
function Soldier:bindStateMachine(stateMachine)
    self.stateMachine = stateMachine  
    self.stateMachine:bindSoldier(self)      
end

--待机
function Soldier:onIdle()
    self:toggleState(StateId.idle)
end

--是否是待机状态
function Soldier:isIdle()
    return self:isState(StateId.idle)
end

--行走
function Soldier:onWalk()
    self:toggleState(StateId.walk)
end

--行走结束
function Soldier:onWalkEnd()
    self:onIdle()
end

--是否是行走状态
function Soldier:isWalk()
    return self:isState(StateId.walk)
end

--攻击
function Soldier:onAttack()
    self:toggleState(StateId.attack)
end

--攻击结束
function Soldier:onAttackEnd()
    self:onIdle()
end

--是否是攻击状态
function Soldier:isAttack()
    return self:isState(StateId.attack)
end

--冰冻
function Soldier:onFrozen()
    self:toggleState(StateId.frozen)
end

--冰冻结束
function Soldier:onFrozenEnd()
    self:onIdle()
end

--是否是冰冻状态
function Soldier:isFrozen()
    return self:isState(StateId.frozen)
end

--眩晕
function Soldier:onVertigo()
    self:toggleState(StateId.vertigo)
end

--眩晕结束
function Soldier:onVertigoEnd()
    self:onIdle()
end

--是否是眩晕状态
function Soldier:isVertigo()
    return self:isState(StateId.vertigo)
end

--中毒
function Soldier:onPoisoning()
    self:toggleState(StateId.poisoning)
end

--中毒结束
function Soldier:onPoisoningEnd()
    self:removeSubState(StateId.poisoning)
end

--是否是眩晕状态
function Soldier:isPoisoning()
    return self:isState(StateId.poisoning)
end

--变羊
function Soldier:onSheep()
    self:toggleState(StateId.sheep)
end

--变羊结束
function Soldier:onSheepEnd()
    self:removeSubState(StateId.sheep)
end

--是否是羊状态
function Soldier:isSheep()
    return self:isState(StateId.sheep)
end

--网捕
function Soldier:onNetCatch()
    self:toggleState(StateId.netCatch)
end

--网捕结束
function Soldier:onNetCatchEnd()
    self:removeSubState(StateId.netCatch)
end

--是否是网捕状态
function Soldier:isNetCatch()
    return self:isState(StateId.netCatch)
end

--切换状态
function Soldier:toggleState(iStateId)
    self.stateMachine:toggleState(iStateId)
end

--移除子状态
function Soldier:removeSubState(iStateId)
    self.stateMachine:removeSubState(iStateId)
end

function Soldier:isState(iStateId)
    return self.stateMachine:isState(iStateId)
end

--iMutexSubStateId:互斥的子状态id
function Soldier:toggleStateFail(iCurrentStateId, iNewStateId, iMutexSubStateId)
    if iMutexSubStateId == nil then
        print("无法从状态 ( " .. StateId.name[iCurrentStateId] .. " ) 切换到状态 ( " .. StateId.name[iNewStateId] .. " )")
    else
        print("无法从状态 ( " .. StateId.name[iCurrentStateId] .. " ) 切换到状态 ( " .. StateId.name[iNewStateId] .. " ),有互斥状态 ( " .. StateId.name[iMutexSubStateId] .. " )")
    end
end

function Soldier:addSubStateFail(iSubStateId)
    print("无法新增子状态 ( " .. iSubStateId .. " )")
end

return Soldier

高层模块的调用,跟上一个版本一样,基本没什么改变,好的设计要对外保持调用不变或者少变,将细节的部分封装在内部

function MainScene:registerKeyPad6()
    self:setKeypadEnabled(true)
    self:addNodeEventListener(cc.KEYPAD_EVENT, function(event)
        self:keyPadCallBack6(event)
    end)
end

function MainScene:keyPadCallBack6(event)
    local iKeyCode = event.code
    if iKeyCode == 124 then --A,攻击
        self.soldier:onAttack()
    elseif iKeyCode == 132 then --I,待机
        self.soldier:onIdle()
    elseif iKeyCode == 146 then --W,行走
        self.soldier:onWalk()
    elseif iKeyCode == 129 then --F,冰冻
        self.soldier:onFrozen()
    elseif iKeyCode == 130 then --G,冰冻结束
        self.soldier:onFrozenEnd()
    elseif iKeyCode == 145 then --V,眩晕
        self.soldier:onVertigo()
    elseif iKeyCode == 142 then --S,变羊
        self.soldier:onSheep()
    elseif iKeyCode == 137 then --N,网捕
        self.soldier:onNetCatch()
    elseif iKeyCode == 139 then --P,中毒
        self.soldier:onPoisoning()
    end
end

function MainScene:edition6()
    self:registerKeyPad6()
    local Soldier = require("app.edition6.soldier.Soldier")
    self.soldier = Soldier.new()
    local stateMachine = self:createStateMachine1()
    self.soldier:bindStateMachine(stateMachine)
end

function MainScene:createStateMachine1()
    local StateCreator = require("app.edition6.state.StateCreator")
    local StateMachine = require("app.edition6.stateMachine.StateMachine")
    local StateRule = require("app.edition6.stateRule.StateRule")
    local StateRuleConfig = require("app.edition6.config.StateRuleConfig")

    local stateRule = StateRule.new()
    stateRule:bindStateRuleConfig(StateRuleConfig)

    local stateCreator = StateCreator.new()

    local stateMachine = StateMachine.new()
    stateMachine:bindStateRule(stateRule)
    stateMachine:bindStateCreator(stateCreator)

    return stateMachine
end

下一篇文章会开发demo工程,不用安装环境应该就可以跑起来,到此为止,我们已经

实现一个相对高级一点的状态机,能够实现角色的多个状态,比较高的扩展性,新加一个

状态只需要新加一个具体状态类,继承主状态基类或子状态基类,各个状态的实现是独立的,

状态切换规则也提取出来,可以根据不同的需求绑定不同的规则,这里只能说相对高级一点,

为什么,下一篇再说

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值