JavaScript & HTML5: Implementing the 2D Physics Engine Core - Physics Engine and Rigid Shapes

JavaScript & HTML5: Implementing the 2D Physics Engine Core - Physics Engine and Rigid Shapes
« Prev
Next »

Physics Engine and Rigid Shapes

My blog focuses on primitive objects that do not change shape during their physical interactions, or objects that are rigid. For example, a falling Lego block bouncing off of your desk and landing on a hardwood floor would be considered an interaction between rigid objects. This type of simulation is known as a rigid body physics simulation, or more simply a rigid body simulation.

The computation involved in simulating the interactions between arbitrary rigid shapes can be algorithmically complicated and computationally costly. For these reasons, rigid body simulations are often based on a limited set of simple geometric shapes, for example, rigid circles and rectangles. In typical game engines, these simple rigid shapes can be attached to geometrically complex game objects for approximating their physics simulations, for example, attaching rigid circles on spaceships and using the rigid body physics simulation of the rigid circles to approximate the physical interactions between the spaceships.

The physics engine you will build is based on simulating the interactions between rigid circles and rectangles. This simulation consists of four fundamental steps :

  1. Implementing motions
  2. Detecting collisions
  3. Resolving the collisions
  4. Deriving responses to the collisions

The rest of this blog leads you to build the infrastructure to represent simple rigid circles and rectangles. The following articles present the intricate details of collision detection, motion approximation, collision resolution, and collision responses.

 

The Rigid Shape Project

This project demonstrates how to implement the basic infrastructure to encapsulate the characteristics of a rigid body. You can see an example of this project running in Figure 1.

Running the Rigid Shape Project 

Figure 1. Running the Rigid Shape Project

The source code to this project is defined in the Rigid Shape Project folder.

Project Goals :

  • To define the base class for all rigid shape objects.
  • To lay the foundation for building a rigid shape physics simulator.
  • To understand the relationships between rigid shape classes and the engine core functionality.
  • To define an initial scene for testing your implement.

The List Object in Engine Core

You will begin by defining a list object, mAllObjects, to keep track of all defined rigid shapes. The mAllObjects list allows the simulation of physical interaction among all defined rigid shapes. To conveniently support the simulation computation, the mAllObjects list is defined in the gEngine.Core component .

  1. Edit js and add the following line inside gEngine.Core. This creates a list for keeping track of all defined rigid shapes.
var mAllObjects = [];
  1. Update the mPublic variable in the js to allow access to the newly defined list object. This is accomplished in the following code snippet.
var mPublic = {

    mAllObjects: mAllObjects,
    mWidth: mWidth,
    mHeight: mHeight,
    mContext: mContext

};

The Rigid Shape Base Class

You can now define a base class for the rectangle and circle shape objects. This base class will encapsulate all the functionality that is common to the two shapes.

  1. Start by creating a new subfolder called RigidBody under the SiteRoot (or public_html) folder. In the RigidBody folder, create a new file and name it js
  2. Edit js to define the constructor. For now the constructor only receives one vector argument representing the center of the object. The rotation angle of the rigid shape has a default value of 0. The created object is then pushed into the global object list, mAllObjects.
function RigidShape(center) {

    this.mCenter = center;
    this.mAngle = 0;
    gEngine.Core.mAllObjects.push(this);

}

The Rigid Rectangle Class

With the base abstract class for rigid shapes defined, you can now create the first concrete rigid shape, the rigid rectangle.

  1. Under the RigidBody folder, create a new file and name it js.
  2. Edit this file to create a constructor that receives the center, a width and height In the constructor, define the type of rigid body as Rectangle, allocate an array to store the vertex positions of the rectangle, and a separate array to store the face normal vectors (to be discussed later).
var Rectangle = function (center, width, height) {

    RigidShape.call(this, center);
    this.mType = "Rectangle";
    this.mWidth = width;
    this.mHeight = height;
    this.mVertex = [];
    this.mFaceNormal = [];

};
  1. In the constructor, compute the vertex positions of the rectangle using the center, width, and height information.
//0--TopLeft;1--TopRight;2--BottomRight;3--BottomLeft

this.mVertex[0] = new Vec2(center.x - width / 2, center.y - height / 2);
this.mVertex[1] = new Vec2(center.x + width / 2, center.y - height / 2);
this.mVertex[2] = new Vec2(center.x + width / 2, center.y + height / 2);
this.mVertex[3] = new Vec2(center.x - width / 2, center.y + height / 2);
  1. Next, compute the face normal vectors. As illustrated in Figure 2, face normals are vectors that are perpendicular to the edges and point away from the center of the rectangle. Notice that the face normal vectors are normalized with a length of 1. In addition, notice the relationship between the rectangle vertices and the corresponding face normals. Face normal index-0 is in the same direction as the vector from vertex 2 to 1. This direction is perpendicular to the edge formed by vertices 0 and 1. In this way, face normal index-0 is the direction pointing away from the rectangle that is perpendicular to the first edge, and so on. The face normal vectors will be used later for determining collisions.

The face normals of a rectangle 

Figure 2. The face normals of a rectangle

//0--Top;1--Right;2--Bottom;3--Left


//mFaceNormal is normal of face toward outside of rectangle
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();
  1. Ensure the newly defined Rectangle class properly inherits from the RigidShape base class by including the following code after the constructor.
var prototype = Object.create(RigidShape.prototype);

prototype.constructor = Rectangle;
Rectangle.prototype = prototype;
  1. Now you can create the draw function for the rectangle object. The strokeRect function of the context, a reference to the canvas, is invoked to accomplish this. Corresponding translation and rotation must be defined in order to draw the rectangle at the proper position and orientation. The implementation is shown as follows.
Rectangle.prototype.draw = function (context) {

    context.save();
    context.translate(this.mVertex[0].x, this.mVertex[0].y);
    context.rotate(this.mAngle);
    context.strokeRect(0, 0, this.mWidth, this.mHeight);
    context.restore();

};

The Rigid Circle Class

You can now implement the rigid circle object based on an overall structure that is similar to that of the rigid rectangle.

  1. Under the RigidBody folder, create a new file and name it js.
  2. Edit this file to create a constructor that initializes the radius of the circle, the rigid body type as Circle, and an mStartpoint position for the purpose of drawing a reference line to visualize the rotation angle of a circle. Initially, without rotation, the reference line is vertical, connecting the center of the circle to the top of the circumference. Changing the rotation angle of the circle will result in this line being rotated.
var Circle = function (center, radius) {

    RigidShape.call(this, center);

    this.mType = "Circle";
    this.mRadius = radius;

    // The start point of line in circle
    this.mStartpoint = new Vec2(center.x, center.y - radius);

};
  1. Similar to the Rectangle class, you must include the following code to ensure that the Circle class properly inherits from the RigidShape base class.
var prototype = Object.create(RigidShape.prototype);

prototype.constructor = Circle;
Circle.prototype = prototype;
  1. Distinct from that of the rectangle, the arc function of the context is used to draw the circle onto the canvas. In addition, you need to draw the rotation reference line from the center to the mStartpoint, the top of the circle.
Circle.prototype.draw = function (context) {

    context.beginPath();

    //draw a circle
    context.arc(this.mCenter.x, this.mCenter.y, this.mRadius, 0, Math.PI *  2, true);


    //draw a line from start point toward center
    context.moveTo(this.mStartpoint.x, this.mStartpoint.y);
    context.lineTo(this.mCenter.x, this.mCenter.y);
    context.closePath();
    context.stroke();

};

Modify the User Control Script

You will modify the UserControl.js file for testing the new functionality.

  1. Edit the js file in the SiteRoot (or public_html) folder.
  2. Add the gObjectNum variable, an index to the mAllObjects array representing the currently selected object. Notice that this variable is defined before the definition of the userControl function and is a global variable.
var gObjectNum = 0;
  1. Within the userControl function, define supports for the creation of random rectangles and circles with the F and G keys.
if (keycode === 70) {    // f

    var r1 = new Rectangle(new Vec2(Math.random()*width*0.8, Math.random()*height*0.8),
                         Math.random() * 30+10, Math.random() * 30+10);

}

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

    var r1 = new Circle(new Vec2(Math.random()*width*0.8, Math.random()*height*0.8),
                     Math.random() * 10 + 20);

}
  1. Within the userControl function, define supports for selecting an object index based on the up/down arrows and the 0 to 9 keys.
if (keycode >= 48 && keycode <= 57) {  //number

    if (keycode - 48 < gEngine.Core.mAllObjects.length)

        gObjectNum = keycode - 48;

}

if (keycode === 38) {   //up arrow

    if (gObjectNum > 0)

        gObjectNum--;

}

if (keycode === 40) {   // down arrow

    if (gObjectNum < gEngine.Core.mAllObjects.length-1)
        gObjectNum++;

}

Integrate into the Core

