Showing posts with label Game Design. Show all posts
Showing posts with label Game Design. Show all posts

Wednesday, February 25, 2015

How I Made It: TreeDudes Launch iOS Game

The Game: Arrange various tools in Rube Goldberg fashion to help the character land on a target after being flung from the launch platform. (See it in iTunes)

Tools

Cocos2d-iPhone: This is a great library. I would have used SpriteKit but it didn't exist when I started this project.

Chipmunk Physics: I bought the license so I could have the Objective-C library. Was it worth the $250? Yes. I believe it was. I didn't have to waste time mentally "bridging" between Obj-C and C. It's a high quality library.

iDraw: Vector drawing software. All artwork is drawn in iDraw. I started with InkScape, but I found iDraw was easier to use and the file format was easier to read. What? What could be easier than SVG? Well, I generated my physics environment by drawing it as a layer in the file, then parsing it and writing Objective-C to build the Chipmunk physics objects at runtime. InkScape uses a lot of transforms, and reverse engineering those got really hard. iDraw uses a zipped plist as a file format, which made it really easy to crack it open and see what it was doing. Its internals are very easy to read and understand, and made my code generator a lot easier to write. It was definitely worth the $25 on the App Store.

Audacity: Sound effects were recorded using things around the house (like a finger in the cheek for the balloon popping sound), recorded on an iPod Touch. I used Audacity to filter out the background noise and tweak the pitches and things to get the right sound.

GarageBand: My wife came up with the main menu audio loop, which I believe is just a pre-canned Apple Loop with a bit of a rhythm added in. She used the iPad version.

bmGlyph: Used for creating bitmapped fonts. I started by doing all my text as graphics in iDraw, but when I went add Spanish localization, I had to switch to text. It was also worth the $10 on the App Store. And the developer is very responsive. I e-mailed him with a couple of questions and a problem, and he asked me to send a sample file so he could see what I was seeing. He sent back some suggestions to accomplish what I was trying to do.

TexturePacker: Used to create sprite sheets to optimize my graphics footprint (a little).

GraphicsMagick: Command line tool used to crop images and prepare them for loading into sprite sheets.

Xcode: This is the de facto for Apple development, of course. I am a Vi-guy, so I used the Xvim plugin so I could feel a little more at home.

iOS Simulator: I used this mainly to debug the In-App purchases and the GameCenter interaction. It was very difficult debugging the gameplay, since the Cocos2d and Chipmunk animations were run by the simulator and were very slow.

Languages

Objective-C: All of the game is written in Obj-C. This was my first exposure to it and after the learning curve, I would prefer Obj-C over C++ for almost any development effort. (Note: I am excited by Swift, and am using it on a current project that is just getting underway.)

Python: My supporting scripts are written in Python. idraw_to_gamelevel is the script that cracks open the iDraw files and generates the Objective-C classes that implement each level. export is the script that took the manually exported png and 2x png output from iDraw, cropped out individual images, created sprite sheets, and copied them over to the right place in the Xcode project. I decided to export the 2x and 1x images separately from iDraw. I used GraphicsMagick to crop, but the quality of the scaled files (downscaled from 2x to 1x) wasn't as good.

Workflow

Once I finally zeroed in on the game concept, it took about year and a half of mornings, evenings, weekends, and burning out a few times, to get it ready to submit to the App Store.  There was a lot of learning I had to do, and I'm convinced I did a lot of things wrong. There is some major technical debt rolled into this app that I will definitely have to refactor if I do any more work on it.

