-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathedge_colliders_from_svg.rb
More file actions
228 lines (181 loc) · 5.35 KB
/
edge_colliders_from_svg.rb
File metadata and controls
228 lines (181 loc) · 5.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# This script updates one or more EdgeCollider2D components in a Unity scene to
# match paths in an SVG file.
#
# First, make sure your Unity scene contains a separate GameObject for each
# collider. The name of this GameObject should be something like
# 'Outside Collider', and an EdgeCollider2D should be added to it.
#
# Wrap these collider GameObjects in a Transform positioned at the top-left
# corner of the background image with a scale of 1 / (pixels per unit). This
# ensures that a point at (x, -y) in Unity corresponds to the point at (x, y)
# pixels in the SVG file.
#
# The SVG file must contain a group with ID 'Edge-Colliders'. For each
# collider, create path inside this group. The ID of the path should match the
# name of the corresponding GameObject, but with spaces replaced with hyphens,
# for example 'Outside-Collider'.
#
# If the level background is created using Pixelmator Pro, you can create the
# paths using the pen tool. Ensure that the stroke align option is set to
# center to prevent Pixelmator from outlining the path when exporting to SVG.
# The name of each path shape should match the name of the corresponding
# GameObject, and the group should be called 'Edge Colliders'; the spaces will
# be converted to hyphens automatically on export.
require 'nokogiri'
require 'yaml'
unless ARGV.length === 2
STDERR.puts "Usage: ruby #{$PROGRAM_NAME} <path to unity scene> <path to svg file>"
exit 2
end
scene_path, svg_path = ARGV
svg = Nokogiri::XML(File.open(svg_path))
edge_colliders_container = svg.css('[id=Edge-Colliders]').first
if edge_colliders_container.nil?
STDERR.puts "Expected #{svg_path} to contain an element with ID 'Edge-Colliders'"
exit 1
end
paths = edge_colliders_container.children.select { |child| child.name == 'path' }
EdgeColliderData = Struct.new(:name, :points)
def parse_path_data data_string
tokens = data_string.split(' ')
points = []
while command = tokens.shift
expected_arguments = {
M: 2,
L: 2,
C: 6,
Z: 0,
}[command.to_sym]
if expected_arguments.nil?
STDERR.puts "Got unexpected command #{command} in:\n #{data_string}"
exit 1
end
arguments = tokens.shift(expected_arguments).map { |arg| Float(arg).round }
case command
when 'M', 'L'
points << arguments
when 'C'
points << arguments.last(2)
when 'Z'
points << points.first
end
end
points
end
edge_collider_data = paths.map do |path|
name = path['id'].gsub('-', ' ')
points = parse_path_data(path['d'])
EdgeColliderData.new(name, points)
end
class Scene
HEADER_REGEX = /^--- !u!\d+ &(\d+)$/
class SceneObject
attr_reader :id, :lines
def initialize scene, header, id
@scene = scene
@header = header
@id = id
@lines = []
end
def to_s
[@header, *@lines].join
end
def yaml
@yaml ||= YAML.load(@lines.join("\n"))
end
def game_object? name
yaml.has_key?('GameObject') && yaml['GameObject']['m_Name'] == name
end
def component_ids
yaml['GameObject']['m_Component'].map { |entry| entry['component']['fileID'] }
end
def components
component_ids.map { |id| @scene.get_by_id(id) }
end
def edge_collider_component
component = components.find(&:edge_collider?)
if component.nil?
STDERR.puts "Expected the GameObject to have an EdgeCollider2D component:\n#{to_s}"
exit 1
end
component
end
def edge_collider?
yaml.has_key? 'EdgeCollider2D'
end
def points= points
new_lines =
if points.empty?
[" m_Points: []\n"]
else
[" m_Points:\n"]
end
points.each do |point|
x, y = point
new_lines << " - {x: #{x}, y: #{-y}}\n"
end
start_index = -1
remove_count = 1
@lines.each_with_index do |line, index|
if start_index > -1
if line.start_with?(' -')
remove_count += 1
else
break
end
elsif start_index == -1 && line.start_with?(' m_Points:')
start_index = index
end
end
@lines.slice!(start_index, remove_count)
@lines.insert(start_index, *new_lines)
@yaml = nil
end
end
def initialize path
@path = path
@initial_lines = []
@objects = []
object = nil
File.read(path).lines.each do |line|
header_match = HEADER_REGEX.match(line)
if header_match
id = header_match[1].to_i
object = SceneObject.new(self, line, id)
@objects << object
elsif object
object.lines << line
else
@initial_lines << line
end
end
end
def to_s
([@initial_lines] + @objects.map(&:to_s)).join
end
def save!
File.write(@path, to_s)
end
def get_by_name name
object = @objects.find { |object| object.game_object?(name) }
if object.nil?
STDERR.puts "Expected the scene to contain a GameObject called #{name}"
exit 1
end
object
end
def get_by_id id
object = @objects.find { |object| object.id == id }
if object.nil?
STDERR.puts "Expected the scene to contain an object with ID #{id}"
exit 1
end
object
end
end
scene = Scene.new(scene_path)
edge_collider_data.each do |data|
component = scene.get_by_name(data.name).edge_collider_component
component.points = data.points
end
scene.save!