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 .
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Intermediate Box2D Physics: Forces, Ray Casts, and Sensors
25 mins
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 to a simple physics platformer that we created in the How To Use SpriteHelper and LevelHelper Tutorial. Don’t worry if you don’t have SpriteHelper or LevelHelper – this tutorial doesn’t require them.
So if you don’t have it already, download the sample project and fire it up to see where we’re at, and let’s start having fun with Box2D!
Using Forces
If you try out the game, you’ll notice that the monsters fall from the sky and bounce on the ground.
That’s not very alien-like! It would be a lot better if the aliens floated menacingly in the air.
This brings us to a common question – how can we make some objects affected by gravity, but not others?
One technique you can use is to not use gravity at all, but just apply the gravity force manually on all sprites that are affected by gravity. Or alternatively, you can apply an anti-gravity force on things that are not affected by gravity.
We’ll try the latter. Switch to ActionLayer.mm and make the following changes:
// Add right after updateHero method
- (void)updateMonsters:(ccTime)dt {
NSArray *monsters = [_lhelper bodiesWithTag:MONSTER];
for(NSValue* monsterValue in monsters) {
b2Body* monsterBody = (b2Body*)[monsterValue pointerValue];
monsterBody->ApplyForce(-1 * monsterBody->GetMass() * _world->GetGravity(),
monsterBody->GetWorldCenter());
monsterBody->SetAngularVelocity(1.0);
}
}
// Inside update, add right after call to updateHero
[self updateMonsters:dt];
The first bit of code here gets the list of monsters in the game via a helper method that retrieves all of the Box2D bodies set up with the MONSTER tag in LevelHelper.
Next, we need to apply the force to the monster to get it to float.
Force is in newtons (N), which is the force required to accelerate one kilogram of mass at a rate of one meter per second squared. We know we want to accelerate our monster the opposite of gravity (set up to -10 meters per second sauared in LevelHelper), so we have to multiply that by the monster’s mass to get Newtons. We also multiply by negative one to reverse the gravity.
We also set the angular velocity to 1 radian/second here (which is a slowish turnaround), so the monster spins around as if he’s looking for an intruder.
Compile and run, and now you have floating and spinning monsters!
By the way, you may wonder when you should use impulses and when you should use forces. A common rule of thumb is if you need something to move instantly (like our hero jumping), use an impulse. If you need an object to move over a period of time (like the permanent float effect here), use forces instead.
Ray Cast Visualization
Next we’ll move onto something I’m particular fond of (and it’s not just because of the name) – ray casting!
The idea behind Box2D ray casting is you specify a start point and an end point, and Box2D will trace along the line from start to end, and tell you every Box2D fixture that it collides with.
You can then do whatever you want with that information. We’re going to use this to simulate line of sight for our monsters. If we draw a line from the eyeball straight out and the first thing it hits is our hero (rather than a wall) – he’s been seen!
Before we can use Box2D for ray casting, we have to figure out the start point and end point of the lines to ray cast. It’s easy to make mistakes while doing this, so one thing I like to do is use Cocos2D drawing methods to visualize the points I’m using to make sure it’s working OK.
We’re going to need a class to keep track of the start and end points for each monster (along with a few other bits), so go to File\New\New File, choose iOS\Cocoa Touch\Objective-C class, and click Next. Enter NSObject for Subclass of, click Next, name the new class MonsterData.m, and click Save.
Replace MonsterData.h with the following:
#import <Foundation/Foundation.h>
@interface MonsterData : NSObject
@property CGPoint eye;
@property CGPoint target;
@property BOOL canSeePlayer;
@property double lastShot;
@end
This creates a simple subclass of NSObject with four properties we’ll need. Notice this abbreviated format – it doesn’t need to specify the instance variables because the compiler will create those automatically for us now. Pretty cool eh?
Switch to MonsterData.m and replace it with the following:
#import "MonsterData.h"
@implementation MonsterData
@synthesize eye;
@synthesize target;
@synthesize canSeePlayer;
@synthesize lastShot;
@end
Now let’s make use of this. Make the following changes to ActionLayer.mm:
// Add to top of file
#import "MonsterData.h"
// Add to the bottom of setupLevelHelper
NSArray *monsters = [_lhelper spritesWithTag:MONSTER];
for (CCSprite *monster in monsters) {
MonsterData *data = [[[MonsterData alloc] init] autorelease];
[LevelHelperLoader setCustomValue:data withKey:@"data" onSprite:monster];
}
Here we loop through all of the monsters sprites, create an empty MonsterData for them, and store it on the sprite under the “data” key. This setCustomValue method is something LevelHelper lets you do to associate extra information like this with a sprite. If you weren’t using LevelHelper, you could subclass CCSprite instead or create a dictionary and set it on the sprite’s userData.
Next add the following inside updateMonsters, right after the call to SetAngularVelocity:
b2Vec2 eyeOffset = b2Vec2(0, -0.5);
b2Vec2 eye = monsterBody->GetWorldPoint(eyeOffset);
b2Vec2 target = eye - monsterBody->GetWorldCenter();
target.Normalize();
target *= 20.0;
target = eye + target;
CCSprite *monsterSprite = (CCSprite*)monsterBody->GetUserData();
MonsterData * monsterData = [LevelHelperLoader customValueWithKey:@"data" forSprite:monsterSprite];
monsterData.eye = ccp(eye.x * [LevelHelperLoader pixelsToMeterRatio], eye.y * [LevelHelperLoader pixelsToMeterRatio]);
monsterData.target = ccp(target.x * [LevelHelperLoader pixelsToMeterRatio],
target.y * [LevelHelperLoader pixelsToMeterRatio]);
monsterData.canSeePlayer = NO;
The first bit figures out the start point (where the eye is) and the end point (a certain distance away from where the eye is looking). Let’s go over how that works.
The position of the eye is easy – it’s just 0.5 Box2D units down from the center of the sprite.
To get the target, we start by subtracting the center of the monster from the eye’s position. This gives us a vector pointing in the direction of where the eye is relative to the center of the monster.
We then call normalize on that vector to make it unit length (1). This makes it so that we can multiply it by the desired length we want, and we’ll have a vector pointing in the desired direction, at the desired length. Once we have that, we just add it to the start point to get the final target.
The rest of the code just converts these Box2D coordinates to Cocos2D coordinates and stores it in the sprite’s MonsterData.
As the final step, add the following inside draw right after the call to DrawDebugData:
NSArray *monsters = [_lhelper spritesWithTag:MONSTER];
for (CCSprite *monster in monsters) {
MonsterData * data = [LevelHelperLoader customValueWithKey:@"data" forSprite:monster];
if (!data.canSeePlayer) {
glColor4ub(0, 255, 0, 255);
} else {
glColor4ub(255, 0, 0, 255);
}
ccDrawLine(data.eye, data.target);
}
Every time we draw the layer, we loop through the monsters, and look for the MonsterData key. We use the built-in ccDrawLine function to draw a line from the eye to the target. Note we color the line red if he can see the player, green otherwise. Right now it will always be green.
Compile and run, and you should now see lines drawn that indicate where the monsters are looking:
These will serve as the input we’ll send to Box2D to get it to do the ray casting.