Unity 2D 从零精通:第二阶段 - 核心机制入门,让世界拥有物理法则
引言:从静态画面到动态交互
欢迎回来!在第一阶段,我们成功创建了一个静态场景,并让一艘飞船响应我们的键盘输入。这很棒,但它还不是一个“游戏”。游戏的核心在于交互——物体之间的碰撞、得分、失败、胜利条件等等。
在这一篇,我们将为我们的小宇宙注入灵魂:物理。我们将学习Unity强大的2D物理引擎,让物体不再只是冰冷的图片,而是拥有体积、质量,并能相互碰撞的实体。我们的终极目标是,将上一篇的简单场景,升级为一个完整的、可玩的小游戏原型:玩家控制飞船发射子弹,击中外星行星即可得分。
第一章:理解Unity的2D物理引擎
在现实生活中,物体之所以会下落、碰撞、反弹,是因为重力和材质属性。在Unity中,我们通过为游戏对象添加特定的物理组件来模拟这些行为。
1.1 核心组件:刚体(Rigidbody 2D)
刚体(Rigidbody 2D) 是物理系统的核心。任何一个想要受物理引擎控制的游戏对象(例如受重力影响、被力推动、与其他刚体碰撞),都必须附加此组件。
- 功能:它赋予对象物理属性,如质量(Mass)、阻力(Drag)、重力缩放(Gravity Scale)等。
- 重要提示:在2D项目中,务必使用
Rigidbody 2D,而不是Rigidbody。Rigidbody是用于3D物理的,二者不能混用。
1.2 定义形状:碰撞体(Collider 2D)
仅有刚体,物体还只是一个有质量的“点”。我们需要用碰撞体(Collider 2D) 来定义它的物理形状。
- 功能:它是一个不可见的形状,用于物理引擎检测碰撞。常见的2D碰撞体有:
Box Collider 2D:矩形,适用于箱子、平台、墙壁。Circle Collider 2D:圆形,适用于球、行星、子弹。Capsule Collider 2D:胶囊形,适用于角色。Polygon Collider 2D:可自定义的多边形,适用于复杂形状。
- 重要提示:一个物体可以没有刚体只有碰撞体(作为静态的障碍物),但一个有刚体的物体必须至少有一个碰撞体才能参与碰撞。
1.3 碰撞 vs. 触发(Trigger)
这是两个极易混淆但至关重要的概念。
- 普通碰撞(Collision):两个带有碰撞体的刚体相遇时,物理引擎会计算碰撞效果,阻止它们相互穿透,并根据物理属性(如弹性)产生反弹。这是默认行为。
- 触发(Trigger):如果你勾选了碰撞体上的
Is Trigger复选框,该碰撞体就变成了一个“触发器”。物理引擎将忽略它的物理碰撞效果(物体可以穿透它),但会发送一个特殊的消息,允许你编写代码来响应“穿透”这个事件。- 典型用途:收集物品、检测玩家进入某个区域(如陷阱或 checkpoint)、子弹击中目标。
第二章:实践一:为行星添加物理属性
让我们先从简单的开始,为上个场景中的行星添加物理属性,让它成为一个“实体”。
- 选中行星:在Hierarchy中选中
Planet对象。 - 添加刚体:在Inspector中点击
Add Component,搜索并添加Rigidbody 2D。 - 配置刚体:添加后,你会看到一系列属性。为了让它成为一个静态的、不会被移动的障碍物,我们将
Body Type从Dynamic(动态,受物理影响)改为Static(静态,不受力影响,性能更好)。对于背景中的行星,这是合适的选择。
现在,我们的行星已经有了物理存在感!虽然它现在是静态的不会动,但它已经准备好被其他动态物体撞击了。
第三章:创建子弹与预制体(Prefab)
我们的飞船需要武器。我们将创建子弹,并引入Unity中一个极其重要的概念——预制体(Prefab)。
3.1 创建子弹对象
- 在Hierarchy中右键 ->
2D Object -> Sprite,创建一个新的Sprite对象。 - 重命名为
Bullet。 - 在它的Sprite Renderer中,分配一个简单的子弹图片(可以是一个小圆点或小矩形)。如果没有,可以在Project窗口右键 ->
Create -> Sprites -> Square创建一个Unity自带的白色方形,然后在其Sprite Renderer的Color属性中把它调成红色。 - 使用缩放工具(按R键)把它调小。
- 添加物理组件:
- 添加
Rigidbody 2D。保持其Body Type为Dynamic(我们希望子弹被发射出去)。 - 添加
Circle Collider 2D(或Box Collider 2D)。你会看到一个绿色的线框出现在子弹上,这就是碰撞体的形状。确保它大致匹配子弹的视觉大小。
- 添加
- 配置子弹物理:我们希望子弹快速飞行,不受重力影响,并且不会因为旋转而变得奇怪。
- 在
Rigidbody 2D组件中:- 将
Gravity Scale设置为0(子弹不应下落)。 - 勾选
Freeze RotationZ(冻结Z轴旋转,防止子弹在空中翻滚)。
- 将
- 在
3.2 创建子弹脚本
子弹需要自动向上移动。
- 在
Scripts文件夹中,创建一个新的C#脚本,命名为Bullet。 - 双击打开并编辑它:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bullet : MonoBehaviour
{
public float speed = 10f; // 子弹速度
// Update is called once per frame
void Update()
{
// 每帧向上移动(在2D世界中,Y轴是上下)
// Time.deltaTime 用于保证移动速度与帧率无关
transform.Translate(Vector2.up * speed * Time.deltaTime);
}
}
- 保存脚本,并将其附加到Hierarchy中的
Bullet对象上。
3.3 神圣的预制体(Prefab)
现在我们有一个完美的子弹对象。但问题是:我们需要在游戏运行时动态地创建(实例化) 很多颗子弹,而不是在编辑场景时预先摆好。
这就是预制体(Prefab) 的用武之地。预制体是一个存储在Project视图中的游戏对象模板。你可以把它想象成一个“蓝图”。当你需要这个对象时,你可以通过代码根据这个“蓝图”快速生成一个它的复制品。
创建预制体:
-
在Project窗口的
Assets文件夹下,创建一个名为Prefabs的新文件夹。 -
将Hierarchy中的
Bullet对象直接拖拽到Project窗口的Prefabs文件夹中。 -
成功后,
Bullet在Project中的图标会变成蓝色,而Hierarchy中的Bullet对象名字也会变成蓝色。这表示它现在是预制体的一个实例。 -
重要: 现在,从Hierarchy中删除这个
Bullet实例。我们不再需要它存在于初始场景中,我们只保留它的“蓝图”(预制体),在需要时通过代码生成。
第四章:发射子弹——实例化与输入
现在,我们需要修改玩家的脚本,使其在按下按键(比如空格键)时,在飞船的位置创建一个子弹预制体。
4.1 修改玩家脚本(PlayerMovement)
重新打开PlayerMovement脚本,我们将在其中添加发射逻辑。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
public float moveSpeed = 5f;
public float horizontalLimit = 8f;
// 新增:声明一个公共变量来引用子弹预制体
public GameObject bulletPrefab;
// 新增:子弹生成点的偏移量,让子弹从飞船头部而不是中心发射
public Transform firePoint;
// 新增:发射冷却时间,防止连续快速发射
public float fireRate = 0.5f;
private float nextFireTime = 0f;
void Update()
{
// 原有的移动代码
HandleMovement();
// 新增:发射输入检测
HandleShooting();
}
void HandleMovement()
{
float horizontalInput = Input.GetAxis("Horizontal");
Vector3 movement = new Vector3(horizontalInput, 0f, 0f);
transform.Translate(movement * moveSpeed * Time.deltaTime);
Vector3 currentPosition = transform.position;
currentPosition.x = Mathf.Clamp(currentPosition.x, -horizontalLimit, horizontalLimit);
transform.position = currentPosition;
}
void HandleShooting()
{
// 检查是否按下了发射键(这里用空格键)并且冷却时间已过
if (Input.GetKey(KeyCode.Space) && Time.time >= nextFireTime)
{
// 发射子弹!
Shoot();
// 设置下一次允许发射的时间
nextFireTime = Time.time + fireRate;
}
}
void Shoot()
{
// 关键方法:Instantiate实例化
// 参数一:要创建的对象(我们的子弹预制体)
// 参数二:创建对象的位置(firePoint的位置)
// 参数三:创建对象的旋转(firePoint的旋转,这里用Quaternion.identity表示无旋转)
Instantiate(bulletPrefab, firePoint.position, firePoint.rotation);
}
}
4.2 在Unity中设置
- 保存脚本并返回Unity。
- 选中Hierarchy中的
PlayerShip对象。 - 指定预制体:在Inspector中,你会看到
PlayerMovement脚本组件多出了几个新字段。将Project窗口中Prefabs文件夹里的Bullet预制体,拖拽到Bullet Prefab这个字段上。 - 创建发射点:
- 在Hierarchy中右键点击
PlayerShip->Create Empty。这将创建一个空的子对象。重命名为FirePoint。 - 使用移动工具(W),将这个
FirePoint对象拖到飞船精灵的顶部,作为子弹生成的位置。 - 确保
FirePoint在Inspector中的Rotation的Z值是0。
- 在Hierarchy中右键点击
- 指定发射点:选中
PlayerShip,在Inspector的PlayerMovement组件中,将刚刚创建的FirePoint对象拖拽到Fire Point字段上。
现在,点击播放!你应该可以用空格键发射子弹了。子弹会从飞船头部生成并向上飞行。
第五章:碰撞检测——让子弹击中目标
现在子弹穿过了行星,没有任何事情发生。我们需要检测碰撞并做出反应。我们将使用触发(Trigger) 方式,因为子弹击中行星后,行星不应该有物理反弹,而是应该被“销毁”。
5.1 设置碰撞体为触发器
- 选中Project窗口中
Prefabs文件夹里的Bullet预制体。 - 在Inspector中,找到它的
Circle Collider 2D组件,勾选Is Trigger。这样它就不会物理阻挡物体,但会发送触发消息。 - 选中Hierarchy中的
Planet对象,确保它的碰撞体没有勾选Is Trigger(我们希望它是一个实心的障碍物)。
5.2 编写碰撞检测代码
当两个碰撞体相遇,且其中至少一个是触发器(Trigger)时,Unity会自动在相应的脚本中调用特定的方法。我们需要在Bullet脚本中编写这些方法。
重新打开Bullet脚本,添加以下代码:
public class Bullet : MonoBehaviour
{
public float speed = 10f;
void Update()
{
transform.Translate(Vector2.up * speed * Time.deltaTime);
// 新增:如果子弹飞得太远,自动销毁它,避免无限生成造成性能问题
if(transform.position.y > 10f)
{
Destroy(gameObject);
}
}
// 新增:关键方法!当另一个带有碰撞体的对象进入(穿透)这个触发器时调用
private void OnTriggerEnter2D(Collider2D collision)
{
// collision参数代表的是“谁”撞入了我们
// 我们可以检查撞入物体的标签(Tag)来判断它是什么
if (collision.gameObject.CompareTag("Planet"))
{
Debug.Log("Bullet hit a planet!"); // 在控制台打印信息,用于调试
// 销毁被击中的行星
Destroy(collision.gameObject);
// 销毁子弹本身
Destroy(gameObject);
}
}
}
5.3 设置标签(Tag)
代码中检查了碰撞对象的标签是否为"Planet"。我们需要给行星对象设置这个标签。
- 在Hierarchy中选中
Planet对象。 - 在Inspector顶部,点击
Tag下拉框 ->Add Tag...。 - 在出现的面板中,点击
+号,添加一个新标签,命名为Planet。 - 再次选中
Planet对象,在Tag下拉框中选择我们刚刚创建的Planet标签。
现在,再次播放游戏!控制飞船,发射子弹。当子弹击中行星时,行星和子弹都应该消失,并且在Unity编辑器底部的控制台(Console窗口)会看到"Bullet hit a planet!"的调试信息。
恭喜!你已经实现了游戏最核心的交互循环!
第六章:完善游戏——计分系统
一个完整的游戏需要有目标。让我们添加一个简单的计分系统,每摧毁一个行星就加10分。
6.1 创建游戏管理器(Game Manager)
我们将使用单例模式(Singleton) 来创建一个全局可访问的GameManager,这是管理游戏全局状态(如分数、生命、游戏状态)的最佳实践。
- 在
Scripts文件夹中创建新的C#脚本,命名为GameManager。 - 双击打开并编辑:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; // 引入UI命名空间,用于操作Text组件
public class GameManager : MonoBehaviour
{
// 单例实例,允许其他脚本轻松访问
public static GameManager instance;
// 当前分数
private int score = 0;
// 用于显示分数的UI Text组件
public Text scoreText;
// Awake在Start之前调用
void Awake()
{
// 实现单例模式
if (instance == null)
{
instance = this; // 如果实例不存在,设为自己
}
else
{
Destroy(gameObject); // 如果实例已存在,销毁自己,防止重复
}
}
// 一个公共方法,让其他脚本(如Bullet)可以调用它来加分
public void AddScore(int pointsToAdd)
{
score += pointsToAdd; // 增加分数
UpdateScoreUI(); // 更新UI显示
}
// 更新UI上的分数文本
void UpdateScoreUI()
{
if (scoreText != null)
{
scoreText.text = "Score: " + score;
}
}
}
6.2 创建UI显示分数
- 在Hierarchy中右键 ->
UI -> Text。这会创建一个Canvas(画布)和一个子对象Text。 - 选中
Text对象,在Inspector中:- 在
Rect Transform中,你可以调整其锚点(Anchor)到屏幕左上角。 - 在
Text (Script)组件中:- 输入
Text字段为Score: 0。 - 调整
Font Size、Color,使其清晰可见。
- 输入
- 在
6.3 连接计分系统
- 指定UI Text:
- 在Hierarchy中创建一个空的GameObject,重命名为
GameManager。 - 将
GameManager脚本附加到它上面。 - 将Hierarchy中
Canvas下的Text对象,拖拽到GameManager的Score Text字段上。
- 在Hierarchy中创建一个空的GameObject,重命名为
- 修改Bullet脚本,在击中时调用计分:
重新打开Bullet脚本,修改OnTriggerEnter2D方法:
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.gameObject.CompareTag("Planet"))
{
// 在销毁行星前,通知GameManager加分
GameManager.instance.AddScore(10); // 加10分
Destroy(collision.gameObject);
Destroy(gameObject);
}
}
再次播放游戏!现在,当你击毁行星时,屏幕左上角的分数会不断增加!
第七章:总结与展望
你在本篇取得的巨大成就:
- 理解了Unity 2D物理引擎的核心组件:
Rigidbody 2D和Collider 2D。 - 掌握了碰撞(Collision)与触发(Trigger) 的关键区别与应用场景。
- 学习并实践了至关重要的Prefab(预制体)概念,以及如何使用
Instantiate()方法动态生成对象。 - 实现了复杂的交互逻辑:通过脚本检测输入、实例化对象、处理物理触发事件。
- 引入了游戏架构思想:使用单例模式的
GameManager来管理全局游戏状态(分数)。 - 创建了你的第一个完整游戏原型!一个功能齐全的射击小游戏。
下一篇预告:《第三阶段:2D的灵魂——艺术与动画》
静态的Sprite已经无法满足我们了!在下一篇文章中,我们将:
- 深入学习Sprite Editor的使用,如切片(Slicing)和设置枢轴点(Pivot)。
- 为我们的飞船创建流畅的动画(Animation Clip),比如 idle 待机和喷气推进。
- 学习使用动画器(Animator Controller) 来管理不同状态之间的切换(例如,通过代码控制何时播放奔跑动画)。
- 让我们游戏的外观真正“活”起来。
练习与思考:
- 尝试为子弹添加一个
Tag,并为行星编写代码,使得如果行星被子弹以外的物体(比如玩家自己)撞到,也会被销毁。 - 添加一个“游戏结束”的条件,比如玩家撞到行星游戏就结束。(提示:可以参考子弹的触发检测,写在玩家的脚本里)。
- 让行星不再是静态的,而是动态的(Dynamic),并给它一个初始速度,让它从屏幕上方坠落,形成一个躲避游戏。
你的游戏开发技能正在飞速成长。保持热情,我们下一阶段见!
1086

被折叠的 条评论
为什么被折叠?



