Using Wheel Joints

We chose an easy way to get started by saving the code of UfrmMain in the supplied PingPong example then replacing it with the code of our demonstration shown below. We removed from the form all components except the TImage. (Select the compiler menu item Project > Project Options > Parsing to confirm that the syntax mode is Delphi).

See our online Smart Pascal demo on which this was based. The Smart Pascal version uses revolute joints instead of wheel joints so the wheels have no springs. Set the damping in the Lazarus example to a high value (about 10) to leave the chassis stuck on the second ramp.

You might prefer the PasSFML version below. (Whereas GLCanvas matches Box2D in having Y values increasing up the screen, the Y axis in PasSFML graphics is reversed).

Pascal Code

unit UfrmMain;

interface
{$I ..\..\Physics2D\Physics2D.inc}

uses
  Classes, Graphics, Controls, Forms,  Dialogs, ExtCtrls,
  UOpenGLCanvas in '..\..\OpenGL Canvas\UOpenGLCanvas.pas',
  UPhysics2D in '..\..\Physics2D\UPhysics2D.pas',
  UPhysics2DTypes in '..\..\Physics2D\UPhysics2DTypes.pas',
  UPhysics2DHelper in '..\..\Physics2D\UPhysics2DHelper.pas';

type
  TfrmMain = class(TForm)
    imgDisplay: TImage;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    private
    const
      BW = 0.5; // bar width (for the ground)
      CHASSIS_WIDTH = 5;
      CHASSIS_HEIGHT = 1;
      WHEEL_RADIUS = 1;
      AXLE_DX = 0.2;  // Distance of axle from nearest end of chassis
      AXLE_DY = 0.4;  // Distance of axle from bottom of chassis
      SPRING_FREQUENCY = 4;
      SPRING_DAMPING = 0.7;
      FRAME_RATE = 1 / 60;
      SCALE = 10;
      RESTITUTION = 0.2;
      FRICTION = 0.9;
      RADTODEG = 180 / 3.142;
    var
      GLCanvas: TGLCanvas;
      FWorld: Tb2World;
      FChassis, FRearWheel, FFrontWheel: Tb2Body;
      FRamps: array[1..2] of Tb2Body;
      FAxleJointDef: Tb2WheelJointDef;
    procedure InitializePhysics;
    procedure Display;
    procedure TimerProgress(const deltaTime, newTime: Double);
  end;

var
  frmMain: TfrmMain;

implementation

uses
  MSTimer;

{$R *.dfm}

procedure TfrmMain.FormCreate(Sender: TObject);
begin
  MSCadencer := TMSTimer.Create;
  MSCadencer.OnProgress := TimerProgress;
  GLCanvas := TGLCanvas.Create(imgDisplay, True, True, False, True);
  GLCanvas.DefaultFont.WinColor := clBlack;
  InitializePhysics;
end;

procedure TfrmMain.FormDestroy(Sender: TObject);
begin
  if Assigned(FWorld) then
    FWorld.Free;
  MSCadencer.Free;
  GLCanvas.Free;
end;

procedure TfrmMain.InitializePhysics;
var
  BodyDef: Tb2BodyDef;
  FixtureDef: Tb2FixtureDef;
  Vertices: array[0..2] of TVector2;
  TriangleShape: Tb2PolygonShape;
begin
  FWorld := Tb2World.Create(MakeVector(0, -10));
  FWorld.AllowSleeping := True;
  // 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;
  // Create the ground
  FixtureDef.Shape := Tb2PolygonShape.Create;
  Tb2PolygonShape(FixtureDef.Shape).SetAsBox(0.5 * imgDisplay.Width / SCALE, BW / 2);
  BodyDef.Position := MakeVector(0.5 * imgDisplay.Width / SCALE, BW / 2);
  FWorld.CreateBody(BodyDef, False).CreateFixture(FixtureDef, False, False, False);
  // Create two ramps.
  Vertices[0] := MakeVector(-5, -1.0);
  Vertices[1] := MakeVector(5, 1.0);
  Vertices[2] := MakeVector(5, -1.0);
  TriangleShape := Tb2PolygonShape.Create;
  TriangleShape.SetVertices(@Vertices[0], 3); // passing array of TVector2 to the shape
  FixtureDef.Shape := TriangleShape;
  BodyDef := Tb2BodyDef.Create;
  BodyDef.Position := MakeVector(0.3 * imgDisplay.Width / SCALE,  0.5 );
  FRamps[1] := FWorld.CreateBody(BodyDef, False);
  Framps[1].CreateFixture(FixtureDef, False, False, False);
  BodyDef := Tb2BodyDef.Create;
  BodyDef.Position := MakeVector(0.3 * imgDisplay.Width / SCALE + 7, 0.5 );
  FRamps[2] := FWorld.CreateBody(BodyDef, False);
  Framps[2].CreateFixture(FixtureDef, False, False, False);
  // Create chassis
  Tb2PolygonShape(FixtureDef.Shape).SetAsBox(CHASSIS_WIDTH / 2, CHASSIS_HEIGHT / 2);
  BodyDef := Tb2BodyDef.Create;
  BodyDef.BodyType := b2_DynamicBody;
  BodyDef.Position := MakeVector(3, BW + WHEEL_RADIUS + CHASSIS_HEIGHT / 2 - AXLE_DY);

  FChassis := FWorld.CreateBody(BodyDef, False);
  FChassis.CreateFixture(FixtureDef, False, False);
  // Create rear wheel
  FixtureDef.Shape := Tb2CircleShape.Create;
  FixtureDef.Shape.m_radius := WHEEL_RADIUS;
  BodyDef.Position := MakeVector(FChassis.GetPosition.x - CHASSIS_WIDTH / 2 + AXLE_DX,
                                 FChassis.GetPosition.y - CHASSIS_HEIGHT / 2 + AXLE_DY);
  FRearWheel := FWorld.CreateBody(BodyDef, False);
  FRearWheel.CreateFixture(FixtureDef, False, False);
  // Create front wheel
  BodyDef.Position := MakeVector(FChassis.GetPosition.x + CHASSIS_WIDTH / 2 - AXLE_DX,
                                  FChassis.GetPosition.y - CHASSIS_HEIGHT / 2 + AXLE_DY);
  FFrontWheel := FWorld.CreateBody(BodyDef, False);
  FFrontWheel.CreateFixture(FixtureDef, False);
  // Create rear wheel joint
  FAxleJointDef := Tb2WheelJointDef.Create;
  FAxleJointDef.Initialize(FChassis, FRearWheel, FRearWheel.GetPosition, MakeVector(0, 1));
  FAxleJointDef.enableMotor := True;
  FAxleJointDef.motorSpeed := -3.5;
  FAxleJointDef.maxMotorTorque := 1000;
  FAxleJointDef.frequencyHz := SPRING_FREQUENCY;
  FAxleJointDef.dampingRatio := SPRING_DAMPING;

  FWorld.CreateJoint(FAxleJointDef, False);
  // Create front wheel joint
  FAxleJointDef.Initialize(FChassis, FFrontWheel, FFrontWheel.GetPosition, MakeVector(0, 1));
  FAxleJointDef.enableMotor := False;
  FWorld.CreateJoint(FAxleJointDef, False);

  Display;
  MSCadencer.Enabled := True;
end;

procedure TfrmMain.Display;
var
  Pos: TVector2;
  Theta: PhysicsFloat;
  i: integer;
  FixtureList: Tb2Fixture;
  Shape: Tb2Shape;
  Vert: Tb2PolyVertices;
begin
  GLCanvas.RenderingBegin(clGreen);
  GLCanvas.SetBrushColorWin(clBlack, 255, False);
  GLCanvas.FillRect(0, 0, imgDisplay.Width, BW * SCALE); // the ground
  // Draw both ramps
  for i := 1 to 2 do
    begin
      Pos := FRamps[i].GetPosition;
      FixtureList := FRamps[i].GetFixtureList;
      Shape := FixtureList.GetShape;
      Vert := Tb2PolygonShape(Shape).m_vertices;
      // Draw filled triangle.
      GLCanvas.FillTriangle((Vert[0].X + Pos.X) * SCALE, (Vert[0].Y + Pos.Y) * SCALE,
                            (Vert[1].X + Pos.X) * SCALE, (Vert[1].Y + Pos.Y) * SCALE,
                            (Vert[2].X + Pos.X) * SCALE, (Vert[2].Y + Pos.Y) * SCALE);
    end;
  Pos := FChassis.GetPosition;
  Theta := FChassis.GetAngle;
  GLCanvas.SetTranslateX(GLCanvas.TranslateX + Pos.X * SCALE);
  GLCanvas.SetTranslateY(GLCanvas.TranslateY + Pos.Y * SCALE);
  GLCanvas.SetRotation(GLCanvas.Rotation + Theta * RADTODEG);
  GLCanvas.SetBrushColorWin(clRed, 255, False);
  GLCanvas.FillRect(-CHASSIS_WIDTH * SCALE / 2, -CHASSIS_HEIGHT * SCALE / 2,
                    CHASSIS_WIDTH * SCALE / 2, CHASSIS_HEIGHT * SCALE / 2);
  GLCanvas.SetRotation(GLCanvas.Rotation - Theta * RADTODEG);
  GLCanvas.SetTranslateY(GLCanvas.TranslateY -Pos.Y * SCALE);
  GLCanvas.SetTranslateX(GLCanvas.TranslateX -Pos.X * SCALE);

  Pos := FRearWheel.GetPosition;
  GLCanvas.SetBrushColorWin(clBlack, 255, False);
  GLCanvas.FillEllipseRect((Pos.X - WHEEL_RADIUS) * SCALE, (Pos.Y + WHEEL_RADIUS) * SCALE,
                           (Pos.X + WHEEL_RADIUS) * SCALE, (Pos.Y - WHEEL_RADIUS) * SCALE);
  Pos := FFrontWheel.GetPosition;
  GLCanvas.FillEllipseRect((Pos.X - WHEEL_RADIUS) * SCALE, (Pos.Y + WHEEL_RADIUS) * SCALE,
                           (Pos.X + WHEEL_RADIUS) * SCALE, (Pos.Y - WHEEL_RADIUS) * SCALE);
  GLCanvas.RenderingEnd;
end;

procedure TfrmMain.TimerProgress(const deltaTime, newTime: Double);
const
  FixedStep = FRAME_RATE;
var
  dt: Double;
begin
  if deltaTime < 1.2 * FixedStep then
    FWorld.Step(deltaTime, 6, 2)
  else
    begin
      dt := deltaTime;
      while dt > 0 do
        begin
          FWorld.Step(FixedStep, 6, 2);
          dt := dt - FixedStep;
        end;
      end;
  Display;
  Application.ProcessMessages;
end;

end.    

Code of Form

object frmMain: TfrmMain
  Left = 430
  Height = 117
  Top = 77
  Width = 410
  BorderIcons = [biSystemMenu, biMinimize]
  BorderStyle = bsSingle
  Caption = 'Rendering Rectangles'
  ClientHeight = 117
  ClientWidth = 410
  Color = clBtnFace
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'Tahoma'
  KeyPreview = True
  OnCreate = FormCreate
  OnDestroy = FormDestroy
  Position = poDesktopCenter
  LCLVersion = '1.4.4.0'
  object imgDisplay: TImage
    Left = 6
    Height = 100
    Top = 6
    Width = 400
  end
end   

PasSFML Version

See our page entitled Downloading PasSFML and Learning from Examples for the files that you will need in the folder of this console program. The PasSFML version of our marionette is a more complicated example with mouse input.

program Box2DPasSFMLTest;
{$Mode Delphi}
{$Apptype GUI}
{$I ..\..\Physics2D\Physics2D.inc}
uses
  SysUtils, Variants, Classes, SfmlGraphics, SfmlSystem, SfmlWindow,
  UPhysics2D in '..\..\Physics2D\UPhysics2D.pas',
  UPhysics2DTypes in '..\..\Physics2D\UPhysics2DTypes.pas',
  UPhysics2DHelper in '..\..\Physics2D\UPhysics2DHelper.pas';
const
  WINDOW_WIDTH = 400;
  WINDOW_HEIGHT = 100;
  BW = 0.5; // bar width (for the ground)
  CHASSIS_WIDTH = 5;
  CHASSIS_HEIGHT = 1;
  WHEEL_RADIUS = 1;
  AXLE_DX = 0.2;  // Distance of axle from nearest end of chassis
  AXLE_DY = 0.4;  // Distance of axle from bottom of chassis
  SPRING_FREQUENCY = 4;
  SPRING_DAMPING = 0.7;
  FRAME_RATE = 1 / 60;
  SCALE = 10;
  RESTITUTION = 0.2;
  FRICTION = 0.9;
var
  World: Tb2World;
  Chassis, RearWheel, FrontWheel: Tb2Body;
  Ramps: array[1..2] of Tb2Body;
  AxleJointDef: Tb2WheelJointDef;
  Window: TSfmlRenderWindow;
  ChassisRect, GroundRect: TSfmlRectangleShape;
  Wheel: TSfmlCircleShape;
  Ramp: TSfmlConvexShape;
  Event: TsfmlEvent;
  Settings: TsfmlContextSettings;

procedure InitializePhysics;
var
  BodyDef: Tb2BodyDef;
  FixtureDef: Tb2FixtureDef;
  Vertices: array[0..2] of TVector2;
  TriangleShape: Tb2PolygonShape;
begin
  World := Tb2World.Create(MakeVector(0, -10));
  World.AllowSleeping := True;
  // 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;
  // Create the ground
  FixtureDef.Shape := Tb2PolygonShape.Create;
  Tb2PolygonShape(FixtureDef.Shape).SetAsBox(0.5 * WINDOW_WIDTH / SCALE, BW / 2);
  BodyDef.Position := MakeVector(0.5 * WINDOW_WIDTH / SCALE, BW / 2);
  World.CreateBody(BodyDef, False).CreateFixture(FixtureDef, False, False, False);
  // Create two ramps.
  Vertices[0] := MakeVector(-5, -1.0);
  Vertices[1] := MakeVector(5, 1.0);
  Vertices[2] := MakeVector(5, -1.0);
  TriangleShape := Tb2PolygonShape.Create;
  TriangleShape.SetVertices(@Vertices[0], 3); // passing array of TVector2 to the shape
  FixtureDef.Shape := TriangleShape;
  BodyDef := Tb2BodyDef.Create;
  BodyDef.Position := MakeVector(0.3 * WINDOW_WIDTH / SCALE,  0.5 );
  Ramps[1] := World.CreateBody(BodyDef, False);
  Ramps[1].CreateFixture(FixtureDef, False, False, False);
  BodyDef := Tb2BodyDef.Create;
  BodyDef.Position := MakeVector(0.3 * WINDOW_WIDTH / SCALE + 7, 0.5 );
  Ramps[2] := World.CreateBody(BodyDef, False);
  Ramps[2].CreateFixture(FixtureDef, False, False, False);
  // Create chassis
  Tb2PolygonShape(FixtureDef.Shape).SetAsBox(CHASSIS_WIDTH / 2, CHASSIS_HEIGHT / 2);
  BodyDef := Tb2BodyDef.Create;
  BodyDef.BodyType := b2_DynamicBody;
  BodyDef.Position := MakeVector(3, BW + WHEEL_RADIUS + CHASSIS_HEIGHT / 2 - AXLE_DY);

  Chassis := World.CreateBody(BodyDef, False);
  Chassis.CreateFixture(FixtureDef, False, False);
  // Create rear wheel
  FixtureDef.Shape := Tb2CircleShape.Create;
  FixtureDef.Shape.m_radius := WHEEL_RADIUS;
  BodyDef.Position := MakeVector(Chassis.GetPosition.x - CHASSIS_WIDTH / 2 + AXLE_DX,
                                 Chassis.GetPosition.y - CHASSIS_HEIGHT / 2 + AXLE_DY);
  RearWheel := World.CreateBody(BodyDef, False);
  RearWheel.CreateFixture(FixtureDef, False, False);
  // Create front wheel
  BodyDef.Position := MakeVector(Chassis.GetPosition.x + CHASSIS_WIDTH / 2 - AXLE_DX,
                                 Chassis.GetPosition.y - CHASSIS_HEIGHT / 2 + AXLE_DY);
  FrontWheel := World.CreateBody(BodyDef, False);
  FrontWheel.CreateFixture(FixtureDef, False);
  // Create rear wheel joint
  AxleJointDef := Tb2WheelJointDef.Create;
  AxleJointDef.Initialize(Chassis, RearWheel, RearWheel.GetPosition, MakeVector(0, 1));
  AxleJointDef.enableMotor := True;
  AxleJointDef.motorSpeed := -3.5;
  AxleJointDef.maxMotorTorque := 1000;
  AxleJointDef.frequencyHz := SPRING_FREQUENCY;
  AxleJointDef.dampingRatio := SPRING_DAMPING;
  World.CreateJoint(AxleJointDef, False);
  // Create front wheel joint
  AxleJointDef.Initialize(Chassis, FrontWheel, FrontWheel.GetPosition, MakeVector(0, 1));
  AxleJointDef.enableMotor := False;
  World.CreateJoint(AxleJointDef);
