为 JavaScript 游戏构建一个简单的 2D 物理引擎

简介

2D 游戏有许多不同的形状和大小。在某些情况下,构建自己的 2D 物理引擎,生成碰撞检测等系统模拟是一个不错的选择,在使用 JavaScript 时更是如此。在任何平台开发强大的物理引擎都非常困难,而比较简单、简洁的引擎往往更容易。如果您需要一个流行的物理引擎的简化版本,默默地从头开始构建就可以高效地完成此工作。

本文将探索一个物理引擎的实现,从而构建一个平台游戏的基本框架。使用现有的物理引擎(如 Box2D)与示例中的引擎进行比较。代码片段显示了组件间的交互。

您也可以下载本文中使用的示例的源代码。

为什么要更简单?

在游戏开发中,更简单意味着很多东西。在利用物理引擎时,更简单通常是指计算的复杂性。在确定游戏的最低公共标准后,复杂性变得更加彼此相关。在计算方面,复杂性意味着处理可能需要更长的时间,或者物理上的算法可能比较困难。伴随本文的相关操作,您不需掌握微积分知识。

由于 JavaScript 能够很好地与 HTML5 canvas 结合,所以您经常会看到将它应用到基于画布的游戏中。在某些情况下(例如 iOS 或 Android 移动平台),画布的图形会成为游戏中的一个主要因素。需要构建一个较小的资源平台,这意味着您需要尽可能进行压缩处理,为 CPU 执行昂贵的图形计算留出足够的空间。

CPU 利用率

处理是从一个经过测试的、功能强大的库转移到一个内部开发的、简洁的解决方案的主要原因。我们需要关注的处理被称为CPU 利用率。CPU 利用率是程序或游戏运行时中可用或正在使用的处理量。物理引擎可能占用与游戏其他部分同样多的 CPU 处理。更简单的选择意味着更小的 CPU 利用率。

在运行游戏时,您的目标通常是每秒 30-60 帧,这意味着游戏循环必须在 33-16 毫秒内。图 1显示了一个示例。如果遵循更复杂的解决方案,这意味着会影响可能占用游戏的部分 CPU 使用率的其他特性。尽可能地从任何游戏组件中减少 CPU 使用率,从长远来看,这是很有帮助的。

图 1. 示例 CPU 利用率循环步骤

确定组件

使用 2D 时,可以模拟或伪造许多不同的复杂效果,只要您有足够的引擎处理。在构建游戏时,要考虑您需要使用哪些组件。确定您需要构建哪些组件,这样做可以避免强迫引擎完全计算哪些组件。类似于 图 2所示的点重力这样的效果是很难伪造的,而较小的撞击区域则可以轻松地完成。

图 2. 点重力

构建一个物理引擎

本节讨论了什么组成了物理引擎,以及如何确定其特性。

构建物理引擎的第一个重要步骤是选择特性和操作的顺序。确定使用哪些特性看起来似乎微不足道,但特性有助于形成物理引擎的组件,指出可能会有困难的领域。在示例应用程序中,您要构建一个类似于 图 3 中所显示的游戏引擎。

图 3. 平台游戏

在图 3中的框表示:

  • 玩家:包含对角线的框
  • 获胜条件、目标:实心的黑框
  • 普通平台:实线框
  • 弹性平台:虚线框

除了可视化简单的编程图形,您还可可视化游戏的功能。当玩家达到获胜条件/目标时,他们将会获胜。在构建该游戏平台中,所以您需要一些最基本的物理引擎构建块:

  • 速度、加速度和重力
  • 碰撞检测
  • 弹跳碰撞
  • 正常的碰撞

位置属性用于驱动玩家。碰撞检测允许玩家在游戏中达到目标和左右移动。碰撞类型允许游戏有不同类型的地面。但在游戏中只有一名玩家,并且基本上只有一个动态的对象,所以您可以在代码中减少碰撞量。

现在,游戏的特性和物理方面特性已经确定,您可以开始映射出物理引擎的结构。

选择正确的引擎运行时

物理引擎主要有两种形式:高精度和实时。高精度的引擎用于模拟困难或关键物理计算;实时引擎是您在视频游戏中看到的类型。对于本例中的物理引擎,您将使用一个实时引擎,它会无限地运行其引擎计算,直到要求它停止。

实时引擎提供了两个选项来控制物理引擎的计时:

