Mini Game Tutorial Normal

About This Tutorial

This tutorial illustrates how to make a small game (TankGame) with the Game Framework for RubyCocoa. The objective of this tutorial is to let you make a game that runs on MiniKidsGames or your own games based on the game framework.

Thanks to RubyCocoa, you can easily make a game compared to writing a game in Objective-C or C. However, it doesn't help you make a game in a short time because you have to know a lot of things like windows and views and controllers before you write some code.

With the power of the game framework, many things are done in the framework so you can concentrate on writing your game specific code. This tutorial explains not only how to make a game but also the mechanisms behind your game, which is done by the framework.

What you learn with this tutorial

You will learn how to make a simple game that runs on MiniKidsGames. This tutorial shows you how to make "TankGame" explaining what you should implement with the game framework including some basic things to make a game.

What you should know before reading this tutorial

As this tutorial shows how to make a game by using RubyCocoa, you should have some knowledge Ruby, in addition to a little knowledge about RubyCocoa and Cocoa frameworks behind it. This tutorial shows some information or references about RubyCocoa and Cocoa classes, but it is good to know these things beforehand. You don't know about these things? Don't worry, you will get some knowledge through this tutorial.

Note

This document was written for 0.3.8 so there are some differences from the current version (0.4.0). Major changes that you should know are:

What this tutorial provides and what it doesn't

The game framework in MiniKidsGames provides the common code that is frequently written in simple game applications (e.g. creating a window, checking multiple key status). It also includes a game template for your convenient.

Thus, this tutorial focuses more on illustrating how to make a game by using the framework (including difference between programing with the game framework and with Cocoa frameworks). It also mentions some basic information in making a game such as drawing objects.

Since much hard work is done by the game framework (e.g. switching to full screen, speaking sounds, obtaining multiple key status, and creating the main window), this tutorial doesn't provide all the information that you need to make a game from scratch, but provides the information for making a game that runs on MiniKidsGames.

About the game you will make through this tutorial

The game you will make is called "TankGame." The objective of this game is to destroy all the enemy tanks (red tanks) before one of these crosses the borderline.

Your tank (blue) has a canon that can launch a cannon ball per one second. You move your tank left or right, adjusting the aiming sight and fire. If a canon hits a tank near its target point, the enemy tank is destroyed.

LetThemFight.jpg SIZE:800x622(?KB)

Have fun!

Table of Contents

  1. Get It Started
  2. Let It Draw
  3. Let It Move
  4. Let Them Move
  5. Let It Blow
  6. Let Them Fight
  7. Let It Sound
  8. Wrap It Up?

Parent:MiniKidsGames|Start Reading>Get It Started

Get It Started Normal

The first thing you do is to create a new game file. There is a game template in the framework so you can create it with the following steps.

  1. Install the Xcode Project Template for the game framework
    • You can download it from here
  2. Create a new Xcode project with RubyCocoa Game template
    • Choose "File" -> "New Project" at the menu bar
    • Select "Cocoa-Ruby Game Application"
      NewProject.jpg SIZE:516x429(?KB)
    • Enter the name of a new project
      ProjectName.jpg SIZE:516x429(?KB)
  3. Add a new game file with the game file template
    • Open the "Classes/ruby/games" group at the left pane
      AddingNewFile.jpg SIZE:202x276(?KB)
    • Right-click the "games" group
    • Select "Add" -> "New File" at the pop-up menu
    • Select "RubyCocoa Game Template"
      NewGameFile.jpg SIZE:516x429(?KB)
    • Enter the name of a new file (say TankGame.rb)
      • The base name of the filename is used to replace the class names

That's it. Now you have a new file "TankGame.rb" in the games group. By double-clicking the filename, you can see the code below:

require 'Game'
require 'State'
require 'Event'
require 'View'
require 'Utility'

class TankGameModel
  def init()
    # this method is called when Application is launched.
    # It is also called when TankGamePlayingState 
    # becomes the current state
    # do some initialization here
  end

  # Add some methods here
end

class TankGamePlayingState < State
  # define your timer IDs here
  MYGAME_TIMER = 1000

  def enter()
    super()
    @model.init()

    # Define context dependent event handlers
    # Each handlers are defined in a form of [EventType, Content, Proc object]
    # See KeyEvent.rb, MouseEvent.rb and TimerEvent.rb for event specific info
    # Other game's source code would also help you
    contexts = [[TimerEvent::Fired, MYGAME_TIMER, proc {|id| tick() }]]
    contexts.each {|context| EventContext.addContext(*context) }
    @timer = TimerEvent.new(0.1, true, MYGAME_TIMER)
  end
  
  def tick()
    # do something when timer event occurs
    return true
  end
  
  # add some event handlers or context handlers here
end

class TankGameView < View
  def notifyStateDidChange()
    # If you need some initialization process,
    # Override notifyStateDidChange and write what you want here.
    # If you don't need any initialization process for this view class,
    # Just remove this method
    ColorCollector.init(['black'])
  end    

  #
  # This method is called from MiniGamesView
  # 
  def drawRect()
    NSColor.blackColor.set
    NSRectFill(@bounds)
    
    # Use ShadowMaker module to easily draw shadow for objects
    setShadow(NSColor.lightGrayColor, [6.0, -6.0], 3.5, 0.6)
    # Use TextMaker module to easily draw Text
    drawCenterizedText("Draw Something", 70, NSColor.whiteColor)
  end
end

class TankGame < Game
  def initialize()
    @bindings = [[TankGamePlayingState, TankGameModel, TankGameView]]
    @initialState = TankGamePlayingState
  end
  def self.title() "TankGame" end
  def self.titleImage() nil end # return the name of an image here
end

Thanks to the Xcode template, you don't have to copy the template nor replace the class names. That's helpful isn't it?

Run It Anyway

The game template actually works as it is so why don't you run it? Press the "Build & Go" button (or Command-R key) at the project window. Then you see the main title of MiniKidsGames.

