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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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