gosu 0.7.7.2-universal-darwin-8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,232 @@
1
+ # The tutorial game over a landscape rendered with OpenGL.
2
+ # Basically shows how arbitrary OpenGL calls can be put into
3
+ # the block given to Window#gl, and that Gosu Images can be
4
+ # used as textures using the gl_tex_info call.
5
+
6
+ begin
7
+ # In case you use Gosu via RubyGems.
8
+ require 'rubygems'
9
+ rescue LoadError
10
+ # In case you don't.
11
+ end
12
+
13
+ require 'gosu'
14
+ require 'gl'
15
+ require 'glu'
16
+
17
+ include Gl
18
+ include Glu
19
+
20
+ module ZOrder
21
+ Stars, Player, UI = *0..3
22
+ end
23
+
24
+ # The only really new class here.
25
+ # Draws a scrolling, repeating texture with a randomized height map.
26
+ class GLBackground
27
+ # Height map size
28
+ POINTS_X = 7
29
+ POINTS_Y = 7
30
+ # Scrolling speed
31
+ SCROLLS_PER_STEP = 50
32
+
33
+ def initialize(window)
34
+ @image = Gosu::Image.new(window, "media/Earth.png", true)
35
+ @scrolls = 0
36
+ @height_map = Array.new(POINTS_Y) { Array.new(POINTS_X) { rand } }
37
+ end
38
+
39
+ def scroll
40
+ @scrolls += 1
41
+ if @scrolls == SCROLLS_PER_STEP then
42
+ @scrolls = 0
43
+ @height_map.shift
44
+ @height_map.push Array.new(POINTS_X) { rand }
45
+ end
46
+ end
47
+
48
+ def exec_gl
49
+ # Get the name of the OpenGL texture the Image resides on, and the
50
+ # u/v coordinates of the rect it occupies.
51
+ # gl_tex_info can return nil if the image was too large to fit onto
52
+ # a single OpenGL texture and was internally split up.
53
+ info = @image.gl_tex_info
54
+ return unless info
55
+
56
+ # Pretty straightforward OpenGL code.
57
+
58
+ glDepthFunc(GL_GEQUAL)
59
+ glEnable(GL_DEPTH_TEST)
60
+ glEnable(GL_BLEND)
61
+
62
+ glMatrixMode(GL_PROJECTION)
63
+ glLoadIdentity
64
+ glFrustum(-0.10, 0.10, -0.075, 0.075, 1, 100)
65
+
66
+ glMatrixMode(GL_MODELVIEW)
67
+ glLoadIdentity
68
+ glTranslate(0, 0, -4)
69
+
70
+ glEnable(GL_TEXTURE_2D)
71
+ glBindTexture(GL_TEXTURE_2D, info.tex_name)
72
+
73
+ offs_y = 1.0 * @scrolls / SCROLLS_PER_STEP
74
+
75
+ 0.upto(POINTS_Y - 2) do |y|
76
+ 0.upto(POINTS_X - 2) do |x|
77
+ glBegin(GL_TRIANGLE_STRIP)
78
+ z = @height_map[y][x]
79
+ glColor4d(1, 1, 1, z)
80
+ glTexCoord2d(info.left, info.top)
81
+ glVertex3d(-0.5 + (x - 0.0) / (POINTS_X-1), -0.5 + (y - offs_y - 0.0) / (POINTS_Y-2), z)
82
+
83
+ z = @height_map[y+1][x]
84
+ glColor4d(1, 1, 1, z)
85
+ glTexCoord2d(info.left, info.bottom)
86
+ glVertex3d(-0.5 + (x - 0.0) / (POINTS_X-1), -0.5 + (y - offs_y + 1.0) / (POINTS_Y-2), z)
87
+
88
+ z = @height_map[y][x + 1]
89
+ glColor4d(1, 1, 1, z)
90
+ glTexCoord2d(info.right, info.top)
91
+ glVertex3d(-0.5 + (x + 1.0) / (POINTS_X-1), -0.5 + (y - offs_y - 0.0) / (POINTS_Y-2), z)
92
+
93
+ z = @height_map[y+1][x + 1]
94
+ glColor4d(1, 1, 1, z)
95
+ glTexCoord2d(info.right, info.bottom)
96
+ glVertex3d(-0.5 + (x + 1.0) / (POINTS_X-1), -0.5 + (y - offs_y + 1.0) / (POINTS_Y-2), z)
97
+ glEnd
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ # Roughly adapted from the tutorial game. Always faces north.
104
+ class Player
105
+ Speed = 7
106
+
107
+ attr_reader :score
108
+
109
+ def initialize(window, x, y)
110
+ @image = Gosu::Image.new(window, "media/Starfighter.bmp", false)
111
+ @beep = Gosu::Sample.new(window, "media/Beep.wav")
112
+ @x, @y = x, y
113
+ @score = 0
114
+ end
115
+
116
+ def move_left
117
+ @x = [@x - Speed, 0].max
118
+ end
119
+
120
+ def move_right
121
+ @x = [@x + Speed, 800].min
122
+ end
123
+
124
+ def accelerate
125
+ @y = [@y - Speed, 50].max
126
+ end
127
+
128
+ def brake
129
+ @y = [@y + Speed, 600].min
130
+ end
131
+
132
+ def draw
133
+ @image.draw(@x - @image.width / 2, @y - @image.height / 2, ZOrder::Player)
134
+ end
135
+
136
+ def collect_stars(stars)
137
+ stars.reject! do |star|
138
+ if Gosu::distance(@x, @y, star.x, star.y) < 35 then
139
+ @score += 10
140
+ @beep.play
141
+ true
142
+ else
143
+ false
144
+ end
145
+ end
146
+ end
147
+ end
148
+
149
+ # Also taken from the tutorial, but drawn with draw_rot and an increasing angle
150
+ # for extra rotation coolness!
151
+ class Star
152
+ attr_reader :x, :y
153
+
154
+ def initialize(animation)
155
+ @animation = animation
156
+ @color = Gosu::Color.new(0xff000000)
157
+ @color.red = rand(255 - 40) + 40
158
+ @color.green = rand(255 - 40) + 40
159
+ @color.blue = rand(255 - 40) + 40
160
+ @x = rand * 800
161
+ @y = 0
162
+ end
163
+
164
+ def draw
165
+ img = @animation[Gosu::milliseconds / 100 % @animation.size];
166
+ img.draw_rot(@x, @y, ZOrder::Stars, @y, 0.5, 0.5, 1, 1, @color, :additive)
167
+ end
168
+
169
+ def update
170
+ # Move towards bottom of screen
171
+ @y += 3
172
+ # Return false when out of screen (gets deleted then)
173
+ @y < 650
174
+ end
175
+ end
176
+
177
+ class GameWindow < Gosu::Window
178
+ def initialize
179
+ super(800, 600, false)
180
+ self.caption = "Gosu & OpenGL Integration Demo"
181
+
182
+ @gl_background = GLBackground.new(self)
183
+
184
+ @player = Player.new(self, 400, 500)
185
+
186
+ @star_anim = Gosu::Image::load_tiles(self, "media/Star.png", 25, 25, false)
187
+ @stars = Array.new
188
+
189
+ @font = Gosu::Font.new(self, Gosu::default_font_name, 20)
190
+ end
191
+
192
+ def update
193
+ @player.move_left if button_down? Gosu::Button::KbLeft or button_down? Gosu::Button::GpLeft
194
+ @player.move_right if button_down? Gosu::Button::KbRight or button_down? Gosu::Button::GpRight
195
+ @player.accelerate if button_down? Gosu::Button::KbUp or button_down? Gosu::Button::GpUp
196
+ @player.brake if button_down? Gosu::Button::KbDown or button_down? Gosu::Button::GpDown
197
+
198
+ @player.collect_stars(@stars)
199
+
200
+ @stars.reject! { |star| !star.update }
201
+
202
+ @gl_background.scroll
203
+
204
+ @stars.push(Star.new(@star_anim)) if rand(20) == 0
205
+ end
206
+
207
+ def draw
208
+ # gl will execute the given block in a clean OpenGL environment, then reset
209
+ # everything so Gosu's rendering can take place again.
210
+
211
+ gl do
212
+ glClearColor(0.0, 0.2, 0.5, 1.0)
213
+ glClearDepth(0)
214
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
215
+
216
+ @gl_background.exec_gl
217
+ end
218
+
219
+ @player.draw
220
+ @stars.each { |star| star.draw }
221
+ @font.draw("Score: #{@player.score}", 10, 10, ZOrder::UI, 1.0, 1.0, 0xffffff00)
222
+ end
223
+
224
+ def button_down(id)
225
+ if id == Gosu::Button::KbEscape
226
+ close
227
+ end
228
+ end
229
+ end
230
+
231
+ window = GameWindow.new
232
+ window.show
@@ -0,0 +1,449 @@
1
+ # A (too) simple Gorilla-style shooter for two players.
2
+ # Shows how Gosu and RMagick can be used together to generate a map, implement
3
+ # a dynamic landscape and generally look great.
4
+ # Also shows a very minimal, yet effective way of designing a game's object system.
5
+
6
+ # Doesn't make use of Gosu's Z-ordering. Not many different things to draw, it's
7
+ # easy to get the order right without it.
8
+
9
+ # Known issues:
10
+ # * Collision detection of the missiles is lazy, allows shooting through thin walls.
11
+ # * The look of dead soldiers is, err, by accident. Soldier.png needs to be
12
+ # designed in a less obfuscated way :)
13
+
14
+ begin
15
+ # In case you use Gosu via RubyGems.
16
+ require 'rubygems'
17
+ rescue LoadError
18
+ # In case you don't.
19
+ end
20
+
21
+ require 'gosu'
22
+ require 'rmagick'
23
+
24
+ NULL_PIXEL = Magick::Pixel.from_color('none')
25
+
26
+ # The class for this game's map.
27
+ # Design:
28
+ # * Dynamic map creation at startup, holding it as RMagick Image in @image
29
+ # * Testing for solidity by testing @image's pixel values
30
+ # * Drawing by (re)creating an array of Gosu::Image instances, each representing
31
+ # a part of the large @image
32
+ # * Blasting holes into the map is implemented by drawing and erasing portions
33
+ # of @image, then setting the corresponding Gosu::Image instances to nil, so
34
+ # they will be recreated in Map#draw
35
+ # Note: The splitting is done because recreating such a large Gosu::Image for
36
+ # every map change would be a very noticeable delay!
37
+
38
+ class Map
39
+ WIDTH, HEIGHT = 800, 600
40
+ TILE_SIZE = 100
41
+ TILES_X = WIDTH / TILE_SIZE
42
+ TILES_Y = HEIGHT / TILE_SIZE
43
+
44
+ def initialize(window)
45
+ # We'll need the window later for re-creating Gosu images.
46
+ @window = window
47
+
48
+ # Let's start with something simple and load the sky via RMagick.
49
+ # Loading JPEG files isn't possible with Gosu, so say wow!
50
+ sky = Magick::Image.read("media/Sky.jpg").first
51
+ @sky = Gosu::Image.new(window, sky, true)
52
+
53
+ # This is the one large RMagick image that represents the map.
54
+ @image = Magick::Image.new(WIDTH, HEIGHT) { self.background_color = 'none' }
55
+
56
+ # Set up a Draw object that fills with an earth texture.
57
+ earth = Magick::Image.read('media/Earth.png').first.resize(1.5)
58
+ gc = Magick::Draw.new
59
+ gc.pattern('earth', 0, 0, earth.columns, earth.rows) { gc.composite(0, 0, 0, 0, earth) }
60
+ gc.fill('earth')
61
+ gc.stroke('#603000').stroke_width(1.5)
62
+ # Draw a smooth bezier island onto the map!
63
+ polypoints = [0, HEIGHT]
64
+ 0.upto(TILES_X) do |x|
65
+ polypoints += [x * TILE_SIZE, HEIGHT * 0.2 + rand(HEIGHT * 0.8)]
66
+ end
67
+ polypoints += [WIDTH, HEIGHT]
68
+ gc.bezier(*polypoints)
69
+ gc.draw(@image)
70
+
71
+ # Create a bright-dark gradient fill, an image from it and change the map's
72
+ # brightness with it.
73
+ fill = Magick::GradientFill.new(0, HEIGHT * 0.4, WIDTH, HEIGHT * 0.4, '#fff', '#666')
74
+ gradient = Magick::Image.new(WIDTH, HEIGHT, fill)
75
+ gradient = @image.composite(gradient, 0, 0, Magick::InCompositeOp)
76
+ @image.composite!(gradient, 0, 0, Magick::MultiplyCompositeOp)
77
+
78
+ # Finally, place the star in the middle of the map, just onto the ground.
79
+ star = Magick::Image.read('media/LargeStar.png').first
80
+ star_y = 0
81
+ star_y += 20 until solid?(WIDTH / 2, star_y)
82
+ @image.composite!(star, (WIDTH - star.columns) / 2, star_y - star.rows * 0.85,
83
+ Magick::DstOverCompositeOp)
84
+
85
+ # Creates an X*Y array for the Gosu images.
86
+ # (Initialized to nil automatically).
87
+ @gosu_images = Array.new(TILES_X) { Array.new(TILES_Y) }
88
+ end
89
+
90
+ def solid?(x, y)
91
+ # Map is open at the top.
92
+ return false if y < 0
93
+ # Map is closed on all other sides.
94
+ return true if x < 0 or x >= 800 or y >= 600
95
+ # Inside of the map, determine solidity from the map image.
96
+ @image.pixel_color(x, y) != NULL_PIXEL
97
+ end
98
+
99
+ def draw
100
+ # Sky background.
101
+ @sky.draw(0, 0, 0)
102
+ # All the tiles.
103
+ TILES_Y.times do |y|
104
+ TILES_X.times do |x|
105
+ # Recreate images that haven't been created yet, or need to be recreated
106
+ # due to map changes.
107
+ if @gosu_images[x][y].nil? then
108
+ part = @image.crop(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE)
109
+ @gosu_images[x][y] = Gosu::Image.new(@window, part, true)
110
+ end
111
+ # At last - draw it!
112
+ @gosu_images[x][y].draw(x * TILE_SIZE, y * TILE_SIZE, 0)
113
+ end
114
+ end
115
+ end
116
+
117
+ # Radius of a crater.
118
+ RADIUS = 25
119
+ # Radius of a crater, SHadow included.
120
+ SH_RADIUS = 45
121
+
122
+ def blast(x, y)
123
+ # This code assumes at most 2x2 tiles are affected by a blast, so
124
+ # don't change the RADIUS to 200 or something ;)
125
+ # Calculate the x/y indices of the two to four affected tiles.
126
+ # (left/right and top/bottom might be the same).
127
+
128
+ left = (x - SH_RADIUS) / TILE_SIZE
129
+ right = (x + SH_RADIUS) / TILE_SIZE
130
+ top = (y - SH_RADIUS) / TILE_SIZE
131
+ bottom = (y + SH_RADIUS) / TILE_SIZE
132
+
133
+ # Set affected images to nil.
134
+ # A 'double-free' doesn't hurt if e.g. left == right! However, we have to watch out
135
+ # for out-of-bounds errors.
136
+
137
+ @gosu_images[left][top] = nil unless left < 0 or top < 0
138
+ @gosu_images[right][top] = nil unless right >= TILES_X or top < 0
139
+ @gosu_images[left][bottom] = nil unless left < 0 or bottom >= TILES_Y
140
+ @gosu_images[right][bottom] = nil unless right >= TILES_X or bottom >= TILES_Y
141
+
142
+ # Create the crater image (basically a circle shape that is used to erase
143
+ # parts of the map) and the crater shadow image, if they don't exist
144
+ # already.
145
+
146
+ if @crater_image.nil? then
147
+ @crater_image = Magick::Image.new(2 * RADIUS, 2 * RADIUS) { self.background_color = 'none' }
148
+ gc = Magick::Draw.new
149
+ gc.fill('black').circle(RADIUS, RADIUS, RADIUS, 0)
150
+ gc.draw(@crater_image)
151
+ @crater_shadow = @crater_image.shadow(0, 0, (SH_RADIUS - RADIUS) / 2, 1)
152
+ end
153
+
154
+ # Draw the shadow (twice for more intensity), then erase a circle from the map.
155
+ @image.composite!(@crater_shadow, x - SH_RADIUS, y - SH_RADIUS, Magick::AtopCompositeOp)
156
+ @image.composite!(@crater_shadow, x - SH_RADIUS, y - SH_RADIUS, Magick::AtopCompositeOp)
157
+ @image.composite!(@crater_image, x - RADIUS, y - RADIUS, Magick::DstOutCompositeOp)
158
+ end
159
+ end
160
+
161
+ # Player class.
162
+ # Note that applies to the whole game:
163
+ # All objects implement an informal interface.
164
+ # draw: Draws the object (obviously)
165
+ # update: Moves the object etc., returns false if the object is to be deleted
166
+ # hit_by?(missile): Returns true if an object is hit by the missile, causing
167
+ # it to explode on this object.
168
+
169
+ class Player
170
+ # Magic numbers considered harmful! This is the height of the
171
+ # player as used for collision detection.
172
+ HEIGHT = 14
173
+
174
+ attr_reader :x, :y, :dead
175
+
176
+ def initialize(window, x, y, color)
177
+ # Only load the images once for all instances of this class.
178
+ @@images ||= Gosu::Image.load_tiles(window, "media/Soldier.png", 40, 50, false)
179
+
180
+ @window, @x, @y, @color = window, x, y, color
181
+ @vy = 0
182
+
183
+ # -1: left, +1: right
184
+ @dir = -1
185
+
186
+ # Aiming angle.
187
+ @angle = 90
188
+ end
189
+
190
+ def draw
191
+ if dead then
192
+ # Poor, broken soldier.
193
+ @@images[0].draw_rot(x, y, 0, 290 * @dir, 0.5, 0.65, @dir * 0.5, 0.5, @color)
194
+ @@images[2].draw_rot(x, y, 0, 160 * @dir, 0.95, 0.5, 0.5, @dir * 0.5, @color)
195
+ else
196
+ # Was moved last frame?
197
+ if @show_walk_anim
198
+ # Yes: Display walking animation.
199
+ frame = Gosu::milliseconds / 200 % 2
200
+ else
201
+ # No: Stand around (boring).
202
+ frame = 0
203
+ end
204
+
205
+ # Draw feet, then chest.
206
+ @@images[frame].draw(x - 10 * @dir, y - 20, 0, @dir * 0.5, 0.5, @color)
207
+ angle = @angle
208
+ angle = 180 - angle if @dir == -1
209
+ @@images[2].draw_rot(x, y - 5, 0, angle, 1, 0.5, 0.5, @dir * 0.5, @color)
210
+ end
211
+ end
212
+
213
+ def update
214
+ # First, assume that no walking happened this frame.
215
+ @show_walk_anim = false
216
+
217
+ # Gravity.
218
+ @vy += 1
219
+
220
+ if @vy > 1 then
221
+ # Move upwards until hitting something.
222
+ @vy.times do
223
+ if @window.map.solid?(x, y + 1)
224
+ @vy = 0
225
+ break
226
+ else
227
+ @y += 1
228
+ end
229
+ end
230
+ else
231
+ # Move downwards until hitting something.
232
+ (-@vy).times do
233
+ if @window.map.solid?(x, y - HEIGHT - 1)
234
+ @vy = 0
235
+ break
236
+ else
237
+ @y -= 1
238
+ end
239
+ end
240
+ end
241
+
242
+ # Soldiers are never deleted (they may die, though, but that is a different
243
+ # concept).
244
+ true
245
+ end
246
+
247
+ def aim_up
248
+ @angle -= 2 unless @angle < 10
249
+ end
250
+
251
+ def aim_down
252
+ @angle += 2 unless @angle > 170
253
+ end
254
+
255
+ def try_walk(dir)
256
+ @show_walk_anim = true
257
+ @dir = dir
258
+ # First, magically move up (so soldiers can run up hills)
259
+ 2.times { @y -= 1 unless @window.map.solid?(x, y - HEIGHT - 1) }
260
+ # Now move into the desired direction.
261
+ @x += dir unless @window.map.solid?(x + dir, y) or
262
+ @window.map.solid?(x + dir, y - HEIGHT)
263
+ # To make up for unnecessary movement upwards, sink downward again.
264
+ 2.times { @y += 1 unless @window.map.solid?(x, y + 1) }
265
+ end
266
+
267
+ def try_jump
268
+ @vy = -12 if @window.map.solid?(x, y + 1)
269
+ end
270
+
271
+ def shoot
272
+ @window.objects << Missile.new(@window, x + 10 * @dir, y - 10, @angle * @dir)
273
+ end
274
+
275
+ def hit_by?(missile)
276
+ if Gosu::distance(missile.x, missile.y, x, y) < 30 then
277
+ # Was hit :(
278
+ @dead = true
279
+ return true
280
+ else
281
+ return false
282
+ end
283
+ end
284
+ end
285
+
286
+ # Implements the same interface as Player, except it'S a missile!
287
+
288
+ class Missile
289
+ attr_reader :x, :y, :vx, :vy
290
+
291
+ def initialize(window, x, y, angle)
292
+ # All missile instances use the same sound.
293
+ @@explosion_sound ||= Gosu::Sample.new(window, "media/Explosion.wav")
294
+
295
+ # Horizontal/vertical velocity.
296
+ @vx, @vy = Gosu::offset_x(angle, 20).to_i, Gosu::offset_y(angle, 20).to_i
297
+
298
+ @window, @x, @y = window, x + @vx, y + @vy
299
+ end
300
+
301
+ def update
302
+ # Movement, gravity
303
+ @x += @vx
304
+ @y += @vy
305
+ @vy += 1
306
+ # Hit anything?
307
+ if @window.map.solid?(x, y) or @window.objects.any? { |o| o.hit_by?(self) } then
308
+ # Create great particles.
309
+ 5.times { @window.objects << Particle.new(@window, x - 25 + rand(51), y - 25 + rand(51)) }
310
+ @window.map.blast(x, y)
311
+ # Weeee, stereo sound!
312
+ @@explosion_sound.play_pan((2 * @x - Map::WIDTH) / Map::WIDTH)
313
+ return false
314
+ else
315
+ return true
316
+ end
317
+ end
318
+
319
+ def draw
320
+ # Just draw a small quad.
321
+ @window.draw_quad(x-2, y-2, 0xff800000, x+2, y-2, 0xff800000,
322
+ x-2, y+2, 0xff800000, x+2, y+2, 0xff800000, 0)
323
+ end
324
+
325
+ def hit_by?(missile)
326
+ # Missiles can't be hit by other missiles!
327
+ false
328
+ end
329
+ end
330
+
331
+ # Very minimal object that just draws a fading particle.
332
+
333
+ class Particle
334
+ def initialize(window, x, y)
335
+ # All Particle instances use the same image
336
+ @@image ||= Gosu::Image.new(window, 'media/Smoke.png', false)
337
+
338
+ @x, @y = x, y
339
+ @color = Gosu::Color.new(255, 255, 255, 255)
340
+ end
341
+
342
+ def update
343
+ @y -= 5
344
+ @x = @x - 1 + rand(3)
345
+ @color.alpha -= 5
346
+
347
+ # Remove if faded completely.
348
+ @color.alpha > 0
349
+ end
350
+
351
+ def draw
352
+ @@image.draw(@x - 25, @y - 25, 0, 1, 1, @color)
353
+ end
354
+
355
+ def hit_by?(missile)
356
+ # Smoke can't be hit!
357
+ false
358
+ end
359
+ end
360
+
361
+ # Finally, the class that ties it all together.
362
+ # Very straightforward implementation.
363
+
364
+ class GameWindow < Gosu::Window
365
+ attr_reader :map, :objects
366
+
367
+ def initialize()
368
+ super(800, 600, false)
369
+ self.caption = "Medal of Anna - Gosu & RMagick Integration Demo"
370
+
371
+ # Texts to display in the appropriate situations.
372
+ @player_instructions = []
373
+ @player_won_messages = []
374
+ 2.times do |plr|
375
+ @player_instructions << Gosu::Image.from_text(self,
376
+ "It is the #{ plr == 0 ? 'green' : 'red' } toy soldier's turn.\n" +
377
+ "(Arrow keys to walk and aim, Return to jump, Space to shoot)",
378
+ Gosu::default_font_name, 25, 0, width, :center)
379
+ @player_won_messages << Gosu::Image.from_text(self,
380
+ "The #{ plr == 0 ? 'green' : 'red' } toy soldier has won!",
381
+ Gosu::default_font_name, 25, 5, width, :center)
382
+ end
383
+
384
+ # Create everything!
385
+ @map = Map.new(self)
386
+ @players = [Player.new(self, 200, 40, 0xff308000), Player.new(self, 600, 40, 0xff803000)]
387
+ @objects = @players.dup
388
+
389
+ # Let any player start.
390
+ @current_player = rand(2)
391
+ # Currently not waiting for a missile to hit something.
392
+ @waiting = false
393
+ end
394
+
395
+ def draw
396
+ # Draw the main game.
397
+ @map.draw
398
+ @objects.each { |o| o.draw }
399
+
400
+ # If any text should be displayed, draw it - and add a nice black border around it
401
+ # by drawing it four times, with a little offset in each direction.
402
+
403
+ cur_text = @player_instructions[@current_player] if not @waiting
404
+ cur_text = @player_won_messages[1 - @current_player] if @players[@current_player].dead
405
+
406
+ if cur_text then
407
+ x, y = 0, 30
408
+ cur_text.draw(x - 1, y, 0, 1, 1, 0xff000000)
409
+ cur_text.draw(x + 1, y, 0, 1, 1, 0xff000000)
410
+ cur_text.draw(x, y - 1, 0, 1, 1, 0xff000000)
411
+ cur_text.draw(x, y + 1, 0, 1, 1, 0xff000000)
412
+ cur_text.draw(x, y, 0, 1, 1, 0xffffffff)
413
+ end
414
+ end
415
+
416
+ def update
417
+ # if waiting for the next player's turn, continue to do so until the missile has
418
+ # hit something.
419
+ @waiting &&= !@objects.grep(Missile).empty?
420
+
421
+ # Remove all objects whose update method returns false.
422
+ @objects.reject! { |o| o.update == false }
423
+
424
+ # If it's a player's turn, forward controls.
425
+ if not @waiting and not @players[@current_player].dead then
426
+ player = @players[@current_player]
427
+ player.aim_up if button_down? Gosu::KbUp
428
+ player.aim_down if button_down? Gosu::KbDown
429
+ player.try_walk(-1) if button_down? Gosu::KbLeft
430
+ player.try_walk(1) if button_down? Gosu::KbRight
431
+ player.try_jump if button_down? Gosu::KbReturn
432
+ end
433
+ end
434
+
435
+ def button_down(id)
436
+ if id == Gosu::KbSpace and not @waiting and not @players[@current_player].dead then
437
+ # Shoot! This is handled in button_down because holding space shouldn't
438
+ # auto-fire - the shots would come from different players.
439
+ @players[@current_player].shoot
440
+ @current_player = 1 - @current_player
441
+ @waiting = true
442
+ end
443
+ # Very important feature! ;)
444
+ close if id == Gosu::KbEscape
445
+ end
446
+ end
447
+
448
+ # So far we have only defined how everything *should* work - now set it up and run it!
449
+ GameWindow.new.show