熔炉密林动作标签系统

状态标签系统详解 (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 行为逻辑:

  1. 攻击前检查玩家是否在 busy 状态。
  2. 等待玩家当前动作结束。
  3. 选择最佳攻击时机(确信命中)。

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,
})

五、标签系统的设计

  1. 明确性 (Clarity)

    • 每个标签都有明确的语义。
    • tags = { "busy", "attack" } (清晰:正在攻击,不可打断)
    • tags = { "somewhat_busy" } (模糊:什么程度?)
  2. 可组合性 (Composability)
    标签可以自由组合以描述复杂状态:

    tags = { 
        "attack",             -- 攻击中
        "busy",               -- 忙碌
        "nointerrupt",        -- 不可打断
        "abouttoattack",      -- 即将攻击
        "canmovewhilebusy"    -- 忙碌时可移动
    }
    
  3. 动态性 (Dynamism)
    标签可以在运行时动态添加/移除,实现精细的手感控制(如 Just Frame 判定)。

  4. 检查效率 (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:明确动作硬直与取消窗口。
  • 动态标签管理:实现运行时灵活的手感调整。
posted @ 2026-02-11 14:10  Lycra776  阅读(33)  评论(0)    收藏  举报