资讯动态

Unity3D FPS完整工程拆解:从射击逻辑到敌人AI实战指南

发布时间:2026/10/21 11:57:21 来源:尧图企业网站定制
简介面向FPS游戏开发者的Unity3D完整工程源码包专为想学习联机射击游戏制作的开发者准备可作为从入门到进阶的实战案例。压缩包共1609个文件以bin二进制资源文件为主1252个并包含prefab预制体、mat材质、cs脚本、dll动态库与asset资源等另有一些meta、log和txt说明文件便于核对工程配置与运行记录整体约38.91MB已有2409人浏览学习。这套源码覆盖了FPS开发中的核心环节场景搭建与光照、CharacterController角色移动与第一人称视角控制、子弹发射与射线检测、碰撞特效触发、UNet或Mirror网络同步、NavMesh敌人AI寻路以及UI计分、音频混合与性能优化等并包含可运行场景与脚本源码便于直接导入Unity运行调试。通过阅读项目结构和关键脚本可以快速理解多人在线FPS游戏的整体架构掌握从地面场景构建到联机对战实现的完整流程。无论是想复现射击手感还是研究角色移动与网络同步实现都能从中获得可运行的示例对系统学习Unity3D游戏开发具有较高参考价值。1. 一个能直接跑起来的 Unity3D FPS 完整工程先看它怎么把「开枪」做成一整套逻辑拿到这份 Unity3D_FPS 完整工程第一件事别急着翻代码先把 Demo 场景跑起来。很多 FPS 模板资源拆开以后缺胳膊少腿要么只有第一人称控制器没有敌人要么武器是用贴图硬拼的要么角色掉血后 UI 根本不刷新。这份完整版的直接价值在于从移动、镜头、射击到敌人 AI、血条弹药 UI、刷怪和波次循环是一条闭合的战斗链路。对想上手 Unity3D FPS 项目开发的开发者来说它适合拿来当「能跑的参考实现」——照着改手感、换武器、换敌人行为都比从零搭一套省事得多。下面按「认识结构 → 跑通核心模块 → 排查问题 → 二次开发」的顺序带你把它拆开。2. 工程结构与技术选型这一层层的目录都在解决什么问题拆惯了模板工程的人都知道目录结构其实就是作者的架构决策。这份完整版的 Assets 目录按「场景—脚本—预制体—配置—美术」切分脚本里又按 Player、Weapon、Enemy、UI、System 分模块做到玩法代码不散落在场景里。先看清楚这些目录后面改东西才知道该去哪里找。2.1 资源包里有什么从目录结构看项目组织方式目录作用关键内容Assets/Scenes实际可玩的场景Main战斗主场景、Menu、Boot / 初始化Assets/ScriptsC# 玩法与系统代码Player / Weapon / Enemy / UI / System 五个子目录Assets/Prefabs可复用预制体武器模型、敌人单位、子弹特效、UI 面板Assets/Settings工程配置输入配置、渲染管线设置、物理层管理Assets/Art美术资源模型、贴图、材质、动画与音频Assets/Scenes 里通常会放一个可直接开玩的 Main.unity 和一个负责初始化的 Boot.unity。Boot 只把 GameManager 这类常驻物体加载出来再异步切到 Main这样后续你想加主菜单、加 Loading 界面都不需要动战斗场景。如果你打开工程发现只有一个 Main 场景说明作者把初始化逻辑放在了场景里或者用脚本挂载了入口跑之前先看一下场景里有没有挂 DontDestroyOnLoad 的对象这决定了你要从哪个入口开始改。Prefabs 单独放的原因很直接敌人、枪械、飘字伤害这些预制体既在场景里出现又会被对象池动态生成不放进 Prefabs 目录后面做 Resources 加载或者拖引用都会很痛苦。我一般会在 Prefabs 下再建一个 Pooled 子目录把需要池化的特效和子弹单独归拢后面你会看到这跟对象池脚本是一一对应的。提示拿到工程先找「场景里有没有 DontDestroyOnLoad 的常驻物体」这决定你从哪开始改初始化逻辑。2.2 技术选型输入系统、射线检测、对象池、渲染管线的取舍第一个大选型是输入。这份工程大概率用的是新 Input SystemInput Action 文件而不是旧 Input Manager原因是新输入系统天然支持键鼠和手柄两套绑定而且 FPS 玩法后续一定需要自定义按键功能。旧 Input 要自己在 Update 里用 Input.GetAxis 写一堆 if新系统用 action map 一次配置就够。如果你打开工程发现脚本里全是 Input.GetButtonDown那就是旧输入方案不影响跑但后面做自定义按键会难改不少——提前心里有个数。第二个选型是射击判定。完整 FPS 模板里「开枪命中」几乎清一色用 Physics.Raycast而不是生成一个子弹碰撞体。原因很直白子弹速度太快Unity 物理步长默认固定 0.02 秒高速运动物体在两次物理更新之间会直接穿透目标用碰撞体模拟子弹属于自己给自己挖坑。射线检测在命中精度、性能和代码可读性上都是最优解代价是模拟不了真实弹道但模板阶段没人需要弹道。第三个选型是对象池。子弹命中特效、弹痕、敌人死亡爆炸这类高频生成又很快销毁的对象模板里通常有一个 ObjectPool 单例而不是 Instantiate/Destroy 一把梭。Instantiate 在低端机上会引起明显的 GC 卡顿对象池用「先借后还」的方式规避第四章写敌人生成时你会看到完整实现。第四个选型是渲染管线。这份资源在内置渲染管线Built-in和 URP 之间都可能出现。快速判断方法看 Project Settings 里选择的 Render Pipeline Asset 是谁再看场景里有没有 Post-Processing Volume。URP 版本的后处理辉光、景深都配置在 Volume 里内置管线版本则用 Post Processing Stack。你改画面风格之前先搞清楚管线不然调了半天 Bloom 发现不生效这是 FPS 画面调优里最常见的玄学之一。3. 核心模块拆解一发子弹从枪口到伤害结算经过了哪些代码这个工程的战斗闭环是「移动 → 瞄准 → 开火 → 命中 → 伤害 → 击杀 → 波次推进」。我从玩家脚本、武器脚本、伤害接口三个层次来拆每一层你都可以直接抄走。3.1 角色移动与镜头控制鼠标手感的第一道关PlayerMotor 的移动逻辑是模板里最容易被改坏的部分。先看核心移动代码public class PlayerMotor : MonoBehaviour { public float walkSpeed 4f; public float sprintSpeed 6.5f; public float jumpForce 8f; private CharacterController controller; private Vector3 moveDir; private float verticalVelocity; void Awake() { controller GetComponentCharacterController(); } void Update() { Vector2 input Vector2.zero; // 新 Input System 中由 InputAction 回调写入 if (Keyboard.current ! null) { input.x (Keyboard.current.dKey.isPressed ? 1f : 0f) - (Keyboard.current.aKey.isPressed ? 1f : 0f); input.y (Keyboard.current.wKey.isPressed ? 1f : 0f) - (Keyboard.current.sKey.isPressed ? 1f : 0f); } float speed Input.GetKey(KeyCode.LeftShift) ? sprintSpeed : walkSpeed; Vector3 forward transform.forward; Vector3 right transform.right; moveDir (forward * input.y right * input.x) * speed; if (controller.isGrounded) { verticalVelocity -1f; if (Input.GetKeyDown(KeyCode.Space)) verticalVelocity jumpForce; } else { verticalVelocity Physics.gravity.y * Time.deltaTime; } moveDir.y verticalVelocity; controller.Move(moveDir * Time.deltaTime); } }逻辑说明这里用 transform.forward 和 transform.right 而不是世界坐标方向是为了让移动跟随镜头朝向——第一人称移动必须和视角方向一致否则你扭头 90 度再按 W 会发现人在斜着走。用 CharacterController.Move 而不是 Rigidbody是因为 FPS 场景里角色不需要物理反馈Move 自带碰撞和斜坡处理不会像刚体那样把场景里的物体推走。参数说明walkSpeed4 是很慢的战术步伐sprintSpeed6.5 是中等偏快。如果你喜欢更利落的移动手感把 walkSpeed 提到 6、sprintSpeed 提到 8如果模板里加了移动加速度缓动把对应的 lerpTime 改成 0.05 秒以内不然会有「溜冰感」。jumpForce8 在默认重力下跳高约 3.2 米写实向 FPS 建议压到 6 以下。再看镜头控制 CameraLookpublic class CameraLook : MonoBehaviour { public float mouseSensitivity 2f; public float pitchLimit 89f; public Transform cameraTransform; private float yaw; private float pitch; void Update() { Vector2 look Mouse.current.delta.ReadValue() * mouseSensitivity; yaw look.x; pitch - look.y; pitch Mathf.Clamp(pitch, -pitchLimit, pitchLimit); // 身体只旋转 Yaw镜头只旋转 Pitch transform.rotation Quaternion.Euler(0f, yaw, 0f); cameraTransform.localRotation Quaternion.Euler(pitch, 0f, 0f); } }逻辑说明身体和镜头分开旋转是为了防止万向节问题和「低头时整个人跟着倒」的经典错误。yaw 累加在角色 transform 上pitch 用 Clamp 限死在正负 89 度镜头永远不会翻转。mouseSensitivity2 是中间值电竞向玩家一般调到 1.2 到 1.5想追求「指哪打哪」可以拉到 3。这里的 Mouse.current.delta 是帧间相对位移不受屏幕分辨率影响比旧 Input 的 Input.GetAxis(Mouse X) 少了很多跳变问题。3.2 武器与射击射线检测、后坐力散布、弹匣管理武器射击脚本是整个 FPS 工程里信息量最集中的文件直接看子弹发射和散布的实现public class WeaponShooter : MonoBehaviour { public float damage 30f; public float fireRate 0.12f; // 秒/发约 500 RPM public float spreadAngle 1.5f; // 腰射散布角度 public float range 100f; public LayerMask hitMask; public GameObject muzzleEffect; public ParticleSystem bulletImpact; public AudioClip fireClip; private float lastFireTime; private int currentAmmo 30; private bool isReloading; public void TryFire() { // 冷却、弹药、换弹三合一前置判断 if (Time.time - lastFireTime fireRate || currentAmmo 0 || isReloading) return; lastFireTime Time.time; currentAmmo--; Vector3 dir GetDirectionWithSpread(); Ray ray new Ray(mainCamera.position, dir); if (Physics.Raycast(ray, out RaycastHit hit, range, hitMask)) { IDamageable target hit.collider.GetComponentInParentIDamageable(); target?.TakeDamage(damage, hit.point); // 命中点生成弹孔特效走对象池避免 GC 卡顿 ObjectPool.Spawn(bulletImpact, hit.point, Quaternion.LookRotation(hit.normal)); } // 枪口生成开火特效 ObjectPool.Spawn(muzzleEffect, muzzle.position, muzzle.rotation); } private Vector3 GetDirectionWithSpread() { Vector3 forward mainCamera.forward; // 角度随机散布远近视觉一致性比平面随机更好 float x Random.Range(-spreadAngle, spreadAngle); float y Random.Range(-spreadAngle, spreadAngle); return Quaternion.Euler(x, y, 0f) * forward; } }逻辑说明TryFire 把「能不能开枪」的所有判断集中在一个函数里——冷却计时、弹药是否耗尽、是否在换弹中。想加「开镜时缩小散布」就在判断后加一个分支。射线从相机位置而不是枪口位置出发这是 FPS 标准做法因为准星在画面中心从枪口位置出射线会明显打偏。散布用角度随机而不是给命中点加平面偏移是为了保证远近距离下的视觉散布一致这是容易被忽略的细节。参数说明damage30 对应 100 血敌人 4 发带走单人 FPS 的 TTK击杀耗时通常设计在 0.4 到 0.6 秒fireRate0.12 打满 4 发约 0.48 秒符合主流手感。hitMask 是灵异 Bug 重灾区你把敌人放到新的 Layer 却忘在 hitMask 勾选就会出现「打中特效但没伤害」的现象后面避坑章节专门讲。弹匣管理走 currentAmmo--换弹触发 isReloading 协程完成后复位弹药UI 那边订阅弹药变更事件自动刷新不会每帧去 Find 弹药文本。3.3 伤害判定与命中反馈IDamageable 接口串联起敌人和 UI模板里伤害系统做得好不好看敌人是不是实现了 IDamageable 接口public interface IDamageable { void TakeDamage(float amount, Vector3 hitPoint); } public class EnemyHealth : MonoBehaviour, IDamageable { public float maxHealth 100f; public float armor 0f; public event System.Action OnDied; public event System.Actionfloat, float OnHealthChanged; private float health; void Awake() health maxHealth; public void TakeDamage(float amount, Vector3 hitPoint) { float final Mathf.Max(0f, amount - armor); health - final; OnHealthChanged?.Invoke(health, maxHealth); if (health 0f) { OnDied?.Invoke(); // 触发对象池回收、掉落物生成、击杀计数 } } }逻辑说明这是典型的面向接口设计——WeaponShooter 不认识敌人具体是谁只调 IDamageable.TakeDamage。后面你要加可破坏的箱子、加 BOSS、加队友都能直接复用同一套射击代码这是 FPS 工程里最值得抄走的设计。敌人血条 UI 订阅 OnHealthChanged 刷新弹药 UI 订阅弹药事件刷新都是事件驱动而不是每帧 GetComponent 查数值这是模板里隐藏的架构亮点。参数说明armor0 时伤害公式退化为普通扣血设 armor10 表示每发子弹固定少扣 10 点适合做精英敌人或头盔判定。命中反馈链路也不复杂命中的敌人身上会生成弹孔特效这个特效对象几秒后自动回对象池。整套伤害结算从 Raycast 到接口调用再到 UI 刷新全程没有一次 Find 或者 GetComponent 轮询这也是它跑起来帧数稳定的原因。4. 敌人 AI 与战斗循环状态机如何让敌人从巡逻到追杀你模板里的敌人 AI 一般不用行为树而是手写状态机枚举加 switch 分发。这个选择非常适合学习——比 Behavior Tree 轻量逻辑在同一个文件里一眼能看完改分支条件也容易。4.1 敌人状态机巡逻、警觉、追击、攻击的切换条件public enum EnemyState { Patrol, Investigate, Chase, Attack, Dead } public class EnemyController : MonoBehaviour, IDamageable { public float visionRange 20f; public float visionAngle 60f; public float attackRange 5f; public float attackCooldown 1.2f; public float chaseSpeed 4f; public float patrolSpeed 1.5f; private EnemyState state EnemyState.Patrol; private Transform player; private float attackTimer; private NavMeshAgent agent; void Update() { switch (state) { case EnemyState.Patrol: PatrolUpdate(); if (CanSeePlayer()) state EnemyState.Chase; break; case EnemyState.Chase: agent.speed chaseSpeed; agent.SetDestination(player.position); if (Vector3.Distance(transform.position, player.position) attackRange) state EnemyState.Attack; break; case EnemyState.Attack: attackTimer Time.deltaTime; LookAtPlayer(); if (attackTimer attackCooldown) { attackTimer 0f; AttackPlayer(); } break; } } private bool CanSeePlayer() { Vector3 toPlayer player.position - transform.position; if (toPlayer.magnitude visionRange) return false; return Vector3.Angle(transform.forward, toPlayer) visionAngle; } }逻辑说明四个状态加一个 switch 就完成了「巡逻 → 追击 → 攻击」的闭环对模板项目来说足够清晰。CanSeePlayer 里的两个条件分别是距离和角度Vector3.Angle 判断视野锥——敌人只能看到前方 60 度内的目标这就是「背后蹲人」玩法成立的基础。NavMeshAgent 负责寻路配合场景里烘焙好的 NavMesh 数据自动绕开障碍物。参数说明visionRange20 是中距离警戒visionAngle60 接近人类视野写实值。做强警戒敌人就把范围拉到 30、角度拉到 90做傻子敌人就压到 10 和 45。更进阶的模板会在 Patrol 和 Chase 之间插一个 Investigate 状态——玩家刚暴露时敌人走到最后看见玩家的位置查看这个状态对单人关卡的压迫感提升非常明显模板如果没有二次开发时值得补上。4.2 敌人刷新与对象池死亡之后如何不卡顿地再次使用public class ObjectPool : MonoBehaviour { public static ObjectPool Instance; private Dictionarystring, QueueGameObject poolMap new Dictionarystring, QueueGameObject(); public GameObject Spawn(GameObject prefab, Vector3 position, Quaternion rotation) { string key prefab.name; if (poolMap.TryGetValue(key, out QueueGameObject cache) cache.Count 0) { GameObject go cache.Dequeue(); go.transform.SetPositionAndRotation(position, rotation); go.SetActive(true); return go; } return Instantiate(prefab, position, rotation); } public void Despawn(GameObject go, float delay 0f) { if (delay 0f) { StartCoroutine(DespawnDelayed(go, delay)); return; } go.SetActive(false); // 按名字放回对应队列等下次 Spawn 复用 } }逻辑说明这个池用字典加队列做先进先出Spawn 先从池里取没有才 InstantiateDespawn 不 Destroy而是 SetActive(false) 放回队列。弹壳、弹孔、拖尾特效这种短暂对象用带 delay 的 Despawn比如子弹特效 1 秒后自动回池。密集交火 10 秒会产生几十上百个临时对象不走对象池的话 GC 会把帧率直接拉下来。细节注意点用 prefab.name 做 key 有个小隐患——场景里同名预制体多了会串正规做法是用 prefab 的 GetInstanceID 或自定义 IdKey 字段模板为了可读性用名字你二次开发到中期可以换掉。敌人死亡流程是 EnemyHealth 的 OnDied 触发布置计算击杀数、掉落物、本体回池Spawner 发现存活数量不足就继续补怪。4.3 波次与玩家重生战斗节奏由谁控制public class WaveSystem : MonoBehaviour { public int waveIndex 1; public int enemiesPerWave 6; public float spawnInterval 2f; public Transform[] spawnPoints; private int aliveCount; private bool isWaveActive; void StartNextWave() { waveIndex; int enemyCount enemiesPerWave waveIndex * 2; // 每轮递增 2 只 StartCoroutine(SpawnWave(enemyCount)); } IEnumerator SpawnWave(int count) { isWaveActive true; for (int i 0; i count; i) { Transform point spawnPoints[Random.Range(0, spawnPoints.Length)]; GameObject enemy ObjectPool.Instance.Spawn(enemyPrefab, point.position, point.rotation); enemy.GetComponentEnemyHealth().OnDied () aliveCount--; aliveCount; yield return new WaitForSeconds(spawnInterval); } isWaveActive false; } }逻辑说明waveIndex 参与 formula enemiesPerWave waveIndex * 2这就是难度曲线数据驱动的雏形。你想让后期每波多 10 只把乘数改成 10不用动逻辑代码。spawnPoints 是场景里摆好的刷怪点随机取一个配合 NavMesh敌人出生后会自己朝玩家位置压过来。isWaveActive 这个锁很重要否则会出现上一波没清完下一波已经开始刷怪的叠加问题。玩家重生的逻辑通常在 PlayerHealth 里死亡后 UI 显示倒计时3 秒后把玩家位置重置到出生点恢复满血满弹药。模板一般把重生倒计时、出生点列表放在 GameManager 里和 WaveSystem 通过事件通信——WaveSystem 订阅 PlayerDied 事件来暂停刷怪等玩家重生后再继续。这套事件驱动的方式让两个系统不用互相持有引用是工程里值得保留的架构习惯。5. 常见问题与避坑记录五个跑不顺现场的完整排查以下五个问题是我拆同类 FPS 模板时被人问得最多、也亲眼见过最多翻车现场的每条按「现象 → 原因 → 解决」梳理你可以当排查手册用。注意如果同时中了多条按「输入 → 物理 → 场景 → 性能」的顺序排查效率最高。5.1 鼠标视角发飘转头时有明显滞后现象刚跑通 Demo鼠标转视角时画面拖泥带水快速甩枪时准星跟不上手。原因两个地方。一是镜头脚本里做了平滑Smooth Damp模板为了「电影感」给镜头加了阻尼对射击操作来说是致命手感问题二是帧率没锁定帧数波动导致每帧位移量不均匀。解决先把 CameraLook 里的 smoothTime 改成 0 或直接去掉平滑保证一帧一响应。还是飘的话在 Project Settings → Player 里把目标帧率锁定 144 或 60。还有一种玄学情况系统开启了指针精确度增强新 Input System 对系统级鼠标加速非常敏感处理方式是去操作系统关闭「提高指针精确度」。5.2 子弹明明打在敌人身上却没有伤害现象准星瞄得很正子弹命中特效在敌人身上弹了火花但血条纹丝不动。原因最常见两种。一是 hitMask 的 LayerMask 没勾选敌人所在的 Layer——你把敌人放到了新 Layer 上射线只认默认层二是射线打中了敌人的子物体碰撞体但子物体上没有挂 IDamageable如果代码用 GetComponent 而不是 GetComponentInParent接口查询直接落空。解决在 WeaponShooter 的命中分支里加一行 Debug.Log(hit.collider.gameObject.name / hit.collider.gameObject.layer)看打印出来的名字和 Layer再对照 hitMask 勾选的层。接口查询记得用 GetComponentInParent () 而不是 GetComponent除非你把 IDamageable 挂在每一个子碰撞体上。5.3 场景打开后灰蒙蒙一片或者灯光全部失效现象打开 Main 场景画面不是全黑就是过度泛白跟作者截图完全是两个游戏。原因这类模板 90% 是渲染管线配置没有生效。URP 工程被内置管线打开或反过来场景里的 Post-Processing Volume 找不到对应的 Render Pipeline Asset后处理直接失效另一种情况是场景用了烘焙光照而你把 Lighting 数据清掉或没加载 GI暗部就全黑了。解决先看 Project Settings → Graphics 里有没有指定 Render Pipeline Asset没有就手动拖对应的 URP Asset 进去。再打开 Window → Rendering → Lighting确认 Auto Generate 是否开启没开就点 Generate Lighting 重新烘焙。特别注意URP 模板千万别顺手装旧版 Post Processing 包两个后处理系统同时存在会互相打架。5.4 敌人站着不动或全部挤在墙角现象玩家进入视野范围内敌人既不追也不打甚至多只敌人叠在同一个点。原因敌不动的大部分原因是场景没有烘焙 NavMesh。NavMeshAgent 找不到可走地面就不会计算路径自然不追。敌人挤墙角则是因为刷怪点重合或者 Spawner 把多个敌人刷在同一个半径内容Agent 之间又没做分离。解决打开 Window → AI → Navigation选中场景里所有地面和障碍物在 Bake 面板点 Bake 生成 NavMesh然后看敌人是否开始巡逻。刷怪点重合就在 spawnPoints 里加一个 2 到 4 米的随机偏移。还有个隐蔽坑NavMeshAgent 的 Stopping Distance 如果设到 5 米敌人会在 5 米外停下攻击距离小于 Stopping Distance表现就是「敌人看着你不打」。5.5 打包后 UI 发虚、帧率比编辑器低一半现象编辑器里一百多帧打包出来只有四五十帧准星和血条还模糊。原因UI 模糊大多是 Canvas 的 Render Mode 用了 World Space 或 Screen Space - Camera且背后的 UI Camera 或默认分辨率设置不合理。帧率崩塌大概率来自战斗特效——特效粒子没被对象池回收死亡和命中产生的粒子越积越多DrawCall 和粒子更新量一起上涨。解决HUD 类 Canvas 固定用 Screen Space - Overlay除非你要做 3D 世界里的投影 UI。帧率问题先打开 Profiler 看 GPU 段的 Particle System 耗时把特效的 Max Particles 降档或者确保特效生命周期结束后调用 ObjectPool.Despawn 回池而不是挂 AutoDestroy 直接 Destroy。编辑器性能和打包不一致很正常——打包后的帧率由 Quality Settings 里的 vSync Count 控制以打包版本为准。6. 二次开发改造三个优先动手的方向和一个保命习惯6.1 手感三旋钮加速度、镜头阻尼、后坐力恢复FPS 手感调优不是改一个参数的事而是三组参数联动。第一是移动加速度管 lerpTime 的数值越小越跟手建议 0.05 秒。第二是镜头阻尼smoothTime 改到 0 后如果觉得太贼加回 0.02而不是 0.5。第三是后坐力恢复模板里开火后准星上跳准星回落速度由 recoilRecovery 控制主流做法是上跳 0.15 秒内完成、回落 0.6 秒内完成形成一个「上扬快、回落慢」的节奏玩家需要压枪。参数模板常见值改造方向walkSpeed4调 6 更爽快调 3 更适合潜行玩法lerpTime0.1~0.2压到 0.05 消除溜冰感smoothTime0.08射击玩法归零观赏玩法保留spreadAngle1.5°开镜时动态降到 0.3°recoilRecoveryTime0.8s分两段快回 40%、慢回 60%6.2 换枪换角色动画事件、开镜与挂点改造换武器模型时最常翻车的不是贴图是挂点。确认武器模型的握把点对在挂在右手骨骼的 gunPoint 上扳机位置要匹配开火动画。开火动画用 Animation Event 在子弹发射那一帧调用 TryFire比在 Update 里轮询动画进度精准得多。开镜ADS改造不要动枪模型位置按标准做法写一个摄像机 FOV 从 60 到 40 的插值协程同时把鼠标灵敏度按比例降到 60%这也是主机平台 FPS 的通用做法。6.3 从 Demo 到正式关卡把配置从代码里抽出去最后一步改造思路是数据驱动把 WaveSystem 里每波敌人数、间隔、刷怪点等数值抽到一个 ScriptableObject 关卡配置里每张新地图只需要新建一个配置资产不用再碰代码。敌人属性也一样——血量、移速、伤害做成 EnemyStats 资产配几只新敌人就是复制资产改数值的事。这套动作做完你的修改面就从「改代码」变成「改数据」出错概率直线下降。以前我拿到别人的 FPS 工程上来就换敌人贴图、调角色速度结果伤害链路断了三个地方都不知道从哪查起。从那以后我每次拿到模板都强制先完整跑三遍 Demo再动第一行代码每改一个模块就只验证一个行为绝不两处同时改。这样做虽然慢但踩坑成本反而最低——FPS 模板最怕的不是功能缺而是你把手感改坏了却说不清哪里坏。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