Talking about Actionscript 3, Box2D and Flash.
If you like Flash games, then you probably know Gibbets, a physics game where you must save hanging people by cutting their ropes with an arrow.
An interesting feature of the game is the realistic trajectory followed by the arrow. You may say: “well, Box2D is a physics engine, I managed to fire a thousand bullets with it, I can shoot an arrow”.
And you’re right… let’s see this simple script which just creates and fires something like an arrow from the bottom left of the stage once you click on the stage, according to mouse position:
package {
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
import Box2D.Dynamics.*;
import Box2D.Collision.*;
import Box2D.Collision.Shapes.*;
import Box2D.Common.Math.*;
public class Main extends Sprite {
private var world:b2World=new b2World(new b2Vec2(0,10),true);
private var worldScale:int=30;
public function Main():void {
debugDraw();
wall(320,470,640,20);
wall(630,240,20,480);
addEventListener(Event.ENTER_FRAME, update);
stage.addEventListener(MouseEvent.CLICK,addArrow);
}
private function addArrow(e:MouseEvent):void {
var angle:Number=Math.atan2(mouseY-450,mouseX);
var vertices:Vector.=new Vector.();
vertices.push(new b2Vec2(-1.4,0));
vertices.push(new b2Vec2(0,-0.1));
vertices.push(new b2Vec2(0.6,0));
vertices.push(new b2Vec2(0,0.1));
var bodyDef:b2BodyDef= new b2BodyDef();
bodyDef.position.Set(0,450/worldScale);
bodyDef.type=b2Body.b2_dynamicBody;
var polygonShape:b2PolygonShape = new b2PolygonShape();
polygonShape.SetAsVector(vertices,4);
var fixtureDef:b2FixtureDef = new b2FixtureDef();
fixtureDef.shape=polygonShape;
fixtureDef.density=1;
fixtureDef.friction=0.5;
fixtureDef.restitution=0.5;
var body:b2Body=world.CreateBody(bodyDef);
body.CreateFixture(fixtureDef);
body.SetLinearVelocity(new b2Vec2(20*Math.cos(angle),20*Math.sin(angle)));
body.SetAngle(angle);
}
private function debugDraw():void {
var debugDraw:b2DebugDraw=new b2DebugDraw();
var debugSprite:Sprite=new Sprite();
addChild(debugSprite);
debugDraw.SetSprite(debugSprite);
debugDraw.SetDrawScale(worldScale);
debugDraw.SetFlags(b2DebugDraw.e_shapeBit|b2DebugDraw.e_jointBit);
debugDraw.SetFillAlpha(0.5);
world.SetDebugDraw(debugDraw);
}
private function wall(pX:Number,pY:Number,w:Number,h:Number):void {
var bodyDef:b2BodyDef=new b2BodyDef();
bodyDef.position.Set(pX/worldScale,pY/worldScale);
var polygonShape:b2PolygonShape=new b2PolygonShape();
polygonShape.SetAsBox(w/2/worldScale,h/2/worldScale);
var fixtureDef:b2FixtureDef=new b2FixtureDef();
fixtureDef.shape=polygonShape;
fixtureDef.density=1;
fixtureDef.restitution=0.4;
fixtureDef.friction=0.5;
var theWall:b2Body=world.CreateBody(bodyDef);
theWall.CreateFixture(fixtureDef);
}
private function update(e : Event):void {
world.Step(1/30,5,5);
world.ClearForces();
world.DrawDebugData();
}
}
}
As you can see, there’s nothing new in it, so we can test it:
Click on the stage to fire the arrow. As you can see, the trajectory is accurate, but the arrow does not rotate to follow it as it would in real life… it’s just a flying bar which does not rotate as we expect.
That’s because arrow typical trajectory is due to air resistance, which is not included in Box2D simulation, and above all air resistance on its tail.
So I followed the tutorial and the theory published on iforce2d to create realistic arrows, and once converted its code from C to AS3, I got:
package {
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
import Box2D.Dynamics.*;
import Box2D.Collision.*;
import Box2D.Collision.Shapes.*;
import Box2D.Common.Math.*;
import Box2D.Dynamics.Contacts.*;
public class Main extends Sprite {
private var world:b2World=new b2World(new b2Vec2(0,10),true);
private var worldScale:int=30;
private var dragConstant:Number=0.05;
private var dampingConstant:Number=1.5;
private var arrowVector:Vector.=new Vector.();
public function Main():void {
debugDraw();
wall(320,470,640,20);
wall(630,240,20,480);
addEventListener(Event.ENTER_FRAME, update);
stage.addEventListener(MouseEvent.CLICK,addArrow);
}
private function addArrow(e:MouseEvent):void {
var angle:Number=Math.atan2(mouseY-450,mouseX);
var vertices:Vector.=new Vector.();
vertices.push(new b2Vec2(-1.4,0));
vertices.push(new b2Vec2(0,-0.1));
vertices.push(new b2Vec2(0.6,0));
vertices.push(new b2Vec2(0,0.1));
var bodyDef:b2BodyDef= new b2BodyDef();
bodyDef.position.Set(0,450/worldScale);
bodyDef.type=b2Body.b2_dynamicBody;
bodyDef.userData="arrow";
var polygonShape:b2PolygonShape = new b2PolygonShape();
polygonShape.SetAsVector(vertices,4);
var fixtureDef:b2FixtureDef = new b2FixtureDef();
fixtureDef.shape=polygonShape;
fixtureDef.density=1;
fixtureDef.friction=0.5;
fixtureDef.restitution=0.5;
var body:b2Body=world.CreateBody(bodyDef);
body.CreateFixture(fixtureDef);
body.SetLinearVelocity(new b2Vec2(25*Math.cos(angle),25*Math.sin(angle)));
body.SetAngle(angle);
body.SetAngularDamping(dampingConstant);
arrowVector.push(body);
}
private function debugDraw():void {
var debugDraw:b2DebugDraw=new b2DebugDraw();
var debugSprite:Sprite=new Sprite();
addChild(debugSprite);
debugDraw.SetSprite(debugSprite);
debugDraw.SetDrawScale(worldScale);
debugDraw.SetFlags(b2DebugDraw.e_shapeBit|b2DebugDraw.e_jointBit);
debugDraw.SetFillAlpha(0.5);
world.SetDebugDraw(debugDraw);
}
private function wall(pX:Number,pY:Number,w:Number,h:Number):void {
var bodyDef:b2BodyDef=new b2BodyDef();
bodyDef.position.Set(pX/worldScale,pY/worldScale);
bodyDef.userData="wall";
var polygonShape:b2PolygonShape=new b2PolygonShape();
polygonShape.SetAsBox(w/2/worldScale,h/2/worldScale);
var fixtureDef:b2FixtureDef=new b2FixtureDef();
fixtureDef.shape=polygonShape;
fixtureDef.density=1;
fixtureDef.restitution=0.4;
fixtureDef.friction=0.5;
var theWall:b2Body=world.CreateBody(bodyDef);
theWall.CreateFixture(fixtureDef);
}
private function update(e : Event):void {
world.Step(1/30,5,5);
world.ClearForces();
for (var i:Number=arrowVector.length-1; i>=0; i--) {
var body:b2Body=arrowVector[i];
var flightSpeed:Number=Normalize2(body.GetLinearVelocity());
var bodyAngle:Number=body.GetAngle();
var pointingDirection:b2Vec2=new b2Vec2(Math.cos(bodyAngle),- Math.sin(bodyAngle));
var flyingAngle:Number=Math.atan2(body.GetLinearVelocity().y,body.GetLinearVelocity().x);
var flightDirection:b2Vec2=new b2Vec2(Math.cos(flyingAngle),Math.sin(flyingAngle));
var dot:Number=b2Dot(flightDirection,pointingDirection);
var dragForceMagnitude:Number=(1-Math.abs(dot))*flightSpeed*flightSpeed*dragConstant*body.GetMass();
var arrowTailPosition:b2Vec2=body.GetWorldPoint(new b2Vec2(-1.4,0));
body.ApplyForce(new b2Vec2((dragForceMagnitude*-flightDirection.x),(dragForceMagnitude*-flightDirection.y)),arrowTailPosition);
if (body.GetPosition().x*worldScale>500) {
for (var c:b2ContactEdge=body.GetContactList(); c; c=c.next) {
var contact:b2Contact=c.contact;
var fixtureA:b2Fixture=contact.GetFixtureA();
var fixtureB:b2Fixture=contact.GetFixtureB();
var bodyA:b2Body=fixtureA.GetBody();
var bodyB:b2Body=fixtureB.GetBody();
if (bodyA.GetUserData()=="wall"||bodyB.GetUserData()=="wall") {
arrowVector.splice(i,1);
}
}
}
}
world.DrawDebugData();
}
private function b2Dot(a:b2Vec2, b:b2Vec2):Number {
return a.x * b.x + a.y * b.y;
}
private function Normalize2(b:b2Vec2):Number {
return Math.sqrt(b.x * b.x + b.y * b.y);
}
}
}
You can follow the tutorial on the original page, I just adapted it to AS3 and stopped the “wind on tail” simulation once the arrow hits a wall. Here is the result:
Again, click on the stage to shoot an arrow.
Collision detection is not that accurate because this is not the scope of this post, anyway we have an accurate physical simulation of a flying arrow.
But I also like simple things, and so I simplified the script just manually modifying arrow roation according to its direction, until it touches something:
package {
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
import Box2D.Dynamics.*;
import Box2D.Collision.*;
import Box2D.Collision.Shapes.*;
import Box2D.Common.Math.*;
import Box2D.Dynamics.Contacts.*;
public class Main extends Sprite {
private var world:b2World=new b2World(new b2Vec2(0,10),true);
private var worldScale:int=30;
private var arrowVector:Vector.=new Vector.();
public function Main():void {
debugDraw();
wall(320,470,640,20);
wall(630,240,20,480);
addEventListener(Event.ENTER_FRAME, update);
stage.addEventListener(MouseEvent.CLICK,addArrow);
}
private function addArrow(e:MouseEvent):void {
var angle:Number=Math.atan2(mouseY-450,mouseX);
var vertices:Vector.=new Vector.();
vertices.push(new b2Vec2(-1.4,0));
vertices.push(new b2Vec2(0,-0.1));
vertices.push(new b2Vec2(0.6,0));
vertices.push(new b2Vec2(0,0.1));
var bodyDef:b2BodyDef= new b2BodyDef();
bodyDef.position.Set(0,450/worldScale);
bodyDef.type=b2Body.b2_dynamicBody;
bodyDef.userData="arrow";
var polygonShape:b2PolygonShape = new b2PolygonShape();
polygonShape.SetAsVector(vertices,4);
var fixtureDef:b2FixtureDef = new b2FixtureDef();
fixtureDef.shape=polygonShape;
fixtureDef.density=1;
fixtureDef.friction=0.5;
fixtureDef.restitution=0.5;
var body:b2Body=world.CreateBody(bodyDef);
body.CreateFixture(fixtureDef);
body.SetLinearVelocity(new b2Vec2(20*Math.cos(angle),20*Math.sin(angle)));
body.SetAngle(angle);
arrowVector.push(body);
}
private function debugDraw():void {
var debugDraw:b2DebugDraw=new b2DebugDraw();
var debugSprite:Sprite=new Sprite();
addChild(debugSprite);
debugDraw.SetSprite(debugSprite);
debugDraw.SetDrawScale(worldScale);
debugDraw.SetFlags(b2DebugDraw.e_shapeBit|b2DebugDraw.e_jointBit);
debugDraw.SetFillAlpha(0.5);
world.SetDebugDraw(debugDraw);
}
private function wall(pX:Number,pY:Number,w:Number,h:Number):void {
var bodyDef:b2BodyDef=new b2BodyDef();
bodyDef.position.Set(pX/worldScale,pY/worldScale);
bodyDef.userData="wall";
var polygonShape:b2PolygonShape=new b2PolygonShape();
polygonShape.SetAsBox(w/2/worldScale,h/2/worldScale);
var fixtureDef:b2FixtureDef=new b2FixtureDef();
fixtureDef.shape=polygonShape;
fixtureDef.density=1;
fixtureDef.restitution=0.4;
fixtureDef.friction=0.5;
var theWall:b2Body=world.CreateBody(bodyDef);
theWall.CreateFixture(fixtureDef);
}
private function update(e : Event):void {
world.Step(1/30,5,5);
world.ClearForces();
for (var i:Number=arrowVector.length-1; i>=0; i--) {
var body:b2Body=arrowVector[i];
var flyingAngle:Number=Math.atan2(body.GetLinearVelocity().y,body.GetLinearVelocity().x);
body.SetAngle(flyingAngle);
if (body.GetPosition().x*worldScale>200) {
for (var c:b2ContactEdge=body.GetContactList(); c; c=c.next) {
var contact:b2Contact=c.contact;
var fixtureA:b2Fixture=contact.GetFixtureA();
var fixtureB:b2Fixture=contact.GetFixtureB();
var bodyA:b2Body=fixtureA.GetBody();
var bodyB:b2Body=fixtureB.GetBody();
if (bodyA.GetUserData()=="wall"||bodyB.GetUserData()=="wall") {
arrowVector.splice(i,1);
}
}
}
}
world.DrawDebugData();
}
}
}
And this is the final result:
Once again, click on the stage to shoot an arrow.
Download the source code (3rd example only).
Which way would you use to simulate a flying arrow?
Next time, shooting arrow simulation with commented source code.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.