-- 主轨道行 和 其中的五条轨道 local STATIC_TRACK_LINE = increment(track_line_top) local BASE_TRACK = increment(static_track_top) local BOLT_CAUGHT_TRACK = increment(static_track_top) local SAFETY_TRACK = increment(static_track_top) -- 待实现 local ADS_TRACK = increment(static_track_top) local MAIN_TRACK = increment(static_track_top) local SPRINT_TRACK = increment(static_track_top)
-- 进入"不空挂"状态 function bolt_caught_states.normal.entry(this, context) -- 因为进入不空挂状态没什么需要做的,因此什么都不做直接转进该状态 this.bolt_caught_states.normal.update(this, context) end
-- 更新"空挂"状态 function bolt_caught_states.bolt_caught.update(this, context) -- 如果检测到子弹数不为 0 了(此时是换弹了),那么手动触发一次转到"不空挂"状态的输入 if (not isNoAmmo(context)) then context:trigger(this.INPUT_BOLT_NORMAL) end end
-- 转出"不空挂"状态 function bolt_caught_states.normal.transition(this, context, input) -- 如果收到了"空挂"的输入,那么直接转到"空挂"状态,"'空挂'的输入"是在上文 update 方法中出现的 if (input == this.INPUT_BOLT_CAUGHT) then return this.bolt_caught_states.bolt_caught end end
输入(input)就是给状态机一个从外部输入的信号。 一些预设的全局信号会在玩家做出特定动作的时候发出,比如换弹时按下 R 的一瞬间会给状态机输入一个 INPUT_RELOAD 的信号。 由于既定的全局信号并不能覆盖所有会遇到的情况,因此输入信号是可以自定义的。 这些输入的信号一般都是由状态的 transition 方法来检测,用于判断切换到另一状态的时机,比如:
-- 转出"不空挂"状态 function bolt_caught_states.normal.transition(this, context, input) -- 如果收到了"空挂"的输入,那么直接转到"空挂"状态,"'空挂'的输入"是在上文 update 方法中出现的 if (input == this.INPUT_BOLT_CAUGHT) then return this.bolt_caught_states.bolt_caught end end
local bolt_caught_states = { -- normal 是不空挂的正常状态 normal = {}, -- bolt_caught 是空挂时的状态 bolt_caught = {}}
枪机状态初始化
可以看到在状态定义里有两个子状态,因为枪机状态分为“空挂”和“不空挂”这两种情况,因此在定义时分出 normal 和 bolt_caught 两个子状态, 在初始化状态里也要注意将状态的初始子状态设置为不空挂(normal)。 而枪机状态的状态方法就比基态要复杂一些了,这里先来看子状态设置为“不空挂”(normal)时的状态方法: 首先是进入(entry)“不空挂”状态需要进行的操作,因为没有任何需要进行的变化,因此只需要触发 update 方法等待进一步的操作即可。
-- 进入"不空挂"状态 function bolt_caught_states.normal.entry(this, context) -- 因为进入不空挂状态没什么需要做的,因此什么都不做直接转进该状态 this.bolt_caught_states.normal.update(this, context) end
-- 更新"不空挂"状态 function bolt_caught_states.normal.update(this, context) -- 如果弹药数量是 0 了,那么立刻手动触发一次转到"空挂"状态的输入 if (isNoAmmo(context)) then context:trigger(this.INPUT_BOLT_CAUGHT) end end
-- 转出"不空挂"状态 function bolt_caught_states.normal.transition(this, context, input) -- 如果收到了"空挂"的输入,那么直接转到"空挂"状态,"'空挂'的输入"是在上文 update 方法中出现的 if (input == this.INPUT_BOLT_CAUGHT) then return this.bolt_caught_states.bolt_caught end end
接下来是“空挂”(bolt_caught)子状态的状态方法:
-- 进入"空挂"状态 function bolt_caught_states.bolt_caught.entry(this, context) -- 进入空挂时在主轨道行的空挂轨道播放空挂的动画 context:runAnimation("static_bolt_caught", context:getTrack(STATIC_TRACK_LINE, BOLT_CAUGHT_TRACK), false, LOOP, 0) end -- 更新"空挂"状态 function bolt_caught_states.bolt_caught.update(this, context) -- 如果检测到子弹数不为 0 了(此时是换弹了),那么手动触发一次转到"不空挂"状态的输入 if (not isNoAmmo(context)) then context:trigger(this.INPUT_BOLT_NORMAL) end end -- 转出"空挂"状态 function bolt_caught_states.bolt_caught.transition(this, context, input) -- 如果收到了来自上文 update 方法的输入,则转到"不空挂"状态 if (input == this.INPUT_BOLT_NORMAL) then -- 由于并没有一个"不空挂"的动画,因此必须在这里把空挂动画停止了才能转到"不空挂"状态,否则你会在换完弹之后发现依旧处于空挂状态 context:stopAnimation(context:getTrack(STATIC_TRACK_LINE, BOLT_CAUGHT_TRACK)) return this.bolt_caught_states.normal end end
-- 播放丢枪动画的方法 local function runPutAwayAnimation(context) local put_away_time = context:getPutAwayTime() -- 此处获取的轨道是位于主轨道行上的主轨道 local track = context:getTrack(STATIC_TRACK_LINE, MAIN_TRACK) -- 播放 put_away 动画,并且将其过渡时长设为从上下文里传入的 put_away_time * 0.75 context:runAnimation("put_away", track, false, PLAY_ONCE_HOLD, put_away_time * 0.75) -- 设定动画进度为最后一帧 context:setAnimationProgress(track, 1, true) -- 将动画进度向前拨动 {put_away_time} context:adjustAnimationProgress(track, -put_away_time, false) end
然后是换弹相关的输入:
-- 玩家拿着枪按下 R (或者别的什么自己绑定的换弹键)时会自动输入换弹信号 if (input == INPUT_RELOAD) then runReloadAnimation(context) -- 换弹动画播放完后返回闲置态(也就是返回自己) return this.main_track_states.idle end
玩家按 R 的时候会有程序控制的全局输入,通过检测这个换弹信号去播放换弹动画,然后要回到闲置(手动挡车的挡位回归空挡)。 这个播放换弹动画的方法也是个lua函数,那里面有检测战术换弹和空仓换弹的相关内容,和丢枪动画的函数一样在整个状态机的开头部分。
-- 玩家在射击时会自动输入 shoot 信号 if (input == INPUT_SHOOT) then context:popShellFrom(0) -- 默认射击抛壳 -- 返回闲置态(也就是返回自己),这里不播放射击动画是因为射击动画应该在 gun_kick 状态里播 return this.main_track_states.idle end
-- 玩家按下检视键后会输入检视信号 if (input == INPUT_INSPECT and context:getAimingProgress() < 1) then runInspectAnimation(context) -- 检视需要转到检视态,因为检视过程中屏幕中央准星是隐藏的,因此需要一个检视态来调控准星 return this.main_track_states.inspect end
-- 进入检视态 function main_track_states.inspect.entry(this, context) -- 检视是需要隐藏屏幕中央准星 context:setShouldHideCrossHair(true) end -- 退出检视态 function main_track_states.inspect.exit(this, context) context:stopAnimation(context:getTrack(STATIC_TRACK_LINE, MAIN_TRACK)) -- 退出后恢复屏幕中央准星 context:setShouldHideCrossHair(false) end -- 更新检视态 function main_track_states.inspect.update(this, context) -- 当检测到动画停止了(播完了)时手动触发一次退出信号 if (context:isStopped(context:getTrack(STATIC_TRACK_LINE, MAIN_TRACK))) then context:trigger(this.INPUT_INSPECT_RETREAT) end end -- 转出检视态 function main_track_states.inspect.transition(this, context, input) -- 当收到来自 update 的退出信号时返回到闲置态,此时不需要停止动画是因为在 update 里是动画已经停止了才发出的退出信号 if (input == this.INPUT_INSPECT_RETREAT) then return this.main_track_states.idle end -- 特殊地,射击与瞄准应当打断检视,当检测到射击输入或瞄准进度不为0时应该直接停止动画并返回闲置态 if (input == INPUT_SHOOT or context:getAimingProgress() > 0) then context:stopAnimation(context:getTrack(STATIC_TRACK_LINE, MAIN_TRACK)) return this.main_track_states.idle end return this.main_track_states.idle.transition(this, context, input) end -- 结束主状态部分
function gun_kick_state.transition(this, context, input) -- 玩家按下开火键时需要在射击轨道行里寻找空闲轨道去播放射击动画(如果没有空闲会分配新的),需要注意的是射击动画要向下混合 if (input == INPUT_SHOOT) then local track = context:findIdleTrack(GUN_KICK_TRACK_LINE, false) -- 这里是混合动画,一般是可叠加的 gun kick context:runAnimation("shoot", track, true, PLAY_ONCE_STOP, 0) end return nil end
在这三个子状态里,idle 对应的是拿着枪不移动,run 是疾跑,walk 是行走,可以看到 walk 里的状态参数还细分了行走方向。 如果你拆过默认枪包的默认动画就会发现,默认动画里是有行走和奔跑动画的,这些动画的内容就是枪身的抖动和摇晃。
老规矩我们还是来逐句拆解:
-- 更新静止态 function movement_track_states.idle.update(this, context) -- 此处获取的是混合轨道行的移动轨道 local track = context:getTrack(BLENDING_TRACK_LINE, MOVEMENT_TRACK) -- 如果轨道空闲,则播放 idle 动画 -- 注意此处没有写成是在 entry 播放 idle 动画是因为要实时检测轨道是否空闲 if (context:isStopped(track) or context:isHolding(track)) then context:runAnimation("idle", track, true, LOOP, 0) end end -- 转出静止态 function movement_track_states.idle.transition(this, context, input) -- 如果玩家在奔跑则转去奔跑态 if (input == INPUT_RUN) then if (context:isStopped(context:getTrack(STATIC_TRACK_LINE, MAIN_TRACK))) then return this.movement_track_states.run else return this.movement_track_states.walk end -- 如果玩家在行走则转去行走态 elseif (input == INPUT_WALK) then return this.movement_track_states.walk end end
-- 更新奔跑态 function movement_track_states.run.update(this, context) local track = context:getTrack(BLENDING_TRACK_LINE, MOVEMENT_TRACK) local state = this.movement_track_states.run; -- 等待 run_start 结束,然后循环播放 run ,此处的判断准则是轨道是否挂起,也就是为什么 entry 里播放动画要选 PLAY_ONCE_HOLD 模式 if (context:isHolding(track)) then context:runAnimation("run", track, true, LOOP, 0.2) -- 检测是否奔跑的标志位 0 state.mode = 0 context:anchorWalkDist() -- 打 walkDist 锚点,确保 run 动画的起点一致 end if (state.mode ~= -1) then if (not context:isOnGround()) then -- 如果玩家在空中,则播放 run_hold 动画以稳定枪身 if (state.mode ~= 1) then state.mode = 1 context:runAnimation("run_hold", track, true, LOOP, 0.6) end else -- 如果玩家在地面,则切换回 run 动画 if (state.mode ~= 0) then state.mode = 0 context:runAnimation("run", track, true, LOOP, 0.2) end -- 根据 walkDist 设置 run 动画的进度 context:setAnimationProgress(track, (context:getWalkDist() % 2.0) / 2.0, true) end end end
这一段是奔跑的 update 方法,这里有一个判断是玩家是否在空中,对应的是玩家在疾跑时跳跃,最后一句设定 run 动画的进度是为了防止 run 动画在跳起和落地的时候发生模型瞬移。
-- 转出奔跑态 function movement_track_states.run.transition(this, context, input) -- 收到闲置输入则转去闲置态 if (input == INPUT_IDLE) then return this.movement_track_states.idle -- 收到行走输入则转去行走态 elseif (input == INPUT_WALK or not context:isStopped(context:getTrack(STATIC_TRACK_LINE, MAIN_TRACK))) then return this.movement_track_states.walk end end
-- 主轨道行 和 其中的五条轨道 local STATIC_TRACK_LINE = increment(track_line_top) local BASE_TRACK = increment(static_track_top) local BOLT_CAUGHT_TRACK = increment(static_track_top) local SAFETY_TRACK = increment(static_track_top) -- 待实现 local ADS_TRACK = increment(static_track_top) local MAIN_TRACK = increment(static_track_top) local SPRINT_TRACK = increment(static_track_top) -- 开火的轨道行 local GUN_KICK_TRACK_LINE = increment(track_line_top) -- 混合轨道行 和 其中的两条轨道,用于叠加动画,如跑步走路跳跃, LOOP_TRACK 只有定义却尚未启用,因此作用尚不得知 local BLENDING_TRACK_LINE = increment(track_line_top) local MOVEMENT_TRACK = increment(blending_track_top) local SLIDE_TRACK = increment(blending_track_top) local OVER_HEAT_TRACK = increment(blending_track_top) local OVER_HEATING_TRACK = increment(blending_track_top) local LOOP_TRACK = increment(blending_track_top)
function gun_kick_state.transition(this, context, input) -- 玩家按下开火键时需要在射击轨道行里寻找空闲轨道去播放射击动画(如果没有空闲会分配新的),需要注意的是射击动画要向下混合 if (input == INPUT_SHOOT) then local track = context:findIdleTrack(GUN_KICK_TRACK_LINE, false) -- 这里是混合动画,一般是可叠加的 gun kick context:runAnimation("shoot", track, true, PLAY_ONCE_STOP, 0) end return nil end
还是以上一节的余弹显示为例,当我确定了要改造空挂状态方法后,我要在 normal 子状态时显示余弹,由于此时的默认状态机的空挂轨道是没有动画在播放的,因此我需要在这里播放动画。 在 normal 的 entry 方法里填上播放余弹显示动画,在 update 里根据余下的弹药量动态调整动画的进度,在进入空挂时启动空挂。(是的我知道 RD704 其实没有空挂,但是在这里有一个空挂会比较方便)
在列出了以上内容后我就能直接开始改了。
-- 更新"不空挂"状态 function bolt_caught_states.normal.update(this, context) -- 如果弹药数量是0了, 那么立刻手动触发一次转到"空挂"状态的输入 if (inNoAmmo(context)) then context:stopAnimation(context:getTrack(STATIC_TRACK_LINE, BOLT_CAUGHT_TRACK)) context:trigger(this.INPUT_BOLT_CAUGHT) else local a = context:getAmmoCount() context:setAnimationProgress(context:getTrack(STATIC_TRACK_LINE, BOLT_CAUGHT_TRACK), 0.1+(03-a)*0.25, false) end end -- 进入"不空挂"状态 function bolt_caught_states.normal.entry(this, context) context:runAnimation("static_ammo_display", context:getTrack(STATIC_TRACK_LINE, BOLT_CAUGHT_TRACK), false, PLAY_ONCE_STOP, 0) -- 因为进入不空挂状态没什么需要做的, 因此什么都不做直接转进改状态 this.bolt_caught_states.normal.update(tihs, context) end
if (input == INPUT_FIRE_SELECT) then context:runAnimation("fanning_1", context:getTrack(STATIC_TRACK_LINE, MAIN_TRACK), false, PLAY_ONCE_STOP, 0.2) return main_track_states.idle end