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

有时,第三方物理库(比如 Box2D)的复杂性和使用开销对于 JavaScript 游戏而言是不可回避的。本文将遍历一个简单的 2D 物理引擎的完整实现,您可学习如何创建一个简单的物理世界,包含游戏对象之间的重力和碰撞检测。本文还讨论物理引擎的结构、检测和解决碰撞的一些算法,以及 “启动自己的” 物理引擎的原因。

Adam Ranfelt, 软件开发人员, The Nerdery

Adam RanfeltAdam Ranfelt 是 The Nerdery 的技术不可知论承诺的一个活生生的例子。Adam 于 2009 年毕业于 University of Minnesota,并获得计算机科学学士学位,他喜欢在技术的最前沿工作。他是 HTML、ActionScript、Java 和 Objective-C 方面的专家。Adam 于 2010 年加入 The Nerdery,在此之前,他曾在 Curb-Crowser Design 工作过,他在那里构建和维护了大量数字媒体项目。作为在工作中使用正确技术的坚定信徒,Adam 精通 10 多种编程语言,并且该数量还在增加。在 2012 年,他被晋升为首席软件工程师,以表彰他的技术和领导能力。



2013 年 1 月 04 日

简介

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

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

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


为什么要更简单?

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

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

CPU 利用率

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

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

图 1. 示例 CPU 利用率循环步骤
16 毫秒的循环步长间隔中的游戏逻辑、物理和渲染的 CPU 利用率

确定组件

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

图 2. 点重力
点重力图说明了向着中心的重力作用

构建一个物理引擎

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

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

图 3. 平台游戏
游戏场景描绘

图 3 中的框表示:

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

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

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

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

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

选择正确的引擎运行时

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

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

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

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

清单 1. 包含 polyfill 的 requestAnimFrame
// call requestAnimFrame with a parameter of the
// game loop function to call
requestAnimFrame(loopStep);

物理引擎循环

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

图 4. 物理循环步骤
运行循环步骤的示例流程:首先,用户与游戏逻辑交互,然后是位置逻辑、碰撞检测、碰撞解决,最后是渲染

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

清单 2. 物理循环步骤的伪代码
1 User Interaction
2 Positional Logic
3 Detect Collisions
4 Resolve Collisions

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

刚体物理

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

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


引擎的各组成部分

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

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

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

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

图 5. 物理引擎
物理引擎步骤示例过程:首先,计算新的位置,然后是碰撞检测程序,然后是冲突解决

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


物理实体

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

清单 3. 实体 JavaScript 对象
// 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;
    },

    displace: function() {
        // While not supported in this engine
	       // the displacement collisions could include
        // friction to slow down entities as they slide
        // across the colliding entity
    }
};

// The physics entity will take on a shape, collision
// and type based on its parameters. These entities are
// built as functional objects so that they can be
// instantiated by using the 'new' keyword.

var PhysicsEntity = function(collisionName, type) {

    // Setup the defaults if no parameters are given
    // Type represents the collision detector's handling
    this.type = type || PhysicsEntity.DYNAMIC;

    // Collision represents the type of collision
    // another object will receive upon colliding
    this.collision = collisionName || PhysicsEntity.ELASTIC;

    // Take in a width and height
    this.width  = 20;
    this.height = 20;

    // Store a half size for quicker calculations
    this.halfWidth = this.width * .5;
    this.halfHeight = this.height * .5;

    var collision = Collision[this.collision];
    collision.call(this);

    // Setup the positional data in 2D

    // Position
    this.x = 0;
    this.y = 0;

    // Velocity
    this.vx = 0;
    this.vy = 0;

    // Acceleration
    this.ax = 0;
    this.ay = 0;

    // Update the bounds of the object to recalculate
    // the half sizes and any other pieces
    this.updateBounds();
};

