首页 > Computer > c++ > Build a simple 2D physics engine for JavaScript games
2013
02-17

Build a simple 2D physics engine for JavaScript games

Introduction

2D games come in many different shapes and sizes. In some cases, building your own 2D physics engine, which provides an approximate simulation of systems such as collision detection, is a good choice—especially when using JavaScript. Developing a robust physics engine for any platform is difficult, but often a simpler, concise engine is more suitable. If you’re in need of a slimmed down version of a popular physics engine, building one from scratch, without all the bells and whistles, can get the job done efficiently.

In this article, explore one implementation of a physics engine to build the basic framework of a platforming game. Use existing physics engines, such as Box2D, to compare with the example implementation. Code snippets show how the components interact.

You can also download the source code for the examples used in this article.


Why simpler?

In game development, simpler can mean a lot of things. With a physics engine, simpler usually refers to the complexity of the calculations. Complexity becomes more relevant after you identify the lowest common denominator for your game. Complexity, in terms of calculations, means that the processing may take longer or that the math for the physics may be difficult. To follow along with this article, you will not need a working knowledge of calculus.

Since JavaScript works well with HTML5 canvas, you’ll often see physics applied to a canvas-based game. In some cases, such as a mobile platform like iOS or Android, the graphics from canvas become a dominating factor in your game. Building to a smaller resource platform means you need to squeeze as much processing as possible to leave enough room for the CPU to perform expensive graphics computations.

CPU utilization

Processing is the main reason to move from a well-tested, robust library to a home-grown, concise solution. The processing to be concerned about is called CPU utilization. CPU utilization is the amount of processing that’s available, or that’s being used, during your program’s or game’s runtime. Physics engines can take up as much CPU processing as the other parts of your game. Simpler choices mean less CPU utilization.

When running games, you typically aim for a target of 30-60 frames per second, which means your game’s loop must fit within 33-16 milliseconds. Figure 1 shows an example. When a more complex solution is followed, it means compromising other features that may take up some of the CPU utilization of your game. Shaving off CPU usage from any of your game components, whenever possible, helps in the long run.

Figure 1. Example CPU utilization loop step
CPU utilization of game logic, physics, and rendering in a 16ms loop step interval

Deciding on the components

When using 2D, many different and elaborate effects can be emulated or faked, as long as you have an adequate handling of the engine. When building your game, consider what components you need. Decide which pieces you need to build out and which ones you can avoid forcing your engine to calculate completely. Things like point gravity, illustrated in Figure 2, are harder to fake, while smaller hit areas can be easily accomplished.

Figure 2. Point gravity
Point gravity diagram illustrating gravity towards the center


Building a physics engine

This section discusses what makes up a physics engine and how to decide which features to include.

The first major step in building a physics engine is to choose the features and the order of operations. Deciding on the features may seem trivial, but the features help form the physics engine components and could indicate areas that might be difficult. For the example application, you’ll build an engine to a game like the one shown in Figure 3.

Figure 3. Platforming game
Game scene depiction

The boxes in Figure 3 indicate:

  • Player: Box containing diagonal lines
  • Win condition, goal: Solid, black box
  • Normal platform: Solid-line box
  • Bouncy platform: Dashed-line box

Simple programmer graphics aside, you can visualize the features of the game. Players win when they reach the win condition/goal. The game is a platformer, so you’ll need some of the most basic building blocks of a physics engine:

  • Velocity, acceleration, and gravity
  • Collision detection
  • Bouncy collisions
  • Normal collisions

The positional attributes will be used to drive the player. The collision detection will allow for the player to reach the goal and move around the game. The collision types will allow the game to have different ground types. By only having one player and essentially one dynamic object in the game, you can tone down the amount of collisions in the code.

Now that the game’s features and the physics aspects of the features are identified, you can begin mapping out the physics engine structure.

Selecting the correct engine runtime

Physics engines come in two main forms: high precision and real-time. High precision engines are used to simulate difficult or critical physics calculations; real-time engines are the types you see in video games. For the physics engine in the example, you’ll use a real-time engine that infinitely runs its engine calculations until required to stop.

Real-time engines offer two options for controlling the physics engine timing:

Static

Always provides the engine with a non-changing amount of time that is expected to pass every frame. Static real-time engines run at different speeds on different computers, so it’s common that they behave differently.

Dynamic

Feeds an elapsed time to the engine.

The example physics engine in this article needs to be running continuously, so you need to set up an infinite loop to run against the engine. This processing pattern is called the game loop. For this article, each one of the operations you’ll run in that loop is called a step. Use the requestAnimationFrame API to use the dynamic option. Listing 1 shows the code required to run requestAnimationFrame. It uses a polyfill housed on the Khronos Group CVS Repository.

Listing 1. requestAnimFrame with polyfill

// call requestAnimFrame with a parameter of the
// game loop function to call
requestAnimFrame(loopStep);

Physics engine loop

Deciding on the order of operations within the loop might seem simple but it is not a trivial decision. While several different options are available for this step, you’ll design the engine based on the features identified thus far, as shown in Figure 4.

Figure 4. Physics loop step
Run loop step example process: First, user interaction to game logic, then positional logic, collision detection, collision resolution, and, finally, render

Listing 2 shows that the step performs the calculation so that it does each type of calculation in a single pass. The other approach for this calculation would be to individually perform each object’s calculations, but that generally produces odd results due to dependencies on other calculations.

Listing 2. Physics loop step pseudocode

1 User Interaction
2 Positional Logic
3 Detect Collisions
4 Resolve Collisions

Now that you have the workflow, the features, and a decision on the type of engine to build, you can start building the pieces.

Rigid body physics

Physics, as a science, is very vast and includes several different kinds of calculations. Newtonian physics makes up the common position, velocity, and acceleration calculations, while electromagnetism makes up magnets or electricity and can be used to emulate gravity in a physics system. Each of these areas of physics are great in their own right, but the level of complexity for those calculations goes beyond the scope of this article.

When deciding on your physics system, the construction of the engine depends upon the type of calculations you want to do. The example engine will implement rigid body physics, or physics that does not deform. With rigid body physics you can avoid calculating force-based deformities that you would see in soft body dynamics or extra force modifications that you would see in any sort of multi-gravitational system.


Pieces of the engine

Physics engines are fairly complex computationally but are rather simple to structure once you know the pattern. Listing 2 included a high-level loop step pseudocode. Each calculation in that step can consist of its own object or API. The engine object graph consists of the following main components:

  • Physics entity
  • Collision detector
  • Collision solver
  • Physics engine

The entity, which is the object, body, or model that the engine acts upon, is the least active. The equivalent in Box2D is a b2Body class. The detector and solver work in concert by first finding collisions between entities and then passing over the collisions to the solver. Collisions are then converted into modifications applied to any entities the collisions are acting on.

Physics engines, while encompassing the engine in its entirety, essentially manage, prepare, and communicate with each component through each phase in the system. Figure 5 shows the relationship between each element in the physics engine.

Figure 5. Physics engine
Physics engine step example process: First, calculate new positions, followed by collision detector, and then collision resolution

The four components make up the main force behind our engine. The first component you’ll implement is the physics entity, which will represent every object on the screen.


Physics entity

Though the physics entity makes up the smallest and simplest of the components in the engine, it is the most important. As noted previously, the entity will represent each element on the screen. Entities, in both games and physics, represent the object’s state by holding all the relevant metadata to that entity, as shown in Listing 3.

Listing 3. Entity JavaScript object

// 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';

Entity as a model

As the logic in Listing 3 shows, the physics entity stores nothing but raw data and produces different variants of the data set. If you’re familiar with any of the MV* patterns, such as MVC or MVP, the entity represents the Model component of those patterns. (See Resources for information on MVC.) The entity stores several pieces of data—all to represent the state of that object. For every step in the physics engine, those entities change, which ultimately changes the state of the engine as a whole. The different groupings of the entity’s state data are discussed in more depth later.

You might have noticed that the entity in Listing 3 does not include a render or draw function to display the entity on to the screen. By decoupling the physics logic and representations from the graphical representations, you can render out any graphical representation, thereby providing the power to skin the game entities. Despite the use of rectangles to represent the objects, as shown in Figure 6, you aren’t limited to rectangular images. Entities can have any image applied.

Figure 6. Bounding box and sprite
Sprite with a hit area within the center

Position, velocity, and acceleration: The positional data

The entity includes positional data, which is information depicting how the entity can move in space. Positional data includes basic logic that you would see in Newtonian kinematic physics equations (see Resources). Of these data points, the example entity is concerned about the acceleration, velocity, and position.

