首先,我们将 Atlas 素材里一开始我们准备好的 player_1 素材拖曳到 Hierarchy 面板。这时候就会产生第二个角色。我们在 Hierarchy 面板中选中刚刚加入的 player_1,然后在 Inspector 面板中,最顶端的 player_1 改为 Test_NPC。以区分他是玩家或是 NPC,然后我们将他的坐标(Position)的 X 轴修改为 -0.2,让他出现在玩家左侧。

接下来,在 Inspector 面板中,点击 Add Component 按钮,添加一个 BoxCollider2D 元件,该元件就是用来检测物体碰撞,碰撞包括:触碰到NPC和其对话、触碰到怪物造成伤害、触碰到墙体无法继续前进等等。调整框的大小,这里我选择和我的玩家一样。 调整参数为:Offset X: 0, Y: -0.01,Size X: 0.1, Y: 0.1

制作地板

我们点击 Atlas,然后在 Inspector 面板中点击 Sprite Editor。我们找到两个合适的图标作为地板和墙体。我选择了深色的块作为墙壁,并将其命名为 wall_0;然后选择另一个较为浅色的块作为地板,将其命名为 floor_0。切割完毕之后,点击 Apply,这时候发现 Atlas 图集的小箭头多出了两个图标。

当我们将 floor_0 图块直接拖拽到 Hierarchy 面板上,默认他会出现在原点(x = 0, y = 0),也就是我们玩家的位置。这时候你会发现,我们的玩家被这个图块挡住了。

修改渲染图层

为了解决这个问题,我们需要知道一个概念:图层。在 Hierarchy 面板选择 player_0,在 Inspector 面板定位到 Sprite Renderer 元件的 Sorting Layer 项(如果没有这个属性,则是隐藏在 Additional Layer 里)点开下拉框,然后点击 Add Sorting Layer。这时候 Inspector 面板将会跳转至 Tags & Layer,并且展开 Sorting Layers 属性列。

在 Sorting Layer 点击右侧的 + 号,新建一个图层 Actor(角色)。添加完毕后,我们再点击 Hierarchy 面板中的 player_0,再找到 Inspector 面板里的 Sorting Layer。点开下拉框选择刚刚添加的 Actor,然后你的 玩家就会出现在地板前面了!

同理,对 NPC 也要执行同样的操作。

接下来,我们需要再继续复制粘贴几个地板,然后再把墙壁放上去。结果大概这样(如果你有强迫症,每个区块之间的间隔为0.16)我们现在用比较笨的方法去弄这个,后续将会使用 Tile Palette 功能进行地图绘制。

碰撞的概念

首先我们要知道几个需求,地板是没有 BoxCollider 的,所以不被考虑在内:

  1. 玩家不可经过踩上去的地方(碰到这些,玩家会被阻拦)
    1. 墙体
    2. NPC
  2. 玩家可以踩上去的地方(触发某些机制)
    1. 开关
    2. 回复泉水
    3. 陷阱

所以,我们需要增加两个图层(Layer)。这和 Sorting Layer 是不同的,Sorting Layer 是关于渲染的层级关系,而 Layer 可以理解为一个组别,我们需要实现的是:当玩家碰上这些块的时候,会出现什么事情。

现在,我们选择 Hierarchy 界面的 wall_0 然后在 Inspector 界面的 Layer 下拉菜单中点击 Add Layer。我们在 User Layer 中依次添加 Blocking 和 Actor 层。

点击 Hierarchy 面板中的 wall_0,然后按住 CTRL 选择 wall_1(可以多选)。如果某些需要选择的物体在 Hierarchy 面板中是连续的,可以直接长按 Shift 多选。然后 Layer 下拉菜单中选择 Blocking,这将作为我们玩家的障碍物。然后我们也要给他俩加上 BoxCollider2D 元件。

之后再选择 player_0 和 Test_NPC,将 Layer 切换为刚刚创建的 Actor 层。

别担心,之后不会那么麻烦。我们会把这些琐碎的事情交给 Tilemap 来做,但是现在为了让大家理解原理,就先搞得麻烦一点。

有了碰撞体之后,还是不能阻止玩家经过的。我们需要让角色知道“哦,这是墙体,我不能穿过去”。那要让角色知道的事情,就需要用代码来完成了。

编程

碰撞检测的编程思路是这样的:我们需要在移动的时候,往移动的方向前方投影一个看不见的方块,如果这个方块检测到障碍物,那么人物就不能向前移动。

所以我们需要声明 RaycastHit2D hit; 变量来检测。

我们在之前朝向切换和开始移动之间写上一段代码,具体位置在这里:

if (moveDelta.x > 0)
{
transform.localScale = Vector3.one;
}
else if (moveDelta.x < 0)
{
transform.localScale = new Vector3(-1, 1, 1);
}

/// 在这里添加新代码 ///

transform.translate(moveDelta * Time.DeltaTime);

