Unity新手制作跑酷小游戏详细教程攻略
❤️嗨喽大家好呀!今天我来分享一个unity跑酷小游戏的详细制作步骤~
专为新手打造,零基础也可以轻松驾驭,跟着上手试一试也是可以的哦!我们将要使用Unity2021或者更新的版本和c#脚本来制作:
首先我们要知道游戏核心概念:
• 玩家:一个方块(Cube)代表角色,只能左右移动。
• 赛道:一条长长的平面(Plane)作为跑道。
• 障碍物:随机生成的立方体障碍物(Cube),玩家需要躲避。
• 机制:玩家控制角色左右移动躲避障碍物,碰到障碍物游戏结束,坚持时间越长得分越高。
第一步:项目设置与环境搭建 (保姆级开始!)
1. 启动 Unity Hub: 打开 Unity Hub。
2. 新建项目:
◦ 点击 New Project。
◦ 选择 3D Core 模板 (确保是 3D 项目)。
◦ 给项目起个名字,比如 MyFirstRunner。
◦ 选择一个合适的文件夹存放项目。
◦ 点击 Create Project。Unity 会初始化新项目。
3. 熟悉界面 (快速概览):
◦ 场景视图 (Scene View): 3D 世界,你在这里摆放物体。
◦ 游戏视图 (Game View): 游戏运行时的画面预览。
◦ 层级视图 (Hierarchy): 列出当前场景中的所有游戏对象 (GameObject)。
◦ 项目视图 (Project): 你的项目资源库 (脚本、材质、模型等)。
◦ 检查器视图 (Inspector): 显示当前选中游戏对象的详细信息 (位置、旋转、缩放、组件等)。
◦ 工具栏 (Toolbar): 常用工具 (移动、旋转、缩放物体等) 和播放/暂停按钮。
第二步:创建游戏世界 - 赛道和玩家
1. 创建地面 (跑道):
◦ 在 Hierarchy 面板空白处 右键 -> 3D Object -> Plane。这将是我们的跑道。
◦ 在 Inspector 面板中,找到 Transform 组件:
◦ 将 Position 设置为 (0, 0, 0)。
◦ 将 Scale 设置为 (1, 1, 20)。这将使跑道变长(Z 轴放大 20 倍)。
◦ 重命名: 在 Hierarchy 中选中 Plane,按 F2 或右键选择 Rename,将其命名为 Ground。
2. 创建玩家角色:
◦ 在 Hierarchy 面板空白处 右键 -> 3D Object -> Cube。
◦ 在 Inspector 的 Transform 中:
◦ 设置 Position 为 (0, 0.5, 0)。(Y=0.5 让方块底部刚好在地面上)
◦ 设置 Scale 为 (1, 1, 1)。
◦ 重命名: 将其命名为 Player。
3. 添加颜色 (可选但推荐):
◦ 在 Project 视图的 Assets 文件夹上 右键 -> Create -> Folder,命名为 Materials。
◦ 进入 Materials 文件夹,右键 -> Create -> Material。命名为 PlayerMat。
◦ 选中 PlayerMat,在 Inspector 中找到 Albedo (主颜色) 属性,点击旁边的色块,选择一个喜欢的颜色(比如蓝色)。
◦ 将这个材质球 PlayerMat 从 Project 视图拖拽到 Hierarchy 中的 Player 立方体上。玩家方块就变色了!
◦ 同样方法,创建一个 GroundMat(比如绿色),拖拽给 Ground 平面。
4. 调整摄像机视角:
◦ 在 Hierarchy 中选中 Main Camera。
◦ 在 Scene 视图,使用鼠标中键平移、右键旋转、滚轮缩放,调整一个合适的视角(比如从玩家后方稍高的位置看跑道)。也可以在 Inspector 的 Transform 和 Camera 组件中直接调整数值。目标是让玩家在屏幕下方,能看到前方的跑道。
第三步:让玩家动起来 - 左右移动控制
1. 创建脚本文件夹: 在 Project 视图的 Assets 上 右键 -> Create -> Folder,命名为 Scripts。
2. 创建玩家控制脚本:
◦ 进入 Scripts 文件夹,右键 -> Create -> C# Script。命名为 PlayerController。
◦ 双击 PlayerController 脚本文件,它会在你的默认代码编辑器(如 Visual Studio)中打开。
3. 编写移动代码 (超级详细解释):
using UnityEngine;
public class PlayerController : MonoBehaviour
{
// 变量声明区:定义脚本需要使用的数据
public float moveSpeed = 5f; // 玩家左右移动的速度 (public 使其在Unity编辑器中可调)
private float horizontalInput; // 存储键盘水平输入值 (-1 左, 0 不动, 1 右)
// Update 方法:Unity 每帧自动调用一次 (约60次/秒),用于处理实时输入和游戏逻辑
void Update()
{
// 1. 获取玩家输入
horizontalInput = Input.GetAxis("Horizontal"); // 读取键盘 A/D 或 左右箭头 的输入,值在 -1 到 1 之间
// 2. 计算移动方向 (只沿X轴左右移动)
Vector3 moveDirection = new Vector3(horizontalInput, 0f, 0f); // (X, Y, Z) Y=0不上下动,Z=0不前后动
// 3. 应用移动 (使用 Time.deltaTime 确保移动速度与帧率无关)
transform.Translate(moveDirection * moveSpeed * Time.deltaTime);
}
}
4. 解释关键点:
◦ public float moveSpeed = 5f;:定义了一个公共浮点数变量,表示移动速度。public 使其出现在 Unity 编辑器的 Inspector 中,方便你随时调整数值测试效果。
◦ private float horizontalInput;:定义一个私有变量,用来存储当前帧获取到的水平输入值。
◦ Input.GetAxis("Horizontal"):Unity 内置的输入系统方法。默认映射到键盘的 A/D 键和左右箭头键。返回一个 -1 到 1 之间的浮点数。
◦ Vector3 moveDirection = new Vector3(horizontalInput, 0f, 0f);:创建一个三维向量 (Vector3)。horizontalInput 决定 X 方向(左右)的移动分量。Y 和 Z 分量设为 0,表示我们不需要上下或前后移动。
◦ transform.Translate(...):transform 是当前脚本所附加的游戏对象(玩家方块)的 Transform 组件引用。Translate 方法用于沿着给定的方向和距离移动物体。
◦ moveDirection * moveSpeed * Time.deltaTime:计算这一帧的实际移动量。
◦ moveDirection:方向向量(长度可能小于1,因为是 -1 到 1)。
◦ moveSpeed:控制移动快慢的系数。
◦ Time.deltaTime:极其重要! 表示上一帧到当前帧经过的时间(秒)。乘以它可以使移动速度独立于游戏的帧率(FPS)。无论电脑快慢,每秒移动的距离都是 moveSpeed 个单位。不加它,移动速度会随帧率剧烈变化!
5. 保存脚本: 在代码编辑器中保存文件 (Ctrl+S / Cmd+S)。
6. 将脚本附加给玩家:
◦ 回到 Unity 编辑器。
◦ 在 Hierarchy 中选中 Player 对象。
◦ 将 Project 视图 Scripts 文件夹中的 PlayerController 脚本拖拽到 Inspector 视图的最下方空白区域。或者点击 Inspector 底部的 Add Component 按钮,搜索 PlayerController 并添加。
◦ 现在 Player 的 Inspector 中应该能看到 PlayerController 组件,并且有一个可以调节的 Move Speed 滑块/输入框(因为它是 public 变量)。
7. 测试移动:
◦ 点击 Unity 工具栏上方的 Play 按钮 (三角形)。
◦ 按键盘上的 A/左箭头 和 D/右箭头 键。你应该能看到玩家方块在跑道上左右移动了!
◦ 点击 Play 按钮再次停止游戏。
第四步:生成障碍物 - 源源不断的挑战
1. 创建障碍物预制体 (Prefab):
◦ 在 Hierarchy 中 右键 -> 3D Object -> Cube。重命名为 Obstacle。
◦ 在 Inspector 的 Transform 中,设置 Position 为 (0, 0.5, 0)(和玩家初始高度一致)。设置 Scale 为 (1, 1, 1) 或调整成你想要的障碍物大小(比如 (0.5, 1, 0.5))。
◦ 创建一个新材质 ObstacleMat(比如红色),拖拽给这个 Obstacle 立方体。
◦ 关键步骤: 将这个 Obstacle 对象从 Hierarchy 视图拖拽到 Project 视图的 Assets 文件夹中(最好先创建一个 Prefabs 文件夹存放)。你会看到它变成一个蓝色的预制体 (Prefab)。预制体就像一个模板,我们可以在代码中动态生成它的实例。
◦ 删除场景中的实例: 现在可以安全地从 Hierarchy 中删除刚才创建的 Obstacle 对象。预制体模板已经保存在 Project 里了。
2. 创建障碍物生成器脚本:
◦ 在 Scripts 文件夹中,右键 -> Create -> C# Script。命名为 SpawnManager。
◦ 双击打开进行编辑。
3. 编写障碍物生成代码:
using UnityEngine;
using System.Collections; // 可能需要用 Coroutine
public class SpawnManager : MonoBehaviour
{
// 变量声明
public GameObject obstaclePrefab; // 引用障碍物预制体 (在Unity编辑器中拖拽赋值)
private Vector3 spawnPos = new Vector3(0, 0, 20); // 生成点的位置 (X随机, Y=0.5, Z在玩家前方远处)
private float startDelay = 2f; // 游戏开始后第一次生成的延迟时间
private float repeatRate = 2f; // 之后每隔多少秒生成一次
private PlayerController playerControllerScript; // 引用玩家控制脚本,用于检测游戏是否结束
// Start 方法:在游戏开始时自动调用一次
void Start()
{
// 1. 获取玩家脚本的引用 (假设玩家对象叫 "Player")
playerControllerScript = GameObject.Find("Player").GetComponent<PlayerController>();
// 2. 开始重复生成障碍物的协程
InvokeRepeating("SpawnObstacle", startDelay, repeatRate);
}
// 自定义方法:生成一个障碍物
void SpawnObstacle()
{
// 如果游戏还没结束 (通过玩家脚本中的标志判断)
if (playerControllerScript.gameOver == false)
{
// 随机生成障碍物的X位置 (左右车道)
float randomX = Random.Range(-4f, 4f); // 调整数值匹配你的跑道宽度
spawnPos.x = randomX;
// 实例化障碍物:创建预制体的一个副本,放在 spawnPos 位置,使用默认旋转
Instantiate(obstaclePrefab, spawnPos, obstaclePrefab.transform.rotation);
}
}
}
4. 解释关键点:
◦ public GameObject obstaclePrefab;:这是一个公共变量,用来存放我们在 Unity 编辑器中创建的障碍物预制体。稍后需要手动拖拽赋值。
◦ private Vector3 spawnPos = new Vector3(0, 0, 20);:定义障碍物生成点的初始位置。X 会在每次生成时随机化,Y 设为障碍物高度的一半(确保在地面上),Z 设为玩家前方足够远的距离(如 20)。
◦ private PlayerController playerControllerScript;:声明一个变量来存储对 PlayerController 脚本的引用。我们需要用它来检查游戏是否结束(避免游戏结束后继续生成障碍物)。
◦ GameObject.Find("Player").GetComponent<PlayerController>();:在 Start 方法中,我们通过游戏对象的名字 "Player" 找到玩家对象,然后从它身上获取 PlayerController 脚本组件并存储到变量中。这是一种获取其他对象引用的一种方式(注意对象名字要匹配!)。
◦ InvokeRepeating("SpawnObstacle", startDelay, repeatRate);:Unity 提供的方法。它会在 startDelay 秒后,开始每隔 repeatRate 秒调用一次名为 SpawnObstacle 的方法。非常适合定时生成。
◦ if (playerControllerScript.gameOver == false):在生成障碍物之前,先检查 PlayerController 脚本中一个(我们稍后会创建的)gameOver 标志是否为 false(即游戏没有结束)。如果游戏结束了,就不生成。
◦ float randomX = Random.Range(-4f, 4f);:在 -4 到 4 之间随机生成一个 X 坐标。你需要根据你的跑道宽度(Ground 的 Scale X)和玩家移动范围调整这个数值! 确保障碍物生成在跑道内,且玩家能移动到那里。
◦ spawnPos.x = randomX;:更新生成点的 X 坐标为随机值。
◦ Instantiate(obstaclePrefab, spawnPos, obstaclePrefab.transform.rotation);:这是核心方法。Instantiate 用于在运行时创建(克隆)一个预制体对象。参数分别是:要克隆的预制体 (obstaclePrefab)、生成位置 (spawnPos)、旋转角度(这里使用预制体本身的旋转 obstaclePrefab.transform.rotation)。
5. 保存脚本。
6. 设置障碍物生成器:
◦ 在 Hierarchy 中创建一个空的游戏对象:右键 -> Create Empty。重命名为 SpawnManager。
◦ 选中这个 SpawnManager 对象。
◦ 将 SpawnManager 脚本从 Project/Scripts 拖拽到它的 Inspector 上。
◦ 关键赋值: 在 SpawnManager 组件的 Inspector 中,你会看到 Obstacle Prefab 变量是空的。将 Project/Prefabs 文件夹中的 Obstacle 预制体拖拽到这个空槽上。这样脚本就知道要生成哪个预制体了。
◦ 调整 Start Delay 和 Repeat Rate 的值,控制障碍物出现的频率(例如 2 秒开始,每 2 秒一个)。
第五步:碰撞检测与游戏结束 - 碰到就失败!
1. 修改玩家控制脚本 (PlayerController):
◦ 在 Project/Scripts 中双击 PlayerController 再次打开。
◦ 添加以下代码:
using UnityEngine;
public class PlayerController : MonoBehaviour
{
public float moveSpeed = 5f;
private float horizontalInput;
public bool gameOver = false; // 新增:游戏结束标志 (public 让生成器能访问)
// 新增:Rigidbody 组件引用 (用于物理碰撞检测)
private Rigidbody playerRb;
// Start 方法:在游戏开始时调用一次
void Start()
{
// 获取当前游戏对象上的 Rigidbody 组件
playerRb = GetComponent<Rigidbody>();
}
void Update()
{
// 只有游戏未结束时才接受输入移动
if (!gameOver)
{
horizontalInput = Input.GetAxis("Horizontal");
Vector3 moveDirection = new Vector3(horizontalInput, 0f, 0f);
transform.Translate(moveDirection * moveSpeed * Time.deltaTime);
}
// 如果游戏结束,这里可以添加其他逻辑(比如显示失败动画)
}
// 新增:OnCollisionEnter 方法 (Unity内置碰撞检测回调)
// 当这个物体(带Collider和Rigidbody)与其他带Collider的物体发生碰撞时调用
private void OnCollisionEnter(Collision collision)
{
// 检查碰撞到的物体的标签 (Tag)
if (collision.gameObject.CompareTag("Obstacle"))
{
Debug.Log("Game Over!"); // 在控制台输出信息
gameOver = true; // 设置游戏结束标志为 true
// 可以在这里添加游戏结束的效果(比如停止玩家移动、播放音效等)
}
}
}
2. 解释新增内容:
◦ public bool gameOver = false;:新增一个公共布尔变量作为游戏结束标志。初始为 false(游戏进行中)。当玩家碰到障碍物时设为 true。public 使得 SpawnManager 脚本可以读取它来决定是否停止生成障碍物。
◦ private Rigidbody playerRb;:声明一个变量来存储玩家的 Rigidbody 组件引用。物理碰撞检测需要它。
◦ void Start() { playerRb = GetComponent<Rigidbody>(); }:在游戏开始时,获取当前游戏对象 (Player) 上的 Rigidbody 组件并赋值给 playerRb。注意:我们现在还没有给玩家添加 Rigidbody!下一步会加。
◦ if (!gameOver) { ... }:在 Update 的移动逻辑外包裹一个判断。只有当 gameOver 为 false(即游戏未结束)时,才处理输入和移动。游戏结束后玩家不能再移动。
◦ private void OnCollisionEnter(Collision collision):这是 Unity 内置的碰撞事件回调方法。当附加了此脚本的对象(必须同时有 Collider 和 Rigidbody)与另一个带有 Collider 的物体发生碰撞时,这个方法会被自动调用。
◦ if (collision.gameObject.CompareTag("Obstacle")):检查与玩家发生碰撞的那个物体 (collision.gameObject) 是否带有标签 (Tag) "Obstacle"。我们需要给障碍物设置这个标签!
◦ Debug.Log("Game Over!");:在 Unity 控制台输出 "Game Over!" 信息,方便调试。
◦ gameOver = true;:将游戏结束标志设为 true。这会同时影响玩家自身的移动(Update 中的移动逻辑停止)和障碍物生成器(SpawnManager 检测到 gameOver 为 true 后停止生成)。
3. 保存脚本。
4. 给玩家添加 Rigidbody 组件:
◦ 在 Hierarchy 中选中 Player 对象。
◦ 在 Inspector 底部点击 Add Component 按钮。
◦ 搜索 Rigidbody 并添加。
◦ 在添加的 Rigidbody 组件中,通常需要取消勾选 Use Gravity。因为我们只做简单的左右移动,不需要重力让玩家下坠(除非你的游戏需要)。如果开着重力,玩家会一直下坠穿过地面!确认 Is Kinematic 也是取消勾选状态(这是用于非物理驱动的运动,我们用的是 Translate 移动,所以不用开)。
5. 给障碍物添加标签 (Tag):
◦ 在 Project 视图,选中你的 Obstacle 预制体。
◦ 在 Inspector 顶部,找到 Tag 下拉菜单(默认是 "Untagged")。
◦ 点击下拉菜单 -> Add Tag...。
◦ 在打开的 Tags & Layers 窗口中,点击 + 号添加一个新标签,命名为 "Obstacle"。
◦ 关闭 Tags & Layers 窗口。
◦ 再次选中 Obstacle 预制体,在 Inspector 的 Tag 下拉菜单中,选择我们刚刚创建的 "Obstacle" 标签。
◦ 重要: 确保点击 Inspector 顶部的 Apply 按钮!这样才能将标签的更改保存回预制体。否则,新生成的障碍物实例还是没有标签。
第六步:让世界动起来 - 模拟玩家前进
目前玩家是静止的,障碍物从远处生成后“飞”向玩家。为了模拟玩家在跑道上奔跑的感觉,更常见的做法是让玩家保持不动(只左右移动),而让整个跑道和障碍物一起向后移动。这样视觉效果就是玩家在向前跑。
1. 修改障碍物生成器脚本 (SpawnManager):
◦ 我们不再需要 SpawnManager 直接移动物体。移动逻辑将放在障碍物自身上。
2. 创建障碍物移动脚本:
◦ 在 Scripts 文件夹,右键 -> Create -> C# Script。命名为 MoveLeft。
◦ 双击打开编辑。
3. 编写障碍物(和背景)移动代码:
using UnityEngine;
public class MoveLeft : MonoBehaviour
{
public float speed = 10f; // 向左移动的速度 (public 可调)
private PlayerController playerControllerScript; // 引用玩家脚本检查游戏结束
void Start()
{
// 获取玩家脚本引用 (方式同生成器)
playerControllerScript = GameObject.Find("Player").GetComponent<PlayerController>();
}
void Update()
{
// 如果游戏未结束,让物体持续向左移动
if (!playerControllerScript.gameOver)
{
transform.Translate(Vector3.left * speed * Time.deltaTime);
}
// (可选) 如果物体移动出屏幕太远,销毁它节省资源
if (transform.position.x < -15f) // 调整数值到屏幕左边界之外
{
Destroy(gameObject); // 销毁这个游戏对象本身
}
}
}
4. 解释:
◦ public float speed = 10f;:定义向左移动的速度(正值,因为 Vector3.left 是负 X 方向)。
◦ private PlayerController playerControllerScript; 和 void Start() 中的赋值:和之前一样,获取玩家脚本引用以检查 gameOver。
◦ if (!playerControllerScript.gameOver) { transform.Translate(Vector3.left * speed * Time.deltaTime); }:如果游戏没结束,就让当前物体(障碍物或背景)每帧向左移动一段距离(速度 * 时间)。
◦ if (transform.position.x < -15f) { Destroy(gameObject); }:可选但推荐。 检查物体的 X 位置是否小于 -15(假设屏幕左边界在 -10 左右)。如果物体已经移动到屏幕左边很远看不见了,就销毁 (Destroy) 这个游戏对象,释放内存资源。注意: 这个检查基于 X 轴,如果你的物体是沿 Z 轴移动(比如背景板),需要修改条件(如 transform.position.z < -10f)。
5. 保存脚本。
6. 应用脚本到障碍物:
◦ 在 Project 视图中,选中你的 Obstacle 预制体。
◦ 点击 Inspector 底部的 Add Component 按钮,搜索 MoveLeft 并添加。
◦ 在 Move Left 组件中,可以设置障碍物的移动 Speed(比如 10)。
◦ 记得点 Apply 保存预制体修改!
7. 应用脚本到背景/地面 (可选,让地面也移动增强动感):
◦ 在 Hierarchy 中选中 Ground 对象。
◦ 点击 Add Component,搜索 MoveLeft 并添加。
◦ 设置它的 Speed(必须和障碍物的移动速度相同! 比如 10)。这样地面和障碍物以相同速度后退,感觉玩家在原地奔跑。
◦ 问题: 地面移动后,玩家会很快“跑”到地面尽头。我们需要一个机制让地面无限循环。
第七步:无限循环的背景 (可选但推荐)
为了让地面看起来无限长,我们可以使用两个(或多个)地面片段拼接,当一个片段完全移出屏幕左侧时,立即将其位置重置到最后一个片段的后方。
1. 创建第二个地面片段:
◦ 在 Hierarchy 中选中 Ground。
◦ 按 Ctrl+D (Cmd+D) 复制一份。重命名为 Ground2。
◦ 在 Inspector 的 Transform 中,设置 Ground2 的 Position 的 Z 值。它应该紧挨着第一个地面的尾部。
◦ 计算:Ground 的原始 Scale Z 是 20,Unity 物体的中心在原点。所以 Ground 的尾部在 Z = 0 + (20 / 2) * 1 = 10(因为 Scale Z=20,长度就是 20 个单位)。Ground2 的头部应该在 Z=10,所以 Ground2 的 Position Z 应该设为 10 + (20 / 2) = 20。(更直观的方法:在 Scene 视图手动移动 Ground2 到 Ground 的尾部紧挨着)。
◦ 假设 Ground 位置是 (0,0,0),Scale 是 (1,1,20),那么 Ground2 位置设为 (0,0,20)。
2. 修改 MoveLeft 脚本,添加背景循环逻辑:
◦ 双击打开 MoveLeft 脚本。
◦ 修改如下:
using UnityEngine;
public class MoveLeft : MonoBehaviour
{
public float speed = 10f;
private PlayerController playerControllerScript;
private float leftBound = -15f; // 物体销毁边界 (X轴)
private float groundLength = 40f; // 单个地面片段的长度 (Z轴方向,根据你的Scale计算)
void Start()
{
playerControllerScript = GameObject.Find("Player").GetComponent<PlayerController>();
// 如果是地面,计算它的长度 (Scale Z * 10? 注意Unity Plane默认1x1单位,缩放Scale Z=20后实际长度是20单位)
// 如果你的地面是用Cube做的且Scale Z=20,那么长度就是20。
// 这里假设是Plane且Scale Z=20 -> 长度20。如果是Cube Scale Z=40 -> 长度40。根据实际情况设置 groundLength!
// groundLength = 20f; // 取消注释并根据你的对象类型和Scale设置
}
void Update()
{
if (!playerControllerScript.gameOver)
{
transform.Translate(Vector3.left * speed * Time.deltaTime);
}
// 销毁超出左边界的物体 (主要用于障碍物)
if (transform.position.x < leftBound && gameObject.CompareTag("Obstacle"))
{
Destroy(gameObject);
}
// 背景地面循环逻辑 (假设地面对象名字包含"Ground")
if (gameObject.CompareTag("Ground")) // 给地面也设置一个"Ground"标签!
{
// 检查当前地面片段是否完全移出屏幕左侧 (假设起点在0,移到了 -groundLength/2 或更左)
// 更通用的方法是检查位置Z? 不,我们是在X方向移动背景?矛盾了!
// 重要修正!
// 我们的跑酷是物体向左移动 (X减小)。但跑道是沿Z轴铺开的。背景应该沿什么轴移动?
// 方案1 (推荐修正): 让背景沿Z轴负方向移动,模拟玩家在Z轴正方向跑。
// 修改 MoveLeft 脚本中的移动方向为 Vector3.back * speed * Time.deltaTime
// 然后检查 transform.position.z < -groundLength/2 (或其他值) 来重置位置
// 方案2 (本教程最初矛盾): 地面Scale Z=20很长,但移动方向是X。这不符合常理。
// 为了快速修复本教程的矛盾,我们临时改变策略:只循环地面在Z方向?不,移动方向是X。这很混乱。
// 重新设计移动方向以符合逻辑 (重要!)
// 放弃之前的X轴移动,改为让所有需要移动的物体(障碍物、背景)沿 Z轴负方向移动 (Vector3.back)!
// 玩家左右移动还是X轴不变。
// 修改步骤:
// 1. 在 MoveLeft 脚本中,将 transform.Translate(Vector3.left * ...) 改为 transform.Translate(Vector3.back * ...)
// 2. 在障碍物销毁判断中,将 transform.position.x < leftBound 改为 transform.position.z < -10f (或其他合适的Z值,表示物体跑到玩家后方太远了)
// 3. 在背景循环逻辑中,检查 transform.position.z < -groundLength 之类 (见下方修正后代码)
}
}
}
由于之前设计存在方向矛盾,我们进行修正:
• 目标: 玩家在 Z 轴正方向奔跑的感觉。障碍物和背景应该向 Z 轴负方向移动。
• 修改 MoveLeft 脚本 (最终修正版):
using UnityEngine;
public class MoveLeft : MonoBehaviour // 名字虽叫MoveLeft,现在其实是MoveBackward了
{
public float speed = 10f; // 现在代表向后移动的速度 (Z轴负方向)
private PlayerController playerControllerScript;
private float destroyZ = -10f; // 物体移出屏幕后方多远时销毁 (Z轴)
void Start()
{
playerControllerScript = GameObject.Find("Player").GetComponent<PlayerController>();
}
void Update()
{
if (!playerControllerScript.gameOver)
{
// 修改:沿Z轴负方向移动 (Vector3.back)
transform.Translate(Vector3.back * speed * Time.deltaTime);
}
// 修改:销毁移出后方太远的物体 (Z轴判断)
if (transform.position.z < destroyZ)
{
// 如果是障碍物,直接销毁
if (gameObject.CompareTag("Obstacle"))
{
Destroy(gameObject);
}
// 如果是地面,重置位置到队列末尾 (需要知道地面长度)
else if (gameObject.CompareTag("Ground"))
{
// 假设有两个地面片段,每个长度是 groundLength (根据Scale Z计算)
float groundLength = 40f; // 例如:Plane Scale Z=20 -> 长度20?注意Unity Plane 1x1单位实际大小是10x10? 很混乱!
// 更可靠的方法:使用Bounds或预设长度。这里简化,假设已知长度40单位。
transform.position += new Vector3(0, 0, groundLength * 2); // 向后跳两个地面的长度
}
}
}
}
• 解释修正:
◦ 移动方向改为 Vector3.back (即负 Z 轴)。
◦ 销毁条件改为 transform.position.z < destroyZ (比如 -10,表示物体在玩家后方 10 单位之外,看不见了)。
◦ 在销毁判断中:
◦ 如果是障碍物 ("Obstacle"),直接 Destroy。
◦ 如果是地面 ("Ground"),不销毁,而是将它瞬间移动到所有地面片段的最后方。transform.position += new Vector3(0, 0, groundLength * 2); 假设你有 2 个地面片段,每个长度是 groundLength (比如 40),那么将一个移出屏幕的地面片段向前(Z 轴正方向)移动 2 * groundLength 的距离(80 单位),它就跳到了最后一个地面的后面,形成无缝循环。groundLength 需要根据你实际的地面对象尺寸设置。
• 调整地面设置:
◦ 确保两个 Ground 片段 (Ground 和 Ground2) 首尾相接铺在 Z 轴上。
◦ 给它们都添加 MoveLeft 脚本(现在实际是向后移动)。
◦ 设置合适的 speed(和障碍物一样)。
◦ 在 MoveLeft 组件中,根据你的地面长度设置 groundLength 变量(可能需要硬编码或通过其他方式获取)。
◦ 给两个地面对象添加标签 "Ground" (像之前给障碍物加 "Obstacle" 标签一样)。
• 调整障碍物生成位置: 回到 SpawnManager 脚本。之前 spawnPos 的 Z 设为 20(玩家前方)。现在移动方向是 Z,玩家位置 Z≈0。所以生成在 Z=20 是合理的。X 随机化不变。
• 调整摄像机: 可能需要重新调整 Main Camera 的位置和角度,使其看向 Z 轴正方向。
第八步:计分系统 - 看看你能跑多远
1. 创建 UI 文本显示分数:
◦ 在 Hierarchy 空白处 右键 -> UI -> Text - TextMeshPro。(如果第一次用 TextMeshPro,Unity 会提示导入 TMP Essentials,点 Import)。
◦ 一个叫 Text (TMP) 的 Canvas 和 Text 对象会被创建。
◦ 选中 Text (TMP) 对象。
◦ 在 Inspector 的 Rect Transform 组件中:
◦ 调整锚点 (Anchors) 到左上角 (Top-Left)。通常点击方框,选择左上角的选项。
◦ 设置 Pos X, Pos Y (比如 50, -50) 让文本显示在屏幕左上角。
◦ 设置 Width 和 Height (比如 300, 100)。
◦ 在 TextMeshPro - Text (UI) 组件中:
◦ 在 Text Input 框输入 "Score: 0"。
◦ 调整 Font Size (比如 36),Color (比如白色),Alignment (左对齐)。
◦ 重命名这个 Text 对象为 ScoreText。
2. 创建计分管理器脚本:
◦ 在 Scripts 文件夹,右键 -> Create -> C# Script。命名为 ScoreManager。
◦ 双击打开编辑。
3. 编写计分代码:
using UnityEngine;
using TMPro; // 引入 TextMeshPro 命名空间
public class ScoreManager : MonoBehaviour
{
public TextMeshProUGUI scoreText; // 引用UI文本组件 (public 拖拽赋值)
private int score = 0; // 当前分数
public float scoreRate = 1f; // 每秒增加多少分
private float timer = 0f; // 计时器
private PlayerController playerControllerScript; // 引用玩家脚本检查游戏结束
void Start()
{
// 获取玩家脚本引用
playerControllerScript = GameObject.Find("Player").GetComponent<PlayerController>();
// 初始化分数显示
UpdateScoreText();
}
void Update()
{
// 如果游戏未结束,计时加分
if (!playerControllerScript.gameOver)
{
timer += Time.deltaTime;
if (timer >= 1f / scoreRate) // 每 (1/scoreRate) 秒加一次分
{
score += 1; // 每次加1分 (或者 Mathf.FloorToInt(scoreRate) 如果你想按rate值加)
timer = 0f; // 重置计时器
UpdateScoreText(); // 更新UI显示
}
}
}
// 更新UI文本的方法
void UpdateScoreText()
{
scoreText.text = "Score: " + score.ToString();
}
}
4. 解释:
◦ using TMPro;:必须引入这个命名空间才能使用 TextMeshPro 相关的类。
◦ public TextMeshProUGUI scoreText;:声明一个公共变量来引用我们创建的 UI 文本组件。需要在 Unity 编辑器中拖拽赋值。
◦ private int score = 0;:存储当前分数。
◦ public float scoreRate = 1f;:控制每秒增加多少分。设为 1 表示每秒加 1 分。设为 10 表示每秒加 10 分。
◦ private float timer = 0f;:一个计时器,用来计算距离上一次加分过了多久。
◦ void UpdateScoreText():一个辅助方法,将当前 score 更新到 UI 文本上。
◦ 在 Update 中:游戏未结束时,累加 timer。当 timer 累计超过 1 / scoreRate 秒时(例如 scoreRate=1 时就是 1 秒),分数 score 加 1,重置 timer,并调用 UpdateScoreText 刷新显示。
5. 保存脚本。
6. 设置计分器:
◦ 在 Hierarchy 中创建一个空对象 GameManager (或直接用 SpawnManager,但分开更好)。
◦ 将 ScoreManager 脚本拖拽到 GameManager 对象上。
◦ 选中 GameManager。
◦ 在 Inspector 的 Score Manager 组件中,你会看到 Score Text 是空的。
◦ 将 Hierarchy 中的 ScoreText 对象拖拽到 Score Text 槽上。
◦ 可以调整 Score Rate 的值(比如 1 表示每秒 1 分)。
第九步:游戏结束UI (基础版)
1. 创建游戏结束文本:
◦ 在 Hierarchy 中,选中 Canvas (应该已经存在,ScoreText 在它下面)。
◦ 右键 -> UI -> Text - TextMeshPro。创建一个新的 Text 对象。
◦ 重命名为 GameOverText。
◦ 在 Rect Transform 中:
◦ 锚点设为屏幕中心 (Middle-Center)。
◦ Pos X, Pos Y 设为 0, 0。
◦ 设置足够大的 Width 和 Height (比如 400, 200)。
◦ 在 TextMeshPro - Text (UI) 组件中:
◦ 输入 "Game Over!"。
◦ 设置大字体 (比如 72),醒目的颜色 (比如红色)。
◦ 居中对齐。
◦ 默认隐藏它: 在 Inspector 最顶部,取消勾选对象名字旁边的复选框。这样它一开始是不可见的。
2. 修改玩家控制脚本 (PlayerController):
◦ 打开 PlayerController 脚本。
◦ 添加变量和逻辑显示游戏结束文本:
using UnityEngine;
using TMPro; // 引入TMPro显示文本
public class PlayerController : MonoBehaviour
{
... // 已有的变量
public TextMeshProUGUI gameOverText; // 引用GameOverText UI (public 拖拽赋值)
public GameObject restartButton; // 引用重新开始按钮 (可选)
void Start()
{
playerRb = GetComponent<Rigidbody>();
// 确保游戏开始时不显示GameOver
gameOverText.gameObject.SetActive(false); // 隐藏文本
if (restartButton != null) restartButton.SetActive(false); // 隐藏按钮 (可选)
}
private void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("Obstacle"))
{
Debug.Log("Game Over!");
gameOver = true;
// 显示Game Over UI
gameOverText.gameObject.SetActive(true); // 显示文本
if (restartButton != null) restartButton.SetActive(true); // 显示按钮 (可选)
}
}
... // Update 等
}
3. 设置引用:
◦ 保存脚本。
◦ 在 Hierarchy 中选中 Player。
◦ 在 PlayerController 组件的 Inspector 中:
◦ 将 Game Over Text 拖拽到 Game Over Text 槽上(指向 GameOverText 对象)。
◦ (可选) 如果你创建了重新开始按钮,也拖拽到 Restart Button 槽上。
4. (可选) 创建重新开始按钮:
◦ 在 Canvas 下 右键 -> UI -> Button - TextMeshPro。
◦ 重命名为 RestartButton。
◦ 调整 Rect Transform 位置(比如在 Game Over 文字下方)。
◦ 选中 RestartButton 下的子对象 Text (TMP),修改按钮文字为 "Restart"。
◦ 添加重新开始功能:
◦ 选中 RestartButton 对象。
◦ 在 Inspector 的 Button 组件底部,点击 On Click () 事件列表的 + 号。
◦ 将 Hierarchy 中的 GameManager (或你放 ScoreManager 的对象) 拖拽到事件槽中。
◦ 在右侧下拉菜单选择函数:ScoreManager -> RestartGame() (我们还没写这个方法)。
◦ 创建重新开始方法 (在 ScoreManager 或其他管理器脚本中):
// 在 ScoreManager 脚本中添加
public void RestartGame()
{
// 1. 重置分数
score = 0;
UpdateScoreText();
// 2. 重置玩家位置和状态 (需要引用PlayerController)
PlayerController playerController = GameObject.Find("Player").GetComponent<PlayerController>();
playerController.gameOver = false;
playerController.transform.position = new Vector3(0, 0.5f, 0); // 初始位置
playerController.gameOverText.gameObject.SetActive(false); // 隐藏GameOver UI
this.gameObject.SetActive(false); // 隐藏自己(按钮) 或 playerController.restartButton.SetActive(false);
// 3. 清理场景中的障碍物 (需要给障碍物加个标签如"Obstacle", 或在SpawnManager管理)
GameObject[] obstacles = GameObject.FindGameObjectsWithTag("Obstacle");
foreach (GameObject obstacle in obstacles)
{
Destroy(obstacle);
}
// 4. (可选) 重新开始障碍物生成 (如果SpawnManager用CancelInvoke停止了)
SpawnManager spawnManager = GameObject.Find("SpawnManager").GetComponent<SpawnManager>();
// 可能需要重置SpawnManager的状态或重新调用 InvokeRepeating
spawnManager.CancelInvoke(); // 停止之前的生成
spawnManager.InvokeRepeating("SpawnObstacle", spawnManager.startDelay, spawnManager.repeatRate); // 重新开始生成
}
• 这个方法需要访问 PlayerController、SpawnManager 和清理障碍物。确保相关对象有正确的标签和引用。这是一个相对复杂的部分,新手可以先不做按钮,每次游戏结束手动按 Unity 的播放按钮重启。
第十步:最终测试与调整
1. 点击 Play 按钮!
2. 测试:
◦ 左右移动 (A/D 或箭头键) 是否流畅。
◦ 障碍物是否在正前方远处随机生成。
◦ 障碍物是否以恒定速度向玩家“飞来”(实际是玩家不动,障碍物向后移动)。
◦ 玩家碰撞到障碍物时,控制台是否打印 "Game Over!",玩家是否停止移动,障碍物是否停止生成,Game Over 文本是否显示。
◦ 分数是否每秒在增加。
◦ 地面是否无缝循环(如果实现了)。
◦ 超出屏幕的障碍物是否被销毁(不会堆积占用内存)。
◦ (可选) 点击 Restart 按钮是否能重置游戏状态。
3. 调整:
◦ 在 Inspector 中调整各种速度 (PlayerController.moveSpeed, MoveLeft.speed, SpawnManager.repeatRate, SpawnManager.startDelay),找到合适的难度和节奏。
◦ 调整障碍物生成 X 坐标范围 (Random.Range(-x, x)) 匹配跑道宽度。
◦ 调整摄像机角度和位置获得最佳视角。
◦ 调整 UI 元素的位置、大小和颜色。
恭喜!你已经完成了一个基础的 Unity 跑酷小游戏!
后续扩展方向 (学有余力)
• 美术升级: 使用免费或购买的 3D 模型替换方块和地面。添加玩家奔跑动画、跳跃动画、障碍物被撞动画。
• 音效: 添加背景音乐、跳跃音效、碰撞音效、得分音效。使用 AudioSource 组件。
• 跳跃功能: 修改 PlayerController,检测空格键按下,给玩家的 Rigidbody 施加一个向上的力 (AddForce),实现跳跃躲避低矮障碍物。注意添加地面检测防止无限跳。
• 多种障碍物: 创建不同形状、大小、需要不同方式(跳/蹲/左右)躲避的障碍物预制体,修改 SpawnManager 随机生成它们。
• 道具系统: 生成磁铁(吸分数)、护盾(抵挡一次碰撞)、加速等道具。
• 更酷的 UI: 添加开始菜单、暂停菜单、高分榜。
• 手机触控: 使用 Unity 的 Input System 或 Touch API 实现手机屏幕上的左右滑动控制。
• 粒子效果: 玩家奔跑时的尘土、碰撞时的爆炸效果。使用 Particle System。
到这里这个保姆级教程就涵盖了从零开始制作一个完整小游戏的核心流程和关键代码啦。动手实践,相信自己,动手试一试,看你也可以!遇到问题查阅 Unity 文档或搜索解决方案或者评论区留言,你的努力是学习游戏开发的最佳途径!祝你开发顺利!♥️
(附加)没有素材的同学们可以借鉴unity的官方商店下载使用哦!