【Unity2D系列】【2】核心机制入门,让世界拥有物理法则。


Unity 2D 从零精通:第二阶段 - 核心机制入门,让世界拥有物理法则

引言:从静态画面到动态交互

欢迎回来!在第一阶段,我们成功创建了一个静态场景,并让一艘飞船响应我们的键盘输入。这很棒,但它还不是一个“游戏”。游戏的核心在于交互——物体之间的碰撞、得分、失败、胜利条件等等。

在这一篇,我们将为我们的小宇宙注入灵魂:物理。我们将学习Unity强大的2D物理引擎,让物体不再只是冰冷的图片,而是拥有体积、质量,并能相互碰撞的实体。我们的终极目标是,将上一篇的简单场景,升级为一个完整的、可玩的小游戏原型:玩家控制飞船发射子弹,击中外星行星即可得分。

第一章:理解Unity的2D物理引擎

在现实生活中,物体之所以会下落、碰撞、反弹,是因为重力和材质属性。在Unity中,我们通过为游戏对象添加特定的物理组件来模拟这些行为。

1.1 核心组件:刚体(Rigidbody 2D)

刚体(Rigidbody 2D) 是物理系统的核心。任何一个想要受物理引擎控制的游戏对象(例如受重力影响、被力推动、与其他刚体碰撞),都必须附加此组件。

  • 功能:它赋予对象物理属性,如质量(Mass)、阻力(Drag)、重力缩放(Gravity Scale)等。
  • 重要提示:在2D项目中,务必使用 Rigidbody 2D,而不是 RigidbodyRigidbody 是用于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)、子弹击中目标。
第二章:实践一:为行星添加物理属性

让我们先从简单的开始,为上个场景中的行星添加物理属性,让它成为一个“实体”。

  1. 选中行星:在Hierarchy中选中Planet对象。
  2. 添加刚体:在Inspector中点击 Add Component,搜索并添加 Rigidbody 2D
  3. 配置刚体:添加后,你会看到一系列属性。为了让它成为一个静态的、不会被移动的障碍物,我们将 Body TypeDynamic(动态,受物理影响)改为 Static(静态,不受力影响,性能更好)。对于背景中的行星,这是合适的选择。

现在,我们的行星已经有了物理存在感!虽然它现在是静态的不会动,但它已经准备好被其他动态物体撞击了。

第三章:创建子弹与预制体(Prefab)

我们的飞船需要武器。我们将创建子弹,并引入Unity中一个极其重要的概念——预制体(Prefab)

3.1 创建子弹对象
  1. 在Hierarchy中右键 -> 2D Object -> Sprite,创建一个新的Sprite对象。
  2. 重命名为Bullet
  3. 在它的Sprite Renderer中,分配一个简单的子弹图片(可以是一个小圆点或小矩形)。如果没有,可以在Project窗口右键 -> Create -> Sprites -> Square 创建一个Unity自带的白色方形,然后在其Sprite Renderer的Color属性中把它调成红色。
  4. 使用缩放工具(按R键)把它调小。
  5. 添加物理组件
    • 添加 Rigidbody 2D。保持其 Body TypeDynamic(我们希望子弹被发射出去)。
    • 添加 Circle Collider 2D(或Box Collider 2D)。你会看到一个绿色的线框出现在子弹上,这就是碰撞体的形状。确保它大致匹配子弹的视觉大小。
  6. 配置子弹物理:我们希望子弹快速飞行,不受重力影响,并且不会因为旋转而变得奇怪。
    • Rigidbody 2D 组件中:
      • Gravity Scale 设置为 0(子弹不应下落)。
      • 勾选 Freeze Rotation Z(冻结Z轴旋转,防止子弹在空中翻滚)。
3.2 创建子弹脚本

子弹需要自动向上移动。

  1. Scripts文件夹中,创建一个新的C#脚本,命名为Bullet
  2. 双击打开并编辑它:
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);
    }
}
  1. 保存脚本,并将其附加到Hierarchy中的Bullet对象上。
3.3 神圣的预制体(Prefab)

现在我们有一个完美的子弹对象。但问题是:我们需要在游戏运行时动态地创建(实例化) 很多颗子弹,而不是在编辑场景时预先摆好。

这就是预制体(Prefab) 的用武之地。预制体是一个存储在Project视图中的游戏对象模板。你可以把它想象成一个“蓝图”。当你需要这个对象时,你可以通过代码根据这个“蓝图”快速生成一个它的复制品。

创建预制体:

  1. 在Project窗口的Assets文件夹下,创建一个名为Prefabs的新文件夹。

  2. 将Hierarchy中的Bullet对象直接拖拽到Project窗口的Prefabs文件夹中。

  3. 成功后,Bullet在Project中的图标会变成蓝色,而Hierarchy中的Bullet对象名字也会变成蓝色。这表示它现在是预制体的一个实例

  4. 重要: 现在,从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中设置
  1. 保存脚本并返回Unity。
  2. 选中Hierarchy中的PlayerShip对象。
  3. 指定预制体:在Inspector中,你会看到PlayerMovement脚本组件多出了几个新字段。将Project窗口中Prefabs文件夹里的Bullet预制体,拖拽Bullet Prefab 这个字段上。
  4. 创建发射点
    • 在Hierarchy中右键点击PlayerShip -> Create Empty。这将创建一个空的子对象。重命名为FirePoint
    • 使用移动工具(W),将这个FirePoint对象拖到飞船精灵的顶部,作为子弹生成的位置。
    • 确保FirePoint在Inspector中的Rotation的Z值是0
  5. 指定发射点:选中PlayerShip,在Inspector的PlayerMovement组件中,将刚刚创建的FirePoint对象拖拽Fire Point 字段上。