Velocity is the change in position over time, and, similarly, acceleration is the change in velocity over time. Calculating the change in position ends up being a fairly straightforward calculation, as shown in Listing 4.

Listing 4. Position equation

p(n + 1) = v * t + p(n)

Where:

  • p is the position of the entity. This is often represented with x and y.
  • v is the speed or velocity. This is the amount of position change over time.
  • t is the amount of time that passed. In JavaScript, this is measured in milliseconds.

Based on the equation in Listing 4, you know that the entity’s position will inevitably be impacted by the time applied to it. Similarly, the velocity is updated by acceleration, as shown in Listing 5.

Listing 5. Velocity equation

v(n + 1) = a * t + v(n)

Where:

  • v is the speed or velocity. This is the amount of position change over time.
  • a is the acceleration of the entity. This is the amount of velocity change over time.
  • t is the amount of time that passed. In JavaScript, this is measured in milliseconds.

Listing 5 is similar to Listing 4, except that a represents the entity’s acceleration. Though you won’t be looping these equations into the entity’s logic since it is meant to only store it, it’s important to know what those parameters mean. You also need to consider the spatial representation of these entities.

Width and height: The spatial data

Spatial data refers to the parameters required to represent the hit area and space that the object takes. Elements such as shape and size affect the spatial data. For our example platform, you’ll take a “smoke and mirrors” approach to the hit area. (Smoke and mirrors, a metaphor for deception or fraudulence, is based on magicians’ illusions.)

It’s common to use a hit area larger or smaller than the graphic or sprite representing the entity. Such hit areas often constitute a bounding box or rectangle. In the interest of simplicity and need for fewer calculations, the implementation will only use rectangles to test hit areas.

Rectangles, in graphics and physics, can be represented by four numbers. Use the upper left point for position, and use the width and height for size, as illustrated in Figure 7.

Figure 7. Box representation
Box with spatial measurements

The bounding boxes only need position and size, as all other parameters are dependent on those components. Other calculations that also help are calculations for midpoints and edges, both of which are commonly used for collision calculations.

Restitution: The collision data

The final component of the entity is the collision data, or the information determining how the object should behave during collisions. For the example, collisions will include only displacement and elastic collisions, so the requirements are fairly limited, as shown in Figure 8. The elastic collisions follow the naming patterns used in Box2D and define bounciness as restitution.

Figure 8. Fully elastic collision example
Example collision resolution trajectory path

All of the data components provide the system with enough data to begin calculating all the changes that will occur over time in the example implementation.


Advanced collision concepts

The entity is sufficient for the example system, but there are often other parameters used for various collision solving. This section briefly discusses some of the other types.

Real world values

In games, designers often model their worlds based on our world and measure things to the units in real-world space. Implementations that model their systems off of real world units, such as meters, only need a scale factor to convert their data points to and from these distances.

This article doesn’t provide an example of the conversions, but Box2D (see Resources) provides a conversion ratio with supporting examples in several of their examples.

Mass and force

Mass and force make up a majority of physics engines, which might make you wonder why the example system does not. Because the example system does not require a mass, you can rely on the fact that force is a product of mass and acceleration and assume the mass is one unit. You can see this calculation in Listing 6.

Listing 6. Force equation working out mass

// 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;

The calculations are outside the scope of this article. The sum of all forces, or the conservation of energy, are two physics calculations that you can use to calculate different collisions involving several moving entities rather than the singular entity used by the example system.

Shapes and concavity

The example system will support non-rotational bounding boxes, but sometimes more precise collision detection and solving are required. In some such cases, you should consider a polygonal approach, or include several bounding boxes to represent a single entity, as shown in Figure 9.

Figure 9. Concave and convex
Concave polygon and convex polygon

When a shape has a section broken out of it, you can end up with a shape that is concave. As shown in Figure 9, concave shapes encompass shapes that have any side that pushes inwards towards the center. Concave shapes often require more precise and complex collision-detection algorithms. The other option, convex, are shapes that do not have any sides that push inwards towards the center. It is important to consider what kind of complexity your system will support when deciding on your shape support.


Collision detector

The second major step for the physics engine is the detection and resolution of collisions. A collision detector does exactly what the name implies. The collision detector for this article will be simple and will include calculations to find whether a rectangle collides with another rectangle, but the objects often can collide with various types of objects, as shown in Listing 7.