// Physics entity calculations
PhysicsEntity.prototype = {

    // Update bounds includes the rect's
    // boundary updates
    updateBounds: function() {
        this.halfWidth = this.width * .5;
        this.halfHeight = this.height * .5;
    },

    // Getters for the mid point of the rect
    getMidX: function() {
        return this.halfWidth + this.x;
    },

    getMidY: function() {
        return this.halfHeight + this.y;
    },

    // Getters for the top, left, right, and bottom
    // of the rectangle
    getTop: function() {
        return this.y;
    },
    getLeft: function() {
        return this.x;
    },
    getRight: function() {
        return this.x + this.width;
    },
    getBottom: function() {
        return this.y + this.height;
    }
};

// Constants

// Engine Constants

// These constants represent the 3 different types of
// entities acting in this engine
// These types are derived from Box2D's engine that
// model the behaviors of its own entities/bodies

// Kinematic entities are not affected by gravity, and
// will not allow the solver to solve these elements
// These entities will be our platforms in the stage
PhysicsEntity.KINEMATIC = 'kinematic';

// Dynamic entities will be completely changing and are
// affected by all aspects of the physics system
PhysicsEntity.DYNAMIC   = 'dynamic';

// Solver Constants

// These constants represent the different methods our
// solver will take to resolve collisions

// The displace resolution will only move an entity
// outside of the space of the other and zero the
// velocity in that direction
PhysicsEntity.DISPLACE = 'displace';

// The elastic resolution will displace and also bounce
// the colliding entity off by reducing the velocity by
// its restituion coefficient
PhysicsEntity.ELASTIC = 'elastic';

实体作为一个模型

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

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

图 6. 边框和子画面
子画面,中心是撞击区域

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

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

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

清单 4. 位置方程式
p(n + 1) = v * t + p(n)

其中:

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

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

清单 5. 速度方程式
v(n + 1) = a * t + v(n)

其中:

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

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

宽度和高度:空间数据

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

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

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

图 7. 框的表示
包含空间度量的框

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

复原:碰撞数据

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

图 8. 完全弹性碰撞的示例
示例碰撞解决轨迹路径

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


高级冲撞概念

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

现实世界的值

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

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

质量和力

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

清单 6. 力方程式计算出质量
// Our mass is always equal to one
var mass = 1;

// Force = mass * acceleration
var force = mass * acceleration;

// We can work the mass out of the equation
// and it won't change anything
force = acceleration;

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

形状和凹凸感

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

图 9. 凹和凸
凹多边形凸多边形

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


碰撞检测程序

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

清单 7. 碰撞检测程序 collideRect 测试
// Rect collision tests the edges of each rect to
// test whether the objects are overlapping the other
CollisionDetector.prototype.collideRect = 
    function(collider, collidee) {

    // Store the collider and collidee edges
    var l1 = collider.getLeft();
    var t1 = collider.getTop();
    var r1 = collider.getRight();
    var b1 = collider.getBottom();
    
    var l2 = collidee.getLeft();
    var t2 = collidee.getTop();
    var r2 = collidee.getRight();
    var b2 = collidee.getBottom();
    
    // If the any of the edges are beyond any of the
    // others, then we know that the box cannot be
    // colliding
    if (b1 < t2 || t1 > b2 || r1 < l2 || l1 > r2) {
        return false;
    }
    
    // If the algorithm made it here, it had to collide
    return true;
};

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

图 10. 碰撞检测示例
具有碰撞目标的碰撞检测示例

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


碰撞求解器

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

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

图 11. 碰撞解决位移图
碰撞解决方案的位移过程图:首先,检测碰撞,然后找到位移空间,通过位移解决碰撞

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

