ruck 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +21 -0
- data/README.markdown +146 -0
- data/Rakefile +11 -0
- data/VERSION +1 -1
- data/examples/ex01.rb +27 -0
- data/examples/ex02.rb +31 -0
- data/examples/ex03.rb +75 -0
- data/examples/ex04.rb +33 -0
- data/examples/ex05.rb +33 -0
- data/examples/ex06.rb +110 -0
- data/examples/space/media/Beep.wav +0 -0
- data/examples/space/media/Space.png +0 -0
- data/examples/space/media/Star.png +0 -0
- data/examples/space/media/Starfighter.bmp +0 -0
- data/examples/space/space.rb +307 -0
- data/lib/ruck.rb +4 -4
- data/lib/ruck/clock.rb +137 -0
- data/lib/ruck/event_clock.rb +39 -0
- data/lib/ruck/shred.rb +135 -0
- data/lib/ruck/shreduler.rb +149 -0
- data/ruck.gemspec +45 -8
- data/spec/clock_spec.rb +218 -0
- data/spec/shred_spec.rb +116 -0
- data/spec/shreduler_spec.rb +200 -0
- metadata +58 -12
- data/README +0 -48
- data/lib/ruck/shreduling.rb +0 -135
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,307 @@
|
|
1
|
+
## Notes from the original file:
|
2
|
+
## Author: Dirk Johnson
|
3
|
+
## Version: 1.0.0
|
4
|
+
## Date: 2007-10-05
|
5
|
+
## License: Same as for Gosu (MIT)
|
6
|
+
## Comments: Based on the Gosu Ruby Tutorial, but incorporating the Chipmunk Physics Engine
|
7
|
+
## See http://code.google.com/p/gosu/wiki/RubyChipmunkIntegration for the accompanying text.
|
8
|
+
## originally http://gosu.googlecode.com/svn/trunk/examples/ChipmunkIntegration.rb
|
9
|
+
|
10
|
+
## ruck adaptation by Tom Lieber, 2010-08-01
|
11
|
+
##
|
12
|
+
## all of these are now handled by shreds in a GameShreduler:
|
13
|
+
## - physics updates
|
14
|
+
## - star animation
|
15
|
+
## - adding stars
|
16
|
+
## - drawing
|
17
|
+
## - framerate calculation
|
18
|
+
|
19
|
+
require "rubygems"
|
20
|
+
require "gosu"
|
21
|
+
require "chipmunk"
|
22
|
+
require "ruck"
|
23
|
+
|
24
|
+
include Ruck
|
25
|
+
|
26
|
+
SCREEN_WIDTH = 640
|
27
|
+
SCREEN_HEIGHT = 480
|
28
|
+
|
29
|
+
# Amount of time in seconds between physics steps.
|
30
|
+
# (6 times per frame at 60fps)
|
31
|
+
PHYSICS_STEP_SIZE = (1.0 / 60.0) / 6.0
|
32
|
+
|
33
|
+
# a real-time shreduler that expects you to wake it up every now
|
34
|
+
# and then with GameShreduler#catch_up, when it'll run all the
|
35
|
+
# shreds that were scheduled for the time period between the last
|
36
|
+
# call and now. it's not very accurate unless you're calling it often.
|
37
|
+
class GameShreduler < Ruck::Shreduler
|
38
|
+
def start_time
|
39
|
+
@start_time ||= Time.now
|
40
|
+
end
|
41
|
+
|
42
|
+
def actual_now
|
43
|
+
Time.now - start_time
|
44
|
+
end
|
45
|
+
|
46
|
+
def catch_up
|
47
|
+
run_until actual_now
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Numeric
|
52
|
+
# convenience method for converting from radians to a unit Vec2
|
53
|
+
def radians_to_vec2
|
54
|
+
CP::Vec2.new(Math::cos(self), Math::sin(self))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
module ZOrder
|
59
|
+
# sprite layers
|
60
|
+
Background, Stars, Player, UI = *0..3
|
61
|
+
end
|
62
|
+
|
63
|
+
class Ship
|
64
|
+
attr_reader :shape
|
65
|
+
|
66
|
+
def initialize(window, shape)
|
67
|
+
@image = Gosu::Image.new(window, "media/Starfighter.bmp", false)
|
68
|
+
@shape = shape
|
69
|
+
@shape.body.p = CP::Vec2.new(0.0, 0.0) # position
|
70
|
+
@shape.body.v = CP::Vec2.new(0.0, 0.0) # velocity
|
71
|
+
|
72
|
+
# Keep in mind that down the screen is positive y, which means that PI/2 radians,
|
73
|
+
# which you might consider the top in the traditional Trig unit circle sense is actually
|
74
|
+
# the bottom; thus 3PI/2 is the top
|
75
|
+
@shape.body.a = (3*Math::PI/2.0) # angle in radians; faces towards top of screen
|
76
|
+
end
|
77
|
+
|
78
|
+
# Directly set the position of our ship
|
79
|
+
def warp(vect)
|
80
|
+
@shape.body.p = vect
|
81
|
+
end
|
82
|
+
|
83
|
+
# Apply negative torque
|
84
|
+
def turn_left
|
85
|
+
@shape.body.t -= 900.0
|
86
|
+
end
|
87
|
+
|
88
|
+
# Apply positive torque
|
89
|
+
def turn_right
|
90
|
+
@shape.body.t += 900.0
|
91
|
+
end
|
92
|
+
|
93
|
+
# Apply forward force
|
94
|
+
def accelerate
|
95
|
+
@shape.body.apply_force((@shape.body.a.radians_to_vec2 * 3000.0), CP::Vec2.new(0.0, 0.0))
|
96
|
+
end
|
97
|
+
|
98
|
+
# Apply even more forward force
|
99
|
+
def boost
|
100
|
+
@shape.body.apply_force((@shape.body.a.radians_to_vec2 * 6000.0), CP::Vec2.new(0.0, 0.0))
|
101
|
+
end
|
102
|
+
|
103
|
+
# Apply reverse force
|
104
|
+
def reverse
|
105
|
+
@shape.body.apply_force(-(@shape.body.a.radians_to_vec2 * 3000.0), CP::Vec2.new(0.0, 0.0))
|
106
|
+
end
|
107
|
+
|
108
|
+
# Wrap to the other side of the screen when we fly off the edge
|
109
|
+
def validate_position
|
110
|
+
@shape.body.p = CP::Vec2.new(@shape.body.p.x % SCREEN_WIDTH, @shape.body.p.y % SCREEN_HEIGHT)
|
111
|
+
end
|
112
|
+
|
113
|
+
def draw
|
114
|
+
@image.draw_rot(@shape.body.p.x, @shape.body.p.y, ZOrder::Player, @shape.body.a.radians_to_gosu)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# animated star sprite
|
119
|
+
class Star
|
120
|
+
attr_reader :shape
|
121
|
+
|
122
|
+
def initialize(animation, shape)
|
123
|
+
@animation = animation
|
124
|
+
@color = Gosu::Color.new(0xff000000)
|
125
|
+
@color.red = rand(255 - 40) + 40
|
126
|
+
@color.green = rand(255 - 40) + 40
|
127
|
+
@color.blue = rand(255 - 40) + 40
|
128
|
+
@shape = shape
|
129
|
+
@shape.body.p = CP::Vec2.new(rand * SCREEN_WIDTH, rand * SCREEN_HEIGHT) # position
|
130
|
+
@shape.body.v = CP::Vec2.new(0.0, 0.0) # velocity
|
131
|
+
@shape.body.a = (3*Math::PI/2.0) # angle in radians; faces towards top of screen
|
132
|
+
|
133
|
+
@frame = 0
|
134
|
+
@anim_shred = spork_loop do
|
135
|
+
@frame = (@frame + 1) % @animation.size
|
136
|
+
Shred.yield(0.1)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def draw
|
141
|
+
img = @animation[@frame]
|
142
|
+
img.draw(@shape.body.p.x - img.width / 2.0,
|
143
|
+
@shape.body.p.y - img.height / 2.0,
|
144
|
+
ZOrder::Stars, 1, 1, @color, :additive)
|
145
|
+
end
|
146
|
+
|
147
|
+
def kill
|
148
|
+
@anim_shred.kill
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
class GameWindow < Gosu::Window
|
153
|
+
def initialize
|
154
|
+
super(SCREEN_WIDTH, SCREEN_HEIGHT, false, 16)
|
155
|
+
self.caption = "Gosu & Chipmunk & ruck Integration Demo"
|
156
|
+
|
157
|
+
@score = 0
|
158
|
+
@stars = Array.new
|
159
|
+
@dead_star_shapes = []
|
160
|
+
|
161
|
+
ruck_start
|
162
|
+
|
163
|
+
load_resources
|
164
|
+
|
165
|
+
setup_physics
|
166
|
+
end
|
167
|
+
|
168
|
+
def ruck_start
|
169
|
+
@shreduler = GameShreduler.new
|
170
|
+
@shreduler.make_convenient
|
171
|
+
|
172
|
+
spork_loop(PHYSICS_STEP_SIZE) do
|
173
|
+
physics_update
|
174
|
+
end
|
175
|
+
|
176
|
+
spork_loop do
|
177
|
+
if @stars.length < 25
|
178
|
+
body = CP::Body.new(0.0001, 0.0001)
|
179
|
+
shape = CP::Shape::Circle.new(body, 25/2, CP::Vec2.new(0.0, 0.0))
|
180
|
+
shape.collision_type = :star
|
181
|
+
|
182
|
+
@space.add_body(body)
|
183
|
+
@space.add_shape(shape)
|
184
|
+
|
185
|
+
@stars.push(Star.new(@star_animation_frames, shape))
|
186
|
+
end
|
187
|
+
|
188
|
+
Shred.yield(rand * 1)
|
189
|
+
end
|
190
|
+
|
191
|
+
spork_loop do
|
192
|
+
Shred.wait_on(:frame)
|
193
|
+
|
194
|
+
@background_image.draw(0, 0, ZOrder::Background)
|
195
|
+
@ship.draw
|
196
|
+
@stars.each { |star| star.draw }
|
197
|
+
@font.draw("Score: #{@score}", 10, 10, ZOrder::UI, 1.0, 1.0, 0xffffff00)
|
198
|
+
@font.draw("Framerate: #{@framerate}", 10, 33, ZOrder::UI, 1.0, 1.0, 0xffffff00)
|
199
|
+
end
|
200
|
+
|
201
|
+
@framerate = 0.0
|
202
|
+
@frames = 0
|
203
|
+
spork_loop do
|
204
|
+
Shred.wait_on(:frame)
|
205
|
+
@frames += 1
|
206
|
+
@framerate = @frames / @shreduler.actual_now
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def load_resources
|
211
|
+
@background_image = Gosu::Image.new(self, "media/Space.png", true)
|
212
|
+
@beep = Gosu::Sample.new(self, "media/Beep.wav")
|
213
|
+
@font = Gosu::Font.new(self, Gosu::default_font_name, 20)
|
214
|
+
@star_animation_frames = Gosu::Image::load_tiles(self, "media/Star.png", 25, 25, false)
|
215
|
+
end
|
216
|
+
|
217
|
+
def setup_physics
|
218
|
+
@space = CP::Space.new
|
219
|
+
@space.damping = 0.2
|
220
|
+
|
221
|
+
# create the body and shape for the ship
|
222
|
+
body = CP::Body.new(10.0, 150.0) # mass, inertia
|
223
|
+
shape_array = [CP::Vec2.new(-25.0, -25.0), # the "top" is towards 0 radians (the right)
|
224
|
+
CP::Vec2.new(-25.0, 25.0),
|
225
|
+
CP::Vec2.new(25.0, 1.0),
|
226
|
+
CP::Vec2.new(25.0, -1.0)]
|
227
|
+
shape = CP::Shape::Poly.new(body, shape_array, CP::Vec2.new(0,0))
|
228
|
+
shape.collision_type = :ship
|
229
|
+
@space.add_body(body)
|
230
|
+
@space.add_shape(shape)
|
231
|
+
|
232
|
+
@ship = Ship.new(self, shape)
|
233
|
+
@ship.warp(CP::Vec2.new(SCREEN_WIDTH/2, SCREEN_HEIGHT/2))
|
234
|
+
|
235
|
+
# we cannot remove shapes or bodies from space within a collision closure,
|
236
|
+
# so we save them in @dead_star_shapes to be removed in physics_update
|
237
|
+
@space.add_collision_func(:ship, :star) do |ship_shape, star_shape|
|
238
|
+
@score += 10
|
239
|
+
@beep.play
|
240
|
+
@dead_star_shapes << star_shape
|
241
|
+
end
|
242
|
+
|
243
|
+
# prevent stars from bumping into one another
|
244
|
+
@space.add_collision_func(:star, :star, &nil)
|
245
|
+
end
|
246
|
+
|
247
|
+
def update
|
248
|
+
GC.enable
|
249
|
+
GC.start
|
250
|
+
GC.disable
|
251
|
+
end
|
252
|
+
|
253
|
+
def draw
|
254
|
+
@shreduler.catch_up
|
255
|
+
@shreduler.raise_all :frame
|
256
|
+
@shreduler.catch_up
|
257
|
+
end
|
258
|
+
|
259
|
+
def physics_update
|
260
|
+
@dead_star_shapes.each do |shape|
|
261
|
+
@stars.delete_if do |star|
|
262
|
+
if star.shape == shape
|
263
|
+
star.kill
|
264
|
+
true
|
265
|
+
end
|
266
|
+
end
|
267
|
+
@space.remove_body(shape.body)
|
268
|
+
@space.remove_shape(shape)
|
269
|
+
end
|
270
|
+
@dead_star_shapes.clear
|
271
|
+
|
272
|
+
# forces are cumulative, so reset them for this update
|
273
|
+
@ship.shape.body.reset_forces
|
274
|
+
|
275
|
+
# Wrap around the screen to the other side
|
276
|
+
@ship.validate_position
|
277
|
+
|
278
|
+
# apply forces to ship based on keyboard state
|
279
|
+
if button_down? Gosu::KbLeft
|
280
|
+
@ship.turn_left
|
281
|
+
end
|
282
|
+
if button_down? Gosu::KbRight
|
283
|
+
@ship.turn_right
|
284
|
+
end
|
285
|
+
|
286
|
+
if button_down? Gosu::KbUp
|
287
|
+
if ( (button_down? Gosu::KbRightShift) || (button_down? Gosu::KbLeftShift) )
|
288
|
+
@ship.boost
|
289
|
+
else
|
290
|
+
@ship.accelerate
|
291
|
+
end
|
292
|
+
elsif button_down? Gosu::KbDown
|
293
|
+
@ship.reverse
|
294
|
+
end
|
295
|
+
|
296
|
+
@space.step(PHYSICS_STEP_SIZE)
|
297
|
+
end
|
298
|
+
|
299
|
+
def button_down(id)
|
300
|
+
if id == Gosu::KbEscape
|
301
|
+
close
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
window = GameWindow.new
|
307
|
+
window.show
|
data/lib/ruck.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
|
2
2
|
module Ruck
|
3
|
-
require "logger"
|
4
|
-
LOG = Logger.new(STDOUT)
|
5
|
-
LOG.level = Logger::WARN
|
6
3
|
end
|
7
4
|
|
8
|
-
require File.join(File.dirname(__FILE__), "ruck", "
|
5
|
+
require File.join(File.dirname(__FILE__), "ruck", "clock")
|
6
|
+
require File.join(File.dirname(__FILE__), "ruck", "event_clock")
|
7
|
+
require File.join(File.dirname(__FILE__), "ruck", "shreduler")
|
8
|
+
require File.join(File.dirname(__FILE__), "ruck", "shred")
|
data/lib/ruck/clock.rb
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
|
2
|
+
require "rubygems"
|
3
|
+
require "priority_queue"
|
4
|
+
|
5
|
+
module Ruck
|
6
|
+
|
7
|
+
# Clock keeps track of occurrences on a virtual timeline. Clocks
|
8
|
+
# can be configured to run fast or slow relative to another clock
|
9
|
+
# by changing their relative_rate and providing them a parent via
|
10
|
+
# add_child_clock.
|
11
|
+
#
|
12
|
+
# Clocks and their sub-clocks always tell the same time. When
|
13
|
+
# fast_forward is called, they advance in lock-step. You should only
|
14
|
+
# call fast_forward on the root of any tree of Clocks.
|
15
|
+
#
|
16
|
+
# = A warning about fast_forward and time travel
|
17
|
+
#
|
18
|
+
# When using a Clock with no children, there's little reason to ever
|
19
|
+
# call fast_forward because in that case Clock is little more than
|
20
|
+
# a priority queue. When using a Clock with children, before ever
|
21
|
+
# changing a Clock's relative_rate, you should fast_forward to the
|
22
|
+
# VIRTUAL instant that change is meant to take place. This ensures
|
23
|
+
# that the change happens at that time and future occurrences are
|
24
|
+
# scheduled correctly.
|
25
|
+
#
|
26
|
+
# (For an example of why this is important, consider two connected
|
27
|
+
# clocks, where the child's relative_rate is 1.0. If 5 time units in,
|
28
|
+
# the relative_rate is changed to 5,000 and fast_forward(5) isn't
|
29
|
+
# called, the first 5 time units of the child's clock are also
|
30
|
+
# affected by the change, and some occurrences could afterward take
|
31
|
+
# place at t < 5.)
|
32
|
+
|
33
|
+
class Clock
|
34
|
+
attr_reader :now # current time in this clock's units
|
35
|
+
attr_accessor :relative_rate # rate relative to parent clock
|
36
|
+
|
37
|
+
def initialize(relative_rate = 1.0)
|
38
|
+
@relative_rate = relative_rate
|
39
|
+
@now = 0
|
40
|
+
@children = []
|
41
|
+
@occurrences = PriorityQueue.new
|
42
|
+
end
|
43
|
+
|
44
|
+
# fast-forward this clock and all children clocks by the given time delta
|
45
|
+
def fast_forward(dt)
|
46
|
+
adjusted_dt = dt * @relative_rate
|
47
|
+
@now += adjusted_dt
|
48
|
+
@children.each { |sub_clock| sub_clock.fast_forward(adjusted_dt) }
|
49
|
+
end
|
50
|
+
|
51
|
+
# adds the given clock as a child of this one. a clock should only be
|
52
|
+
# the child of one other clock, please.
|
53
|
+
def add_child_clock(clock)
|
54
|
+
@children << clock
|
55
|
+
clock
|
56
|
+
end
|
57
|
+
|
58
|
+
# schedules an occurrence at the given time with the given object,
|
59
|
+
# defaulting to the current time
|
60
|
+
def schedule(obj, time = nil)
|
61
|
+
@occurrences[obj] = time || now
|
62
|
+
end
|
63
|
+
|
64
|
+
# dequeues the earliest occurrence from this clock or any child clocks.
|
65
|
+
# returns nil if it wasn't there, or its relative_time otherwise
|
66
|
+
def unschedule(obj)
|
67
|
+
if @occurrences[obj]
|
68
|
+
obj, time = @occurrences.delete obj
|
69
|
+
unscale_time(time)
|
70
|
+
else
|
71
|
+
relative_time = @children.first_non_nil { |clock| clock.unschedule(obj) }
|
72
|
+
unscale_relative_time(relative_time) if relative_time
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# returns [obj, relative_time], where relative_time is the offset from
|
77
|
+
# now in parent's time units
|
78
|
+
def next
|
79
|
+
clock, (obj, relative_time) = next_with_clock
|
80
|
+
[obj, relative_time] if obj
|
81
|
+
end
|
82
|
+
|
83
|
+
# unschedules and returns the next object as [obj, relative_time],
|
84
|
+
# where relative_time is the offset from now in parent's time units
|
85
|
+
def unschedule_next
|
86
|
+
clock, (obj, relative_time) = next_with_clock
|
87
|
+
if obj
|
88
|
+
clock.unschedule(obj)
|
89
|
+
[obj, relative_time]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
protected
|
94
|
+
|
95
|
+
# returns [clock, [obj, relative_time]]
|
96
|
+
def next_with_clock
|
97
|
+
min = nil
|
98
|
+
|
99
|
+
if @occurrences.length > 0
|
100
|
+
obj, time = @occurrences.min
|
101
|
+
min = [self, [obj, unscale_time(time)]]
|
102
|
+
end
|
103
|
+
|
104
|
+
# earliest occurrence of each child, converting to absolute time
|
105
|
+
@children.each do |child_clock|
|
106
|
+
obj, relative_time = child_clock.next
|
107
|
+
next unless obj
|
108
|
+
relative_time = unscale_relative_time(relative_time)
|
109
|
+
min = [child_clock, [obj, relative_time]] if min.nil? || relative_time < min[1][1]
|
110
|
+
end
|
111
|
+
|
112
|
+
min
|
113
|
+
end
|
114
|
+
|
115
|
+
# convert an absolute time in this clock's units to an offset from
|
116
|
+
# now in parent clock's units
|
117
|
+
def unscale_time(time)
|
118
|
+
unscale_relative_time(time - now)
|
119
|
+
end
|
120
|
+
|
121
|
+
# convert an offset from now in this clock's units to a time
|
122
|
+
# delta from now in parent clock's units
|
123
|
+
def unscale_relative_time(relative_time)
|
124
|
+
relative_time / @relative_rate.to_f
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
module Enumerable
|
130
|
+
def first_non_nil
|
131
|
+
each do |o|
|
132
|
+
val = yield o
|
133
|
+
return val if val
|
134
|
+
end
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
end
|