Select TankGame and hit the space key (at this point, you didn't make any title image for this game so the default title image is used.) You see a message "Draw Something." This game does nothing but draw this message and quit. Press "Ctrl-Q" to go back to the main menu.

Top:Mini Game Tutorial|Next>Let It Draw

Let It Draw Normal

I think you already know how does it look like. Before making a real game, why don't we draw something by changing the code in TankGameView.drawRect to know the basic drawing techniques.

Basic drawings

Before letting it draw, take a look at the code in the template to know what is going on.

def drawRect()
  # 1. Fill background
  NSColor.blackColor.set
  NSRectFill(@bounds)
   
  # 2. Set shadow for objects
  setShadow(NSColor.lightGrayColor, [6.0, -6.0], 0.7, 0.6)

  # 3. Draw a string
  drawCenterizedText("Draw Something", 80, NSColor.whiteColor)
end

This method is automatically invoked after an event process (like TankGameState.keyDown or TankGameState.tick ). The main things it does are shown below.

  1. Fill background
    This is the typical code seen in many Cocoa games. The first line sets the background to black. The second line fills the whole display in the window. @bounds is an NSRect object that represents the origin and size of this view.
  2. Set shadow for objects
    This code makes shadows underneath all subsequent drawing objects. The game framework provides ShadowMaker module so you can call setShadow to set the shadow. Arguments for the method are: (1) color (NSColor object), (2) offset (NSPoint object), (3) blur diameter (Float), and (4) alpha value (0.0 - 1.0). See ShadowMaker module for further information
  3. Draw a text string
    This code draws "Draw Something" at the center of the window. See TextMaker module for further information.

To draw something in your game, you must write drawing procedures in this method or in some other methods that are called from this method.

Drawing shapes

There are lots of objects you draw in a game. In drawRect method, you can draw as many objects as you want by using Cocoa frameworks drawing objects such as NSBezierPath, NSImage, and NSString. This section shows how to draw these objects in drawRect method.

Drawing a rectangle

You can draw a rectangle by using NSRectFill function. The code would be like this.

NSColor.whiteColor.set
NSRectFill(NSRect.new([x, y], [width, height]))

You can also use NSBezier.bezierPathWithRect to draw a rectangle. See NSBezierPath class reference for more detail.

Drawing a polygon

To draw a polygon, using NSBezierPath will get the job done. The code below will draw a small white triangle.

NSColor.yellowColor.set
path = NSBezierPath.bezierPath
point = NSPoint.new(100, 100)
path.moveToPoint(point)
[[150, 150], [200, 100], [100, 100]]].each do |point| 
  path.lineToPoint(NSPoint.new(*point))
end
path.stroke

You can designate as many points as you want to draw more complex shapes. Using path.fill instead of path.stroke will draw a filled triangle. In this case, you don't need the last point.

Drawing an oval

The code below will draw a blue oval at the point (100, 100) where its width and height are 200 and 150 respectively.

NSColor.blueColor.set
rect = NSRect.new(NSPoint.new(100, 100), NSSize.new(200, 150))
path = NSBezierPath.bezierPathWithOvalInRect(rect)

As same as drawing polygons, you can write path.fill to get a filled oval. For more detail about BezierPath, see NSBezierPath class reference.

Drawing an image

To draw an image in you game, using NSImage is the easiest way. The code to draw an image "Elephant.tiff" at the center of the view would be like this:

image = NSImage.alloc.initByReferencingFile("/Users/me/images/Elephant.tiff")
point = Field.centerizedPoint(image.size)
image.compositeToPoint(point,:operation, NSCompositeSourceOver)

If you want to draw an image that is stored in Resources folder under your application path, you can use NSImage.imageNamed to create an image object. To make an image object for "Elephant.tiff" in the resource folder would be like this:

image = NSImage.imageNamed("Elephant")

See NSImage class reference for more detail.

Drawing a text string

As shown in the code at the beginning of this page, you can use drawCenterizedText to draw a text string. The following code will draw "How are you?" at the center of the window.

drawText("How are you?", 70, NSColor.whiteColor);

You can also use drawText defined in TextMaker module that is included in the View class. The following code will draw "Hello" at the point (100, 100):

drawText("Hello", 100, NSColor.white, NSPoint.new(100, 100))

See TextMaker module for more details.

Making shadows

To make shadows for drawing objects, simply use the setShadow method. Arguments for the method are: (1) color (NSColor object), (2) offset (NSPoint object), (3) blur diameter (Float), and (4) alpha value (0.0 - 1.0). The code below makes shadows with lightGrayColor at offset (6.0, -6.0).

setShadow(NSColor.lightGrayColor, [6.0, -6.0], 0.7, 0.6)

Blur diameter is set to 0.7 (very clear) and opacity is set to 0.6 (about half transparent). You should call this method before drawing any objects.

Let it Draw these objects

Here shows the code for drawing all the objects above.

def drawRect()
  NSColor.blackColor.set
  NSRectFill(@bounds)
  setShadow(NSColor.lightGrayColor, [6.0, -6.0], 0.7, 0.6)

  NSColor.whiteColor.set
  NSRectFill(NSRect.new([100, 300], [100, 150]))

  NSColor.yellowColor.set
  path = NSBezierPath.bezierPath
  point = NSPoint.new(100, 100)
  path.moveToPoint(point)
  [[150, 150], [200, 100], [100, 100]].each {|point| path.lineToPoint(NSPoint.new(*point)) }
 path.fill

  NSColor.blueColor.set
  rect = NSRect.new(NSPoint.new(400, 100), NSSize.new(200, 150))
  path = NSBezierPath.bezierPathWithOvalInRect(rect)
  path.fill

  image = NSImage.alloc.initByReferencingFile("/Applications/MiniKidsGames.app/Contents/Resources/Elephant.tiff")
  point = Field.centerizedPoint(image.size)
  point.y += @bounds.size.height / 4;
  image.compositeToPoint(point,:operation, NSCompositeSourceOver)

  drawCenterizedText("I drew 'em", 80, NSColor.whiteColor, 
                      NSPoint.new(0, -@bounds.size.height / 4)
end

Now run the application and select TankGame. You will see the window shown as below.

LetItDraw.jpg SIZE:800x622(?KB)

Inside the Game Framework - How the game framework invokes drawRect()

Now you know how to draw objects at TankGameView.drawRect. But you may wonder from where and from whom this method is called.

The game framework invokes Game.drawRect:

  • after a timer event is handled
  • after a key event is handled
  • after a mouse event is handled

When an event occurs, the framework creates an Event object (one of KeyEvent, TimerEvent, or MouseEvent). The Event object, then, calls event handler in the current State object. At the time the event handler finishes its process, the event object requests MiniGamesView, the top-level view of MiniKidsGames, to redraw if the event handler returns true. Finally, MiniGamesView invokes drawRect method in the current View object.

You may have another question: "how does MiniGamesView know the current View?" The answer for this question is that MiniGamesView asks the Game class to get the current View. The Game class is a super class for all the games, which contains binding information among View, Model, and State classes.

If you want to know more detail about this mechanism, take a look at MiniGamesView.rb. Reading the code at drawRect will tell you much more information.

What we've created so far

TankGame-LetItDraw.rb SIZE:x(?KB) is the file that we made so far. Rename the file to "TankGame.rb" and copy it to the project folder so you can take a look at what's going on.

Get It Started<Prev|Top:Mini Game Tutorial|Next>Let It Move

Let It Move Normal

The next thing you learn is how to move a character in a game along with key events and / or timer events. This chapter illustrates how to:

  • represent characters in a game
  • make a Model that organizes the characters
  • implement event handlers

Characters in TankGame

Most of games, like shooting games and RPG, have their own characters such as fighters and enemies. Each character has its point, size and other data that are needed for playing a game.

In TankGame, there are four classes that represent the characters - Tank, CannonBall, AimingSight, and Enemy.

Tank class

The tank has the following data:

point
the current position of the tank (the center point of the tank image)
aiming
the aiming sight (an Aiming object) - will be implemented later
launchTimer
time left to the next launch - will be implemented later
image
an NSImage object that has a image of the tank
size
width and height of the tank.

The tank also has the following methods:

initialize
initializes the data in a Tank object.
move
moves the tank and its aiming sight.
launch
launches a cannon ball - implement later
draw
for drawing the tank. This method is called from TankGameView.drawRect

Okay, take a look at the class definition for the tank.

class Tank < ImageObject
  def initialize()
    point = Field.centerizedPoint(NSSize.new(0,0))
    point.y = 120
    super(point, "Tank")
  end

  def move(difference)
    prevX = @point.x
    @point.x += difference.x
    @point.x = Field.width * 0.2 if (@point.x <= Field.width * 0.2)
    @point.x = Field.width * 0.8 if (@point.x >= Field.width * 0.8)
  end

You may notice that there are some missing data and methods. these are described in the super class of Tank - ImageObject. Let's see its class definition.

ImageObject class

As other drawing object like Enemy and Aiming need their images, I created their super class to put all common code. Here is definition of the class.

# Model part of ImageObject
class ImageObject
  def initialize(point, name)
    @imageFile = name
    @point = point
  end

  def size()
    return image().size
  end

  attr_reader :point
end
# View part of ImageObject
class ImageObject
  def init()
    @@image = {} if (!defined?(@@image))
    @@image[self.class] = NSImage.imageNamed(@imageFile) if (!@@image[self.class])
    @image = @@image[self.class]
  end

  def draw()
    point = NSPoint.new(@point.x - image().size.width / 2, 
                        @point.y - image().size.height / 2)
    @image.compositeToPoint(point,:operation, NSCompositeSourceOver)
  end

  def image()
    init() if (!defined?(@image))
    return @image
  end
end

First of all, this class is separated into two parts - Model part and View part. It's a little bit tricky but not that weird. I intentionally separate this class into these two parts to improve the maintainability (You don't have to be worried that much about this separation at this point). The former part is used by both a Model object and a View object, and the latter part is used only by a View object.

The instance variables in this classes are image and point, which are needed for both moving and drawing a character. In init method, the ImageObject class reads an image data from the Resource folder in MiniKidsGames with a given name.

The draw method, which is called from TankGameView.drawRect, draws an image. This methods puts the image at @point - (image.width/2, image.height / 2). This means that @point becomes the center point of the image.

TankGameModel - Organizer for all the characters

A model in a game represents the world of TankGame. The TankGameModel class has a Tank object, CannonBall objects, and Enemy objects. It also checks if enemies are hit by cannon balls or things like that. At this point, there is only one character object, so let TankGameModel have a Tank object. The TankGameModel provides a method moveTank for moving the tank at this point.

class TankGameModel
  def init()
    @tank = Tank.new()
  end

  def moveTank(direction)
    direction.x *= 10
    direction.y *= 10
    @tank.move(direction)
  end  

  attr_reader :tank
end

TankGamePlayingState

This class receives events and manipulates the TankGameModel. At this point, there are two things to do - initialization and key event handling.

Initialization

Every State object in a game must initialize some data at its enter method. This initialization includes the registration of event handlers, timer generation. The enter method in TankGamePlayingState is shown below:

def enter()
  super()
  @model.init()

  contexts = [[TimerEvent::Fired, TANKGAME_TIMER, proc {|id| tick(); true }]]
  contexts.each { |context| EventContext.addContext(*context) }
  @timer = TimerEvent.new(0.1, true, TANKGAME_TIMER)
end

This method does three things:

  1. registers the common event handlers (e.g. quitting the game when Ctrl-Q is pressed.) by invoking super()
  2. initializes TankGameModel by invoking @model.init
  3. registering event contexts to specify an action for Timer events by invoking EventContext.addContext.
    • tick is invoked when the timer fires.
  4. Generate a timer (that fires every 0.1 sec) by invoking TimerEvent.new

Key Event Handling

In a State class, there are three different means of receiving a key event: 1) implementing keyDown method, 2) using EventContext, and 3) checking multiple key status periodically by using the KeyEvent.keyStatus method in a timer event handler (See Event Handling, KeyEvent class reference, and the source code of other games for farther key event handling).

As TankGame needs to capture multiple key status to move the tank and its aiming sight at the same time, you need to check KeyEvent.keyStatus at the tick method. Implementation of the method is shown below:

