gosu 0.8.7.2 → 0.9.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/COPYING +29 -0
- data/Gosu/Audio.hpp +1 -0
- data/Gosu/Font.hpp +8 -4
- data/Gosu/Graphics.hpp +28 -18
- data/Gosu/GraphicsBase.hpp +17 -13
- data/Gosu/Image.hpp +54 -45
- data/Gosu/Input.hpp +5 -5
- data/Gosu/Version.hpp +3 -3
- data/Gosu/Window.hpp +1 -8
- data/README.txt +25 -0
- data/ext/gosu/gosu_wrap.cxx +1495 -1275
- data/ext/gosu/gosu_wrap.h +2 -2
- data/lib/gosu/patches.rb +71 -35
- data/lib/gosu/preview.rb +9 -142
- data/lib/gosu/swig_patches.rb +20 -10
- data/rdoc/gosu.rb +1185 -0
- data/src/Bitmap/BitmapUtils.cpp +9 -9
- data/src/Graphics/Common.hpp +3 -1
- data/src/Graphics/Graphics.cpp +100 -38
- data/src/Graphics/Image.cpp +57 -15
- data/src/Graphics/LargeImageData.cpp +9 -10
- data/src/Graphics/LargeImageData.hpp +1 -1
- data/src/Graphics/TexChunk.cpp +5 -6
- data/src/Graphics/TexChunk.hpp +1 -4
- data/src/Graphics/Texture.cpp +10 -3
- data/src/Graphics/Texture.hpp +1 -2
- data/src/Iconv.hpp +2 -2
- data/src/Input/Input.cpp +25 -9
- data/src/Input/InputTouch.mm +5 -3
- data/src/Text/Font.cpp +13 -8
- data/src/Window.cpp +66 -37
- data/src/WindowTouch.mm +3 -3
- metadata +79 -92
- data/examples/ChipmunkIntegration.rb +0 -275
- data/examples/CptnRuby.rb +0 -223
- data/examples/GosuZen.rb +0 -68
- data/examples/MoreChipmunkAndRMagick.rb +0 -155
- data/examples/OpenGLIntegration.rb +0 -226
- data/examples/RMagickIntegration.rb +0 -417
- data/examples/TextInput.rb +0 -154
- data/examples/Tutorial.rb +0 -131
- data/examples/media/Beep.wav +0 -0
- data/examples/media/CptnRuby Gem.png +0 -0
- data/examples/media/CptnRuby Map.txt +0 -25
- data/examples/media/CptnRuby Tileset.png +0 -0
- data/examples/media/CptnRuby.png +0 -0
- data/examples/media/Cursor.png +0 -0
- data/examples/media/Earth.png +0 -0
- data/examples/media/Explosion.wav +0 -0
- data/examples/media/Landscape.svg +0 -10
- data/examples/media/LargeStar.png +0 -0
- data/examples/media/Smoke.png +0 -0
- data/examples/media/Soldier.png +0 -0
- data/examples/media/Space.png +0 -0
- data/examples/media/Star.png +0 -0
- data/examples/media/Starfighter.bmp +0 -0
@@ -1,417 +0,0 @@
|
|
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
|
data/examples/TextInput.rb
DELETED
@@ -1,154 +0,0 @@
|
|
1
|
-
# This example demonstrates the use of the TextInput functionality.
|
2
|
-
# One can tab through, or click into the text fields and change it's contents.
|
3
|
-
|
4
|
-
# At its most basic form, you only need to create a new TextInput instance and
|
5
|
-
# set the text_input attribute of your window to it. Until you set this
|
6
|
-
# attribute to nil again, the TextInput object will build a text that can be
|
7
|
-
# accessed via TextInput#text.
|
8
|
-
|
9
|
-
# The TextInput object also maintains the position of the caret as the index
|
10
|
-
# of the character that it's left to via the caret_pos attribute. Furthermore,
|
11
|
-
# if there is a selection, the selection_start attribute yields its beginning,
|
12
|
-
# using the same indexing scheme. If there is no selection, selection_start
|
13
|
-
# is equal to caret_pos.
|
14
|
-
|
15
|
-
# A TextInput object is purely abstract, though; drawing the input field is left
|
16
|
-
# to the user. In this case, we are subclassing TextInput to add this code.
|
17
|
-
# As with most of Gosu, how this is handled is completely left open; the scheme
|
18
|
-
# presented here is not mandatory! Gosu only aims to provide enough code for
|
19
|
-
# games (or intermediate UI toolkits) to be built upon it.
|
20
|
-
|
21
|
-
require 'rubygems'
|
22
|
-
require 'gosu'
|
23
|
-
|
24
|
-
class TextField < Gosu::TextInput
|
25
|
-
# Some constants that define our appearance.
|
26
|
-
INACTIVE_COLOR = 0xcc666666
|
27
|
-
ACTIVE_COLOR = 0xccff6666
|
28
|
-
SELECTION_COLOR = 0xcc0000ff
|
29
|
-
CARET_COLOR = 0xffffffff
|
30
|
-
PADDING = 5
|
31
|
-
|
32
|
-
attr_reader :x, :y
|
33
|
-
|
34
|
-
def initialize(window, font, x, y)
|
35
|
-
# TextInput's constructor doesn't expect any arguments.
|
36
|
-
super()
|
37
|
-
|
38
|
-
@window, @font, @x, @y = window, font, x, y
|
39
|
-
|
40
|
-
# Start with a self-explanatory text in each field.
|
41
|
-
self.text = "Click to change text"
|
42
|
-
end
|
43
|
-
|
44
|
-
# Example filter method. You can truncate the text to employ a length limit (watch out
|
45
|
-
# with Ruby 1.8 and UTF-8!), limit the text to certain characters etc.
|
46
|
-
def filter text
|
47
|
-
text.upcase
|
48
|
-
end
|
49
|
-
|
50
|
-
def draw
|
51
|
-
# Depending on whether this is the currently selected input or not, change the
|
52
|
-
# background's color.
|
53
|
-
if @window.text_input == self then
|
54
|
-
background_color = ACTIVE_COLOR
|
55
|
-
else
|
56
|
-
background_color = INACTIVE_COLOR
|
57
|
-
end
|
58
|
-
@window.draw_quad(x - PADDING, y - PADDING, background_color,
|
59
|
-
x + width + PADDING, y - PADDING, background_color,
|
60
|
-
x - PADDING, y + height + PADDING, background_color,
|
61
|
-
x + width + PADDING, y + height + PADDING, background_color, 0)
|
62
|
-
|
63
|
-
# Calculate the position of the caret and the selection start.
|
64
|
-
pos_x = x + @font.text_width(self.text[0...self.caret_pos])
|
65
|
-
sel_x = x + @font.text_width(self.text[0...self.selection_start])
|
66
|
-
|
67
|
-
# Draw the selection background, if any; if not, sel_x and pos_x will be
|
68
|
-
# the same value, making this quad empty.
|
69
|
-
@window.draw_quad(sel_x, y, SELECTION_COLOR,
|
70
|
-
pos_x, y, SELECTION_COLOR,
|
71
|
-
sel_x, y + height, SELECTION_COLOR,
|
72
|
-
pos_x, y + height, SELECTION_COLOR, 0)
|
73
|
-
|
74
|
-
# Draw the caret; again, only if this is the currently selected field.
|
75
|
-
if @window.text_input == self then
|
76
|
-
@window.draw_line(pos_x, y, CARET_COLOR,
|
77
|
-
pos_x, y + height, CARET_COLOR, 0)
|
78
|
-
end
|
79
|
-
|
80
|
-
# Finally, draw the text itself!
|
81
|
-
@font.draw(self.text, x, y, 0)
|
82
|
-
end
|
83
|
-
|
84
|
-
# This text field grows with the text that's being entered.
|
85
|
-
# (Usually one would use clip_to and scroll around on the text field.)
|
86
|
-
def width
|
87
|
-
@font.text_width(self.text)
|
88
|
-
end
|
89
|
-
|
90
|
-
def height
|
91
|
-
@font.height
|
92
|
-
end
|
93
|
-
|
94
|
-
# Hit-test for selecting a text field with the mouse.
|
95
|
-
def under_point?(mouse_x, mouse_y)
|
96
|
-
mouse_x > x - PADDING and mouse_x < x + width + PADDING and
|
97
|
-
mouse_y > y - PADDING and mouse_y < y + height + PADDING
|
98
|
-
end
|
99
|
-
|
100
|
-
# Tries to move the caret to the position specifies by mouse_x
|
101
|
-
def move_caret(mouse_x)
|
102
|
-
# Test character by character
|
103
|
-
1.upto(self.text.length) do |i|
|
104
|
-
if mouse_x < x + @font.text_width(text[0...i]) then
|
105
|
-
self.caret_pos = self.selection_start = i - 1;
|
106
|
-
return
|
107
|
-
end
|
108
|
-
end
|
109
|
-
# Default case: user must have clicked the right edge
|
110
|
-
self.caret_pos = self.selection_start = self.text.length
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
class TextInputWindow < Gosu::Window
|
115
|
-
def initialize
|
116
|
-
super(300, 200, false)
|
117
|
-
self.caption = "Text Input Example"
|
118
|
-
|
119
|
-
font = Gosu::Font.new(self, Gosu::default_font_name, 20)
|
120
|
-
|
121
|
-
# Set up an array of three text fields.
|
122
|
-
@text_fields = Array.new(3) { |index| TextField.new(self, font, 50, 30 + index * 50) }
|
123
|
-
|
124
|
-
@cursor = Gosu::Image.new(self, "media/Cursor.png", false)
|
125
|
-
end
|
126
|
-
|
127
|
-
def draw
|
128
|
-
@text_fields.each { |tf| tf.draw }
|
129
|
-
@cursor.draw(mouse_x, mouse_y, 0)
|
130
|
-
end
|
131
|
-
|
132
|
-
def button_down(id)
|
133
|
-
if id == Gosu::KbTab then
|
134
|
-
# Tab key will not be 'eaten' by text fields; use for switching through
|
135
|
-
# text fields.
|
136
|
-
index = @text_fields.index(self.text_input) || -1
|
137
|
-
self.text_input = @text_fields[(index + 1) % @text_fields.size]
|
138
|
-
elsif id == Gosu::KbEscape then
|
139
|
-
# Escape key will not be 'eaten' by text fields; use for deselecting.
|
140
|
-
if self.text_input then
|
141
|
-
self.text_input = nil
|
142
|
-
else
|
143
|
-
close
|
144
|
-
end
|
145
|
-
elsif id == Gosu::MsLeft then
|
146
|
-
# Mouse click: Select text field based on mouse position.
|
147
|
-
self.text_input = @text_fields.find { |tf| tf.under_point?(mouse_x, mouse_y) }
|
148
|
-
# Advanced: Move caret to clicked position
|
149
|
-
self.text_input.move_caret(mouse_x) unless self.text_input.nil?
|
150
|
-
end
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
TextInputWindow.new.show
|