See this tutorial for instructions on creating Box2D levels in InkScape and importing them into Flash. It also explains floors and ceilings. Additionally, you can check out the SVG Parser thread off the Box2D forums to get updates that may not be posted here.

Functionality

The new parser will supports all declarations of curves. Additionally, it requires you to provide a new argument called resolution. Resolution is the number of segments each curve is split into.

If you pass a value less than 1 for resolution, the parser will use the formula (Line Width * 10) to determine resolution. So if you set the Line Stroke Width to 2 in InkScape, the curve will be split into 20 segments. Basically, thicker lines make smoother curves.

The green curves were drawn from left-to-right, so they are floors and check collision on their upper edge. The red curves are drawn right-to-left and check collision on their lower edge. Notice that none of the curves cover an angle of more than 180 degrees; you may end up with unexpected results if you do go over this limit.

Caution

You will need to use continuous collision checking on objects that are going to roll along curves at high speeds. The setBullet flag will help with this, but setting the whole world to continuous collision is needed for good looking rolls. Additionally, objects will roll with jerky motion if you set the line resolution too high.

The Parser in Action



SVG Curve Parser Source

To use the curve parser, import b2SVG.as and call parseSVG.

    parseSVG(SVG, b2World, Number, Number)

  • SVG variable (see this tutorial)
  • World – Your b2world object
  • Ratio – Scale of pixels:units in your Box2D world
  • Resolution – If > one, parses all curves with the passed argument. If < one, uses (Line-Width * 10) to determine curve resolution.

The Code

b2SVG.as

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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
/**
* @author John Nesky <http://www.johnnesky.com>
* Bezier curves added by Quest Yarbrough <http://www.ezqueststudios.com>
* partially based on some code by Helen Triolo <http://flash-creations.com/notes/sample_svgtoflash.php>
* 
* This code is available under the LGPL license. <http://www.gnu.org/licenses/lgpl-3.0.txt>
*/
 
package 
{
  import Box2D.Dynamics.b2World;
  import Box2D.Common.Math.b2Vec2;
  import Box2D.Collision.Shapes.b2EdgeChainDef;
  import Box2D.Collision.Shapes.b2ShapeDef;
  import Box2D.Collision.Shapes.b2Shape;
  import b2Bezier;
  public class b2SVG
  {
 
    public static function parseSVG(svg: XML, world: b2World, RATIO:Number, useDefaultCurveResolution:Number): Array {
      var ns: Namespace = svg.namespace("");
      var useCurveThickness:Boolean = false;
      var resolution:Number;
 
      if (useDefaultCurveResolution > 0) {
        resolution = useDefaultCurveResolution; 
      } else {
        useCurveThickness = true;
      }
 
      var chainDef: b2EdgeChainDef = new b2EdgeChainDef();
      chainDef.friction = 0.5;
      chainDef.restitution = 0.0;
      for each (var path: XML in svg..ns::path)
      {
        if (useCurveThickness) resolution = Math.round(returnStrokeWidth(path.@style) * 10);
 
        // the entire path:
        var d: String = path.@d;
 
        // Inkscape, Illustrator, and other programs use slightly different
        // formats for the path, because the SVG specs are very flexible. This
        // puts some strain on the people who write path parsers (me!), but 
        // Helen Triolo's example illustrates an easy way to convert the path
        // to a consistent format and then break it up into an array:
 
        // replace whitespace with commas:
        var letter: String;
        for each (letter in [" ","\f","\n","\r","\t"])
          d = d.split(letter).join(",");
 
        // surround letters with commas:
        for each (letter in ["M","m","Z","z","L","l","H","h","V","v","C","c","S","s","Q","q","T","t","A","a"])
          d = d.split(letter).join("," + letter + ",");
 
        // insert commas before negative symbols:
        d = d.split("-").join(",-");
 
        // now get all tokens separated by commas:
        var args: Array = d.split(",");
 
        // remove empty strings:
        args = args.filter(filterEmptyString);
 
 
        var currentPosition: b2Vec2 = new b2Vec2(0,0);
        var control1: b2Vec2 = null;
        var control2: b2Vec2 = null;
        var control3: b2Vec2 = null;
        var prevControl: b2Vec2 = null;
        var prevCommand: String = null;
        var relative: Boolean = false;
        var curve: Array;
		var shapes: Array = new Array();
 
        chainDef.vertices.length = 0;
        var i: int = 0;
        while (true)
        {
          if (i == args.length) {
            // cleanup: If a path is incomplete, finish it without closing the loop
            //          and add it to the Box2D world.
            if (chainDef.vertices.length >= 2) {
              chainDef.vertexCount = chainDef.vertices.length;
              chainDef.isALoop = false;
              shapes.push(world.GetGroundBody().CreateShape(chainDef)); 
            }
            break;
          }
 
          var command: String = args[i];
          i++;
 
          switch (command)
          {
            case "Z":
            case "z":
              // closepath: If a path is incomplete, finish it, close the loop,
              //            and add it to the Box2D world. 
              if (chainDef.vertices.length >= 3) {
                chainDef.vertices.pop(); // the last vertex of the loop is redundant.
                chainDef.vertexCount = chainDef.vertices.length;
                chainDef.isALoop = true;
				shapes.push(world.GetGroundBody().CreateShape(chainDef));
              }
              chainDef.vertices.length = 0;
              chainDef.vertices.push(currentPosition);
              break;
            case "M":
            case "m":
              // moveto: If a path is incomplete, finish it without closing the loop
              //          and add it to the Box2D world.
              //          Start a new path.
              if (chainDef.vertices.length >= 2) {
                chainDef.vertexCount = chainDef.vertices.length;
                chainDef.isALoop = false;
                shapes.push(world.GetGroundBody().CreateShape(chainDef));
              }
              relative = (command == "m");
              if (relative) {
                currentPosition = new b2Vec2(currentPosition.x + args[i] / RATIO,
                                             currentPosition.y + args[i+1] / RATIO);
              } else {
                currentPosition = new b2Vec2(args[i] / RATIO, args[i+1] / RATIO);
              }
              i += 2;
              chainDef.vertices.length = 0;
              chainDef.vertices.push(currentPosition);
 
              // According to the SVG spec, a moveto command can be implicitly followed
              // by lineto coordinates. So there is no "break" here.
            case "L":
            case "l":
              // lineto: a series of straight lines. Keep parsing until you hit a non-number. 
              if (command == "l") relative = true;
              else if (command == "L") relative = false;
              while (!isNaN(parseFloat(args[i])))
              {
                if (relative) {
                  currentPosition = new b2Vec2(currentPosition.x + args[i] / RATIO,
                                               currentPosition.y + args[i+1] / RATIO);
                } else {
                  currentPosition = new b2Vec2(args[i] / RATIO, args[i+1] / RATIO);
                }
                i += 2;
                chainDef.vertices.push(currentPosition);
              }
              break;
            case "H":
            case "h":
              // horizontal lineto: a series of horizontal lines. 
              //                    keep parsing until you hit a non-number. 
              //                    Box2D works much better if adjacent parallel lines
              //                    are merged into one, so I'll go ahead and merge them.
              relative = (command == "h");
              do
              {
                if (relative) {
                  currentPosition = new b2Vec2(currentPosition.x + args[i] / RATIO,
                                               currentPosition.y);
                } else {
                  currentPosition = new b2Vec2(args[i] / RATIO, currentPosition.y);
                }
                i++;
              } while (!isNaN(parseFloat(args[i])));
              chainDef.vertices.push(currentPosition);
              break;
            case "V":
            case "v":
              // vertical lineto: a series of vertical lines. 
              //                  keep parsing until you hit a non-number. 
              //                  Box2D works much better if adjacent parallel lines
              //                  are merged into one, so I'll go ahead and merge them.
              relative = (command == "v");
              do
              {
                if (relative) {
                  currentPosition = new b2Vec2(currentPosition.x,
                                               currentPosition.y + args[i] / RATIO);
                } else {
                  currentPosition = new b2Vec2(currentPosition.x, args[i] / RATIO);
                }
                i++;
              } while (!isNaN(parseFloat(args[i])));
              chainDef.vertices.push(currentPosition);
              break;
            case "C":
            case "c":
              // curveto
              relative = (command == "c");
              do
              {
                if (relative) {
                  control1 = new b2Vec2(currentPosition.x + args[i] / RATIO,
                                        currentPosition.y + args[i+1] / RATIO);
                  control2 = new b2Vec2(currentPosition.x + args[i+2] / RATIO,
                                        currentPosition.y + args[i+3] / RATIO);
                  control3 = new b2Vec2(currentPosition.x + args[i+4] / RATIO,
                                        currentPosition.y + args[i+5] / RATIO);
                } else {
                  control1 = new b2Vec2(args[i] / RATIO, args[i+1] / RATIO);
                  control2 = new b2Vec2(args[i+2] / RATIO, args[i+3] / RATIO);
                  control3 = new b2Vec2(args[i+4] / RATIO, args[i+5] / RATIO);
                }
                i += 6;
 
                curve = b2Bezier.parseCurve([currentPosition, control1, control2, control3], resolution);
                curve.shift(); // the first point is redundant, "currentPosition" is already added.
                chainDef.vertices = chainDef.vertices.concat(curve);
 
                currentPosition = control3;
              } while (!isNaN(parseFloat(args[i])));
 
              prevControl = control2;
              break;
            case "S":
            case "s":
              // shorthand curveto
              relative = (command == "s");
              do
              {
                if (prevCommand == "C" || prevCommand == "c" || prevCommand == "S" || prevCommand == "s")
                {
                  control1 = new b2Vec2(currentPosition.x*2 - prevControl.x,
                                        currentPosition.y*2 - prevControl.y);
                } else {
                  control1 = new b2Vec2(currentPosition.x,
                                        currentPosition.y);
                }
                if (relative) {
                  control2 = new b2Vec2(currentPosition.x + args[i] / RATIO,
                                        currentPosition.y + args[i+1] / RATIO);
                  control3 = new b2Vec2(currentPosition.x + args[i+2] / RATIO,
                                        currentPosition.y + args[i+3] / RATIO);
                } else {
                  control2 = new b2Vec2(args[i] / RATIO, args[i+1] / RATIO);
                  control3 = new b2Vec2(args[i+2] / RATIO, args[i+3] / RATIO);
                }
                i += 4;
 
                curve = b2Bezier.parseCurve([currentPosition, control1, control2, control3], resolution);
                curve.shift(); // the first point is redundant, "currentPosition" is already added.
                chainDef.vertices = chainDef.vertices.concat(curve);
 
                currentPosition = control3;
                prevControl = control2;
                prevCommand = command;
 
              } while (!isNaN(parseFloat(args[i])));
 
              break;
            case "Q":
            case "q":
            case "T":
            case "t":
            case "A":
            case "a":
              throw new Error("TODO: Unimplemented path command: " + command);
              break;
          }
          prevCommand = command;
        }
      }
	  return shapes;
    }
 
    private static function filterEmptyString(item: *, index: int, array: Array): Boolean {
      return item != "";
    }
 
    public static function returnStrokeWidth(pathS:String):Number
    {
      var bar:Array;
      bar = (pathS.replace("px", "")).split(";"); //get rid of px in line-width and split
      return Number(bar[3].slice(13, bar[3].length));
    }
 
  }
 
}