def tick()
  x = y = 0
  state = KeyEvent.keyStatus
  x -= 1 if (state.include?(KeyEvent::LEFT_ARROW))
  x += 1 if (state.include?(KeyEvent::RIGHT_ARROW))
  y += 1 if (state.include?(KeyEvent::UP_ARROW))
  y -= 1 if (state.include?(KeyEvent::DOWN_ARROW))
  @model.moveTank(NSPoint.new(x, y))
  return true # return true if it is needed to update the window. false otherwise.
end

KeyEvent.keyStatus returns an array object containing the key codes that are currently pressed down. Each key code is defined in KeyEvent.rb. Checking each key code with Array.include? tells you which keys are currently pressed.

As a State class doesn't know much about the world of TankGame, so it just converts key status to a direction as a vector. It, then, invokes @model.moveTank with the vector (as an NSPoint object). Thanks to the game framework, you already know an TankGameModel object (stored in @model). You can just call any method in the object without obtaining it by yourself.

Let's look at TankGameModel.moveTank to see what it does. it receives a vector and calculates how much the tank should move at a time. It, then, invokes @tank.move to actually moves the tank.

TankGameView

You created the Tank class, the world of TankGame, and a key event handler. One more thing you need to do is to implement the TankGameView class.

As most of drawing process is done by ImageObject.draw, the drawing process in this class is much simpler than the one shown in the previous chapter.

class TankGameView < View
  # invoked when the current state is changed into TankGamePlayingState
  # This method initializes the View part of ImageObject
  def notifyStateDidChange()
    @model.tank.init()
  end

  def drawRect()
    NSColor.whiteColor.colorWithAlphaComponent(0.9).set
    NSRectFill(@bounds)
    
    # Use ShadowMaker class to easily draw shadow for objects
    setShadow(NSColor.lightGrayColor, [6.0, -6.0], 0.7, 0.6)

    @model.tank.draw()
  end
end

That's it. Run the game and see what happens. A tank moves as you press cursor buttons.

LetItMove.jpg SIZE:800x622(?KB)

Having a tank move within the window is not like hours of fun. Maybe a several seconds of fun. Let's move on. Add some more drawing objects for this game at the next chapter.

Inside the game framework - MVC pattern

The game framework is based on MVC pattern, which decomposes the roles of your application in three different classes - Model, View, and Controller.

Model and View

A Model has a role of holding data. It also provides interface methods to manipulates the data. In a game, it contains data such as life, score, high score, and drawing objects. It also moves drawing objects, checks if drawing objects is destroyed or something like that. Sometimes a model delegates its roles to some drawing objects for simplicity and maintainability. Drawing objects such as Tank and Enemy are considered as a part of a Model.

A View is in charge of representing the data in a model so users can see those data. In a game, it draws all the characters that are stored in Model classes. It also plays a sound if needed.

Characters, unlike non-game applications, also belong to a View. Though it's not that beautiful from software engineering perspectives, it's not that bad to hold draw method in a drawing object. In a game, many of data in drawing objects highly depend on View (e.g. points and animation timings).

Though there is some ways to divide a drawing objects into a Model-specific class and a View-specific class (like Tank and TankRepresentater), it is not effective in performance since a View class has to find which View-specific character class should be called for a given Model-specific character class (by using Factory pattern, for example). This also means you need to relate the Model-specific class with the View-specific class somehow.

I prefer splitting such a class into two parts - a Model part and a View part - without separating these into different classes for simplicity and performance. Though it sounds like it is against the Model-View separation in MVC pattern, actually it is not because M-V separation doesn't always mean that you should separate M and V into two different classes. The most important thing in M-V separation is to separate the roles in the application into Model and View.

Controller

A Controller has a role of manipulates a Model in respond to an event. In the game framework, subclasses of State have this role. A state class receives events as shown in TankGamePlayingState section. Roles of a state class are:

  • to receive events (by using such as Event.addListener and EventContext.setContexts)
  • to connect methods in a Model with events
  • to check a status in a model to transit to another state

What we've created so far

LetItMove.tar.gz SIZE:x(?KB) is the file that we made so far. Here are the steps to run the game.

  1. untar the tar.gz file at the project folder
  2. Add a group named "TankGame" to the "Resources" group at the left pane of the Xcode project window.
  3. Right click the group and select "Add" -> "Existing Files" to add Tank.tiff file
  4. Run the application by pressing the "Build & Run" button

Let It Draw<Prev|Top:Mini Game Tutorial|Next>Let Them Move

Let Them Move Normal

You enjoyed the tank moving around? I always like to see the things moves in my games. To make it more fun, let other characters move as well.

In this section, you learn how to:

  • implement more characters
  • implement a timer handler
  • move characters on a timer event

More Characters

There are some other characters in the TankGame world - enemies, aiming sight, and cannon balls. As you already implemented the code for drawing images in ImageObject, you can concentrate on writing code for manipulating these characters.

A Tank needs its enemies

Let's make the Enemy class that represents enemies that try to invade the tank's territory. There are two things different from the Tank class in the previous chapter. One is the move method and another is the reached? method.

As enemies have to move without key events, this method decreases a random number from @point.y (an enemy goes downward). The move method should be called from TankGameModel when a timer event occurs.

To make a game playable, TankGameModel needs to know if a player wins or loses. According to the rule of this game, a player loses when an enemy crosses the border line. So this class has the reached? method to check if an enemy crosses the line. To make it simple, reached? returns true if a center point of an enemy passes the border line (as @point represents the center point of a character).

class Enemy < ImageObject
  WIDTH = 125
  MAX_ENEMIES = 5
  
  # sets the position of each enemy according to step (0-4)
  def initialize(step, speed = 4)
    @step = step
    x = setXPosition()
    super(NSPoint.new(x, Field.height * 0.8), "Enemy")
    @speed = speed
  end

  def setXPosition()
    return (Field.width - WIDTH * (MAX_ENEMIES - 1)) / 2 + @step * WIDTH
  end 
  
  # moves down by random offset between 0 and @speed (=4 by default)
  def move()
    @point.x = setXPosition()
    diff = rand(@speed)
    @point.y -= diff
  end
  
  # returns true if the center point of an enemy reaches the boarder line
  def reached?(line)
    return true if (@point.y <= line)
    return false
  end
  
  attr_reader :point
  private :setXPosition
