Showing posts with label Tools. Show all posts
Showing posts with label Tools. 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

Saturday, June 28, 2014

Why I can't stop using Vim

In 1992 or 1993, my brother-in-law hired me to help him with some maintenance on a software system he had written for a customer. It ran on SCO Xenix.

"Hey, Bro, why doesn't 'EDIT' work on this computer?" I was a DOS rookie back in those early days, having just started my major in Computer Science.

"Um, there's no EDIT command on Xenix. Let me introduce you to vi. You use hjkl to move around, plus a few other keystrokes. Once you get used to it, it's not that bad." I swear he had a sly grin on his face as he said that, but he was serious. Vi was what I was going to have to use. Only now, as I write this, do I recall that he was, at that time, working in Windows with a shiny graphical editor. Oh, well, he was the boss, after all.

Anyway, I learned to move around pretty quickly, hitting 'i' to start typing, and 'Esc' to move around again. I wouldn't say I was a wizard, but I got the work done. We would tap away on Wednesday nights and Saturday mornings in our work sessions, while our wives played Super Mario Brothers out in the front room. Good times. I really miss it sometimes.

That was the side job. The day job kept me programming on OS/2 and some Windows. The stock editor on OS/2 was a decent editor - you typed text, and it saved it just fine. I cranked out many, many lines of code using that editor.

We're programmers. Editing text is the very essence of what we do. We get excited about these sorts of things.

Fast forward to 1997. My brother-in-law once again helped me get my foot in the door at a small telecommunications company in Phoenix. My first workstation was a Sun SPARCstation 20, running Solaris. Cool, I thought, I can use vi to get going. But by now, everyone was using UltraEdit on their Windows machines. I didn't have one of those, but I really needed the productivity boost of a better editor. A little Yahoo searching turned up some vi clone called Vim. There was a new release of it, version 5, and it came with syntax highlighting! Take that, UltraEdit!

This is getting long... Let's just say, I used Vim for all my editing on that SPARC station. And when I finally got a Windows PC, I was elated to find that there was a Windows version of Vim, with a GUI and everything!

Vim and I have had a bit of a fickle relationship over the years. Although it does an amazing job helping you type text, it's not flashy, and it's not perfect, and because of those imperfections, I strayed from time to time, looking for something to fix those imperfections, and still give me the second-nature editing reflexes I had come to enjoy from Vim.

Here's a list of the things that have caused me to look beyond Vim.
  • viml - Vim's built in scripting language is tedious to work in. Some folks have done some amazing things with it, but, try as I have, I can't get myself to enjoy it.
  • Glossy finish - I admit, I'm guilty of a little editor envy when I see some of the prettier interfaces out there, especially these days.
  • Emacs's Org Mode - this is such a killer emacs app. Others have tried to replicate it for Vim. Someday.
  • Context-aware code completion - OmniComplete provides a decent framework, but nobody has really matched the power of Visual Studio, Eclipse, or Xcode.

Yes, I have strayed, and flirted with many other editors...

  • JEdit (Ugh, looks like a Java Swing App, and too much clutter everywhere)
  • Emacs (Love the features and the eLISP, but can't tolerate the myriad key chords needed to navigate around my files.)
  • Emacs + Evil Mode (Evil mode is REALLY good. The only thing that made me frown was poor code navigation via ETAGS.)
  • J (A tiny little Java based editor, built around ABCLisp from Armed Bear. I really loved this editor, but it fell behind in features, and I needed more power.) EDIT: This editor is still alive (somewhat) on github: https://github.com/kevinkrouse/j/tree/master/j. Thanks to bokchoi on ihackernews.com for the link.
  • Programmers Notepad (Decent editor, but not enough features).
  • Editra (a Python/WxWindows based editor, with Vi emulation. But it also lacked things like smart indent and other features I have come to take for granted.)
  • Notepad++ (A very respectable editor. Tons of features. But for some reason, I just don't enjoy using it. Maybe it's all the visual clutter around the borders of the screen?)
  • Visual SlickEdit (I actually bought a license for this once, about a decade ago. I liked using it, but I could only afford one platform, and there are lots of free editors that do most of the same things. With Vim in my toolbox, I'll never pay $300 to edit text.)
  • TextMate (When on a Mac, it had to be tried. The free TextMate 2 is a great editor, and I flirted with this one the longest. Were it not for the things I'm about to mention below, I'd be using this editor as much as possible.)
  • A whole slew of respected MacOS based editors: Kod, Smultron, SublimeText, TextWrangler, etc.

Here's where they all fell short, and what took me back to Vim every time...
  • hjkl for navigation
  • / 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
  • ^, $ 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.

THAT's what keeps me coming back to vim.

Yes, it still has its imperfections. I end up using IDE's for a large part of the work I do, and they provide a lot of what I wish vim would do better. But there are some plugins that bring the above features into those environments...

For Eclipse, I use the Vrapper plugin. Works great!

For Xcode, I have used XVim. It's not perfect, and crashes Xcode in a couple of situations. But it's getting better, and is actively developed.

As a special bonus for having read my lengthy article all the way to the bottom. Here's a chunk of my .vimrc that allows me to have nested "project" settings in my directory tree...
" Search for any .vimsettings files in the path to the file.
" Source them if you find them.
function! ApplyLocalSettings(dirname)
    " Don't try to walk a remote directory tree -- takes too long, too many
    " what if's
    let l:netrwProtocol = strpart(a:dirname, 0, stridx(a:dirname, "://"))
    if l:netrwProtocol != ""
        return
    endif

    " Convert windows paths to unix style (they still work)
    let l:curDir = substitute(a:dirname, "", "/", "g")
    let l:parentDir = strpart(l:curDir, 0, strridx(l:curDir, "/"))
    if isdirectory(l:parentDir)
        call ApplyLocalSettings(l:parentDir)
    endif

    " Now walk back up the path and source .vimsettings as you find them. This
    " way child directories can 'inherit' from their parents
    let l:settingsFile = a:dirname . "/.vimsettings"
    if filereadable(l:settingsFile)
        exec ":source " . l:settingsFile
    endif
endfunction
autocmd! BufEnter * call ApplyLocalSettings(expand("<afile>:p:h"))

I posted it to Reddit a couple of years ago, and the feedback was overall positive. There were a few good ideas posted in the comments, so here's the link.

Thanks for reading. I'd love to hear your feedback below.

EDIT: spelling corrections; added links to other editors