清单 8. 位移碰撞解决
resolveElastic: function(player, entity) {
    // Find the mid points of the entity and player
    var pMidX = player.getMidX();
    var pMidY = player.getMidY();
    var aMidX = entity.getMidX();
    var aMidY = entity.getMidY();
    
    // To find the side of entry calculate based on
    // the normalized sides
    var dx = (aMidX - pMidX) / entity.halfWidth;
    var dy = (aMidY - pMidY) / entity.halfHeight;
    
    // Calculate the absolute change in x and y
    var absDX = abs(dx);
    var absDY = abs(dy);
    
    // If the distance between the normalized x and y
    // position is less than a small threshold (.1 in this case)
    // then this object is approaching from a corner
    if (abs(absDX - absDY) < .1) {

        // If the player is approaching from positive X
        if (dx < 0) {

            // Set the player x to the right side
            player.x = entity.getRight();

        // If the player is approaching from negative X
        } else {

            // Set the player x to the left side
            player.x = entity.getLeft() - player.width;
        }

        // If the player is approaching from positive Y
        if (dy < 0) {

            // Set the player y to the bottom
            player.y = entity.getBottom();

        // If the player is approaching from negative Y
        } else {

            // Set the player y to the top
            player.y = entity.getTop() - player.height;
        }
        
        // Randomly select a x/y direction to reflect velocity on
        if (Math.random() < .5) {

            // Reflect the velocity at a reduced rate
            player.vx = -player.vx * entity.restitution;

            // If the object's velocity is nearing 0, set it to 0
            // STICKY_THRESHOLD is set to .0004
            if (abs(player.vx) < STICKY_THRESHOLD) {
                player.vx = 0;
            }
        } else {

            player.vy = -player.vy * entity.restitution;
            if (abs(player.vy) < STICKY_THRESHOLD) {
                player.vy = 0;
            }
        }

    // If the object is approaching from the sides
    } else if (absDX > absDY) {

        // If the player is approaching from positive X
        if (dx < 0) {
            player.x = entity.getRight();

        } else {
        // If the player is approaching from negative X
            player.x = entity.getLeft() - player.width;
        }
        
        // Velocity component
        player.vx = -player.vx * entity.restitution;

        if (abs(player.vx) < STICKY_THRESHOLD) {
            player.vx = 0;
        }

    // If this collision is coming from the top or bottom more
    } else {

        // If the player is approaching from positive Y
        if (dy < 0) {
            player.y = entity.getBottom();

        } else {
        // If the player is approaching from negative Y
            player.y = entity.getTop() - player.height;
        }
        
        // Velocity component
        player.vy = -player.vy * entity.restitution;
        if (abs(player.vy) < STICKY_THRESHOLD) {
            player.vy = 0;
        }
    }
};

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

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

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


引擎

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

清单 9. 物理引擎
Engine.prototype.step = function(elapsed) {
    var gx = GRAVITY_X * elapsed;
    var gy = GRAVITY_Y * elapsed;
    var entity;
    var entities = this.entities;
    
    for (var i = 0, length = entities.length; i < length; i++) {
        entity = entities[i];
        switch (entity.type) {
            case PhysicsEntity.DYNAMIC:
                entity.vx += entity.ax * elapsed + gx;
                entity.vy += entity.ay * elapsed + gy;
                entity.x  += entity.vx * elapsed;
                entity.y  += entity.vy * elapsed;
                break;
            case PhysicsEntity.KINEMATIC:
                entity.vx += entity.ax * elapsed;
                entity.vy += entity.ay * elapsed;
                entity.x  += entity.vx * elapsed;
                entity.y  += entity.vy * elapsed;
                break;
        }
    }
    
    var collisions = this.collider.detectCollisions(
        this.player, 
        this.collidables
    );

    if (collisions != null) {
        this.solver.resolve(this.player, collisions);
    }
};

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

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


结束语

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

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


下载

描述名字大小
文章源代码HomebrewPhysicsEngineSource.zip5KB

参考资料

学习

获得产品和技术

讨论

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

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development
ArticleID=852538
ArticleTitle=为 JavaScript 游戏构建一个简单的 2D 物理引擎
publish-date=01042013