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

Friday, February 20, 2015

Should I Still Be Using Vim in 2015? (Yes)

Last summer, I wrote a post about why I keep looking for something better than Vim, and why I keep coming back to it when it's time to actually sit down and get some work done. I've been editing the Vi way since 1992, but there are some things about Vim that I wish were better (easier to script, prettier to look at, better context-aware completion). So I occasionally wander away from Vim to see if things are actually better. The past few months have included the longest such wandering episode I've had in years. There are newer options for text editors out there, and they have passionate followers. Here's what I've seen on my little text editor excursion (in no particular order).

Atom
http://www.atom.io

Github's text editor was released as open source last summer after an invitation-only beta. It's off to an impressive start with a great community around it.

What did I like about it? 

  • Cross-platform. I used it on Mac and Windows (performs a little less smoothly on Windows).
  • Autocomplete2 plug-in is pretty good (though it was very slow looking up C completions, presumably calling out to clang).
  • Package manager built-in. 
  • Extremely easy to find and set configuration options via the settings pane search field.

Why didn't I stick with it? It has a huge memory footprint at runtime, and spawns at least two processes just to edit a single file. It gets sluggish if you try to open very large text files. Built first on webkit, then on Chrome, it is a platform suited for displaying rich content, but it does so at great expense.

Brackets
http://www.brackets.io

Adobe's text editor geared toward web developers has been around for a few years. It's community is less productive than Atom's. But the editor is solid.

What did I like about it?

  • Cross-platform.
  • Inline editors for things like CSS options and colors.
  • Live preview

Why didn't I stick with it? It does a great job in web development, but is just a regular editor for other types of projects. It is built on a similar platform as Atom (webkit or Chrome). I criticize it the the same as Atom - huge resources are required to edit a file. 

LightTable

This is a very interesting project. It aims to be a platform for evolving how we develop software. The problem is it's really slow.  Really slow. And unlike Atom, which is highly usable right out of the box, the cool features shown on the website require configuration to achieve, and the docs weren't helpful, and it just wasn't worth it. 

Sublime Text

Loads of people love this editor. I just don't. It didn't impress me that it could offer anything I didn't already have available to me in TextMate. Being a paid product, I couldn't bring myself to pay for it (which I believe you should, if you use it) when it didn't provide a clear advantage over other options I have that don't cost anything.

TextMate2

This venerable editor for Mac is fast, lightweight and has a loyal following. The fan base has waned, though, as the long-awaited TextMate2 took too long to come about. In reading posts and forums, it seems like most folks that bailed went toward SublimeText. But TextMate2 is now available as free software. 

What did I like about it?
  • You can tell its author(s) understand Unix workflows in the way bundles (plug-ins) work. It's a comfortable environment for me. 
  • It's fast, and lightweight.
  • Once you become familiar with the keyboard shortcuts, you can be very efficient navigating and editing code and text in general.
  • Rmate is nice - it's a ruby script you can install on remote machines you access that allows you to easily edit a remote file in TextMate on your local machine. Works great.

Why didn't I stick with it? At my day job, I have to work in a Windows environment very often, and it's not cross-platform. Also, although you can get very efficient at editing text, the keyboard shortcuts don't seem to have any rhyme or reason to the composition. I may be getting old, but I just couldn't remember the (seemingly) random key combinations to do things. I always had to have a cheat sheet handy.

Emacs

Yes, no editor walkabout would be complete without at least attempting (once again) to like Emacs. I just don't. Maybe it's the learning curve? Maybe I'm just really picky? I ended up using Aquamacs this time around, and was impressed by it. They managed to put a glossy finish on a stodgy old editor and have pulled it off really well. Their default settings make it more of a good Mac citizen, which I appreciated. But, in the end, I just didn't enjoy the learning process (same as always). One thing I liked about Emacs this time around, though, was the ability to sudo while editing a remote file via Tramp. I wish Vim's netrw could do that.

The Return to Vim

Anyone reading this article might infer that I'm just itching to leave Vim as soon as I find a viable alternative. There is some truth to that. I think everybody wants to improve their lives if they can. But anything that is going to aspire to replace Vim in my toolbox is going to have to overcome more than 20 years of familiarity with the Vi way of navigating text. 

For me, there is just no more efficient way to navigate and manipulate text, especially source code, than the Vi way. 

Vim is cross platform in a way that no other cross platform editor can be, except maybe Emacs, but the key chords are too burdensome (yes I'm that petty), even after remapping my CapsLock and Return keys. The problem all other editors have between Mac and Windows is the difference between Cmd and Ctrl. Yep - on Mac it's Cmd-S to save. On Windows it's Ctrl-S. Using an Apple keyboard, this is a big deal, because muscle memory causes me to hit Cmd-S, which on a PC keyboard usually ends up being Alt-S, and in VirtualBox, is Win-S. See my dilemma? In Gvim, or MacVim, I can use the common keystrokes if I want to, but I don't have to. Instead, I can use :w, which I don't even have to think about. I think 'Save' and my fingers have typed :w within milliseconds. Even in Emacs, after remembering what it is, I'd still have to hit Ctrl-x, Ctrl-s. That's just too much :)

No other editor provides the notion of Text Objects. That is one of Vim's secret killer features. Having the ability to address a chunk of text (a block of code, the contents within parentheses, array brackets, within quotes, etc) as a unit and the act on it (select it in visual mode, copy it, cut it, change it, use it as input to a search command, etc.) is a huge benefit.

And just like I said in that article last year, returning from my excursion to Vim feels like coming home. It's like drinking water, or breathing air. What I said last summer still applies...

Here's where they all fell short, and what [takes] me back to Vim every time...
  • hjkl for navigation (EDIT: 'hjkl' is really an identifier for the whole concept of using plain old keys to do what you need to do. No special modifier keys are necessary.)
  • / for searching (really, no other editor has made it this simple)
  • m to bookmark a spot, ' to hop back to it
  • and ctags
  • q to record a macro; @ to execute it again
  • 1G to go to the top of the file, G to go to the bottom (EDIT: commenters helped me relearn this one: gg will do the same thing, cutting out the need to hit the Shift key).
  • ^, $ to go to the front, or end, of a line.
  • Text Objects! Holy cow, if you're a Vim user and you're not using these, you're missing something special.
  • Platform ubiquity. (Mac, Linux, Windows - Vim works great on all the platforms I use day in and day out.)

What it boils down to, really, is that the above keys have become a part of me. They are so ingrained in my brain and reflexes, that I think of what I want done in the text and my hands just naturally do it for me. It's like drinking water. I don't have to think about how to go about bringing the cup to my mouth and tipping it just right. I just do it, and keep going on about my business.
Please don't hate me because I wander. It's just more validation that Vim can stand up to any competition.

I don't claim to have the most dazzling .vimrc. But if you're interested, you can see mine on github: https://github.com/kornerstoane/.vim. Some of it is really old. I'd love to hear about any recommendations for changes to it.