尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity入门实战:从零开发“跳一跳”游戏,掌握物理、UI与脚本编程

Unity入门实战:从零开发“跳一跳”游戏,掌握物理、UI与脚本编程 1. 项目概述为什么选择“跳一跳”作为Unity入门项目如果你刚接触Unity或者想找一个能串联起物理、动画、UI和简单逻辑的完整小项目来练手那“跳一跳”这个经典玩法绝对是个宝藏。它规则简单到一句话就能说清按住屏幕蓄力松开让角色跳跃到下一个平台根据跳跃距离精准度得分。但正是这份简单让它成为了检验游戏开发基本功的绝佳试金石。我当年带新人第一个实战案例基本就是它因为它能逼着你把Unity那些核心模块——从刚体物理、动画状态机到UI交互和场景管理——全都实实在在地用一遍而不是停留在看教程的层面。市面上很多“入门教程”做完一个打砖块或者跑酷就结束了总觉得缺了点什么。“跳一跳”则不同它要求你处理“蓄力与释放”的精确输入计算抛物线轨迹动态生成看似随机实则可控的平台还要有一套清晰的分数和连击反馈。整个过程下来你对GameObject、Component、Prefab预制体这些核心概念的理解会深刻得多。更重要的是它的扩展性极强做完基础版你完全可以自己加道具、加皮肤、加关卡甚至改成多人对战乐趣无穷。最关键的是我们这次从零开始用的所有资源和工具都是免费的确保你能无门槛地跟着做下来。2. 核心思路与架构设计拆解“跳一跳”的骨架在动手写代码之前我们先得把游戏拆解成几个可以独立实现、再组合起来的模块。用盖房子的思路来想你得先有图纸。对于“跳一跳”我们可以把它分解为以下四个核心层2.1 场景与对象层游戏里都有哪些“演员”这是最直观的一层。在Unity的Hierarchy视图里你会创建这些主要对象玩家角色 (Player)通常是一个胶囊体(Capsule)或者一个方块上面挂着控制脚本和刚体(Rigidbody)组件让它能受物理引擎影响进行跳跃。平台 (Platform)游戏的核心交互对象。我们需要两种平台一种是固定的“起跳平台”另一种是后续动态生成的“目标平台”。平台本身可以是一个Cube为了美观可以贴上不同的材质。摄像机 (Main Camera)跟随玩家角色移动确保角色始终在画面中央偏下的位置为跳跃预留视野空间。UI画布 (Canvas)显示当前分数、历史最高分、蓄力指示器比如一个逐渐拉长的箭头或能量条和游戏结束界面。2.2 逻辑控制层游戏运行的“大脑”这一层由C#脚本驱动是游戏的核心。主要包含以下几个脚本PlayerController.cs附着在玩家角色上。它负责监听玩家的输入鼠标按下/抬起或触摸屏的触摸事件。根据按下的时长计算蓄力大小。在按下时可能还需要控制一个蓄力指示器如一个Scale在Y轴变大的箭头Sprite的显示。在松开时根据蓄力值给角色刚体施加一个力实现跳跃。PlatformSpawner.cs通常附着在一个空物体上比如叫“GameManager”。它负责管理平台的生成逻辑。游戏开始时生成第一个平台和玩家之后每当玩家成功落在一个平台上就在前方某个随机位置生成下一个新平台。控制平台的生成范围比如X轴和Z轴的随机偏移确保游戏既有挑战性又不会 impossible。GameManager.cs游戏的中央管理器。它负责管理游戏状态开始、进行中、结束。计算和更新分数。跳得越准落在平台中心得分越高甚至可能有“精准中心”的额外加分和连击效果。处理游戏结束逻辑角色掉落并弹出重新开始界面。2.3 物理与运动层让跳跃“真实”起来Unity内置的物理引擎NVIDIA PhysX是我们的好帮手。我们不需要自己写复杂的抛物线公式只需要为玩家角色添加Rigidbody组件。这样它就会受到重力影响。在PlayerController脚本中通过Rigidbody.AddForce()方法施加一个向上的力ForceMode.Impulse瞬间冲量模式很合适。跳跃的距离和高度就由这个力的大小和方向决定。通过Physics.Raycast射线检测来判断角色是否稳稳地站在了平台上这是触发得分和生成新平台的关键条件。2.4 表现与反馈层让游戏“好看”又好懂游戏不是冷冰冰的模拟需要给玩家即时的反馈动画角色跳跃时可以有一个简单的缩放或旋转动画。更高级一点可以做一个“蓄力颤抖”的动画。粒子效果角色起跳时脚下可以有一个灰尘粒子落在平台中心时爆出一个星星粒子效果。UI反馈分数变化时不是简单地改数字可以做一个放大再缩小的动画。蓄力条要有平滑的填充效果。连击时屏幕边缘可以闪一下特效。音效跳跃声、落地声、得分声、打破记录声这些短小的音效能极大提升游戏体验。3. 从零开始搭建开发环境与创建基础场景工欲善其事必先利其器。我们首先需要一个干净、可用的Unity开发环境。3.1 Unity编辑器的安装与项目初始化下载Unity Hub这是管理不同Unity版本和项目的官方工具。访问Unity官网下载对应你操作系统的Unity Hub。安装Unity编辑器在Hub的“安装”页面添加一个Unity版本。对于这个2D/3D混合的小游戏我推荐使用Unity 2021.3 LTS或2022.3 LTS版本。LTS长期支持版稳定社区资源丰富踩坑少。安装时在模块选择页面确保勾选了“Windows Build Support”或“macOS Build Support”根据你的系统以及“Android Build Support”如果你想后续发布到手机。其他的如iOS、WebGL等按需选择。创建新项目打开Hub点击“新建项目”。模板选择“3D (Core)”。虽然“跳一跳”视角像是2D但它的跳跃有纵深Z轴用3D模板更灵活。给项目起个名字比如“MyJumpJump”选择好存储路径点击创建。注意如果Unity编辑器启动后出现黑屏或无响应对应热词中的“unity程序打开黑屏无响应”这通常与显卡驱动有关。尝试更新显卡驱动到最新版本或者在Unity Hub编辑器的“设置”-“许可证”中切换为“个人版”许可证。如果问题依旧可以尝试以管理员身份运行Unity。3.2 构建基础游戏场景项目打开后你会看到默认的3D场景里有一个主摄像机和一个方向光。我们开始搭建基础场景创建地面和玩家在Hierarchy面板右键 - 3D Object - Plane重命名为“Ground”。这作为初始的地面。你可以调整它的Scale比如放大到(10,1,10)。再次右键 - 3D Object - Capsule重命名为“Player”。这就是我们的主角。将它放置在Ground上方比如Position设为(0, 1, 0)。设置摄像机选中Main Camera调整它的Transform。一个经典的“跳一跳”视角是侧上方45度角俯视。你可以尝试设置Position为(0, 10, -10)Rotation为(45, 0, 0)。这样既能看清角色又能看到前方的平台。为了让摄像机跟随玩家我们稍后会写脚本。暂时先这样。创建第一个平台右键 - 3D Object - Cube重命名为“StartPlatform”。把它放在Player脚下作为起跳点。调整它的Scale比如(3, 0.5, 1)让它变成一个长条状。为了美观我们可以给它加个颜色。在Project面板右键 - Create - Material命名为“PlatformMat”。选中它在Inspector面板将Albedo颜色调成你喜欢的比如绿色。然后将这个材质球拖到StartPlatform上。添加物理组件选中Player点击Inspector底部的“Add Component”搜索并添加“Rigidbody”组件。这个组件会让物体受物理引擎控制。保持默认设置即可但可以稍微调大“Mass”质量到2让跳跃手感更沉稳。选中StartPlatform和Ground同样添加Rigidbody组件。但关键的一步勾选Rigidbody组件上的“Is Kinematic”是运动学物体。这意味着它们不受物理力影响不会被撞飞但可以与其他刚体发生碰撞。同时为了性能也可以勾选“Freeze Rotation”冻结所有旋转防止它们意外翻倒。4. 核心功能实现编写玩家控制与跳跃逻辑现在进入最核心的编码环节。我们将创建第一个C#脚本。4.1 PlayerController脚本实现蓄力与跳跃在Project面板的Assets文件夹下右键 - Create - C# Script命名为“PlayerController”。双击用你喜欢的代码编辑器如Visual Studio打开。我们先定义几个关键变量并获取必要的组件引用using UnityEngine; public class PlayerController : MonoBehaviour { // 公开变量方便在Unity编辑器内调试和调整 public float maxForce 15f; // 最大跳跃力 public float forceMultiplier 10f; // 蓄力时间到跳跃力的换算系数 public Transform aimIndicator; // 指向目标的箭头指示器可选 private Rigidbody rb; private bool isCharging false; // 是否正在蓄力 private float chargeTime 0f; // 蓄力时间 private Vector3 jumpDirection; // 跳跃方向固定向前即Z轴正方向 void Start() { rb GetComponentRigidbody(); if (rb null) { Debug.LogError(PlayerController: Rigidbody component not found!); } // 初始化跳跃方向为世界坐标的Z轴正方向向前 jumpDirection Vector3.forward; // 如果有蓄力指示器初始隐藏 if (aimIndicator ! null) aimIndicator.gameObject.SetActive(false); } }接下来在Update函数中处理输入。我们使用鼠标左键模拟触摸屏按压void Update() { // 鼠标左键按下或触摸开始时开始蓄力 if (Input.GetMouseButtonDown(0)) { StartCharging(); } // 鼠标左键按住期间持续蓄力 if (isCharging) { chargeTime Time.deltaTime; // Time.deltaTime是上一帧的时间保证蓄力增长与帧率无关 // 可以在这里更新蓄力指示器的视觉表现比如拉长箭头 UpdateAimIndicator(); } // 鼠标左键抬起或触摸结束时执行跳跃 if (Input.GetMouseButtonUp(0) isCharging) { Jump(); } } void StartCharging() { // 只有角色在地面上时才能开始蓄力防止空中连跳 if (IsGrounded()) { isCharging true; chargeTime 0f; if (aimIndicator ! null) { aimIndicator.gameObject.SetActive(true); // 将指示器放在角色脚下并指向跳跃方向 aimIndicator.position transform.position Vector3.up * 0.1f; aimIndicator.rotation Quaternion.LookRotation(jumpDirection); } } } void UpdateAimIndicator() { if (aimIndicator ! null) { // 根据蓄力时间改变指示器的长度直观显示力度 float indicatorLength Mathf.Clamp(chargeTime * 2f, 0.5f, 3f); aimIndicator.localScale new Vector3(0.1f, 0.1f, indicatorLength); } }实现跳跃函数和接地检测void Jump() { if (!isCharging) return; // 计算最终的跳跃力并限制上限 float currentForce Mathf.Clamp(chargeTime * forceMultiplier, 0, maxForce); // 给刚体施加一个冲力。ForceMode.Impulse表示瞬间施加一个力适合跳跃 // 力的方向是向上Y轴和向前Z轴的组合 Vector3 forceVector new Vector3(0, 1.5f, 1).normalized * currentForce; rb.AddForce(forceVector, ForceMode.Impulse); // 重置蓄力状态 isCharging false; chargeTime 0f; if (aimIndicator ! null) aimIndicator.gameObject.SetActive(false); Debug.Log(Jumped with force: currentForce); } bool IsGrounded() { // 使用射线检测判断角色是否在地面 // 从角色中心偏下的位置向下发射一条很短的射线 RaycastHit hit; float rayLength 0.6f; // 射线长度略大于角色底部到脚底的距离 Vector3 rayStart transform.position Vector3.up * 0.1f; // 从腰部偏下开始 if (Physics.Raycast(rayStart, Vector3.down, out hit, rayLength)) { // 如果射线打到了物体并且该物体是平台可以通过Tag或Layer判断 if (hit.collider.CompareTag(Platform)) { return true; } } // 可视化射线调试时非常有用 Debug.DrawRay(rayStart, Vector3.down * rayLength, Color.green); return false; }保存脚本回到Unity。将PlayerController脚本拖拽到Hierarchy中的Player对象上。你会看到Inspector面板出现了我们定义的公共变量。把Max Force和Force Multiplier分别设为15和10可以先默认后续调试。如果创建了蓄力指示器比如一个细长的Cube也把它拖拽赋值给Aim Indicator。4.2 创建平台预制体与生成器制作平台预制体在场景中创建一个新的Cube重命名为“Platform”调整Scale为(2, 0.3, 1)。给它一个不同于起跳平台的颜色材质比如蓝色。为它添加Rigidbody并勾选Is Kinematic和冻结旋转。关键一步在Inspector顶部的Tag下拉菜单点击“Add Tag...”新建一个Tag命名为“Platform”。然后选中场景中的这个Platform Cube将它的Tag设置为“Platform”。这样我们的IsGrounded()检测才能生效。将这个Platform从Hierarchy拖到Project面板的Assets文件夹里。你会看到一个蓝色的预制体Prefab图标。之后就可以删除场景中的这个实例了我们将用代码动态生成它。编写PlatformSpawner脚本创建一个新的C#脚本命名为“PlatformSpawner”。这个脚本需要知道在哪里生成平台相对于玩家或上一个平台、平台的预制体是什么、生成的范围有多大。using UnityEngine; public class PlatformSpawner : MonoBehaviour { public GameObject platformPrefab; // 拖入我们刚创建的Platform预制体 public Transform playerTransform; // 拖入Player对象用于计算生成位置 public float minDistance 3f; // 距离玩家的最小生成距离 public float maxDistance 6f; // 距离玩家的最大生成距离 public float minHeight -1f; // Y轴的最小偏移可以生成在下坡 public float maxHeight 2f; // Y轴的最大偏移可以生成在上坡 private GameObject lastPlatform; // 记录上一个生成的平台 void Start() { if (platformPrefab null || playerTransform null) { Debug.LogError(PlatformSpawner: Prefab or Player reference not set!); return; } // 游戏开始时可以生成第一个目标平台除了起始平台 SpawnNewPlatform(); } // 这个方法将由GameManager或玩家落地时调用 public void SpawnNewPlatform() { // 计算生成位置 Vector3 spawnPosition CalculateSpawnPosition(); // 实例化平台预制体 lastPlatform Instantiate(platformPrefab, spawnPosition, Quaternion.identity); // 可以给新平台一个随机的颜色或轻微旋转增加变化 Renderer rend lastPlatform.GetComponentRenderer(); if (rend ! null) { rend.material.color new Color( Random.Range(0.6f, 1f), Random.Range(0.6f, 1f), Random.Range(0.6f, 1f) ); } } Vector3 CalculateSpawnPosition() { // 基础位置玩家当前位置的前方 Vector3 basePos playerTransform.position Vector3.forward * 5f; // 添加随机偏移 float randomX Random.Range(-2f, 2f); // X轴左右随机 float randomZ Random.Range(minDistance, maxDistance); // Z轴前后距离随机 float randomY Random.Range(minHeight, maxHeight); // Y轴高度随机 Vector3 randomOffset new Vector3(randomX, randomY, randomZ); // 如果存在上一个平台也可以基于上一个平台的位置生成 // 这里我们选择基于玩家前方生成逻辑更简单 return basePos randomOffset; } }在场景中创建一个空GameObject重命名为“GameManager”。将PlatformSpawner脚本挂载上去。然后将Project里的Platform预制体拖到脚本的Platform Prefab槽把Hierarchy里的Player拖到Player Transform槽。5. 游戏逻辑中枢实现分数计算与状态管理游戏能跳了平台能生了现在需要把它们串联起来并加入游戏的核心——分数。5.1 创建GameManager脚本新建C#脚本命名为“GameManager”。这个脚本将是一个单例Singleton方便其他脚本访问。它负责管理游戏分数。判断游戏是否结束玩家掉落。调用平台生成。控制UI的显示与隐藏。using UnityEngine; using UnityEngine.UI; // 引入UI命名空间 using UnityEngine.SceneManagement; // 用于重新加载场景 public class GameManager : MonoBehaviour { public static GameManager Instance; // 单例实例 public Text scoreText; // 当前分数UI文本 public Text highScoreText; // 最高分UI文本 public GameObject gameOverPanel; // 游戏结束界面面板 private int currentScore 0; private int highScore 0; private bool isGameOver false; private PlatformSpawner platformSpawner; void Awake() { // 简单的单例模式实现 if (Instance null) { Instance this; } else { Destroy(gameObject); } } void Start() { // 加载保存的最高分 highScore PlayerPrefs.GetInt(HighScore, 0); UpdateHighScoreUI(); // 查找PlatformSpawner组件 platformSpawner FindObjectOfTypePlatformSpawner(); if (platformSpawner null) { Debug.LogError(GameManager: Could not find PlatformSpawner in scene.); } // 隐藏游戏结束面板 if (gameOverPanel ! null) gameOverPanel.SetActive(false); } // 当玩家成功落在一个平台上时由平台或PlayerController调用 public void OnPlayerLanded(Vector3 landingPosition, GameObject platform) { if (isGameOver) return; // 1. 计算本次跳跃得分 int scoreToAdd CalculateScore(landingPosition, platform); currentScore scoreToAdd; UpdateScoreUI(); // 2. 检查并更新最高分 if (currentScore highScore) { highScore currentScore; PlayerPrefs.SetInt(HighScore, highScore); UpdateHighScoreUI(); // 可以在这里触发一个“新纪录”特效或音效 } // 3. 生成下一个平台 if (platformSpawner ! null) { platformSpawner.SpawnNewPlatform(); } Debug.Log(Landed! Added scoreToAdd points. Total: currentScore); } int CalculateScore(Vector3 landingPos, GameObject platform) { // 基础分成功着陆就得10分 int baseScore 10; // 精准度加分计算落点与平台中心的距离 Vector3 platformCenter platform.transform.position; float distanceFromCenter Vector3.Distance( new Vector3(landingPos.x, 0, landingPos.z), // 忽略Y轴高度差 new Vector3(platformCenter.x, 0, platformCenter.z) ); float platformRadius platform.transform.localScale.x / 2f; // 粗略以X方向半宽作为半径 float accuracyRatio 1f - Mathf.Clamp01(distanceFromCenter / platformRadius); // 精准度越高加分越多。例如落在中心ratio1额外加20分 int bonusScore Mathf.RoundToInt(20 * accuracyRatio); return baseScore bonusScore; } // 游戏结束例如玩家掉出屏幕 public void GameOver() { if (isGameOver) return; isGameOver true; Debug.Log(Game Over! Final Score: currentScore); // 显示游戏结束界面 if (gameOverPanel ! null) { gameOverPanel.SetActive(true); // 可以在游戏结束面板上显示最终分数 Text finalScoreText gameOverPanel.transform.Find(FinalScoreText)?.GetComponentText(); if (finalScoreText ! null) finalScoreText.text 得分: currentScore; } // 可以在这里停止玩家输入、播放失败音效等 } // UI更新方法 void UpdateScoreUI() { if (scoreText ! null) scoreText.text 分数: currentScore; } void UpdateHighScoreUI() { if (highScoreText ! null) highScoreText.text 最高分: highScore; } // 提供给UI按钮调用的方法 public void RestartGame() { // 最简单的方式重新加载当前场景 SceneManager.LoadScene(SceneManager.GetActiveScene().name); } public void QuitGame() { // 在编辑器里停止播放在打包后的程序中退出 #if UNITY_EDITOR UnityEditor.EditorApplication.isPlaying false; #else Application.Quit(); #endif } }5.2 完善PlayerController的着陆检测与通信现在需要修改PlayerController脚本让它能在着陆时通知GameManager。在PlayerController中添加public class PlayerController : MonoBehaviour { // ... 之前已有的变量 ... private bool wasGrounded false; // 上一帧是否在地面 private GameObject currentPlatform; // 当前站立的平台 void Update() { // ... 原有的输入检测代码 ... // 每帧检测接地状态变化 bool grounded IsGrounded(); if (!wasGrounded grounded) { // 刚刚着陆 OnLanded(); } wasGrounded grounded; // 简单的游戏结束判断如果角色掉到很深的地方 if (transform.position.y -10f) { GameManager.Instance?.GameOver(); } } // 修改IsGrounded方法使其能返回检测到的平台 bool IsGrounded() { RaycastHit hit; float rayLength 0.6f; Vector3 rayStart transform.position Vector3.up * 0.1f; if (Physics.Raycast(rayStart, Vector3.down, out hit, rayLength)) { if (hit.collider.CompareTag(Platform)) { currentPlatform hit.collider.gameObject; // 记录当前平台 return true; } } Debug.DrawRay(rayStart, Vector3.down * rayLength, Color.green); currentPlatform null; return false; } void OnLanded() { if (currentPlatform ! null) { // 通知GameManager传递着陆点角色当前位置和平台对象 GameManager.Instance?.OnPlayerLanded(transform.position, currentPlatform); } } }5.3 设置UI界面在Hierarchy右键 - UI - Canvas创建一个画布。在Canvas下创建Text重命名为“ScoreText”用于显示当前分数。调整RectTransform将其锚点(Anchor)设为左上角调整位置。在Text组件中设置字体大小、颜色文本内容设为“分数: 0”。另一个Text重命名为“HighScoreText”用于显示最高分。同样放在左上角在ScoreText下方。Panel重命名为“GameOverPanel”。将其锚点拉伸至全屏并设置一个半透明的背景色如黑色Alpha150。初始状态取消勾选SetActive false。在GameOverPanel下创建Text重命名为“FinalScoreText”显示“游戏结束最终分数”。Button重命名为“RestartButton”子Text改为“重新开始”。另一个Button重命名为“QuitButton”子Text改为“退出游戏”。关联UI元素选中场景中的GameManager对象。将Canvas下的ScoreText和HighScoreText分别拖到GameManager脚本的对应公共变量槽中。将GameOverPanel整个对象拖到Game Over Panel槽中。分别为两个UI按钮的OnClick事件添加监听点击“”号将GameManager对象拖入然后分别选择GameManager.RestartGame和GameManager.QuitGame方法。6. 打磨与优化让游戏体验更上一层楼基础功能都实现了现在我们来打磨细节让游戏从“能玩”变成“好玩”。6.1 摄像机平滑跟随当前的摄像机是固定的角色跳远后就出画面了。我们需要一个跟随脚本。创建C#脚本“CameraFollow”using UnityEngine; public class CameraFollow : MonoBehaviour { public Transform target; // 跟随的目标Player public Vector3 offset new Vector3(0, 10, -10); // 摄像机相对于目标的偏移 public float smoothSpeed 5f; // 跟随平滑度值越大跟随越紧 void LateUpdate() // 在目标移动后更新摄像机避免抖动 { if (target null) return; // 计算期望的摄像机位置 Vector3 desiredPosition target.position offset; // 使用线性插值平滑移动 Vector3 smoothedPosition Vector3.Lerp(transform.position, desiredPosition, smoothSpeed * Time.deltaTime); transform.position smoothedPosition; // 让摄像机始终看着目标可选我们的固定角度可能更好 // transform.LookAt(target); } }将脚本挂到Main Camera上把Player对象拖到Target变量中。调整Offset和Smooth Speed直到感觉跟随流畅自然。6.2 添加音效与简单粒子效果音效准备几个简短的音效文件.wav或.mp3如“jump.wav”、“land.wav”、“score.wav”。将它们导入Unity的Assets文件夹。在PlayerController的Jump()方法里添加AudioSource.PlayClipAtPoint(jumpSoundClip, transform.position);在GameManager的OnPlayerLanded里添加得分音效。粒子效果在Player脚下创建一个粒子系统Particle System。右键 - Effects - Particle System重命名为“LandingDust”。调整粒子参数Start Lifetime调短0.5Start Speed调低Emission Rate调高Shape设为半球形朝上。在PlayerController的OnLanded()方法中触发这个粒子系统的播放landingParticles.Play();6.3 蓄力视觉反馈优化我们之前用了一个简单的箭头拉伸。可以做得更炫使用UI Slider滑动条作为蓄力条。在Canvas下创建Slider将其锚点放在屏幕底部。将Slider的Handle Slide Area和Handle都禁用或隐藏只保留Background和Fill Area让它看起来像一个水平进度条。在PlayerController中获取这个Slider的引用在UpdateAimIndicator方法中根据chargeTime更新Slider的valuepowerSlider.value chargeTime / (maxForce / forceMultiplier);还可以改变Fill Area的颜色从绿色安全渐变到红色最大力。7. 常见问题与调试技巧实录开发过程中你几乎一定会遇到下面这些问题。这里是我踩过坑后总结的排查思路。7.1 角色跳跃无力或直接穿墙而过症状按下鼠标角色轻轻动一下或者根本不动甚至直接掉下去。排查检查刚体首先确认Player对象上有Rigidbody组件。没有刚体AddForce是无效的。检查力的大小在Jump()方法里用Debug.Log打印出计算出的currentForce值。如果一直是0说明chargeTime没增长检查StartCharging的触发条件和IsGrounded()的检测是否准确。检查力的方向forceVector的Y和Z分量是否合理可以尝试先给一个固定的力测试比如rb.AddForce(Vector3.up * 5f, ForceMode.Impulse);看能否跳起来。碰撞体缺失确保Player和Platform都有Collider组件Cube默认有Box ColliderCapsule默认有Capsule Collider。没有Collider物理碰撞就不会发生射线检测也会失败。图层碰撞矩阵检查Edit - Project Settings - Physics。确保Player和Platform所在图层的“碰撞矩阵”是相互勾选的默认都是勾选的。如果修改了图层这里可能被禁用。7.2 平台生成位置诡异或重叠症状新平台生成在奇怪的地方比如玩家头顶或者和旧平台叠在一起。排查检查生成位置计算在PlatformSpawner.CalculateSpawnPosition()方法里用Debug.DrawLine或Debug.Log打印出计算出的spawnPosition。看看随机偏移量randomX, randomZ, randomY的范围是否合理。基于谁的位置生成我们的示例代码是基于玩家当前位置向前生成。如果玩家跳跃过程中位置变化很快可能导致生成位置飘忽不定。可以考虑基于“上一个成功着陆的平台”的位置来生成下一个这样逻辑更稳定。防止重叠生成新平台前可以用Physics.OverlapBox或Physics.CheckSphere检查目标位置周围是否有其他平台如果有就重新计算一个位置。7.3 游戏卡顿特别是生成多个平台后症状游戏运行一段时间后越来越卡。排查平台数量无限增长这是最常见的原因。我们只生成新平台没有销毁旧平台。玩家跳走后后面的平台还留在场景里成百上千个Cube肯定会卡。解决方案在PlatformSpawner中维护一个平台列表当列表长度超过一定数量比如10个时销毁最早生成的那个平台。或者在每个平台离开摄像机视野后用OnBecameInvisible方法自我销毁。复杂的Update逻辑检查所有脚本的Update方法避免在里面进行复杂的计算或每帧查找对象如FindObjectOfType。将不变的计算移到Start或缓存起来。粒子系统或音频未回收实例化的粒子或播放的音频源在播放完毕后要销毁或回收。7.4 UI按钮点击无反应症状点击重新开始或退出按钮什么都没发生。排查事件监听是否正确绑定在Unity编辑器中双击检查按钮的OnClick事件列表。确保GameManager对象被正确拖入并且下拉菜单中选择了正确的方法名RestartGame或QuitGame。GameManager单例是否有效在游戏运行时检查Hierarchy中是否存在GameManager对象。如果不存在GameManager.Instance就是null按钮调用会失败。面板遮挡确认GameOverPanel在激活时其Raycast Target是开启的并且没有其他全屏UI面板遮挡了按钮。7.5 精准得分计算不准症状明明感觉跳在平台中心却没有拿到最高加分。排查着陆点检测时机OnLanded是在IsGrounded从false变为true时触发的。此时角色的位置可能还在“滑动”或“微调”。可以尝试延迟几帧再计算分数或者取一个更稳定的位置如碰撞接触点hit.point。平台中心计算我们用了platform.transform.position这是预制体的轴心点(Pivot)。如果你的平台模型轴心不在几何中心这里就会出错。确保在建模或调整Cube时其轴心在中心。或者在预制体里放一个空子物体作为“得分中心点”。距离计算忽略Y轴我们的计算用new Vector3(landingPos.x, 0, landingPos.z)把Y轴归零了这是正确的因为平台可能有高度差我们只关心水平面上的落点精度。走到这一步你的“跳一跳”已经是一个功能完整、体验流畅的游戏了。从环境搭建、物理交互、代码逻辑到UI和反馈你已经走完了一个小型Unity项目开发的全流程。这个过程中最重要的不是复现我的每一行代码而是理解每个决策背后的原因为什么用刚体而不是直接改Transform为什么用射线检测接地单例模式在这里解决了什么问题你可以在此基础上继续深化给平台添加不同的类型移动平台、破碎平台、加分平台为角色添加跳跃动画和落地翻滚引入连击系统连续中心着陆获得倍数得分甚至加入简单的敌人和障碍。这个小小的项目就像一颗种子能生长出无数种可能。编程和游戏开发最大的乐趣就在于看着自己脑海中的想法通过一行行代码变成屏幕上鲜活的世界。
返回列表