gosling 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/gosling.rb +4 -0
- data/lib/gosling/actor.rb +235 -0
- data/lib/gosling/circle.rb +48 -0
- data/lib/gosling/collision.rb +126 -0
- data/lib/gosling/image_library.rb +17 -0
- data/lib/gosling/inheritance_error.rb +4 -0
- data/lib/gosling/polygon.rb +52 -0
- data/lib/gosling/rect.rb +38 -0
- data/lib/gosling/sprite.rb +35 -0
- data/lib/gosling/transform.rb +223 -0
- data/lib/gosling/version.rb +3 -0
- data/spec/actor_spec.rb +670 -0
- data/spec/circle_spec.rb +48 -0
- data/spec/collision_spec.rb +1183 -0
- data/spec/image_library_spec.rb +15 -0
- data/spec/images/bg2.png +0 -0
- data/spec/images/cursor.png +0 -0
- data/spec/images/display_case.png +0 -0
- data/spec/images/full_icons_07_18.png +0 -0
- data/spec/images/key.png +0 -0
- data/spec/images/nil.png +0 -0
- data/spec/polygon_spec.rb +213 -0
- data/spec/rect_spec.rb +87 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/sprite_spec.rb +43 -0
- data/spec/transform_spec.rb +328 -0
- metadata +98 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2e286ba691604561ac6676e60e49699cd3167587
|
4
|
+
data.tar.gz: 6e2083451057aa524bed3f13012134e829a55f80
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6232f177ad08bc4869af947c3c6748c004a052d47ca92247a8d0c54910117a6c8984aa5c5d7d35edb109e31ca5281536fc52c5013928b43bdd958e5edb1c6c44
|
7
|
+
data.tar.gz: 35fe442a079c785663f73640b006171c57872fe0c189bf55c9aa1146fd88c336f4cdabf52db410f3a84361fa862081dff505391c563e9f1f80e16729f1514a5f
|
data/lib/gosling.rb
ADDED
@@ -0,0 +1,235 @@
|
|
1
|
+
require_relative 'transform.rb'
|
2
|
+
|
3
|
+
require 'gosu'
|
4
|
+
|
5
|
+
module Gosling
|
6
|
+
class Actor
|
7
|
+
attr_reader :transform, :parent, :children, :window
|
8
|
+
attr_accessor :is_visible, :is_tangible, :are_children_visible, :are_children_tangible, :is_mask, :color
|
9
|
+
|
10
|
+
def initialize(window)
|
11
|
+
@window = window
|
12
|
+
@transform = Transform.new
|
13
|
+
@parent = nil
|
14
|
+
@children = []
|
15
|
+
@is_visible = true
|
16
|
+
@is_tangible = true
|
17
|
+
@are_children_visible = true
|
18
|
+
@are_children_tangible = true
|
19
|
+
@is_mask = false
|
20
|
+
@color = Gosu::Color.from_hsv(rand(360), rand(), rand())
|
21
|
+
end
|
22
|
+
|
23
|
+
def inspect
|
24
|
+
"#<#{self.class}:#{self.object_id}>"
|
25
|
+
end
|
26
|
+
|
27
|
+
def parent=(parent)
|
28
|
+
return if parent == @parent
|
29
|
+
unless parent
|
30
|
+
raise Gosling::InheritanceError.new("You should use Actor.remove_child() instead of setting the parent directly.") if @parent.has_child?(self)
|
31
|
+
end
|
32
|
+
@parent = parent
|
33
|
+
if @parent
|
34
|
+
raise Gosling::InheritanceError.new("You should use Actor.add_child() instead of setting the parent directly.") unless @parent.has_child?(self)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_child(child)
|
39
|
+
return if @children.include?(child)
|
40
|
+
raise Gosling::InheritanceError.new("An Actor cannot be made a child of itself.") if child == self
|
41
|
+
ancestor = parent
|
42
|
+
until ancestor.nil?
|
43
|
+
raise Gosling::InheritanceError.new("Adding a child's ancestor as a child would create a circular reference.") if child == ancestor
|
44
|
+
ancestor = ancestor.parent
|
45
|
+
end
|
46
|
+
|
47
|
+
child.parent.remove_child(child) if child.parent
|
48
|
+
@children.push(child)
|
49
|
+
child.parent = self
|
50
|
+
end
|
51
|
+
|
52
|
+
def remove_child(child)
|
53
|
+
return unless @children.include?(child)
|
54
|
+
|
55
|
+
@children.delete(child)
|
56
|
+
child.parent = nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def has_child?(child)
|
60
|
+
@children.include?(child)
|
61
|
+
end
|
62
|
+
|
63
|
+
def x
|
64
|
+
@transform.translation[0]
|
65
|
+
end
|
66
|
+
|
67
|
+
def x=(val)
|
68
|
+
array = @transform.translation.to_a
|
69
|
+
array[0] = val
|
70
|
+
@transform.set_translation(Vector.elements(array))
|
71
|
+
end
|
72
|
+
|
73
|
+
def y
|
74
|
+
@transform.translation[1]
|
75
|
+
end
|
76
|
+
|
77
|
+
def y=(val)
|
78
|
+
array = @transform.translation.to_a
|
79
|
+
array[1] = val
|
80
|
+
@transform.set_translation(Vector.elements(array))
|
81
|
+
end
|
82
|
+
|
83
|
+
def pos
|
84
|
+
Vector[x, y, 0]
|
85
|
+
end
|
86
|
+
|
87
|
+
def pos=(val)
|
88
|
+
array = @transform.translation.to_a
|
89
|
+
array[0] = val[0]
|
90
|
+
array[1] = val[1]
|
91
|
+
@transform.set_translation(Vector.elements(array))
|
92
|
+
end
|
93
|
+
|
94
|
+
def center_x
|
95
|
+
@transform.center[0]
|
96
|
+
end
|
97
|
+
|
98
|
+
def center_x=(val)
|
99
|
+
array = @transform.center.to_a
|
100
|
+
array[0] = val
|
101
|
+
@transform.set_center(Vector.elements(array))
|
102
|
+
end
|
103
|
+
|
104
|
+
def center_y
|
105
|
+
@transform.center[1]
|
106
|
+
end
|
107
|
+
|
108
|
+
def center_y=(val)
|
109
|
+
array = @transform.center.to_a
|
110
|
+
array[1] = val
|
111
|
+
@transform.set_center(Vector.elements(array))
|
112
|
+
end
|
113
|
+
|
114
|
+
def scale_x
|
115
|
+
@transform.scale[0]
|
116
|
+
end
|
117
|
+
|
118
|
+
def scale_x=(val)
|
119
|
+
array = @transform.scale.to_a
|
120
|
+
array[0] = val
|
121
|
+
@transform.set_scale(Vector.elements(array))
|
122
|
+
end
|
123
|
+
|
124
|
+
def scale_y
|
125
|
+
@transform.scale[1]
|
126
|
+
end
|
127
|
+
|
128
|
+
def scale_y=(val)
|
129
|
+
array = @transform.scale.to_a
|
130
|
+
array[1] = val
|
131
|
+
@transform.set_scale(Vector.elements(array))
|
132
|
+
end
|
133
|
+
|
134
|
+
def rotation
|
135
|
+
@transform.rotation
|
136
|
+
end
|
137
|
+
|
138
|
+
def rotation=(val)
|
139
|
+
@transform.set_rotation(val)
|
140
|
+
end
|
141
|
+
|
142
|
+
def render(matrix)
|
143
|
+
end
|
144
|
+
|
145
|
+
def draw(matrix = nil)
|
146
|
+
matrix ||= Matrix.identity(3)
|
147
|
+
transform = matrix * @transform.to_matrix
|
148
|
+
render(transform) if @is_visible
|
149
|
+
if @are_children_visible
|
150
|
+
@children.each { |child| child.draw(transform) }
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def is_point_in_bounds(point)
|
155
|
+
false
|
156
|
+
end
|
157
|
+
|
158
|
+
def get_actor_at(point)
|
159
|
+
hit = nil
|
160
|
+
if @are_children_tangible
|
161
|
+
@children.reverse_each do |child|
|
162
|
+
hit = child.get_actor_at(point)
|
163
|
+
if hit
|
164
|
+
break if @is_mask
|
165
|
+
return hit
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
hit = self if hit == nil && @is_tangible && is_point_in_bounds(point)
|
170
|
+
hit = @parent if @is_mask && hit == self
|
171
|
+
hit
|
172
|
+
end
|
173
|
+
|
174
|
+
def get_actors_at(point)
|
175
|
+
actors = []
|
176
|
+
if @are_children_tangible
|
177
|
+
@children.reverse_each do |child|
|
178
|
+
actors |= child.get_actors_at(point)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
actors.push(self) if @is_tangible && is_point_in_bounds(point)
|
182
|
+
actors.uniq!
|
183
|
+
if @is_mask
|
184
|
+
actors.map! { |actor| (actor == self) ? @parent : actor }
|
185
|
+
end
|
186
|
+
actors
|
187
|
+
end
|
188
|
+
|
189
|
+
def get_global_transform
|
190
|
+
tf = if parent
|
191
|
+
parent.get_global_transform * @transform.to_matrix
|
192
|
+
else
|
193
|
+
@transform.to_matrix
|
194
|
+
end
|
195
|
+
return tf
|
196
|
+
end
|
197
|
+
|
198
|
+
def get_global_position
|
199
|
+
tf = get_global_transform
|
200
|
+
Transform.transform_point(tf, Vector[@transform.center[0], @transform.center[1], 0])
|
201
|
+
end
|
202
|
+
|
203
|
+
def alpha
|
204
|
+
@color.alpha
|
205
|
+
end
|
206
|
+
|
207
|
+
def alpha=(val)
|
208
|
+
@color.alpha = val
|
209
|
+
end
|
210
|
+
|
211
|
+
def red
|
212
|
+
@color.red
|
213
|
+
end
|
214
|
+
|
215
|
+
def red=(val)
|
216
|
+
@color.red = val
|
217
|
+
end
|
218
|
+
|
219
|
+
def green
|
220
|
+
@color.green
|
221
|
+
end
|
222
|
+
|
223
|
+
def green=(val)
|
224
|
+
@color.green = val
|
225
|
+
end
|
226
|
+
|
227
|
+
def blue
|
228
|
+
@color.blue
|
229
|
+
end
|
230
|
+
|
231
|
+
def blue=(val)
|
232
|
+
@color.blue = val
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require_relative 'actor.rb'
|
2
|
+
require_relative 'collision.rb'
|
3
|
+
|
4
|
+
module Gosling
|
5
|
+
class Circle < Actor
|
6
|
+
RENDER_VERTEX_COUNT = 16
|
7
|
+
|
8
|
+
attr_reader :radius
|
9
|
+
|
10
|
+
def initialize(window)
|
11
|
+
super(window)
|
12
|
+
@radius = 0
|
13
|
+
end
|
14
|
+
|
15
|
+
def radius=(val)
|
16
|
+
raise ArgumentError.new("Circle.radius cannot be negative") if val < 0
|
17
|
+
@radius = val
|
18
|
+
end
|
19
|
+
|
20
|
+
def get_point_at_angle(radians)
|
21
|
+
raise ArgumentError.new("Expected Numeric, but received #{radians.inspect}!") unless radians.is_a?(Numeric)
|
22
|
+
Vector[Math.cos(radians) * @radius, Math.sin(radians) * @radius, 0]
|
23
|
+
end
|
24
|
+
|
25
|
+
def render(matrix)
|
26
|
+
local_vertices = (0...RENDER_VERTEX_COUNT).map do |i|
|
27
|
+
get_point_at_angle(Math::PI * 2 * i / RENDER_VERTEX_COUNT)
|
28
|
+
end
|
29
|
+
global_vertices = local_vertices.map { |v| Transform.transform_point(matrix, v) }
|
30
|
+
i = 2
|
31
|
+
while i < global_vertices.length
|
32
|
+
v0 = global_vertices[0]
|
33
|
+
v1 = global_vertices[i-1]
|
34
|
+
v2 = global_vertices[i]
|
35
|
+
@window.draw_triangle(
|
36
|
+
v0[0].to_f, v0[1].to_f, @color,
|
37
|
+
v1[0].to_f, v1[1].to_f, @color,
|
38
|
+
v2[0].to_f, v2[1].to_f, @color,
|
39
|
+
)
|
40
|
+
i += 1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def is_point_in_bounds(point)
|
45
|
+
Collision.is_point_in_shape?(point, self)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module Gosling
|
4
|
+
class Collision
|
5
|
+
# 11.4 - 6.7 (get_separation_axes) - 4.7 (project_onto_axis)
|
6
|
+
def self.test(shapeA, shapeB)
|
7
|
+
return false if shapeA.instance_of?(Actor) || shapeB.instance_of?(Actor)
|
8
|
+
|
9
|
+
return false if shapeA === shapeB
|
10
|
+
|
11
|
+
separation_axes = get_separation_axes(shapeA, shapeB)
|
12
|
+
|
13
|
+
separation_axes.each do |axis|
|
14
|
+
projectionA = project_onto_axis(shapeA, axis)
|
15
|
+
projectionB = project_onto_axis(shapeB, axis)
|
16
|
+
return false unless projections_overlap?(projectionA, projectionB)
|
17
|
+
end
|
18
|
+
|
19
|
+
return true
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.is_point_in_shape?(point, shape)
|
23
|
+
raise ArgumentError.new("Collision.get_normal() requires a point and an actor") unless point.is_a?(Vector) && point.size == 3 && shape.is_a?(Actor)
|
24
|
+
|
25
|
+
return false if shape.instance_of?(Actor)
|
26
|
+
|
27
|
+
separation_axes = []
|
28
|
+
if shape.instance_of?(Circle)
|
29
|
+
centers_axis = point - shape.get_global_position
|
30
|
+
separation_axes.push(centers_axis) if centers_axis && centers_axis.magnitude > 0
|
31
|
+
else
|
32
|
+
separation_axes.concat(get_polygon_separation_axes(shape.get_global_vertices))
|
33
|
+
end
|
34
|
+
|
35
|
+
separation_axes.each do |axis|
|
36
|
+
shape_projection = project_onto_axis(shape, axis)
|
37
|
+
point_projection = point.inner_product(axis)
|
38
|
+
return false unless shape_projection.min <= point_projection && point_projection <= shape_projection.max
|
39
|
+
end
|
40
|
+
|
41
|
+
return true
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.get_normal(vector)
|
45
|
+
raise ArgumentError.new("Collision.get_normal() requires a length 3 vector") unless vector.is_a?(Vector) && vector.size == 3
|
46
|
+
raise ArgumentError.new("Cannot determine normal of zero-length vector") if vector.magnitude == 0
|
47
|
+
|
48
|
+
Vector[-vector[1], vector[0], 0]
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.get_polygon_separation_axes(vertices)
|
52
|
+
unless vertices.is_a?(Array) && vertices.reject { |v| v.is_a?(Vector) && v.size == 3 }.empty?
|
53
|
+
raise ArgumentError.new("Collission.get_polygon_separation_axes() expects an array of vectors similar to that produced by Polygon.get_vertices")
|
54
|
+
end
|
55
|
+
|
56
|
+
axes = (0...vertices.length).map do |i|
|
57
|
+
axis = vertices[(i + 1) % vertices.length] - vertices[i]
|
58
|
+
(axis.magnitude > 0) ? get_normal(axis).normalize : nil
|
59
|
+
end
|
60
|
+
axes.compact
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.get_circle_separation_axis(circleA, circleB)
|
64
|
+
unless circleA.is_a?(Actor) && circleB.is_a?(Actor)
|
65
|
+
raise ArgumentError.new("Collision.get_circle_separation_axis() expects two circles")
|
66
|
+
end
|
67
|
+
axis = circleB.get_global_position - circleA.get_global_position
|
68
|
+
(axis.magnitude > 0) ? axis.normalize : nil
|
69
|
+
end
|
70
|
+
|
71
|
+
# 4.8 - 2.6 - 1.6 - .4
|
72
|
+
def self.get_separation_axes(shapeA, shapeB)
|
73
|
+
unless shapeA.is_a?(Actor) && !shapeA.instance_of?(Actor)
|
74
|
+
raise ArgumentError.new("Expected a child of the Actor class, but received #{shapeA.inspect}!")
|
75
|
+
end
|
76
|
+
|
77
|
+
unless shapeB.is_a?(Actor) && !shapeB.instance_of?(Actor)
|
78
|
+
raise ArgumentError.new("Expected a child of the Actor class, but received #{shapeB.inspect}!")
|
79
|
+
end
|
80
|
+
|
81
|
+
separation_axes = []
|
82
|
+
|
83
|
+
unless shapeA.instance_of?(Circle)
|
84
|
+
separation_axes.concat(get_polygon_separation_axes(shapeA.get_global_vertices))
|
85
|
+
end
|
86
|
+
|
87
|
+
unless shapeB.instance_of?(Circle)
|
88
|
+
separation_axes.concat(get_polygon_separation_axes(shapeB.get_global_vertices))
|
89
|
+
end
|
90
|
+
|
91
|
+
if shapeA.instance_of?(Circle) || shapeB.instance_of?(Circle)
|
92
|
+
axis = get_circle_separation_axis(shapeA, shapeB)
|
93
|
+
separation_axes.push(axis) if axis
|
94
|
+
end
|
95
|
+
|
96
|
+
separation_axes.map! { |v| v[0] < 0 ? v * -1 : v }
|
97
|
+
separation_axes.uniq
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.project_onto_axis(shape, axis)
|
101
|
+
raise ArgumentError.new("Expected Actor, but received #{shape.inspect}!") unless shape.is_a?(Actor)
|
102
|
+
raise ArgumentError.new("Expected Vector, but received #{shape.inspect}!") unless axis.is_a?(Vector)
|
103
|
+
|
104
|
+
global_vertices = if shape.instance_of?(Circle)
|
105
|
+
global_tf = shape.get_global_transform
|
106
|
+
local_axis = global_tf.inverse * Vector[axis[0], axis[1], 0]
|
107
|
+
v = shape.get_point_at_angle(Math.atan2(local_axis[1], local_axis[0]))
|
108
|
+
[v, v * -1].map { |vertex| Transform.transform_point(global_tf, vertex) }
|
109
|
+
else
|
110
|
+
shape.get_global_vertices
|
111
|
+
end
|
112
|
+
|
113
|
+
projections = global_vertices.map { |vertex| vertex.inner_product(axis) }.sort
|
114
|
+
[projections.first, projections.last]
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.projections_overlap?(a, b)
|
118
|
+
raise ArgumentError.new("Collision.projections_overlap?() expects arrays") unless a.is_a?(Array) && b.is_a?(Array)
|
119
|
+
raise ArgumentError.new("Collision.projections_overlap?() projection arrays must be length 2") unless a.length == 2 && b.length == 2
|
120
|
+
|
121
|
+
a.sort! if a[0] > a[1]
|
122
|
+
b.sort! if b[0] > b[1]
|
123
|
+
(a[0] <= b[1] && (b[1] <= a[1] || b[0] <= a[0])) || (b[0] <= a[1] && (a[0] <= b[0] || a[1] <= b[1]))
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module Gosling
|
4
|
+
class ImageLibrary
|
5
|
+
@@cache = {}
|
6
|
+
|
7
|
+
include Singleton
|
8
|
+
|
9
|
+
def self.get(filename)
|
10
|
+
raise ArgumentError.new("File not found: '#{filename}' in '#{Dir.pwd}'") unless File.exists?(filename)
|
11
|
+
unless @@cache.has_key?(filename)
|
12
|
+
@@cache[filename] = Gosu::Image.new(filename, tileable: true)
|
13
|
+
end
|
14
|
+
@@cache[filename]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|