ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unity开放世界生存游戏开发:从零搭建中配机器可跑的最小原型

Unity开放世界生存游戏开发:从零搭建中配机器可跑的最小原型 开篇先给一个判断Unity 做开放世界生存游戏真正的门槛从来不是引擎功能不够而是很多教程一上来就丢给你密密麻麻的源码却不讲清楚“中配机器该怎么搭地形、角色、交互和生存循环”。这篇文章就是为准备入坑开放世界生存游戏、又不想被高配美术资产绑架的开发者准备的。我们会从一个空白 3D 项目开始逐步实现角色移动、第三人称相机、资源采集和物品拾取这一套最小可玩原型同时解释每一步背后的设计选择。很多人以为开放世界就是“地图做得大、场景摆得多”这是最大的误区。如果你的机器只是中等配置首先被卡死的往往不是 CPU而是大量光源、植被和物理组件带来的渲染压力。所以本系列第一篇文章我会刻意绕开复杂贴图和密集植被先用 Unity 自带的基础几何体、地形工具和少量脚本把核心玩法跑通。这既能让开发环境更轻量也能让后续替换美术资产时不被耦合死。1. 这篇文章真正要解决的问题开放世界生存游戏听起来是个“大工程”但多数新手项目失败并不是因为不会写某个功能而是在早期把精力浪费在了表面装饰上。场景铺了几百米却没想清楚玩家进入世界后要做什么模型换了十几个角色却连走路都不顺。这篇文章要解决的是以下三个问题中配电脑上如何搭建一个帧率稳定、可扩展的 3D 生存游戏原型。不追求电影级画质先保证逻辑跑得通。如何用 Unity 自带功能减少重复造轮子。比如 CharacterController 处理角色移动Terrain 工具做地形Raycast 做采集交互。如何把生存游戏最常见的“移动-采集-拾取-背包”循环用干净代码串起来。为后续增加饥饿度、温度、建筑系统打好基础。所以适合读这篇文章的读者不是完全没碰过 Unity 的小白也不是只想要最终资源包的人而是那些已经会新建项目、知道 Inspector 和 Scene 视图大概怎么用但还没有独立完成过一个可玩 Demo 的初学者。如果你正处于这个阶段这篇教程可以帮你把“零散功能”变成“一条顺畅的游戏流程”。2. 开放世界生存游戏的核心概念与适用场景在开始写代码之前先把几个概念说清楚。开放世界和生存游戏是两种设计理念叠加后的产物理解它们各自的约束才知道代码该往哪个方向写。2.1 开放世界的本质空间自由度与内容密度开放世界意味着玩家可以在一个连续的大地图上自由移动不被关卡门、空气墙、强制剧情切分。但在开发层面“自由”是有代价的场景加载策略、物体激活管理、地形范围、碰撞体数量都会影响性能。对中配机器来说做开放世界必须在“内容密度”上做控制。你可以把地图做得很宽但不要遍地都是高精模型和实时灯光。Unity 的 Terrain 系统在处理大面积地形时效率很高配合树上 Simple 顶点着色和树下的 LOD 策略能在不太损失观感的情况下保住性能。2.2 生存游戏的核心有限资源与持续决策生存游戏的本质不是打怪而是让玩家在资源约束下做持续决策。饥饿了要去收集食物天黑前要找到掩体寒冷时要靠近火源。每一个核心机制都在逼着玩家“动起来”。生存循环的基本结构是探索发现资源→ 采集获取资源→ 消耗维持状态→ 被迫再探索。这套循环越紧密玩家就越难停下来。本文先做“探索-采集-收集”这一环消耗部分留到系列下一篇实现。2.3 为什么用 Unity 而不是其他引擎Unity 在这类项目上有三个明显优势资产导入方便FBX、OBJ、Blender 模型都能直接拖入。社区和 Asset Store 生态成熟动画、地图、UI 都能找到现成方案。C# 脚本加上组件化设计很适合“功能拼装”比如给一个石头挂上 ResourceNode 脚本它立刻就能被采集。当然缺点也很明显大场景的渲染管线和资源管理需要开发者自己规划。如果你不主动控制灯光数量、阴影距离、粒子系统密度任何机器都会卡。这就是为什么文章后面第四节开始每个环节都带性能意识。3. 环境准备Unity 安装与项目创建3.1 安装 Unity Hub 与编辑器打开 Unity 官网下载 Unity Hub。安装后建议登录账号并申请一个个人免费许可证。对于学习项目来说个人版足够用发布时会多一个启动画面但不影响功能。编辑器版本选择上我建议优先选择Unity 6之前的 LTS 版本比如 2022 LTS 或 2021 LTS。这类版本经过长期维护Bug 少网络教程也多。如果你已经安装的是 Unity 6也没关系下面用的 API 大都是兼容的。安装组件时注意开发平台勾选 Windows / Mac / Linux。如果不做 VR 或 Web 端不要额外装 Android/iOS 模块白占磁盘空间。文档和语言包按需勾选中文语言包能降低刚开始的阅读成本但引擎界面最好用英文方便查手册。3.2 创建 3D 项目的关键设置打开 Unity Hub点击新建项目选择3DBuilt-In Render Pipeline模板而不是 URP 或 HDRP。原因很简单内置渲染管线在老旧和中配显卡上表现更稳设置项最少对新手最友好。等后续需要高质量渲染再迁移到 URP当前阶段没必要。项目名称建议使用英文比如OpenWorldSurvivalTutorial。Unity 对中文路径兼容性时好时坏但为了后期打包和团队协作尽早养成纯英文路径的习惯否则在 Build 阶段容易踩坑。创建项目后先做三件事打开Edit - Project Settings - Player把 Company Name 和 Product Name 改成自己的项目名避免默认值在打包时出问题。打开File - Build Settings观察当前平台。Windows 的话一般没问题Mac 环境注意 Architecture 选择。调整一次场景视图的 Lighting 设置。如果是旧版本自带默认光源就不用动如果新建项目场景很暗或很亮先到Window - Rendering - Lighting里点击 Generate Lighting让光照贴图烘焙一次。环境准备好的标志是你运行一个只有一个 Cube 的临时场景帧率能在 100 FPS 以上且控制台没有任何红色报错。这一步顺利通过说明版本和驱动没有大问题。4. 核心流程拆解把项目拆成下面几个环节后面每一节都会对应一个可验证的产出。4.1 规划项目文件夹在正式写代码前先建立干净的目录结构。这看起来不起眼但对中后期项目维护影响巨大。建议如下Assets/ ├── Scenes/ // 存放场景文件 ├── Scripts/ // 所有 C# 脚本 │ ├── Player/ │ ├── Camera/ │ ├── Interaction/ │ └── Inventory/ ├── Prefabs/ // 可复用预制体比如资源节点、拾取物 ├── Terrain/ // 地形数据 └── Materials/ // 材质Unity 对中文文件名支持有限建议全部使用英文。后续共享给队友时这个习惯能省掉大量路径冲突问题。4.2 搭建基础场景先在地面放一个 Plane 或 Cube 拉伸为平地再加一个方向光。然后创建一个玩家胶囊体命名为Player。在 Player 上添加 CharacterController 组件这是 Unity 专门用来处理第三人称角色移动的组件自带碰撞和重力逻辑比 Rigidbody 更适合“走路手感”。最容易被忽略的一点是CharacterController 默认的中心点在胶囊体中心而玩家视觉上通常希望中心点在脚底。你可以在 Player 物体下新建一个空物体放在脚底位置命名为GroundCheck后续脚本要用它做接地检测。4.3 确定玩家对象层级结构玩家对象的层级建议这样组织Player ├── CameraRig // 空物体挂相机跟随脚本 │ └── Main Camera // 相机 ├── GroundCheck // 接地检测点 └── Capsule // 胶囊体碰撞或模型相机不要直接塞进 Player 的子节点里否则旋转会出现“翻身”问题。正确做法是让相机作为独立物体或在 Player 下放一个空的 CameraRig通过旋转角度控制视角这也是第三人称游戏的标准写法。4.4 制作资源节点与拾取物用 Cube 缩放成一个树桩或岩石样子的几何体加上碰撞体然后做成 Prefab存到Prefabs/Resources目录。这里我们不要求美术造型只要求它能被射线检测、有血量、能掉落物品。拾取物可以用一个小 Sphere 或者 Cube 做成添加 Sphere Collider 并勾选Is Trigger。这样玩家走过时能触发 OnTriggerEnter而不会产生物理阻挡。4.5 划分脚本职责我不会把所有逻辑都写进一个大而全的 PlayerController 里。项目早期或许方便但一旦要加背包、动画、音效、体力系统这个脚本会膨胀到几千行可维护性很差。本系列采用“组件式”设计PlayerController负责移动、跳跃。ThirdPersonCamera负责相机跟随和视角旋转。ResourceCollector负责射线采集。ResourceNode负责被采集和掉落。PickupItem负责物品拾取。Inventory负责物品存储与统计。每个脚本只关心一件事互相通过组件引用或 Tag 关联。比如 ResourceCollector 只发射线不管射线打中后对方是树桩还是石头ResourceNode 只关心自己被打了几下不关心是谁打的。这个解耦思路是后续扩展生存系统的骨架。5. 完整示例与代码实现现在进入实操。下面的代码基于 Unity 内置渲染管线和 CharacterController版本以实际安装为准重点演示通用思路。5.1 玩家角色控制脚本创建Assets/Scripts/Player/PlayerController.csusing UnityEngine; [RequireComponent(typeof(CharacterController))] public class PlayerController : MonoBehaviour { [Header(移动参数)] public float moveSpeed 5f; public float jumpHeight 2f; public float gravity -9.81f; [Header(接地检测)] public Transform groundCheck; public float groundCheckRadius 0.2f; public LayerMask groundMask; private CharacterController controller; private Vector3 velocity; private bool isGrounded; void Awake() { controller GetComponentCharacterController(); } void Update() { // 1. 接地检测 isGrounded Physics.CheckSphere(groundCheck.position, groundCheckRadius, groundMask); if (isGrounded velocity.y 0) { velocity.y -2f; // 轻微向下让角色贴合地面 } // 2. 读取输入 float x Input.GetAxis(Horizontal); float z Input.GetAxis(Vertical); // 3. 计算移动方向走世界坐标或相对相机方向 Vector3 move (transform.right * x transform.forward * z); // 如果不希望斜向移动速度超过直线速度做归一化 if (move.magnitude 1f) { move.Normalize(); } controller.Move(move * moveSpeed * Time.deltaTime); // 4. 跳跃 if (Input.GetButtonDown(Jump) isGrounded) { velocity.y Mathf.Sqrt(jumpHeight * -2f * gravity); } // 5. 应用重力 velocity.y gravity * Time.deltaTime; controller.Move(velocity * Time.deltaTime); } }关键点解释CharacterController.Move()每帧调用Unity 会处理碰撞和滑动。接地检测必须用 Sphere 或 Ray不能只看controller.isGrounded因为 CharacterController 自带接地检测在爬坡或紧贴墙壁时并不完全可靠。Mathf.Sqrt(jumpHeight * -2f * gravity)是根据目标跳跃高度反推初速度的常用公式避免用固定加速度去猜。如果你想让角色跟随相机方向移动而不是固定在世界坐标系可以先用Transform.EulerAngles获取相机 y 轴旋转再乘以输入向量。这个改动放到后面章节。在 Player 物体上挂好此脚本把 GroundCheck 拖入groundCheck字段将 GroundCheck 的 y 轴放在胶囊体底部边缘并让地面所在层设为groundMask。运行场景后WASD 或方向键就能让胶囊体在地面移动空格跳跃松开后能自然掉落回地面。这是第一个可验证里程碑。5.2 第三人称相机跟随脚本创建Assets/Scripts/Camera/ThirdPersonCamera.csusing UnityEngine; public class ThirdPersonCamera : MonoBehaviour { [Header(目标)] public Transform target; [Header(距离与角度)] public float distance 5f; public float mouseSensitivity 2f; public float minYAngle -20f; public float maxYAngle 60f; [Header(平滑)] public float smoothTime 0.15f; private float currentX 0f; private float currentY 40f; private Vector3 velocity; void LateUpdate() { if (target null) { Debug.LogWarning(ThirdPersonCamera 未指定目标); return; } // 根据鼠标输入累积角度 currentX Input.GetAxis(Mouse X) * mouseSensitivity; currentY - Input.GetAxis(Mouse Y) * mouseSensitivity; currentY Mathf.Clamp(currentY, minYAngle, maxYAngle); // 计算相机相对目标的位置 Quaternion rotation Quaternion.Euler(currentY, currentX, 0); Vector3 desiredPosition target.position rotation * new Vector3(0, 0, -distance); // 平滑跟随视觉效果更稳 transform.position Vector3.SmoothDamp(transform.position, desiredPosition, ref velocity, smoothTime); // 看向目标 transform.LookAt(target.position Vector3.up * 1.5f); } }这段代码解决了两个新手最容易遇到的诡异问题相机直接挂到 Player 子物体上会导致角色转向时相机跟着“甩头”画面眩晕。使用Vector3.Lerp每帧 0.2 的插值当鼠标移动快时会觉得“拖不动”。SmoothDamp 的响应更自然对中配机器上的帧率波动也更宽容。运行后用鼠标移动视角相机会在玩家身后保持一个稳定距离并且不会穿模。要处理穿模可以增加射线检测用来探测相机与目标之间是否有墙体。最简单的方法是把相机位置和目标之间用Physics.Linecast检测如果检测到碰撞体就缩短 distance这个优化留给读者自己扩展。5.3 资源采集系统射线检测与耐久度接下来做一个可交互的资源节点。它要能接收射线记录当前耐久被打到 0 时掉落物品。创建Assets/Scripts/Interaction/ResourceNode.csusing UnityEngine; public class ResourceNode : MonoBehaviour { [Header(耐久)] public int maxHealth 5; public int currentHealth; [Header(掉落物)] public GameObject dropPrefab; public int dropCount 2; public float dropOffsetY 0.5f; void Start() { currentHealth maxHealth; } public void TakeDamage(int amount) { currentHealth - amount; if (currentHealth 0) { SpawnDrops(); Destroy(gameObject); } } private void SpawnDrops() { if (dropPrefab null) { Debug.LogWarning(ResourceNode 未配置掉落物); return; } for (int i 0; i dropCount; i) { // 给掉落物随机偏移避免全堆在一个点 Vector3 randomOffset Random.insideUnitSphere * 0.3f; randomOffset.y dropOffsetY; Instantiate(dropPrefab, transform.position randomOffset, Quaternion.identity); } } }创建Assets/Scripts/Interaction/ResourceCollector.csusing UnityEngine; public class ResourceCollector : MonoBehaviour { [Header(采集距离)] public float range 3f; [Header(每次伤害)] public int damageAmount 1; [Header(特效)] public GameObject hitEffectPrefab; void Update() { // 鼠标左键点击采集 if (Input.GetMouseButtonDown(0)) { TryCollect(); } } private void TryCollect() { Ray ray Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit, range)) { ResourceNode node hit.collider.GetComponentInParentResourceNode(); if (node ! null) { node.TakeDamage(damageAmount); if (hitEffectPrefab ! null) { GameObject effect Instantiate(hitEffectPrefab, hit.point, Quaternion.identity); Destroy(effect, 1f); } Debug.Log(采集进度: node.currentHealth / node.maxHealth); } } } }注意几个细节GetComponentInParent比GetComponent更稳妥。因为资源节点的模型可能是根节点的子物体射线打到的 Collider 挂在不层级的子物体上。特效对象为了避免内存持续膨胀用Destroy(effect, 1f)在 1 秒后自毁。热词里提到的“粒子特效内存泄露”很多时候就是特效挂载在场景里又没有设置自动销毁导致的。这里用的Physics.Raycast没有任何 LayerMask意味着射线可能打到场景里的其他物体。实际项目中建议对可采集物体单独定义一个 Layer然后把 LayerMask 传给 Raycast避免点到地面或天空盒时白白发起一次检测。5.4 拾取物与简单背包资源节点掉出拾取物后玩家走过去收集并记录到背包中。先做拾取物脚本。创建Assets/Scripts/Interaction/PickupItem.csusing UnityEngine; public class PickupItem : MonoBehaviour { [Header(物品配置)] public string itemName 木材; public int amount 1; [Header(拾取检测)] public float pickupRadius 0.5f; public float rotateSpeed 30f; private void Start() { // 自动添加球形触发器方便玩家走过拾取 SphereCollider col gameObject.AddComponentSphereCollider(); col.isTrigger true; col.radius pickupRadius; } void Update() { transform.Rotate(Vector3.up * rotateSpeed * Time.deltaTime); } void OnTriggerEnter(Collider other) { if (other.CompareTag(Player)) { Inventory inventory other.GetComponentInventory(); if (inventory ! null) { inventory.AddItem(itemName, amount); Destroy(gameObject); } } } }创建Assets/Scripts/Inventory/Inventory.csusing System.Collections.Generic; using UnityEngine; public class Inventory : MonoBehaviour { private Dictionarystring, int items new Dictionarystring, int(); public void AddItem(string itemName, int amount) { if (items.ContainsKey(itemName)) { items[itemName] amount; } else { items.Add(itemName, amount); } Debug.Log([背包] itemName amount 当前总数: items[itemName]); } public int GetItemCount(string itemName) { if (items.ContainsKey(itemName)) { return items[itemName]; } return 0; } }这里用Dictionarystring, int做物品栈简单直观。虽然实际项目里会用 ItemData 资产替换字符串作为 key但在第一阶段字符串足够用。如果需要扩展可以把 itemName 改为 Id再用ScriptableObject配置物品属性。把 PickupItem 脚本挂到资源节点配置的 dropPrefab 上再把 Inventory 挂到 Player 物体上。运行后用鼠标左键连续点击树桩打到耐久耗尽地面会出现旋转的小球玩家走到小球附近即可拾取控制台会输出背包日志。5.5 让地形和资源生成更像“开放世界”如果我们只是手动往场景里摆几个 Cube 资源节点离“开放世界”还很远。改进方法有很多第一个阶段推荐用代码生成树木/石头位置避免手工铺场景。创建Assets/Scripts/World/ResourceSpawner.csusing UnityEngine; using System.Collections.Generic; public class ResourceSpawner : MonoBehaviour { [Header(地层区域)] public Bounds spawnArea new Bounds(Vector3.zero, new Vector3(40f, 10f, 40f)); [Header(资源预制体)] public GameObject treePrefab; public GameObject rockPrefab; [Header(数量)] public int treeCount 20; public int rockCount 10; void Start() { SpawnResources(treePrefab, treeCount); SpawnResources(rockPrefab, rockCount); } private void SpawnResources(GameObject prefab, int count) { if (prefab null) return; for (int i 0; i count; i) { Vector3 randomPosition new Vector3( Random.Range(spawnArea.min.x, spawnArea.max.x), 0f, Random.Range(spawnArea.min.z, spawnArea.max.z) ); // 用射线往下寻找地形表面让物体正好插在地面上 if (Physics.Raycast(new Vector3(randomPosition.x, 50f, randomPosition.z), Vector3.down, out RaycastHit hit, 100f)) { randomPosition.y hit.point.y; GameObject go Instantiate(prefab, randomPosition, Quaternion.identity); go.transform.SetParent(transform); } } } }在实际地形场景里高度不是 0所以需要用向下射线检测地表。这样生成的资源点会贴合地形起伏不会悬在半空。需要注意性能如果树或石头的数量很大比如几百个每个都挂 ResourceNode 脚本并做碰撞检测会给物理系统带来压力。中配机器上的优化思路是优先不要给远处的资源节点开启碰撞或者启用 LOD 组和 Culling 距离。这是后面专题再展开的内容。6. 运行结果与效果验证代码写完以后不要直接进入“加功能”环节。先跑一遍完整流程确认以下验证点。6.1 验证操作打开场景点击 Play。WASD 移动角色能走、能跳。鼠标移动视角相机不抖动、不穿模。走到树桩前按住鼠标左键观察控制台日志出现采集进度。继续点击树桩直到耐久归零树桩销毁掉落物出现。操作角色走进拾取物控制台输出[背包] 木材 1。如果第 2 步角色不动优先检查方向键按键是否正确、CharacterController 是否挂载、groundMask是否包含地面所在的层。如果第 3 步视角倾斜或反转检查Mouse Y轴的 Input 设置。Unity 的鼠标 Y 轴默认是向上为正但大部分游戏镜头向下看时鼠标应该往上推。脚本里用的是currentY - Input.GetAxis(Mouse Y)如果这个符号反了把减号改成加号。如果第 4 步点击没反应可能是射线范围太小或者场景里没有 ResourceNode。先把range调大到 5并在 Console 里查看是否有Raycast命中的打印。调试时可以临时在TryCollect里加一句Debug.Log(hit.collider.name)确认点到了哪个物体。如果第 6 步拾取了但是背包没变化检查 Player 上是否挂载 Inventory以及 PickupItem 的OnTriggerEnter是否被触发。对于 CharacterController 控制的物体碰撞检测依赖 CharacterController 的移动如果拾取物只有 SphereCollider 而没有其他碰撞体点击也不会出问题但玩家走过去时 OnTriggerEnter 仍然生效。6.2 帧率验证在统计面板查看帧率打开Window - Analytics - Frame Debugger或者直接在代码里显示1f / Time.deltaTime。中配机器上当前这个最小场景应该保持在 100 FPS 以上。如果只有二三十帧优先检查是否开启了方向光阴影而场景又没有烘焙光照或者是不是有模型导入了顶级的贴图。当前阶段不要急着把大量资源预制体同时摆进场景先跑通 10 个树、5 个石头确认帧率不崩再逐步增加到 50 个、100 个。这样能清楚感知到从哪一刻开始性能跳崖。7. 常见问题与排查思路问题现象可能原因排查方式解决方案角色移动很“漂”像在冰面CharacterController 的Move被多次调用或脚本里没有每帧清零速度检查Update确认 Move 只调用一次观察 Inspector 中速度向量把移动向量归一化计算速度时只累加重力跳跃无效或不触发GroundCheck 位置太高地面 Mask 没包含地面层在场景视图中把 GroundCheck 拖到胶囊体底部检查 Layer重新接地检测点或直接在 Update 里打印isGrounded采集射线打不到资源资源节点没有 Collider射线被其他碰撞体挡住Range 过短用 Debug.DrawRay 画出射线给资源节点添加 BoxCollider设置正确的 LayerMask点击多次才掉耐久鼠标左键点击频率较低或GetMouseButtonDown只触发一次确认是否使用 GetMouseButton考虑用GetMouseButton连续采集改为按住时每帧执行TryCollect并增加冷却时间控制采集频率相机穿模相机与角色之间有墙体但脚本未做射线检测观察相机目标点与角色之间是否有物体在 LateUpdate 做 SphereCast 并动态缩短距离生成资源全部飞在天上ResourceSpawner 下探射线方向不对或地面层没匹配检查生成的 y 值是否异常确认 Down ray 起点足够高在 Start 里输出 hit.point.y如果为 0说明没碰撞到地面粒子特效大量累积特效 Instantiate 后没有销毁看 Hierarchy 中特效对象数量是否持续增长用Destroy(effect, 1f)或者使用协程延迟销毁有一个容易被忽略的坑是 Unity 的 Input 系统。新版本项目默认使用新版 Input System 时会发现Input.GetAxis不可用或一直返回 0。解决方式有两种打开 Player Settings将 Active Input Handling 改为Both或者在项目创建时直接选Input System Package并改用InputAction类。本系列目前采用旧版输入 API所以建议创建项目时保持默认的 Input Manager旧版或改为Both。如果你已经是 Input System 新环境后续代码里控制代码需要适配。8. 最佳实践与工程建议8.1 善用 Layer 管理每个交互物体在 Unity 中创建自定义 Layer比如Environment、Interactable然后所有资源节点的 Collider 统一放到Interactable层。采集脚本的 Raycast 改成int interactableMask 1 LayerMask.NameToLayer(Interactable); Physics.Raycast(ray, out hit, range, interactableMask);这样做的好处是无论场景中将来出现多少装饰物、地面、建筑射线只对交互层生效既提高性能也减少逻辑错误。8.2 所有资源节点统一做成 Prefab场景里的树、石头、矿石都应该做成 Prefab。不要直接拖动场景里的物体复制这样后续改血量、改掉落物时不需要去场景一个个改。把 Prefab 放在 Assets/Prefabs 下命名规范建议Resource_Tree、Resource_Rock、Pickup_Wood。8.3 使用 ScriptableObject 管理物品配置到后期字符串itemName会带来很多麻烦。拼写错误、命名不一致、难以统一显示图标。推荐用 ScriptableObject 表示物品类型[CreateAssetMenu(fileName ItemData, menuName Survival/ItemData)] public class ItemData : ScriptableObject { public string displayName; public Sprite icon; public int maxStack 99; }然后 Inventory 改成存储ItemData到数量的映射。这个改动会带来更好的类型安全但本系列第一篇为了保持门槛低先用字符串。8.4 注意 GameObject 层级和地面检测每个角色的 GroundCheck 位置要和胶囊体的底对齐而不是在胶囊体中心的默认位置。最好在层级结构中新建空物体然后把 y 设为 0.01f 以下。如果需要支持斜坡和楼梯建议把groundCheckRadius调大到 0.25f 或者用多个检测点组合。不要为了省事直接依赖 CharacterController 的isGrounded在地形弯曲时它经常不准。8.5 采集反馈必须有可视信号很多新手脚本里只有 Debug.Log 和耐久度变化没有视觉或音效反馈玩家会觉得“点了一个寂寞”。至少在采集命中一瞬间出示一个短暂的闪白效果。播放一次轻量粒子效果。让资源节点的 Scale 轻微抖动。最简单的抖动模型IEnumerator HitAnimation() { Vector3 originalScale transform.localScale; transform.localScale originalScale * 0.95f; yield return new WaitForSeconds(0.05f); transform.localScale originalScale; }这种小细节会在“手感”和“教程演示效果”上拉开很大差距。8.6 性能优化从第一个原型就开始不要在后续已经堆了几百个物体后才去优化。从代码生成资源后就要监控 Draw Call、三角形数量和物理碰撞体数量。Unity 的 Stats 面板打开快捷键是 CtrlShiftP或菜单 Window - General - Game Stats。这个面板会显示当前帧的三角面数和 Draw Call。对于中配机器所有静态地形和物体尽量标记Static便于合并。方向光阴影距离从默认的 150 降到 30可以显著提升帧率。使用Occlusion Culling时新生成的动态物体不会被剔除所以早期不用开先把场景静态烘焙跑通。8.7 版本管理是开发开放世界项目的基础即使个人开发也推荐从第一天就使用 Git。项目空间里只需提交 Assets 和 ProjectSettings 文件夹Library、Temp、Logs都不要提交。可以在项目根目录加.gitignore网上有现成的 Unity 模板。定好一个“可玩版本”后打 Tag别等到代码跑不起来才回滚。9. 总结与后续学习方向这篇文章带你把一个空的 Unity 3D 项目变成一个有角色移动、第三人称视角、资源采集、物品拾取和背包记录的最小可玩原型。对于中配机器你已经能够评估当前场景的性能表现并且知道从哪里观察 Draw Call 和三角面数。如果这一步你能完整跑通下一个阶段我建议按以下顺序继续推进加入生存基础数值比如饥饿度、口渴度、生命值用一个StatusSystem脚本统一管理。实现日夜循环。用Time.timeScale控制游戏内时间速度切换方向光强度同时可以结合热词里常提到的unity 热力图思路把玩家的活动区域数据可视化帮助自己分析玩家在哪片区域停留时间最久。给资源采集加上工具限制。比如砍树必须拿斧头让玩家制作斧头后才能继续推进这样游戏目标就从“到处乱点”变成了“制定计划”。引入建筑模式。这需要用到预制体网格摆放和地面检测是开放世界生存游戏里最复杂的交互之一也最容易遇到场景性能问题。本系列后续会继续深入地形生成、UI 背包系统、AI 动物与敌人、持久化存档等模块。下一篇会是背包 UI 和数据驱动的物品系统祝跑通顺利。
返回列表