Spear Fishing with Box2D

In Johnny's Island you have to catch fish for food and the fish can also be used for shark bait to distract the sharks when collecting drift wood. The mechanic for catching fish is spearfishing. This spearfishing mechanic on the surface looks really simple and I think works well in the game. In fact, there are games in the app store entirely based around this one mechanic and that's fine but it's just one of many mini-games in Johnny's Island. The main thing that makes this all work is Box2D. Box2D is an open-source 2D physics engine used by many of the popular games out there and that's what I'm going to be covering today.

What's covered

  • setting up the physics bodies
  • attaching the spear to a motorized Revolute Joint
  • applying forces (gravity settings, swimming fish and the projectile spear)
  • collisions
  • make the spear stick (Weld Joints)

First here's a video demonstrating how the spearfishing mechanic works:

Setting up the spear

When creating the spear fixtures and bodies it's important to make it as symmetrical and balanced as possible. I used a diamond shape for the spear head to make sure it was a little bit more front heavy. This will ensure it flies through the air nice and straight (see Fig 1 to see how it compares to the sprite image for the spear)

spear

Fig 1: This shows the spear sprite and the Box2D body definition

CODE: spear head and spear shaft bodies

        float halfWidth = (sprite.getWidth()/2 / PIXELS_TO_METERS);
        float halfHeight = (sprite.getHeight()/2 / PIXELS_TO_METERS);
    	
    	BodyDef bodyDefSpearShaft = new BodyDef();
	    bodyDefSpearShaft.type = BodyDef.BodyType.DynamicBody;
	    bodyDefSpearShaft.position.set(position.x,position.y);
	    bodyDefSpearShaft.angle=(float) Math.toRadians(0);
        bodySpearShaft = world.createBody(bodyDefSpearShaft);
        bodySpearShaft.setUserData(new BodyUserData(0,"SpearShaftBody"));

	    BodyDef bodyDefSpearHead = new BodyDef();
	    bodyDefSpearHead.type = BodyDef.BodyType.DynamicBody;
	    bodyDefSpearHead.position.set(position.x,position.y);
	    bodyDefSpearHead.angle=(float) Math.toRadians(0);
	    bodySpearHead = world.createBody(bodyDefSpearHead);
	    bodySpearHead.setUserData(new BodyUserData(0,"SpearHeadBody"));
	    
        //shaft fixture
        PolygonShape shapeShaft = new PolygonShape();
        Vector2[] verticesShaft = new Vector2[4];
        verticesShaft[0] = new Vector2(halfWidth-0.9f, halfHeight-0.07f);
        verticesShaft[1] = new Vector2(halfWidth-0.9f, -halfHeight+0.07f);
        verticesShaft[2] = new Vector2(-halfWidth, -halfHeight+0.07f);
        verticesShaft[3] = new Vector2(-halfWidth, halfHeight-0.07f);
        shapeShaft.set(verticesShaft);
        FixtureDef fixtureDefShaft = new FixtureDef();
		fixtureDefShaft.shape = shapeShaft;
		fixtureDefShaft.density = 0.1f;
		fixtureDefShaft.friction = 0.0f;
		fixtureDefShaft.restitution = 0.0f;
		
        //spear head fixture
        PolygonShape shapeSpearHead = new PolygonShape();
        Vector2[] vertices = new Vector2[4];
		vertices[0] = new Vector2(0.5f,  0.0f);
		vertices[1] = new Vector2(halfWidth-0.5f, -0.1f);
		vertices[2] = new Vector2(halfWidth-0.0f,  0.0f);
		vertices[3] = new Vector2(halfWidth-0.5f,  0.1f);        
		shapeSpearHead.set(vertices);
		FixtureDef fixtureDefSpearHead = new FixtureDef();
		fixtureDefSpearHead.shape = shapeSpearHead;
		fixtureDefSpearHead.density = 0.1f;
		fixtureDefSpearHead.friction = 0.0f;
		fixtureDefSpearHead.restitution = 0.0f;

		bodySpearShaft.createFixture(fixtureDefShaft);
		bodySpearHead.createFixture(fixtureDefSpearHead);

//dispose shapes
shapeShaft.dispose();
shapeSpearHead.dispose();

If you look at the code above, you can see I used two fixtures and two separate bodies respectively. You don't necessarily need to do it this way and could probably just add both fixtures to one body but I did it this way because I want only the spear head fixture to be made "sticky" during collision detection. You wouldn't want the spear shaft to stick to the fish when it hits, right? So I also set some userdata to a class I made to hold different things for use during collision detection which will make sense later. But seeing as I have chosen to use two separate bodies I need to weld those bodies together to form the whole spear. See code below for a simple weld joint.

CODE: BodyUserData class

