熔炉密林动作标签系统
状态标签系统详解 (State Tag System)
一、状态标签系统概述
1. 核心概念
状态标签是状态机系统中用于控制行为和可打断性的标记系统。
从 stategraph.lua 的源码结构可以看出其定义方式:
State = Class(function(self, args)
self.name = args.name -- 状态名称
self.onenter = args.onenter -- 进入回调
self.onexit = args.onexit -- 退出回调
self.onupdate = args.onupdate -- 更新回调
self.ontimeout = args.ontimeout -- 超时回调
self.tags = args.tags or {} -- 状态标签 ← 核心!
self.events = {} -- 事件列表
self.timeline = args.timeline -- 时间轴
end)
2. 标签的作用
标签系统主要解决以下关键逻辑问题:
- ✅ 什么时候可以打断动作?
- ✅ 什么时候可以接受新输入?
- ✅ 当前角色在做什么?
- ✅ 优先级如何判断?
二、核心状态标签详解
1. busy - 忙碌标签(最重要)
含义: 角色正在执行重要动作,不应被常规操作打断。
-- 攻击状态示例 [sg_bandiball.lua:77]
State({
name = "pierce",
tags = { "attack", "busy" }, -- 攻击中,不能打断
onenter = function(inst, target)
inst.AnimState:PlayAnimation("pierce_elite")
inst.Physics:StartPassingThroughObjects()
end,
})
使用场景:
-- 检查是否可以执行新动作
if inst.sg:HasStateTag("busy") then
return -- 忙碌中,不接受新输入
end
-- 转换到新状态
inst.sg:GoToState("new_state")
| 何时添加 Busy 标签 | 何时不添加 Busy 标签 |
|---|---|
| ✅ 攻击动作 | ❌ 移动状态 |
| ✅ 受击/硬直 | ❌ 空闲状态 |
| ✅ 交互动作(开门、拾取) | ❌ 可取消的蓄力 |
| ✅ 播放过场动画 |
2. attack - 攻击标签
含义: 正在执行攻击动作。
tags = { "attack", "busy" } -- 攻击通常伴随着忙碌状态
作用:
- 让 AI 知道角色正在攻击。
- 触发攻击追踪逻辑。
- 用于连招系统的判断。
3. moving - 移动标签
含义: 正在移动中。
-- [sg_bandicoot.lua:827]
State({
name = "walk_to_run",
tags = { "moving", "running", "busy" }, -- 移动中、跑步中、忙碌
})
作用:
-- 检查是否在移动
if inst.sg:HasStateTag("moving") then
-- 处理移动逻辑
end
-- 移动时允许转向
if inst.sg:HasStateTag("moving") then
inst.Transform:SetRotation(new_direction)
end
4. interruptible / nointerrupt - 可打断控制
含义: 明确控制动作是否可以被强制打断。
-- 不可打断的冲锋 [sg_antleer.lua:136]
State({
name = "charge",
tags = { "attack", "busy", "nointerrupt" }, -- 完全不可打断
onenter = function(inst)
inst.sg:SetTimeout(2) -- 2秒后自动结束
end,
})
优先级逻辑:
nointerrupt>busy>interruptible> (无标签)
5. canmovewhilebusy - 忙碌时可移动
含义: 即使在 busy 状态下,角色仍被允许进行移动操作。
-- 控制逻辑示例 [sg_common.lua:81-82]
if inst.sg:HasStateTag("busy") and not inst.sg:HasStateTag("canmovewhilebusy") then
return -- 忙碌且不可移动,拒绝输入
end
使用场景:
- 某些技能施放时可以缓慢移动。
- 装弹/换弹时的移动。
- 特殊状态下的机动性调整。
6. dodging - 闪避标签
含义: 正在执行闪避或翻滚动作。
-- 典型闪避状态
tags = {
"dodging", -- 闪避中
"busy", -- 忙碌
"nointerrupt" -- 不可打断
}
-- 完美闪避窗口
tags = {
"dodging",
"perfect_dodge_window" -- 可触发完美闪避
}
三、标签系统的实际应用
1. 输入处理流程
-- [sg_bandicoot.lua:663]
EventHandler("dodge", function(inst, dir)
-- 检查是否可以闪避
if not (inst.sg:HasStateTag("busy") or -- 忙碌中?
inst.components.timer:HasTimer("dodge_cd")) -- 冷却中?
then
inst.sg:GoToState("dodge") -- 执行闪避
end
end)
流程图解:
graph TD
A[玩家按下闪避键] --> B{检查 HasStateTag 'busy'?}
B -- 是 --> C[拒绝输入]
B -- 否 --> D{检查冷却时间?}
D -- 冷却中 --> C
D -- 可用 --> E[GoToState 'dodge']
2. AI 决策系统
-- [bosscoroutine.lua:351]
function BossCoroutine:WaitForNotBusy()
while self.inst.sg:HasStateTag("busy") do
if (self.phasechanged or self:CheckInterrupt()) then
break
end
coroutine.yield() -- 等待状态改变
end
end
AI 行为逻辑:
- 攻击前检查玩家是否在
busy状态。 - 等待玩家当前动作结束。
- 选择最佳攻击时机(确信命中)。
3. 移动控制
-- [behaviors\bandicoot_chaseandattack.lua:21-30]
function BandicootChaseAndAttack:TryRunDirection(dir, candodge)
-- 检查是否在移动
if not self.inst.sg:HasStateTag("moving") then
-- 不在移动,可以闪避
if candodge then
self.inst:PushEvent("dodge", dir)
end
end
-- 检查是否可以移动
if not self.inst.sg:HasStateTag("busy") then
self.inst.components.locomotor:RunInDirection(dir)
end
end
四、完整的状态生命周期示例
以下是一个典型的攻击状态,展示了标签如何在时间轴(Timeline)中动态变化:
State({
name = "light_attack",
tags = { "attack", "busy", "interruptible" }, -- 初始:攻击中,忙碌,但前摇可被完美闪避打断
onenter = function(inst)
-- 1. 进入状态时添加标签
inst.sg:AddStateTag("abouttoattack") -- 标记为即将攻击
inst.AnimState:PlayAnimation("attack")
inst.components.combat:StartAttack()
end,
timeline = {
-- 第 5 帧:前摇结束,准备判定
FrameEvent(5, function(inst)
inst.sg:RemoveStateTag("interruptible") -- 移除可打断标签
inst.sg:AddStateTag("nointerrupt") -- 此时动作不可取消
end),
-- 第 10 帧:造成伤害
FrameEvent(10, function(inst)
inst.components.combat:DoAttack()
inst.sg:AddStateTag("canmovewhilebusy") -- 进入后摇,允许移动取消
end),
-- 第 15 帧:开放完全取消窗口
FrameEvent(15, function(inst)
inst.sg:RemoveStateTag("nointerrupt") -- 重新变为可打断
end),
},
events = {
-- 动画播放结束
EventHandler("animover", function(inst)
inst.sg:GoToState("idle")
end),
},
onexit = function(inst)
-- 2. 退出状态时清理标签(通常状态机自动处理 tags 列表,但动态添加的需注意)
inst.sg:RemoveStateTag("abouttoattack")
inst.sg:RemoveStateTag("canmovewhilebusy")
end,
})
五、标签系统的设计
-
明确性 (Clarity)
- 每个标签都有明确的语义。
- ✅
tags = { "busy", "attack" }(清晰:正在攻击,不可打断) - ❌
tags = { "somewhat_busy" }(模糊:什么程度?)
-
可组合性 (Composability)
标签可以自由组合以描述复杂状态:tags = { "attack", -- 攻击中 "busy", -- 忙碌 "nointerrupt", -- 不可打断 "abouttoattack", -- 即将攻击 "canmovewhilebusy" -- 忙碌时可移动 } -
动态性 (Dynamism)
标签可以在运行时动态添加/移除,实现精细的手感控制(如 Just Frame 判定)。 -
检查效率 (Efficiency)
通常使用哈希表实现,保证HasStateTag为 O(1) 复杂度。
六、在 Unity (C#) 中实现类似系统
using System;
using System.Collections.Generic;
public class StateTagSystem
{
private HashSet<string> currentTags = new HashSet<string>();
// 检查标签 O(1)
public bool HasStateTag(string tag)
{
return currentTags.Contains(tag);
}
// 检查多个标签(AND)
public bool HasAllTags(params string[] tags)
{
foreach (var tag in tags)
{
if (!currentTags.Contains(tag)) return false;
}
return true;
}
// 检查多个标签(OR)
public bool HasAnyTag(params string[] tags)
{
foreach (var tag in tags)
{
if (currentTags.Contains(tag)) return true;
}
return false;
}
// 添加标签
public void AddStateTag(string tag)
{
currentTags.Add(tag);
OnTagChanged?.Invoke(tag, true);
}
// 移除标签
public void RemoveStateTag(string tag)
{
currentTags.Remove(tag);
OnTagChanged?.Invoke(tag, false);
}
// 清空所有标签
public void ClearTags()
{
currentTags.Clear();
}
public event Action<string, bool> OnTagChanged;
}
// 使用示例
public class PlayerState : MonoBehaviour
{
private StateTagSystem tagSystem = new StateTagSystem();
void Update()
{
// 检查是否可以接受输入
if (tagSystem.HasStateTag("busy") &&
!tagSystem.HasStateTag("canmovewhilebusy"))
{
return; // 忙碌且不可移动,拒绝输入
}
// 处理输入
if (Input.GetKeyDown(KeyCode.Space))
{
if (!tagSystem.HasAnyTag("busy", "dodging"))
{
StartDodge();
}
}
}
}
七、标签系统的调试技巧
-- 调试命令:查看当前状态标签
function c_showtags()
local inst = ConsoleCommandPlayer()
if inst and inst.sg then
print("=== Current State Tags ===")
print("State:", inst.sg:GetCurrentStateName())
for tag, _ in pairs(inst.sg.tags) do
print(" -", tag)
end
end
end
-- 可视化调试 (在角色头顶绘制标签)
function d_drawtags()
local inst = GetDebugEntity()
if not inst or not inst.sg then return end
local y = 0
for tag, _ in pairs(inst.sg.tags) do
TheDebugRenderer:DrawText(
inst:GetPosition() + Vector3(0, y + 2, 0),
tag,
WEBCOLORS.YELLOW
)
y = y + 0.5
end
end
总结
Rotwood 的状态标签系统是其动作流畅性的核心保障:
busy:控制动作的根本优先级。attack:标记并广播攻击行为。moving:解耦移动与动作逻辑。interruptible/nointerrupt:明确动作硬直与取消窗口。- 动态标签管理:实现运行时灵活的手感调整。

浙公网安备 33010602011771号