Welcome to the first “Geek” category post. We launched our first game, Teragati, on February 16, 2010, and in the weeks since then we’ve been spending more time on marketing. Time to take a break and talk a little about engineering!
Teragati is based on cocos2d for iPhone, which is a popular open-source iPhone/iPod Touch game library. We used version 0.90 but backported a few improvements from later versions as they became available. Teragati also uses the physics library Box2D, which is helpfully integrated with current versions of cocos2d. There are a few great tutorials on the web to help with initial cocos2d-Box2D integration, but because cocos2d has been moving so fast, they’re typically out of date by the time you need them. I also ran into a few issues throughout game development that others will probably hit, too. To help other developers looking to get started, here’s our main loop. We’ve removed a few parts that don’t serve any educational purpose.
-(void) tick:(ccTime)dt {
Player *player = (Player *)[self getChildByTag:kPlayerTag];
if (player != nil && player.isDead) {
// flash the screen
// start game-over countdown
}
if (gameStats.gameOver) {
[[SoundPlayer get] stopAllLoopingSounds];
[[CCDirector sharedDirector] replaceScene:[TitleLayer scene]];
return;
}
// This needs to happen before we convert body
// coordinates because otherwise we'll possibly
// create a body and display its sprite before the
// sprite coordinates have been assigned at least
// once. In most cases this would cause the sprite
// to flicker for one frame down in the lower-left corner.
[gameStats tick:dt];
if (!gameStats.gameNearOver && !gameStats.gameOver) {
[self handleLevelLogic:dt];
[currentLevelLogic tick:dt];
}
// http://gafferongames.com/game-physics/fix-your-timestep/
const int32 velocityIterations = 8;
const int32 positionIterations = 1;
world->Step(dt, velocityIterations, positionIterations);
world->ClearForces();
// Iterate over the bodies in the physics world
b2Body *bodiesToDelete[128];
int b2d = 0;
for (b2Body* b = world->GetBodyList(); b != NULL;
b = b->GetNext()) {
if (b->GetUserData() != NULL) {
Actor *actor = static_cast<Actor *>(b->GetUserData());
if (actor.isHibernating) {
continue;
}
if (actor.isDead) {
if (actor.isRecyclable) {
[actorFactory returnActor:actor];
} else {
bodiesToDelete[b2d++] = b;
[actor cleanUp];
}
continue;
}
[actor tick:dt];
}
}
for (int i = 0; i < b2d; ++i) {
world->DestroyBody(bodiesToDelete[i]);
}
// One of our local variables might have been invalidated,
// so we get it again.
player = (Player *)[self getChildByTag:kPlayerTag];
if (player != nil) {
evCamera.targetPosition = b2Vec2(player.body->GetPosition());
}
[evCamera tick:dt];
starLayer.cameraPosition = evCamera.globalPosition;
// Now we loop again. See comments above about flicker
// in lower-left corner.
for (b2Body* b = world->GetBodyList(); b != NULL;
b = b->GetNext()) {
if (b->GetUserData() != NULL) {
Actor *actor = static_cast<Actor *>(b->GetUserData());
if (actor.isHibernating) {
continue;
}
[actor updateTransform:evCamera];
}
}
}
You’ll recognize some of this from the cocos2d-Box2D template that comes with cocos2d. A few quick points to get out of the way:
- We didn’t have to keep a reference to the Player object around at all times; that’s why we start out the loop by grabbing it from the root node (which in this case happens to be ourselves). We certainly could have kept the reference as a member variable, but midway through development a light went off in my head that the cocos2d tagging feature gave us the freedom to kill CCNodes without needing to worry about who still had dangling references to them. Instead, consumers ask for the reference each time, and properly handle the case where it’s gone.
- I never experimented with changing the Box2D iteration values. There was never a need to decrease them (presumably to increase speed) or increase them (presumably to improve simulation accuracy).
- The “world->ClearForces();” change is necessary for newer versions of Box2D. Otherwise your forces accumulate, and the first time you update your Box2D version, your objects quickly accelerate off the screen.
- I’m not sure whether it’s strictly necessary to avoid destroying Box2D bodies while looping through them, but in order to keep the doubly-linked list working properly, I’d have had to make the code awkward. So we accumulate the bodies that are done and then delete them in a batch afterward.
The “isRecyclable” concept at line 43 refers to certain kinds of Actors (which bind CCSprites, Box2D bodies, and character logic together) that can be reused. A perfect example is the big rock, which appears at the top of the screen, floats to the bottom, and is never seen again — try killing a missile head-on so your ship bounces backward and you’ll note a big space void behind you. Rather than constantly allocing and releasing the BigRock Actors, we hide them offscreen in an ActorFactory and then recycle them when needed at the top of the screen. Most Actors are recyclable. Exceptions are transient objects such as the blooms of particles that appear when the ship runs over a powerup.
As a side note, I suspect that Objective C “protocols” are a better language-specific way for interfaces to express that they can or can’t do certain things. I’ve been meaning to look into what a protocol is. Maybe next project.
The main thing I wanted to share about this loop was the ordering of certain major operations:
- Create new objects.
- Let each object run its individual logic.
- Map Box2D positions to screen positions.
During early development, I frequently rearranged loop logic to suit syntactic needs (e.g., variable declaration). I occasionally had problems where an object would come into existence and blink for a moment at an arbitrary spot on the screen. What was happening was the object was created and displayed without getting a chance to get its own bearings at least once. This was a real problem when one Actor created other Actors as needed (e.g., a collision or a timer). So the object-creating code got extensively refactored into LevelLogic classes (which are in charge of creating rocks, powerups, and missiles), and they always run first in the main loop. Then individual Actors get their slice of time, possibly flagging that other objects should be created on the next loop iteration. Finally, after everyone is in place, we use the camera to map Box2D world positions to screen coordinates.