BuySellAds.com

My book Barcodes with iOS is nearing completion. Buy it now to get early access!
Our DNA is written in Objective-C
Jump

Physics 101 – UIKit app with Box2D for Gravity

Personally I was most at the edge of my seat at the “Voices that Matters Conference” in Seattle when Rod Strougo showed us how to make a physics-enabled game with Cocos2D and Box2D in under an hour. It really was as sexy as he sounds. Eros did what every good TV-cook would do, he had most things already prepared. On the flight home I wanted to see if I could just take the physics part (without Cocos2D) and make a UIKit app with it.

The goal of this experiment was to have a UIView with multiple square subviews of different sizes that would start falling as soon as the app starts. The first tricky part is how to add the latest version of the Box2D physics engine to your iPhone project. Then we need to mediate between the different units and coordinate systems of UIKit and Box2D. Finally when we got it all running, we also want to add  the current gravity vector to affect the boxes.

So, to get started we need to get the latest version of Box2D. It’s also included in Cocos2D, but for this tutorial we don’t care about getting the entire Cocos2D project. Box2D has a site on Google Code which tells us how to check out a read-only-copy of Box2D.

Adding Box2D

So we start a terminal, cd into a suitable directory and use this svn command to check out:

svn checkout http://box2d.googlecode.com/svn/trunk/ box2d-read-only

In the Box2D/Box2D subfolder of the folder this command creates you find all the C++ files that make up the engine.

Next we’ll create a new View-based iPhone app, I’m calling mine UIKitPhysics.

Now choose Add – Existing Files. Navigate to the innermost Box2D folder of the project, it’s 2 levels deep, and add it to your project.

If you want to be able to update it from the repository in the future, you need to not copy it to your project. If you don’t care about updating then you can copy the files. I choose to copy it because it makes setting of the header search path easier later on.

So now we have a Box2D group in our project. This is one way of adding it, the other would be to build a static library in Box2D and add this, but the above mentioned method looks easy enough for our first physics-enabled app.

There are still a couple more tweaks necessary to get it all working properly.

Box2D is written entirely in C++, but we are used to be working in Objective-C. Xcode usually uses the file extension to discern which compiler to use. Now if we are going to uses some function calls into Box2D those will be in C++ syntax. So we have two options to get the compiler to accept these as valid syntax. Either you modify the file extension to .mm or you force the compiler directly to interpret the affected files as C++. I think the latter is cleaner.

This tweak is necessary for all .m files which use C++ syntax and/or include the Box2D header. Since the default project probably has an include of the our view controller’s header in the app delegate, you need to make the same adjustment for the app delegate’s .m file. Get Info on the those files and change the file type to “sourcecode.cpp.objcpp”. This does not hurt, your normal Objective-C code can still be compiled normally.

Box2D uses angle brackets for all imports. So as the last step we need to add the location where the Box2D directory resides to the header search path. This can be done in the Project Settings or Target Settings. Since we want it to be in effect for all targets it’s preferable to set it in the Project Settings. Go to Project – Edit Project Settings, look for “Header Search Paths” and add this setting.

I’m using the environment variable ${PROJECT_DIR} so that it always works regardless where our project currently resides. Should you have Box2D outside of the project then you need to add the full path at this stage.

Now with this setting any #import with angle brackets will also work if the Box2D folder is inside your project root directory.

UIKit, meet Mr. Box2D

Now we are ready to add physical behaviors to our UIKit app. But first a couple of tidbits to understand the differences between those two.

Any two-dimensional object in Box2D has it’s own representation which is completely independent from how it is drawn. Nor does Box2D draw anything nor does it move anything. Therefore we have to create a physical representation for each view that we want to attach physics to. We have to set a timer to poll where the physical object have moved and rotated to at set intervals and update our UIKit views accordingly.

Another difference is in the coordinate system used. UIKit has the (0,0) point in the upper left, Box2D in the lower left corner. UIKit uses pixels as measurement, Box2D meters. So we have to find a good value of how many pixels should correspond to one meter in our physical model. I chose 16 pixels to make up a meter, and defined it at the top of our view controller.

#define PTM_RATIO 16

