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:
- deprecated RubyCocoa syntax (see RubyCocoa Deprecated Stuff for more info)
- Fixed method names (centerized -> centered)
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.
Have fun!
Table of Contents
- Get It Started
- Let It Draw
- Let It Move
- Let Them Move
- Let It Blow
- Let Them Fight
- Let It Sound
- Wrap It Up?
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.
- Install the Xcode Project Template for the game framework
- You can download it from here
- Create a new Xcode project with RubyCocoa Game template
- Choose "File" -> "New Project" at the menu bar
- Select "Cocoa-Ruby Game Application"
- Enter the name of a new project
- Add a new game file with the game file template
- Open the "Classes/ruby/games" group at the left pane
- Right-click the "games" group
- Select "Add" -> "New File" at the pop-up menu
- Select "RubyCocoa Game Template"
- Enter the name of a new file (say TankGame.rb)
- The base name of the filename is used to replace the class names
- Open the "Classes/ruby/games" group at the left pane
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.
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.
- 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. - 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 - 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.
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
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.
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:
- registers the common event handlers (e.g. quitting the game when Ctrl-Q is pressed.) by invoking super()
- initializes TankGameModel by invoking @model.init
- registering event contexts to specify an action for Timer events by invoking EventContext.addContext.
- tick is invoked when the timer fires.
- 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.
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
is the file that we made so far. Here are the steps to run the game.
- untar the tar.gz file at the project folder
- Add a group named "TankGame" to the "Resources" group at the left pane of the Xcode project window.
- Right click the group and select "Add" -> "Existing Files" to add Tank.tiff file
- Run the application by pressing the "Build & Run" button
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
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
is the file that we made so far. Here are the steps to run the game.
- Untar the tar.gz file at the project folder
- Add a group named "TankGame" to the "Resources" group at the left pane of the Xcode project window. (if not exists)
- Right click the group and select "Add" -> "Existing Files" to add all the tiff files
- Run the application by pressing the "Build & Run" button
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
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
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
What we've created so far
is the file that we made so far. Here are the steps to run the game.
- Untar the tar.gz file at the project folder
- Add a group named "TankGame" to the "Resources" group at the left pane of the Xcode project window. (if not exists)
- 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)
- Run the application by pressing the "Build & Run" button
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
is the file that we made so far. Here are the steps to run the game.
- Untar the tar.gz file at the project folder
- Add a group named "TankGame" to the "Resources" group at the left pane of the Xcode project window. (if not exists)
- Right click the group and select "Add" -> "Existing Files" to add all the sound files (*.m4a and *.aif)
- Run the application by pressing the "Build & Run" button
Let Them Fight<Prev|Top:Mini Game Tutorial|Next>Brush It Up









