We need your burps!

Doodle Burp is available now on the App Store now, and we need your burps! That’s right, the next Doodle Burp update will include new and exciting burps, and one of them might be yours! Record your best burp in WAV or MP3 format (please try to keep the background noise down), and email it to burps@attachmentcomputing.com. Include the name you’d like us to use when we thank you in the credits. We won’t be able to accept every burp, and submitted burp recordings become our property.

Your burps can make a difference. Send them in today!

Available on the iPhone App Store

 

Teragati’s main loop

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:

  1. Create new objects.
  2. Let each object run its individual logic.
  3. 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.

 

Just added to the App Store description:

SPECIAL! We love retro video games from the 1980s (can’t you tell from Teragati?), so we had to celebrate the upcoming sequel to our favorite 1982 light-cycle movie (and game) Tron. If you have the highest verified Teragati score on OpenFeint by noon Pacific time on July 15, 2010, we’ll buy Tron Legacy 3D movie tickets for you and a friend! Write us at our support email address below for full details. Void where prohibited, etc. Good luck!

So here’s the deal:

  • Play Teragati. Get good at it. Make sure you’re using OpenFeint and that your scores are appearing on the Highest Score Leaderboard.
  • On July 15, 2010, at noon Pacific time, we’ll note the OpenFeint account that has the highest verified Teragati score. We’ll be using our top-secret algorithms to eliminate faked scores.
  • We’ll arrange an OpenFeint IM with that account, where we’ll get contact information. We’ll then send that account $40 USD, which we are pretty sure should cover two tickets to go see Tron Legacy 3D anywhere in the world. Note that the movie is scheduled to open in December 2010 (see the trailer now! It rocks!). The winner doesn’t have to spend the money on the movie, but we’ll be very, very disappointed otherwise.
  • Legalese: Attachment Computing LLC (“AC”) makes the final decision who wins. AC’s decision is final. Total amount awarded is $40. Ties or multiple winners split the $40 evenly. AC has no affiliation with Tron Legacy or its makers; we’re just big fans of the movie and have a feeling that Teragati players are, too. In the event that AC can’t determine a winner, AC will pick one at random from the OpenFeint high-score leaderboard. All winners must be age 18 or older. Winners are responsible for all taxes, fees, popcorn, parking, and gas. AC reserves the right to cancel this contest at any time. Void where prohibited. Employees of AC aren’t eligible, and they’ll all be going to see the movie anyway.

 
Page 1 of 212
© 2010 Attachment Computing