然后我们说到,RaycastHit2D 的作用是放置一个投影,用于检测前方是否有障碍物,所以我们调用 Physic2D.BoxCast() 函数,创建一个投影块。具体检测两个轴,x 和 y。我们先检测 y 轴(上下),BoxCast() 函数需要传递 6 个参数,分别是:

  1. 投影块要出现的位置:角色的位置
  2. 投影块的大小:角色的 BoxCollider2D 的大小
  3. 角度:角色移动不需要角度,所以传递 0
  4. 方向:所需要计算的方向(这里只能计算一个轴的方向(上下、左右))。它的计算判断应该是取 x y 轴的点,然后算与原点的角度。
  5. 投影块的距离:我们传递绝对值,这样不管是向左走(-1)还是向右走(1)都能为 1
  6. 需要检测的碰撞体所在图层:检测 Blocking 和 Actor 层

当检测的投影块碰撞(collider)为空,则可以移动。代码如下

// 放置一个方块来检测,确保我们能够朝这个方向移动。如果检测为空,则表示可以移动
hit = Physics2D.BoxCast(
transform.position, // 角色的位置
boxCollider.size, // 碰撞体的大小
0, // 角色不需要移动角度,所以为 0
new Vector2(0, moveDelta.y), // 方向计算其中一个方向 moveDelta.y
Mathf.Abs(moveDelta.y * Time.deltaTime), // 检测距离为我们需要移动的距离
LayerMask.GetMask("Actor", "Blocking")); // 需要检测的层

// 如果检测到空的
if (hit.collider == null)
{
// 让他动起来
// 因为我们只检测 y 轴,所以我们要修改为 y 轴
transform.Translate(0, moveDelta.y * Time.deltaTime, 0);
}

同样,我们也需要给 x 轴有同样的操作:

// 放置一个方块来检测,确保我们能够朝这个方向移动。如果检测为空,则表示可以移动
hit = Physics2D.BoxCast(
transform.position, // 角色的位置
boxCollider.size, // 碰撞体的大小
0, // 角色不需要移动角度,所以为 0
new Vector2(moveDelta.x, 0), // 这个是检测 x 轴的,所以需要改
Mathf.Abs(moveDelta.x * Time.deltaTime), // 这个是检测 x 轴的,所以需要改
LayerMask.GetMask("Actor", "Blocking")); // 需要检测的层

// 如果检测到空的
if (hit.collider == null)
{
// 因为检测 x 轴,所以我们要修改为 x 轴
transform.Translate(moveDelta.x * Time.deltaTime, 0, 0);
}

修改后的完整代码如下

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{
private BoxCollider2D boxCollider;
private Vector3 moveDelta;
private RaycastHit2D hit; // 用于碰撞检测

public void Start()
{
boxCollider = GetComponent<BoxCollider2D>();
}

public void FixedUpdate()
{
float x = Input.GetAxisRaw("Horizontal");
float y = Input.GetAxisRaw("Vertical");

moveDelta = new Vector3(x, y, 0);

if (moveDelta.x > 0)
{
transform.localScale = Vector3.one;
}
else if (moveDelta.x < 0)
{
transform.localScale = new Vector3(-1, 1, 1);
}

// 放置一个方块来检测,确保我们能够朝这个方向移动。如果检测为空,则表示可以移动
hit = Physics2D.BoxCast(
transform.position, // 角色的位置
boxCollider.size, // 碰撞体的大小
0, // 角色不需要移动角度,所以为 0
new Vector2(0, moveDelta.y), // 方向计算其中一个方向 moveDelta.y
Mathf.Abs(moveDelta.y * Time.deltaTime), // 检测距离为我们需要移动的距离
LayerMask.GetMask("Actor", "Blocking")); // 需要检测的层

// 如果检测到空的
if (hit.collider == null)
{
// 让他动起来
// 因为我们只检测 y 轴,所以我们要修改为 y 轴
transform.Translate(0, moveDelta.y * Time.deltaTime, 0);
}


hit = Physics2D.BoxCast(
transform.position,
boxCollider.size,
0, // 角色不需要移动角度,所以为 0
new Vector2(moveDelta.x, 0), // 这个是检测 x 轴的,所以需要改
Mathf.Abs(moveDelta.x * Time.deltaTime), // 这个是检测 x 轴的,所以需要改
LayerMask.GetMask("Actor", "Blocking"));

if (hit.collider == null)
{
// 因为我们检测 x 轴,所以我们要修改为 x 轴
transform.Translate(moveDelta.x * Time.deltaTime, 0, 0);
}
}
}

检测的时候,我们发现玩家居然完全不能移动,怎么回事?

因为我们检测的是 Blocking 层和 Actor 层、玩家也在 Actor 层里,所以他就检测自己,觉得“自己和自己碰撞了”。那需要解决这个问题,我们需要打开项目设置:Unity 左上角 Edit / Project Settings 然后弹出的对话框中左侧选择 Physic 2D。在右侧的属性列表中找到 Queries Start in Colliders,取消勾选。

设置结束后,运行游戏。可以发现玩家可以在地图上奔跑,然后遇到障碍物会停下了。