4. Responding to user input
4.1 Refactor: the Sprite class
Our game is going to consist of three types of moving objects: rocks, ships, and missiles. Let’s pull some commonly-useful stuff out into a base class that we’ll call Sprite.
class Sprite < OSX::NSObject attr_accessor :position, :velocity, :radius, :color def initWithPosition_(position) @position = position @velocity = OSX::NSPoint.new(0, 0) 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 end
The Rock class now only needs a few specifics.
class Rock < Sprite def initWithPosition_(position) super(position) @velocity = OSX::NSPoint.new(rand-0.5,rand-0.5) @color = OSX::NSColor.whiteColor @radius = 30 self end def draw @color.set OSX::NSBezierPath.bezierPathWithOvalInRect( OSX::NSRect.new(@position.x-radius, @position.y-@radius, 2*@radius, 2*@radius)).stroke end end
4.2 Let's fly: the Ship class
Now let’s use that Sprite class to create our ships. We add a few attributes to help us fly our ship around and handle steering and acceleration in moveWithBounds_. We’ll draw our ship as a wedge—and stretch our brains a bit to come up with the set of coordinates used by our NSBezierPath. Don’t worry if the math doesn’t make sense to you (unless you want to write more games, then you should be ready for much harder stuff than this).
class Ship < Sprite attr_accessor :direction, :angle, :acceleration def initWithPosition_(position) super(position) @radius = 10 @color = OSX::NSColor.redColor @direction = OSX::NSPoint.new(0, 1) @angle = @acceleration = 0 self end def moveWithBounds_(bounds) super(bounds) if @angle != 0 cosA = Math::cos(@angle) sinA = Math::sin(@angle) x = @direction.x * cosA - @direction.y * sinA y = @direction.y * cosA + @direction.x * sinA @direction.x, @direction.y = x, y end if @acceleration != 0 @velocity.x += @acceleration * @direction.x @velocity.y += @acceleration * @direction.y end end def draw @color.set x0,y0 = @position.x, @position.y x, y = @direction.x, @direction.y r = @radius path = OSX::NSBezierPath.bezierPath path.moveToPoint(OSX::NSPoint.new(x0 + r*x, y0 + r*y)) path.lineToPoint(OSX::NSPoint.new(x0 + r * (-x +y), y0 + r * (-x -y))) path.lineToPoint(OSX::NSPoint.new(x0, y0)) path.lineToPoint(OSX::NSPoint.new(x0 + r * (-x -y), y0 + r * (+x -y))) path.fill end end
Create a ship by adding this line to your Game’s awakeFromNib function:
@ship = Ship.alloc.initWithPosition_(OSX::NSPoint.new(bounds.width/2, bounds.height/2))
Add the following line to Game draw:
@ship.draw if @ship
Add this to Game tick to make sure that the ship moves:
@ship.moveWithBounds_(@bounds) if @ship
We need a way to respond to user input events. We’ll capture them in our GameView but delegate them to the Game. Start by adding these three methods to GameView:
ns_overrides :acceptsFirstResponder def acceptsFirstResponder true end ns_overrides :keyDown_ def keyDown(event) @game.keyDown(event.keyCode) end ns_overrides :keyUp_ def keyUp(event) @game.keyUp(event.keyCode) end
Make sure that you define acceptsFirstResponder. If you forget it, your view won’t get any keyboard events.
Your GameView functions call these methods that should be defined in Game:
def keyDown(code) case code when KEY_LEFT_ARROW: @ship.angle = TURN_ANGLE if @ship when KEY_RIGHT_ARROW: @ship.angle = -TURN_ANGLE if @ship when KEY_UP_ARROW: @ship.acceleration = ACCELERATION if @ship when KEY_DOWN_ARROW: @ship.acceleration = -ACCELERATION if @ship end end def keyUp(code) case code when KEY_LEFT_ARROW, KEY_RIGHT_ARROW: @ship.angle = 0 if @ship when KEY_UP_ARROW, KEY_DOWN_ARROW: @ship.acceleration = 0 if @ship end end
Here are some more helpful constants; put them at the beginning of your source file.
TURN_ANGLE = 0.2 ACCELERATION = 1 KEY_LEFT_ARROW = 123 KEY_RIGHT_ARROW = 124 KEY_DOWN_ARROW = 125 KEY_UP_ARROW = 126
Now stop and play. You should be able to fly around inside your window, happily passing through rocks and popping from one side of the screen to another. A mathematician would say that you are moving on a surface called a torus, essentially a bagel or doughnut shape. If you feel like taking a snack break now, that’s as good a reason as any.
4.3 Shoot until you drop: the Missile class
There’s one more kind of object that we need in our game world: missiles. They are a lot like rocks, but much smaller, and we give them the same color as the ship that shoots them.
class Missile < Sprite def initWithPosition_velocity_color_(position, velocity, color) initWithPosition_(position) @velocity = velocity @color = color @radius = 3 self end def draw @color.set OSX::NSBezierPath.bezierPathWithOvalInRect( OSX::NSRect.new(@position.x-@radius, @position.y-@radius, 2*@radius, 2*@radius)).fill end end
We’ll create missiles by calling the shoot function which we will add to our Ship class. It launches missiles from the center of our ship in the direction that the ship is pointing. Missile velocity is relative to the ship’s velocity; missiles launched from moving ships move faster than missiles launched from stationary ones.
def shoot missilePosition = OSX::NSPoint.new(position.x+direction.x, position.y+direction.y) missileVelocity = OSX::NSPoint.new( MISSILE_SPEED * direction.x + velocity.x, MISSILE_SPEED * direction.y + velocity.y) Missile.alloc.initWithPosition_velocity_color_(missilePosition, missileVelocity, @color) end
At the top of your file, set MISSILE_SPEED = 10 and KEY_SPACE = 49, then add the following to your game’s keyDown function:
when KEY_SPACE: @missiles << @ship.shoot if @ship
Note that we’ve added another list of objects to our Game. Make sure that you initialize it in the Game awakeFromNib. Call the following from Game tick:
@missiles.each {|missile| missile.moveWithBounds_(@bounds)}
and be sure to draw them in Game draw:
@missiles.each {|missile| missile.draw}
Got it? Now play some more. You should be able to fly around as before, and when you press the space bar, missiles shoot from the front of your ship. But we don’t detect any collisions, and the missiles never disappear, so eventually your game world gets overloaded by the hundreds or thousands of missiles flying around. We’ll take care of all this in the next chapter.
4.4 Another snapshot: RubyRocks.rb
Here’s another snapshot if you’re having trouble following along.
NUMBER_OF_ROCKS = 10 MISSILE_SPEED = 10 TURN_ANGLE = 0.2 ACCELERATION = 1 KEY_SPACE = 49 KEY_LEFT_ARROW = 123 KEY_RIGHT_ARROW = 124 KEY_DOWN_ARROW = 125 KEY_UP_ARROW = 126 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 ns_overrides :acceptsFirstResponder def acceptsFirstResponder true end ns_overrides :keyDown_ def keyDown(event) @game.keyDown(event.keyCode) end ns_overrides :keyUp_ def keyUp(event) @game.keyUp(event.keyCode) 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 @ship = Ship.alloc.initWithPosition_(OSX::NSPoint.new(bounds.width/2, bounds.height/2)) @rocks = [] NUMBER_OF_ROCKS.times { @rocks << Rock.alloc.initWithPosition_(OSX::NSPoint.new(rand(bounds.width), rand(bounds.height))) } @missiles = [] end def draw OSX::NSColor.blackColor.colorWithAlphaComponent(0.9).set OSX::NSRectFill(@bounds) @rocks.each {|rock| rock.draw} @ship.draw if @ship @missiles.each {|missile| missile.draw} end def tick(timer) @rocks.each {|rock| rock.moveWithBounds_(@bounds)} @ship.moveWithBounds_(@bounds) if @ship @missiles.each {|missile| missile.moveWithBounds_(@bounds)} end def keyDown(code) case code when KEY_SPACE: @missiles << @ship.shoot if @ship when KEY_LEFT_ARROW: @ship.angle = TURN_ANGLE if @ship when KEY_RIGHT_ARROW: @ship.angle = -TURN_ANGLE if @ship when KEY_UP_ARROW: @ship.acceleration = ACCELERATION if @ship when KEY_DOWN_ARROW: @ship.acceleration = -ACCELERATION if @ship end end def keyUp(code) case code when KEY_LEFT_ARROW, KEY_RIGHT_ARROW: @ship.angle = 0 if @ship when KEY_UP_ARROW, KEY_DOWN_ARROW: @ship.acceleration = 0 if @ship end end end class Sprite < OSX::NSObject attr_accessor :position, :velocity, :radius, :color def initWithPosition_(position) @position = position @velocity = OSX::NSPoint.new(0, 0) 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 end class Rock < Sprite def initWithPosition_(position) super(position) @velocity = OSX::NSPoint.new(rand-0.5,rand-0.5) @color = OSX::NSColor.whiteColor @radius = 30 self end def draw @color.set OSX::NSBezierPath.bezierPathWithOvalInRect( OSX::NSRect.new(@position.x-@radius, @position.y-@radius, 2*@radius, 2*@radius)).stroke end end class Ship < Sprite attr_accessor :direction, :angle, :acceleration def initWithPosition_(position) super(position) @radius = 10 @color = OSX::NSColor.redColor @direction = OSX::NSPoint.new(0, 1) @angle = @acceleration = 0 self end def moveWithBounds_(bounds) super(bounds) if @angle != 0 cosA = Math::cos(@angle) sinA = Math::sin(@angle) x = @direction.x * cosA - @direction.y * sinA y = @direction.y * cosA + @direction.x * sinA @direction.x, @direction.y = x, y end if @acceleration != 0 @velocity.x += @acceleration * @direction.x @velocity.y += @acceleration * @direction.y end end def draw @color.set x0,y0 = @position.x, @position.y x, y = @direction.x, @direction.y r = @radius path = OSX::NSBezierPath.bezierPath path.moveToPoint(OSX::NSPoint.new(x0 + r*x, y0 + r*y)) path.lineToPoint(OSX::NSPoint.new(x0 + r * (-x +y), y0 + r * (-x -y))) path.lineToPoint(OSX::NSPoint.new(x0, y0)) path.lineToPoint(OSX::NSPoint.new(x0 + r * (-x -y), y0 + r * (+x -y))) path.fill end def shoot missilePosition = OSX::NSPoint.new(position.x+direction.x, position.y+direction.y) missileVelocity = OSX::NSPoint.new( MISSILE_SPEED * direction.x + velocity.x, MISSILE_SPEED * direction.y + velocity.y) return Missile.alloc.initWithPosition_velocity_color_(missilePosition, missileVelocity, @color) end end class Missile < Sprite def initWithPosition_velocity_color_(position, velocity, color) initWithPosition_(position) @velocity = velocity @color = color @radius = 3 self end def draw @color.set OSX::NSBezierPath.bezierPathWithOvalInRect( OSX::NSRect.new(@position.x-@radius, @position.y-@radius, 2*@radius, 2*@radius)).fill end end
Did you find an error? Is something missing? Post your comment or suggestion below!
Comments (0) post