伤害区域和敌人
在上一节中我们使用了触发器的知识来让角色能拾取生命包的功能,在本节中我们将设置一些阻碍来阻止我们的角色。
在此之前我们先将角色的生命值设置为满。
- 打开RubyController脚本
- 在Start函数中奖currentHealth = 1 的语句删除。
添加伤害区域
首先添加一个伤害区域,这个步骤和前面的生命包的操作相同,唯一的区别就是生命的变化应为-1。可以在Assets > Art > Sprites > Environment 中找到一个名为Damageable的精灵并进行操作和代码的编写。
这里不重复说明直接给出伤害区域的代码。
public class DamageZone : MonoBehaviour
{
void OnTriggerEnter2D(Collider2D other)
{
RubyController controller = other.GetComponent<RubyController >();
if (controller != null)
{
controller.ChangeHealth(-1);
}
}
}
当我们启动游戏的时候让角色移动到伤害区域上方,此时我们发现一个问题,只有角色进入到伤害区域之后才会收到伤害,而停留在伤害区域内则不会伤害角色。
解决方案:将函数名称从OnTriggerEnter2D更改为OnTriggerStay2D来解决。刚体在触发器内的每一帧都会调用此函数,而不是在刚体刚进入时仅调用一次。
现在角色在区域内会一直收到伤害,但是伤害的速度太快了,并且如果我们停止移动,角色不会受到伤害。
解决方案:
- 打开角色预制件,在Rigidbody组件中将Sleeping Mode设置为Never Sleep
- 打开RubyController脚本并进行修改。
public class RubyController : MonoBehaviour
{
public float speed = 3.0f;
public int maxHealth = 5;
public float timeInvincible = 2.0f;
public int health { get { return currentHealth; }}
int currentHealth;
bool isInvincible;
float invincibleTimer;
Rigidbody2D rigidbody2d;
float horizontal;
float vertical;
// 在第一次帧更新之前调用 Start
void Start()
{
rigidbody2d = GetComponent<Rigidbody2D>();
currentHealth = maxHealth;
}
// 每帧调用一次 Update
void Update()
{
horizontal = Input.GetAxis("Horizontal");
vertical = Input.GetAxis("Vertical");
if (isInvincible)
{
invincibleTimer -= Time.deltaTime;
if (invincibleTimer < 0)
isInvincible = false;
}
}
void FixedUpdate()
{
Vector2 position = rigidbody2d.position;
position.x = position.x + speed * horizontal * Time.deltaTime;
position.y = position.y + speed * vertical * Time.deltaTime;
rigidbody2d.MovePosition(position);
}
public void ChangeHealth(int amount)
{
if (amount < 0)
{
if (isInvincible)
return;
isInvincible = true;
invincibleTimer = timeInvincible;
}
currentHealth = Mathf.Clamp(currentHealth + amount, 0, maxHealth);
Debug.Log(currentHealth + "/" + maxHealth);
}
}
这里我们说明一下进行的修改:
变量
- 一个名为 timeInvincible 的公共浮点变量。将此变量设置为 public 是因为你希望能够在 Inspector 中动态更改变量以调整该值。
一个名为 isInvincible 的私有 bool 变量,用于存储当前是否处于无敌状态。bool(boolean 的缩写)可让我们存储“true”或“false”。这在 if 语句中特别有用。
一个名为 invincibleTimer 的私有浮点变量。此变量将存储 Ruby 在恢复到可受伤状态之前剩下的无敌状态时间。
函数
- 在 ChangeHealth 函数中,你添加了一项检查以查看当前是否正在伤害角色(换言之,如果变化小于 0,则表示减小生命值)。如果是这样,你首先要检查 Ruby 是否已经处于无敌状态,如果是,那么将退出该函数,因为她现在无法受到伤害。否则,由于 Ruby 正受到伤害,你将 isInvincible bool 设置为 true 并将 invincibleTimer 变量设置为 timeInvincible,从而使 Ruby 处于无敌状态。
在 Update 函数中,如果 Ruby 处于无敌状态,则从计时器减去 deltaTime。这实际上是在倒计时。当该时间小于或等于零时,计时器结束,Ruby 的无敌状态也结束,因此通过将 bool 重置为 false 来消除她的无敌状态。这样,下次调用 ChangeHealth 来伤害 Ruby 时,你就不会提前退出并再次伤害她、重置她的无敌状态等等。
让我们进入运行模式并测试你的脚本。如果你让 Ruby 停留在伤害区域,她应该每两秒才受到伤害,因为你将无敌时间设置为两秒。尝试在 Inspector 中使用不同的值,必要时更改时间。
关于图形
现在需要我们思考,如果我们要调整伤害区域的大小应该怎么去做,当然可以通过拉伸图形的方式,但很多时候可能会让图形发生变形显得很难看。
但使用Sprite Renderer平铺精灵可以防止这种形变的发生。
- 首先,确保游戏对象的缩放在Transform组件中设置为1,1,1。
-
然后在 Sprite Renderer 组件中将 Draw Mode 设置为 Tiled,并将 Tile Mode 更改为 Adaptive。
此时将显示警告,告诉你精灵可能显示有误,在 Project 窗口中选择 Damageable 精灵,并将 Mesh Type 更改为 Full Rect,按下 Inspector 底部的 Apply。现在,如果单击 Hierarchy 中的 Damage Zone,Inspector 应该不再显示警告。
现在,当你使用矩形工具调整游戏对象的大小时,你会看到游戏对象拉伸到可以容纳两个精灵的程度,然后显示两个精灵而不是过度拉伸!注意:仅在你使用矩形工具而不是缩放工具时才有效,因为缩放会更改游戏对象的比例,不再是 Sprite Renderer Inspector 中的 Draw Mode 字段下可以看到的平铺大小。
但是你会发现自己的碰撞体没有按比例缩放。请选中 Box Collider 2D 组件的 Auto Tiling 属性,以便碰撞体随精灵一起平铺
添加敌人
就像你的主角一样,由于你希望敌人移动并与主角和环境碰撞,因此敌人需要 Rigidbody2D 和 BoxCollider2D。并且将敌人的Gravity Scale设置为0.
移动
创建一个名为 EnemyController 的新脚本,并将这个脚本附加到敌人角色。现在,让我们编写一个脚本来使让敌人循环上下移动。这里根据之前的知识可以写出来,我们直接放结果。
public class EnemyController2 : MonoBehaviour
{
public float speed;
Rigidbody2D rigidbody2D;
// 在第一次帧更新之前调用 Start
void Start()
{
rigidbody2D = GetComponent<Rigidbody2D>();
}
void FixedUpdate()
{
Vector2 position = rigidbody2D.position;
position.x = position.x + Time.deltaTime * speed;
rigidbody2D.MovePosition(position);
}
}
接着,我们需要让敌人来回移动,这里我们就使用垂直方向来回移动吧!使用名为 vertical 的公共 bool 变量。你可以在 Update 中进行测试以查看 vertical 是否为 true。如果为 true,则在你的世界中将敌人沿着 y 轴(而不是 x 轴)移动。
同样,这里我们也需要一个计时器来让敌人向一个方向移动后转向并重置计时器。
public class EnemyController : MonoBehaviour
{
public float speed;
public bool vertical;
public float changeTime = 3.0f;
Rigidbody2D rigidbody2D;
float timer;
int direction = 1;
// 在第一次帧更新之前调用 Start
void Start()
{
rigidbody2D = GetComponent<Rigidbody2D>();
timer = changeTime;
}
void Update()
{
timer -= Time.deltaTime;
if (timer < 0)
{
direction = -direction;
timer = changeTime;
}
}
void FixedUpdate()
{
Vector2 position = rigidbody2D.position;
if (vertical)
{
position.y = position.y + Time.deltaTime * speed * direction;;
}
else
{
position.x = position.x + Time.deltaTime * speed * direction;;
}
rigidbody2D.MovePosition(position);
}
}
在 Start 函数中,你可以将计时器初始化为反转敌人方向之前的时间。
在 Update 中,你将计时器递减,然后进行测试以查看计时器是否小于 0,如果小于 0,则可以更改方向并重置计时器。由于这与物理无关,因此不必在 FixedUpdate 中执行此操作。
最后,可以将 speed 乘以 direction。
伤害
现在我们要实现的下一个功能就是敌人与角色碰撞时,让角色收到伤害,在这里,我们不能使用触发器,因为敌人是实体,无法穿过。
就像你用过的 OnTriggerEnter2D 一样,你也可以使用 OnCollisionEnter2D(这是刚体与某个对象碰撞时调用的函数)。在此示例中,你的敌人与世界或主角发生碰撞时,便会调用 OnCollisionEnter2D。就像你对伤害区域所做的那样,你也可以进行测试以查看敌人是否与你的主角发生了碰撞。
在敌人的脚本中我们需要添加代码检查与敌人碰撞的游戏对象是否具有角色的脚本,如果检测到就对角色实施伤害。
最终敌人的脚本如下:
public class EnemyController : MonoBehaviour
{
public float speed;
public bool vertical;
public float changeTime = 3.0f;
Rigidbody2D rigidbody2D;
float timer;
int direction = 1;
// 在第一次帧更新之前调用 Start
void Start()
{
rigidbody2D = GetComponent<Rigidbody2D>();
timer = changeTime;
}
void Update()
{
timer -= Time.deltaTime;
if (timer < 0)
{
direction = -direction;
timer = changeTime;
}
}
void FixedUpdate()
{
Vector2 position = rigidbody2D.position;
if (vertical)
{
position.y = position.y + Time.deltaTime * speed * direction;;
}
else
{
position.x = position.x + Time.deltaTime * speed * direction;;
}
rigidbody2D.MovePosition(position);
}
void OnCollisionEnter2D(Collision2D other)
{
RubyController player = other.gameObject.GetComponent<RubyController >();
if (player != null)
{
player.ChangeHealth(-1);
}
}
}
完成了角色的敌人设置之后,下一步就是要让角色动起来,因此会说明精灵动画。