JavaScript & HTML5: Implementing the 2D Physics Engine Core - The Core Engine Loop

JavaScript & HTML5: Implementing the 2D Physics Engine Core - The Core Engine Loop
« Prev
Next

The Core Engine Loop

One of the most important characteristics of any physics engine is the support of seemingly intuitive and continuous interactions between the objects and the graphical simulation elements. In reality, these interactions are implemented as a continuous running loop that receives and processes the calculations, updates the object states, and renders the objects. This constantly running loop is referred to as the engine loop.

To convey the proper sense of intuitiveness, each cycle of the engine loop must be completed within a normal human’s reaction time. This is often referred to as real time, which is the amount of time that is too short for humans to detect visually. Typically, real-time can be achieved when the engine loop is running at a rate of higher than 40 to 60 cycles in a second. Since there is often one drawing operation in each loop cycle, the loop cycle’s rate can also be expressed as frames per second (FPS ) , or the frame rate. An FPS of 60 is a good target for performance. This is to say, your engine must process calculations, update the object states, and then draw the canvas all within 1/60th of a second!

The loop itself, including the implementation details, is the most fundamental control structure for an engine. With the main goal of maintaining real-time performance, the details of an engine loop’s operation are of no concern to the rest of the physics engine. For this reason, the implementation of an engine loop should be tightly encapsulated in the core of the engine, with its detailed operations hidden from other elements.

Engine Loop Implementations

An engine loop is the mechanism through which logic and drawing are continuously executed. A simple engine loop consists of processing the input, updating the state of objects, and drawing those objects, as illustrated in the following pseudocode:

initialize();

while(game running) {

    input();
    update();
    draw();

}

As discussed, an FPS of 60 or higher is ideal to maintain the sense of real-time interactivity. When the game complexity increases, one problem that may arise is when sometimes a single loop can take longer than 1/60th of a second to complete, causing the game to run at a reduced frame rate. When this happens, the entire game will appear to slow down. A common solution is to prioritize which operations to emphasis and which to skip. Since correct input and updates are required for an engine to function as designed, it is often the draw operation that is skipped when necessary. This is referred to as frame skipping, and the following pseudocode illustrates one such implementation:

elapsedTime = now;
previousLoop = now;

while(game running) {

    elapsedTime += now - previousLoop;
    previousLoop = now;
    input();

    while( elapsedTime >= UPDATE_TIME_RATE ) {

        update();
        elapsedTime -= UPDATE_TIME_RATE;

    }

    draw();

}

In the previous pseudocode listing, UPDATE_TIME_RATE is the required real-time update rate. When the elapsed time between the engine loop cycle is greater than the UPDATE_TIME_RATE, the update function will be called until it is caught up. This means that the draw operation is essentially skipped when the engine loop is running too slowly. When this happens, the entire game will appear to run slowly, with lagging play input response and frames skipped. However, the game logic will continue to be correct.

Notice that the while loop that encompasses the update function call simulates a fixed update time step of UPDATE_TIME_RATE. This fixed time step update allows for a straightforward implementation in maintaining a deterministic game state.

The Core Engine Loop Project

This project demonstrates how to incorporate a loop into your engine and to support real-time simulation by updating and drawing the objects accordingly. You can see an example of this project running in Figure 3. The source code to this project is defined in the Core Engine Loop Project folder.

 Running the Core Engine Loop Project

Figure 3. Running the Core Engine Loop Project

The goals of the project are as follows:

  • To understand the internal operations of an engine loop.
  • To implement and encapsulate the operations of an engine loop.
  • To gain experience with continuous update and draw to simulate animation.

Implement the Engine Loop Component

The engine loop component is a core engine functionality and thus should be implemented as a property of the gEngine.Core. The actual implementation is similar to the pseudocode listing discussed.

  1. Edit the js file.
  2. Add the necessary variables to determine the loop frequency.
var mCurrentTime, mElapsedTime, mPreviousTime = Date.now(), mLagTime = 0;
var kFPS = 60;          // Frames per second
var kFrameTime = 1 / kFPS;
var mUpdateIntervalInSeconds = kFrameTime;
var kMPF = 1000 * kFrameTime; // Milliseconds per frame.
  1. Update the runGameLoop function to keep track of the elapsed time between frames and to ensure that the update function is called at the frame rate frequency.
var runGameLoop = function () {

    requestAnimationFrame(function () {
    runGameLoop();

    });

    //compute how much time has elapsed since the last RunLoop
    mCurrentTime = Date.now();
    mElapsedTime = mCurrentTime - mPreviousTime;
    mPreviousTime = mCurrentTime;
    mLagTime += mElapsedTime;

    //Update the game the appropriate number of times.
    //Update only every Milliseconds per frame.
    //If lag larger then update frames, update until caught up.
    while (mLagTime >= kMPF) {

        mLagTime -= kMPF;
        update();

    }

    updateUIEcho();
    draw();

};
  1. Modify the updateUIEcho function to print out additional relevant application state information, like how to rotate and move the selected rigid shape. The code in bold is the only addition to the function.
