Skip to content

amirrajan/ios-ruby-game-dev

Repository files navigation

Mobile Game Development in Ruby

Game development is fun!

Here are some sample apps that will help you build games for iOS using... drumroll

Ruby :-) (specifically using RubyMotion).

Steps to get started with RubyMotion:

  • You need a mac.
  • Download the RubyMotion Starter Pack from the RubyMotion website.
  • You need to be on the El Cap OS (at least).
  • You need to download XCode (you can get it from the AppStore).
  • You need to then open XCode and accept the license.
  • You need to install XCode command line tools by running xcode-select --install.
  • You need Ruby installed, preferablly version 2.3.1p112 or later. Use rbenv for managing Ruby versions.

Overview of Sample Projects

You'll want to go through the projects in the following order:

  • hello-world: This sample app shows how to set up a scene, get a label on the page, and have it update within the game loop.
  • create-sprite: This sample app shows how to create a single sprite and put it on the screen. Within the game loop, the sprite's angle is updated so that it spins.
  • touch-event: This sample app shows how to accept a simple tap event from the player.
  • particle-effects: This sample app shows how to create particle effects programatically.
  • collision-detection: This sample app shows how to do collision detection using SpriteKit's physics engine.
  • physics: This sample app shows how to use SpriteKit's physic engine to apply gravity to sprites.
  • actions: This sample app shows how to use SpriteKit's declarative animations to create a camera shake.
  • animated-sprite: This sample app shows how to chain frame by frame sprite drawings together to create a moving character.
  • Space Invaders: This sample app takes a lot of things learned from the sample apps above and makes a Space Invaders game.

ObjectiveC to Ruby

Answers on StackOverflow for iOS are usually in ObjectiveC. There's no denying that. This section goes into how you can translate ObjectiveC to Ruby. It's surprisingly simple, even if you've never written a line of ObjectiveC. If you're gun shy about using Ruby, you'll still get a great overview of how to read ObjectiveC and Java code contextualized through Ruby lenses. So let's get started. You can also read Learn Ruby the Hard Way to learn more about the language (independent of a framework).

Here is how a Person class would be created in ObjectiveC. There are two properties firstName and lastName, and an instance method called sayHello that returns a string. Instance methods in ObjectiveC are denoted by the - sign preceeding the function name:

Class Construction

//Person.h
@interface Person : NSObject
@property NSString *firstName;
@property NSString *lastName;
- (NSString*)sayHello;
@end

//Person.m
#import "Person.h"
@implementation Person
- (NSString*)sayHello {
  return [NSString stringWithFormat:@"Hello, %@ %@.",
                                    _firstName,
                                    _lastName];
}
@end

Here is how you would instanciate an instance of Person and call the sayHello function.

Person *person = [[Person alloc] init];
person.firstName = @"Amir";
person.lastName = @"Rajan";
NSLog([person sayHello]);

Let's break this down:

Method invocation in ObjectiveC is done through paired brackets []. So instead of person.sayHello() you have [person sayHello]. All ObjectiveC classes have an alloc class method, and an init instance method. The alloc class method creates a memory space for the instance of the class, and the init method initializes it (essentially a constructor).

Properties are accessed using the . operator (as opposed to the []).

String literals must be preceded by an @ sign. This is required for backwards compatability with C (all C code is valid ObjectiveC code... ObjectiveC is a superset of C).

NSLog is essentially puts. It's a global C method so is available anywhere.

Here is the same Person class and usage, but in Ruby.

class Person
  attr_accessor :firstName, :lastName

  def sayHello
    "Hello, #{@firstName} #{@lastName}"
  end
end

The usage should be pretty straight forward. It's important to interalize the mechancial aspect of converting ObjectiveC to Ruby. Basically, you remove the [] and replace it with .. We'll have more examples later.

person = Person.alloc.init
person.firstName = "Amir"
person.lastName = "Rajan"
NSLog(person.sayHello)

Also, in Ruby, you can use Person.alloc.init, but Person.new is also available to you and does the same thing.

Method Anatomy

Generally speaking, all APIs in iOS use named parameters (and by extension, most iOS developers follow suit with their own classes). Here is an example how you would declare a method in ObjectiveC.

- (void)setDobWithMonth:(NSInteger)month
                withDay:(NSInteger)day
               withYear:(NSInteger)year {

}

This was really weird the first time I saw it too. So let's break down the code above. First, the - sign preceeding the method name denotes that this is an instance method (a + sign denotes that it's a class method... more on that later). The next token (void) denotes the return type.

Now for the fun part. The method name includes the name of the first parameter (usually the method name is seperated from the parameter name using the word With as a delimeter). The setDobWithMonth, withDay, withYear are the public names or the parameters. This is what users of the method will see when autocompletion pops up.

The tokens month, day, year are what the public names are bound to within the method body. This is crazy/weird I know. Take a moment to internalize this. In short, a method's name includes its named parameters, and each parameter has a outward facing name and an internal binding.

Here is how you would call the method.

[person setDobWithMonth:1 withDay:1 withYear:2013];

If you were to tell a teammate to call this method you would say.

Call the setDobWithMonth:withDay:withYear method.

Now that you understand the anatomy of a ObjectiveC function. Here is how you would do exact same thing in Ruby.

def setDobWithMonth(month,
  withDay,
  withYear)

end

And here is the invocation:

person.setDobWithMonth(1, withDay: 1, withYear: 2013)

Now, of course, you can nix the public names of the methods if you wish, and simply have:

def setDobWithMonth(month, day, year)

end

person.setDobWithMonth(1, 1, 2013)

But it's important to know how to call methods with named parameters, because all iOS APIs are built that way.

Blocks

There is a reason why Fucking Block Syntax exists. It's because it's terrifying. Let's break down the next piece of code, which does an POST (HTTP), passing a dictionary, to a url, and providing a callback on success.

- (void)
   post:(NSDictionary *)objectToPost
  toUrl:(NSString *)toUrl
success:(void (^)(RKMappingResult *mappingResult))success { }

Building upon what we learned in the previous section. The name of this method is post:toUrl:success (a more idiomatic name would probably be postWithObject:withUrl:success... but I digress).

The block is denoted by the ^ token. The full type signature for the block is void (^)(RKMappingResult) (╯°□°)╯︵ ┻━┻.

Here is how you would invoke the post:toUrl:success method:

[client post: @{ @"firstName": @"Amir", @"lastName": @"Rajan" }
       toUrl: @"http://localhost/people"
     success:^(RKMappingResult *result) {
       //callback code here
     }];

Again, method invocation is denoted by using []. For backward compatability with C, dictionary literals must be prefixed with the @, and of course, strings must also be prefixed with the @ sign. I call it the bloody stump character, because after you've typed a good 100 times, your fingers will end up being bloody stumps.

The callback success is denoted with the ^, plus the typed parameters, plus the callback method body. Here's how you would write and invoke the same thing in Ruby:

def post(objectToPost, toUrl: toUrl, success: success)

end

client post({ firstName: "Amir", lastName: "Rajan" },
  toUrl: "http://localhost/people",
  success: lambda { |result|
    #callback code here
  })

With what you've read so far, you should be able to translate the ObjectiveC code to Ruby. This was taken from an actual StackOverflow answer:

_label.layer.backgroundColor = [UIColor whiteColor].CGColor;

[UIView animateWithDuration:2.0 animations:^{
  _label.layer.backgroundColor = [UIColor greenColor].CGColor;
} completion:NULL];

Don't look, below until you think you've got the translation figured out.

@label.layer.backgroundColor = UIColor.whiteColor.CGColor

UIView.animateWithDuration(2.0, animations: lambda {
  @label.layer.backgroundColor = UIColor.greenColor.CGColor
}, completion: nil)

Categories vs Mixins

To extend a sealed/third party classes, ObjectiveC has the concept of a Category. Let's say we want to take the animation logic from the StackOverflow question above, and make it a part of UIView (a class within iOS's UIKit library). Here is how you would do it in ObjectiveC.

First we define the preprocessor directive (method with named parameters, blocks, and bloody stump characters):

@interface UIView (UIViewExtensions)
- (void)animate:(double)duration block:(void (^)(void))block;
- (void)animate:(void (^)(void))block;
@end

ObjectiveC has method overloading, so we provide two implementations of the function, one that takes in a duration and another that has a default value.

@implementation UIView (UIViewExtensions)
- (void)animate:(void (^)(void))block {
  [self animate:0.5 block:block];
}

- (void)animate:(double)duration block:(void (^)(void))block {
  [UIView beginAnimations:NULL context:NULL];
  [UIView setAnimationDuration:duration];
  block();
  [UIView commitAnimations];
}
@end

This is the usage:

[label setAlpha:0];
[label animate:1 block:^{ [label setAlpha:1]; }];

In Ruby, we simply open the class/use a mixin. For berevity, I've just opened the class #yolo.

class UIView
  def animate duration = 0.5, &block
    UIView.beginAnimations(nil, context:nil)
    UIView.setAnimationDuration(duration)
    instance_eval &block
    UIView.commitAnimations
  end
end

  def animate duration = 0.5, &block
    UIView.beginAnimations(nil, context:nil)
    UIView.setAnimationDuration(duration)
    instance_eval &block
    UIView.commitAnimations
  end
end

Here is how you would invoke that function.

label.setAlpha 0
label.animate 1 { label.setAlpha 1 }

Benefit of Class Macros

Ruby class macros are great, and significantly increase readability. Here is a class from my game A Dark Room, which describes an encounter with an enemy.

class SnarlingBeastEvent < EncounterEvent
  title "a snarling beast"
  text "a snarling beast leaps out of the underbrush."
  damage 1

  def loot
    {
      fur: { min: 3, max: 7, chance: 1.0 }
    }
  end
end

Here is the definition of the class macro.

class EncounterEvent
  def self.title title
    define_method("title") { title }
  end

  def self.text text
    define_method("text") { text }
  end

  def self.health health
    define_method("health") { health }
  end
end

Given that ObjectiveC:

  • Doesn't have class macros.
  • Makes you use the bloody stump character.
  • Doesn't have the concept of a symbol.
  • Forces you to explicitly box and unbox value types from dictionaries.

You get a death by 1000 paper cuts. Here is the ObjectiveC code that accomplishes the same thing as the Ruby code with class macros.

Define the preprocessor directive. One class method initNew, and one instance method loot:

@interface SnarlingBeastEvent : EncounterEvent
+ (id)initNew;
- (NSDictionary *)loot;
@end

Call parent constructor with specific attributes. Override loot method. The dictionary literal contstruction, and boxing/unboxing apis are horrid:

@implementation SnarlingBeastEvent
+ (id)initNew {
  return [super initWithAttributes: @{
    @"health": [[NSNumber alloc]initWithInt: 1],
    @"title": @"a snarling beast",
    @"text": @"a snarling beast leaps out of the underbrush."
  }];
}

- (NSDictionary *)loot {
  return @{
    @"fur": @{
      @"min": [[NSNumber alloc]initWithDouble:3.0],
      @"max": [[NSNumber alloc]initWithDouble:7.0],
      @"chance": [[NSNumber alloc]initWithDouble:1.0]
    }
  };
}
@end

Base class implementation. Preprocessor directive, plus implementation. The dictionary boxing/unboxing apis are horrid:

@interface EncounterEvent : NSObject
@property NSString *title;
@property NSString *text;
@property NSInteger health;
+ (id)initWithAttributes:(NSDictionary *)attributes;
@end

@implementation EncounterEvent
+ (id)initWithAttributes:(NSDictionary *)attributes {
  EncounterEvent *e = [[EncounterEvent alloc] init];
  e.title = [attributes valueForKey:@"title"];
  e.text = [attributes valueForKey:@"text"];
  NSNumber *num = [attributes valueForKey:@"health"];
  e.health = [num integerValue];
  return e;
}
@end

Congratulations! You can now read and translate ObjectiveC to Ruby.

About

Apps that will help you build games for iOS using Ruby.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages