译:Celeste 和 TowerFall 中的物理

作者:highway★
2020-06-10
22 32 4

作者:Matt Thorson(注:配色是蔚蓝哦~ 什么,不是蔚蓝么)

译者注:本文没有配图,可能挺枯燥。要是各位爷有耐性看完了,嘿~ 感觉还挺有帮助,给咱刷个跑车什么的,心里刷就行。给作者多刷几个,大游艇大火箭您随意。

我被问到很多关于 TowerFall 还有 Celeste 中物理工作原理的问题。这是个非常简单的系统,不过我花了大概十多年研究基于 tile 的平台游戏,才整明白。我很久以前写 gamemaker 的平台游戏(platformer)引擎用过类似的基本概念,从那时候开始,我对它做了很多简化和改进。Celeste 和 TowerFall 的引擎是用 c# 写的,因此我们有很多出色的功能,像委托(delegates)、和结构(structs),这让一切都变得更好。虽然这倒不是啥有突破性的东西,不过我觉着还是写下来好,没准儿会对谁有帮助呢!

(这篇文章本来是关于 Towerfall 的,在我们做 Celeste 之前就已经写过。这也适用于 Celeste,我更新了一下,这也会对那些以 Celeste 为参考点的读者更有帮助。)

我们所有的物理都由两个类来处理:SolidsActors(这里不翻译了,保留原单词比较好)。Solid,当然就是关卡内那些可碰撞的几何体。Actor,就是物理对象,比如玩家、弓箭、怪物、宝箱什么的。一切需要移动并与关卡内几何体产生互动的任何对象都是 Actor。该系统有下面这些简单的约束:

  • 所有的 collider 都有轴对齐的边界框(AABBs)(译注:如果这位爷您跟我一样不了解 AABB 是啥,请看这里
  • 所有 collider 的位置、宽和高都是整数
  • 除了特殊情况外,Actor 和 Solid 永远不会重叠
  • Solid 之间不会相互作用

Actor 基础

咱们先来假设一下,Solid 永远不会移动 --- 然后我们要怎样确保不管 Actor 怎么移动都不与 Solid 重叠?Actor 有一个非常简单的核心 API,这个个 API 有这俩函数:

public void MoveX(float, Action); 
public void MoveY(float, Action);

这俩老哥都将移动量作为 float,将碰撞动作回调作为 C# 委托。Actor 对自己的速度、加速度或者重力都没有任何概念。扩展 Actor 的每个 class 都会搞定这些,保持追踪它的速度,并在适当的时候传递给这些函数。

咱们来看看 MoveX 函数:

public void MoveX(float amount, Action onCollide) 
{ 
  xRemainder += amount; 
  int move = Round(xRemainder);
  if (move != 0) 
  { 
    xRemainder -= move; 
    int sign = Sign(move);
    while (move != 0) 
    { 
      if (!collideAt(solids, Position + new Vector2(sign, 0)) 
      { 
        //旁边没有 Solid
        Position.X += sign; 
        move -= sign; 
      } 
      else 
      { 
        //撞上 Solid 了!
        if (onCollide != null) 
          onCollide(); 
        break; 
      } 
    } 
  } 
}

首先,我们将 amount( 移动量)添加到我们的“remainder”计数器中。由于位置是用整数表示的,所以我们只能以像素为单位移动,不能 1/2 像素、1/4 像素这样移动,因此我们用 round 函数处理一下 remainder,只有 move 不等于 0 时候我们处理移动。

现在咱们知道了我们要移动多远,我们只需要每次搞定一个像素就行了。对于每个像素,我们都会提前检查一遍是否有障碍物,如果没有,我们就移动。如果撞到墙了,我们就停止移动,然后调用传入的碰撞委托。

我们为什么要传递碰撞委托?后面再说,基本上,它让我们可以适用相同的 MoveXMoveY 函数来完成很多不同类型的移动。这也意味着扩展 Actor 的类,这样在碰撞的时候我们可以轻松的切换行为。

现在,我们让 Actor 可以移动了,并且不会与 Solid 相交 -- 假设 Solid 不会移动(我们前面说过了,这里我再提一句)

移动 Solid 的准备

这里有点儿费劲了。这些年来,我看过也写过很多关于移动 Solid 的有缺陷的实现方法。在我十几岁那会,玩过一些游戏,有些游戏在移动平台物理处理这方面做的非常残暴,我现在终于找到了解决方案,咱们向下看。

public void Move(float, float);

Solid 只需要一个移动函数,并且不需要碰撞委托,因为 Solid 之间不能发生任何碰撞。如果您让 Solid 往右挪 30 像素,那甭管旁边杵着几个 Solid 老铁跟那儿挡着,它都会跟您说,“没毛病,哥肯定给您挪到那儿。”
但是呢,这 30 像素的漫长道路上,要是有 Actor 们跟这儿挡道儿,咱就得处理一下子了,因为 Solid 不能跟 Actor 重叠啊。
在咱们了解 Solid 移动函数之前呢,咱先在 Actor 的 API 里加点儿东西:

public virtual bool IsRiding(Solid solid); 
public virtual void Squish();

Actor 里咱加了个 IsRiding,咱就把移动的 Solid 想象成一匹马,Actor 要是站上面了,就是在骑它,IsRiding 就用来检测 Actor 骑没骑它。但是呢,某些 Actor 吧,可能希望重写这个函数来更改行为,比如在 Towerfall 中,玩家在抓着 Solid 的边缘悬挂的时候也能骑 Solid,但飞行的怪物就永远不会骑 Solid。在 Celeste 里,Madeline(就是您操控的内个小姑娘)站在 Solid 上或者贴边儿的时候都会骑。

第二个我们加的东西,就是挤压函数,在两个 Solid 直接,Actor 会被挤压。默认情况下,就是销毁 Actor。

电梯

当 Solid 与 Actor 交互时,它可以通过两种方式:载客(译注:这个 Solid 是位的哥,脾气火爆,你要不上车,它就撞您) 或者 推动。要是 Actor 站 Solid 上面了,那就被带走,但是要是 Solid 的移动导致他们重叠了,那 Actor 就会被推走。重要的一点,推动 优先于 载客 ---- 就是说如果两种情况同时出现了,那就算作是推动。

下面是 Solid.Move

public void Move(float x, float y) 
{ 
  xRemainder += x; 
  yRemainder += y; 
  int moveX = Round(xRemainder); 
  int moveY = Round(yRemainder);
  if (moveX != 0 || moveY != 0) 
  { 
    //遍历关卡中的每个 Actor,如果 actor.IsRiding 为 true 就添加到 list 里
    List riding = GetAllRidingActors();        
    //关闭此 Solid 的碰撞,让通过它移动的 Actor 不会卡住。
    Collidable = false;     
    if (moveX != 0) 
    { 
      xRemainder -= moveX; 
      Position.X += moveX; 
      if (moveX > 0) 
      { 
        foreach (Actor actor in Level.AllActors) 
        { 
          if (overlapCheck(actor)) 
          { 
            //往右推动
            actor.MoveX(this.Right — actor.Left, actor.Squish); 
          } 
          else if (riding.Contains(actor)) 
          { 
            //往右载客
            actor.MoveX(moveX, null); 
          } 
        } 
      } 
      else 
      { 
        foreach (Actor actor in Level.AllActors) 
        { 
          if (overlapCheck(actor)) 
          { 
            //往左推动 
            actor.MoveX(this.Left — actor.Right, actor.Squish); 
          } 
          else if (riding.Contains(actor)) 
          { 
            //往左载客
            actor.MoveX(moveX, null); 
          } 
        } 
      } 
    } 
    if (moveY != 0) 
    { 
      //Y 轴移动 
      … 
    } 
    //重新启用此 Solid 的碰撞
    Collidable = true; 
  } 
}

这块儿咱写了不少代码,哈,所以下面咱得看看都写了啥。

首先,将 amount 移动量添加到 remainder 中。Solid 共享整数锁定位置的 Actor 约束,他们必须得使用相同的系统才能知道啥时候要移动。

其次,我们创建了一个在此 Solid 上的每个 Actor 的 List,也就是我们应该载客的名单(译注:看着没,老哥转行不开出租了,可能是开的大公交,一下上一大堆人)。简单的循环检测一下每个 Actor 的 IsRiding 就可以搞定。在实际移动之前执行此操作很重要,因为移动可能会让我们超出 IsRiding 检测的范围。

第三,咱先暂时关闭此 Solid 的碰撞。当推动和载着 Actor 时,咱通过调用 Actor.Move 函数来解决。我们不想让 Actor 在移动过程中考虑这个 Solid 是在推它还是载着它走。

接着,我们一次移动一个轴,并开始解决 Actor 交互。我们要进行重叠检测来看看我们是否需要推动任何 Actor。这里有个要注意的地方,我们要在移动之前进行载客检测,移动之后进行推动检测。

如果发现自己与任何 Actor 重叠,我们就要推动他们。无论我们是否也随身载着这些 Actor,都要优先考虑,因为前面我们说过了,推动的优先级高于载客。如果我们没有推动给定的 Actor,那我们就要检测它是不是已经放入我们要载客的 Actor 列表里了。如果该检测也没有通过,那这个 Actor 就跟咱无关,咱就甭管它了。

现在,咱们来看看我们推动 Actor 和载着 Actor 时的区别。

推动 Actor 无法获得我们的全部移动量 - 只能推动我们的领先边缘与其最接近边缘之间的像素差。这是为了确保 Actor 保持与正在执行推动的 Solid 的侧边平齐。

推动还使用了 Actor.Squish 回调,回想一下,默认情况下,这会销毁 Actor。如果 Solid 将一个 Actor 推入另一个 Solid 中,有些不好的事就要发生,虽然咱们没那么残暴,但是我们不允许重叠啊,咱也只能销毁 Actor。看到这里,您可能想尝试其他操作,比如让 Actor BLA BLA BLA,您的想法我虽然看不到,不过可能会很有趣吧~ 在 Towerfall 和 Celeste 中,根据游戏的玩法,我们有很多不同的处理方式。

载着 Actor 可以获得全部的平台移动速度,而且没有碰撞回调。这里没有被挤压的危险 --- 就算 Actor 撞墙了,也没事儿。

就是这样!希望对您有所帮助 :)

(译注:就是这样,这么突然的结束,我缓冲一下啊,翻译文章比写代码画图想系统还累,我就吐个槽,各位爷吉祥。)

(译注 2:还没吐完,我想到了小学时代课本上那位网红,他说“人的生命是有限的,可是,为人民服务是无限的,我要把有限的生命,投入到无限的为人民服务之中去。” 这一刻,我体会到了那种修仙的冲动。就让我们在这崇高的名言的洗礼下结束吧,回见了您呐~ 对喽,各位爷别忘了在心里给 matt 刷大游艇大火箭啊~)

Matt Thorson

来自 DEV 的凝视



注:原文地址。如有翻译不当的地方,请指正。

-H

2020/06/07

本文为用户投稿,不代表 indienova 观点。

近期点赞的会员

 分享这篇文章

您可能还会对这些文章感兴趣

参与此文章的讨论

  1. herqu 2020-06-10

    好人一生平安

    • highway★ 2020-06-10

      @herqu:孙悦姐,是你么?

    • herqu 2020-06-11

      @highway★:认错了哈哈哈

    • highway★ 2020-06-11

      @herqu:噢,不是著名歌手啊

您需要登录或者注册后才能发表评论

登录/注册