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)
CODE: spear head and spear shaft bodiesfloat 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 classpublic class BodyUserData { String name; int uniqueID; public BodyUserData(int uniqueID, String name) { this.name=name; this.uniqueID=uniqueID; } }
//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).
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 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;quot;fish&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;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.
if (!dead){ //speed throttling, make sure the fish isn't going more than the maxVelocity if (fishBody.getLinearVelocity().len()&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 spearif (!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); }
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.
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("SpearHeadBody") && userDataBodyB.name.equals("fish")) { bodyB.setFixedRotation(false); collisionsToMakeSticky.add(new StickyInfo(bodyA, bodyB, contact.getWorldManifold().getPoints())); fishCaught = true; isSpeared = true; indexOfSpearedFish = userDataBodyB.uniqueID; }else if (userDataBodyA.name.equals("fish") && userDataBodyB.name.equals("SpearHeadBody")){ bodyB.setFixedRotation(false); collisionsToMakeSticky.add(new StickyInfo(bodyA, bodyB, contact.getWorldManifold().getPoints())); fishCaught = true; isSpeared = true; indexOfSpearedFish = userDataBodyA.uniqueID; } else if ((userDataBodyA.name.equals("SpearHeadBody") && userDataBodyB.name.equals("SeabedBody") || userDataBodyA.name.equals("SeabedBody") && userDataBodyB.name.equals("SpearHeadBody"))) { collisionsToMakeSticky.add(new StickyInfo(bodyA, bodyB, contact.getWorldManifold().getPoints())); fishCaught = false; isSpeared = true; } }else if ((userDataBodyA.name.equals("fish") && userDataBodyB.name.equals("SeabedBody") || userDataBodyA.name.equals("SeabedBody") && userDataBodyB.name.equals("fish"))) { //I don't do anything here }else if ((userDataBodyA.name.equals("SpearShaftBody") && userDataBodyB.name.equals("SeabedBody") || userDataBodyA.name.equals("SeabedBody") && userDataBodyB.name.equals("SpearShaftBody"))) { //I don't do anything here } } }); }
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.
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; }