gosu-examples 1.0.3

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