ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Unity复刻三国战纪:2D动作游戏的连招与打击感实现全攻略

Unity复刻三国战纪:2D动作游戏的连招与打击感实现全攻略 简介一套使用Unity引擎开发2D街机游戏《三国战纪》的完整工程资源适合Unity初学者、独立游戏开发者以及想复现经典横版战斗玩法的学习者。资源覆盖场景搭建、精灵动画状态机、物理碰撞、输入控制、音频管理、用户界面、性能优化及多平台发布等核心环节可配合源码脚本与预制体直接研究学习。压缩包共443个文件以C#逻辑脚本、角色动画文件、精灵图与资源配置文件为主另含着色器、预制体与动画控制器整体仅4.5MB便于下载与解压。已有335人学习下载适合通过实例快速掌握Unity 2D游戏开发主干技术。通过阅读脚本注释和动画状态配置可以理解角色移动、攻击、伤害反馈等实现逻辑同时参考场景组织和预制体层级树能学习到街机游戏常见的对象管理与资源引用方式。1. 用 Unity 复刻 2D 街机《三国战纪》为什么同类项目最容易卡在连招和打击感“三国战纪”这四个字对老玩家是街机厅记忆对现在的 Unity 开发者则是一个特别能练手的 2D 横版清版动作游戏题材。很多人以为这类游戏“场景一条横轴、敌人走过去砍就行”真动手才发现移动手感、连招窗口、攻击判定帧、命中反馈每一处都是决定“玩起来像不像街机”的细节。这篇笔记不聊情怀就聊怎么用 Unity 的 2D 工具链把这个品类从零跑通——场景、角色、敌人、打击感一条线做下来顺带把碰撞穿透、判定丢失、输入迟滞这些会让项目翻车的坑提前拆掉。适合手里有一张角色图集、想快速验证横版清版玩法的独立开发者也适合用这个题材练动作游戏基本功的 Unity 新手。2. 项目与场景搭建2D 项目底子没打好后面连招和特效全是返工先说个血泪经验复刻这类游戏第一个坑不是代码是项目模板和渲染管线选得太随意。等到角色做了一半、技能特效一上才发现光效渲不出来、Shader 不兼容那时候再换管线等于全部重来。所以这一章先花小半天把地基定死。2.1 URP 还是内置管线先定渲染路线再开项目Unity Hub 新建项目时有两个和 2D 相关的选项“2D”模板走的是 Built-in 内置管线“Universal 2D”模板自带 URP 和 2D Renderer。我一般直接选 Universal 2D理由很实际三国战纪这类游戏的爽感很大程度靠技能光效、辉光和屏幕震动URP 下的后处理泛光Bloom开起来非常省事URP 自带的 2D Light 也能给场景加点火光、昼夜变化之类的廉价氛围内置管线想做这些得找插件或者手写 Shader维护成本全在自己身上。那什么时候用内置管线如果你的素材风格是纯像素、且大量依赖自定义 Shader 做特殊效果比如水墨晕开、溶解、马赛克内置管线对自定义 Shader 的兼容性更稳。URP 下很多老 Shader 要改成 URP 兼容写法这一改就是好几个晚上。对比项内置管线URP后处理光效需要额外方案或手写自带 Bloom开箱即用2D Light基本没有2D Renderer 内置自定义 Shader兼容最稳需要 URP 兼容重写性能上限合批一般SRP Batcher 合批更好另一个在新项目里就要定的是 Pixels Per UnitPPU。三国战纪原版素材按 320x224 的分辨率绘制放到现代屏幕上不糊的关键是 PPU 和相机尺寸匹配。常见做法以角色原始像素高度反推 PPU比如角色图高 64 像素、希望在屏幕上大约显示 2.5 格高度PPU 取 64 / 2.5 ≈ 25.6就近取 32。然后在 Project Settings 里把游戏分辨率设成 PPU 的整数倍关系避免相机出现半像素偏移。这个值定了之后再改所有场景坐标和碰撞体尺寸都要跟着动所以一开始就写进项目规范里。2.2 Tilemap 搭第一关场景碰撞合并与图层遮挡基准场景我用 Tilemap 搭这是 Unity 2D 项目里最成熟的方案。打开 Window 2D Tilemap Palette创建地面、平台、背景三张 Tilemap然后把整个场景的渲染层级按一张表固定下来这是后面所有遮挡问题的基础对象Sorting Order说明背景-20最远一层天空和远景地面 Tilemap-10玩家脚下的可碰撞地面角色、敌人0动态物体之间按 Y 轴排序前景 Tilemap10草丛、柱子、屋檐玩家走到后面时被遮住UI100血条、连击数给地面 Tilemap 挂上 Tilemap Collider 2D 之后一定要再挂一个 Composite Collider 2D并且把 Tilemap Collider 2D 上的 Used By Composite 勾上。不加 Composite 时每个 Tile 都会生成一个独立碰撞体同屏几十个敌人时物理引擎要反复做大量碰撞相交检测性能是线性恶化的加了 Composite 后所有 Tile 合并成一个完整碰撞形状物理相交检测次数骤降这是优化里性价比最高的一步。调试时可以在组件上把碰撞体可视化打开确认合并后的形状没有破洞。摄像机跟随用 CinemachinePackage Manager 里搜 Cinemachine 装上创建一个 CM CameraBody 选 Framing TransposerFollow 目标指向玩家。街机场景跟得太紧会让画面来回抖横轴上留 0.1 到 0.2 的 Dead Zone玩家在小范围内移动时相机不动推进出去才开始平移。参数上 Ahead Distance 设 0.5、Slow Zone Width 设 0.2手感最接近街机厅那种“人在画面中间但场景有呼吸感”的节奏。2.3 从街机图集到 Animator切割、像素参数与状态机过渡条件素材导入时把 Texture Type 设为 Sprite、Sprite Mode 设为 MultipleFilter Mode 用 Point像素风必须否则边缘糊一圈Compression 关掉或选无压缩。然后打开 Sprite EditorSlice Type 选 By Grid按行列切割。这里要注意网上流传的三国战纪素材是从街机 ROM 里拆出来的不同角色的站位和大小并不统一按格子切完还要逐帧微调包围盒这一步没有捷径我一般按“每张图先把边缘空白裁掉再统一锚点”的顺序处理一遍。注意这类从街机 ROM 提取的素材仅适合本地学习和技术验证不能上架发布。发布版本必须换原创美术否则版权问题跑不掉。切割完成后就是 Animator 状态机。清版游戏角色的基础状态我固定用这一套Idle、Run、Attack_1、Attack_2、Attack_3、Hurt、Die。参数只留两个Speedfloat和 Comboint。过渡条件里Idle 到 Run 判断 Speed 0.1攻击段之间判断 Combo 等于 2 或 3。关键设置是攻击动画全部关闭 Loop TimeHas Exit Time 关掉这样连招可以随时打断当前动画切入下一段手感才不会像幻灯片一样等动画播完再动。动画事件只用来做音效和连招窗口通知不做攻击判定原因到第 5 章专门展开。3. 角色控制器横版移动、Hitbox 判定与四段连招的代码实现场景和动画就绪后下面这一章是整个项目最值钱的部分——手感。我见过太多 demo 死在移动手感上角色要么起步像溜冰要么转向像卡车。这章直接给代码和参数照着改就能用。3.1 移动手感GetAxisRaw 加自算加速度别再让内置平滑拖手新手最容易犯的错误是用 Input.GetAxis 读水平轴。GetAxis 自带一整套平滑参数Sensitivity、Gravity、Dead它会让你按键后数值慢慢涨上去、松手后慢慢掉下来听起来“平滑”但横版格斗要求的是瞬时响应。正确做法是用 GetAxisRaw 读原始输入再加自己的加速度逻辑二者差别就是街机和 RPG 手感的差别。// PlayerMovement.cs - 横版移动核心逻辑 using UnityEngine; [RequireComponent(typeof(Rigidbody2D))] public class PlayerMovement : MonoBehaviour { public float moveSpeed 6f; // 最大水平速度世界单位/秒 public float acceleration 60f; // 起步加速度越大响应越猛 public float deceleration 50f; // 松手后减速越大停得越干脆 private Rigidbody2D rb; private float moveInput; private void Start() { rb GetComponentRigidbody2D(); } private void Update() { // 输入读取放 Update按键状态是逐帧的这里读最及时 moveInput Input.GetAxisRaw(Horizontal); } private void FixedUpdate() { // 物理写入放 FixedUpdate速度改动会在物理步进里统一生效 float targetVx moveInput * moveSpeed; float currentVx rb.velocity.x; float step (moveInput ! 0f ? acceleration : deceleration) * Time.fixedDeltaTime; float newVx Mathf.MoveTowards(currentVx, targetVx, step); rb.velocity new Vector2(newVx, rb.velocity.y); } }为什么不直接写rb.velocity new Vector2(moveInput * moveSpeed, rb.velocity.y)直接赋值等于瞬间到达目标速度角色看起来像被拽着走没有起步加速的“蹬地感”。用 MoveTowards 每帧逼近目标速度加速度和减速度就变成两个独立旋钮调整起来非常直观。参数上我建议 moveSpeed 取 6 到 8低于 5 角色显得拖沓高于 8 在普通碰撞配置下容易穿模加速度 60 意味着约 0.1 秒达到满速这是街机清版游戏最舒服的起步时间deceleration 比 acceleration 略小或相等都行松手后大约 0.12 秒停住停顿太慢会显得角色脚下抹油。3.2 攻击判定Hitbox 激活时长、LayerMask 过滤与命中去重攻击判定我不用射线也不做距离判断而是用一个临时碰撞体在角色子节点下建一个空物体挂 BoxCollider2D 并勾 Is Trigger默认关闭。攻击时激活它持续一小段时间再关闭凡是在这段时间内进入这个碰撞体的敌人都会被打到。Trigger 的好处是它不参与物理碰撞不会把敌人推走、也不会让自己弹开击退效果用代码控制更干净。// PlayerAttack.cs - 攻击判定与命中结算 using System.Collections.Generic; using UnityEngine; public class PlayerAttack : MonoBehaviour { public Collider2D hitbox; // 子节点上挂的 Trigger 碰撞体 public float hitboxDuration 0.10f; // 判定持续时长秒 public LayerMask enemyMask; // 只检测敌人所在 Layer private HashSetCollider2D alreadyHit new HashSetCollider2D(); private float timer; private void Start() { hitbox.enabled false; } public void BeginAttack(int comboIndex) { alreadyHit.Clear(); hitbox.enabled true; // 第三、四段重击判定稍长视觉上更有分量 timer comboIndex 3 ? hitboxDuration * 1.2f : hitboxDuration; } private void Update() { if (!hitbox.enabled) return; timer - Time.deltaTime; if (timer 0f) hitbox.enabled false; } private void OnTriggerEnter2D(Collider2D other) { // LayerMask 位运算过滤比 CompareTag 的字符串比较更快 if ((enemyMask.value (1 other.gameObject.layer)) 0) return; // 同一段攻击对同一敌人只结算一次避免连击时重复触发伤害 if (!alreadyHit.Add(other)) return; other.GetComponentEnemy().TakeDamage(10); } }hitboxDuration 是关键参数0.08 到 0.15 秒之间手感最好。小于 0.08 秒动画播放速度稍快一点就会漏帧大于 0.2 秒命中会显得“发飘”明明刀已经收回来了敌人还在掉血。Hitbox 的尺寸和位置我建议用手调而不是代码生成在 Sprite 编辑器里打开碰撞体预览把攻击判定框大致对准角色前方拳头的位置再微调 Offset X 让它略超出角色前脚即可。去重的 HshSet 必须有OnTriggerEnter2D 在物理引擎同一帧内对已经重叠的碰撞体可能触发多次不去重就会出现“一刀砍出三刀伤害”的怪现象。3.3 连招窗口0.3 秒窗口配 Combo 序号别用 SetTrigger 传连段四段连招不是简单的“按四下攻击打四下”中间有个窗口机制。每段攻击收招后留出 0.3 到 0.45 秒这个时间内按攻击键进入下一段超过窗口还没按连招断掉角色回到第一段。窗口太短新手连不上太长会显得攻击拖泥带水三国战纪类的成年玩家手感区段就在这个区间。// ComboChain.cs - 四段连招的窗口判断 using UnityEngine; public class ComboChain : MonoBehaviour { public int maxCombo 4; public float comboWindow 0.4f; // 收招后的有效输入窗口秒 private Animator animator; private int currentCombo; private float windowTimer; private bool canNext; // 当前是否处于连招窗口内 private void Awake() { animator GetComponentAnimator(); } private void Update() { // 窗口计时超时则连招断掉 if (canNext) { windowTimer - Time.deltaTime; if (windowTimer 0f) { currentCombo 0; canNext false; } } if (Input.GetButtonDown(Fire1)) { // 起手条件当前没在连招中或者正处在窗口内 if (currentCombo 0 || canNext) { currentCombo currentCombo maxCombo ? 1 : currentCombo 1; canNext false; animator.SetInteger(Combo, currentCombo); // 在这里调用 PlayerAttack.BeginAttack(currentCombo) } } } // 由攻击动画的 Animation Event 在收招阶段调用 public void OnAttackFinished() { canNext true; windowTimer comboWindow; } }这里用animator.SetInteger(Combo, currentCombo)而不是 SetTrigger原因是连招时输入频率很高Trigger 本质是一次性事件快速连按时很容易丢触发而 integer 参数会一直保持到下一段攻击把它覆盖动画过渡条件用 Equals 比较反而更可靠。窗口打开时机也很讲究OnAttackFinished 不要等整段动画播放完在攻击判定结束后的收招阶段就调用玩家可以提前输入下一段这叫“取消后摇”是清版游戏连招流畅度的关键如果把窗口放到动画完全播完玩家会觉得角色“硬直太长、节奏跟不上”。四段连招的最后一段建议做特殊处理给被击中的敌人施加一个向前的击飞速度同时本段攻击附带屏幕震动触发这部分到第 6 章再说。4. 敌人生成与对象池同屏敌人一多就掉帧的解法与容量参数三国战纪的核心玩法就是一波人冲上来让你砍敌人和攻击特效都是高频生成销毁的对象。我第一版图省事全用 Instantiate 和 Destroy结果一打团战帧率肉眼可见地掉。这一章解决的就是这个问题。4.1 Instantiate 和 Destroy 到底贵在哪为什么清版游戏必须池化Instantiate 的代价不只是“创建一个 GameObject”那么简单它要执行 Awake、OnEnable加载资源引用设置 transform 层级全程在主线程完成Destroy 也不是立刻消失而是放到帧末统一处理。当一秒内同时生成十几个敌人加几十个特效时主线程就被这些创建销毁操作占满了帧率自然向下掉。更隐蔽的问题是堆分配频繁创建和销毁会不断产生短命对象触发 GC 时游戏会卡顿一下在动作游戏里这一下卡顿足以让玩家骂人。对象池的思路就是把这些高频对象变成“租用”而不是“新建”敌人死了不动画销毁而是关闭后回收下次要生成时重新激活、重置状态。激活和重置的开销远小于创建和销毁而且完全不产生 GC。清版游戏里敌人的生成模式是可控的池容量按“单波峰值”预分配这是最稳妥的工程做法。4.2 通用对象池 GamePool 初始容量、扩容与回收时机直接写一个和游戏逻辑解耦的泛型池不依赖具体类型敌人、特效、掉落物都能用。// GamePool.cs - 与 Unity 解耦的通用对象池 using System.Collections.Generic; using UnityEngine; public class GamePoolT where T : Component { private T prefab; private QueueT pool new QueueT(); private ListT active new ListT(); public GamePool(T prefab, int initialSize) { this.prefab prefab; for (int i 0; i initialSize; i) { T instance Object.Instantiate(prefab); instance.gameObject.SetActive(false); pool.Enqueue(instance); } } public T Spawn(Vector3 pos, Quaternion rot) { if (pool.Count 0) { T item pool.Dequeue(); item.transform.SetPositionAndRotation(pos, rot); item.gameObject.SetActive(true); active.Add(item); return item; } // 池满时扩容一次性扩到当前容量的两倍减少多次触发扩容 T newItem Object.Instantiate(prefab, pos, rot); active.Add(newItem); return newItem; } public void Despawn(T item) { item.gameObject.SetActive(false); active.Remove(item); pool.Enqueue(item); } public int ActiveCount active.Count; }两个容量参数需要按玩法算。敌人池单波峰值的一半作为初始容量。比如一波出 8 个敌人、同时可能还有上波残敌池开 12 到 15 就够。特效池按满屏幕最热闹的时刻算比如一套重击同时爆 5 颗火星、每颗火星持续 0.6 秒且每秒生成 30 个粒子峰值特效对象差不多 15 个池就开 20。池太小会频繁触发扩容扩容本身就是在 Instantiate性能收益打折池太大会有一堆对象白占内存清版游戏没必要。出池时一定要在 OnEnable 里重置血量、位置、颜色、动画状态否则会看到“复活”的敌人带着上一条命的残血出现这种 bug 我排查过一下午。4.3 波次生成与出生点选择间隔 0.8 秒和“别堵门”的排布波次生成用协程写最直观纯时间驱动还能随时 StopCoroutine 暂停。下面这个生成器是常见做法一组出生点随机选一个每 0.8 秒出一个敌人打完后等 5 秒再开下一波。// WaveManager.cs - 波次生成与敌人生成 using System.Collections; using UnityEngine; public class WaveManager : MonoBehaviour { public Transform[] spawnPoints; // 场景边缘的出生点 public Enemy enemyPrefab; public int enemiesPerWave 6; // 单波小兵数 public float spawnInterval 0.8f; // 同波内每个敌人的出生间隔 public float waveInterval 5f; // 波与波之间的休息时间 private GamePoolEnemy enemyPool; private void Start() { enemyPool new GamePoolEnemy(enemyPrefab, 8); StartCoroutine(SpawnLoop()); } private IEnumerator SpawnLoop() { while (true) { for (int i 0; i enemiesPerWave; i) { if (enemyPool.ActiveCount enemyPool.MaxCapacity()) yield break; // 满员暂停 Transform sp spawnPoints[Random.Range(0, spawnPoints.Length)]; Enemy enemy enemyPool.Spawn(sp.position, Quaternion.identity); enemy.Init(i % 2 0 ? 1f : 0.8f, i % 3 0 ? 2 : 1); yield return new WaitForSeconds(spawnInterval); } yield return new WaitForSeconds(waveInterval); } } }spawnInterval 低于 0.5 秒时玩家会被敌人瞬间围死在角落没有任何操作空间大于 1.2 秒又显得节奏稀拉。0.7 到 1.0 是清版游戏的标准区间。出生点选择上随机看似公平但可能连续三次都抽到玩家正前方的点敌人排着队冲过来。常见的做法是“玩家反方向优先”计算每个出生点到玩家的向量选与玩家面向方向夹角最大的一个保证敌人基本从画面边缘和背后进场玩家有转身应对的余地。这个逻辑加一个评估函数即可我这边没有专门贴出来。判定层这里顺便提一句敌人和玩家最好分别放在 EnemyLayer 和 PlayerLayer攻击 Hitbox 检测用的 LayerMask 只包含 EnemyLayer。Layer 过滤是位运算比 CompareTag 的字符串比较快得多同时注意 LayerMask 和 Rendering LayerMask 是两个完全不同的概念前者管物理检测后者管渲染分层别在物理检测里用后者过滤二者混用是新手常见翻车点。5. Unity 2D 清版街机避坑实录碰撞穿透、判定丢失与手感迟滞这一章是从“能跑起来”到“手感接近街机”的分水岭下面几条是我项目里真实踩过、后来整理成规范的问题。每条都是“现象、原因、解决”三段按最疼的几条写。5.1 高速移动偶尔穿模Collision Detection 与 Fixed Timestep 一起调现象角色以 7 格每秒速度冲刺时偶尔直接穿过地面或敌人碰撞体尤其斜着冲下坡时概率明显。原因Rigidbody2D 默认的 Collision Detection 是 Discrete离散检测只在物理步进时做一次。如果一帧内物体移动的距离超过了目标碰撞体的厚度这一次物理计算就会“跳过去”直接漏掉碰撞。速度越快漏检概率越大。解决两个设置一起改。角色 Rigidbody2D 的 Collision Detection Mode 改成 Continuous Speculative它对高速移动的漏检补偿最明显也比连续检测便宜再把 Project Settings Physics2D 的 Fixed Timestep 从默认 0.02 降到 0.01物理模拟频率翻倍漏检窗口进一步缩小。代价是物理计算量翻倍清版游戏同屏刚体数量本来就少这个代价完全可接受。5.2 攻击判定丢失或重复结算判定逻辑别绑在动画事件上现象攻击动画播放正常但伤害偶尔不出或者重击时同一个敌人被连续扣了两次血。原因把 Hitbox 激活放在了 Animation Event 上。动画只要被打断、闪帧、重新播放事件就可能不触发或重复触发同时 OnTriggerEnter2D 在同一物理帧对已重叠的碰撞体可能回调多次。判定逻辑依赖动画事件等于把核心战斗逻辑交给动画系统的时序稳定性完全没有保证。解决攻击判定全部走代码。每段攻击开始时由PlayerAttack.BeginAttack()激活 Hitbox持续一段时间后自动关闭不依赖任何事件。命中去重用 HashSet 在单段攻击内做一次过滤。Animation Event 只留给音效和连招窗口通知比如第 3 章的 OnAttackFinished这类事件即使丢一帧也不影响战斗公平性。5.3 按键黏脚、操作迟滞输入读取位置错了手感就废一半现象玩家按攻击键后角色慢半拍才出手快速连招时感觉越按越“黏”。原因三个叠加因素。一是用了 Input.GetAxis它自带输入平滑数值是渐入渐出而不是瞬时跳变二是输入读取写在 FixedUpdate 里FixedUpdate 不是每帧都调用输入被物理步进间隔切碎三是垂直同步开启时输入缓冲叠加感知上再慢一档。解决GetAxis 换成 GetAxisRaw原始输入没有任何平滑输入读取统一放 Update物理写入留在 FixedUpdate第 3 章的代码就是这个结构关闭 Project Settings Quality 里的 VSync。如果用了新 Input System注意连按时监听 Action 的 Performed 而不是 Started否则快速连点可能吞掉第一下。5.4 渲染层级被地面盖住Sorting Order 规则与 Y Sort 参数现象角色走着走着突然被 Tilemap 地面盖住或者敌人和角色前后遮挡关系随机有时看着像敌人站在玩家头顶。原因同一 Sorting Layer 下Unity 只按 Sorting Order 值排序值相同时按物体创建顺序这是不可控的。Tilemap 整体只有一个 Sorting Order如果不给地面和角色规划明确的层级渲染顺序就会乱。解决按第 2 章那张表定规则背景 -20、地面 -10、角色 0、前景区 10、UI 100。角色和敌人同为 0 层时加一个 Y Sort 脚本每帧更新sortingOrder (int)(transform.position.y * -50f)。y 越小画面里越靠下数值越大绘制越靠前这样玩家站在敌人上方时敌人会遮住玩家反过来玩家会遮住敌人——这就是 2D 游戏里“站在柱子后面被遮住”的正解。数值取 -50 足够排序又不至于在坐标翻转时闪烁取 -1000 反而容易在边界产生跳变。5.5 粒子特效一多就掉帧上限、合并与对象池缺一不可现象技能同时爆火、灰尘、刀光时帧率能掉一半上手机测一次卡一次。原因ParticleSystem 默认每个粒子是一次独立的渲染调用多个特效叠加时 Draw Call 暴涨特效对象又用 Instantiate 生成、Destroy 销毁GC 压力叠上去直接雪崩。解决特效全部进对象池沿用第 4 章的 GamePool每个粒子的关键参数做上限控制Max Particles 不超过 50Emission Rate 不要超过每秒 100Start Lifetime 短于 0.6 秒。特效纹理合到一张 Sprite Atlas 里Renderer 共用一份材质合批后 Draw Call 会大幅下降。屏幕震动别在 Update 里手写 Sin 叠加用 DOTween 的震动模式配合衰减参数既省代码又不会因为手写的震荡函数把摄像机推飞。6. 打击感最后一块拼图Hit Stop 0.06 秒、受击闪白与音效节奏如果你把前面的代码都调通了现在游戏应该“能打”但很可能还是觉得“刀是刀、人是人”砍下去没有街机那种“闷”的一下。真正的秘密不是粒子不是屏幕震动而是命中停顿——Hit Stop。Hit Stop 的做法是在命中瞬间把时间缩放暂停极短的一段时间让双方动画和位移都停住玩家视觉上会“感受”到这一刀的重量。清版游戏里这个停顿通常在 0.05 到 0.08 秒之间超过 0.1 秒玩家就会觉得游戏卡了。// HitStop.cs - 命中停顿实现 using System.Collections; using UnityEngine; public class HitStop : MonoBehaviour { public static HitStop Instance { get; private set; } private void Awake() { Instance this; } public void Stop(float duration) { if (Time.timeScale 1f) return; // 避免重复触发时停顿叠加 StartCoroutine(HitCoroutine(duration)); } private IEnumerator HitCoroutine(float duration) { Time.timeScale 0f; yield return new WaitForSecondsRealtime(duration); // 不受 timeScale 影响 Time.timeScale 1f; } }调用时机是命中结算的第一帧轻击给 0.05 秒重击和第四段连招给 0.07 到 0.08 秒。注意 Time.timeScale 0 会停止所有依赖时间缩放的逻辑DOTween 动画要额外 SetUpdate(true) 让它无视时间缩放继续执行否则屏幕震动会在停帧瞬间被冻结反而不对。受击闪白在命中瞬间也别落下被击者的 SpriteRenderer 颜色闪到白色再快速恢复配合 Hit Stop 和一次短促的震动优先级顺序玩家大脑会自动把这三件事合成“这一刀很重”的信号。音效的节奏比音效本身更重要——命中音效要比挥空音效早 0.02 秒左右响起听感上会觉得判定更果断。我早期项目里把粒子调得满天飞打击感还是不对后来把闪帧加上第一刀下去就立刻知道问题在哪了。这些参数没那么多玄学0.06 秒停帧、0.1 秒闪白、一次真实音效比你在特效上努力一个月都管用。希望帮到你。本文还有配套的精品资源点击获取
返回列表