Unity3D俯视角射击游戏实战:从零到一掌握游戏开发核心模块

Unity3D俯视角射击游戏实战:从零到一掌握游戏开发核心模块
1. 项目概述为什么选择射击游戏作为Unity3D的入门实战如果你刚接触Unity3D面对空白的项目窗口和琳琅满目的菜单可能会感到一丝迷茫。网上教程千千万从“Hello World”到复杂的渲染管线究竟从哪里开始才能真正“上手”而不是停留在看懂的层面我的建议是从一个具体的、有明确目标的游戏类型入手而俯视角3D射击游戏就是我心目中那个近乎完美的“新手毕业设计”。为什么是它首先射击游戏的核心循环清晰明了移动、瞄准、射击、受击、得分。这个循环几乎涵盖了游戏开发最基础的几大模块输入控制、物理碰撞、动画状态机、UI交互和简单的游戏逻辑。你不用一开始就纠结于复杂的剧情叙事或庞大的开放世界而是可以专注于如何让一个角色在场景里“动起来”并“打中东西”。其次它的视觉反馈即时且强烈。当你按下按键角色移动点击鼠标子弹飞出并击中目标爆出一团火花——这种即时的正反馈是支撑你度过学习初期枯燥阶段的最佳动力。这个项目实战的目标不是让你做出一个商业级的《使命召唤》而是带你走完一个最小可行游戏产品的完整开发流程。从搭建场景、创建角色、编写核心脚本到处理敌人AI、设计UI界面最后打包发布。你会遇到坑也会填坑这个过程积累的经验远比孤立地学习某个组件要扎实得多。我当年就是靠着一个类似的射击Demo真正理解了GameObject、Component、Prefab这些核心概念是如何在项目中协同工作的。2. 项目核心模块拆解与设计思路一个看似简单的射击游戏背后是由多个相互关联的模块组成的。在动手写代码之前理清这些模块和它们之间的关系能让你在开发过程中不至于迷失方向。我把这个项目的核心分为以下五个部分2.1 玩家控制模块移动与射击的基石这是游戏的“手感”来源直接决定了玩家体验的好坏。对于俯视角游戏我们通常采用“摇杆”式移动配合鼠标瞄准的设计。移动方案选择常见的有两种。一是使用物理引擎Rigidbody通过给角色施加力AddForce来实现移动。这种方式移动感更真实带有惯性但控制起来不够“跟手”对于需要精确走位的射击游戏可能不是最佳选择。二是直接修改Transform的Position也就是所谓的“Character Controller”思路。我们这里采用后者因为它响应更快控制更直接。我们会通过获取水平A/D和垂直W/S输入构建一个移动方向向量然后乘以速度Speed和时间Time.deltaTime来每帧更新位置。瞄准与旋转俯视角的瞄准通常让角色朝向鼠标位置。这里的关键在于将鼠标在屏幕上的坐标转换为游戏世界中的坐标。我们需要用到Camera.main.ScreenToWorldPoint方法但要注意这个方法返回的世界坐标Z轴是相机的Z值我们需要将其Z值设置为角色所在的平面比如地面Y0那么角色坐标的Y值可能为1这里需要根据你的坐标系调整。得到鼠标的世界坐标后计算其与角色位置的向量差并使用Mathf.Atan2函数计算出角度最后将这个角度赋值给角色的旋转Rotation。射击逻辑射击不仅仅是实例化一颗子弹。它涉及1射击点枪口位置2子弹Prefab3射击方向4射击间隔防止连发过快5弹药与装弹逻辑。我们会创建一个子弹的预制体上面挂载一个脚本用于控制飞行和碰撞检测。当玩家按下鼠标左键时在枪口位置实例化一颗子弹并给它一个朝向鼠标方向的初始速度。2.2 敌人AI模块从“木桩”到“追击者”敌人是玩家的互动对象其AI的复杂程度决定了游戏的挑战性。对于新手项目我们实现一个基础的“巡逻-追击-攻击”状态机就足够了。状态设计我们可以定义敌人的几个状态Patrol巡逻、Chase追击、Attack攻击、Dead死亡。使用枚举Enum来管理这些状态非常清晰。巡逻状态敌人在指定点之间来回移动或在一个范围内随机移动。可以设置几个空GameObject作为路径点Waypoint敌人按顺序移动到下一个点。追击状态当玩家进入敌人的“察觉范围”时敌人状态切换为追击。追击的逻辑就是每帧计算自身与玩家的方向然后朝玩家移动。这里可以引入一个“停止距离”比如距离玩家3个单位时就不再靠近准备攻击。攻击状态当敌人与玩家距离小于“攻击距离”且处于追击状态一段时间或直接满足条件后进入攻击状态。攻击可以表现为播放攻击动画、生成一个攻击碰撞体、或像玩家一样发射子弹。攻击后应有冷却时间。视觉与听觉感知最简单的感知是用物理触发器Trigger Collider。给敌人附加一个球体碰撞体作为“视觉范围”当玩家进入时触发OnTriggerEnter开始追击。更高级一点可以加入射线检测Raycast只有“看到”玩家时才追击这样敌人就不会隔墙追你了。2.3 战斗与生命值模块伤害与反馈的核心战斗系统是游戏循环的驱动。核心是生命值Health组件和伤害Damage处理。生命值组件这是一个可以挂在玩家和敌人身上的通用脚本。它包含一个currentHealth变量和一个maxHealth变量。提供TakeDamage(int damage)公共方法当被调用时减少当前生命值并触发受伤效果如屏幕闪红、播放音效。如果生命值归零则调用Die()方法处理死亡逻辑播放死亡动画、销毁对象、触发得分等。伤害传递如何判断谁打中了谁通常通过碰撞检测和标签Tag或层级Layer来实现。在子弹的脚本中在OnCollisionEnter或OnTriggerEnter方法中检测碰撞到的物体。如果碰撞体有“Enemy”标签就获取该物体上的Health组件并调用其TakeDamage方法。同理敌人的攻击碰到玩家标签时也对玩家造成伤害。反馈的重要性单纯的数字减少很无趣。必须加入丰富的反馈击中敌人时的喷血粒子效果、音效、屏幕震动Camera Shake、敌人受击的硬直动画Hit Reaction、玩家受伤时的画面特效Post-processing Vignette或Color Grading变红等。这些反馈是游戏“爽感”的直接来源。2.4 UI与游戏状态管理模块信息的桥梁UI是玩家与游戏世界信息交互的窗口。对于射击游戏核心UI包括血量条、分数/弹药显示、准星、游戏开始/结束界面。血量条的实现通常使用Unity的UI Slider组件。将Slider的maxValue设置为最大生命值value设置为当前生命值。在玩家的Health脚本中每当生命值变化时就更新对应Slider的value。敌人的血条则复杂一些通常使用世界空间World Space的UI挂在敌人头顶并让血条始终面向相机Billboard。游戏状态管理这是一个常被新手忽略但至关重要的模块。我们需要一个全局的“游戏管理器”GameManager单例来管理游戏的整体状态如当前分数、游戏是否进行中、玩家是否死亡、关卡逻辑等。单例模式确保GameManager在场景中只有一个实例并且可以方便地从任何脚本访问。例如GameManager.Instance.AddScore(100)。状态控制它负责在玩家死亡时弹出“游戏结束”UI在点击“重新开始”按钮时重置场景。这避免了游戏逻辑散落在各个角色脚本中使架构更清晰。2.5 场景、音效与粒子效果模块氛围的营造者这是让游戏从“功能原型”变成“可玩作品”的关键一步。场景搭建技巧不要在一个巨大的平面上放几个方块就了事。利用Unity的ProBuilder工具快速搭建一些简单的掩体如箱子、矮墙或者从Asset Store下载一些免费的模块化资源包。灯光Lighting非常重要即使是简单的场景加上一个方向光Directional Light并调整好角度和强度再烘焙一下光照Bake Lighting画面质感会立刻提升。后处理Post Processing也能极大改善视觉效果加上一点环境光遮蔽Ambient Occlusion、泛光Bloom和色彩校正Color Grading游戏的画面会显得更专业。音效系统为不同的动作添加音效移动脚步声、射击声、子弹击中声、敌人受伤呻吟声、爆炸声、背景音乐。使用Unity的AudioSource组件和AudioClip。一个最佳实践是使用对象池Object Pool来管理频繁播放的音效如射击声避免频繁实例化销毁造成的性能开销。粒子效果Unity的粒子系统Particle System非常强大。为射击制作枪口火焰、为击中制作火花或血雾、为爆炸制作烟雾和碎片。粒子效果是视觉反馈的灵魂。你不需要自己从头调Asset Store上有大量免费或付费的粒子效果包如“Unity Particle Pack”可以直接拿来使用和学习。3. 分步实战从零搭建你的射击游戏理论说再多不如动手做一遍。下面我们开始一步步实现核心功能。我会假设你已经有Unity Hub和Unity Editor建议2021 LTS或更新版本的基础环境。3.1 第一步项目初始化与场景搭建创建新项目打开Unity Hub新建一个3D项目Core或URP模板均可URP画面效果更现代。给项目起个名字比如“TopDownShooterTutorial”。组织项目文件夹在Project窗口创建清晰的文件夹结构这是一个好习惯。我通常创建_Scripts,_Prefabs,_Scenes,_Materials,_Models,_Audio,_Particles,_UI。下划线让这些文件夹排在前面。搭建基础场景删除默认的SampleScene新建一个场景保存为Main到_Scenes文件夹。在Hierarchy中创建一个Plane命名为Ground作为地面。给它一个材质比如灰绿色。创建一个Directional Light调整角度让场景有明暗对比。创建几个Cube缩放后作为场景中的掩体或障碍物摆放在地面上。最后调整Main Camera的位置和角度使其呈现一个经典的俯视角比如Position (0, 15, -10), Rotation (45, 0, 0)。3.2 第二步创建玩家角色与移动控制创建玩家对象在场景中创建一个Capsule胶囊体命名为Player。重置其Transform。这将是我们的玩家角色。你可以从Asset Store下载一个免费的低多边形Low Poly士兵模型替换它但胶囊体在开发阶段完全够用。编写移动脚本在_Scripts文件夹下创建C#脚本PlayerMovement挂载到Player上。using UnityEngine; public class PlayerMovement : MonoBehaviour { public float moveSpeed 5f; // 移动速度 public Rigidbody rb; // 可选如果使用物理移动 public Camera cam; // 主相机用于鼠标瞄准 private Vector3 movement; // 存储每帧的移动输入 private Plane groundPlane new Plane(Vector3.up, Vector3.zero); // 假设地面是Y0的平面 private float rayDistance; void Start() { // 如果Start时没赋值尝试获取 if (rb null) rb GetComponentRigidbody(); if (cam null) cam Camera.main; } void Update() { // 处理移动输入基于Transform float horizontal Input.GetAxisRaw(Horizontal); // A/D 或 左右箭头 float vertical Input.GetAxisRaw(Vertical); // W/S 或 上下箭头 movement new Vector3(horizontal, 0f, vertical).normalized; // 归一化防止斜向移动更快 // 处理鼠标瞄准让角色朝向鼠标 HandleRotation(); } void FixedUpdate() { // 在FixedUpdate中执行物理移动更平滑 if (rb ! null) { // 使用物理移动带惯性 rb.MovePosition(rb.position movement * moveSpeed * Time.fixedDeltaTime); } else { // 直接修改位置无惯性响应更快 transform.Translate(movement * moveSpeed * Time.deltaTime, Space.World); } } void HandleRotation() { // 创建一条从相机出发穿过鼠标位置的射线 Ray ray cam.ScreenPointToRay(Input.mousePosition); // 假设地面是一个平面Y0计算射线与地面的交点 if (groundPlane.Raycast(ray, out rayDistance)) { Vector3 pointToLook ray.GetPoint(rayDistance); // 让角色在X-Z平面旋转看向目标点保持Y轴不变俯视角 transform.LookAt(new Vector3(pointToLook.x, transform.position.y, pointToLook.z)); } } }注意这里我提供了两种移动方式。通过注释掉FixedUpdate中的rb.MovePosition或transform.Translate你可以切换体验。对于快节奏射击游戏我更喜欢直接修改Transform因为它更“跟手”。HandleRotation方法让角色始终面朝鼠标这是俯视角射击的核心感觉。3.3 第三步实现射击与子弹系统创建子弹预制体在场景中创建一个Sphere或Capsule缩小尺寸如Scale 0.2命名为Bullet。添加一个Rigidbody组件取消勾选Use Gravity无重力勾选Is Kinematic我们不希望物理引擎干扰其直线运动或者不勾选用力来驱动。添加一个碰撞体如Sphere Collider。编写子弹脚本创建脚本Bullet并挂载。using UnityEngine; public class Bullet : MonoBehaviour { public float speed 20f; public int damage 10; public float lifeTime 2f; // 子弹存活时间避免一直飞行 public GameObject hitEffect; // 击中特效预制体 private Rigidbody rb; void Start() { rb GetComponentRigidbody(); // 给子弹一个向前的力 rb.AddForce(transform.forward * speed, ForceMode.VelocityChange); // 一定时间后自动销毁 Destroy(gameObject, lifeTime); } void OnTriggerEnter(Collider other) { // 避免与发射者、其他子弹碰撞 if (other.CompareTag(Player) || other.CompareTag(Bullet)) return; // 实例化击中特效 if (hitEffect ! null) { Instantiate(hitEffect, transform.position, Quaternion.identity); } // 尝试对敌人造成伤害 EnemyHealth enemyHealth other.GetComponentEnemyHealth(); if (enemyHealth ! null) { enemyHealth.TakeDamage(damage); } // 击中任何物体后销毁子弹 Destroy(gameObject); } }完善玩家射击修改或创建PlayerShooting脚本。using UnityEngine; public class PlayerShooting : MonoBehaviour { public GameObject bulletPrefab; public Transform firePoint; // 枪口位置可以创建一个空子物体放在角色前方 public float fireRate 0.2f; // 射击间隔 private float nextFireTime 0f; void Update() { if (Input.GetButton(Fire1) Time.time nextFireTime) // Fire1 默认为鼠标左键 { Shoot(); nextFireTime Time.time fireRate; } } void Shoot() { // 实例化子弹 GameObject bullet Instantiate(bulletPrefab, firePoint.position, firePoint.rotation); // 可以在这里播放射击音效 // AudioSource.PlayClipAtPoint(shootSound, firePoint.position); } }配置将Bullet对象从场景拖入Project窗口的_Prefabs文件夹使其成为预制体。然后从场景中删除这个实例。在Player上挂载PlayerShooting脚本将子弹预制体拖拽赋值给bulletPrefab创建一个空子物体命名为FirePoint放在胶囊体前方并将其拖拽赋值给firePoint。3.4 第四步创建敌人AI与生命值系统创建敌人预制体类似玩家创建一个胶囊体命名为Enemy赋予红色材质以便区分。为其添加Rigidbody和碰撞体。编写通用生命值脚本创建Health脚本它可以同时用于玩家和敌人。using UnityEngine; using UnityEngine.Events; // 用于事件 public class Health : MonoBehaviour { public int maxHealth 100; public int currentHealth; public UnityEvent onDamaged; // 受伤时触发的事件 public UnityEvent onDie; // 死亡时触发的事件 public HealthBar healthBar; // 可选的UI血条引用 void Start() { currentHealth maxHealth; if (healthBar ! null) { healthBar.SetMaxHealth(maxHealth); } } public void TakeDamage(int damage) { currentHealth - damage; currentHealth Mathf.Clamp(currentHealth, 0, maxHealth); // 更新UI血条 if (healthBar ! null) { healthBar.SetHealth(currentHealth); } // 触发受伤事件可用于播放受伤动画、音效等 onDamaged.Invoke(); if (currentHealth 0) { Die(); } } void Die() { // 触发死亡事件可用于播放死亡动画、增加分数等 onDie.Invoke(); // 销毁对象或设置为非激活 Destroy(gameObject); // 或者gameObject.SetActive(false); // 用于对象池 } }编写敌人AI脚本创建EnemyAI脚本实现简单的巡逻追击逻辑。using UnityEngine; using UnityEngine.AI; // 使用导航系统 public class EnemyAI : MonoBehaviour { public Transform player; public float sightRange 10f; public float attackRange 3f; public Transform[] patrolPoints; private int currentPatrolIndex 0; private NavMeshAgent agent; private Health health; private bool isDead false; void Start() { agent GetComponentNavMeshAgent(); health GetComponentHealth(); if (player null) player GameObject.FindGameObjectWithTag(Player).transform; // 监听死亡事件 health.onDie.AddListener(OnEnemyDie); } void Update() { if (isDead) return; float distanceToPlayer Vector3.Distance(transform.position, player.position); if (distanceToPlayer sightRange distanceToPlayer attackRange) { // 追击玩家 ChasePlayer(); } else if (distanceToPlayer attackRange) { // 进入攻击范围停止移动并攻击 agent.isStopped true; AttackPlayer(); } else { // 巡逻 Patrol(); } } void ChasePlayer() { agent.isStopped false; agent.SetDestination(player.position); } void Patrol() { agent.isStopped false; if (patrolPoints.Length 0) return; // 如果到达当前巡逻点前往下一个 if (!agent.pathPending agent.remainingDistance 0.5f) { currentPatrolIndex (currentPatrolIndex 1) % patrolPoints.Length; } agent.SetDestination(patrolPoints[currentPatrolIndex].position); } void AttackPlayer() { // 这里实现攻击逻辑例如播放动画、调用玩家受伤 // 例如player.GetComponentHealth().TakeDamage(10); Debug.Log(Enemy Attacking!); // 可以在这里设置攻击冷却计时器 } void OnEnemyDie() { isDead true; agent.isStopped true; // 可以播放死亡动画然后由Health脚本销毁 Debug.Log(gameObject.name died!); } // 在Scene视图中绘制可视化范围便于调试 void OnDrawGizmosSelected() { Gizmos.color Color.red; Gizmos.DrawWireSphere(transform.position, attackRange); Gizmos.color Color.yellow; Gizmos.DrawWireSphere(transform.position, sightRange); } }配置导航网格要让NavMeshAgent工作需要为场景烘焙导航网格NavMesh。在Window - AI - Navigation 打开导航窗口。在Bake页签下确保你的地面Ground和障碍物Walkable的Navigation Static被勾选在Inspector窗口右上角。然后点击Bake按钮。烘焙后场景中可行走的区域会显示为蓝色。组装敌人将Health和EnemyAI脚本挂到敌人预制体上。给敌人添加NavMeshAgent组件。在场景中创建几个空物体作为巡逻点Patrol Points拖拽赋值给EnemyAI脚本的数组。给玩家对象打上Player标签并在敌人的player字段中赋值。将敌人预制体拖入场景多个进行测试。3.5 第五步集成UI与游戏管理创建玩家血条在Canvas下创建UI - Slider重命名为PlayerHealthBar。将其Rotation改为(90,0,0)Rect Transform的Anchor设置为Bottom并调整宽度和位置使其在屏幕底部居中。将Slider的Max Value设置为玩家最大生命值如100并取消勾选Interactable。创建脚本HealthBar挂载到Slider上。using UnityEngine; using UnityEngine.UI; public class HealthBar : MonoBehaviour { public Slider slider; public void SetMaxHealth(int health) { slider.maxValue health; slider.value health; } public void SetHealth(int health) { slider.value health; } }* 将PlayerHealthBar Slider自身拖拽赋值给脚本的slider字段。然后在玩家的Health组件上将healthBar字段指向这个HealthBar脚本组件。创建分数显示在Canvas下创建UI - Text - TextMeshPro如果未导入TMPUnity会提示导入重命名为ScoreText。编写一个简单的GameManager单例来管理分数。using UnityEngine; using TMPro; public class GameManager : MonoBehaviour { public static GameManager Instance; // 单例实例 public TextMeshProUGUI scoreText; private int currentScore 0; void Awake() { if (Instance null) { Instance this; DontDestroyOnLoad(gameObject); // 跨场景不销毁 } else { Destroy(gameObject); } } void Start() { UpdateScoreUI(); } public void AddScore(int points) { currentScore points; UpdateScoreUI(); } void UpdateScoreUI() { if (scoreText ! null) scoreText.text Score: currentScore.ToString(); } }* 在场景中创建一个空GameObject命名为GameManager挂载此脚本。将UI上的ScoreText对象拖拽赋值。 * 在敌人的Health脚本的Die方法中或者在子弹击中敌人造成死亡时调用GameManager.Instance.AddScore(100);来增加分数。游戏结束逻辑在GameManager中增加游戏状态管理。// 在GameManager中补充 public GameObject gameOverPanel; // 游戏结束UI面板 public void GameOver() { // 暂停游戏时间 Time.timeScale 0f; // 显示游戏结束UI if (gameOverPanel ! null) gameOverPanel.SetActive(true); // 可以在这里禁用玩家输入等 }* 在Canvas下创建一个Panel作为gameOverPanel上面可以放“Game Over”文字和“Restart”按钮。初始设置为SetActive(false)。 * 在玩家的Health脚本的Die方法中调用GameManager.Instance.GameOver();。4. 性能优化、调试与发布前检查当核心功能都跑通后我们需要让游戏更流畅、更稳定并最终打包成可执行文件。4.1 性能优化要点对象池管理子弹和敌人频繁的Instantiate和Destroy是性能杀手。对于子弹、敌人、特效这类频繁生成销毁的对象一定要使用对象池Object Pool。Unity官方现在有ObjectPool类UnityEngine.Pool使用起来很方便。基本原理是游戏开始时预先创建一定数量的对象并禁用需要时从池中取出激活用完后再放回池中禁用而不是销毁。合理使用碰撞层Layer为玩家、敌人、子弹、环境等设置不同的物理层并在Edit - Project Settings - Physics中设置层之间的碰撞矩阵。例如子弹不需要和环境物体碰撞敌人之间不需要互相碰撞。这能显著减少不必要的物理计算。优化Draw Call合并使用相同材质的静态物体Static Batching。对于大量重复的物体如草地、石子考虑使用GPU Instancing。减少场景中实时光源的数量多使用烘焙光照Baked Lighting。代码优化避免在Update中做昂贵的操作如FindGameObjectWithTag、GetComponent。在Start或Awake中缓存引用。使用Coroutine协程来处理延时逻辑而不是在Update里用计时器。4.2 常见问题与调试技巧敌人不移动或导航出错检查是否烘焙了NavMesh敌人所在的区域是否在蓝色导航网格内敌人是否添加了NavMeshAgent组件调试在EnemyAI脚本的OnDrawGizmosSelected中绘制了视觉和攻击范围在Scene视图可以直观看到。检查agent.path.status是否为NavMeshPathStatus.PathComplete。子弹穿透敌人或碰撞检测失败检查子弹和敌人的碰撞体Collider是否启用是否至少有一个物体有Rigidbody如果是触发器碰撞至少一个要有Rigidbody碰撞双方的Layer是否被碰撞矩阵屏蔽调试在子弹的OnTriggerEnter方法里用Debug.Log(other.name)打印碰撞到的物体名看是否被触发。游戏感觉“卡顿”或输入延迟检查移动和旋转逻辑是否写在Update里而物理移动写在FixedUpdate里FixedUpdate的默认频率是0.02秒50Hz如果Update中处理输入FixedUpdate中处理移动可能会感觉延迟。可以将所有移动逻辑都放在Update中但使用Time.deltaTime。或者确保输入采集也在FixedUpdate中。使用ProfilerWindow - Analysis - Profiler。运行游戏查看CPU和GPU的占用情况找到性能瓶颈通常是过多的Draw Call或某个脚本的Update耗时过长。UI显示不正常检查Canvas的渲染模式是否正确Screen Space - Overlay 最常用Rect Transform的锚点Anchors和轴心Pivot设置是否合理血条如果是World Space是否设置了正确的Camera4.3 打包发布与后续扩展构建设置Build SettingsFile - Build Settings。将你的主场景拖入Scenes In Build列表。选择目标平台PC, Mac Linux Standalone。点击Player Settings可以设置公司名、产品名、图标、分辨率等。发布前测试务必在Development Build模式下打一个包并勾选Autoconnect Profiler和Script Debugging。这样你可以在打包后的可执行文件中使用Profiler和Console查看日志进行真机或脱离编辑器测试。后续扩展方向增加更多武器创建Weapon基类派生Pistol,Rifle,Shotgun等管理射速、伤害、弹道、换弹逻辑。设计关卡创建多个场景通过触发器或目标如消灭所有敌人来加载下一关。在GameManager中管理关卡进度。加入音效和背景音乐为每一个动作配上合适的音效背景音乐可以随游戏状态平静、战斗切换。完善敌人AI增加更多状态如寻找掩体、逃跑使用更先进的寻路算法或者引入行为树Behavior Tree插件。添加本地化如果你的游戏有文本可以考虑使用Unity的Localization包来支持多语言。这个项目就像一块坚实的跳板。当你完成了它就意味着你已经掌握了Unity3D开发中最常用、最核心的那20%的知识足以支撑你去探索另外80%更广阔的世界。接下来无论是学习更复杂的动画系统、网络多人游戏、还是转向URP/HDRP渲染管线你都有了可以参照和修改的原始代码库。最重要的是你拥有了一个“从零到一”的完整项目经验这份经验在理解任何新知识时都会给你提供一个坚实的上下文。动手去改去加新功能去把它变成你自己的作品这才是学习的真正开始。