Let It Move Wide
Mini Game Tutorial > Let It Move

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

Leave a comment

Begin the comment with //pukiwiki if you want to write a comment in PukiWiki format.

You must be logged in to post a comment.