现在,点击播放!你应该可以用空格键发射子弹了。子弹会从飞船头部生成并向上飞行。

第五章:碰撞检测——让子弹击中目标

现在子弹穿过了行星,没有任何事情发生。我们需要检测碰撞并做出反应。我们将使用触发(Trigger) 方式,因为子弹击中行星后,行星不应该有物理反弹,而是应该被“销毁”。

5.1 设置碰撞体为触发器
  1. 选中Project窗口中Prefabs文件夹里的Bullet预制体。
  2. 在Inspector中,找到它的Circle Collider 2D组件,勾选 Is Trigger。这样它就不会物理阻挡物体,但会发送触发消息。
  3. 选中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"。我们需要给行星对象设置这个标签。

  1. 在Hierarchy中选中Planet对象。
  2. 在Inspector顶部,点击Tag下拉框 -> Add Tag...
  3. 在出现的面板中,点击+号,添加一个新标签,命名为Planet
  4. 再次选中Planet对象,在Tag下拉框中选择我们刚刚创建的Planet标签。

现在,再次播放游戏!控制飞船,发射子弹。当子弹击中行星时,行星和子弹都应该消失,并且在Unity编辑器底部的控制台(Console窗口)会看到"Bullet hit a planet!"的调试信息。

恭喜!你已经实现了游戏最核心的交互循环!

第六章:完善游戏——计分系统

一个完整的游戏需要有目标。让我们添加一个简单的计分系统,每摧毁一个行星就加10分。

6.1 创建游戏管理器(Game Manager)

我们将使用单例模式(Singleton) 来创建一个全局可访问的GameManager,这是管理游戏全局状态(如分数、生命、游戏状态)的最佳实践。

  1. Scripts文件夹中创建新的C#脚本,命名为GameManager
  2. 双击打开并编辑:
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显示分数
  1. 在Hierarchy中右键 -> UI -> Text。这会创建一个Canvas(画布)和一个子对象Text
  2. 选中Text对象,在Inspector中:
    • Rect Transform中,你可以调整其锚点(Anchor)到屏幕左上角。
    • Text (Script)组件中:
      • 输入Text字段为Score: 0
      • 调整Font SizeColor,使其清晰可见。
6.3 连接计分系统
  1. 指定UI Text
    • 在Hierarchy中创建一个空的GameObject,重命名为GameManager
    • GameManager脚本附加到它上面。
    • 将Hierarchy中Canvas下的Text对象,拖拽到GameManager的Score Text字段上。
  2. 修改Bullet脚本,在击中时调用计分
    重新打开Bullet脚本,修改OnTriggerEnter2D方法:
private void OnTriggerEnter2D(Collider2D collision)
{
    if (collision.gameObject.CompareTag("Planet"))
    {
        // 在销毁行星前,通知GameManager加分
        GameManager.instance.AddScore(10); // 加10分

        Destroy(collision.gameObject);
        Destroy(gameObject);
    }
}

再次播放游戏!现在,当你击毁行星时,屏幕左上角的分数会不断增加!

第七章:总结与展望

你在本篇取得的巨大成就:

  1. 理解了Unity 2D物理引擎的核心组件:Rigidbody 2DCollider 2D
  2. 掌握了碰撞(Collision)与触发(Trigger) 的关键区别与应用场景。
  3. 学习并实践了至关重要的Prefab(预制体)概念,以及如何使用Instantiate()方法动态生成对象。
  4. 实现了复杂的交互逻辑:通过脚本检测输入、实例化对象、处理物理触发事件。
  5. 引入了游戏架构思想:使用单例模式GameManager来管理全局游戏状态(分数)。
  6. 创建了你的第一个完整游戏原型!一个功能齐全的射击小游戏。

下一篇预告:《第三阶段:2D的灵魂——艺术与动画》
静态的Sprite已经无法满足我们了!在下一篇文章中,我们将:

  • 深入学习Sprite Editor的使用,如切片(Slicing)和设置枢轴点(Pivot)。
  • 为我们的飞船创建流畅的动画(Animation Clip),比如 idle 待机和喷气推进。
  • 学习使用动画器(Animator Controller) 来管理不同状态之间的切换(例如,通过代码控制何时播放奔跑动画)。
  • 让我们游戏的外观真正“活”起来。

练习与思考:

  1. 尝试为子弹添加一个Tag,并为行星编写代码,使得如果行星被子弹以外的物体(比如玩家自己)撞到,也会被销毁。
  2. 添加一个“游戏结束”的条件,比如玩家撞到行星游戏就结束。(提示:可以参考子弹的触发检测,写在玩家的脚本里)。
  3. 让行星不再是静态的,而是动态的(Dynamic),并给它一个初始速度,让它从屏幕上方坠落,形成一个躲避游戏。

你的游戏开发技能正在飞速成长。保持热情,我们下一阶段见!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值