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.
- checksums.yaml +4 -4
- data/Gosu/Audio.hpp +171 -171
- data/Gosu/Bitmap.hpp +96 -96
- data/Gosu/Color.hpp +204 -204
- data/Gosu/Directories.hpp +36 -36
- data/Gosu/Font.hpp +83 -83
- data/Gosu/Gosu.hpp +34 -34
- data/Gosu/Graphics.hpp +115 -115
- data/Gosu/GraphicsBase.hpp +110 -110
- data/Gosu/IO.hpp +269 -269
- data/Gosu/Image.hpp +122 -122
- data/Gosu/ImageData.hpp +61 -61
- data/Gosu/Input.hpp +149 -149
- data/Gosu/Inspection.hpp +14 -14
- data/Gosu/Math.hpp +135 -135
- data/Gosu/Platform.hpp +93 -93
- data/Gosu/Sockets.hpp +156 -156
- data/Gosu/TR1.hpp +56 -56
- data/Gosu/Text.hpp +71 -71
- data/Gosu/TextInput.hpp +70 -70
- data/Gosu/Utility.hpp +28 -28
- data/Gosu/Version.hpp +19 -19
- data/Gosu/Window.hpp +145 -145
- data/examples/ChipmunkIntegration.rb +275 -275
- data/examples/CptnRuby.rb +223 -223
- data/examples/GosuZen.rb +68 -68
- data/examples/MoreChipmunkAndRMagick.rb +155 -155
- data/examples/OpenGLIntegration.rb +225 -225
- data/examples/RMagickIntegration.rb +417 -417
- data/examples/TextInput.rb +154 -154
- data/examples/Tutorial.rb +130 -130
- data/examples/media/Beep.wav +0 -0
- data/examples/media/CptnRuby Map.txt b/data/examples/media/CptnRuby → Map.txt +0 -0
- data/examples/media/Explosion.wav +0 -0
- data/examples/media/Landscape.svg +9 -9
- data/examples/media/Space.png +0 -0
- data/examples/media/Star.png +0 -0
- data/examples/media/Starfighter.bmp +0 -0
- data/lib/1.8/gosu.so +0 -0
- data/lib/1.9/gosu.so +0 -0
- data/lib/2.0/gosu.so +0 -0
- data/lib/2.1/gosu.so +0 -0
- data/lib/FreeImage.dll +0 -0
- data/lib/OpenAL32.dll +0 -0
- data/lib/gosu.rb +19 -16
- data/lib/gosu/patches.rb +81 -81
- data/lib/gosu/preview.rb +143 -139
- data/lib/gosu/run.rb +11 -11
- data/lib/gosu/swig_patches.rb +60 -60
- data/lib/gosu/zen.rb +89 -89
- 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
|