3. Drawing in a window

Let’s get started on the Ruby code for this project. I like to get something working as quickly as possible, so we’ll start by drawing in our game view window.

Open the RubyRocks.rb file. Delete the boilerplate comments at the top of the file. You can also get rid of ‘require osx/cocoa’; the RubyCocoa application structure ensures that the required modules are already loaded. In their place, add the following definition of the GameView class:

GameView [ruby]
class GameView < OSX::NSView
  ib_outlets :game

  def awakeFromNib
    window.setOpaque(false)
    @game.bounds = bounds
    @timer = OSX::NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(
        1.0/60.0, self, :tick, nil, true)    
  end

  ns_overrides :drawRect_
  def drawRect(rect)
    @game.draw
  end

  def tick(timer=nil)
    @game.tick(timer) 
    setNeedsDisplay(true)
  end
end

The GameView class is responsible for our game’s interface to the system and player. Most of the logic for the game itself is in Game. When a Game document is created, Game.nib is loaded, which loads an instance of GameView and calls GameView’s awakeFromNib method. In our case, we make the containing window transparent (just for fun), tell the game object the size of our view, and create an NSTimer that we’ll use to trigger redraws and updates to the game state.

Since we’re overriding the view’s drawRect method, RubyCocoa requires an ns_overrides statement. Our GameView’s drawRect just calls the Game’s draw method.

We’ve set up our timer to run the game at an ambitious 60 frames per second. Each tick calls @game.tick to update the game state and setNeedsDisplay to trigger a redraw of the view.

Game [ruby]
class Game < OSX::NSDocument
  attr_accessor :bounds

  ns_overrides :windowNibName
  def windowNibName
    return "Game"
  end

  def draw
    OSX::NSColor.blackColor.colorWithAlphaComponent(0.9).set
    OSX::NSRectFill(@bounds)
  end
  
  def tick(timer)
  end
end

There’s not much to see here yet. Our tick function is just a placeholder. We’ll be using Quartz to draw our game objects; draw initially just fills the screen with a semi-transparent black. Run the application again to make sure it’s all working.

The Xcode console might shows a warning message that says: “NSView not correctly initialized. Did you forget to call super?” This message was a known problem in RubyCocoa that is fixed in recent releases.

Let’s get something moving. We’ll create a class to represent the rocks that we’ll be shooting. At first, they’ll just be drifting around in space. They are initialized with a random velocity, each time moveWithBounds_ is called, we update their position. We draw our rocks with the NSBezierPath class.

Rock [ruby]
class Rock < OSX::NSObject
  attr_accessor :position, :velocity, :radius, :color

  def initWithPosition_(position)
    @position = position
    @velocity = OSX::NSPoint.new(rand-0.5,rand-0.5)
    @color = OSX::NSColor.whiteColor
    @radius = 30
    self
  end

  def moveWithBounds_(bounds)
    @position.x += @velocity.x
    @position.y += @velocity.y    
    @position.x = bounds.width if @position.x < 0
    @position.x = 0 if @position.x > bounds.width
    @position.y = bounds.height if @position.y < 0
    @position.y = 0 if @position.y > bounds.height
  end

  def draw
    @color.set
    OSX::NSBezierPath.bezierPathWithOvalInRect(
        OSX::NSRect.new(@position.x-@radius, @position.y-@radius, 2*@radius, 2*@radius)).stroke
  end
end

We’ll create the rocks in awakeFromNib in class Game.

Game awakeFromNib [ruby]
  def awakeFromNib
    @rocks = []
    NUMBER_OF_ROCKS.times {
        @rocks << Rock.alloc.initWithPosition_(OSX::NSPoint.new(rand(bounds.width), rand(bounds.height)))
    }
  end

You’ll need to define NUMBER_OF_ROCKS somewhere. I suggest that you put NUMBER_OF_ROCKS = 10 in a line at the beginning of your RubyRocks.rb file.

Modify the Game draw function to draw the rocks:

Game draw [ruby]
  def draw
    OSX::NSColor.blackColor.colorWithAlphaComponent(0.9).set
    OSX::NSRectFill(@bounds)
    @rocks.each {|rock| rock.draw}
  end

And now we have something to do when the clock ticks: we’ll move our rocks.

Game tick [ruby]
  def tick(timer)
    @rocks.each {|rock| rock.moveWithBounds_(@bounds)}
  end

Now try it! You should see a peaceful looking field of circles drifting independently across your window. As they drift off the edges of the window they pop back onto the opposite edge.

You now know enough to create beautiful animated art. But there’s more ahead of us. Next we will see how to make our creations interactive.

Here’s the code that we’ve written so far.

RubyRocks.rb (snapshot) [ruby]
NUMBER_OF_ROCKS = 10

class GameView < OSX::NSView
  ib_outlets :game

  def awakeFromNib
    window.setOpaque(false)
    @game.bounds = bounds
    @timer = OSX::NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(
        1.0/60.0, self, :tick, nil, true)    
  end

  ns_overrides :drawRect_
  def drawRect(rect)
    @game.draw
  end

  def tick(timer=nil)
    @game.tick(timer) 
    setNeedsDisplay(true)
  end
end