end

An Aiming Sight

An AimingSight object moves left and right as the tank moves. Pressing up or down key moves it up or down. The move method restricts the range of y-axis so an aiming object won't go off the view. The View part of this class does not exist since ImageObject does what all this class needs.

class AimingSight < ImageObject
  def initialize(point)
    super(point, "Aiming")
  end
  
  def move(direction)
    upperLimit = Field.height * 0.9
    lowerLimit = Field.height * 0.2
    @point.x += direction.x
    @point.y += direction.y
    @point.y = upperLimit if (@point.y >= upperLimit)
    @point.y = lowerLimit if (@point.y <= lowerLimit)
  end
end

Make a Cannon Ball

The CannonBall class represents a cannon ball. When you press the space key, it flies from the tank to the point where the aiming sight is at the launch time. When it reaches its target point, it disappears (by setting @active to false).

As this class doesn't use any image files, it is not a subclass of ImageObject even though it has similar methods. it draws a circle to represents a cannon ball instead. Cannon.move let a cannon ball fly toward its target point. Once it is launched, TankGameModel.tick calls move until it disappears.

As same as ImageObject, CannonBall is separated into a Model part and a View part. The Model part is shown below:

class CannonBall
  SIZE = 20
  def initialize(start, target)
    # It's better not use @point = start.clone; This will raise exception due to null data on RubyCocoa-0.11.1
    @point = NSPoint.new(start.x, start.y)
    @target = NSPoint.new(target.x, target.y)
    @active = true
  end

  def move()
    @point.y += @speed
    @active = false if (@point.y > @target.y)
  end

The View part of CannonBall is shown below:

class CannonBall
  def draw()
    if (@active)
      NSColor.blueColor.set
      point = NSPoint.new(@point.x - SIZE / 2, @point.y - SIZE / 2)
      rect = NSRect.new(point, NSSize.new(SIZE, SIZE))
      path = NSBezierPath.bezierPathWithOvalInRect(rect)
      path.fill
    end
  end
end

Get the Tank loaded

Now we have cannon balls and an aiming sight. Let's stick these together with Tank. Update the Tank class so it:

  • has an AimingSight object. (at initialize)
  • launches a cannon ball. (at launch)
  • moves an AimingSight object (at move)
  • waits one second for the next cannon launch. (at move and launch)

Here is the updated code.

class Tank < ImageObject
  def initialize()
    point = Field.centerizedPoint(NSSize.new(0,0))
    point.y = 120
    super(point, "Tank")

    # updated
    @aiming = AimingSight.new(NSPoint.new(point.x, Field.height / 2))
    @launchTimer = 0
  end

  def move(difference)
    prevX = @point.x
    @point.x += difference.x
    @point.x = Field.width * 0.2 if (@point.x <= Field.width * 0.2)
    @point.x = Field.width * 0.8 if (@point.x >= Field.width * 0.8)

    # updated
    difference.x = @point.x - prevX
    @aiming.move(difference)
    @launchTimer -= 1 if (@launchTimer > 0)
  end 

  # updated
  def launch()
    if (@launchTimer <= 0)
      @launchTimer = 10
      return CannonBall.new(@point, @aiming.point)
    end
  end

  attr_reader :aiming
end

Create the World of Your Game

There are pretty much drawing objects in your game. Every time you create some drawing objects, these should be stored either in this class or in other characters as TankGameModel represents the world of your game.

Let's change the code like shown below so that TankGameModel can move all the characters on some events.

class TankGameModel
  def init()
    @tank = Tank.new()
    # updated
    @enemies = []
    @cannons = []
    Enemies::MAX_ENEMIES.times {|i| @enemies << Enemy.new(i) }
  end

  # invoked on timer event (direction is set by referring to the key status)
  def moveTank(direction)
    direction.x *= 10
    direction.y *= 10
    @tank.move(direction)
  end
 
  # updated
  # invoked on timer event
  # This method moves all the characters but the Tank
  def moveCharacters()
    @borderline = 200
    @cannons.each do |cannon|
      cannon.move()
      cannon.checkCollision(@enemies)
      @cannons.delete(cannon) if (!cannon.active)
    end

    @enemies.each do |enemy|
      enemy.move()
      @enemies.delete(enemy) if (!enemy.active)
      if (enemy.reached?(@borderline))
        init()
      end
    end
    if (@enemies.size == 0)
      init()
    end
  end

  # updated
  # invoked on a space key event
  def launch()
    cannon = @tank.launch()
    @cannons << cannon if (cannon)
  end

  attr_reader :tank, :enemies, :cannons, :borderline
end

First of all, you need to initialize the world of TankGame. The init method clears all the characters, and then it makes a tank object and five enemies.

The next thing this class does is to move enemies and cannons (if any). The moveCharacters method, which is called by a timer event handler, moves Enemy objects and CannonBall objects. It also asks each Enemy abject if it reaches the border line. if the center point of an Enemy object crosses the boarder line, you are dead. At this point, let the model call init when you die just to prevent Enemy objects from going off the window.

The launch method asks a tank object to launch a Cannon object. This method is called by the key event handler in TankGamePlayingState when you press the space key.

Events Rule the World

As you added some characters into Model, its relevant State object needs to manipulate the Model object so it can move these characters. Moving these is very simple. add the following code to the tick method in TankGamePlayingState.

@model.moveCharacters()

Another thing you need to do is to let the tank launch a cannon ball when you press the space key. In this case, I chose to use EventContext. Change the enter method to receive the space key event.

 def enter()
   super()
   @model.init()

   contexts = [[NSKeyDown, KeyEvent::SPACE, proc {|event| @model.launch(); false } ],
                 [TimerEvent::Fired, TANKGAME_TIMER, proc {|id| tick(); true }]]
   contexts.each { |context| EventContext.addContext(*context) }
   @timer = TimerEvent.new(0.1, true, TANKGAME_TIMER)
 end

As State.enter already add an event context for Ctrl-Q (see State.enter in State.rb), I added an event context for space key event. The statement:

contexts = [[NSKeyDown, KeyEvent::SPACE, proc {|event| @model.launch(); false} ],
              [TimerEvent::Fired, TANKGAME_TIMER, proc {|id| tick(); true }]]

makes an Array object that represents the event context. The first event context indicates When NSKeyDown event occurs for KeyEvent::SPACE (space key is hit), call a given Ruby Proc object {|event| @model.launch(); false } with an NSEvent object (event). The proc object, then, calls @model.launch(), returning false (updating the window is not needed),

In this case, "event" that is being passed to Ruby Proc object is not used. If you need to do some complex task, you may need the content of this event object.

The next line add the event contexts to the EventContext class.

 contexts.each { |context| EventContext.addContext(*context) }

Draw the World of Your Game

It's time for drawing your game world. As TankGameModel holds all the characters, TankGameView asks TankGameModel to get these. TankGameView will ask all these characters to draw at drawRect (Remember, you must draw everything in drawRect or in the functions that are called from drawRect).

One thing you should know is that a character drawn later comes front side (it may overlaps another object that is drawn earlier). So be careful about the z-order of your game world.

Anyway, the drawRect method draws the border line, the Tank object, Enemy objects, CannonBall objects, and the AimingSight object. The updated drawRect is shown below:

 def drawRect()
   NSColor.whiteColor.set
   NSRectFill(@bounds)
   
   # Use ShadowMaker class to easily draw shadow for objects
   setShadow(NSColor.lightGrayColor, [6.0, -6.0], 3.7, 0.6)

   # draw the border line
   NSColor.blackColor.set
   path = NSBezierPath.bezierPath
   point = NSPoint.new(0, @model.deadline)
   path.moveToPoint(point)
   point.x = Field.size.width
   path.lineToPoint(point)
   path.stroke

   # draw Tank
   @model.tank.draw()
   # draw Enemies and Cannons
   (@model.enemies + @model.cannons).each {|obj| obj.draw()}
   # draw an aiming sight (This becomes the top-most object in z-order)
   @model.tank.aiming.draw()
 end

See what you have made so far

LetThemMove.jpg SIZE:800x622(?KB)

They move, the tank launches cannons. Wow, it looks like a real game. It's now way much better than the previous one..... We'll make it more playable in the next chapter.

Inside the Game Framework - Event Context

I'll tell you a little about the event context mechanism. The event context has a set of array objects. Each array object contains a pair of "a concrete event you want to receive," and "an action when you receive the event."

In TankGamePlayingState.enter, NSKeyEvent and KeyEvent::SPACE specify an event that you receive, and the Ruby Proc object { |event| @model.launch(); false } specifies an action for the event.

When an event occurs, an Event object asks EventContext class if there is an event context that matches the event. If matched, the Event object invokes a Ruby proc that is designated in the matched event context. If there is no matched context, the Event object tries to invoke the default listener method (e.g. keyDown for KeyDown event, or tick for a TimerEvent).

This mechanism brings switch or if - else statements in an event handler into deep inside the framework. Actually you can do the same thing that is shown in TankGamePlayingState.enter() by the following code.

def keyDown(event)
  if (event.keycode == KeyEvent::SPACE)
    @model.launch()
  end
  return false
end

See Event Handling for farther information about event handling.

What we've created so far

LetThemMove.tar.gz SIZE:x(?KB) is the file that we made so far. Here are the steps to run the game.

  1. Untar the tar.gz file at the project folder
  2. Add a group named "TankGame" to the "Resources" group at the left pane of the Xcode project window. (if not exists)
  3. Right click the group and select "Add" -> "Existing Files" to add all the tiff files
  4. Run the application by pressing the "Build & Run" button

Let It Move<Prev|Top:Mini Game Tutorial|Next>Let It Blow

Let Them Fight Normal

This chapter is under construction. Thanks for your patience.

In this chapter, you will learn how to:

  • implement multiple states
  • change the current state

Wanted dead or alive

Now TankGame has all the characters - the Tank, CannonBalls, Enemies, and AimingSight. It launches. It explodes. So what do we need?

Actually we still need some stuff to make this game playable. There is no "game over." There is no score, no high score, and much more. Yes, they want to fight each other. Enemies want your tank dead or alive, so let them fight!

Let Them Fight

Let me explain a little bit about what we will create before we get started.

  • There are three tanks available
    • The game shows "Game Over" when no tank is left
  • The tank is dead when an enemy reaches the border line
  • An enemy is destroyed when it is blown by a cannonball
  • You can go to the next stage when all the tank are destroyed
    • The game shows the stage number at the beginning of each stage.
  • Enemies move a little faster at the next stage

Okay, here we go!

Change the rules - by updating the Model

Initialization routines

class TankGameModel
  MAX_LIFE = 3
  
  # invoked at the beginning of each stage
  def reset()
    @tank = Tank.new()
    @enemies = []
    @cannons = []
    @cleared = false
    @dead = false
    Enemy::MAX_ENEMIES.times do |i| 
      @enemies << Enemy.new(i, @speed)
    end
  end

  # invoked when this game is selected
  def init()
    @highScore = Preferences.valueForKey('HighScore').to_i
    @score = 0
    @stage = 1
    @life = MAX_LIFE
    @speed = 4
  end

Checking dead or alive

 def moveCharacters()
   reset() if (@dead || @cleared)
   @borderline = 200
   @cannons.each do |cannon|
     cannon.move()
     cannon.checkCollision(@enemies)
     @cannons.delete(cannon) if (!cannon.active)
   end

   @enemies.each do |enemy|
     enemy.move()
     if (!enemy.active)
       @enemies.delete(enemy)
       @score += 100
       @highScore = @score if (@score > @highScore)
     end
     die() if (enemy.reached?(@borderline))
   end
   clearStage() if (@enemies.size == 0)
 end

 def die()
   @life -= 1
   @dead = true
   @gameOver = true if (@life <= 0)
 end

 def clearStage()
   @cleared = true
   @speed += 1
   @stage += 1
 end

