ruck 0.2.0 → 0.3.0
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.
- 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
|