静态
始终为引擎提供对传递每一帧所预期的不变的时间量。静态实时引擎以不同的速度在不同的计算机上运行,所以它们有不同的行为,这很常见。
动态
将经过的时间馈送到引擎。

在本文中的示例物理引擎需要连续运行,所以您需要设置一个针对引擎运行的无限循环。这种处理模式被称为游戏循环。在本文中,在这个循环中要运行的每一个操作被称为步骤。使用requestAnimationFrame API 来使用动态选项。清单 1显示了运行requestAnimationFrame所需的代码。它使用存储在 Khronos Group CVS Repository 中的一个 polyfill。

清单 1. 包含 polyfill 的requestAnimFrame

物理引擎循环

决定循环中的操作顺序看起来似乎很简单,但它不是一个简单的决定。虽然这一步有若干个不同的可用选项,但您应该根据目前已经确定的特性来设计引擎,如图 4所示。

图 4. 物理循环步骤

清单 2显示,该步骤执行了计算,以便它在单次传递中执行每种类型的计算。此计算的另一种方法是单独执行每个对象的计算,但由于依赖于其他计算,这种访问通常会产生奇怪的结果。

清单 2. 物理循环步骤的伪代码

现在,您已经有了工作流和特性,并确定了要构建的引擎类型,然后您可以开始构建设各个部分。

刚体物理

物理作为一门科学,范围非常广阔,包括几种不同类型的计算。牛顿物理学由常见的位置、速度和加速度计算组成,而电磁学由磁力或电力组成,并且可以用于在一个物理系统中模拟重力。这些领域的物理本身都非常好,但这些计算的复杂性超出了本文的讨论范围。

在决定物理系统时,引擎的构造取决于您想要执行的计算类型。例如,示例引擎将实现刚体物理,即不变形的物理。如果使用刚体物理,可以避免计算在软体动力学中看到的以力为基础的变形,也可以避免在任何形式的多引力系统中都能看到的额外力量修改。

引擎的各组成部分

物理引擎的计算相当复杂,但是,如果知道了模式,它的构造就相当简单。清单 2包括一个高层次的循环步骤伪代码。在该步骤中的每个计算都可以包括它自己的对象或 API。该引擎对象图包括以下主要组件:

  • 物理实体
  • 碰撞检测程序
  • 碰撞求解器
  • 物理引擎

实体是使用引擎的对象、主体或模型,是最不活跃的一部分。相当于 Box2D 中的b2Body类。检测程序和求解器配合工作,首先发现实体之间的碰撞,然后将其转换为应用于受碰撞影响的任何实体的修改。

物理引擎虽然在其整体上涵盖了引擎,但实际上,系统中的每一个阶段都要管理、准备每个组件,并和每个组件通信。图 5显示了物理引擎中的每个元素之间的关系。

图 5. 物理引擎

四个组件构成了我们的引擎背后的主要力量。您要实现的第一个组件是物理实体,它代表在屏幕上的每一个对象。

物理实体

虽然物理实体组成了引擎中最小和最简单的组件,但它是最重要的。正如前面提到的,实体将代表在屏幕上的每个元素。实体,在游戏和物理中均表示对象的状态,它保存与该实体的所有相关元数据,如 清单 3 所示。

清单 3. 实体 JavaScript 对象

实体作为一个模型

如清单 3中的逻辑所示,物理实体只存储原始数据,并产生数据集的不同变体。如果您熟悉 MV* 模式(如 MVC 或 MVP),实体代表这些模式中的 Model 组件。(有关 MVC 的信息,请参阅参考资料。)实体存储若干块数据,它们全部都代表该对象的状态。对于物理引擎中的每一个步骤,这些实体都会产生相应的变化,并最终改变了整体的引擎状态。稍后我们会对实体状态数据的不同分组进行更深入的讨论。

您可能已经注意到,清单 3中的实体不包括将实体显示到屏幕上的渲染程序,或绘制函数。通过从图形表示分离物理逻辑和表示,您可以渲染出任何图形表示,从而提供游戏实体皮肤的能力。尽管可以使用矩形来表示对象,如图 6所示,但您不会受限于矩形图像。实体可以应用任何图像。

图 6. 边框和子画面

位置、速度和加速度:位置数据

实体包括位置数据,这是描述实体如何在空间中移动的信息。位置数据包括您会在牛顿运动物理方程(请参阅参考资料)中看到的基本逻辑。在这些数据点中,示例实体关注加速度、速度和位置。

