ARTICLE DETAIL

资讯详情

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

Unity游戏开发入门:五大核心代码模块构建可交互Demo

Unity游戏开发入门:五大核心代码模块构建可交互Demo 1. 项目概述从“Hello World”到可运行的Demo“基本的 Unity 游戏开发代码示例”这个标题听起来简单但背后藏着几乎所有Unity新手都会经历的那个“从零到一”的挣扎期。你可能刚装好Unity看着空荡荡的场景视图和项目面板满脑子都是“我该从哪里写第一行代码”或者“为什么我的小球掉不下去”。网上的教程要么太零散要么一上来就是复杂的系统设计让人望而却步。这篇内容就是为你解决这个问题的。它不是一份API文档的罗列也不是一个庞大项目的源码剖析而是一份**“手把手”的实操指南**旨在通过几个最核心、最基础的代码片段帮你快速搭建起对Unity脚本编程的直观理解。我们会从创建一个会动的立方体开始逐步深入到角色控制、物理交互、UI反馈和简单的游戏逻辑。目标是让你在半小时内看到一个由自己代码驱动的、有交互、有反馈的微型游戏原型。无论你是编程新手还是从其他引擎转过来的开发者这些基础代码都是你构建更复杂游戏的“积木块”。理解了它们你就能读懂更高级的教程并开始将自己的想法转化为可运行的交互体验。2. 核心模块拆解五大基础代码块构建游戏雏形一个最基本的可交互游戏Demo通常包含几个不可或缺的模块物体移动、玩家输入、物理模拟、状态管理和用户界面。我们将围绕这五个核心拆解出对应的基础代码示例。2.1 模块一物体移动与旋转——让场景“活”起来任何游戏的动态都始于物体的运动。在Unity中让一个物体比如一个Cube动起来最直接的方式就是通过脚本修改它的Transform组件属性。核心代码示例基础移动与旋转using UnityEngine; public class BasicMovement : MonoBehaviour { // 定义移动速度和旋转速度方便在Inspector面板中调整 public float moveSpeed 5.0f; public float rotateSpeed 90.0f; // 度/秒 void Update() { // 1. 移动每帧沿Z轴正方向移动 // Time.deltaTime是关键它使得移动速度与帧率无关确保在不同性能的电脑上移动一致。 transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime); // 2. 旋转每帧绕Y轴旋转 transform.Rotate(Vector3.up * rotateSpeed * Time.deltaTime); } }代码解析与注意事项Update()方法这是Unity的每帧回调函数。所有需要持续、每帧执行的逻辑如移动、检测输入都应放在这里。Time.deltaTime这是本帧与上一帧的时间间隔以秒为单位。将其与速度相乘意味着“每秒移动5个单位”而不是“每帧移动5个单位”。忽略它会导致游戏在高帧率电脑上“飞起”在低帧率电脑上“慢放”。Transform.Translate与Transform.Rotate这是改变物体位置和旋转最直接的方法。Vector3.forward等于(0, 0, 1)代表物体的局部坐标系前向。注意Translate和Rotate默认使用物体的局部坐标系。如果你想让物体始终向世界坐标系的Z轴移动需要改为transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime, Space.World);。扩展使用插值实现平滑移动直接设置transform.position会导致移动生硬。更平滑的做法是使用线性插值Lerp或球形插值Slerp。public class SmoothMovement : MonoBehaviour { public Transform target; // 目标位置 public float smoothTime 0.3f; // 平滑时间越小越快 private Vector3 velocity Vector3.zero; // 当前速度由SmoothDamp函数内部使用 void Update() { if (target ! null) { // Vector3.SmoothDamp 会计算出平滑过渡的位置 transform.position Vector3.SmoothDamp(transform.position, target.position, ref velocity, smoothTime); } } }2.2 模块二玩家输入处理——建立与游戏的桥梁没有输入游戏就是一段动画。Unity的输入系统让我们可以方便地捕获键盘、鼠标、手柄等设备的事件。核心代码示例键盘与鼠标输入using UnityEngine; public class PlayerInputController : MonoBehaviour { public float moveSpeed 10.0f; public float mouseSensitivity 2.0f; private float _rotationX 0; void Update() { HandleKeyboardMovement(); HandleMouseLook(); } void HandleKeyboardMovement() { // 获取WASD或方向键的输入值范围在[-1, 1]之间 float horizontalInput Input.GetAxis(Horizontal); // A/D 或 LeftArrow/RightArrow float verticalInput Input.GetAxis(Vertical); // W/S 或 UpArrow/DownArrow // 计算移动方向向量 Vector3 moveDirection new Vector3(horizontalInput, 0, verticalInput).normalized; // 将移动方向从局部坐标系转换到世界坐标系确保按W是向前摄像机朝向 moveDirection transform.TransformDirection(moveDirection); // 应用移动 transform.position moveDirection * moveSpeed * Time.deltaTime; } void HandleMouseLook() { // 获取鼠标在X和Y轴上的移动增量 float mouseX Input.GetAxis(Mouse X) * mouseSensitivity; float mouseY Input.GetAxis(Mouse Y) * mouseSensitivity; // 水平旋转左右看绕世界Y轴旋转物体 transform.Rotate(Vector3.up * mouseX); // 垂直旋转上下看通常只旋转摄像机而不是整个角色避免翻转 // 这里我们假设这个脚本挂在摄像机或角色的“头部”物体上 _rotationX - mouseY; _rotationX Mathf.Clamp(_rotationX, -90f, 90f); // 限制上下视角范围避免脖子“折断” transform.localRotation Quaternion.Euler(_rotationX, transform.localEulerAngles.y, 0); } void HandleActionInput() { // 检测单次按下事件 if (Input.GetKeyDown(KeyCode.Space)) { Debug.Log(空格键被按下触发跳跃动作。); // 这里可以调用跳跃方法 } // 检测持续按住事件 if (Input.GetMouseButton(0)) // 0-左键1-右键2-中键 { Debug.Log(鼠标左键被按住可能是在发射子弹或攻击。); } // 检测松开事件 if (Input.GetMouseButtonUp(1)) { Debug.Log(鼠标右键已松开。); } } }输入系统要点解析Input.GetAxis用于获取平滑的输入值非常适合移动控制。输入名称“Horizontal”和“Vertical”在Unity的Edit - Project Settings - Input Manager中预定义你可以修改按键、灵敏度等。Input.GetKeyDown/GetMouseButtonDown只在按键被按下的那一帧返回true用于触发一次性的动作如跳跃、射击、交互。Input.GetKey/GetMouseButton在按键被按住的每一帧都返回true用于持续性的动作如连发、蓄力。鼠标视角控制这是一个简单的第一人称视角控制逻辑。关键在于将垂直旋转mouseY应用在局部X轴上并加以角度限制Mathf.Clamp防止视角上下无限翻转造成体验不适。2.3 模块三物理交互与碰撞检测——引入真实感纯坐标移动缺乏真实世界的交互感。Unity内置的PhysX物理引擎通过Rigidbody组件和碰撞器可以轻松模拟重力、碰撞、力等效果。核心代码示例刚体移动与碰撞响应using UnityEngine; [RequireComponent(typeof(Rigidbody))] // 确保挂载此脚本的GameObject一定有Rigidbody组件 public class PhysicsBasedMovement : MonoBehaviour { public float moveForce 500f; public float jumpForce 5f; private Rigidbody _rb; private bool _isGrounded false; void Start() { // 在Start中获取组件引用比在Update中每次获取更高效 _rb GetComponentRigidbody(); } void Update() { // 输入检测仍在Update中进行因为它是每帧精确检测 float horizontalInput Input.GetAxis(Horizontal); float verticalInput Input.GetAxis(Vertical); // 计算一个方向上的力 Vector3 forceDirection new Vector3(horizontalInput, 0, verticalInput); // 对刚体施加力ForceMode.Force表示添加一个持续的力 _rb.AddForce(forceDirection * moveForce * Time.deltaTime); if (_isGrounded Input.GetKeyDown(KeyCode.Space)) { // 跳跃施加一个瞬间的冲量 _rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); _isGrounded false; } } // 碰撞检测方法 void OnCollisionEnter(Collision collision) { // 当发生碰撞时调用 // 简单判断如果碰撞体是“地面”层则认为着地 if (collision.gameObject.CompareTag(Ground)) { _isGrounded true; Debug.Log(碰到地面了); } } void OnCollisionExit(Collision collision) { // 当碰撞结束时调用 if (collision.gameObject.CompareTag(Ground)) { _isGrounded false; } } // 触发器检测方法碰撞体需勾选Is Trigger void OnTriggerEnter(Collider other) { if (other.CompareTag(Pickup)) { Debug.Log(拾取了物品 other.name); Destroy(other.gameObject); // 销毁被拾取的物品 // 这里可以触发加分、播放音效等逻辑 } } }物理交互核心要点RigidbodyvsTransform如果要使用物理引擎重力、碰撞、力就必须通过Rigidbody组件来移动物体使用AddForce、velocity等切忌在拥有Rigidbody的物体上直接修改transform.position这会导致物理模拟错乱。ForceModeForceMode.Force持续力受质量影响和ForceMode.Impulse瞬间冲量受质量影响是最常用的两种。Acceleration和VelocityChange则忽略质量。碰撞Collision与触发Trigger碰撞两个物体都有Collider且至少一个带有Rigidbody且都不勾选Is Trigger。会发生物理阻挡并调用OnCollisionXXX系列方法。触发至少一个碰撞体勾选了Is Trigger。物体将相互穿透不会发生物理阻挡但会调用OnTriggerXXX系列方法。常用于拾取物、区域检测等。CompareTag判断物体标签的方法比collision.gameObject.tag Ground更高效是Unity推荐的做法。2.4 模块四UI交互与游戏状态反馈——连接玩家与数据游戏状态如分数、生命值需要通过UI实时反馈给玩家。Unity的UI系统UGUI与代码的绑定非常直观。核心代码示例分数显示与按钮交互using UnityEngine; using UnityEngine.UI; // 必须引入UI命名空间 public class GameManager : MonoBehaviour { // 单例模式方便全局访问 public static GameManager Instance; public int playerScore 0; public Text scoreText; // 在Inspector中拖入UI Text组件 public Button restartButton; // 在Inspector中拖入UI Button组件 public GameObject gameOverPanel; // 在Inspector中拖入代表游戏结束的UI面板 void Awake() { if (Instance null) Instance this; else Destroy(gameObject); } void Start() { // 初始化UI UpdateScoreUI(); // 隐藏游戏结束面板 if(gameOverPanel ! null) gameOverPanel.SetActive(false); // 为按钮添加点击监听事件 if(restartButton ! null) restartButton.onClick.AddListener(RestartGame); } // 增加分数的方法可以被其他脚本调用如拾取物品时 public void AddScore(int points) { playerScore points; UpdateScoreUI(); // 更新UI显示 // 简单的游戏逻辑分数达到100则胜利 if (playerScore 100) { GameVictory(); } } void UpdateScoreUI() { if (scoreText ! null) scoreText.text 得分: playerScore.ToString(); } public void GameOver() { Debug.Log(游戏结束); if (gameOverPanel ! null) gameOverPanel.SetActive(true); // 可以在这里暂停游戏时间 // Time.timeScale 0; } void GameVictory() { Debug.Log(恭喜获胜); if (scoreText ! null) scoreText.text 胜利最终得分: playerScore; } void RestartGame() { Debug.Log(重新开始游戏); playerScore 0; UpdateScoreUI(); if (gameOverPanel ! null) gameOverPanel.SetActive(false); // 重置玩家位置、重生敌人等 // Time.timeScale 1; // 恢复游戏时间 } }UI与逻辑绑定要点UI组件的引用通过public变量在Inspector面板中拖拽赋值这是最简单直接的方式。也可以使用GameObject.Find或transform.Find动态查找但效率较低且易出错。事件监听Button.onClick.AddListener(方法名)是响应UI按钮点击的标准方式。注意传入的方法名不能带括号例如AddScore(10)是错误的应该是() AddScore(10)或一个无参方法。单例模式GameManager使用简单的单例模式使得其他脚本如拾取物品的脚本可以方便地调用GameManager.Instance.AddScore(10)来更新全局状态这是一种常见的设计模式。Time.timeScale将其设置为0可以暂停所有受时间影响的游戏逻辑如Time.deltaTime、物理模拟、动画设置为1恢复正常。这是实现游戏暂停/继续的常用手段。2.5 模块五动画状态机与简单AI——赋予角色生命对于角色除了移动还需要动画来表现状态。Unity的Animator Controller动画状态机是管理动画播放的核心工具。同时给非玩家角色NPC添加简单的AI如巡逻能立刻提升游戏感。核心代码示例动画控制与巡逻AIusing UnityEngine; using UnityEngine.AI; // 使用导航系统需要引入此命名空间 public class CharacterAnimation : MonoBehaviour { private Animator _animator; private Rigidbody _rb; void Start() { _animator GetComponentAnimator(); _rb GetComponentRigidbody(); } void Update() { // 计算当前移动速度 magnitude 是向量的长度 float speed _rb.velocity.magnitude; // 将速度值传递给Animator控制器中的“Speed”参数 if (_animator ! null) { _animator.SetFloat(Speed, speed); // 示例根据按键播放攻击动画 if (Input.GetMouseButtonDown(0)) { _animator.SetTrigger(Attack); } // 示例检测是否在地面控制跳跃/下落动画 // 这里需要根据你的接地检测逻辑来设置IsGrounded布尔参数 // _animator.SetBool(IsGrounded, _isGrounded); } } } // 简单的巡逻AI使用导航系统 public class SimplePatrolAI : MonoBehaviour { public Transform[] patrolPoints; // 巡逻点数组 public float waitTime 2f; // 在每个点等待的时间 private int _currentPointIndex 0; private NavMeshAgent _agent; private float _waitCounter 0; private bool _isWaiting false; void Start() { _agent GetComponentNavMeshAgent(); if (patrolPoints.Length 0) { MoveToNextPoint(); } } void Update() { // 如果正在等待则计时 if (_isWaiting) { _waitCounter - Time.deltaTime; if (_waitCounter 0) { _isWaiting false; MoveToNextPoint(); } } // 如果没有在等待且已经到达目的地路径计算完毕且剩余距离很小 else if (!_agent.pathPending _agent.remainingDistance _agent.stoppingDistance) { StartWaiting(); } } void MoveToNextPoint() { if (patrolPoints.Length 0) return; // 设置导航代理的目标位置 _agent.destination patrolPoints[_currentPointIndex].position; // 更新索引循环巡逻 _currentPointIndex (_currentPointIndex 1) % patrolPoints.Length; } void StartWaiting() { _isWaiting true; _waitCounter waitTime; // 可以在这里播放一个“ idle ”待机动画 // _animator.SetBool(IsMoving, false); } }动画与AI要点解析Animator参数驱动代码通过SetFloat,SetBool,SetTrigger等方法驱动Animator Controller中定义的参数从而控制动画状态的切换。这是脚本与动画师工作衔接的标准方式。导航系统NavMesh要实现自动寻路需要三步在场景中烘焙Bake导航网格NavMesh在Window - AI - Navigation窗口中操作。为AI角色添加NavMeshAgent组件。在代码中设置_agent.destination即可。AI状态管理即使是简单的巡逻AI也包含“移动”和“等待”两种状态。通过布尔变量_isWaiting和计时器_waitCounter来管理状态切换这是一种基础的有限状态机FSM思想。3. 项目整合实战构建一个微型收集游戏现在让我们把上述所有代码块整合起来创建一个简单的微型游戏原型“立方体冒险家”。玩家控制一个立方体在平台上移动、跳跃收集旋转的宝石同时避开巡逻的敌人。分数通过UI显示收集完所有宝石即获胜。3.1 场景与对象设置创建场景新建一个3D项目保存场景为MiniGame。搭建环境创建一个Plane作为地面重命名为Ground为其添加TagGround。创建几个Cube缩放后作为墙壁和障碍物。创建玩家创建一个Cube重命名为Player。为Player添加Rigidbody组件取消勾选Use Gravity可以先测试移动后续再加。为Player添加Box Collider组件。创建一个C#脚本PlayerController.cs将**模块二输入处理和模块三物理交互**的代码融合进去并挂载到Player上。// PlayerController.cs 简化融合示例 public class PlayerController : MonoBehaviour { public float moveForce 10f; public float jumpForce 5f; private Rigidbody _rb; private bool _isGrounded; void Start() { _rb GetComponentRigidbody(); } void Update() { float h Input.GetAxis(Horizontal); float v Input.GetAxis(Vertical); Vector3 force new Vector3(h, 0, v) * moveForce; _rb.AddForce(force * Time.deltaTime); if (_isGrounded Input.GetKeyDown(KeyCode.Space)) { _rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); _isGrounded false; } } void OnCollisionEnter(Collision col) { if(col.gameObject.CompareTag(Ground)) _isGrounded true; } void OnCollisionExit(Collision col) { if(col.gameObject.CompareTag(Ground)) _isGrounded false; } void OnTriggerEnter(Collider other) { if(other.CompareTag(Pickup)) GameManager.Instance.AddScore(10); } }创建可收集物创建一个Sphere重命名为Pickup为其添加TagPickup。为Pickup添加一个旋转动画在Inspector中点Add Component搜索Animation创建新动画如Rotate.anim在动画窗口中让其绕Y轴旋转。为Pickup添加Sphere Collider组件并勾选Is Trigger。复制多个Pickup散布在地图上。创建简单敌人创建一个Capsule重命名为Enemy。为Enemy添加NavMeshAgent组件。创建几个空物体作为Enemy的巡逻点Patrol Points摆放在场景中。将**模块五巡逻AI**的脚本挂载到Enemy上并在Inspector中将巡逻点数组赋值。为Enemy添加一个Box Collider。创建一个脚本EnemyDamage.cs当玩家碰到敌人时调用GameManager.Instance.GameOver()。public class EnemyDamage : MonoBehaviour { void OnCollisionEnter(Collision collision) { if (collision.gameObject.CompareTag(Player)) { GameManager.Instance.GameOver(); } } }设置UI在Canvas下创建一个Text重命名为ScoreText调整位置和大小。创建一个Button重命名为RestartButton将其子物体Text的内容改为“重新开始”。创建一个Panel重命名为GameOverPanel将其设置为非激活状态取消勾选Inspector顶部的复选框。在Panel下可以放一些文本如“游戏结束”。创建GameManager创建一个空物体重命名为GameManager。将**模块四UI交互**的GameManager脚本挂载上去。在Inspector中将ScoreText、RestartButton、GameOverPanel分别拖拽到脚本对应的公共变量槽中。烘焙导航网格选中Ground和所有障碍物在Navigation窗口的Object页签下确保它们都标记为Navigation Static。在Navigation窗口的Bake页签下点击Bake按钮。烘焙完成后地面会显示为蓝色的导航网格。3.2 核心逻辑串联与调试依赖关系确保PlayerController和EnemyDamage脚本中调用GameManager.Instance的方法前GameManager的Awake方法已经执行单例已初始化。Unity脚本生命周期中Awake总是先于任何Start和Update执行所以通常没问题。标签Tag管理检查Ground、Player、Pickup等Tag是否已正确创建并分配给对应物体。在Edit - Project Settings - Tags and Layers中管理。物理层Layer与碰撞矩阵如果玩家和敌人、子弹之间需要复杂的碰撞关系例如子弹不打自己人可以通过设置Layer并在Physics Settings中配置碰撞矩阵来实现。游戏流程测试运行游戏用WASD控制玩家移动空格键跳跃。触碰旋转的宝石Pickup宝石应消失右上角分数应增加。故意撞上巡逻的敌人应触发GameOver显示游戏结束面板。点击“重新开始”按钮分数应清零面板隐藏玩家和敌人状态应被重置这里需要你扩展RestartGame方法例如重置玩家位置、重新生成宝石等。4. 常见问题与避坑指南实录在实际编写和运行这些基础代码时你几乎一定会遇到下面这些问题。这里记录了我踩过的坑和解决方案。4.1 代码编译与脚本挂载问题问题1脚本挂载后Inspector面板上显示“Missing (Mono Script)”原因脚本文件名与类名不一致。Unity通过文件名查找类。如果脚本文件叫PlayerMove.cs但里面的类定义为public class PlayerController就会导致此问题。解决始终保持脚本文件名与内部主类名完全一致包括大小写。问题2在代码中使用了UnityEngine.UI的类如Text,Button但编译报错“The type or namespace name UI does not exist”原因未添加对应的程序集引用。虽然UnityEngine是默认引用的但UnityEngine.UI位于独立的程序集中。解决在脚本文件顶部添加using UnityEngine.UI;命名空间。如果还不行检查项目是否包含了必要的.dll通常新建UI项目会自动包含。问题3Update中的移动速度时快时慢原因没有使用Time.deltaTime。帧率FPS高时Update调用频繁物体移动就快帧率低时则慢。解决所有与帧率相关的位移、旋转、计时操作都必须乘以Time.deltaTime将“每帧的量”转换为“每秒的量”。4.2 物理与碰撞相关疑难杂症问题4给物体加了Rigidbody和Collider但它直接穿过地面掉下去了原因1地面或其他碰撞体没有Collider组件。原因2碰撞体的Is Trigger被错误地勾选了。原因3碰撞体的大小Scale或位置极端异常导致物理引擎无法有效计算。排查首先确保双方都有非Trigger的Collider。其次在场景视图中选择物体按快捷键ShiftC可以显示碰撞体轮廓绿色线框检查其形状和位置是否合理。问题5使用AddForce后物体移动非常慢或者根本不动原因1Rigidbody的质量Mass太大。一个Cube默认质量是1如果力太小如1加速度a F/m就会很小。原因2使用了ForceMode.Force但忘记乘以Time.deltaTime。AddForce的力是持续力需要在Update中每帧施加一小部分。原因3物体受到了巨大的阻力Drag或角阻力Angular Drag。解决适当增加力的大小如从10调到500或检查Rigidbody组件的Drag值空气阻力默认为0如果太大物体会很快停下。问题6OnTriggerEnter方法没有被调用原因1至少有一方的Collider没有勾选Is Trigger。原因2其中一方没有Rigidbody组件。虽然官方文档说静态碰撞体无Rigidbody也能触发但为了稳定建议双方至少有一方有Rigidbody。原因3脚本没有挂载在正确的物体上或者方法名拼写错误大小写敏感。排查确保触发双方A有Rigidbody非TriggerColliderB有TriggerCollider。在方法内加一句Debug.Log(“触发!”)是最快的调试方式。4.3 输入与UI交互的典型陷阱问题7鼠标控制视角时上下旋转不自然或直接翻转原因没有对上下旋转的角度进行限制Clamp当角度超过90度或-90度时Unity的欧拉角表示会出现万向节死锁导致突然翻转。解决如模块二代码所示用一个私有变量_rotationX累积垂直旋转角度并用Mathf.Clamp将其限制在[-90, 90]之间再将这个值赋给transform.localRotation。问题8UI按钮点击没有反应原因1按钮被其他UI元素如图片、面板遮挡拦截了射线投射Raycast。所有UI默认都接收射线投射。原因2按钮的Interactable属性被设置为false。原因3事件监听代码有误例如AddListener传入了一个带返回值的方法或者方法名后多了括号。排查检查按钮层级确保其在上层。在AddListener时使用匿名函数或Lambda表达式可以快速测试restartButton.onClick.AddListener(() Debug.Log(“Clicked!”));。4.4 导航与动画的常见坑问题9NavMeshAgent不移动或者报错“Failed to create agent because it is not close enough to the NavMesh”原因1目标点destination不在烘焙的导航网格NavMesh上。原因2AI角色初始位置不在导航网格上。原因3导航网格没有烘焙成功或者烘焙的区域Walkable设置不正确。解决确保场景已正确烘焙出蓝色的导航网格。将AI角色的NavMeshAgent组件的Base Offset调高一点或者将其初始位置放置在导航网格上方。使用NavMesh.SamplePosition可以在代码中寻找最近的可行走点。问题10动画状态机参数设置了但动画不切换原因1参数名拼写错误。Animator中的参数名是大小写敏感的。原因2状态之间的过渡Transition条件设置不正确。例如从“Idle”到“Run”需要Speed 0.1但你的代码设置的Speed值一直是0。原因3动画控制器Animator Controller没有赋值给模型Animator组件的Controller属性。排查在Play模式下打开Window - Animation - Animator窗口选中你的角色观察参数值是否按预期变化。检查状态之间的连线过渡上的条件。5. 性能优化与代码结构入门思考当你的基础Demo能跑起来后下一步自然会想到优化和扩展。这里分享几点最初级的、但立竿见影的优化和架构思想。5.1 对象池Object Pooling——应对频繁创建销毁在“收集宝石”的例子里我们用了Destroy来移除宝石。如果宝石需要频繁重生或者子弹需要大量发射反复的Instantiate创建和Destroy销毁会产生内存碎片引发GC垃圾回收卡顿。思路预先创建一堆比如20颗宝石放在一个“池子”列表或队列里。需要时从池中取出一颗激活不需要时将其失活放回池中而不是销毁。简易实现框架public class PickupPool : MonoBehaviour { public GameObject pickupPrefab; public int poolSize 20; private QueueGameObject _pool new QueueGameObject(); void Start() { for (int i 0; i poolSize; i) { GameObject obj Instantiate(pickupPrefab); obj.SetActive(false); _pool.Enqueue(obj); } } public GameObject GetPickup() { if (_pool.Count 0) { GameObject obj _pool.Dequeue(); obj.SetActive(true); return obj; } // 池子空了可以动态扩容或返回null return Instantiate(pickupPrefab); } public void ReturnPickup(GameObject obj) { obj.SetActive(false); _pool.Enqueue(obj); } }然后在收集宝石的代码中调用ReturnPickup而不是Destroy。GameManager的RestartGame方法中再从池中重新激活所有宝石。5.2 使用GetComponent缓存——减少性能开销在Update中频繁调用GetComponent是性能大忌。应该在Start或Awake中获取一次并缓存起来。优化前void Update() { float speed GetComponentRigidbody().velocity.magnitude; ... }优化后private Rigidbody _rb; void Start() { _rb GetComponentRigidbody(); } void Update() { float speed _rb.velocity.magnitude; ... }5.3 脚本职责分离Single Responsibility Principle不要把所有的代码都塞进一个PlayerController里。试着拆分PlayerMovement.cs只负责移动和跳跃的物理逻辑。PlayerAnimation.cs只负责根据速度、状态等参数控制Animator。PlayerHealth.cs只负责管理生命值、受伤、死亡事件。PlayerInputHandler.cs只负责处理原始输入并将其转化为其他脚本可以理解的事件或命令。 这样做的好处是代码清晰易于调试和修改。比如你想更换输入系统从旧Input Manager到新的Input System只需要重写PlayerInputHandler其他脚本几乎不用动。5.4 使用事件Events进行解耦在之前的整合示例中PlayerController直接调用了GameManager.Instance.AddScore()。这造成了紧耦合。如果以后想换一个计分系统或者同时更新多个UI就需要修改玩家代码。改进使用C#的事件Action或UnityEvent。在PlayerController中public class PlayerController : MonoBehaviour { // 定义一个事件当拾取物品时触发 public System.Actionint OnPickupCollected; void OnTriggerEnter(Collider other) { if (other.CompareTag(Pickup)) { Destroy(other.gameObject); // 触发事件并传递分数值 OnPickupCollected?.Invoke(10); } } }在GameManager的Start方法中void Start() { // 找到玩家并订阅事件 PlayerController player FindObjectOfTypePlayerController(); if(player ! null) { player.OnPickupCollected AddScore; } }这样PlayerController就完全不知道GameManager的存在它只负责发出“我捡到东西了”这个事件。任何关心这个事件的脚本如GameManager、音效管理器、成就系统都可以自行订阅实现了代码的解耦。
返回列表