The model in which all physical calculations occur is called the “world”. We also need a timer to update our views frequently, so we’ll define two instance variables in our view controller.

#import <Box2D/Box2D.h>
 
@interface UIKitPhysicsViewController : UIViewController
{
	b2World* world;
	NSTimer *tickTimer;
}

And the appropriate dealloc in the implementation.

- (void)dealloc
{
	[tickTimer invalidate], tickTimer = nil;
	[super dealloc];
}

Next we implement a method to set up this world and define a couple of boundaries on all sides so that our objects won’t fall out.

-(void)createPhysicsWorld
{
	CGSize screenSize = self.view.bounds.size;
 
	// Define the gravity vector.
	b2Vec2 gravity;
	gravity.Set(0.0f, -9.81f);
 
	// Do we want to let bodies sleep?
	// This will speed up the physics simulation
	bool doSleep = true;
 
	// Construct a world object, which will hold and simulate the rigid bodies.
	world = new b2World(gravity, doSleep);
 
	world->SetContinuousPhysics(true);
 
	// Define the ground body.
	b2BodyDef groundBodyDef;
	groundBodyDef.position.Set(0, 0); // bottom-left corner
 
	// Call the body factory which allocates memory for the ground body
	// from a pool and creates the ground box shape (also from a pool).
	// The body is also added to the world.
	b2Body* groundBody = world->CreateBody(&groundBodyDef);
 
	// Define the ground box shape.
	b2EdgeShape groundBox;
 
	// bottom
	groundBox.Set(b2Vec2(0,0), b2Vec2(screenSize.width/PTM_RATIO,0));
	groundBody->CreateFixture(&groundBox, 0);
 
	// top
	groundBox.Set(b2Vec2(0,screenSize.height/PTM_RATIO), b2Vec2(screenSize.width/PTM_RATIO,screenSize.height/PTM_RATIO));
	groundBody->CreateFixture(&groundBox, 0);
 
	// left
	groundBox.Set(b2Vec2(0,screenSize.height/PTM_RATIO), b2Vec2(0,0));
	groundBody->CreateFixture(&groundBox, 0);
 
	// right
	groundBox.Set(b2Vec2(screenSize.width/PTM_RATIO,screenSize.height/PTM_RATIO), b2Vec2(screenSize.width/PTM_RATIO,0));
	groundBody->CreateFixture(&groundBox, 0);
}

We create a helper method that finds out the necessary parameters for a view we pass to it to create a representation in the physical world. Note that we use the center of views for their position because as soon as we apply a transform (which we need for the rotation) the frame position and size become unusable. Luckily the center property can be used whatever the transform is.

-(void)addPhysicalBodyForView:(UIView *)physicalView
{
	// Define the dynamic body.
	b2BodyDef bodyDef;
	bodyDef.type = b2_dynamicBody;
 
	CGPoint p = physicalView.center;
	CGPoint boxDimensions = CGPointMake(physicalView.bounds.size.width/PTM_RATIO/2.0,physicalView.bounds.size.height/PTM_RATIO/2.0);
 
	bodyDef.position.Set(p.x/PTM_RATIO, (460.0 - p.y)/PTM_RATIO);
	bodyDef.userData = physicalView;
 
	// Tell the physics world to create the body
	b2Body *body = world->CreateBody(&bodyDef);
 
	// Define another box shape for our dynamic body.
	b2PolygonShape dynamicBox;
 
	dynamicBox.SetAsBox(boxDimensions.x, boxDimensions.y);
 
	// Define the dynamic body fixture.
	b2FixtureDef fixtureDef;
	fixtureDef.shape = &dynamicBox;
	fixtureDef.density = 3.0f;
	fixtureDef.friction = 0.3f;
	fixtureDef.restitution = 0.5f; // 0 is a lead ball, 1 is a super bouncy ball
	body->CreateFixture(&fixtureDef);
 
	// a dynamic body reacts to forces right away
	body->SetType(b2_dynamicBody);
 
	// we abuse the tag property as pointer to the physical body
	physicalView.tag = (int)body;
}

In the view controller’s viewDidLoad method we create our world, call the addPhysicalBodyForView for each subview of our view and finally start the update timer.

