在 Roblox 仿 TSBThe Strongest Battlegrounds玩法的格斗体验里自制英雄并不是简单换一个模型就能完成。真正决定一个英雄是否“能玩”的是常规状态下的行为表现待机、移动、跳跃、下落、冲刺、基础连击这些动作如果不能被一个清晰的状态机管住后续加入技能、绝招、受击和倒地状态时脚本就会迅速失控。本文以自制英雄“机动队·刀锋”的常规状态为主线从角色属性、状态定义、动画切换、手感调优到运行验证逐步完成一个最小可运行的格斗角色原型。最后会给出常见问题排查表和生产发布前必须补的工程措施。1. 为什么“自制英雄”要先解决常规状态1.1 常规状态到底包含哪些行为在很多 TSB 类格斗游戏中“常规状态”指的是角色没有进入技能释放、绝招演出、受击硬直或倒地恢复时由玩家基础操作直接驱动的行为集合。典型行为包括待机角色静止时的默认表现。移动前后左右移动通常搭配跑步动画。跳跃角色离开地面后向上运动阶段的行为。下落角色从跳跃最高点下落直到落地。冲刺短时间加速的位移技巧用于追击或拉开距离。基础攻击普通连击通常由多次攻击按钮触发形成三段连招。这些行为看似简单但如果直接把它们写成“键盘按下就播放动画”的脚本很快会遇到问题。比如角色在空中时按攻击是不是应该打出空中攻击冲刺过程中按攻击是打断冲刺还是等冲刺结束三段连击每一段的有效窗口是多久这些问题都属于常规状态管理的范围。1.2 状态机能避免哪些开发问题引入状态机的核心目的是让同一时刻的角色行为只有一个“唯一答案”。在 Roblox 的 Luau 脚本中玩家输入事件、服务端属性变化、动画控制器回调可能同时发生。如果没有状态机两个脚本会互相覆盖动画、重置位移参数最终表现为角色抖动、动画错乱或技能逻辑重复执行。状态机把角色的行为抽象成一组状态并规定状态之间的转换条件。例如只有处于待机和移动状态时才允许按攻击键进入攻击状态。攻击状态播放过程中再次按攻击键如果处于连击窗口内则进入下一段攻击否则忽略本次输入。角色离开地面后强制从任何地面状态转入跳跃状态。冲刺状态优先于移动和待机但冲刺结束后回到由移动输入决定的状态。这样每个行为都有一块独立逻辑不用在一个输入回调里判断“我现在到底在干什么”。这种抽象对后续扩展非常重要。比如“刀锋”这个英雄如果后续加入瞬闪技能技能状态和常规状态之间只需要再定义一组转换规则不需要重写移动逻辑。1.3 刀锋英雄的常规状态版本规划在动手写代码之前先给“机动队·刀锋”做一个小版本规划。第一版不做技能、不做绝招只做四个核心能力稳定的三段基础连击、可用的冲刺位移、平滑的移动动画、完整的空中状态切换。这是比较稳妥的落地顺序。第一版先让角色“动起来”再让角色“打起来”最后再考虑“手感”。本文的示例代码按 Roblox Studio 的常用工程结构组织服务端脚本负责角色属性初始化和最终伤害校验客户端脚本负责输入采集、状态切换和动画表现。示例中的动画 ID 和数值都是占位内容进入真实项目时要替换成你自己上传到 Roblox 平台的角色资产。2. 环境准备与项目结构先让 Roblox Studio 空模板跑通2.1 前置环境要求开发 Roblox 游戏不需要额外安装 SDK核心工具是 Roblox Studio。建议使用支持 Luau 语法补全的版本方便在脚本编辑器里直接看到函数签名和类型问题。环境项要求说明Roblox Studio当前稳定版本使用 Baseplate 模板新建体验脚本语言LuauRoblox 官方脚本语言对 Lua 5.1 做了扩展角色类型R15建议所有英雄统一使用 R15动画兼容性更好动画资源自行上传的 Animation 资产代码示例里使用占位 ID需替换权限模式默认权限即可单机测试阶段不需要开放插件权限如果是从零开始直接新建一个 Basic Platform 或 Baseplate 模板不需要第三方插件。Roblox 内置的动画编辑器、地形工具只在前置阶段有用本节不依赖它们。2.2 项目目录与服务分工为“刀锋”设计一个最小目录结构。为了不把脚本堆成一团把状态类型、状态管理器、动画控制器拆成 ModuleScript放在 ReplicatedStorage 下让客户端和服务端都能按需加载。建议结构如下StarterPlayerScripts/ PlayerCharacterControl.client.lua -- 玩家输入和状态驱动 StarterPlayer/ StarterCharacterScripts/ CharacterSetup.client.lua -- 角色属性初始化可选 ServerScriptService/ CharacterSpawnServer.lua -- 服务端属性设置和安全校验 ReplicatedStorage/ HeroKit/ StateType.lua -- 状态枚举 StateManager.lua -- 通用状态机 AnimationController.lua -- 动画加载和切换 HeroConfig.lua -- 刀锋的数值配置在这个结构里ReplicatedStorage 中的 HeroKit 是共用模块。客户端脚本从该目录加载状态机和动画控制器服务端脚本引用 HeroConfig 做属性统一。这样同一个英雄的所有数值都集中在一个配置文件中。注意StarterCharacterScripts 下的 LocalScript 会随角色生成被复制到每个玩家角色上不要在脚本顶层直接创建全局监听器否则角色重生后会出现重复绑定。2.3 最小项目检查点完成空模板搭建后先不要写英雄逻辑先确认几个基础行为按下 Play 后角色以 R15 形态生成。角色可以正常移动、跳跃不会掉出地图。打开 Studio 的 Output 窗口没有任何语法错误。在地方放置一个 Part并用 Explorer 确认 ReplicatedStorage 下的 ModuleScript 可以被访问。如果这些基础能力没问题再进入角色搭建环节。否则后续所有脚本问题都会叠加在环境问题上排查成本会高很多。3. 搭建“刀锋”基础角色R15 与 Humanoid 属性3.1 角色外观与装配结构“刀锋”的定位是一名近战特勤型角色武器是固定在右手的短刃。第一版常规状态不需要做武器碰撞只需要让短刃作为装饰附件跟随右手运动后续再做武器伤害判定。在使用 Roblox Studio 编辑角色时推荐直接复制一个默认 R15 角色然后替换外观。这样 HumanoidRootPart、Humanoid、Animator、Motor6D 都已经正确装配不会因为手动连接关节而出错。在 Explorer 中需要重点关注这几个节点Character/ HumanoidRootPart -- 角色的根节点移动和旋转都作用在这里 Humanoid -- 控制移动、跳跃、血量等基础能力 Animator -- 负责播放所有动画轨道 RightHand/ BladeAttachment -- 短刃模型的装配点新建一个BladeHandle模型给它设置CanCollide false、Anchored false然后把模型放在 RightHand 下。这样短刃会跟随右手的动作移动并且不会产生物理碰撞避免在普通移动时推开其他玩家。3.2 Humanoid 关键属性速查Humanoid 是 Roblox 角色控制的核心组件。“刀锋”的常规状态手感很大程度取决于这些属性怎么配。属性示例值作用调低影响调高影响WalkSpeed18地面移动速度角色笨重角色更难控制连招容易冲过头JumpPower55跳跃初速度跳跃高度不够空战周期变长HipHeight2角色脚底离地面中心的高度脚可能陷入地面角色看起来悬浮MaxSlopeAngle60最大爬坡角度小坡上不了会爬上很陡的坡影响走位判断UseJumpPowertrue是否启用 JumpPower不启用时使用跳跃高度单位需要配合 JumpHeight 使用这里的示例值接近 Roblox 默认 R15 人力角色的水平。刀锋作为近战角色建议 WalkSpeed 略低于默认的 16达到 18 到 20给普通玩家一点速度优势但不要超过 22。超过 22 后玩家在常规状态下追击手感确实变快但连段瞄准和碰撞避让的难度也会迅速上升。3.3 初始配置脚本在 ServerScriptService 下新建一个 ServerScript命名为CharacterSpawnServer.lua。local Players game:GetService(Players) local ReplicatedStorage game:GetService(ReplicatedStorage) local HeroConfig require(ReplicatedStorage:WaitForChild(HeroKit):WaitForChild(HeroConfig)) local function setupCharacter(character) local humanoid character:WaitForChild(Humanoid, 5) if not humanoid then return end humanoid.WalkSpeed HeroConfig.WalkSpeed humanoid.JumpPower HeroConfig.JumpPower humanoid.HipHeight HeroConfig.HipHeight humanoid.MaxSlopeAngle HeroConfig.MaxSlopeAngle humanoid.UseJumpPower true local rootPart character:WaitForChild(HumanoidRootPart) rootPart.AssemblyLinearVelocity Vector3.zero end local function onPlayerAdded(player) player.CharacterAdded:Connect(setupCharacter) if player.Character then setupCharacter(player.Character) end end Players.PlayerAdded:Connect(onPlayerAdded) for _, player in ipairs(Players:GetPlayers()) do onPlayerAdded(player) end这段脚本放在服务端的作用是保证属性统一。不要把这些数值写在客户端 LocalScript 里因为客户端只对自己可见的 Humanoid 属性修改可能被服务端覆盖而且攻击判断、移动速度这类数值在正式发布前必须受服务端约束。HeroConfig 示例local HeroConfig { WalkSpeed 18, JumpPower 55, HipHeight 2, MaxSlopeAngle 60, DashSpeed 34, DashDuration 0.25, DashCooldown 1.5, AttackComboWindow 0.6, AttackDamage {10, 12, 18}, AnimationIds { Idle rbxassetid://你的待机动画ID, Walk rbxassetid://你的跑步动画ID, Jump rbxassetid://你的跳跃动画ID, Fall rbxassetid://你的下落动画ID, Dash rbxassetid://你的冲刺动画ID, Attack1 rbxassetid://你的第一段攻击动画ID, Attack2 rbxassetid://你的第二段攻击动画ID, Attack3 rbxassetid://你的第三段攻击动画ID } } return HeroConfig注意rbxassetid://你的动画ID这种写法无法直接运行必须把变量替换成真实的动画资产 ID。在 Roblox Studio 中上传动画后可以从 Explorer 里右键 Animation 资产复制其 ID。4. 常规状态机实现状态定义与转换4.1 状态枚举与状态数据状态机第一步是定义数据。Roblox 的 Luau 中通常用 ModuleScript 返回一个常量表。在 ReplicatedStorage/HeroKit 下新建StateType.lualocal StateType { Idle Idle, Walk Walk, Jump Jump, Fall Fall, Dash Dash, Attack1 Attack1, Attack2 Attack2, Attack3 Attack3 } return StateType所有脚本统一引用这份枚举不要在不同脚本里手写字符串例如idle、Idle 或者待机。字符串不一致是状态机最常见的低级错误统一枚举能避免一半以上的状态切不过去问题。StateManager 还需要记录当前状态、上一个状态以及监听器。这里做一个通用版本后续其他英雄也能直接复用。4.2 状态转换表在写代码前先把“刀锋”常规状态的转换规则列成一张表。这张表既是开发依据也是后续排查时的文档。当前状态触发条件目标状态Idle / Walk按冲刺键且冷却结束DashIdle / Walk按下攻击键Attack1Idle / Walk离开地面Jump / FallJump垂直速度由正转负FallFall落地Idle 或 WalkDash冲刺计时结束根据移动输入回到 Idle 或 WalkAttack1攻击动作结束且连击窗口内按攻击键Attack2Attack2攻击动作结束且连击窗口内按攻击键Attack3Attack3攻击动作结束Idle 或 Walk这张表有几个隐含原则攻击状态不直接在连击窗口内跳到其它状态只能进入下一段攻击。跳跃和下落是互斥的只在垂直速度正负变化时切换。冲刺优先级高于移动低于受击和技能。如果后续加入“受击”状态应让受击状态打断冲刺。三段连击结束后必须回到移动或待机不能停留在 Attack3。4.3 StateManager 最小实现在 ReplicatedStorage/HeroKit 下新建StateManager.lualocal StateManager {} StateManager.__index StateManager function StateManager.new(initialState) local self setmetatable({}, StateManager) self.current initialState or Idle self.previous nil self.listeners {} return self end function StateManager:GetState() return self.current end function StateManager:GetPreviousState() return self.previous end function StateManager:SetState(newState) if self.current newState then return false, state already active end local oldState self.current self.previous oldState self.current newState for _, callback in ipairs(self.listeners) do task.spawn(callback, oldState, newState) end return true end function StateManager:OnStateChanged(callback) table.insert(self.listeners, callback) end return StateManager这里有两个容易踩的坑。第一个坑是“重复设置同状态”。如果不加if self.current newState判断动画控制器会反复播放同一段动画导致动画被重置表现成角色动作抽搐。第二个坑是task.spawn的使用。状态变化回调可能会触发动画播放、音效播放、伤害计算如果某个回调抛异常不能让它破坏状态机的后续通知逻辑所以用 task.spawn 隔离每个监听器。5. 移动、动画与冲刺控制5.1 移动输入与 RunService 驱动客户端脚本放在StarterPlayerScripts下时它的父级不是角色需要通过玩家对象等待角色生成。另一种更简单的方式是放在StarterPlayer.StarterCharacterScripts下这样script.Parent就是角色本体。推荐把输入控制和状态更新放在同一个 LocalScript 中减少跨脚本通信。下面是一个可运行的状态驱动骨架local Players game:GetService(Players) local RunService game:GetService(RunService) local UserInputService game:GetService(UserInputService) local ReplicatedStorage game:GetService(ReplicatedStorage) local HeroKit ReplicatedStorage:WaitForChild(HeroKit) local StateType require(HeroKit:WaitForChild(StateType)) local StateManager require(HeroKit:WaitForChild(StateManager)) local HeroConfig require(HeroKit:WaitForChild(HeroConfig)) local player Players.LocalPlayer local character script.Parent local humanoid character:WaitForChild(Humanoid) local rootPart character:WaitForChild(HumanoidRootPart) local state StateManager.new(StateType.Idle) local dashStartTime 0 local attackIndex 0 local lastAttackTime 0 local function getGroundState() local moveDirection humanoid.MoveDirection local horizontalSpeed math.sqrt(moveDirection.X * moveDirection.X moveDirection.Z * moveDirection.Z) if horizontalSpeed 0.1 then return StateType.Walk end return StateType.Idle end local function isDashReady() local elapsed os.clock() - dashStartTime return elapsed HeroConfig.DashCooldown end local function performDash() if not isDashReady() then return end local current state:GetState() if current StateType.Dash then return end dashStartTime os.clock() state:SetState(StateType.Dash) local moveDirection humanoid.MoveDirection if moveDirection.Magnitude 0.1 then moveDirection rootPart.CFrame.LookVector end local dashVelocity moveDirection * HeroConfig.DashSpeed rootPart.AssemblyLinearVelocity Vector3.new(dashVelocity.X, 0, dashVelocity.Z) task.wait(HeroConfig.DashDuration) if state:GetState() StateType.Dash then state:SetState(getGroundState()) end end local function performAttack() local current state:GetState() if current StateType.Dash then return end local now os.clock() if current StateType.Attack1 or current StateType.Attack2 then if now - lastAttackTime HeroConfig.AttackComboWindow then if current StateType.Attack1 then state:SetState(StateType.Attack2) attackIndex 2 elseif current StateType.Attack2 then state:SetState(StateType.Attack3) attackIndex 3 end lastAttackTime now end return end if current StateType.Idle or current StateType.Walk then attackIndex 1 lastAttackTime now state:SetState(StateType.Attack1) end end UserInputService.InputBegan:Connect(function(input, gameProcessed) if gameProcessed then return end if input.KeyCode Enum.KeyCode.LeftShift then performDash() end if input.KeyCode Enum.KeyCode.Q then performAttack() end end) RunService.RenderStepped:Connect(function() local current state:GetState() if current StateType.Dash then return end if current StateType.Attack1 or current StateType.Attack2 or current StateType.Attack3 then return end local verticalVelocity humanoid.Velocity.Y if humanoid.FloorMaterial Enum.Material.Air then if verticalVelocity 1 then state:SetState(StateType.Jump) elseif verticalVelocity -1 then state:SetState(StateType.Fall) end else state:SetState(getGroundState()) end end)这段代码有几个细节需要解释。performDash里用rootPart.AssemblyLinearVelocity而不是调整 WalkSpeed是因为 WalkSpeed 会立刻触发 Humanoid 的移动逻辑而冲刺往往希望有更好的爆发感。直接设置速度会让角色在短时间远离之后通过状态机回到移动状态时Humanoid 再接管正常移动。performAttack里限制只有 Idle 和 Walk 状态能起手攻击这是常规格斗游戏的常见规则。如果在跳跃或下落状态按攻击当前版本不会响应。这个限制可以避免玩家在空中乱按导致动画和位移冲突。5.2 动画加载与切换动画控制器需要把 Animation 资产加载到 Animator 上。重点关注三个方法LoadAnimation、Play、Stop。local AnimationController {} AnimationController.__index AnimationController function AnimationController.new(humanoid, animationMap) local self setmetatable({}, AnimationController) self.animator humanoid:FindFirstChildOfClass(Animator) self.tracks {} for name, animationId in pairs(animationMap) do local animation Instance.new(Animation) animation.AnimationId animationId self.tracks[name] self.animator:LoadAnimation(animation) end return self end function AnimationController:Play(name, fadeTime) local track self.tracks[name] if not track then return false end track:Play(fadeTime or 0.15, 0, 0.2) return true end function AnimationController:StopAll(fadeTime) for _, track in pairs(self.tracks) do if track.IsPlaying then track:Stop(fadeTime or 0.15) end end end return AnimationController使用方式是在状态机监听器里根据新状态播放对应动画并在进入新状态前停止上一段动画local animationController AnimationController.new(humanoid, HeroConfig.AnimationIds) state:OnStateChanged(function(oldState, newState) animationController:StopAll(0.1) animationController:Play(newState, 0.15) end)这个写法在状态频繁切换时会有问题如果 Idle 和 Walk 之间快速切回动画会不断重置。实际项目中建议增加一个“相同动画不重复播放”的判断或者为 Idle/Walk 这类无缝动画设置较大的淡入淡出时间。这个可以放在手感调优阶段处理。5.3 冲刺与基础连击的手感细节冲刺如果直接修改 Humanoid.WalkSpeed在部分情况下会触发角色加速器的回弹表现成速度忽快忽慢。推荐的方式是使用Humanoid:ChangeState(Enum.HumanoidStateType.Physics)加AssemblyLinearVelocity但那样处理碰撞会更麻烦。第一版先用设置速度的方式注意冲刺结束不要立刻把速度清零而是把控制权交回 Humanoid。基础连击的判定窗口很关键。AttackComboWindow表示第一段攻击结束后玩家在多少秒内再次按攻击键可以进入下一段。窗口太短玩家根本按不出来窗口太长角色会一直处于可连击状态看起来像在保留攻击。建议初始值设为 0.5 到 0.7 秒之后根据测试反馈调整。连击结束后的“攻击后摇”也很重要。攻击动画播放完毕后角色不能立刻无限制再接攻击必须回到 Idle 或 Walk等待下次输入。当前示例中 Attack3 结束后没有自动重置attackIndex需要补充local function onAttackAnimationFinished() attackIndex 0 state:SetState(getGroundState()) end实际做法是给第三段攻击动画绑定Animator的动画事件由服务端或动画事件监听器触发重置。5.4 常规状态的手感参数表“手感”不是一个模糊概念它最终会落到一组可调整的参数上。下表是刀锋第一版需要关注的参数每个参数都必须单独调不要一次性改多个。参数初始值调优方向影响推荐做法WalkSpeed18增加至 20 可提升追击能力影响走位精度每 0.5 步进调整DashSpeed34建议为 WalkSpeed 的 1.8 到 2 倍太快会冲出目标不超过 WalkSpeed 的两倍DashCooldown1.5 秒缩短至 1.0 秒可更灵活太短会失去冷却意义先保证连招节奏DashDuration0.25 秒冲刺距离由速度和时间共同决定时间太长像滑步保持在 0.2 到 0.3 秒AttackComboWindow0.6 秒超过 0.7 秒连招判定太宽松影响攻击节奏多测几轮按键节奏JumpPower55调整为 60 可增加跳跃高度影响空中对战不建议超过 65调参时要记录基准值。最简单的方法是先在纸上写出目标手感比如“角色在平坦地面可以轻松完成三次连招后接一次冲刺”然后只调相关参数连击窗口、冲刺冷却、冲刺持续时间。不要同时调移动速度和跳跃高度否则出现问题无法定位是哪一项导致了手感变化。注意本文所有数值只适合作为起点不要直接当作生产数值使用。手感是强个人化体验必须通过多设备测试确定。6. 运行验证与常见问题排查6.1 验证方式与日志设计常规状态验证分三层。第一层输入验证。确认按下 W、A、S、D 后角色有移动方向RootPart 速度向量正确。第二层状态验证。在 Output 窗口打印状态变化。可以在客户端脚本里加一条监听state:OnStateChanged(function(oldState, newState) print(string.format([刀锋] state %s - %s, oldState, newState)) end)运行后执行一次移动和跳跃预期会看到类似顺序[刀锋] state Idle - Walk [刀锋] state Walk - Jump [刀锋] state Jump - Fall [刀锋] state Fall - Walk如果顺序混乱比如从 Idle 直接跳到 Attack2说明攻击状态机的连击窗口判断有误。第三层动画验证。先单独播放每个动画 ID确认动画片段本身没有问题再绑定状态机。如果动画 ID 写错Output 窗口会出现Animation could not be loaded的日志。6.2 高频问题排查表问题现象可能原因检查方式处理建议角色完全不动WalkSpeed 被其他脚本覆盖输出打印 WalkSpeed在服务端统一初始化避免多个脚本重复赋值状态一直停在 IdleMoveDirection 始终为零打印 idle/walk 判断条件确认 LocalScript 属于本地角色且 HumanoidRootPart 未被锚定动画不切换状态机没有触发 SetState打印状态监听器回调检查 StateType 枚举字符串是否一致攻击只能打一段连击窗口使用时间戳不准确打印 lastAttackTime 和当前时间检查攻击判定是否绑定了动画事件而不是短流程 wait冲刺后速度异常服务端和客户端同时改速度检查是否有两个脚本修改 AssemblyVelocity只保留一个控制端服务端负责校验客户端负责手感跳跃动画和下落动画反复跳FloorMaterial 判断不稳定打印当前 FloorMaterial用 Humanoid.FloorMaterial 判断后增加垂直速度阈值如果是自己搭建的测试地图最好在空旷的 Baseplate 上验证避免地形碰撞和不平整地面干扰状态判断。6.3 发布到生产环境前还要做什么单机跑通只是第一步。如果这是要发布给其他玩家玩的体验常规状态之外还要补以下内容。第一攻击伤害不能由客户端直接计算。当前的客户端代码只处理状态和动画伤害最终应该由服务端在攻击命中事件中触发。攻击动作播放到命中帧时客户端通过 RemoteEvent 上报“我使出了第 n 段攻击”服务端再检测范围和方向。第二冲刺冷却不能被客户端随便重置。客户端记录冷却只是为了显示 UI服务端要维护同一份冷却状态。否则玩家可以通过修改本地脚本绕过冷却。第三状态机脚本要处理角色死亡。Humanoid.Died 事件触发时客户端要停止所有动画、清理状态监听器防止角色重生后残留旧状态。第四动画资产需要上传成 Roblox 平台上的正式资产并记录 ID。开发期间的占位 ID 不能进入发布版本。7. 可复用清单与下一步扩展思路7.1 常规状态开发检查清单把本文的实践经验整理成一张清单在开始下一个英雄的常规状态开发时逐项检查。状态枚举是否统一存放在 ModuleScript 中。状态转换是否有一张明确的转换表。状态机是否阻止重复设置同状态。角色属性是否由服务端初始化。移动输入是否只在客户端驱动。动画 ID 是否替换成真实资产。攻击连击窗口是否由清晰时间戳控制。冲刺是否避免直接依赖 WalkSpeed 达到加速效果。角色死亡时是否清理状态机和动画监听器。伤害计算是否只由服务端执行。所有数值是否集中在 HeroConfig 等配置模块中。每个状态变化是否有日志方便排查。这份清单既适用于刀锋也适用于后续的自制英雄。写新英雄时先把这份清单走一遍能避免大量重复排错。7.2 从常规状态扩展到技能与绝招状态刀锋的常规状态稳定后扩展方向很明确增加技能状态、绝招状态、受击状态和倒地状态。新增状态时前一个版本的状态机转换表要同步更新。推荐扩展顺序受击状态打断当前动作让角色进入短暂硬直。技能状态在常规状态基础上加入一个可释放的突进或范围攻击技能期间禁止普通攻击。绝招状态进入播放演出动画状态期间控制权封锁结束后回到常规状态。空战状态让玩家在跳跃和下落时也能打出一段空中攻击。每一个新状态加入时都要检查它和现有状态的互斥关系。比如受击状态应该能打断攻击和冲刺但绝招状态不应该被普通攻击打断。7.3 对后续版本的一点建议开发 TSB 类武器的自制英雄时最大的技术风险不是写不出功能而是状态增长之后失控。官方内置玩法的背后通常有一整套状态管理和动画事件框架。自建项目时不要把状态机局限在一个英雄脚本里而是把所有英雄共用的状态管理、动画管理、伤害管理抽成通用模块。刀锋的“常规状态”只是起点但它决定了后续所有特殊能力的基础手感。先把这一层打磨稳定再往里面加技能会比先堆功能再回改手感顺畅得多。