Listing 7. Collision detector collideRect test

// 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;
};

Listing 7 is the collision algorithm used to test the bounding boxes. The algorithm uses logic to determine if all of the edges are outside the bounds of the other rectangle in order to determine collision success. This collision detection doesn’t support other shapes, as shown in Figure 10, but it’s sufficient for illustration purposes.

Figure 10. Collision detection example
Collision detection example with collision targets

The single moving entity situation solves the issue of determining which objects to collide with. When building a collision-detection algorithm, it helps to prune off unnecessary entities.


Collision solver

The last part of the collision pipeline is the solver. Once a collision is encountered, entities must calculate their resolved location so the colliding entities are no longer colliding and the new directions are handled. In the example, you have objects that either completely absorb the force applied upon impact or reflect some of the force applied upon collision. The solver includes methods for the displacement calculations and for the elastic collisions mentioned previously.

As shown in Figure 11, the solver resolutions change the direction and position of the player. The example solver will first move the player back to the point where the collision first started in the direction from which the entity was coming.

Figure 11. Collision solve displacement diagram
Collision solution displacement process diagram: First, detect collision, then find displacement space, and resolve collision by displacing

The solver equation will then modify the velocity of the entity. The displacement algorithm will remove the velocity, and the elastic one will use the restitution mentioned earlier to dampen and reflect the player’s velocity, as shown in Listing 8.

Listing 8. Displacement collision resolution

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;
        }
    }
};

In Listing 8, the elastic collision uses a calculation to determine the overlap placement on the rectangle by checking the angle difference in direction between the player and the entity. The rectangles are normalized so that all calculations are handled as if the rectangle were a square. This kind of calculation is represented with the example in Figure 11. In this case, the resolver directly modifies the velocity. When dealing with multiple object collisions, be careful not to greedily set the velocity directly. Changing velocity due to multiple sources requires the use of energy-conservation equations and calculating the different angles and velocities.

The displacement algorithm is also used on the displacement calculation, with the exception that the velocity is set to zero rather than reflected and reduced.

Now that the components are outlined, the final step is to pull the system together in the engine.


Engine

The final component of the system is the engine itself, which is in charge of the workflow pseudocode (see Listing 2). The jobs of the engine generally match that of a controlling entity and include pushing content in and out of the collision components every loop step. The example engine’s implementation manages the positional logic changes and the gravity before collisions are encountered, as shown in Listing 9.

Listing 9. Physics engine

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);
    }
};

Following the physics step, the engine first calculates the gravity and then applies it and the entity’s acceleration to the velocity before calculating the entity’s new x and y position. In the example, the collisions are produced by the collision detector’s detectCollisions method. This detection method functions as a router to use the appropriate method (in this case, collideRect). Likewise, the solver uses the generic resolve method to transfer the call over to resolveElastic or its own displacement-resolver method.

While the engine in this implementation has a fairly trivial set of control, its usage is integral. In other engines, the engine may take a more front-seat role. In some cases, the engine will be sufficient for managing the physics step and the entities interacting in the physics engine. Beware of a common trap: don’t allow the system to overshoot any possible collidable entities. If your entity moves further than the space a collidable entity takes up, your detector will completely ignore it.


Conclusion

When you build your own physics engine, implementing a simple solution for your game can work to your advantage. Though a robust solution is always more desirable, if you build a physics engine where computational complexity is important, or when building to a low processing device such as iOS or Android, you should consider a smoke-and-mirrors approach. When computational resources are low, solutions like those outlined in this article are ideal.

Physics engines come with several parts to make them work. The most important parts are the entity, collision detector, collision solver, and engine core. By having the components working in unison, managing their own specific jobs, simple physics can be accomplished by selecting your shapes and algorithms with care. As long as you make appropriate decisions about your supported features, you can build a smaller subset of the more robust physics engine implementations. The more robust physics engines can always help drive your own to determine the best approach for your own game.


Download

Description Name Size Download method
Article source code HomebrewPhysicsEngineSource.zip 5KB HTTP

Information about download methods

Resources

Learn

Get products and technologies

Discuss

  • developerWorks community: Connect with other developerWorks users while exploring the developer-driven blogs, forums, groups, and wikis.
  • 最后编辑:
    作者:wy182000
    这个作者貌似有点懒,什么都没有留下。

    留下一个回复