- (void)viewDidLoad
{
	[super viewDidLoad];
 
	[self createPhysicsWorld];
 
	for (UIView *oneView in self.view.subviews)
	{
		[self addPhysicalBodyForView:oneView];
	}
 
	tickTimer = [NSTimer scheduledTimerWithTimeInterval:1.0/60.0 target:self selector:@selector(tick:) userInfo:nil repeats:YES];
}

So that we have something to actually make physical let’s go into Interface Builder and create a couple of subviews in UIKitPhysicsViewController.xib.

Finally, we need a timer function to update the position and rotation of our views.

-(void) tick:(NSTimer *)timer
{
	//It is recommended that a fixed time step is used with Box2D for stability
	//of the simulation, however, we are using a variable time step here.
	//You need to make an informed choice, the following URL is useful
	//http://gafferongames.com/game-physics/fix-your-timestep/
 
	int32 velocityIterations = 8;
	int32 positionIterations = 1;
 
	// Instruct the world to perform a single step of simulation. It is
	// generally best to keep the time step and iterations fixed.
	world->Step(1.0f/60.0f, velocityIterations, positionIterations);
 
	//Iterate over the bodies in the physics world
	for (b2Body* b = world->GetBodyList(); b; b = b->GetNext())
	{
		if (b->GetUserData() != NULL)
		{
			UIView *oneView = (UIView *)b->GetUserData();
 
			// y Position subtracted because of flipped coordinate system
			CGPoint newCenter = CGPointMake(b->GetPosition().x * PTM_RATIO,
						self.view.bounds.size.height - b->GetPosition().y * PTM_RATIO);
			oneView.center = newCenter;
 
			CGAffineTransform transform = CGAffineTransformMakeRotation(- b->GetAngle());
 
			oneView.transform = transform;
		}
	}
}

Now we’re set. If we start the app in simulator we see all views drop to the bottom and end up in a static heap. We remember from physics that all objects fall at the same rate in a vacuum, such is the case in this simulation. If we wanted to have air resistance to play a role we probably could add additional forces to the objects in addition to the global gravity, but for sake of simplicity we’ll be satisfied with this result for the time being.

BONUS: And Now with Acceleration

As bonus chapter of this article we want to add the capability to have real gravity affect our view. For this purpose we setup and start an accelerometer and set our view controller as the delegate. This we add at the end of the viewDidLoad method. To get around the warning that your class does not the UIAccelerometerDelegate protocol we add it in angle brackets to the interface declaration:

//Configure and start accelerometer
[[UIAccelerometer sharedAccelerometer] setUpdateInterval:(1.0 / 60.0)];
[[UIAccelerometer sharedAccelerometer] setDelegate:self];

And in the delegate method we simply change the gravity vector of the physical world.

- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
{
	b2Vec2 gravity;
	gravity.Set( acceleration.x * 9.81,  acceleration.y * 9.81 );
 
	world->SetGravity(gravity);
}

One more addition is necessary to get this fully working. In our construction of the physical world we stated that we would allow objects to be sleeping. This lets the Box2D send objects to sleep if they don’t move anymore and don’t get hit by other objects. This makes perfect sense if gravity is always coming from the same direction. But since the gravity vector can now change up to 60 times per second we need to disable sleeping. Otherwise all objects will get stuck on one side if you hold the iPhone still long enough.

// Do we want to let bodies sleep?
// This will speed up the physics simulation
bool doSleep = false;

Note that the accelerometer only sends events on an a physical device. In simulator only initially set gravity vector takes effect. Another funny effect is if you hold the iPhone face up then you can even make the objects weightless.

Conclusion

With little work it is possible to add a fully fledged albeit two-dimensional physics engine that can take control over some UIViews and have them behave as if they where real physical objects. This should allow for a couple of simple games using physics to spice up the interaction.

For anything more you’ll probably have to switch to using OpenGL for the drawing part or if that’s too complicated use the fully fledged Cocos2D engine.

But let me say this: “I love Physics”. so I’m really anxious to see what you can make of this tutorial. Show me what you got!


Categories: Recipes

%d bloggers like this: