Intermediate Box2D Physics: Forces, Ray Casts, and Sensors

If you’ve gone through the Box2D tutorials in this site or in our Learning Cocos2D Book and can’t get enough, this tutorial is for you! This tutorial will cover some intermediate Box2D techniques: applying forces to objects, using ray casts, and using sensors for collision detection. In this tutorial, we’ll be adding some new features […] By .

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Box2D Ray Casting

To use Box2D ray casting, you call a simple function on the world called RayCast, and give it the start and finish point (basically what we just figured out above).

You also pass the function an object that will receive a callback for each fixture the ray intersects. Usually you just squirrel away the information in the object, and retrieve the results from the object after the call to RayCast.

Let’s create a simple Raycast callback class. Go to File\New\New File, choose iOS\C and C++\Header File, and click Next. Name the new header RaysCastCallback.h, and click Save.

Replace the file with the following:

#import "Box2D.h"

class RaysCastCallback : public b2RayCastCallback
{
public:
    RaysCastCallback() : m_fixture(NULL) {
    }
    
    float32 ReportFixture(b2Fixture* fixture, const b2Vec2& point, const b2Vec2& normal, float32 fraction) {        
        m_fixture = fixture;        
        m_point = point;        
        m_normal = normal;        
        m_fraction = fraction;        
        return fraction;     
    }    

    b2Fixture* m_fixture;    
    b2Vec2 m_point;    
    b2Vec2 m_normal;    
    float32 m_fraction;

};

ReportFixture is the method that will get called whenever Box2D detects an intersection. We have a pretty simple implementation – we just squirrel everything away.

One thing about ReportFixture is you can’t make any assumption about the order of the calls (i.e. it’s not necessarily closest to farthest). However, you can do some interesting things with the return value.

  • If you return 0: The ray cast will be terminated immediately. So your ReportFixture will be called at most one time, with one random fixture it collides with.
  • If you return 1: The ray cast will continue. So your ReportFixture will be called for every fixture that collides along the ray. With this implementation, you’ll still be squirreling away one random set of information (whatever the last call gave you).
  • The ray cast will be clipped to the current intersection point. This is what we do here. With this implementation, you’ll be guaranteed that each time ReportFixture is called, the intersection gets closer and closer toward the start point. So by the end the closest intersection will be squirreled away in the instance variables, which is what we want for line of sight!

You could modify this class to discard certain fixtures that might be transparent (like a piece of glass, etc). However this simple implementation is fine for our game.

Switch back to ActionLayer.mm and make the following changes:

// Add to top of file
#import "RaysCastCallback.h"

// Add inside updateMonsters, right after setting canSeePlayer to NO
RaysCastCallback callback;
_world->RayCast(&callback, eye, target);

if (callback.m_fixture) {
    monsterData.target = ccp(callback.m_point.x * [LevelHelperLoader pixelsToMeterRatio], 
        callback.m_point.y * [LevelHelperLoader pixelsToMeterRatio]);
    if (callback.m_fixture->GetBody() == _heroBody) {    
        monsterData.canSeePlayer = TRUE;
    }
}

Here we declare a RaysCastCallback class, and call the RayCast method, passing it as a parameter. It will call the ReportFixture zero or more times on the class, and by the end the closest fixture and contact point should be squirreled away.

We then look to see if it found an intersection, and if so we set the target to the actual point that was found. This will cause the line that is drawn to be truncated to a smaller range if it hits a cloud, etc., which is a cool way to visualize the first thing the monster is seeing since that’s the line we’re drawing.

We finally check to see if the fixture is the hero, and if it is set the flag to true. Remember, this will cause the line to be drawn red.

Compile and run, and move your player to the danger zone underneath the monster, and see if you’re spotted!

Box2D raycasting example

Lasers and Sensors

Obviously it is not a good thing if a scary monster like that spots you. So how are we going to punish our hero for not being careful? It’s obvious – shoot lasers at him!

Back in the previous tutorial, we added a laser to the scene to act as a “template” for future lasers. We can use LevelHelper to create a new laser based on how that laser was set up.

When we set up the laser, we set it up to be a sensor in Box2D. If you aren’t using LevelHelper, you can easily do this by setting the isSensor variable on your fixture to true.

When an object is a sensor, as long as you have the category, mask, and groupIndex properties set up right, you will receive callbacks to your contact listener, but it won’t cause physics reactions like bouncing off another object. This is perfect for our laser, since we want it to go through everything but zap the player if it hits him.

Let’s start just by shooting the lasers – we’ll add collision detection later. Add the following code inside updateMonsters, right after canSeePlayer is set to TRUE:

if (CACurrentMediaTime() - monsterData.lastShot > 1.0) {
    monsterData.lastShot = CACurrentMediaTime();
    
    // Create and position laser
    b2Body *laserBody = [_lhelper newBodyWithUniqueName:@"laserbeam_red" world:_world];
    CCSprite *laserSprite = (CCSprite *)laserBody->GetUserData();
    laserSprite.position = monsterData.eye;
    laserSprite.rotation = monsterSprite.rotation;                        
    laserBody->SetTransform(b2Vec2(laserSprite.position.x/[LevelHelperLoader pixelsToMeterRatio], 
                                   laserSprite.position.y/[LevelHelperLoader pixelsToMeterRatio]), 
                            CC_DEGREES_TO_RADIANS(-laserSprite.rotation));    
    
    // Make laser move
    b2Vec2 laserVel = callback.m_point - eye;
    laserVel.Normalize();
    laserVel *= 4.0;
    laserBody->SetLinearVelocity(laserVel);
    
    [[SimpleAudioEngine sharedEngine] playEffect:@"laser.wav"];
                        
}

We first add some code to prevent the monster from shooting more often than every second.

Next we create a new sprite and body for the laser based on the template, using LevelHelper’s newBodyWithUniqueName method. We set the initial position and rotation of the laser to start at the eye, rotated the same way the monster is. We also manually update the laser body to be at the same position of where we just set the sprite.

Finally, we move the laser manually via SetLinearVelocity. To figure out where to go, we’re using the same technique of finding the vector the eye is looking at like we did earlier.

We also play a cool laser sound effect!

Compile and run, and prepare to be blasted!

Laser created from LevelHelper template

Finishing Touches

Right now the lasers just pass harmlessly by our hero, even if he’s foolishly stood in danger’s way. So let’s fix that by adding lives and collision detection!

If you remember from the Breakout Game Tutorial or our Learning Cocos2D Book, when you want to detect collisions you have to create a contact listener class.

So let’s create a simple ContactListener that simply directs the callbacks back to our action layer.

Go to File\New\New File, choose iOS\C and C++\Header File, and click Next. Name the new header SimpleContactListener.h, and click Save. Then replace SimpleContactListener.h with the following:

#import <Foundation/Foundation.h>
#import "cocos2d.h"
#import "Box2D.h"
#import "ActionLayer.h"

class SimpleContactListener : public b2ContactListener {
public:
    ActionLayer *_layer;
    
    SimpleContactListener(ActionLayer *layer) : _layer(layer) { 
    }
    
    void BeginContact(b2Contact* contact) { 
        [_layer beginContact:contact];
    }
                        
    void EndContact(b2Contact* contact) { 
        [_layer endContact:contact];
    }

    void PreSolve(b2Contact* contact, const b2Manifold* oldManifold) { 
    }

    void PostSolve(b2Contact* contact, const b2ContactImpulse* impulse) {  
    }

};

Pretty simple, eh? Next switch to ActionLayer.h and add a few more instance variables:

int _lives;
b2ContactListener * _contactListener;
BOOL _invincible;

And finally make the following changes to ActionLayer.mm:

// Add to top of file
#import "SimpleContactListener.h"

// Add at bottom of setupWorld
_contactListener = new SimpleContactListener(self);
_world->SetContactListener(_contactListener);

// Add above init
- (void)updateLives {
    // TODO: Next tutorial!  :D
}

// Add inside init, right after setting isTouchEnabled
_lives = 3;
[self updateLives];

// Add right before call to dealloc
- (void)beginContact:(b2Contact *)contact {
    
    if (_gameOver) return;
    
    b2Fixture *fixtureA = contact->GetFixtureA();
    b2Fixture *fixtureB = contact->GetFixtureB();
    b2Body *bodyA = fixtureA->GetBody();
    b2Body *bodyB = fixtureB->GetBody();
    CCSprite *spriteA = (CCSprite *) bodyA->GetUserData();
    CCSprite *spriteB = (CCSprite *) bodyB->GetUserData();
    
    if (!_invincible) {
        if ((spriteA == _hero && spriteB.tag == LASER) ||
            (spriteB == _hero && spriteA.tag == LASER)) {
            _lives--;
            [self updateLives];
            [[SimpleAudioEngine sharedEngine] playEffect:@"whine.wav"];
            if (_lives == 0) {
                [self loseGame];
                return;
            }
            _invincible = YES;
            [_hero runAction:
             [CCSequence actions:
              [CCBlink actionWithDuration:1.5 blinks:9],
              [CCCallBlock actionWithBlock:^(void) {
                 _invincible = NO;
             }],
              nil]];
        }
    }    

}

- (void)endContact:(b2Contact *)contact {
       
}

// Add at *beginning* of dealloc
_world->SetContactListener(NULL);
delete _contactListener;

In setupWorld we create and set the contact listener and in init we set the lives to 3.

The important part is in beginContact. We check to see if the hero is colliding with a laser, and if so subtract a life, play a sound effect, etc. We make the hero invincible for a second and a half after he’s hit to make things a bit easier for the poor dude (and avoid the problem of multiple collisions with the same laser).

Note this demonstrates the cool CCCallBlock actoin – cool!

Compile and run, and you should now be able to be blasted by lasers!