public class BodyUserData {
	String name;
	int uniqueID;
	
	public BodyUserData(int uniqueID, String name) {
		this.name=name;
		this.uniqueID=uniqueID;
	}
}

CODE: weld the two bodies together to form the spear

//weld the spear head to the shaft
WeldJointDef weldJointDef = new WeldJointDef();
weldJointDef.bodyA = bodySpearHead;
weldJointDef.bodyB = bodySpearShaft;
weldJointDef.type = JointType.WeldJoint;
weldJointDef.collideConnected = true;
weldJointDef.frequencyHz=0;
weldJointDef.dampingRatio=0;
Vector2 weldpoint = bodySpearHead.getWorldCenter();
weldJointDef.initialize(weldJointDef.bodyB,weldJointDef.bodyA,  weldpoint);
world.createJoint(weldJointDef);

Now that we've got a the spear created we will create motorized revolute joint. I decided to create another body for the axel and put the revolute joint on that and weld the axel body to the shaft. This joint rotates by enabling the motor (see Fig 2 to see how it rotates).

rotating spear

Fig 2: spear rotating on a motorized revolute joint

CODE: motorized revolute joint

//create revolute joint (motorized axel) and attach it to the spearshaft and motorize it
BodyDef rotationAxelBodyDef = new BodyDef();
rotationAxelBodyDef.type = BodyDef.BodyType.StaticBody;
rotationAxelBodyDef.position.set(position.x,position.y);
rotationAxelBodyDef.angle=(float) Math.toRadians(10);
bodyRotationAxel = world.createBody(rotationAxelBodyDef);
bodyRotationAxel.setUserData("RotationAxelBody");
RevoluteJointDef weldJointDef2 = new RevoluteJointDef();
weldJointDef2.bodyA = bodyRotationAxel;
weldJointDef2.bodyB = bodySpearShaft;
weldJointDef2.type = JointType.RevoluteJoint;
weldJointDef2.collideConnected = true;
weldJointDef2.enableMotor = true;
weldJointDef2.maxMotorTorque = 200;
weldJointDef2.motorSpeed = (float)Math.toRadians(motorSpeed); 
Vector2 weldpoint2 = bodyRotationAxel.getWorldCenter();
weldJointDef2.initialize(weldJointDef2.bodyB,weldJointDef2.bodyA,  weldpoint2);
motorJoint = (RevoluteJoint) world.createJoint(weldJointDef2);

When the angle of the spear reaches a certain point in either direction I toggle the motor direction
CODE: changing motor direction

if (Math.toDegrees(bodySpearShaft.getAngle())< =-145 && !spearLaunched){
	motorJoint.setMotorSpeed((float)Math.toRadians(-motorSpeed));
}else if (Math.toDegrees(bodySpearShaft.getAngle())>=-35 && !spearLaunched){
	motorJoint.setMotorSpeed((float)Math.toRadians(motorSpeed));
}

Setting up the fish

It's also important to make the fish somewhat symmetrical and give it enough weight so that when the spear hits it we don't have it spin erratically out of control. (see Fig 3 to see how it compares to the sprite image for the fish)

fish and wireframe

Fig 3: This shows the fish sprite and the Box2D body definition

CODE: fish body
//fish body
BodyDef fishbodyDef = new BodyDef();
fishbodyDef.type = BodyDef.BodyType.DynamicBody;
fishbodyDef.position.set(this.position.x,this.position.y);
fishbodyDef.angle=(float) Math.toRadians(0);
fishBody = world.createBody(fishbodyDef);
fishBody.setUserData(new BodyUserData(uniqueID,&amp;amp;amp;amp;amp;amp;quot;fish&amp;amp;amp;amp;amp;amp;quot;));
fishBody.setFixedRotation(true);

//front circle mass
CircleShape fishShapeCircle1 = new CircleShape();
fishShapeCircle1.setPosition(new Vector2(0.78f*this.fishScaleX, -0.13f*this.fishScaleY));
fishShapeCircle1.setRadius(0.2f*this.fishScaleY);
FixtureDef fishFixtureDef1 = new FixtureDef();
fishFixtureDef1.shape = fishShapeCircle1;
fishFixtureDef1.friction = 0.0f;
fishFixtureDef1.restitution = 0.2f;
fishFixtureDef1.density = this.fishDensity;
fishBody.createFixture(fishFixtureDef1);

//front circle mass 2
CircleShape fishShapeCircle1a = new CircleShape();
fishShapeCircle1a.setPosition(new Vector2(0.55f*this.fishScaleX, -0.08f*this.fishScaleY));
fishShapeCircle1a.setRadius(0.35f*this.fishScaleY);
FixtureDef fishFixtureDef1a = new FixtureDef();
fishFixtureDef1a.shape = fishShapeCircle1a;
fishFixtureDef1a.friction = 0.0f;
fishFixtureDef1a.restitution = 0.2f;
fishFixtureDef1a.density = this.fishDensity;
fishBody.createFixture(fishFixtureDef1a);

//center circle mass
CircleShape fishShapeCircle2 = new CircleShape();
fishShapeCircle2.setPosition(new Vector2(0.3f*this.fishScaleX, -0.06f*this.fishScaleY));
fishShapeCircle2.setRadius(0.4f*this.fishScaleY);
FixtureDef fishFixtureDef2 = new FixtureDef();
fishFixtureDef2.shape = fishShapeCircle2;
fishFixtureDef2.friction = 0.0f;
fishFixtureDef2.restitution = 0.2f;
fishFixtureDef2.density = this.fishDensity;
fishBody.createFixture(fishFixtureDef2);

//tail mass
PolygonShape shapeTail = new PolygonShape();
Vector2[] verticesTail = new Vector2[4];
verticesTail[0] = new Vector2(0.1f, 0.28f);
verticesTail[1] = new Vector2(0.1f, -0.45f);
verticesTail[2] = new Vector2(-0.6f, -0.06f);
verticesTail[3] = new Vector2(-0.6f, 0.05f);
for (int v=0;v&amp;amp;amp;lt;verticesTail.length;v++){
	verticesTail[v].x *=this.fishScaleX;
	verticesTail[v].y *=this.fishScaleY;
}
shapeTail.set(verticesTail);
FixtureDef fishFixtureDef3 = new FixtureDef();
fishFixtureDef3.shape = shapeTail;
fishFixtureDef2.friction = 0.0f;
fishFixtureDef2.restitution = 0.2f;
fishFixtureDef2.density = this.fishDensity;
fishBody.createFixture(fishFixtureDef3); 

//dispose shapes
fishShapeCircle1.dispose();
fishShapeCircle2.dispose();
shapeTail.dispose();

Making the fish move is done by applying a linear impulse on each update and making sure the linear velocity doesn't exceed a certain amount for each fish. This makes certain the fish moves at a steady pace across the screen. Each fish has a randomized speed and size.

CODE: fish movement
if (!dead){
	//speed throttling, make sure the fish isn't going more than the maxVelocity
	if (fishBody.getLinearVelocity().len()&amp;gt;=maxVelocity){ 
		Vector2 velocity = fishBody.getLinearVelocity();
		velocity.setLength(maxVelocity);
		fishBody.setLinearVelocity(velocity);
	}    

	//apply force in the dirction fish is facing
	float magnitude = 0.1f * delta;
	Vector2 force = new Vector2((float)Math.cos(fishBody.getAngle()) * magnitude , (float)Math.sin(fishBody.getAngle()) * magnitude);
	if (!isFacingRight) force.rotate(180);
	fishBody.applyLinearImpulse(force, fishBody.getPosition(),true);
}

Launching the spear and collisions

Launching the spear consists of stopping rotation, destroying the joint and applying linear impulse in direction the spear is facing.

CODE: launch spear

if (!spearLaunched){
	spearLaunched=true;
	
	//stop spear from rotating
	bodySpearShaft.setLinearVelocity(new Vector2(0f,0f));
	bodySpearShaft.setAngularVelocity(0);
	bodySpearHead.setLinearVelocity(new Vector2(0f,0f));
	bodySpearHead.setAngularVelocity(0);
	
	//destroy the revolute joint and destroy the axel rotation body
	world.destroyJoint(motorJoint);
	world.destroyBody(bodyRotationAxel);

	//launch the spear
	float magnitude = 10.0f;
	Vector2 force = new Vector2((float)Math.cos(bodySpearShaft.getAngle()) * magnitude , (float)Math.sin(bodySpearShaft.getAngle()) * magnitude);
	bodySpearShaft.applyForce(force, bodySpearShaft.getPosition(),true);
}

spear fish

Fig 3: Spear colliding with fish and sticking

As you can see in Fig 3, the spear stops rotating and is immediately applied a force sending it flying. We deal with the sticky collisions using a ContactListener. I put everything pertaining to the spear being "marked as sticky" in the PostSolve of the ContactListener. The PostSolve gets the two fixtures making contact and looks up the userData and pairs off those two fixtures using a class I made called StickyInfo. Effectively, after the world step is completed I'll have an array of StickyInfo objects if the right conditions are met.

CODE: ContactListener
private void createCollisionListener() {
	world.setContactListener(new ContactListener() {

		@Override
		public void beginContact(Contact contact) {
		}

		@Override
		public void endContact(Contact contact) {
		}

		@Override
		public void preSolve(Contact contact, Manifold oldManifold) {
		}

		@Override
		public void postSolve(Contact contact, ContactImpulse impulse) {
			 Fixture fixtureA = contact.getFixtureA();
			 Fixture fixtureB = contact.getFixtureB();
			 Body bodyA=fixtureA.getBody();
			 Body bodyB=fixtureB.getBody();
			 BodyUserData userDataBodyA = (BodyUserData) bodyA.getUserData();
			 BodyUserData userDataBodyB = (BodyUserData) bodyB.getUserData();
			 
			 if (!isSpeared){
				 if (userDataBodyA.name.equals(&quot;SpearHeadBody&quot;) &amp;&amp; userDataBodyB.name.equals(&quot;fish&quot;)) {
					 bodyB.setFixedRotation(false);
					 collisionsToMakeSticky.add(new StickyInfo(bodyA, bodyB, contact.getWorldManifold().getPoints()));
					 fishCaught = true; 
					 isSpeared = true;
					 indexOfSpearedFish = userDataBodyB.uniqueID;
				 }else if (userDataBodyA.name.equals(&quot;fish&quot;) &amp;&amp; userDataBodyB.name.equals(&quot;SpearHeadBody&quot;)){
					 bodyB.setFixedRotation(false);
					 collisionsToMakeSticky.add(new StickyInfo(bodyA, bodyB, contact.getWorldManifold().getPoints()));
					 fishCaught = true;
					 isSpeared = true;
					 indexOfSpearedFish = userDataBodyA.uniqueID;
				 }
				 else if ((userDataBodyA.name.equals(&quot;SpearHeadBody&quot;) &amp;&amp; userDataBodyB.name.equals(&quot;SeabedBody&quot;) ||
								userDataBodyA.name.equals(&quot;SeabedBody&quot;) &amp;&amp; userDataBodyB.name.equals(&quot;SpearHeadBody&quot;))) {
					 collisionsToMakeSticky.add(new StickyInfo(bodyA, bodyB, contact.getWorldManifold().getPoints()));
					 fishCaught = false;
					 isSpeared = true;
				 }
			 }else if ((userDataBodyA.name.equals(&quot;fish&quot;) &amp;&amp; userDataBodyB.name.equals(&quot;SeabedBody&quot;) ||
					userDataBodyA.name.equals(&quot;SeabedBody&quot;) &amp;&amp; userDataBodyB.name.equals(&quot;fish&quot;))) {
				 //I don't do anything here
			 }else if ((userDataBodyA.name.equals(&quot;SpearShaftBody&quot;) &amp;&amp; userDataBodyB.name.equals(&quot;SeabedBody&quot;) ||
					userDataBodyA.name.equals(&quot;SeabedBody&quot;) &amp;&amp; userDataBodyB.name.equals(&quot;SpearShaftBody&quot;))) {
				 //I don't do anything here
			 }
			
		}

	});
}
CODE: StickyInfo class
public class StickyInfo{
	Body bodyA;
	Body bodyB;
	Vector2 contactPoints[];
	public StickyInfo(Body bodyA, Body bodyB,Vector2 contactPoints[]){
		this.bodyA = bodyA;
		this.bodyB = bodyB;
		this.contactPoints = contactPoints;
	}
};

Now all I have to do is after each world step is check and see if there's any StickInfo objects in the collisionsToMakeSticky array and create a weld joint. Now I realize I don't have to do things exactly this way because I'm only spearing one fish. There will only ever be one weld joint created before each round of fishing ends but this idea of having an array of sticky objects can be expanded to shoot more than one spear and catch more than one fish. Anyway, the code is still useful I think.

CODE: welding stuff after the world step
while(collisionsToMakeSticky.size>0){
	StickyInfo si = collisionsToMakeSticky.removeIndex(0);
	//Make the WeldJoint with the bodies si.bodyA and si.bodyB
	WeldJointDef weldJointDef = new WeldJointDef();
	weldJointDef.bodyA = si.bodyA;
	weldJointDef.bodyB = si.bodyB;
	weldJointDef.collideConnected = true;
	weldJointDef.frequencyHz=0;
	weldJointDef.dampingRatio=0;
	weldJointDef.referenceAngle = weldJointDef.bodyB.getAngle() - weldJointDef.bodyA.getAngle();
	weldJointDef.initialize(si.bodyB,si.bodyA,  si.contactPoints[0]);
	spearedJoint = world.createJoint(weldJointDef);
		
	fishingDone=true;
}
Fig 4: Shows the weld joint between the fish and spear

Fig 4: Shows the weld joint between the fish and spear

As you can see from Fig 4 there's a weld joint between the tip of the spear and the fish which is denoted by a line connecting the two fixtures. So that's it! That's all there is to it.

Michael

I draw the things and program the stuff that goes beep-boop

Leave a Reply

Your email address will not be published. Required fields are marked *