速度随时间推移的位置而变化,并且,类似地,加速度是随着时间推移的速度而变化。计算位置的变化最终是一个相当简单的计算,如清单 4所示。

清单 4. 位置方程式

其中:

  • p是实体的位置。这通常用 x 和 y 表示。
  • v是速度或速率。这是随着时间推移的位置变化量。
  • t是所经过的时间量。在 JavaScript 中,这以毫秒为单位进行衡量。

根据清单 4中的方程式,您知道实体的位置将不可避免地受到它所应用的时间的影响。同样地,速度由加速度更新,如清单 5所示。

清单 5. 速度方程式

其中:

  • v是速度或速率。这是随着时间推移的位置变化量。
  • a是实体的加速度。这是随着时间推移的速度变化量。
  • t是所经过的时间量。在 JavaScript 中,这以毫秒为单位进行衡量。

清单 5与 清单 4 类似,区别是a代表实体的加速度。虽然您不会在实体的逻辑中循环使用这些方程式,因为您的目的只是存储它,重要的是要知道这些参数代表什么。您还需要考虑这些实体的空间表示。

宽度和高度:空间数据

空间数据指表示该对象所占用的撞击区域和空间所需的参数。形状和大小等元素会影响空间数据。对于我们的示例平台,您将使用 “烟雾和镜子” 的方法到撞击区域。(烟雾和镜子是欺骗或欺诈行为的一个比喻,是基于魔术师的幻像。)

使用大于或小于代表实体的图形或子画面的撞击区域,这很常见。这种撞击区域往往构成一个边框或矩形。为了保持简单和需要较少计算,实现将只使用矩形来测试撞击区域。

在图形和物理中的矩形都可以由四个数字表示。使用左上角的点表示位置,并使用宽度和高度表示大小,如图 7所示。

图 7. 框的表示

边框只需要位置和大小,因为其他所有参数都依赖于那些组件。有帮助的其他计算是中点和边缘的计算,这两者都常用于碰撞计算。

复原:碰撞数据

实体的最后一个组件是碰撞数据,即确定对象在碰撞过程中应该如何行动的信息。对于该示例,碰撞将只包括位移和弹性碰撞,所以要求是相当有限的,如 图 8 所示。弹性碰撞遵循 Box2D 中使用的命名模式,并将弹力定义为复原。

图 8. 完全弹性碰撞的示例

所有数据组件都为系统提供了足够的数据,以便开始计算在示例实现中随着时间推移而发生的所有变化。

高级冲撞概念

该实体对于示例系统是足够用了,但通常还有其他参数用于多种碰撞解决。本节将会简要讨论其他一些类型。

现实世界的值

在游戏中,设计人员通常基于我们的世界来模拟游戏的世界,并以现实世界空间中的单位进行度量。使用现实世界的单位(比如,米)模拟其系统的实现,只需要一个比例因子,实现其数据点和这些距离的相互转换。

本文不提供转换的示例,但 Box2D 的(请参阅参考资料)在他们的几个示例中提供了转换率以及配套的示例。

质量和力

质量和力组成了大多数的物理引擎,这可能让您想知道为什么示例系统不这样做。因为示例系统不需要质量,您可以依靠力是质量和加速度的产品这个事实,并假设质量是一个单位。您可以在清单 6中看到这个计算。

清单 6. 力方程式计算出质量

该计算超出了本文的范围。所有力的总和,或能量守恒是两个物理计算,您可以用它们来计算出涉及多个实体的不同碰撞,而不是示例系统所用的单一实体。

形状和凹凸感

示例系统支持非旋转边框,但有时也需要更精确的碰撞检测和处理。在某些这样的情况下,您应该考虑多边形的方法,或包括一些边框来代表一个单一实体,如图 9所示。

图 9. 凹和凸

当形状有一部分被打破,您最终就会看到凹的形状。如图 9中所示,凹的形状包括有任意侧面被向内推向中心的形状。凹的形状往往需要更精确和复杂的碰撞检测算法。而另一个选项,凸,是没有任何侧面向内推向中心的形状。在决定形状支持时,重要的是要考虑您的系统支持什么样的复杂性。

碰撞检测程序

物理引擎的第二个重要步骤是碰撞的检测和解决。碰撞检测程序的作用和它的名称所暗示的一样。本文中的碰撞检测程序很简单,将包括确定矩形与另一矩形是否碰撞的计算,但对象往往可以与各种类型的对象碰撞,如清单 7所示。

清单 7. 碰撞检测程序collideRect测试

清单 7是用于测试边框的碰撞算法。该算法使用逻辑来确定是否所有边缘都在其他矩形的边界之外,从而确定碰撞是否成功。此碰撞检测不支持其他形状(如图 10中所示),但使用它已经足以说明问题。

图 10. 碰撞检测示例

单一移动实体的情况可以解决确定要碰撞哪些对象这个问题。在构建一个碰撞检测算法时,它有助于消除不必要的实体。

碰撞求解器

碰撞管道的最后一部分是求解器。一旦遇到碰撞,实体必须计算其解决位置,那么,碰撞实体不再碰撞,并且新方向要被处理。在此示例中,您的对象要么完全吸收所施加的影响力,要么反射发生碰撞时所施加的一部分力。求解器包括位移计算的方法,以及前面提到的弹性碰撞的方法。

如图 11所示, 求解器决定玩家的方向和位置的变化。示例求解器首先从实体来的方向将玩家带回碰撞的起点。

图 11. 碰撞解决位移图

然后,求解器方程式将修改实体的速度。位移算法将删除速度,并且弹性算法将使用前面提到的复原,以减少和反射玩家的速度,如清单 8中所示。

清单 8. 位移碰撞解决

在清单 8中,弹性碰撞使用了一个计算,通过检查在玩家和实体之间的方向的角度差,确定矩形的重叠放置。矩形被标准化,所有计算的处理都将矩形作为正方形。这种计算可以用图 11中的示例表示。在本例中,求解器直接修改了速度。在处理多个对象碰撞时,要小心,不要贪心地直接设置速度。要修改多个源的速度,则要求使用能量守恒方程式和计算不同的角度和速度。

位移计算上也使用位移算法,区别在于速度被设置为零,而不是被反射和降低。

现在已概述了组件,最后一步是在引擎中将这些系统组合在一起。

引擎

系统的最后一个组件是引擎本身,它负责工作流伪代码(参见清单 2)。引擎的工作一般与控制实体相匹配,并包括在每一个循环步骤将内容推进或推出碰撞组件。示例引擎的实现管理了在遇到碰撞之前的位置逻辑变更和重力,如 清单 9 所示。

清单 9. 物理引擎

在物理步骤之后,引擎首先会计算重力,将它和实体的加速度应用于速度,然后再计算实体新的 x 和 y 位置。在该示例中,碰撞由碰撞检测程序的detectCollisions方法产生。该检测方法作为一个路由器,使用适当的方法(在本例中为collideRect)。同样,求解器使用通用的解决方法,将调用转移到resolveElastic或它自己的位移求解器方法。

虽然在此实现的引擎中有一个极其普通的控制,但它的使用是不可或缺的。在其他引擎中,引擎可能会采取主导角色。在某些情况下,引擎将足以管理物理步骤和在物理引擎中的实体交互。要小心一个常见的陷阱:不要让系统超出任何可能碰撞的实体。如果您的实体移动得比可碰撞的实体所占用的空间更远,检测程序会完全忽略它。

结束语

在您构建自己的物理引擎时,为您的游戏实现一个简单的解决方案,这会为您带来一些好处。虽然强大的解决方案总是更可取一些,但如果您要在计算复杂度很重要的地方构建物理引擎,或需要为低端处理设备(如 iOS 或 Android)构建物理引擎,那么应该考虑使用烟雾和镜子方法。当计算资源不足时,本文中所列出的解决方案是理想选择。

物理引擎有几个部分支持它们的工作。最重要的部分是实体、碰撞检测程序、碰撞求解器和引擎核心。通过让各组件协调地工作,管理它们自己的具体作业,小心选择您的形状和算法,可以完成简单的物理引擎。只要您制定了关于所支持的特性的适当决策,就可以构建一个更强大的物理引擎实现的较小子集。更强大的物理引擎始终可以帮助您确定适合自己的游戏的最佳方法。

下载

描述 名字 大小
文章源代码 HomebrewPhysicsEngineSource.zip 5KB

参考资料

学习

获得产品和技术

讨论

  • developerWorks 社区:探索由开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户进行交流。
收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部