I started by getting a basic GameLayer class in place. This is the class that sets up the level (menus, scenery, etc) and then is called by Cocos2d each frame. It would call Chipmunk to calculate all the physics. It also implements all the Chipmunk callbacks for collision handling. It was basic at first, then evolved into much more and took on a lot of technical debt (i.e. things I should have done better, but didn't) because I wasn't disciplined enough to refactor as I went. I just got into heads-down mode and made stuff work.

With the GameLayer in place, I could start adding levels. To create a level, I would draw it out in iDraw, then add the physics layer. The physics layer is just line segments and shapes with specific attributes that would point to more details in a JSON file. The code generator would load both files at the same time and use the attributes to generate line segments and shapes with certain collision behaviors in an Obj-C class that is loaded by the GameLayer for each level (see below for an example).

The rest is just the grinding work of producing artwork (I am not an artist, but would someday love to take the time to develop that skill), and adding functionality to the GameLayer to support new things I wanted to add to the game.

Would I do it differently if I started over?

Yes, I would. While Cocos2d and Chipmunk are fantastic libraries, the workflow around SpriteKit is just so much easier. In order to support Android, I have considered whether this game would work well as a hybrid web app using Cocos2d and Chipmunk Javascript libraries on top of the Apache Cordova framework. It's not a complex game, but some of the levels have a couple thousand line segments and other physics objects. The ChipmunkJS demo app for the Logo Smash, which also has a couple thousand objects, is pretty slow.

Have I made any money?

A little. $40, roughly. About half in early sales to friends and family, and the rest in ads after making it a free download with In-App purchase. $0 from In-App purchases, by the way. I'm not sad about it. I wrote about it recently.

Screenshots

Here are a few screenshots of my Physics generation...

Level 1
Level 1 Scene

Level 1 With Physics
Level 1 Scene with Physics Layer Visible

Level 5 Physics
Level 5 Physics Layer

And finally, here is the code generated for Level 1.

Level1.h
// AUTO-GENERATED: LEVEL 1
#import "GameLevel.h"
@interface GameLevel_1 : GameLevel
@end

Level1.m
// AUTO-GENERATED: LEVEL 1
#import "Level1.h"
#import "Character.h"
#import "FieldObject.h"
#import "CollisionTypes.h"
#import "Screen.h"
#import "Animation.h"

#define POSSIBLY_UNUSED(A) if (A)

@implementation GameLevel_1
- (bool) debug { return NO; }

- (id) init {
  if ((self = [super init])){
    self.ID = 1;
    self.gameSize = CGSizeMake(SCRNX(2048),SCRNY(1024));
    //Gems/Balloons
    [self.gems addObject:[GemDefinition gem:0 at:ccp(SCRNX(936.102388),SCRNY(599.268964))]];
    [self.gems addObject:[GemDefinition gem:0 at:ccp(SCRNX(1015.272542),SCRNY(599.268964))]];
    [self.gems addObject:[GemDefinition gem:0 at:ccp(SCRNX(1094.442735),SCRNY(599.268964))]];
    [self.gems addObject:[GemDefinition gem:0 at:ccp(SCRNX(1051.625752),SCRNY(679.538748))]];
    [self.gems addObject:[GemDefinition gem:1 at:ccp(SCRNX(973.155487),SCRNY(679.538748))]];
    [self.gems addObject:[GemDefinition gem:2 at:ccp(SCRNX(1049.410717),SCRNY(837.354861))]];
    [self.gems addObject:[GemDefinition gem:0 at:ccp(SCRNX(1173.612969),SCRNY(599.268964))]];
    [self.gems addObject:[GemDefinition gem:1 at:ccp(SCRNX(1130.096015),SCRNY(679.538748))]];
    [self.gems addObject:[GemDefinition gem:1 at:ccp(SCRNX(1007.524312),SCRNY(759.808502))]];
    [self.gems addObject:[GemDefinition gem:1 at:ccp(SCRNX(1096.744143),SCRNY(759.808483))]];

  }
  return self;
}

- (ChipmunkSpace*) createSpace {
  ChipmunkSpace *space = [[ChipmunkSpace alloc] init];
  space.gravity = cpv(0.000000,-400.000000);
  space.damping = 0.900000;
  ChipmunkSegmentShape *wall;                      POSSIBLY_UNUSED(wall);
  //LaunchPad
  self.launchPad = [[ChipmunkSegmentShape alloc] initWithBody:[space staticBody] from:cpv(SCRNX(-2.000000),SCRNY(597.000000)) to:cpv(SCRNX(230.000000),SCRNY(597.000000)) radius:0];
  self.launchPad.elasticity = 0;
  self.launchPad.friction = 0;
  self.launchForce = cpv(SCRNX(15.000000),SCRNY(0.000000));
  self.launchPad.collisionType = CT_LaunchPadSegment;
  [space add:self.launchPad];
  self.launchSensor = [[ChipmunkSegmentShape alloc] initWithBody:[space staticBody] from:cpv(SCRNX(-2.000000),SCRNY(597.000000)) to:cpv(SCRNX(230.000000),SCRNY(597.000000)) radius:0];
  self.launchSensor.sensor = YES;
  self.launchSensor.collisionType = CT_LaunchPad;
  [space add:self.launchSensor];
  //Target(s)
  self.target = [[ChipmunkSegmentShape alloc] initWithBody:[space staticBody] from:cpv(SCRNX(1815.437917),SCRNY(121.097526)) to:cpv(SCRNX(1915.267717),SCRNY(121.097526)) radius:2];
  self.target.elasticity = 0;
  self.target.friction = 2.5;
  self.target.collisionType = CT_Target;
  [space add:self.target];
  //Obstacles
  Sensor *sensor;                                  POSSIBLY_UNUSED(sensor);
  ActiveFieldObject *afo;                          POSSIBLY_UNUSED(afo);
  RotationAnimation *rotation;                     POSSIBLY_UNUSED(rotation);
  OscillationAnimation *oscillation;               POSSIBLY_UNUSED(oscillation);
  FollowPathAnimation *followPath;                 POSSIBLY_UNUSED(followPath);
  wall = [[ChipmunkSegmentShape alloc] initWithBody:[space staticBody] from:cpv(SCRNX(223.266024),SCRNY(584.583267)) to:cpv(SCRNX(209.997305),SCRNY(509.934742)) radius:4.000000];
  [wall setPrevNeighbor:cpv(SCRNX(223.266024),SCRNY(584.583267)) nextNeighbor:cpv(SCRNX(209.997305),SCRNY(509.934742))];
  wall.elasticity = 0.000000;
  wall.friction = 1.000000;
  [space add:wall];
  wall = [[ChipmunkSegmentShape alloc] initWithBody:[space staticBody] from:cpv(SCRNX(209.997305),SCRNY(509.934742)) to:cpv(SCRNX(200.227196),SCRNY(414.490535)) radius:4.000000];
  [wall setPrevNeighbor:cpv(SCRNX(209.997305),SCRNY(509.934742)) nextNeighbor:cpv(SCRNX(200.227196),SCRNY(414.490535))];
  wall.elasticity = 0.000000;
  wall.friction = 1.000000;
  [space add:wall];
  wall = [[ChipmunkSegmentShape alloc] initWithBody:[space staticBody] from:cpv(SCRNX(200.227196),SCRNY(414.490535)) to:cpv(SCRNX(192.135846),SCRNY(195.342987)) radius:4.000000];
  [wall setPrevNeighbor:cpv(SCRNX(200.227196),SCRNY(414.490535)) nextNeighbor:cpv(SCRNX(192.135846),SCRNY(195.342987))];
  wall.elasticity = 0.000000;
  wall.friction = 1.000000;
  [space add:wall];
  wall = [[ChipmunkSegmentShape alloc] initWithBody:[space staticBody] from:cpv(SCRNX(192.135846),SCRNY(195.342987)) to:cpv(SCRNX(186.883136),SCRNY(34.529300)) radius:4.000000];
  [wall setPrevNeighbor:cpv(SCRNX(192.135846),SCRNY(195.342987)) nextNeighbor:cpv(SCRNX(186.883136),SCRNY(34.529300))];
  wall.elasticity = 0.000000;
  wall.friction = 1.000000;
  [space add:wall];
  wall = [[ChipmunkSegmentShape alloc] initWithBody:[space staticBody] from:cpv(SCRNX(1798.693016),SCRNY(38.837067)) to:cpv(SCRNX(1816.270816),SCRNY(93.071957)) radius:4.000000];
  [wall setPrevNeighbor:cpv(SCRNX(1798.693016),SCRNY(38.837067)) nextNeighbor:cpv(SCRNX(1816.270816),SCRNY(93.071957))];
  wall.elasticity = 0.000000;
  wall.friction = 1.000000;
  [space add:wall];
  wall = [[ChipmunkSegmentShape alloc] initWithBody:[space staticBody] from:cpv(SCRNX(1816.270816),SCRNY(93.071957)) to:cpv(SCRNX(1817.306316),SCRNY(119.317667)) radius:4.000000];
  [wall setPrevNeighbor:cpv(SCRNX(1816.270816),SCRNY(93.071957)) nextNeighbor:cpv(SCRNX(1817.306316),SCRNY(119.317667))];
  wall.elasticity = 0.000000;
  wall.friction = 1.000000;
  [space add:wall];
  wall = [[ChipmunkSegmentShape alloc] initWithBody:[space staticBody] from:cpv(SCRNX(1931.003741),SCRNY(37.626398)) to:cpv(SCRNX(1918.337241),SCRNY(61.221718)) radius:4.000000];
  [wall setPrevNeighbor:cpv(SCRNX(1931.003741),SCRNY(37.626398)) nextNeighbor:cpv(SCRNX(1918.337241),SCRNY(61.221718))];
  wall.elasticity = 0.000000;
  wall.friction = 1.000000;
  [space add:wall];
  wall = [[ChipmunkSegmentShape alloc] initWithBody:[space staticBody] from:cpv(SCRNX(1918.337241),SCRNY(61.221718)) to:cpv(SCRNX(1913.339041),SCRNY(120.294938)) radius:4.000000];
  [wall setPrevNeighbor:cpv(SCRNX(1918.337241),SCRNY(61.221718)) nextNeighbor:cpv(SCRNX(1913.339041),SCRNY(120.294938))];
  wall.elasticity = 0.000000;
  wall.friction = 1.000000;
  [space add:wall];
  wall = [[ChipmunkSegmentShape alloc] initWithBody:[space staticBody] from:cpv(SCRNX(-2.723418),SCRNY(46.361666)) to:cpv(SCRNX(2497.276587),SCRNY(46.361666)) radius:4.000000];
  [wall setPrevNeighbor:cpv(SCRNX(-2.723418),SCRNY(46.361666)) nextNeighbor:cpv(SCRNX(2497.276587),SCRNY(46.361666))];
  wall.elasticity = 0.000000;
  wall.friction = 1.000000;
  [space add:wall];
  //Gems
  for (GemDefinition* gem in self.gems){
    ChipmunkBody *body = [ChipmunkBody staticBody];
    body.pos = gem.position;
    ChipmunkShape *shape;
    int size = 40 - (5 * gem.type);
    shape = [ChipmunkPolyShape boxWithBody:body width:SCRNX(size) height:SCRNY(size)];
    shape.sensor = YES;
    shape.collisionType = CT_Gem;
    gem.shape = shape;
    [space add:shape];
  }
  //ActiveFieldObjects
  for (ActiveFieldObject *obj in self.gameConfig.activeFieldObjects){
    obj.body = [obj.definition createInSpace:space position:obj.position angle:obj.settings.rotation rogue:NO];
  }
  return space;
} // end of createSpace()

- (void) addSceneryToNode:(CCNode*)node staticNode:(CCNode*)staticNode {
  CCSprite *image;                                       POSSIBLY_UNUSED(image);
  image = [self loadImage:@"L1-Bkg-0-0.png"];
  image.anchorPoint = ccp(0,0);
  image.position = ccp(SCRNX(0.000000),SCRNY(0.000000));
  [node addChild:image z:-999999];
  image = [self loadImage:@"L1-Bkg-1024-0.png"];
  image.anchorPoint = ccp(0,0);
  image.position = ccp(SCRNX(1024.000000),SCRNY(0.000000));
  [node addChild:image z:-999999];
}
@end

Thursday, May 1, 2014

A Simple State Pattern Applied to Game Design

When it comes writing software, less is better, generally speaking. The fewer lines of code you have, the lower the chance of doing something wrong. For me, this is most obvious when it comes to the number of if-statements in my code.

Here's an idea for reducing the number of if-statements used to transition between game states. It's not a new idea. In fact, it's an old design pattern from the GoF Design Patterns book.

Toggalight, my second game, is small in scope, but has the usual need of presenting different UI elements, and responding to user interaction differently depending on whether the game is running, paused, or over.

When I started on this project, I decided I wasn't going to stick myself into an if-statement quagmire again. So I broke out the trusty State pattern and went to work.

Overview

The game has four buttons that cause state changes: Start, Pause, Resume, Start Over. The GameScene class implements a library of functionality that is available to both the view controller and the various state classes.

Note: I'm using a python-like pseudo-code, and will be abstracting most of the details so we can focus on the state structure and transitions. The game is actually written for iOS in Objective-C, using SpriteKit.

BaseGameScene

This is the base class for all game states, and defines the interface expected by the GameScene.

abstract class SceneState
  property gameScene

  // state management - empty base class methods that don't do anything
  method enterState()
  method exitState()

  // Events - empty base class methods that don't do anything
  method onFrameUpdate()
  method onLightTouched()

  method onStartButton()
  method onPauseButton()
  method onResumeButton()
  method onStartOverButton()


RunningState

I'll spare you the details of all the states, but here's the gist of what's in the most complex state, the one that's active while the game is being played.

class RunningState : SceneState

  method enterState()

    // set up UI for state
    gameScene.pauseButton.show()
    gameScene.startButton.hide()
    gameScene.startOverButton.hide()
    gameScene.resumeButton.hide()
    gameScene.helpButton.hide()
    gameScene.calibrateButton.hide()
    gameScene.highScoreLabel.hide()

    // Physics are different between running and game over states
    gameScene.setGameRunningPhysics()

    // If this is a new game (not a resume), layout a new field
    // and reset the clock.  Yes, I know it's a nasty if statement.
    // I'll probably refactor this in the next release.
    if (gameScene.shouldReset)
       gameScene.generateNewField()

    // Unpause the scene
    gameScene.frozen = false


  method exitState()
    // Do nothing. The base class defines an empty method for this

  method onFrameUpdate()
    // Respond to device motion (accelerometer and gyroscope)
    motion = os.getCurrentMotion()
    gameScene.physics.gravity = Vector(motion.roll, motion.pitch)

    // Check time remaining
    // Yup, more refactoring needed here, too. onClockExpired would be
    // a good event to handle at the state level.
    if gameScene.clock.isExpired()
      gameScene.transitionToState(gameScene.gameOverState)

  method onLightTouched(light)
    // Toggle it off/on
    light.on = not light.on
    gameScene.updateScore()

    // If all the lights are turned on, layout a new field
    // This if-statement seems appropriate, actually.
    if light.container.allOn()
      gameScene.generateNewField()


  method onPauseButton()
    gameScene.transitionToState(gameScene.pausedState)


You get the idea. The other states follow the same pattern, doing what they're supposed to do when the events are triggered.

GameScene

Here are the relevant parts of the GameScene class.

class GameScene

  property preStartState
  property runningState
  property pausedState
  property gameOverState

  property currentState

  method init()
    // Lots of code to set up the scene.

    preStartState.gameState = self
    runningState.gameState = self
    pausedState.gameState = self
    gameOverState.gameState = self

    transitionToState(preStartState)

  method transitionToState(newState)
      // Objective-C lets me get away with not checking for 
      // null first. Lazy? yes.
      currentState.exitState()
      currentState = newState
      currentState.enterState()

  method onStartButton()
    currentState.onStartButton()

  method onStartOverButton()
    currentState.onStartOverButton()

  method onPauseButton()
    currentState.onPauseButton()

  method onResumeButton()
    currentState.onResumeButton()

  // This method is called by the "os"
  method frameUpdate()
    currentState.onFrameUpdate()

There you have it. PreStartState goes to RunningState. RunningState goes to PausedState and GameOverState. PausedState goes to RunningState, setting the shouldReset flag or not depending on whether the Resume or Start Over button was pushed. GameOverState goes to RunningState.

Each state sets up the UI according to its context. In the case of this game on iOS, touch events are translated by the OS into the "onButton" events in the view controller, which delegates the events to the GameScene, which in turn delegates them to the current SceneState, which is where the knowledge of what to do is programmed.

That's the essence of the State design pattern applied to my game architecture. It's not necessarily revolutionary, but it's made a world of difference in the way I track down problems and solve them.