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
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
The code on this page compiles with Versions 3.0 (with warnings) and 2.2 of Smart Mobile Studio.
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
- 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
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.