var updateUIEcho = function () {

    document.getElementById("uiEchoString").innerHTML =

    // ... identical to previous project
    mAllObjects[gObjectNum].mCenter.y.toPrecision(3) + "</li>"  +

        "<li>Angle: " + mAllObjects[gObjectNum].mAngle.toPrecision(3) + "</li>"  +                      
    "</ul> <hr>" +

    "<p><b>Control</b>: of selected object</p>" +

    "<ul style=\"margin:-10px\">" +

        "<li><b>Num</b> or <b>Up/Down Arrow</b>: SelectObject</li>" +
        "<li><b>WASD</b> + <b>QE</b>: Position [Move + Rotate]</li>" +                      
    "</ul> <hr>" +
    "<b>F/G</b>: Spawn [Rectangle/Circle] at selected object" +
    "<p><b>H</b>: Fix object</p>" +                      
    "<p><b>R</b>: Reset System</p>" +                      
    "<hr>";

};
  1. Create a new function named update, which will call the update function of every rigid shape defined.
var update = function () {

    var i;

    for (i = 0; i < mAllObjects.length; i++) {

        mAllObjects[i].update(mContext);

    }

};

Extend the Rigid Shape Classes

You are going to modify the rigid shape base class, and both of the Rectangle and Circle classes to support the implementation of simple behavior. While the update function is defined in the rigid shape base class to be invoked by the game engine loop, the detailed implementation of update must necessarily be subclass-specific. For instance, a circle object implements moving behavior by changing the values in its center while a rectangle object must change all of the values in the vertex and face normal arrays to simulate the same movement behavior.

Rigid Shape Base Class
  1. Edit the js file.
  2. Define the update function to be called by the engine loop and implement the simple falling behavior by changing the center position with a constant y-direction vector. Notice that the free fall behavior is only applied when the shape is within the vertical bounds of the canvas.
RigidShape.prototype.update = function () {

    if (this.mCenter.y < gEngine.Core.mHeight && this.mFix !== 0)

        this.move(new Vec2(0, 1));

};

Subclasses are responsible for defining the mFix variable and the move function to control if the shape is fixed where it should not follow the falling behavior and to implement the moving of the shape. It should be emphasized that this rigid shape movement behavior is included here for testing purposes only and will be removed in the next project. Actual physics-based movement of rigid shape objects and the associated physical quantities (including velocity and acceleration) will be introduced and discussed in my late blogs.

Note that by default the canvas coordinate defines the origin, (0, 0), to be located at the top left corner, and positive y direction to be downwards. For this reason, to simulate gravity, you will move all objects in the positive y direction.

The Circle Class

The Circle class is modified to implement movements.

  1. Edit the js file.
  2. Define the mFix instance variable to enable or disable the falling behavior.
var Circle = function (center, radius, fix) {

    // ... code similar to previous project
    this.mFix = fix;


    // ... code similar to previous project
  1. Add a move function to define how a circle is moved by a vector—adding the movement vector to the center and the mStartpoint.
Circle.prototype.move = function (s) {

    this.mStartpoint = this.mStartpoint.add(s);
    this.mCenter = this.mCenter.add(s);

       return this;

};
  1. Add rotate function to implement the rotation of a circle. Note that since a circle is infinitely symmetrical, a rotated circle would appear identical to the original shape. The mStartpoint position allows a rotated reference line to be drawn to indicate angle of rotation of a circle.
// rotate angle in counterclockwise
Circle.prototype.rotate = function (angle) {

    this.mAngle += angle;
    this.mStartpoint = this.mStartpoint.rotate(this.mCenter, angle);

    return this;

};
The Rectangle Class

Similar to the circle class, the Rectangle class must be modified to support the new functionality.

  1. Edit the js file.
  2. Define the mFix instance variable to enable or disable the falling behavior.
var Rectangle = function (center, width, height, fix) {

    // ... code similar to previous project
    this.mFix = fix;

    // ... code similar to previous project
  1. Define the move function by changing the values of all vertices and the center.
Rectangle.prototype.move = function (v) {

    var i;

    for (i = 0; i < this.mVertex.length; i++) {

        this.mVertex[i] = this.mVertex[i].add(v);

    }

    this.mCenter = this.mCenter.add(v);

    return this;

};
  1. Define the rotate function by rotating all over the vertices and recomputing the face normals.
Rectangle.prototype.rotate = function (angle) {

    this.mAngle += angle;
    var i;

    for (i = 0; i < this.mVertex.length; i++) {

        this.mVertex[i] = this.mVertex[i].rotate(this.mCenter, angle);

    }

    this.mFaceNormal[0] = this.mVertex[1].subtract(this.mVertex[2]);
    this.mFaceNormal[0] = this.mFaceNormal[0].normalize();
    this.mFaceNormal[1] = this.mVertex[2].subtract(this.mVertex[3]);
    this.mFaceNormal[1] = this.mFaceNormal[1].normalize();
    this.mFaceNormal[2] = this.mVertex[3].subtract(this.mVertex[0]);
    this.mFaceNormal[2] = this.mFaceNormal[2].normalize();
    this.mFaceNormal[3] = this.mVertex[0].subtract(this.mVertex[1]);
    this.mFaceNormal[3] = this.mFaceNormal[3].normalize();

    return this;

};

Modify User Control Script

You will need to extend the userControl function defined in the UserControl.js file to support movements, rotation, disable/enable gravity, and reset the entire scene.

  1. Edit the js file.
  2. Add statements to support moving, rotating, and toggling of gravity on the selected object.
// move with WASD keys
if (keycode === 87) { //W

    gEngine.Core.mAllObjects[gObjectNum].move(new Vec2(0, -10));

}


if (keycode === 83) { // S

    gEngine.Core.mAllObjects[gObjectNum].move(new Vec2(0, +10));

}


if (keycode === 65) { //A

    gEngine.Core.mAllObject[gObjectNum].move(new Vec2(-10, 0));

}


if (keycode === 68) { //D

    gEngine.Core.mAllObjects[gObjectNum].move(new Vec2(10, 0));

}


// Rotate with QE keys
if (keycode === 81) { //Q

    gEngine.Core.mAllObjects[gObjectNum].rotate(-0.1);

}

if (keycode === 69) { //E

    gEngine.Core.mAllObjects[gObjectNum].rotate(0.1);

}


// Toggle gravity with the H key
if (keycode === 72) { //H

    if(gEngine.Core.mAllObjects[gObjectNum].mFix === 0)

        gEngine.Core.mAllObjects[gObjectNum].mFix = 1;

    else gEngine.Core.mAllObjects[gObjectNum].mFix = 0;

}
  1. Add a statement to reset the scene.
if (keycode === 82) { //R

    gEngine.Core.mAllObjects.splice(5, gEngine.Core.mAllObjects.length);
    gObjectNum = 0;

}
  1. Modify object creation statements of the G and F keys such that the new object is created at the location of the currently selected object, rather than a random position.
if (keycode === 70) { //f

    var r1 = new Rectangle(new Vec2(gEngine.Core.mAllObjects[gObjectNum].mCenter.x,
       gEngine.Core.mAllObjects[gObjectNum].mCenter.y),
       Math.random() * 30 + 10, Math.random() * 30 + 10);

}

if (keycode === 71) { //g

    var r1 = new Circle(new Vec2(gEngine.Core.mAllObjects[gObjectNum].mCenter.x,
       gEngine.Core.mAllObjects[gObjectNum].mCenter.y),
       Math.random() * 10 + 20);


}

Update the Scene

To test the implemented engine loop and object movements, you will create an initial selected object to the scene. This initial object will serve as the cursor position for spawning created rigid shapes. This can be accomplished by editing the MyGame.js file and creating an initial object.

function MyGame() {

    var width = gEngine.Core.mWidth;
    var height = gEngine.Core.mHeight;
    var r1 = new Rectangle(new Vec2(width / 2, height / 2), 3, 3, 0);
    var up = new Rectangle(new Vec2(width / 2, 0), width, 3, 0);
    var down = new Rectangle(new Vec2(width / 2, height), width, 3, 0);
    var left = new Rectangle(new Vec2(0, height / 2), 3, height, 0);
    var right = new Rectangle(new Vec2(width, height / 2), 3, height, 0);

}

 

Observation

Run the project to test your implementation. You will see that the scene is almost the same as that of the previous project except for the small initial cursor object. See that you can change the selected object, and thereby the cursor object, with the 0 to 9, or the up and down arrow keys. Type F and G keys to see that new objects are created at the cursor object location and they always follow the falling behavior. This real-time smooth falling behavior indicates that the engine loop has been successfully implemented. You can play around with the selected shape position using the WASD, QE, and H keys; and move, rotate, and toggle gravity on the selected object. You may also notice that without movement of the cursor object, the newly created objects are clustered together, which can be confusing. That is because the physics simulation has yet to be defined. 

 

Summary

In this blog, you have implemented basic rigid shape classes. Although only simple position, orientation, and drawing are supported, these classes represent a well-defined abstraction, hide implementation details, and thus support future integration of complexity. In the following articles, you will learn about other physical quantities including mass, inertia, friction, and restitution. The engine loop project introduced you to the basics of a continuous update loop that supports real time per-shape computation and enables visually appealing physics simulations. In the next blog, you will begin learning about physics simulation by first examining the collision between rigid shapes in detail.

 

Вас заинтересует / Intresting for you:

JavaScript & HTML5:  Introduct...
JavaScript & HTML5: Introduct... 2529 views Денис Wed, 28 Nov 2018, 15:58:51
Comparing Java with JavaScript
Comparing Java with JavaScript 1296 views Antoni Tue, 27 Nov 2018, 13:21:04
Getting Started with Oracle JE...
Getting Started with Oracle JE... 3853 views Александров Попков Sun, 29 Apr 2018, 10:07:53
What Is the J2ME Platform?
What Is the J2ME Platform? 3229 views Илья Дергунов Sat, 03 Feb 2018, 17:05:49
« Prev
Next
Comments (0)
There are no comments posted here yet
Leave your comments
Posting as Guest
×
Suggested Locations