Rendering Rectangles

The Box2D example supplied with Smart Mobile Studio (Projects\Featured Demos\Games\Box2D\Box2D Demo.sproj) uses debug draw routines to render the graphics. For the creation of this demonstration that uses Smart Pascal drawing routines directly, both Box2D Demo.sproj and Jeremy Hubble's JavaScript page were useful.

The second method below shows you a way of accessing and manipulating the vertices of a polygon.

The demo enables you to select the red rectangle by clicking on it and to fling it around using the mouse or touch. You could recompile the code with different values for the constants FRICTION and RESTITUTION to help you to decide the settings for bodies in your own Box2D games.

See also a simpler example with rendering by Pixi.js.

Demonstration

Box2DRenderDemo.html

If you do not see the demonstration, your school security system might have blocked it. Click here to try it on a separate page.

Smart Pascal Code

unit Unit1;

interface

uses
  System.Types, SmartCL.System, SmartCL.Components, SmartCL.Controls, SmartCL.Application,
  SmartCL.Game, SmartCL.GameApp, SmartCL.Graphics, SmartCL.MouseTouch, SmartCL.Touch, Box2DWrapper;

type
  TCanvasProject = class(TW3CustomGameApplication)
  private
    const BW = 0.5; //Border width
    const MOB_WIDTH = 3;
    const MOB_HEIGHT = 2;
    const FRAME_RATE = 1 / 60;
    const SCALE = 10;
    const RESTITUTION = 0.2;
    const FRICTION = 0.5;
    FWorld: Tb2World;
    FMousePos: Tb2Vec2;
    FIsMouseDown: Boolean;
    FMouseJoint: Tb2MouseJoint;
    FSelectedBody: Tb2Body;
    function GetBodyCB(Fixture: Tb2Fixture): Boolean;
    function GetBodyAtMouse: Tb2Body;
  protected
    procedure ApplicationStarting; override;
    procedure ApplicationClosing; override;
    procedure PaintView(Canvas: TW3Canvas); override;
    procedure MouseDownHandler(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
    procedure MouseMoveHandler(Sender: TObject; Shift: TShiftState; X, Y: Integer);
  end;

implementation

procedure TCanvasProject.ApplicationStarting;
var
  FixtureDef: Tb2FixtureDef;
  BodyDef: Tb2BodyDef;
  Body: Tb2Body;
begin
  inherited;
  FWorld := Tb2World.Create(
    Tb2Vec2.Create(0.0, 10.0),   // gravity
    True                         // allow sleep
  );
  // Create fixture definition (used to describe fixture objects)
  FixtureDef := Tb2FixtureDef.Create;
  FixtureDef.Density := 1.0;
  FixtureDef.Friction := FRICTION;
  FixtureDef.Restitution := RESTITUTION;
  // Create body definition class (used to describe body objects)
  BodyDef := Tb2BodyDef.Create;
  BodyDef.BodyType := btStaticBody;
  // Create the surrounding bars.
  FixtureDef.Shape := Tb2PolygonShape.Create;
  Tb2PolygonShape(FixtureDef.Shape).SetAsBox(0.5 * GameView.Width / SCALE, BW / 2);
  BodyDef.Position.SetXY(0.5 * GameView.Width / SCALE, BW / 2);
  FWorld.CreateBody(BodyDef).CreateFixture(FixtureDef); // Top horizontal bar
  BodyDef.Position.SetXY(0.5 * GameView.Width / SCALE, GameView.Height / SCALE - BW / 2);
  FWorld.CreateBody(BodyDef).CreateFixture(FixtureDef); // Bottom horizontal bar
  Tb2PolygonShape(FixtureDef.Shape).SetAsBox(BW / 2, 0.5  * GameView.Height / SCALE);
  BodyDef.Position.SetXY(BW / 2, 0.5 * GameView.Height / SCALE);
  FWorld.CreateBody(BodyDef).CreateFixture(FixtureDef);  // Left vertical bar
  BodyDef.Position.SetXY(GameView.Width / SCALE - BW / 2, 0.5 * GameView.Height / SCALE);
  FWorld.CreateBody(BodyDef).CreateFixture(FixtureDef);  // Right vertical bar
  // Create a rectangular dynamic object.
  BodyDef.BodyType := btDynamicBody;
  Tb2PolygonShape(FixtureDef.Shape).SetAsBox(MOB_WIDTH / 2, MOB_HEIGHT / 2);
  BodyDef.Position.SetXY(10, MOB_HEIGHT / 2 + BW);
  Body := FWorld.CreateBody(BodyDef);
  Body.CreateFixture(FixtureDef);

  FMousePos := Tb2Vec2.Create(0, 0);
  GameView.OnMouseTouchClick := MouseDownHandler;
  GameView.OnMouseTouchRelease := lambda FIsMouseDown := False; end;
  GameView.OnMouseMove := MouseMoveHandler;
  GameView.OnTouchMove := lambda(sender: TObject; td: TW3TouchData) FMousePos.SetXY(
    td.Touches.Touches[0].PageX  / SCALE, td.Touches.Touches[0].PageY / SCALE); end;
  GameView.Delay := 5;
  GameView.StartSession(False);
end;

procedure TCanvasProject.ApplicationClosing;
begin
  GameView.EndSession;
  inherited;
end;

function TCanvasProject.GetBodyCB(Fixture: Tb2Fixture): Boolean;
begin
  Result := True;
  if Fixture.GetBody.GetType <> btStaticBody then
    if Fixture.GetShape.TestPoint(Fixture.GetBody.GetTransform, FMousePos) then
      begin
        FSelectedBody := Fixture.GetBody;
        Result := False;
      end;
end;

function TCanvasProject.GetBodyAtMouse: Tb2Body;
var
  AABB: Tb2AxisAlignedBoundaryBox;
begin
  AABB := Tb2AxisAlignedBoundaryBox.Create;
  AABB.LowerBound.SetXY(FMousePos.X - 0.001, FMousePos.Y - 0.001);
  AABB.UpperBound.SetXY(FMousePos.X + 0.001, FMousePos.Y + 0.001);
  // Query the world for overlapping shapes.
  Result := nil;
  FWorld.QueryAxisAlignedBoundaryBox(GetBodyCB, AABB);
  Result := FSelectedBody;
end;

procedure TCanvasProject.PaintView(Canvas: TW3Canvas);
var
  Body: Tb2Body;
  MouseJointDef: Tb2MouseJointDef;
begin
  // Draw background and borders
  Canvas.FillStyle := 'rgb(0, 200, 0)';
  Canvas.FillRectF(0, 0, GameView.Width , GameView.Height);
  Canvas.FillStyle := 'rgb(0, 0, 0)';
  Canvas.FillRectF(0, 0, GameView.Width, BW * SCALE);
  Canvas.FillRectF(0, GameView.Height - BW * SCALE, GameView.Width, BW * SCALE);
  Canvas.FillRectF(0, 0, BW * SCALE, GameView.Height);
  Canvas.FillRectF(GameView.Width - BW * SCALE, 0, BW * SCALE, GameView.Height);

  if FIsMouseDown and not Assigned(FMouseJoint) then
    begin
      Body := GetBodyAtMouse;
      if Assigned(Body) then
        begin
          MouseJointDef := Tb2MouseJointDef.Create;
          MouseJointDef.BodyA := FWorld.GetGroundBody;
          MouseJointDef.BodyB := Body;
          MouseJointDef.Target.SetXY(FMousePos.X, FMousePos.Y);
          MouseJointDef.CollideConnected := True;
          MouseJointDef.MaxForce := 300.0 * Body.GetMass;
          FMouseJoint := Tb2MouseJoint(FWorld.CreateJoint(MouseJointDef));
          Body.SetAwake(True);
        end;
    end;
  if Assigned(FMouseJoint) then
    if FIsMouseDown then
      FMouseJoint.SetTarget(FMousePos)
    else
      begin
        FWorld.DestroyJoint(FMouseJoint);
        FMouseJoint := nil;
      end;
  // Advance and draw world
  FWorld.Advance(FRAME_RATE, 6, 2);

  Body := FWorld.GetBodyList;
  while Assigned(Body) do
    begin
      if Body.GetType = btDynamicBody then
        begin
          var Pos := Body.GetPosition;
          var Theta := Body.GetAngle;
          Canvas.Save;
          Canvas.Translate(Pos.X * SCALE, Pos.Y * SCALE);
          Canvas.Rotate(Theta);
          Canvas.FillStyle := 'rgb(200, 0, 0)';
          Canvas.FillRectF(-MOB_WIDTH * SCALE / 2, -MOB_HEIGHT * SCALE / 2, MOB_WIDTH * SCALE, MOB_HEIGHT * SCALE);
          Canvas.Restore;
        end;
    Body := Body.GetNext;
  end;
  FWorld.ClearForces;
end;

procedure TCanvasProject.MouseDownHandler(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  FMousePos.SetXY(X / SCALE, Y / SCALE);
  FIsMouseDown := True;
end;

procedure TCanvasProject.MouseMoveHandler(Sender: TObject; Shift: TShiftState; X, Y: Integer);
begin
  FMousePos.SetXY(X / SCALE, Y / SCALE);
end;

end.    

Smart Pascal Code for Manipulation of Vertices

While not recommended for the simple case of a rectangle, you might find the following alternative code instructive. In order to draw the rectangle (which might be rotating) using its vertices consider the following points.
  • We must multiply by the scale factor SCALE to convert from metres to pixels.
  • We must add the position of the centre of the body to the local positions of the vertices on the body.
  • Rather than using inbuilt matrices for transformations, we show the maths explicitly. These two statements rotate a point X, Y about the origin by an angle theta.
    X := X * cos(theta) - Y * sin(theta);
    Y := X * sin(theta) + Y * cos(theta);

Replace the while loop in the code above with the following.

  while Assigned(Body) do
    begin
      if Body.GetType = btDynamicBody then
        begin
          var Pos := Body.GetPosition;
          var FixtureList := Body.GetFixtureList;
          var Shape := FixtureList.GetShape;
          var Vertices: Tb2Vec2array;
          asm
            @vertices = (@shape).GetVertices();
          end;
          Canvas.FillStyle := 'rgb(200, 0, 0)';
          Canvas.BeginPath;
          // Rotate the rectangle and draw it.
          var Theta := Body.GetAngle;
          var S := sin(Theta);
          var C := cos(Theta);
          Canvas.MoveToF((Vertices[0].X * C - Vertices[0].Y * S + Pos.X) * SCALE,
                         (Vertices[0].X * S + Vertices[0].Y * C + Pos.Y) * SCALE);
          for var i := 1 to 3 do
            Canvas.LineToF((Vertices[i].X * C - vertices[i].Y * S + Pos.X) * SCALE,
                           (Vertices[i].X  * S + vertices[i].Y * C + Pos.Y) * SCALE);
          Canvas.ClosePath;
          Canvas.Fill;
        end;
    Body := Body.GetNext;
  end;   

Toppling Dominoes

Set MOB_WIDTH to 1 and MOB_HEIGHT to 4 and replace the three lines
  BodyDef.Position.SetXY(10, MOB_HEIGHT / 2 + BW);
  Body := FWorld.CreateBody(BodyDef);
  Body.CreateFixture(FixtureDef);
with these six:
  for var i := 1 to 10 do
    begin
      BodyDef.Position.SetXY(i * 2.5, GameView.Height / SCALE - 0.5 * MOB_HEIGHT);
      Body := FWorld.CreateBody(BodyDef);
      Body.CreateFixture(FixtureDef);
    end;      

Tap the top of the first one to the right and watch them knock each other over.

Applying an Impulse

In this example we apply an impulse to the centre of the first domino after a short interval. We have removed the code for mouse and touch control so need only the bottom bar to stop the dominoes from sinking due to gravity.

unit Unit1;

interface

uses
  System.Types, SmartCL.System, SmartCL.Components, SmartCL.Controls, SmartCL.Application,
  SmartCL.Game, SmartCL.GameApp, SmartCL.Graphics, Box2DWrapper;

type
  TCanvasProject = class(TW3CustomGameApplication)
  private
    const BW = 0.5; // Border width
    const MOB_WIDTH = 1;
    const MOB_HEIGHT = 4;
    const FRAME_RATE = 1 / 60;
    const SCALE = 10;
    const RESTITUTION = 0.2;
    const FRICTION = 0.5;
    FWorld: Tb2World;
    FFirstDomino: Tb2Body;
    FFrameCount: integer;
  protected
    procedure ApplicationStarting; override;
    procedure ApplicationClosing; override;
    procedure PaintView(Canvas: TW3Canvas); override;
   end;

implementation

procedure TCanvasProject.ApplicationStarting;
var
  FixtureDef: Tb2FixtureDef;
  BodyDef: Tb2BodyDef;
  Body: Tb2Body;
begin
  inherited;
  FWorld := Tb2World.Create(
    Tb2Vec2.Create(0.0, 10.0),   // gravity
    True                         // allow sleep
  );
  // Create fixture definition (used to describe fixture objects)
  FixtureDef := Tb2FixtureDef.Create;
  FixtureDef.Density := 1.0;
  FixtureDef.Friction := FRICTION;
  FixtureDef.Restitution := RESTITUTION;
  // Create body definition class (used to describe body objects)
  BodyDef := Tb2BodyDef.Create;
  BodyDef.BodyType := btStaticBody;
  // Create the bottom bar.
  FixtureDef.Shape := Tb2PolygonShape.Create;
  Tb2PolygonShape(FixtureDef.Shape).SetAsBox(0.5 * GameView.Width / SCALE, BW);
  BodyDef.Position.SetXY(0.5 * GameView.Width / SCALE, GameView.Height / SCALE);
  FWorld.CreateBody(BodyDef).CreateFixture(FixtureDef); // Bottom horizontal bar
  BodyDef.BodyType := btDynamicBody;
  Tb2PolygonShape(FixtureDef.Shape).SetAsBox(MOB_WIDTH / 2, MOB_HEIGHT / 2);

  BodyDef.Position.SetXY(2.5, GameView.Height / SCALE - 0.5 * MOB_HEIGHT);
  FFirstDomino := FWorld.CreateBody(BodyDef);
  FFirstDomino.CreateFixture(FixtureDef);
  for var i := 2 to 10 do
    begin
      BodyDef.Position.SetXY(i * 2.5, GameView.Height / SCALE - 0.5 * MOB_HEIGHT);
      Body := FWorld.CreateBody(BodyDef);
      Body.CreateFixture(FixtureDef);
    end;
  GameView.Delay := 5;
  GameView.StartSession(False);
end;

procedure TCanvasProject.ApplicationClosing;
begin
  GameView.EndSession;
  inherited;
end;

procedure TCanvasProject.PaintView(Canvas: TW3Canvas);
var
  Body: Tb2Body;
begin
  inc(FFrameCount);
  if FFrameCount = 100 then
    FFirstDomino.ApplyImpulse(TB2Vec2.Create(20, 0), FFirstDomino.GetWorldCenter);
  // Draw background and borders
  Canvas.FillStyle := 'rgb(0, 200, 0)';
  Canvas.FillRectF(0, 0, GameView.Width , GameView.Height);
  Canvas.FillStyle := 'rgb(0, 0, 0)';
  Canvas.FillRectF(0, GameView.Height - BW * SCALE, GameView.Width, BW * SCALE);
  // Advance and draw world
  FWorld.Advance(FRAME_RATE, 6, 2);

  Body := FWorld.GetBodyList;
  while Assigned(Body) do
    begin
      if Body.GetType = btDynamicBody then
        begin
          var Pos := Body.GetPosition;
          var Theta := Body.GetAngle;
          Canvas.Save;
          Canvas.Translate(Pos.X * SCALE, Pos.Y * SCALE);
          Canvas.Rotate(Theta);
          Canvas.FillStyle := 'rgb(200, 0, 0)';
          Canvas.FillRectF(-MOB_WIDTH * SCALE / 2, -MOB_HEIGHT * SCALE / 2, MOB_WIDTH * SCALE, MOB_HEIGHT * SCALE);
          Canvas.Restore;
        end;
    Body := Body.GetNext;
  end;
  FWorld.ClearForces;
end;

end.    
Programming - a skill for life!

Using the Box2D graphics engine for advanced games in Smart Mobile Studio