gosu 0.8.6-x86-mingw32 → 0.8.7-x86-mingw32

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/Gosu/Audio.hpp +171 -171
  3. data/Gosu/Bitmap.hpp +96 -96
  4. data/Gosu/Color.hpp +204 -204
  5. data/Gosu/Directories.hpp +36 -36
  6. data/Gosu/Font.hpp +83 -83
  7. data/Gosu/Gosu.hpp +34 -34
  8. data/Gosu/Graphics.hpp +115 -115
  9. data/Gosu/GraphicsBase.hpp +110 -110
  10. data/Gosu/IO.hpp +269 -269
  11. data/Gosu/Image.hpp +122 -122
  12. data/Gosu/ImageData.hpp +61 -61
  13. data/Gosu/Input.hpp +149 -149
  14. data/Gosu/Inspection.hpp +14 -14
  15. data/Gosu/Math.hpp +135 -135
  16. data/Gosu/Platform.hpp +93 -93
  17. data/Gosu/Sockets.hpp +156 -156
  18. data/Gosu/TR1.hpp +56 -56
  19. data/Gosu/Text.hpp +71 -71
  20. data/Gosu/TextInput.hpp +70 -70
  21. data/Gosu/Utility.hpp +28 -28
  22. data/Gosu/Version.hpp +19 -19
  23. data/Gosu/Window.hpp +145 -145
  24. data/examples/ChipmunkIntegration.rb +275 -275
  25. data/examples/CptnRuby.rb +223 -223
  26. data/examples/GosuZen.rb +68 -68
  27. data/examples/MoreChipmunkAndRMagick.rb +155 -155
  28. data/examples/OpenGLIntegration.rb +225 -225
  29. data/examples/RMagickIntegration.rb +417 -417
  30. data/examples/TextInput.rb +154 -154
  31. data/examples/Tutorial.rb +130 -130
  32. data/examples/media/Beep.wav +0 -0
  33. data/examples/media/CptnRuby Map.txt b/data/examples/media/CptnRuby → Map.txt +0 -0
  34. data/examples/media/Explosion.wav +0 -0
  35. data/examples/media/Landscape.svg +9 -9
  36. data/examples/media/Space.png +0 -0
  37. data/examples/media/Star.png +0 -0
  38. data/examples/media/Starfighter.bmp +0 -0
  39. data/lib/1.8/gosu.so +0 -0
  40. data/lib/1.9/gosu.so +0 -0
  41. data/lib/2.0/gosu.so +0 -0
  42. data/lib/2.1/gosu.so +0 -0
  43. data/lib/FreeImage.dll +0 -0
  44. data/lib/OpenAL32.dll +0 -0
  45. data/lib/gosu.rb +19 -16
  46. data/lib/gosu/patches.rb +81 -81
  47. data/lib/gosu/preview.rb +143 -139
  48. data/lib/gosu/run.rb +11 -11
  49. data/lib/gosu/swig_patches.rb +60 -60
  50. data/lib/gosu/zen.rb +89 -89
  51. metadata +5 -5
