3. Drawing in a window
3.1 The GameView class
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:
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.
3.2 The Game class
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.
3.3 And now Ruby begins to Rock
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.
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.
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:
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.
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.
3.4 A snapshot: RubyRocks.rb
Here’s the code that we’ve written so far.
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
Hi – thanks for the great tutorial. I had a couple of issues with the code:
regards
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.
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?
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.
I retract that statement. It is certainly not the cause.
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
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.
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.