简介
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
1 2 3 |
// call requestAnimFrame with a parameter of the // game loop function to call requestAnimFrame(loopStep); |
物理引擎循环
决定循环中的操作顺序看起来似乎很简单,但它不是一个简单的决定。虽然这一步有若干个不同的可用选项,但您应该根据目前已经确定的特性来设计引擎,如图 4所示。
图 4. 物理循环步骤
清单 2显示,该步骤执行了计算,以便它在单次传递中执行每种类型的计算。此计算的另一种方法是单独执行每个对象的计算,但由于依赖于其他计算,这种访问通常会产生奇怪的结果。
清单 2. 物理循环步骤的伪代码
1 2 3 4 |
1 User Interaction 2 Positional Logic 3 Detect Collisions 4 Resolve Collisions |
现在,您已经有了工作流和特性,并确定了要构建的引擎类型,然后您可以开始构建设各个部分。
刚体物理
物理作为一门科学,范围非常广阔,包括几种不同类型的计算。牛顿物理学由常见的位置、速度和加速度计算组成,而电磁学由磁力或电力组成,并且可以用于在一个物理系统中模拟重力。这些领域的物理本身都非常好,但这些计算的复杂性超出了本文的讨论范围。
在决定物理系统时,引擎的构造取决于您想要执行的计算类型。例如,示例引擎将实现刚体物理,即不变形的物理。如果使用刚体物理,可以避免计算在软体动力学中看到的以力为基础的变形,也可以避免在任何形式的多引力系统中都能看到的额外力量修改。
引擎的各组成部分
物理引擎的计算相当复杂,但是,如果知道了模式,它的构造就相当简单。清单 2包括一个高层次的循环步骤伪代码。在该步骤中的每个计算都可以包括它自己的对象或 API。该引擎对象图包括以下主要组件:
- 物理实体
- 碰撞检测程序
- 碰撞求解器
- 物理引擎
实体是使用引擎的对象、主体或模型,是最不活跃的一部分。相当于 Box2D 中的b2Body类。检测程序和求解器配合工作,首先发现实体之间的碰撞,然后将其转换为应用于受碰撞影响的任何实体的修改。
物理引擎虽然在其整体上涵盖了引擎,但实际上,系统中的每一个阶段都要管理、准备每个组件,并和每个组件通信。图 5显示了物理引擎中的每个元素之间的关系。
图 5. 物理引擎
四个组件构成了我们的引擎背后的主要力量。您要实现的第一个组件是物理实体,它代表在屏幕上的每一个对象。
物理实体
虽然物理实体组成了引擎中最小和最简单的组件,但它是最重要的。正如前面提到的,实体将代表在屏幕上的每个元素。实体,在游戏和物理中均表示对象的状态,它保存与该实体的所有相关元数据,如 清单 3 所示。
清单 3. 实体 JavaScript 对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
// Collision Decorator Pattern Abstraction // These methods describe the attributes necessary for // the resulting collision calculations var Collision = { // Elastic collisions refer to the simple cast where // two entities collide and a transfer of energy is // performed to calculate the resulting speed // We will follow Box2D's example of using // restitution to represent "bounciness" elastic: function(restitution) { this.restitution = restitution || .2; }, |