Recording High Scores

 def recordHighScore()
   Preferences.setValue('HighScore', @highScore)
 end

Let Them Begin

To show the stage number at the beginning of each stage.....

class TankGameStartState < State
  GAMESTART_TIMER = 1002
  def enter()
    super()
    @model.reset()
    @timer = TimerEvent.new(3.0, false, GAMESTART_TIMER)
  end

  def tick(id=nil)
    changeState(TankGamePlayingState)
    return true
  end
end

Let Them end the fight

class TankGameOverState < State
  GAMEOVER_TIMER = 1001
  def enter()
    super()
    @timer = TimerEvent.new(3.0, false, GAMEOVER_TIMER)
  end

  def tick(id=0)
    @model.recordHighScore()
    quit()
    return true
  end
end

Draw everything we need

Draw the score and available tanks

class TankGameView < View
 # updated
 def drawStatus()
   image = @model.tank.image
   x = @bounds.size.width - image.size.width / 4 - 15
   y = @bounds.size.height - image.size.height / 4 - 15
   @model.life.times do |i|
     destRect = NSRect.new(x, y, 
                           image.size.width / 4, image.size.height / 4)
     srcRect = NSRect.new([0,0], image.size)
     image.drawInRect(destRect, :fromRect, srcRect,:operation, 
                       NSCompositeSourceOver, :fraction, 1.0)
     x -= image.size.width / 4 + 5
   end

   x = 20 
   y = @bounds.size.height - 50
   drawText(sprintf('%05d', @model.score), 40, NSColor.blackColor, [x,y])
 end

  def drawRect()
    NSColor.whiteColor.colorWithAlphaComponent(0.9).set
    NSRectFill(@bounds)
    # Using shadow can be a performance bottleneck
    # setShadow(NSColor.lightGrayColor, [6.0, -6.0], 3.7, 0.6)
   
    NSColor.blackColor.set
    path = NSBezierPath.bezierPath
    point = NSPoint.new(0, @model.borderline)
    path.moveToPoint(point)
    point.x = Field.width
    path.lineToPoint(point)
    path.stroke

    drawStatus() # <------ updated

    @model.tank.draw()
    (@model.enemies + @model.cannons).each {|obj| obj.draw()}
    @model.tank.aiming.draw()
  end
end

View classes for new states

TankGameStartView

class TankGameStartView < TankGameView
  def drawRect()
    NSColor.whiteColor.colorWithAlphaComponent(0.9).set
    NSRectFill(@bounds)
#    setShadow(NSColor.lightGrayColor, [6.0, -6.0], 3.7, 0.6)
    drawCenterizedText("Stage #{@model.stage}", 100, NSColor.blackColor)
    drawStatus()
  end
end
Stage1.jpg SIZE:800x622(?KB)
Showing the stage number

TankGameOverState

class TankGameOverView < TankGameView
  def drawRect()
    NSColor.whiteColor.set
    NSRectFill(@bounds)
#    setShadow(NSColor.lightGrayColor, [6.0, -6.0], 3.7, 0.6)
    drawStatus()
    drawCenterizedText("GAME OVER", 100, NSColor.blackColor)
    score = @model.score
    highScore = @model.highScore
    if (score == highScore)
      drawCenterizedText("You made a high score", 50, NSColor.redColor, 
                       NSPoint.new(0, -200))
    else
      drawCenterizedText("High Score: #{highScore}", 50, NSColor.orangeColor, 
                       NSPoint.new(150, -200))
    end
  end
end
GameOver.jpg SIZE:800x622(?KB)
Game Over

Bind everything again

class TankGame < Game
  def initialize()
    # +-- updated
    # v
    @bindings = [[TankGamePlayingState, TankGameModel, TankGameView],
                [TankGameStartState, TankGameModel, TankGameStartView],
                [TankGameOverState, TankGameModel, TankGameOverView]]
    @initialState = TankGameStartState
  end

Check it out

LetThemFight.jpg SIZE:800x622(?KB)
Final Game View

What we've created so far

LetThemFight.tar.gz SIZE:x(?KB) is the file that we made so far. Here are the steps to run the game.

  1. Untar the tar.gz file at the project folder
  2. Add a group named "TankGame" to the "Resources" group at the left pane of the Xcode project window. (if not exists)
  3. Right click the group and select "Add" -> "Existing Files" and add all the tiff files (Add only TankGame.tiff if you already added other images)
  4. Run the application by pressing the "Build & Run" button

Let It Blow<Prev|Top:Mini Game Tutorial|Next>Let It Sound

Let It Sound Normal

To be a game, it must have some sounds. Let's get started. In this chapter, you will learn how to:

  • play a sound
  • play sound loops (BGM)

You also learn what class should play a sound, as well as what class shouldn't.

Playing sounds with Game Framework

There are two different ways to play a sound in Game Framework. One is using NSSound, and another is using BGM that is included in the game framework.

NSSound - for one-shot sound

Cocoa provides an easy way to play sounds - NSSound. Playing a sound with NSSound is very simple.

file = "path/to/soundfile.m4a"
@sound = NSSound.alloc.initWithContentsOfFile(file, :byReference, true)
@sound.play if (@sound)

As NSSound.initWithContentsOfFile returns nil if a sound file cannot be opened. you should add an error handling code if needed.

to stop the sound, simply write a code like this:

@sound.stop if (@sound)

NSSound supports various music file format such as mp3, AAC(*.m4a), wav, and aiff. This is the easiest and effective way to play sounds in your game.

Using a sound file that is stored at the Resource folder in your application, you can specify a sound file like this:

file = NSBundle.mainBundle.resourcePath.to_s + "/soundfile.aiff"

BGM - for music loops

Unfortunately, NSSound doesn't provide a means of playing a sound loop seamlessly. You can try the code below but you will be suffering from hearing a little gap between loops.