class Game < OSX::NSDocument
  attr_accessor :bounds

  ns_overrides :windowNibName
  def windowNibName
    return "Game"
  end

  def awakeFromNib
    @rocks = []
    NUMBER_OF_ROCKS.times {
        @rocks << Rock.alloc.initWithPosition_(OSX::NSPoint.new(rand(bounds.width), rand(bounds.height)))
    }
  end

  def draw
    OSX::NSColor.blackColor.colorWithAlphaComponent(0.9).set
    OSX::NSRectFill(@bounds)
    @rocks.each {|rock| rock.draw}
  end
  
  def tick(timer)
    @rocks.each {|rock| rock.moveWithBounds_(@bounds)}
  end
end

class Rock < OSX::NSObject
  attr_accessor :position, :velocity, :radius, :color

  def initWithPosition_(position)
    @position = position
    @velocity = OSX::NSPoint.new(rand-0.5,rand-0.5)
    @color = OSX::NSColor.whiteColor
    @radius = 30
    self
  end

  def moveWithBounds_(bounds)
    @position.x += @velocity.x
    @position.y += @velocity.y    
    @position.x = bounds.width if @position.x < 0
    @position.x = 0 if @position.x > bounds.width
    @position.y = bounds.height if @position.y < 0
    @position.y = 0 if @position.y > bounds.height
  end

  def draw
    @color.set
    OSX::NSBezierPath.bezierPathWithOvalInRect(
        OSX::NSRect.new(@position.x-@radius, @position.y-@radius, 2*@radius, 2*@radius)).stroke
  end
end

Did you find an error? Is something missing? Post your comment or suggestion below!

Comments (8) post
  1. dan Mon Oct 09 18:47:23 +0000 2006

    Hi – thanks for the great tutorial. I had a couple of issues with the code:

    • for some reason (possibly my mistake) my awakeFromNib method gets invoked before the game ib_outlet has been initialized. without checking first for nil, the example code errors out. don’t know why, but the awake method gets invoked a second time, and everything is set then.
    • definitely an issue with the code above: the code is a little confused about which class @rocks belongs to. if it belongs to Game (which I think it should) then the draw and tick fragments you have above are correct, but the @rock initialization in awakeFromNib (which is part of the View) doesn’t work. i solved this by creating an initialize method in Game, and calling this from the awake.

    regards

  2. bryan Wed Oct 25 14:55:50 +0000 2006

    I’m also having an issue with this code. As it appears above, I get the following when attempting to build:

    RubyRocks.rb:8:in `NSApplicationMain’: NSApplicationMain – RBException_NoMethodError – undefined method `bounds=’ for nil:NilClass (OSX::OCException)

    And I’m not smart enough like dan was above, to figure out a workaround. I’m coming over from Rails, so this kind of app development is a strange and alien world to me.

    But I love the site, and am so glad to have a resource like this. Thanks for your hard work.

  3. Tim Burks Fri Oct 27 21:38:31 +0000 2006

    Bryan, I suspect that your game outlet isn’t set in your nib file. That would cause the @game instance variable to be nil, which would cause the message you reported above. Remember those snide comments I made about nib files in the sidebar in the last chapter? Here’s one more reason to dislike nibs: it’s hard to see when you’ve left something undone in one.

    See my “Mastering Cocoa with Ruby” tutorial for more on this topic. Since I wrote that, I’ve written a new version of RubyRocks that builds the entire interface from Ruby. Should I ditch this example and switch to that one?

  4. Mark Chadwick Wed Nov 08 16:11:08 +0000 2006

    I was trying to figure the very same thing out.

    If you change (on ~line 9 of info.nib) ‘IBLockedObjects’ to ‘IBOpenObjects’ all goes as planned. This, of course, doesn’t mean I have the slightest clue what I’m talking about, but it works.

  5. Mark Chadwick Wed Nov 08 16:18:58 +0000 2006

    I retract that statement. It is certainly not the cause.

  6. Bryan Hill Sat Jan 20 16:41:09 +0000 2007

    I’m having fun with this stuff, thanks for the great site!

    I’m stuck at the Game class. Building the code succeeds, running it gives me the following in the run log:

    RubyRocks.rb:7:in `NSApplicationMain’: NSApplicationMain – RBException_NoMethodError – undefined method `setOpaque’ for nil:NilClass (OSX::OCException)

    I’ve been fiddling with it, but can’t seem to get it…

    Thanks, Bryan

  7. Derick Fay Thu Jun 14 21:00:37 +0000 2007

    I’ve checked and checked my .nib but I’m getting the same error as bryan in the prior comment. I think I’ve connected the GameView correctly….could you update the previous page (on IB) to be more explicit about how to make the connection?

    BTW great site, & I agree entirely with your point about how obfuscating .nibs can be.

  8. Gary Tue Sep 25 19:29:53 +0000 2007

    It seems that the version of RubyCocoa I have (0.12) adds more initial code and sets the parent class of Game to NSPersistentDocument. However, these changes don’t affect the code above.

    When I run the above code, it did not complain, but showed no window. I fixed this problem by going back to the Interface Builder, opening the ‘instances’, clicking on the window instance, clicking command-1 to open the properties as shown on the previous page of this guide, then setting “Visible at launch time” to checked.