You can now modify the Core.js file to integrate and test the newly defined functionality. Your modification will invoke the drawing of all created rigid shapes, and update the User Interface (UI) to properly reflect the state of the application. For now, the drawing will be accomplished through a simple and continuous loop of calling the appropriate drawing functions, or the engine loop. In the next section of this blog, you will implement a more advanced engine loop to handle the physics engine’s calculations.

  1. Open js in the Engine Core folder for editing.
  2. Create a new runGameLoop function . In runGameLoop, call the requestAnimationFrame to specify the function for the next frame redraw. Additionally, invoke two other functions, the draw and updateUIEcho functions, to draw all the defined rigid shapes and to receive user keyboard entries.
var runGameLoop = function () {

    requestAnimationFrame(function () {

        runGameLoop();

    })

    updateUIEcho();
    draw();

};
  1. Define the updateUIEcho function to update the HTML to display the proper state of the application.
var updateUIEcho = function () {

    document.getElementById("uiEchoString").innerHTML = "<p><b>Selected Object:</b>:</p>" +

        "<ul style=\"margin:-10px\">" +
        "<li>Id: " + gObjectNum + "</li>" +
        "<li>Center: " + mAllObjects[gObjectNum].mCenter.x.toPrecision(3) + "," +
             mAllObjects[gObjectNum].mCenter.y.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>" +
        "</ul> <hr>" +
        "<b>F/G</b>: Spawn [Rectangle/Circle] at random location" + "<hr>";

};
  1. Add the draw function to iterate through and invoke the corresponding draw functions of the rigid shapes in the mAllObjects The strokeStyle property is set such that only the currently selected object is drawn in red while the rest are in blue.
var draw = function () {

    mContext.clearRect(0, 0, mWidth, mHeight);

    var i;

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

        mContext.strokeStyle = 'blue';

        if (i === gObjectNum)
            mContext.strokeStyle = 'red';

        mAllObjects[i].draw(mContext);

    }

};
  1. Define support to initialize the engine loop when the script runs for the first time.
var initializeEngineCore = function () {

    runGameLoop();

};
  1. Allow public access to the initializeEngineCore function by including it in the mPublic
var mPublic = {

    initializeEngineCore: initializeEngineCore,
    mAllObjects: mAllObjects,
    mWidth: mWidth,
    mHeight: mHeight,
    mContext: mContext

};

Define the Initial Scene

You can now define a bounded empty environment to test the new functionality.

  1. Create a new file under the SiteRoot (or public_html) folder, and name it js.
  2. Edit this file by creating a new function named MyGame. Inside this function, use the new rigid shape object you just implemented to create the four bounds that define border for future physics simulation.
    function MyGame() {

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

    }

Note that you can modify the initial scene by editing this function. This can become handy in the following article when you want to test the performance of the physics simulation.

Modify the index.html File

To include the new functionality, you need to always remember to include and call them inside the index.html file.

  1. Open the html file for editing.
  2. Modify the body tag to support the handling of keyboard events, define the initial testing environment by instantiating a new MyGame object, and initialize the engine loop by calling the initializeEngineCore.
<body onkeydown="return userControl(event);"



    onload="var game = new MyGame();



    gEngine.Core.initializeEngineCore()">
  1. Add a new table row for echoing the application state.
<table style="padding: 2px">

    <tr>
        <td>
            <div><canvas id="canvas"></canvas></div>
        </td>
        <td>
            <div id=”uiEchoString”> </div>
        </td>
    </tr>

</table>
  1. Remember to include all the new scripts with the <script>
<script type="text/javascript" src="/RigidBody/RigidShape.js"></script>
<script type="text/javascript" src="/RigidBody/Circle.js"></script>
<script type="text/javascript" src="/RigidBody/Rectangle.js"></script>
<script type="text/javascript" src="/EngineCore/Core.js"></script>
<script type="text/javascript" src="/MyGame.js"></script>
<script type="text/javascript" src="/UserControl.js"></script>

You can now run the project and test your implementations. It should look like Figure 1.

 

Observation

You can now run the project to test your implementation. Notice the four bounding borders and the text output to the right that prints instructions for the user and echoes the application state, which includes the index of the selected object. Pressing the F or G key generates a rectangle or circle at a random position with a random size. This drawing simulation seems rather similar to the previous project. The main differences are in the object abstraction and drawing mechanism — RigidShape class definition and engine loop monitoring user input and drawing of all defined objects. In the next project you will evolve the engine loop to support the changing of rigid shape states, including allowing the user to change the attributes of each of the rigid shapes in the scene and simple simulation of falling objects.

« Prev
Next »
Comments (0)
There are no comments posted here yet
Leave your comments
Posting as Guest
×
Suggested Locations