file = "path/to/soundfile.m4a"
@sound = NSSound.alloc.initWithContentsOfFile(file, :byReference, true)
@sound.play if (@sound)
@timer = NSTimer.scheduledTimerWithTimeInterval(interval, :target, 
             self, :selector, :tick, :userInfo, userInfo, :repeats, repeated)

def someTimerDrivenMethod()
  @sound.play if (!@sound.isPlaying)
 end

It is critical when you want to play a sound loop. But don't worry, the BGM class in the game framework provides an easy way out for playing such sound loops.

file = "path/to/soundfile.m4a"
begin
  @sound = BGM.alloc.initWithFile(file)
  @sound.play(true) if (@sound)
rescue
  # BGM.initWithFile raises an exception if it cannot open a given file.
  puts "Cannot open file: #{file}"
end

The argument true of @sound.play designates that the sound will be played continuously. To play one-shot sound, designate false as an argument of BGM.play (you can also use NSSound).

To stop a sound, simply write the following line.

 @sound.stop() if (@sound)

BGM class has similar methods that are in NSSound such as play, pause, resume, isPlaying, and stop. See BGM class reference for farther information.

Let Cannon Balls sound

Let's make an exploding sound when a cannon ball reaches its target, as well as it hits an enemy.

First, add the init method to the beginning of the View part of CannonBall.

def init()
  soundFile = NSBundle.mainBundle.resourcePath.to_s + "/Explosion.m4a"
  # class variable is used to share this sound object with all the instances.
  @@sound = NSSound.alloc.initWithContentsOfFile(soundFile, :byReference, true)
end

Second, add playSound to the View part of CannonBall so it can make a sound when it reaches its target.

def playSound()
  if (@opacity == 1.0)
    @@sound.stop if (@@sound.isPlaying.to_i == 1)
    @@sound.play
  end
end

Finally, update the draw method to start playing a sound when @exploding is set to true

def draw()
  init() if (!defined?(@@sound)) # <------ added
  size = SIZE * (3.0 - @opacity * 2)
  if (@active)
    if (!@exploding)
      NSColor.blueColor.set
    else
      NSColor.redColor.colorWithAlphaComponent(@opacity).set
      playSound() # <------ added
    end
    point = NSPoint.new(@point.x - size / 2, @point.y - size / 2)
    rect = NSRect.new(point, NSSize.new(size, size))
    path = NSBezierPath.bezierPathWithOvalInRect(rect)
    path.fill
  end
end

Let the Tank sound

The tank also make a sound when it launches a cannon ball. There are two modifications here. One is adding two lines of code below to the end of Tank.init:

 if (!defined?(@sound))
   soundFile = NSBundle.mainBundle.resourcePath.to_s + "/CannonLaunch.m4a"
   @sound = NSSound.alloc.initWithContentsOfFile(soundFile, :byReference, true)
   @aiming.init()
 end

You don't need any explanation here because it is pretty much similar to the code for Cannon.

Another one is overriding draw at the View part of Tank. Take a look at the code below:

 def draw()
   super()
   @sound.play if (@launchTimer == 10)
 end

This code is also simple. it draws the image by invoking super. It, then, plays a sound when it launches a cannon ball. The if closure means that it plays a sound only when the space key is pressed since @launchTimer is set to 10 when the key is pressed.

Let Your Game have a BGM

Playing a BGM in a game is not that hard. Add the following methods to TankGameView.

 # invoked when the current state is changed to TankGamePlayingState
 def notifyStateDidChange()
   @model.tank.init() 
   playSound()
 end
 # invoked when the current state is about to transit to another.
 def notifyStateWillChange(state)
   @sound.stop if (@sound)
   super(state)
 end
 
 # starts playing a BGM at the beginning of TankGamePlayingState
 def playSound()
   if (!defined?(@sound) || !@sound)
     soundFile = NSBundle.mainBundle.resourcePath.to_s + "/TankGame.aif" 
     begin
       @sound = BGM.alloc.initWithFile(soundFile)
     rescue
       @sound = nil
     end
   end
   @sound.play(true) if (@sound)
 end

As the notifyStateDidChange method is invoked when the current state is changed to TankGamePlayingState, it is a good place to start playing a BGM at this method.

When leaving TankGamePlayingState, notifyStateWillChange is invoked. Overriding this method gives you a chance to stop the BGM.

That's it.

Check if you can hear sounds

Run the application again. You can hear a BGM. It also makes a sound when you launch a cannon ball, as well as when the cannon ball reaches its target.

Inside the game framework - What class should play a sound?

There are lots of classes in TankGame so far and you might wonder where you should put a code for playing a sound. The answer should be:

  • character (e.g. Tank, Cannon, Enemies, etc..)
  • TankView (or other subclass of View class if any)
  • XXXXState objects (not recommended)

From a software engineering perspective, XXXModel must not play a sound because playing a sound is generally considered as a role of View. So playing sounds in a subclass of View is a good way. However, it is not that convenient to play sounds only in subclasses of View because these classes need to know everything about playing sounds (e.g. when to play, what sound to play), which often makes these classes very fat when there are lots of sounds in your game.

There are two solutions to avoid making View classes fat. One is to let characters play sounds. As a character has roles of both Model and View, playing sounds in its View part is a good way. It often makes the code very simple.

Another solution (or I should say workaround) is to play sounds in subclasses of State (e.g. TankGamePlayingState). This is not beautiful at all since State doesn't have a role of playing sounds, so you should not do this unless it is very hard to play sounds in View classes. The best way to separate the role of M, V, and C in terms of playing a sound is that:

  • a Model object has sound file names (sound objects would be OK)
  • a View object or a View part of a character object plays sounds
  • The Controller triggers either Model or View to play sounds

What we've created so far

LetItSound.tar.gz SIZE:x(?KB) is the file that we made so far. Here are the steps to run the game.

  1. Untar the tar.gz file at the project folder
  2. Add a group named "TankGame" to the "Resources" group at the left pane of the Xcode project window. (if not exists)
  3. Right click the group and select "Add" -> "Existing Files" to add all the sound files (*.m4a and *.aif)
  4. Run the application by pressing the "Build & Run" button

Let Them Fight<Prev|Top:Mini Game Tutorial|Next>Brush It Up