@@ -1,417 +1,417 @@
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
- require 'rubygems'
15
- require 'gosu'
16
- require 'RMagick'
17
-
18
- NULL_PIXEL = Magick::Pixel.from_color('none')
19
-
20
- # The class for this game's map.
21
- # Design:
22
- # * Dynamic map creation at startup, holding it as RMagick Image in @image
23
- # * Testing for solidity by testing @image's pixel values
24
- # * Drawing from a Gosu::Image instance
25
- # * Blasting holes into the map is implemented by drawing and erasing portions
26
- # of @image, then recreating the corresponding area in the Gosu::Image
27
-
28
- class Map
29
- WIDTH, HEIGHT = 800, 600
30
-
31
- def initialize window
32
- # We'll need the window later for re-creating Gosu images.
33
- @window = window
34
-
35
- # Let's start with something simple and load the sky via RMagick.
36
- # Loading SVG files isn't possible with Gosu, so say wow!
37
- # (Seems to take a while though)
38
- sky = Magick::Image.read("media/Landscape.svg").first
39
- @sky = Gosu::Image.new(window, sky, true)
40
-
41
- # Create the map an stores the RMagick image in @image
42
- create_rmagick_map
43
-
44
- # Copy the RMagick Image to a Gosu Image (still unchanged)
45
- @gosu_image = Gosu::Image.new(window, @image, true)
46
- end
47
-
48
- def solid? x, y
49
- # Map is open at the top.
50
- return false if y < 0
51
- # Map is closed on all other sides.
52
- return true if x < 0 or x >= 800 or y >= 600
53
- # Inside of the map, determine solidity from the map image.
54
- @image.pixel_color(x, y) != NULL_PIXEL
55
- end
56
-
57
- def draw
58
- # Sky background.
59
- @sky.draw(0, 0, 0)
60
- # The landscape.
61
- @gosu_image.draw 0, 0, 0
62
- end
63
-
64
- # Radius of a crater.
65
- RADIUS = 25
66
- # Radius of a crater, Shadow included.
67
- SH_RADIUS = 45
68
-
69
- # Create the crater image (basically a circle shape that is used to erase
70
- # parts of the map) and the crater shadow image.
71
- CRATER_IMAGE = begin
72
- crater = Magick::Image.new(2 * RADIUS, 2 * RADIUS) { self.background_color = 'none' }
73
- gc = Magick::Draw.new
74
- gc.fill('black').circle(RADIUS, RADIUS, RADIUS, 0)
75
- gc.draw crater
76
- crater
77
- end
78
- CRATER_SHADOW = CRATER_IMAGE.shadow(0, 0, (SH_RADIUS - RADIUS) / 2, 1)
79
-
80
- def blast x, y
81
- # Draw the shadow (twice for more intensity), then erase a circle from the map.
82
- @image.composite!(CRATER_SHADOW, x - SH_RADIUS, y - SH_RADIUS, Magick::AtopCompositeOp)
83
- @image.composite!(CRATER_SHADOW, x - SH_RADIUS, y - SH_RADIUS, Magick::AtopCompositeOp)
84
- @image.composite!(CRATER_IMAGE, x - RADIUS, y - RADIUS, Magick::DstOutCompositeOp)
85
-
86
- # Isolate the affected portion of the RMagick image.
87
- dirty_portion = @image.crop(x - SH_RADIUS, y - SH_RADIUS, SH_RADIUS * 2, SH_RADIUS * 2)
88
- # Overwrite this part of the Gosu image. If the crater begins outside of the map, still
89
- # just update the inner part.
90
- @gosu_image.insert dirty_portion, [x - SH_RADIUS, 0].max, [y - SH_RADIUS, 0].max
91
- end
92
-
93
- private
94
-
95
- def create_rmagick_map
96
- # This is the one large RMagick image that represents the map.
97
- @image = Magick::Image.new(WIDTH, HEIGHT) { self.background_color = 'none' }
98
-
99
- # Set up a Draw object that fills with an earth texture.
100
- earth = Magick::Image.read('media/Earth.png').first.resize(1.5)
101
- gc = Magick::Draw.new
102
- gc.pattern('earth', 0, 0, earth.columns, earth.rows) { gc.composite(0, 0, 0, 0, earth) }
103
- gc.fill('earth')
104
- gc.stroke('#603000').stroke_width(1.5)
105
- # Draw a smooth bezier island onto the map!
106
- polypoints = [0, HEIGHT]
107
- 0.upto(8) do |x|
108
- polypoints += [x * 100, HEIGHT * 0.2 + rand(HEIGHT * 0.8)]
109
- end
110
- polypoints += [WIDTH, HEIGHT]
111
- gc.bezier(*polypoints)
112
- gc.draw(@image)
113
-
114
- # Create a bright-dark gradient fill, an image from it and change the map's
115
- # brightness with it.
116
- fill = Magick::GradientFill.new(0, HEIGHT * 0.4, WIDTH, HEIGHT * 0.4, '#fff', '#666')
117
- gradient = Magick::Image.new(WIDTH, HEIGHT, fill)
118
- gradient = @image.composite(gradient, 0, 0, Magick::InCompositeOp)
119
- @image.composite!(gradient, 0, 0, Magick::MultiplyCompositeOp)
120
-
121
- # Finally, place the star in the middle of the map, just onto the ground.
122
- star = Magick::Image.read('media/LargeStar.png').first
123
- star_y = 0
124
- star_y += 20 until solid?(WIDTH / 2, star_y)
125
- @image.composite!(star, (WIDTH - star.columns) / 2, star_y - star.rows * 0.85,
126
- Magick::DstOverCompositeOp)
127
- end
128
- end
129
-
130
- # Player class.
131
- # Note that applies to the whole game:
132
- # All objects implement an informal interface.
133
- # draw: Draws the object (obviously)
134
- # update: Moves the object etc., returns false if the object is to be deleted
135
- # hit_by?(missile): Returns true if an object is hit by the missile, causing
136
- # it to explode on this object.
137
-
138
- class Player
139
- # Magic numbers considered harmful! This is the height of the
140
- # player as used for collision detection.
141
- HEIGHT = 14
142
-
143
- attr_reader :x, :y, :dead
144
-
145
- def initialize(window, x, y, color)
146
- # Only load the images once for all instances of this class.
147
- @@images ||= Gosu::Image.load_tiles(window, "media/Soldier.png", 40, 50, false)
148
-
149
- @window, @x, @y, @color = window, x, y, color
150
- @vy = 0
151
-
152
- # -1: left, +1: right
153
- @dir = -1
154
-
155
- # Aiming angle.
156
- @angle = 90
157
- end
158
-
159
- def draw
160
- if dead then
161
- # Poor, broken soldier.
162
- @@images[0].draw_rot(x, y, 0, 290 * @dir, 0.5, 0.65, @dir * 0.5, 0.5, @color)
163
- @@images[2].draw_rot(x, y, 0, 160 * @dir, 0.95, 0.5, 0.5, @dir * 0.5, @color)
164
- else
165
- # Was moved last frame?
166
- if @show_walk_anim
167
- # Yes: Display walking animation.
168
- frame = Gosu::milliseconds / 200 % 2
169
- else
170
- # No: Stand around (boring).
171
- frame = 0
172
- end
173
-
174
- # Draw feet, then chest.
175
- @@images[frame].draw(x - 10 * @dir, y - 20, 0, @dir * 0.5, 0.5, @color)
176
- angle = @angle
177
- angle = 180 - angle if @dir == -1
178
- @@images[2].draw_rot(x, y - 5, 0, angle, 1, 0.5, 0.5, @dir * 0.5, @color)
179
- end
180
- end
181
-
182
- def update
183
- # First, assume that no walking happened this frame.
184
- @show_walk_anim = false
185
-
186
- # Gravity.
187
- @vy += 1
188
-
189
- if @vy > 1 then
190
- # Move upwards until hitting something.
191
- @vy.times do
192
- if @window.map.solid?(x, y + 1)
193
- @vy = 0
194
- break
195
- else
196
- @y += 1
197
- end
198
- end
199
- else
200
- # Move downwards until hitting something.
201
- (-@vy).times do
202
- if @window.map.solid?(x, y - HEIGHT - 1)
203
- @vy = 0
204
- break
205
- else
206
- @y -= 1
207
- end
208
- end
209
- end
210
-
211
- # Soldiers are never deleted (they may die, though, but that is a different
212
- # concept).
213
- true
214
- end
215
-
216
- def aim_up
217
- @angle -= 2 unless @angle < 10
218
- end
219
-
220
- def aim_down
221
- @angle += 2 unless @angle > 170
222
- end
223
-
224
- def try_walk(dir)
225
- @show_walk_anim = true
226
- @dir = dir
227
- # First, magically move up (so soldiers can run up hills)
228
- 2.times { @y -= 1 unless @window.map.solid?(x, y - HEIGHT - 1) }
229
- # Now move into the desired direction.
230
- @x += dir unless @window.map.solid?(x + dir, y) or
231
- @window.map.solid?(x + dir, y - HEIGHT)
232
- # To make up for unnecessary movement upwards, sink downward again.
233
- 2.times { @y += 1 unless @window.map.solid?(x, y + 1) }
234
- end
235
-
236
- def try_jump
237
- @vy = -12 if @window.map.solid?(x, y + 1)
238
- end
239
-
240
- def shoot
241
- @window.objects << Missile.new(@window, x + 10 * @dir, y - 10, @angle * @dir)
242
- end
243
-
244
- def hit_by? missile
245
- if Gosu::distance(missile.x, missile.y, x, y) < 30 then
246
- # Was hit :(
247
- @dead = true
248
- return true
249
- else
250
- return false
251
- end
252
- end
253
- end
254
-
255
- # Implements the same interface as Player, except it'S a missile!
256
-
257
- class Missile
258
- attr_reader :x, :y, :vx, :vy
259
-
260
- # All missile instances use the same sound.
261
- EXPLOSION = Gosu::Sample.new("media/Explosion.wav")
262
-
263
- def initialize(window, x, y, angle)
264
- # Horizontal/vertical velocity.
265
- @vx, @vy = Gosu::offset_x(angle, 20).to_i, Gosu::offset_y(angle, 20).to_i
266
-
267
- @window, @x, @y = window, x + @vx, y + @vy
268
- end
269
-
270
- def update
271
- # Movement, gravity
272
- @x += @vx
273
- @y += @vy
274
- @vy += 1
275
- # Hit anything?
276
- if @window.map.solid?(x, y) or @window.objects.any? { |o| o.hit_by?(self) } then
277
- # Create great particles.
278
- 5.times { @window.objects << Particle.new(@window, x - 25 + rand(51), y - 25 + rand(51)) }
279
- @window.map.blast(x, y)
280
- # Weeee, stereo sound!
281
- EXPLOSION.play_pan((2 * @x - Map::WIDTH) / Map::WIDTH)
282
- return false
283
- else
284
- return true
285
- end
286
- end
287
-
288
- def draw
289
- # Just draw a small quad.
290
- @window.draw_quad(x-2, y-2, 0xff800000, x+2, y-2, 0xff800000,
291
- x-2, y+2, 0xff800000, x+2, y+2, 0xff800000, 0)
292
- end
293
-
294
- def hit_by?(missile)
295
- # Missiles can't be hit by other missiles!
296
- false
297
- end
298
- end
299
-
300
- # Very minimal object that just draws a fading particle.
301
-
302
- class Particle
303
- def initialize(window, x, y)
304
- # All Particle instances use the same image
305
- @@image ||= Gosu::Image.new(window, 'media/Smoke.png', false)
306
-
307
- @x, @y = x, y
308
- @color = Gosu::Color.new(255, 255, 255, 255)
309
- end
310
-
311
- def update
312
- @y -= 5
313
- @x = @x - 1 + rand(3)
314
- @color.alpha -= 5
315
-
316
- # Remove if faded completely.
317
- @color.alpha > 0
318
- end
319
-
320
- def draw
321
- @@image.draw(@x - 25, @y - 25, 0, 1, 1, @color)
322
- end
323
-
324
- def hit_by?(missile)
325
- # Smoke can't be hit!
326
- false
327
- end
328
- end
329
-
330
- # Finally, the class that ties it all together.
331
- # Very straightforward implementation.
332
-
333
- class GameWindow < Gosu::Window
334
- attr_reader :map, :objects
335
-
336
- def initialize()
337
- super(800, 600, false)
338
- self.caption = "Gosu & RMagick Integration Demo"
339
-
340
- # Texts to display in the appropriate situations.
341
- @player_instructions = []
342
- @player_won_messages = []
343
- 2.times do |plr|
344
- @player_instructions << Gosu::Image.from_text(self,
345
- "It is the #{ plr == 0 ? 'green' : 'red' } toy soldier's turn.\n" +
346
- "(Arrow keys to walk and aim, Return to jump, Space to shoot)",
347
- Gosu::default_font_name, 30, 0, width, :center)
348
- @player_won_messages << Gosu::Image.from_text(self,
349
- "The #{ plr == 0 ? 'green' : 'red' } toy soldier has won!",
350
- Gosu::default_font_name, 30, 5, width, :center)
351
- end
352
-
353
- # Create everything!
354
- @map = Map.new(self)
355
- @players = [Player.new(self, 200, 40, 0xff308000), Player.new(self, 600, 40, 0xff803000)]
356
- @objects = @players.dup
357
-
358
- # Let any player start.
359
- @current_player = rand(2)
360
- # Currently not waiting for a missile to hit something.
361
- @waiting = false
362
- end
363
-
364
- def draw
365
- # Draw the main game.
366
- @map.draw
367
- @objects.each { |o| o.draw }
368
-
369
- # If any text should be displayed, draw it - and add a nice black border around it
370
- # by drawing it four times, with a little offset in each direction.
371
-
372
- cur_text = @player_instructions[@current_player] if not @waiting
373
- cur_text = @player_won_messages[1 - @current_player] if @players[@current_player].dead
374
-
375
- if cur_text then
376
- x, y = 0, 30
377
- cur_text.draw(x - 1, y, 0, 1, 1, 0xff000000)
378
- cur_text.draw(x + 1, y, 0, 1, 1, 0xff000000)
379
- cur_text.draw(x, y - 1, 0, 1, 1, 0xff000000)
380
- cur_text.draw(x, y + 1, 0, 1, 1, 0xff000000)
381
- cur_text.draw(x, y, 0, 1, 1, 0xffffffff)
382
- end
383
- end
384
-
385
- def update
386
- # if waiting for the next player's turn, continue to do so until the missile has
387
- # hit something.
388
- @waiting &&= !@objects.grep(Missile).empty?
389
-
390
- # Remove all objects whose update method returns false.
391
- @objects.reject! { |o| o.update == false }
392
-
393
- # If it's a player's turn, forward controls.
394
- if not @waiting and not @players[@current_player].dead then
395
- player = @players[@current_player]
396
- player.aim_up if button_down? Gosu::KbUp
397
- player.aim_down if button_down? Gosu::KbDown
398
- player.try_walk(-1) if button_down? Gosu::KbLeft
399
- player.try_walk(+1) if button_down? Gosu::KbRight
400
- player.try_jump if button_down? Gosu::KbReturn
401
- end
402
- end
403
-
404
- def button_down(id)
405
- if id == Gosu::KbSpace and not @waiting and not @players[@current_player].dead then
406
- # Shoot! This is handled in button_down because holding space shouldn't auto-fire.
407
- @players[@current_player].shoot
408
- @current_player = 1 - @current_player
409
- @waiting = true
410
- end
411
- # Very important feature! ;)
412
- close if id == Gosu::KbEscape
413
- end
414
- end
415
-
416
- # So far we have only defined how everything *should* work - now set it up and run it!
417
- GameWindow.new.show
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
+ require 'rubygems'
15
+ require 'gosu'
16
+ require 'RMagick'
17
+
18
+ NULL_PIXEL = Magick::Pixel.from_color('none')
19
+
20
+ # The class for this game's map.
21
+ # Design:
22
+ # * Dynamic map creation at startup, holding it as RMagick Image in @image
23
+ # * Testing for solidity by testing @image's pixel values
24
+ # * Drawing from a Gosu::Image instance
25
+ # * Blasting holes into the map is implemented by drawing and erasing portions
26
+ # of @image, then recreating the corresponding area in the Gosu::Image
27
+
28
+ class Map
29
+ WIDTH, HEIGHT = 800, 600
30
+
31
+ def initialize window
32
+ # We'll need the window later for re-creating Gosu images.
33
+ @window = window
34
+
35
+ # Let's start with something simple and load the sky via RMagick.
36
+ # Loading SVG files isn't possible with Gosu, so say wow!
37
+ # (Seems to take a while though)
38
+ sky = Magick::Image.read("media/Landscape.svg").first
39
+ @sky = Gosu::Image.new(window, sky, true)
40
+
41
+ # Create the map an stores the RMagick image in @image
42
+ create_rmagick_map
43
+
44
+ # Copy the RMagick Image to a Gosu Image (still unchanged)
45
+ @gosu_image = Gosu::Image.new(window, @image, true)
46
+ end
47
+
48
+ def solid? x, y
49
+ # Map is open at the top.
50
+ return false if y < 0
51
+ # Map is closed on all other sides.
52
+ return true if x < 0 or x >= 800 or y >= 600
53
+ # Inside of the map, determine solidity from the map image.
54
+ @image.pixel_color(x, y) != NULL_PIXEL
55
+ end
56
+
57
+ def draw
58
+ # Sky background.
59
+ @sky.draw(0, 0, 0)
60
+ # The landscape.
61
+ @gosu_image.draw 0, 0, 0
62
+ end
63
+
64
+ # Radius of a crater.
65
+ RADIUS = 25
66
+ # Radius of a crater, Shadow included.
67
+ SH_RADIUS = 45
68
+
69
+ # Create the crater image (basically a circle shape that is used to erase
70
+ # parts of the map) and the crater shadow image.
71
+ CRATER_IMAGE = begin
72
+ crater = Magick::Image.new(2 * RADIUS, 2 * RADIUS) { self.background_color = 'none' }
73
+ gc = Magick::Draw.new
74
+ gc.fill('black').circle(RADIUS, RADIUS, RADIUS, 0)
75
+ gc.draw crater
76
+ crater
77
+ end
78
+ CRATER_SHADOW = CRATER_IMAGE.shadow(0, 0, (SH_RADIUS - RADIUS) / 2, 1)
79
+
80
+ def blast x, y
81
+ # Draw the shadow (twice for more intensity), then erase a circle from the map.
82
+ @image.composite!(CRATER_SHADOW, x - SH_RADIUS, y - SH_RADIUS, Magick::AtopCompositeOp)
83
+ @image.composite!(CRATER_SHADOW, x - SH_RADIUS, y - SH_RADIUS, Magick::AtopCompositeOp)
84
+ @image.composite!(CRATER_IMAGE, x - RADIUS, y - RADIUS, Magick::DstOutCompositeOp)
85
+
86
+ # Isolate the affected portion of the RMagick image.
87
+ dirty_portion = @image.crop(x - SH_RADIUS, y - SH_RADIUS, SH_RADIUS * 2, SH_RADIUS * 2)
88
+ # Overwrite this part of the Gosu image. If the crater begins outside of the map, still
89
+ # just update the inner part.
90
+ @gosu_image.insert dirty_portion, [x - SH_RADIUS, 0].max, [y - SH_RADIUS, 0].max
91
+ end
92
+
93
+ private
94
+
95
+ def create_rmagick_map
96
+ # This is the one large RMagick image that represents the map.
97
+ @image = Magick::Image.new(WIDTH, HEIGHT) { self.background_color = 'none' }
98
+
99
+ # Set up a Draw object that fills with an earth texture.
100
+ earth = Magick::Image.read('media/Earth.png').first.resize(1.5)
101
+ gc = Magick::Draw.new
102
+ gc.pattern('earth', 0, 0, earth.columns, earth.rows) { gc.composite(0, 0, 0, 0, earth) }
103
+ gc.fill('earth')
104
+ gc.stroke('#603000').stroke_width(1.5)
105
+ # Draw a smooth bezier island onto the map!
106
+ polypoints = [0, HEIGHT]
107
+ 0.upto(8) do |x|
108
+ polypoints += [x * 100, HEIGHT * 0.2 + rand(HEIGHT * 0.8)]
109
+ end
110
+ polypoints += [WIDTH, HEIGHT]
111
+ gc.bezier(*polypoints)
112
+ gc.draw(@image)
113
+
114
+ # Create a bright-dark gradient fill, an image from it and change the map's
115
+ # brightness with it.
116
+ fill = Magick::GradientFill.new(0, HEIGHT * 0.4, WIDTH, HEIGHT * 0.4, '#fff', '#666')
117
+ gradient = Magick::Image.new(WIDTH, HEIGHT, fill)
118
+ gradient = @image.composite(gradient, 0, 0, Magick::InCompositeOp)
119
+ @image.composite!(gradient, 0, 0, Magick::MultiplyCompositeOp)
120
+
121
+ # Finally, place the star in the middle of the map, just onto the ground.
122
+ star = Magick::Image.read('media/LargeStar.png').first
123
+ star_y = 0
124
+ star_y += 20 until solid?(WIDTH / 2, star_y)
125
+ @image.composite!(star, (WIDTH - star.columns) / 2, star_y - star.rows * 0.85,
126
+ Magick::DstOverCompositeOp)
127
+ end
128
+ end
129
+
130
+ # Player class.
131
+ # Note that applies to the whole game:
132
+ # All objects implement an informal interface.
133
+ # draw: Draws the object (obviously)
134
+ # update: Moves the object etc., returns false if the object is to be deleted
135
+ # hit_by?(missile): Returns true if an object is hit by the missile, causing
136
+ # it to explode on this object.
137
+
138
+ class Player
139
+ # Magic numbers considered harmful! This is the height of the
140
+ # player as used for collision detection.
141
+ HEIGHT = 14
142
+
143
+ attr_reader :x, :y, :dead
144
+
145
+ def initialize(window, x, y, color)
146
+ # Only load the images once for all instances of this class.
147
+ @@images ||= Gosu::Image.load_tiles(window, "media/Soldier.png", 40, 50, false)
148
+
149
+ @window, @x, @y, @color = window, x, y, color
150
+ @vy = 0
151
+
152
+ # -1: left, +1: right
153
+ @dir = -1
154
+
155
+ # Aiming angle.
156
+ @angle = 90
157
+ end
158
+
159
+ def draw
160
+ if dead then
161
+ # Poor, broken soldier.
162
+ @@images[0].draw_rot(x, y, 0, 290 * @dir, 0.5, 0.65, @dir * 0.5, 0.5, @color)
163
+ @@images[2].draw_rot(x, y, 0, 160 * @dir, 0.95, 0.5, 0.5, @dir * 0.5, @color)
164
+ else
165
+ # Was moved last frame?
166
+ if @show_walk_anim
167
+ # Yes: Display walking animation.
168
+ frame = Gosu::milliseconds / 200 % 2
169
+ else
170
+ # No: Stand around (boring).
171
+ frame = 0
172
+ end
173
+
174
+ # Draw feet, then chest.
175
+ @@images[frame].draw(x - 10 * @dir, y - 20, 0, @dir * 0.5, 0.5, @color)
176
+ angle = @angle
177
+ angle = 180 - angle if @dir == -1
178
+ @@images[2].draw_rot(x, y - 5, 0, angle, 1, 0.5, 0.5, @dir * 0.5, @color)
179
+ end
180
+ end
181
+
182
+ def update
183
+ # First, assume that no walking happened this frame.
184
+ @show_walk_anim = false
185
+
186
+ # Gravity.
187
+ @vy += 1
188
+
189
+ if @vy > 1 then
190
+ # Move upwards until hitting something.
191
+ @vy.times do
192
+ if @window.map.solid?(x, y + 1)
193
+ @vy = 0
194
+ break
195
+ else
196
+ @y += 1
197
+ end
198
+ end
199
+ else
200
+ # Move downwards until hitting something.
201
+ (-@vy).times do
202
+ if @window.map.solid?(x, y - HEIGHT - 1)
203
+ @vy = 0
204
+ break
205
+ else
206
+ @y -= 1
207
+ end
208
+ end
209
+ end
210
+
211
+ # Soldiers are never deleted (they may die, though, but that is a different
212
+ # concept).
213
+ true
214
+ end
215
+
216
+ def aim_up
217
+ @angle -= 2 unless @angle < 10
218
+ end
219
+
220
+ def aim_down
221
+ @angle += 2 unless @angle > 170
222
+ end
223
+
224
+ def try_walk(dir)
225
+ @show_walk_anim = true
226
+ @dir = dir
227
+ # First, magically move up (so soldiers can run up hills)
228
+ 2.times { @y -= 1 unless @window.map.solid?(x, y - HEIGHT - 1) }
229
+ # Now move into the desired direction.
230
+ @x += dir unless @window.map.solid?(x + dir, y) or
231
+ @window.map.solid?(x + dir, y - HEIGHT)
232
+ # To make up for unnecessary movement upwards, sink downward again.
233
+ 2.times { @y += 1 unless @window.map.solid?(x, y + 1) }
234
+ end
235
+
236
+ def try_jump
237
+ @vy = -12 if @window.map.solid?(x, y + 1)
238
+ end
239
+
240
+ def shoot
241
+ @window.objects << Missile.new(@window, x + 10 * @dir, y - 10, @angle * @dir)
242
+ end
243
+
244
+ def hit_by? missile
245
+ if Gosu::distance(missile.x, missile.y, x, y) < 30 then
246
+ # Was hit :(
247
+ @dead = true
248
+ return true
249
+ else
250
+ return false
251
+ end
252
+ end
253
+ end
254
+
255
+ # Implements the same interface as Player, except it'S a missile!
256
+
257
+ class Missile
258
+ attr_reader :x, :y, :vx, :vy
259
+
260
+ # All missile instances use the same sound.
261
+ EXPLOSION = Gosu::Sample.new("media/Explosion.wav")
262
+
263
+ def initialize(window, x, y, angle)
264
+ # Horizontal/vertical velocity.
265
+ @vx, @vy = Gosu::offset_x(angle, 20).to_i, Gosu::offset_y(angle, 20).to_i
266
+
267
+ @window, @x, @y = window, x + @vx, y + @vy
268
+ end
269
+
270
+ def update
271
+ # Movement, gravity
272
+ @x += @vx
273
+ @y += @vy
274
+ @vy += 1
275
+ # Hit anything?
276
+ if @window.map.solid?(x, y) or @window.objects.any? { |o| o.hit_by?(self) } then
277
+ # Create great particles.
278
+ 5.times { @window.objects << Particle.new(@window, x - 25 + rand(51), y - 25 + rand(51)) }
279
+ @window.map.blast(x, y)
280
+ # Weeee, stereo sound!
281
+ EXPLOSION.play_pan((2 * @x - Map::WIDTH) / Map::WIDTH)
282
+ return false
283
+ else
284
+ return true
285
+ end
286
+ end
287
+
288
+ def draw
289
+ # Just draw a small quad.
290
+ @window.draw_quad(x-2, y-2, 0xff800000, x+2, y-2, 0xff800000,
291
+ x-2, y+2, 0xff800000, x+2, y+2, 0xff800000, 0)
292
+ end
293
+
294
+ def hit_by?(missile)
295
+ # Missiles can't be hit by other missiles!
296
+ false
297
+ end
298
+ end
299
+
300
+ # Very minimal object that just draws a fading particle.
301
+
302
+ class Particle
303
+ def initialize(window, x, y)
304
+ # All Particle instances use the same image
305
+ @@image ||= Gosu::Image.new(window, 'media/Smoke.png', false)
306
+
307
+ @x, @y = x, y
308
+ @color = Gosu::Color.new(255, 255, 255, 255)
309
+ end
310
+
311
+ def update
312
+ @y -= 5
313
+ @x = @x - 1 + rand(3)
314
+ @color.alpha -= 5
315
+
316
+ # Remove if faded completely.
317
+ @color.alpha > 0
318
+ end
319
+
320
+ def draw
321
+ @@image.draw(@x - 25, @y - 25, 0, 1, 1, @color)
322
+ end
323
+
324
+ def hit_by?(missile)
325
+ # Smoke can't be hit!
326
+ false
327
+ end
328
+ end
329
+
330
+ # Finally, the class that ties it all together.
331
+ # Very straightforward implementation.
332
+
333
+ class GameWindow < Gosu::Window
334
+ attr_reader :map, :objects
335
+
336
+ def initialize()
337
+ super(800, 600, false)
338
+ self.caption = "Gosu & RMagick Integration Demo"
339
+
340
+ # Texts to display in the appropriate situations.
341
+ @player_instructions = []
342
+ @player_won_messages = []
343
+ 2.times do |plr|
344
+ @player_instructions << Gosu::Image.from_text(self,
345
+ "It is the #{ plr == 0 ? 'green' : 'red' } toy soldier's turn.\n" +
346
+ "(Arrow keys to walk and aim, Return to jump, Space to shoot)",
347
+ Gosu::default_font_name, 30, 0, width, :center)
348
+ @player_won_messages << Gosu::Image.from_text(self,
349
+ "The #{ plr == 0 ? 'green' : 'red' } toy soldier has won!",
350
+ Gosu::default_font_name, 30, 5, width, :center)
351
+ end
352
+
353
+ # Create everything!
354
+ @map = Map.new(self)
355
+ @players = [Player.new(self, 200, 40, 0xff308000), Player.new(self, 600, 40, 0xff803000)]
356
+ @objects = @players.dup
357
+
358
+ # Let any player start.
359
+ @current_player = rand(2)
360
+ # Currently not waiting for a missile to hit something.
361
+ @waiting = false
362
+ end
363
+
364
+ def draw
365
+ # Draw the main game.
366
+ @map.draw
367
+ @objects.each { |o| o.draw }
368
+
369
+ # If any text should be displayed, draw it - and add a nice black border around it
370
+ # by drawing it four times, with a little offset in each direction.
371
+
372
+ cur_text = @player_instructions[@current_player] if not @waiting
373
+ cur_text = @player_won_messages[1 - @current_player] if @players[@current_player].dead
374
+
375
+ if cur_text then
376
+ x, y = 0, 30
377
+ cur_text.draw(x - 1, y, 0, 1, 1, 0xff000000)
378
+ cur_text.draw(x + 1, y, 0, 1, 1, 0xff000000)
379
+ cur_text.draw(x, y - 1, 0, 1, 1, 0xff000000)
380
+ cur_text.draw(x, y + 1, 0, 1, 1, 0xff000000)
381
+ cur_text.draw(x, y, 0, 1, 1, 0xffffffff)
382
+ end
383
+ end
384
+
385
+ def update
386
+ # if waiting for the next player's turn, continue to do so until the missile has
387
+ # hit something.
388
+ @waiting &&= !@objects.grep(Missile).empty?
389
+
390
+ # Remove all objects whose update method returns false.
391
+ @objects.reject! { |o| o.update == false }
392
+
393
+ # If it's a player's turn, forward controls.
394
+ if not @waiting and not @players[@current_player].dead then
395
+ player = @players[@current_player]
396
+ player.aim_up if button_down? Gosu::KbUp
397
+ player.aim_down if button_down? Gosu::KbDown
398
+ player.try_walk(-1) if button_down? Gosu::KbLeft
399
+ player.try_walk(+1) if button_down? Gosu::KbRight
400
+ player.try_jump if button_down? Gosu::KbReturn
401
+ end
402
+ end
403
+
404
+ def button_down(id)
405
+ if id == Gosu::KbSpace and not @waiting and not @players[@current_player].dead then
406
+ # Shoot! This is handled in button_down because holding space shouldn't auto-fire.
407
+ @players[@current_player].shoot
408
+ @current_player = 1 - @current_player
409
+ @waiting = true
410
+ end
411
+ # Very important feature! ;)
412
+ close if id == Gosu::KbEscape
413
+ end
414
+ end
415
+
416
+ # So far we have only defined how everything *should* work - now set it up and run it!
417
+ GameWindow.new.show