end;

procedure InitializeDisplay;
begin
  Settings.antialiasingLevel := 8;
  Window := TSfmlRenderWindow.Create(SfmlVideoMode(WINDOW_WIDTH, WINDOW_HEIGHT, 32),
                                     AnsiString('Wheel Joint Demo'),  [sfTitleBar, sfClose], @Settings );
  Window.SetVerticalSyncEnabled(True);
  GroundRect := TSfmlRectangleShape.Create;
  GroundRect.FillColor := SfmlBlack;
  GroundRect.Size := SfmlVector2f(WINDOW_WIDTH, BW * SCALE);
  GroundRect.Origin := SfmlVector2f(0, 0);
  GroundRect.Position := SfmlVector2f(0, WINDOW_HEIGHT - BW * SCALE );
  Ramp := TSfmlConvexShape.Create;
  Ramp.FillColor := SfmlBlack;
  Ramp.PointCount := 3;
  Ramp.Origin := sfmlVector2f(0, 0);
  ChassisRect := TSfmlRectangleShape.Create;
  ChassisRect.FillColor := SfmlRed;
  ChassisRect.Size := SfmlVector2f(CHASSIS_WIDTH * SCALE, CHASSIS_HEIGHT * SCALE);
  ChassisRect.Origin := SfmlVector2f(CHASSIS_WIDTH * SCALE / 2, CHASSIS_HEIGHT * SCALE / 2);
  Wheel := TSfmlCircleShape.Create;
  Wheel.FillColor := SfmlBlack;
  Wheel.Radius:= WHEEL_RADIUS * SCALE;
end;

procedure Display;
var
  Pos: TVector2;
  Theta: PhysicsFloat;
  i, j: integer;
  FixtureList: Tb2Fixture;
  Shape: Tb2Shape;
  Vert: Tb2PolyVertices;
begin
  Window.Clear(SfmlGreen);
  Window.Draw(GroundRect);
  // Draw both ramps
  for i := 1 to 2 do
    begin
      Pos := Ramps[i].GetPosition;
      FixtureList := Ramps[i].GetFixtureList;
      Shape := FixtureList.GetShape;
      Vert := Tb2PolygonShape(Shape).m_vertices;
      // Draw filled triangle.
      for j := 0 to 2 do
        Ramp.Point[j] := sfmlVector2f(Vert[j].X * SCALE, -Vert[j].Y * SCALE);
      Ramp.Position := sfmlVector2f(Pos.X * SCALE, WINDOW_HEIGHT - Pos.Y * SCALE);
      Window.Draw(Ramp);
    end;
  // Draw chassis
  Pos := Chassis.GetPosition;
  Theta := Chassis.GetAngle;
  ChassisRect.Position := SfmlVector2f(Pos.x * SCALE, WINDOW_HEIGHT - Pos.y * SCALE);
  ChassisRect.Rotate(-Theta);
  Window.Draw(ChassisRect);
  ChassisRect.Rotate(Theta);
  // Draw wheels
  Pos := RearWheel.GetPosition;
  Wheel.Position := SfmlVector2f((Pos.X - WHEEL_RADIUS) * SCALE, WINDOW_HEIGHT - (Pos.y + WHEEL_RADIUS) * SCALE);
  Window.Draw(Wheel);
  Pos := FrontWheel.GetPosition;
  Wheel.Position := SfmlVector2f((Pos.X - WHEEL_RADIUS) * SCALE, WINDOW_HEIGHT - (Pos.y + WHEEL_RADIUS) * SCALE);
  Window.Draw(Wheel);

  Window.Display;
end;

begin
  InitializePhysics;
  InitializeDisplay;
  while Window.isOpen do
    begin
      while SfmlRenderWindowPollEvent(Window.Handle, Event) do
        begin
          if Event.EventType = sfEvtClosed then  // Window closed
            SfmlRenderWindowClose(Window.Handle);
          // Escape key pressed
          if (Event.EventType = sfEvtKeyPressed) and (Event.Key.Code = sfKeyEscape) then
            SfmlRenderWindowClose(Window.Handle);
        end;
      World.Step(FRAME_RATE, 6, 2);
      Display;
    end;
end.
    
Programming - a skill for life!

How to use the Box2D physics engine in Lazarus