b2Bezier.as

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
package  
{
	import Box2D.Common.Math.b2Vec2;
 
	/** 
	 * ...This class accepts an Array of four b2vec2s (corresponding to the SVG format of Bezier curves) and outputs an array of b2vec2s
	 * The returned array contains all of the points that should be declared in an edgeChain version of the curve
	 * This is a Flash port of the C++ version at http://www.box2d.org/forum/viewtopic.php?p=9865#p9865
	 * The math for transforming a Bezien curve is found at http://www.niksula.cs.hut.fi/~hkankaan/Homepages/bezierfast.html
	 * Thanks to Shaktool off the Box2D forums for the help
	 * 
	 * Quest Yarbrough/Ezion <www.ezqueststudios.com>
	 */
	public class b2Bezier 
	{
 
		//Resolution is the number of lines to segment the curve into
		//cPoints must be structured like below.
		//cPoints[0] = starting point
		//cPoints[1] = starting point control point
		//cPoints[2] = end point control point
		//cPoints[3] = end point
 
		public static function parseCurve(cPoints:Array, resolution:Number) 
		{
			if (resolution == 0) {
				return null;
			}
 
			var f:b2Vec2, fd:b2Vec2, fdd:b2Vec2, fddd:b2Vec2, fdd_per_2:b2Vec2, fddd_per_2:b2Vec2, fddd_per_6:b2Vec2;
			f = new b2Vec2(); fd = new b2Vec2(); fdd = new b2Vec2(); fddd = new b2Vec2();
			fdd_per_2 = new b2Vec2(); fddd_per_2 = new b2Vec2(); fddd_per_6 = new b2Vec2();
 
			var t:Number = 1.0 / resolution;
			var t2:Number = t * t;
 
			//I've tried to optimize the amount of
			//multiplications here, but these are exactly
			//the same formulas that were derived earlier
			//for f(0), f'(0)*t etc.
 
			f.x = cPoints[0].x;
			f.y = cPoints[0].y;
 
			fd.x = 3.0 * t * (cPoints[1].x - cPoints[0].x);
			fd.y = 3.0 * t * (cPoints[1].y - cPoints[0].y);
 
			fdd_per_2.x = 3.0 * t2 * (cPoints[0].x - 2.0 * cPoints[1].x + cPoints[2].x);
			fdd_per_2.y = 3.0 * t2 * (cPoints[0].y - 2.0 * cPoints[1].y + cPoints[2].y);
 
			fddd_per_2.x = 3.0 * t2 * t * (3.0 * (cPoints[1].x - cPoints[2].x) + cPoints[3].x - cPoints[0].x);
			fddd_per_2.y = 3.0 * t2 * t * (3.0 * (cPoints[1].y - cPoints[2].y) + cPoints[3].y - cPoints[0].y);
 
			fddd.x = fddd_per_2.x + fddd_per_2.x;
			fddd.y = fddd_per_2.y + fddd_per_2.y;
 
			fdd.x = fdd_per_2.x + fdd_per_2.x;
			fdd.y = fdd_per_2.y + fdd_per_2.y;
 
			fddd_per_6.x = (1.0 / 3) * fddd_per_2.x;
			fddd_per_6.y = (1.0 / 3) * fddd_per_2.y;
 
			var ret:Array = new Array(resolution);
 
			if (!ret) {
				return null;
			}
 
			for (var loop = 0; loop < resolution - 1; loop += 1) {
				ret[loop] = new b2Vec2();
				ret[loop].x = f.x;
				ret[loop].y = f.y;
 
				f.x = f.x + fd.x + fdd_per_2.x + fddd_per_6.x;
				f.y = f.y + fd.y + fdd_per_2.y + fddd_per_6.y;
 
				fd.x = fd.x + fdd.x + fddd_per_2.x;
				fd.y = fd.y + fdd.y + fddd_per_2.y;
 
				fdd.x = fdd.x + fddd.x;
				fdd.y = fdd.y + fddd.y;
 
				fdd_per_2.x = fdd_per_2.x + fddd_per_2.x;
				fdd_per_2.y = fdd_per_2.y + fddd_per_2.y;
			}
 
			ret[ret.length - 1] = new b2Vec2(cPoints[3].x, cPoints[3].y); //last vertice not added by C++ version
 
			return ret;
		}
 
	}
 
}

These classes may be included in the Box2D engine at some point, but until then they are available here. If anyone comes up with errors, drop a response in the comments and I’ll see what I can do.

->

Thanks to John Nesky for helping write the curve parser, and writing/restructuring the entire parser.

Related posts:

  1. Creating Levels in Box2D
  2. Dynamic Movieclip Animation Speed in AS3