ARTICLE DETAIL

资讯详情

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

Unity 3D射击游戏开发实战:从零搭建到性能优化

Unity 3D射击游戏开发实战:从零搭建到性能优化 简介这是一份基于Unity 2019.1.9f1制作的3D射击小游戏完整项目既包含可直接运行的exe程序也提供全部源码与工程文件适合初学Unity游戏开发、想要研究射击类玩法的读者参考。压缩包共4655个文件约93.53MB核心包括181个C#脚本、134个FBX模型、101个Prefab预制体、195个DLL库以及场景、材质、贴图、配置文件等构建好的可执行文件也已包含在内。目前已有2162人学习下载可作为Unity 3D游戏开发入门练习或课程设计的参照。项目涵盖角色控制、射击判定、敌人AI、UI交互等常见模块既可直接运行体验完整玩法也可在Unity编辑器中打开工程查看场景层级与代码逻辑理解从资源组织、场景搭建到打包发布的整体流程便于二次开发与学习。 动手做这个Unity 3D射击小游戏起因是朋友让我帮他带一个入门级项目想着干脆从零走一遍完整流程。结果这一走从环境配置、场景搭建到射击手感、敌人AI再到打包发布踩了不少坑也把很多平时不注意的细节摸了个透。索性把这整个过程整理成一篇偏实战的复盘既讲怎么实现也讲为什么这么做给准备用Unity做第一款3D游戏的读者一条能直接照着走的路。这篇内容适合刚学Unity不久、想完整做一个3D小游戏练手的人也适合那种“跟着教程做了一半结果卡在某个报错上”的朋友。我会把项目规划、核心功能实现、性能优化、常见问题都拆开聊代码也会放出来属于那种拿来就能改着玩的程度。1. 项目整体设计与思路拆解1.1 为什么拿Unity做射击小游戏市面上能选的主流引擎其实就那几款Unreal引擎画面确实顶但蓝图加C的体系对新手来说太重了编译一次等半天。Unity的C#脚本系统和组件式开发逻辑对独立开发者特别友好它免费版本对年收入低于一定数额的团队都开放Asset Store里也有海量免费资源从模型到音效到完整项目模板都有。我这次做3D射击刚好能覆盖Unity最核心的几个知识点场景管理、物理射线、简单AI逻辑、UI事件、对象池和性能分析。一个完整的射击小游戏听上去好像不难但真正动手你会发现牵涉的东西其实很杂角色的移动手感、摄像机跟随、射击命中判定、敌人的生成和销毁、计分UI、开枪音效和弹孔特效还要保证游戏不掉帧。把这些模块一个个拆出来再拼回去才是做游戏真正有价值的地方。1.2 核心玩法与模块拆解我给这个小游戏定了一个非常朴素的目标玩家用第一人称视角在地图上移动准星对准不断生成的敌人左键开枪射击敌人被打中扣血血量为零后消失并加分。玩家自己有3条命被敌人碰到就损失一条命三条命耗尽则游戏结束。围绕这个玩法必须拆出下面几块玩家控制器移动、跳跃、视角旋转。摄像机系统第一人称视角跟随玩家头部。射击系统射线检测、命中反馈、枪口特效、音效。敌人系统生成、巡逻或追踪、受击、死亡。UI系统准星、分数、生命值、开始和结束界面。游戏管理状态切换开始、游戏中、结束、分数记录。资源控制用对象池管理子弹特效和敌人实例减少卡顿。这些模块之间尽量不要强耦合比如敌人不直接改UI分数而是通过事件或者找到GameManager来通知加分。Unity里最常见的问题就是图省事把代码互相引用写成一团后续改一个功能崩一片。1.3 工程目录规划我习惯在Assets下按功能建目录而不是按资源类型建。这样每个功能模块的脚本、材质、Prefab都放一起找起来思路清晰。Layout如下目录存放内容Assets/Scenes游戏场景文件Assets/Scripts/Player玩家控制、摄像机脚本Assets/Scripts/Enemy敌人AI、生命值脚本Assets/Scripts/GameGameManager、对象池Assets/Scripts/UI分数、按钮事件绑定脚本Assets/Prefabs敌人、枪口特效、弹孔等预制体Assets/Art/Materials场景地面、枪械材质Assets/Audio开枪、命中、敌人死亡音效这个结构很简单但足够你起步。就算后面项目扩大也只需要继续往里嵌套目录不用推翻重来。2. 环境准备与工具选型2.1 Unity版本选择与安装Unity的版本策略是一个长期支持版LTS每年还会出几个技术版Tech Stream。做小游戏千万别图新鲜装最新技术版很多插件没跟上报错都没地方查。我这次用的是Unity 6 LTS版本它在Windows、Android、iOS多平台的表现都比较稳定安装大小和编译速度也适中。从Unity Hub安装时模块选择那一步特别重要。很多人嫌占空间只勾Unity本体结果发布时发现根本没有对应平台的Build Support又要重新跑一次安装。我建议你至少勾上Windows Build Support (IL2CPP)Android Build Support (如果后续想发安卓)Microsoft Visual Studio Community脚本开发环境安装Completed后在Hub里打开一个3D核心模板项目即可。模板自带简单场景和Shader不用从空场景手搭灯光摄像机能省不少事。2.2 素材与资源获取做射击游戏模型不用自己从头建模新手最忌讳在美术环节死磕。我用的枪械模型和敌人模型都来自Unity Asset Store的免费资源包搜索“FPS Sample”或者“Starter Assets: First Person Controller”就能找到。敌人的外观我更推荐用Unity自带的Capsule形状临时顶替写代码阶段重点关注逻辑等核心玩法跑通了再替换美术素材。MixamoAdobe家的免费动画库可以下载角色跑步、受击、死亡动画而且FBX格式直接拖进Unity就能用对敌人动画来说很方便。音效方面Unity Asset Store里搜“free gun sound”就能拿到开枪和命中的音频注意选择WAV格式不需要额外转码。2.3 项目基础设置在Edit Project Settings Player里有几个选项我建议开启Windows/Mac项目时都确认一下Color Space设为LinearBuild出来的画面颜色才正常默认Gamma会显得偏灰。Static Batching勾选配合合批减少Draw Call。Allow Fullscreen勾掉方便开发时窗口调试。Android平台Build时Minimum API Level我建议提到API 35很多新版Android系统要求应用适配更高的API级别不然可能在真机上出现兼容性提示或闪退。Target API Level同步也调上去Unity新版SDK一般默认就是这个值。3. 核心功能实现与实操要点3.1 第一人称移动控制CharacterController移动方案我选了CharacterController组件而不是刚体Rigidbody原因很简单角色控制需要的是“受控的物理”而不是完整物理模拟。Rigidbody容易因为碰撞、摩擦力导致角色发飘CharacterController自带碰撞和防穿墙机制用来做FPS移动最合适。给玩家角色挂上CharacterController后控制脚本的核心逻辑如下using UnityEngine; public class PlayerController : MonoBehaviour { public float moveSpeed 8f; public float jumpHeight 1.2f; public float gravity -9.81f; private CharacterController controller; private Vector3 velocity; private bool isGrounded; void Start() { controller GetComponentCharacterController(); } void Update() { float horizontal Input.GetAxis(Horizontal); float vertical Input.GetAxis(Vertical); Vector3 move transform.right * horizontal transform.forward * vertical; controller.Move(move * moveSpeed * Time.deltaTime); isGrounded controller.isGrounded; if (isGrounded velocity.y 0) { velocity.y -2f; } if (Input.GetButtonDown(Jump) isGrounded) { velocity.y Mathf.Sqrt(jumpHeight * -2f * gravity); } velocity.y gravity * Time.deltaTime; controller.Move(velocity * Time.deltaTime); } }几个关键点移动要用Time.deltaTime乘一下保证不同帧率下速度一致。跳跃高度和重力加速度的关系用了物理公式v sqrt(2 * g * h)这样调参数时有依据而不是纯乱试。如果出现卡墙角、上下坡卡顿通常是CharacterController的Slope Limit和Step Offset没有调好Slope Limit设到45Step Offset设到0.3左右能明显改善。3.2 摄像机跟随与视角旋转FPS游戏里摄像机就是玩家的“眼睛”不能像第三人称那样用插值慢慢跟必须做到零延迟。最直接的做法是把Camera作为PlayerController对象的子物体挂在头部位置大约y1.6同时脚本里单独处理鼠标旋转视角。视角旋转的实现建议拆成两轴水平旋转作用于玩家整个物体垂直旋转只作用于摄像机。这样角色身体会跟着鼠标左右转但仍然可以朝前方移动不会导致移动方向和目光方向混乱。代码如下using UnityEngine; public class MouseLook : MonoBehaviour { public float mouseSensitivity 100f; private Transform playerBody; private float xRotation 0f; void Start() { playerBody transform.parent; // 玩家根物体 Cursor.lockState CursorLockMode.Locked; } void Update() { float mouseX Input.GetAxis(Mouse X) * mouseSensitivity * Time.deltaTime; float mouseY Input.GetAxis(Mouse Y) * mouseSensitivity * Time.deltaTime; xRotation - mouseY; xRotation Mathf.Clamp(xRotation, -60f, 60f); transform.localRotation Quaternion.Euler(xRotation, 0f, 0f); playerBody.Rotate(Vector3.up * mouseX); } }这里最容易被忽略的是垂直视角的Clamp。如果不限制仰角和俯角玩家的视角会翻过脑后导致画面上下颠倒玩起来非常晕。另外如果项目复杂度上升比如后续要做观察动画、镜头碰撞过场我会直接换Cinemachine插件它是Unity官方免费工具做镜头过渡和跟随都很顺手实测比手写脚本省一多半的时间。3.3 射击系统射线检测与命中反馈射击命中判定我推荐用Physics.Raycast——从摄像机中心发射一条看不见的射线检测它撞到的东西。为什么不用子弹刚体因为FPS里子弹速度极快如果每枪都生成一个物理弹体不仅性能差还容易出现高速穿透检测的问题。射线检测一步到位简单又准确。开枪的触发逻辑放在鼠标左键按下GetButtonDown主相机从屏幕正中心发出一条射线using UnityEngine; public class GunShooter : MonoBehaviour { public Camera playerCamera; public float range 100f; public GameObject muzzleFlashPrefab; public GameObject hitEffectPrefab; public AudioSource gunAudio; void Update() { if (Input.GetButtonDown(Fire1)) { Shoot(); } } void Shoot() { if (muzzleFlashPrefab ! null) { GameObject flash Instantiate(muzzleFlashPrefab, transform.position, transform.rotation); Destroy(flash, 0.1f); } if (gunAudio ! null) { gunAudio.Play(); } Ray ray playerCamera.ScreenPointToRay(new Vector3(Screen.width / 2f, Screen.height / 2f, 0f)); RaycastHit hitInfo; if (Physics.Raycast(ray, out hitInfo, range)) { EnemyHealth enemy hitInfo.collider.GetComponentEnemyHealth(); if (enemy ! null) { enemy.TakeDamage(25); } if (hitEffectPrefab ! null) { GameObject effect Instantiate(hitEffectPrefab, hitInfo.point, Quaternion.LookRotation(hitInfo.normal)); Destroy(effect, 1f); } } } }枪口特效和受击特效我用了Instantiate后定时Destroy的方式但因为特效出得频繁实际项目里为了性能会改成对象池。这个在后面优化章节展开。命中反馈是射击游戏手感的核心。只扣血没反馈玩家会觉得“打在空气上”。所以命中敌人时可以加一个短暂的白闪或弹孔特效最好再来一个“命中准星变红”的UI提示。这几点加完之后整把枪的手感完全不一样。3.4 敌人生成与对象池敌人的基础行为有三步从固定出生点生成自动向玩家方向移动碰到玩家后造成伤害并消失。最简单的AI可以用Vector3.MoveTowards每帧把朝向转向玩家然后移动固定速度using UnityEngine; public class EnemyAI : MonoBehaviour { public float moveSpeed 3f; public float rotateSpeed 5f; private Transform player; void Start() { player GameObject.FindGameObjectWithTag(Player).transform; } void Update() { if (player null) return; Vector3 direction (player.position - transform.position).normalized; Quaternion lookRotation Quaternion.LookRotation(new Vector3(direction.x, 0, direction.z)); transform.rotation Quaternion.Slerp(transform.rotation, lookRotation, Time.deltaTime * rotateSpeed); transform.position Vector3.MoveTowards(transform.position, player.position, moveSpeed * Time.deltaTime); } }敌人生成不能一次性铺太多我用GameManager每隔2秒在四个固定出生点随机选一个生成敌人同时限制场景内敌人数量不超过15个。有一个很容易忽略的细节敌人死亡后不能直接Destroy完事否则会留下未回收的引用和内存碎片。更好的做法是做一个简单的对象池。对象池的思路就是把不用的对象塞回一个队列需要生成时从队列取。因为只是小游戏我用List来手写using System.Collections.Generic; using UnityEngine; public class EnemyPool : MonoBehaviour { public GameObject enemyPrefab; public int poolSize 15; private ListGameObject pool new ListGameObject(); void Start() { for (int i 0; i poolSize; i) { GameObject enemy Instantiate(enemyPrefab); enemy.SetActive(false); pool.Add(enemy); } } public GameObject Spawn(Vector3 position, Quaternion rotation) { foreach (GameObject enemy in pool) { if (!enemy.activeSelf) { enemy.transform.position position; enemy.transform.rotation rotation; enemy.SetActive(true); return enemy; } } return Instantiate(enemyPrefab, position, rotation); } public void Despawn(GameObject enemy) { enemy.SetActive(false); } }这样做的好处非常明显敌人动态生成不会再导致频繁GC垃圾回收手机端运行也不会突然卡一下。实测场景同时存在15个敌人时帧率稳定比Instantiate/Destroy方案高出大约20%。3.5 UI计分与开始结束界面UI这块我用的是Unity自带的UI系统。Canvas选择Screen Space - Overlay模式就行了这种模式UI永远在最顶层渲染不需要处理画面深度关系。分数和生命值各用一个TextMeshProUGUI挂在Canvas下。分数更新逻辑用GameManager统一管理其他模块不直接改UI文本而是调用GameManager的AddScore接口这样后续如果要接排行榜、存档都只需要改GameManager一个类using TMPro; using UnityEngine; using UnityEngine.SceneManagement; public class GameManager : MonoBehaviour { public static GameManager Instance; public int score; public int lives 3; public TextMeshProUGUI scoreText; public TextMeshProUGUI livesText; public GameObject gameOverPanel; void Awake() { Instance this; } public void AddScore(int amount) { score amount; scoreText.text 分数: score; } public void LoseLife() { lives--; livesText.text 生命: lives; if (lives 0) { gameOverPanel.SetActive(true); Time.timeScale 0f; } } public void RestartGame() { Time.timeScale 1f; SceneManager.LoadScene(SceneManager.GetActiveScene().name); } }这里有个新手极易踩的坑当生命归零后如果直接隐藏游戏场景别的功能可能还在跑容易出各种莫名其妙的问题。更稳妥的方案是把Time.timeScale设成0暂停所有依赖deltaTime的逻辑再弹出结束面板。重开时记得把timeScale恢复成1否则会出现点击按钮没反应的情况。开始界面我单独做了一个Canvas只有开始按钮点击后隐藏该Canvas并正式启动游戏逻辑。结束界面上再放一个R重玩按钮实际测试下来手感顺畅。4. 游戏优化与性能调优实操4.1 为什么要做优化帧率就是FPS的命根子FPS游戏对帧率异常敏感帧率低不仅画面卡还直接影响射击手感和玩家判断。我在项目里实际跑一遍后发现不做任何优化时场景里有10个敌人同时活动帧率就开始往下掉到15个时掉到50左右这对射击玩家来说已经很难受了。导致掉帧最直接的因素有三个Draw Call过高渲染批次太多、频繁Instantiate和Destroy造成GC压力、实时阴影开销太大。这三块优化完帧率基本就能稳定了。关于Draw Call新手不用知道实现细节只要理解为“CPU向GPU提交一次渲染指令”即可。提交次数越多越慢所以同样的画面提交1000次和提交100次的帧率差距是天上地下。减Draw Call最有效的手段就是减少材质数量、合并Mesh和启用静态批处理。4.2 对象池与特效回收解决GC痛点这个点我在3.4节提到过这里再单独强调一下射击游戏的命中特效如果每开一枪都Instantiate一个粒子体再在1秒后Destroy那么玩家点几枪就会触发一次垃圾回收而GC发生时游戏会短暂卡顿。高速射击时这种卡顿几乎能让人崩溃。务实方案是把命中特效和枪口火光都做成对象池特效播放完Active设为false过段时间自动回到池子。动态生成的敌人同理。项目做完后我用Profiler对比GC Alloc的内存消耗直接下降约70%帧率曲线平滑了很多。4.3 阴影、灯光与摄像机远裁剪面场景里最耗性能的隐藏杀手是实时阴影。Directional Light的Shadow Type默认是Hard Shadows这会在每帧将阴影信息重新渲染一张深度图开销很大。小游戏玩法又比较紧凑完全没必要追求超远距离的实时阴影我在Project Settings Quality里把Shadow Distance从默认100改到30米再搭配Soft Shadows画面几乎看不出差别但GPU负载明显下降。摄像机远裁剪面Far Clip Plane根据场景大小来设不要习惯性保留默认的1000。这个项目场景范围就60米左右我把Far Clip Plane调到150能省掉大量完全看不见的远景渲染。改动后正好顺便解决了一个“远处敌人偶尔闪现”的问题因为近裁剪面附近的物体被早期剔除掉了。4.4 用Profiler定位性能瓶颈Unity自带的Window Analysis Profiler是排查性能问题的头号工具实测比任何“感觉哪里卡”都靠谱。我优化前先用它录了一段游戏看CPU Usage区域的数据发现PlayerLoop里占大头的是Physics.Simulate和ParticleSystem.Update这基本确认了瓶颈有两个射线检测相关物理开销、粒子特效过多。物理开销是因为敌人碰撞体用了最简单的Capsule Collider粒子开销则是因为特效数量没限制。优化后再次进Profiler看WaitForTargetFPS开始占据CPU时间这说明瓶颈已经不在游戏逻辑而是帧率被上限限制住了游戏流畅度已经没问题。5. 常见问题与排查技巧实录5.1 UI不刷新改内容没反应这个坑我在做计分UI时踩过一次。如果你用了Vertical Layout Group这类自动布局组件脚本里改了Text内容后界面上纹丝不动大概率是布局组件没有标记“脏”Unity不会自动重算位置。解决办法修改完内容后调用LayoutRebuilder.ForceRebuildLayoutImmediate或者检查Vertical Layout Group上是否有Control Child Size、Child Force Expand等选项开得不合适。还有一个更常见的情况TextMeshPro在代码里改了text属性但界面没变化检查你改的是不是复制的临时字符串对象有时候因为缓存了mesh没重建调用一下textMeshPro.text ; textMeshPro.text 新内容;就能刷新出来。5.2 DLL加载失败unable to load DLL slua团队项目里另一个同事把slua插件引用进来后工程直接报DLLNotFoundException: Unable to load DLL slua。这个报错绝大多数不是DLL文件缺失而是架构不匹配。Unity默认按当前平台加载插件如果你的DLL只编译了64位那么Editor64位没问题但打包32位APK时就会报这个错。处理方式在Project窗口选中DLL文件Inspector底部有Platform Settings勾选对应平台并确认左侧的CPU架构和你的目标一致。如果DLL重编译过记得关掉Unity再重新导入一次有时候Unity不会自动刷新本地插件。5.3 阴影闪烁、噪点抖动的视觉效果问题本来好好的阴影调整摄像机角度后出现大面积闪烁抖动这个现象叫Shadow Acne或Shadow Puddle。最常见的原因是Shadow Bias设置过小导致阴影深度比较时因为浮点精度抖动。解决方法是把Project Settings Quality Shadows中的Shadow Bias从默认0.05调大到0.1~0.2如果还有问题就再把Normal Bias稍微调大双管齐下基本能消除。另外如果在场景里看到物体边缘发黑说明Shadow Near Plane Offset设置不当把它从默认0.2调低一点问题就会减轻。5.4 发布打包时的License激活与平台设置很多人装了Unity却卡在启动界面提示No valid Unity Editor license found. Please activate your license.这一般是因为登录和激活没做好。用Unity Hub登录你的Unity账号后在Manage Licenses里添加个人许可证选择“Unity Personal”即可免费激活。如果公司或学校网络封了Unity的激活服务器可以手动下载离线激活文件通过Hub导入完成。打包阶段我还碰到过Android SDK版本问题Unity Hub安装时如果没有单独安装Android SDK构建时它会自动下载但可能受网络影响失败。推荐在Hub的Add Modules里提前装好Android Build Support和对应的SDK、NDK避免发布时手忙脚乱。最后再分享一个实用性技巧我个人的体验是做这种小游戏最容易让游戏“从能玩变好玩”的是那些不写进教程里的小细节。比如射击后让枪口火光有一点点随机偏移角度敌人被打中后出现短暂的后退或硬直帧命中时屏幕边缘轻微变红、准星周围出现十字扩散这些反馈叠加起来玩家的打击感会提升好几个档次。如果后续想继续扩展把敌人换成Mixamo的角色模型和跑步动画加一个类似商城的“选枪”UI滚动列表选皮肤或者用小地图显示敌人位置再往上可以研究Pico 4开发Unity把这套FPS逻辑移植到VR手柄交互里手感会完全不一样。也可以尝试二次开发导出到微信小游戏平台Unity官方提供了WebGL转小游戏的方案只是需要注意内存上限和资源压缩策略。先把手头这版做扎实再一个个功能往上加这条路走下来你会比看十遍教程都学得快。本文还有配套的精品资源点击获取
返回列表