gosling 1.0.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.
- 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
|