zyps 0.1.1

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.
@@ -0,0 +1,358 @@
1
+ # Copyright 2007 Jay McGavren, jay@mcgavren.com.
2
+ #
3
+ # This file is part of Zyps.
4
+ #
5
+ # Zyps is free software; you can redistribute it and/or modify
6
+ # it under the terms of the GNU Lesser General Public License as published by
7
+ # the Free Software Foundation; either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+
19
+ require 'observer'
20
+
21
+
22
+
23
+ #A virtual environment.
24
+ class Environment
25
+
26
+ include Observable
27
+
28
+ #An array of GameObject objects that reside in the Environment.
29
+ attr_accessor :objects
30
+ #An array of EnvironmentalFactor objects that act on any GameObject in the Environment.
31
+ attr_accessor :environmental_factors
32
+
33
+ def initialize (objects = [], environmental_factors = [])
34
+ @objects, @environmental_factors = objects, environmental_factors
35
+ @clock = Clock.new
36
+ end
37
+
38
+ #Allow everything in the environment to interact with each other.
39
+ #Objects are first moved according to their preexisting vectors and the amount of time since the last call.
40
+ #Then each EnvironmentalFactor is allowed to act on each object.
41
+ #Finally, each GameObject with an act() method is allowed to act on every other object.
42
+ def interact
43
+
44
+ #Get time since last interaction.
45
+ elapsed_time = @clock.elapsed_time
46
+
47
+ objects.each do |object|
48
+
49
+ #Move each object according to its vector.
50
+ object.location.x += object.vector.x * elapsed_time
51
+ object.location.y += object.vector.y * elapsed_time
52
+
53
+ #Have all environmental factors interact with each object.
54
+ environmental_factors.each {|factor| factor.act(factor, object)}
55
+
56
+ #Have all creatures interact with each other.
57
+ if object.respond_to?(:act)
58
+ objects.each do |target|
59
+ next if target.equal?(object) #Ensure object does not act on itself.
60
+ object.act(object, target)
61
+ end
62
+ end
63
+
64
+ end
65
+
66
+ #Mark environment as changed.
67
+ changed
68
+
69
+ #Alert observers.
70
+ notify_observers(self)
71
+
72
+ end
73
+
74
+ end
75
+
76
+
77
+
78
+ #An object in the virtual environment.
79
+ class GameObject
80
+
81
+ #The object's Location in space.
82
+ attr_accessor :location
83
+ #A Color that will be used to draw the object.
84
+ attr_accessor :color
85
+ #A Vector with the object's current speed and direction of travel.
86
+ attr_accessor :vector
87
+ #A String with the object's name.
88
+ attr_accessor :name
89
+ #An array of Strings with tags that determine how the object will be treated by Creature and EnvironmentalFactor objects in its environment.
90
+ attr_accessor :tags
91
+
92
+ def initialize (name = nil, location = Location.new, color = Color.new, vector = Vector.new, age = 0, tags = [])
93
+ @name, @location, @color, @vector, @tags = name, location, color, vector, tags
94
+ self.age = age
95
+ end
96
+
97
+ #Time since the object was created, in seconds.
98
+ def age; Time.new.to_f - @birth_time; end
99
+ def age=(age); @birth_time = Time.new.to_f - age; end
100
+
101
+ end
102
+
103
+
104
+
105
+ #Mixin to have an object (usually a Creature) act on other objects.
106
+ module Responsive
107
+
108
+ #Call Behavior.perform on each of the object's assigned Behaviors, with the object and a target as arguments.
109
+ def act(subject, target)
110
+ behaviors.each {|behavior| behavior.perform(subject, target)}
111
+ end
112
+
113
+ end
114
+
115
+
116
+
117
+ #A Creature is a GameObject that can sense and respond to other GameObjects (including other Creature objects).
118
+ class Creature < GameObject
119
+
120
+ include Responsive
121
+
122
+ #A list of Behavior objects that determine the creature's response to its environment.
123
+ attr_accessor :behaviors
124
+
125
+ #Identical to the GameObject constructor, except that it also takes a list of Behavior objects.
126
+ def initialize (name = nil, location = Location.new, color = Color.new, vector = Vector.new, age = 0, tags = [], behaviors = [])
127
+ super(name, location, color, vector, age, tags)
128
+ @behaviors = behaviors
129
+ end
130
+
131
+ end
132
+
133
+
134
+
135
+ #Something in the environment that acts on creatures.
136
+ class EnvironmentalFactor
137
+
138
+ include Responsive
139
+
140
+ #A list of Behavior objects, each called in the same way as those of a Creature.
141
+ attr_accessor :behaviors
142
+
143
+ def initialize (behaviors = [])
144
+ @behaviors = behaviors
145
+ end
146
+
147
+ end
148
+
149
+
150
+
151
+ #A behavior that a Creature or EnvironmentalFactor object (or other classes that include Responsive) engage in.
152
+ #The target can have its tags or colors changed, it can be "herded", it can be destroyed, or any other action the library user can dream up.
153
+ #Likewise, the subject can change its own attributes, it can approach or flee from the target, it can spawn new Creatures or GameObjects (like bullets), or anything else.
154
+ class Behavior
155
+
156
+ #A list of conditions, which are Proc objects called with the object itself and its target. A condition can consider the tags on the target, the distance from the subject, or any other criteria. If any condition returns false, the behavior will not be carried out.
157
+ attr_accessor :conditions
158
+ #A list of actions, which are Proc objects called with the object and its target when all conditions are met. An action can act on the subject or its target.
159
+ attr_accessor :actions
160
+
161
+ #Optionally takes an array of actions and one of conditions.
162
+ def initialize (actions = [], conditions = [])
163
+ @actions, @conditions = actions, conditions
164
+ end
165
+
166
+ #Calls each Proc object in the list of conditions with the subject and its target. Returns nil if any condition returns false.
167
+ #Then calls each Proc object in the list of actions, also with the subject and its target.
168
+ def perform(subject, target)
169
+ conditions.each {|condition| return nil unless condition.call(subject, target)}
170
+ actions.each {|action| action.call(subject, target)}
171
+ end
172
+
173
+ end
174
+
175
+
176
+
177
+ #An object's color. Has red, green, and blue components, each ranging from 0 to 1.
178
+ #* Red: <tt>Color.new(1, 0, 0)</tt>
179
+ #* Green: <tt>Color.new(0, 1, 0)</tt>
180
+ #* Blue: <tt>Color.new(0, 0, 1)</tt>
181
+ #* White: <tt>Color.new(1, 1, 1)</tt>
182
+ #* Black: <tt>Color.new(0, 0, 0)</tt>
183
+ class Color
184
+
185
+ include Comparable
186
+
187
+ #Components which range from 0 to 1, which combine to form the Color.
188
+ attr_accessor :red, :green, :blue
189
+
190
+ def initialize (red = 1, green = 1, blue = 1)
191
+ @red, @green, @blue = red, green, blue
192
+ end
193
+
194
+ #Automatically constrains value to the range 0 - 1.
195
+ def red=(v); v = 0 if v < 0; v = 1 if v > 1; @red = v; end
196
+ #Automatically constrains value to the range 0 - 1.
197
+ def green=(v); v = 0 if v < 0; v = 1 if v > 1; @green = v; end
198
+ #Automatically constrains value to the range 0 - 1.
199
+ def blue=(v); v = 0 if v < 0; v = 1 if v > 1; @blue = v; end
200
+
201
+ #Compares this Color with another to see which is brighter.
202
+ #The sum of all components (red + green + blue) for each color determines which is greater.
203
+ def <=>(other)
204
+ @red + @green + @blue <=> other.red + other.green + other.blue
205
+ end
206
+
207
+ #Averages each component of this Color with the corresponding component of color2, returning a new Color.
208
+ def +(color2)
209
+ Color.new(
210
+ self.red + color2.red / 2.0,
211
+ self.green + color2.green / 2.0,
212
+ self.blue + color2.blue / 2.0
213
+ )
214
+ end
215
+
216
+ end
217
+
218
+
219
+
220
+ #An object's location, with x and y coordinates.
221
+ class Location
222
+
223
+ #Coordinates can be negative, and don't have to be integers.
224
+ attr_accessor :x, :y
225
+
226
+ def initialize (x = 0, y = 0)
227
+ @x, @y = x, y
228
+ end
229
+
230
+ end
231
+
232
+
233
+
234
+ #An object or force's velocity.
235
+ #Has speed and angle components.
236
+ class Vector
237
+
238
+ #The length of the Vector.
239
+ attr_accessor :speed
240
+
241
+ def initialize (speed = 0, pitch = 0)
242
+ self.speed = speed
243
+ self.pitch = pitch
244
+ end
245
+
246
+ #The angle along the X/Y axes.
247
+ def pitch; Utility.to_degrees(@pitch); end
248
+ def pitch=(degrees)
249
+ #Constrain degrees to 0 to 360.
250
+ value = degrees % 360
251
+ value += 360 if value < 0
252
+ #Store as radians internally.
253
+ @pitch = Utility.to_radians(value)
254
+ end
255
+
256
+ #The X component.
257
+ def x; @speed.to_f * Math.cos(@pitch); end
258
+ def x=(value)
259
+ @speed, @pitch = Math.sqrt(value ** 2 + y ** 2), Math.atan(y / value)
260
+ end
261
+ #The Y component.
262
+ def y; @speed.to_f * Math.sin(@pitch); end
263
+ def y=(value)
264
+ @speed, @pitch = Math.sqrt(x ** 2 + value ** 2), Math.atan(value / x)
265
+ end
266
+
267
+ #Add this Vector to vector2, returning a new Vector.
268
+ #This operation is useful when calculating the effect of wind or thrust on an object's current heading.
269
+ def +(vector2)
270
+ #Get the x and y components of the new vector.
271
+ new_x = (self.x + vector2.x)
272
+ new_y = (self.y + vector2.y)
273
+ new_length_squared = new_x ** 2 + new_y ** 2
274
+ new_length = (new_length_squared == 0 ? 0 : Math.sqrt(new_length_squared))
275
+ new_angle = (new_x == 0 ? 0 : Utility.to_degrees(Math.atan2(new_y, new_x)))
276
+ #Calculate speed and angle of new vector with components.
277
+ Vector.new(new_length, new_angle)
278
+ end
279
+
280
+ end
281
+
282
+
283
+
284
+ #A clock to use for timing actions.
285
+ class Clock
286
+
287
+ def initialize
288
+ @last_check_time = Time.new.to_f
289
+ end
290
+
291
+ #Returns the time in (fractional) seconds since this method was last called (or on the first call, time since the Clock was created).
292
+ def elapsed_time
293
+ time = Time.new.to_f
294
+ elapsed_time = time - @last_check_time
295
+ @last_check_time = time
296
+ elapsed_time
297
+ end
298
+
299
+ end
300
+
301
+
302
+
303
+ #Various methods for working with Vectors, etc.
304
+ module Utility
305
+
306
+ PI2 = Math::PI * 2.0
307
+
308
+ #Get the angle (in degrees) from one Location to another.
309
+ def Utility.find_angle(origin, target)
310
+ #Get vector from origin to target.
311
+ x_difference = target.x - origin.x
312
+ y_difference = target.y - origin.y
313
+ #Get vector's angle.
314
+ radians = Math.atan2(y_difference, x_difference)
315
+ #Result will range from negative Pi to Pi, so correct it.
316
+ radians += PI2 if radians < 0
317
+ #Convert to degrees.
318
+ to_degrees(radians)
319
+ end
320
+
321
+ #Get the distance from one Location to another.
322
+ def Utility.find_distance(origin, target)
323
+ #Get vector from origin to target.
324
+ x_difference = origin.x - target.x
325
+ y_difference = origin.y - target.y
326
+ #Get distance.
327
+ Math.sqrt(x_difference ** 2 + y_difference ** 2)
328
+ end
329
+
330
+ #Convert radians to degrees.
331
+ def Utility.to_degrees(radians)
332
+ radians / PI2 * 360
333
+ end
334
+
335
+ #Convert degrees to radians.
336
+ def Utility.to_radians(degrees)
337
+ radians = degrees / 360.0 * PI2
338
+ radians = radians % PI2
339
+ radians += PI2 if radians < 0
340
+ radians
341
+ end
342
+
343
+ #Reduce a number to within an allowed maximum (or minimum, if the number is negative).
344
+ def Utility.constrain_value(value, absolute_maximum)
345
+ if (value.abs > absolute_maximum) then
346
+ if value >= 0 then
347
+ value = absolute_maximum
348
+ else
349
+ value = absolute_maximum * -1
350
+ end
351
+ end
352
+ value
353
+ end
354
+
355
+ end
356
+
357
+
358
+
@@ -0,0 +1,147 @@
1
+ # Copyright 2007 Jay McGavren, jay@mcgavren.com.
2
+ #
3
+ # This file is part of Zyps.
4
+ #
5
+ # Zyps is free software; you can redistribute it and/or modify
6
+ # it under the terms of the GNU Lesser General Public License as published by
7
+ # the Free Software Foundation; either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+
19
+ require 'gtk2'
20
+
21
+
22
+ #A view of game objects.
23
+ class TrailsView
24
+
25
+ #A Gtk::DrawingArea to render objects on.
26
+ attr_reader :canvas
27
+ #Dimensions of the view.
28
+ attr_reader :width, :height
29
+ #Number of line segments to draw for each object.
30
+ attr_accessor :trail_length
31
+ #Width of the trail behind each object.
32
+ attr_accessor :trail_width
33
+
34
+ def initialize (width = 600, height = 400, trail_length = 5, trail_width = trail_length)
35
+
36
+ @width, @height, @trail_length, @trail_width, @background = width, height, trail_length, trail_width
37
+
38
+ #Create a drawing area.
39
+ @canvas = Gtk::DrawingArea.new
40
+ #Set to correct size.
41
+ resize
42
+
43
+ #Whenever the drawing area needs updating...
44
+ @canvas.signal_connect("expose_event") do
45
+ #Copy buffer bitmap to canvas.
46
+ @canvas.window.draw_drawable(
47
+ @canvas.style.fg_gc(@canvas.state), #Gdk::GC (graphics context) to use when drawing.
48
+ buffer, #Gdk::Drawable source to copy onto canvas.
49
+ 0, 0, #Pull from upper left of source.
50
+ 0, 0, #Copy to upper left of canvas.
51
+ -1, -1 #-1 width and height signals to copy entire source over.
52
+ )
53
+ end
54
+
55
+ #Track a list of locations for each object.
56
+ @locations = Hash.new {|h, k| h[k] = Array.new}
57
+
58
+ end
59
+
60
+ def width= (pixels) #:nodoc:
61
+ @width = pixels
62
+ resize
63
+ end
64
+ def height= (pixels) #:nodoc:
65
+ @height = pixels
66
+ resize
67
+ end
68
+
69
+ #Takes an Environment, and draws it to the canvas.
70
+ #Tracks the position of each GameObject over time so it can draw a trail behind it.
71
+ #The head will match the object's Color exactly, fading to black at the tail.
72
+ #trail_width will be used as the line thickness at the object's head, diminishing to 1 at the tail.
73
+ def update(environment)
74
+
75
+ #Clear the background on the buffer.
76
+ graphics_context = Gdk::GC.new(buffer)
77
+ graphics_context.rgb_fg_color = Gdk::Color.new(0, 0, 0)
78
+ buffer.draw_rectangle(
79
+ graphics_context,
80
+ true, #Filled.
81
+ 0, 0, #Upper-left corner.
82
+ @width, @height #Lower-right corner.
83
+ )
84
+
85
+ #For each GameObject in the environment:
86
+ environment.objects.each do |object|
87
+
88
+ #Add the object's current location to the list.
89
+ @locations[object] << [object.location.x, object.location.y]
90
+ #If the list is larger than the number of tail segments, delete the first position.
91
+ @locations[object].shift if @locations[object].length > @trail_length
92
+
93
+ #For each location in this object's list:
94
+ @locations[object].each_with_index do |location, index|
95
+
96
+ #Skip first location.
97
+ next if index == 0
98
+
99
+ #Divide the current segment number by trail segment count to get the multiplier to use for brightness and width.
100
+ multiplier = index.to_f / @locations[object].length.to_f
101
+
102
+ #Set the drawing color to use the object's colors, adjusted by the multiplier.
103
+ graphics_context.rgb_fg_color = Gdk::Color.new( #Don't use Gdk::GC.foreground= here, as that requires a color to be in the color map already.
104
+ object.color.red * multiplier * 65535,
105
+ object.color.green * multiplier * 65535,
106
+ object.color.blue * multiplier * 65535
107
+ )
108
+
109
+ #Multiply the actual drawing width by the current multiplier to get the current drawing width.
110
+ graphics_context.set_line_attributes(
111
+ (@trail_width * multiplier).ceil,
112
+ Gdk::GC::LINE_SOLID,
113
+ Gdk::GC::CAP_ROUND, #Line ends drawn as semicircles.
114
+ Gdk::GC::JOIN_MITER #Only used for polygons.
115
+ )
116
+
117
+ #Get previous location so we can draw a line from it.
118
+ previous_location = @locations[object][index - 1]
119
+
120
+ #Draw a line with the current width from the prior location to the current location.
121
+ buffer.draw_line(
122
+ graphics_context,
123
+ previous_location[0], previous_location[1],
124
+ location[0], location[1]
125
+ )
126
+
127
+ end
128
+
129
+ end
130
+
131
+ @canvas.queue_draw_area(0, 0, @width, @height)
132
+
133
+ end
134
+
135
+
136
+ private
137
+
138
+ def resize
139
+ @canvas.set_size_request(@width, @height)
140
+ @buffer = nil #Causes buffer to reset its size next time it's accessed.
141
+ end
142
+
143
+ def buffer
144
+ @buffer ||= Gdk::Pixmap.new(@canvas.window, @width, @height, -1)
145
+ end
146
+
147
+ end