This is an update of my previous post updating @roytries's Farseer Platformer tutorial on Sgt. Conker to Farseer 3.0. Since posting that item Farseer games have released Farseer Physics 3.2. This updated version will cover how to bring the original tutorial up to date to version 3.2, and I have added a secion at the end highlighting the changes from 3.0 to 3.2.
*************
Updating the Farseer Platformer tutorial to Farseer 3.2.
The Farseer platformer tutorial on Sgt. Conker (here http://www.sgtconker.com/2010/09/article-xna-farseer-platform-physics-tutorial/) is a brilliant guide to getting started with Farseer and integrating it with your own drawing code rather than using the 'DebugView' that comes with the Farseer samples.
If you haven't already you should go and work through it, as it'll give you a good idea of what Farseer does and roughly how it works. Then when you're done, head back here and we'll go through how to get the tutorial working with Farseer 3.2.
Why bother? Can't I just use the version that the tutorial uses?
Good question! I'll ask the second part of the question first. There is absolutely nothing stopping you using the older version of Farseer in your game, and indeed you may decide that it's preferable. The library for 2.1.3 compiles without error under XNA 4.0, and you may find it easier to work with because the units are simpler (see below).
So why would you bother to learn Farseer 3.2? Rather than trying to go over all the differences here, I'll direct you to the FAQ on the Farseer webpage:
http://farseerphysics.codeplex.com/wikipage?title=Differences%20between%20Farseer%20Physics%20Engine%202.x%20and%203.0&referringTitle=Tutorials
Its down to you to decide whether your project would benefit from the new features or not.
On with the show!
If you're still reading then you're at least curious about updating the tutorial to Farseer 3.2. I'm going to start at the beginning of the tutorial and work my way through, highlighting the changes as I go. If you'd rather just look at the updated source code, then I've included the code files at the bottom of the page.
Set-up
First things first, head to the Farseer webpage and download the latest release. You can either just download the Farseer library, or you can download the version with samples included as this contains a helper class that we're going to be using in a moment. I've also included the helper class in the source below though, so just grabbing the library is enough.
Creating a project
This section should be largely unchanged. I've called my project FarseerPlatformer for consistency. The only thing to note is that you need to make sure you reference the correct FarseerXNA project (3.2 as opposed to 2.1.3).
My first physics
Here we start to see the differences in the new engine.
First of all we need to tweak the using statements that we add to Game1 slightly, as the namespaces in the library are different. Change them to:
In 3.2, we no longer have a PhysicsSimulator. Instead we simply have a World class which we create an instance of. For our purposes we can treat this as a re-naming of the PhysicsSimulator class.
We create a new instance of World as follows:
Two things to note here. First, that the constructor for World is the same as the constructor for PhysicsSimulator, taking a vector2D representing gravity as its argument. Second, that the magnitude of the gravity vector is 20, rather than 500. This is due to a difference in the units that 3.2 uses, which we'll discuss in a moment.
Next up is updating our World each frame. This is done by adding the following to the bottom of the update method (before base.Update(gametime)):
world.Step() is the new update method for the World class. Note that we use milliseconds * 0.001 rather than seconds. This gives us a more accurate timing, which will improve our simulation.
Now we look to creating physics objects. Under 2.1.3, you created a body which managed and reacted to forces on your object, and a geom or geometry which was used for collisions, friction etc.
Under 3.2 you still have these (although a geom is now a Shape) but they are tied together by a Fixture. In fact we can create our object in one go using FixtureFactory. Before we do that however, we need to talk about units.
In 2.1.3, Farseer worked with pixels as its units, so all calculations were done in screen space. Box2D (the physics engine that 3.2 is based on) uses a system called MKS, or Meters, Kilograms, Seconds.
So, if we just punch in the dimensions of the block from the tutorial, the engine tries to simulate a block 64m x 64m, falling about 400m down the screen. As well as that, the engine is optimized for objects with dimensions between 0.1 and 10m, so its unlikely to give us good results.
All of this means we'll need to convert our dimensions from pixels to a scale that works with the engine, and back again when its time to draw. Fear not, help is at hand!
In the Sample project entitled 'Farseer Physics Engine 3.2
SimpleSamples XNA' that you downloaded (or in the source code below) you will find a class called ConvertUnits (in the folder Samples/DemoBaseXNA). This is a helper class designed to solve exactly this problem. Copy this file into your project and change the namespace to FarseerPlatformer (or whatever you called your project).
Now we're ready to create our block. Replace your SetupPhysics method with the following:
As you can see, the parameters for FixtureFactory.CreateRectangle are pretty much an amalgamation of those for creating the Body and Geom objects in the tutorial, and replacing the PhysicsSimulator with our World instance.
The only parameter that possibly deserves special mention is the 4th, which is density.
My first physics
Here we start to see the differences in the new engine.
First of all we need to tweak the using statements that we add to Game1 slightly, as the namespaces in the library are different. Change them to:
using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Factories; using FarseerPhysics.Collision;
In 3.2, we no longer have a PhysicsSimulator. Instead we simply have a World class which we create an instance of. For our purposes we can treat this as a re-naming of the PhysicsSimulator class.
We create a new instance of World as follows:
world = new World(new Vector2(0, 20f));
Two things to note here. First, that the constructor for World is the same as the constructor for PhysicsSimulator, taking a vector2D representing gravity as its argument. Second, that the magnitude of the gravity vector is 20, rather than 500. This is due to a difference in the units that 3.2 uses, which we'll discuss in a moment.
Next up is updating our World each frame. This is done by adding the following to the bottom of the update method (before base.Update(gametime)):
world.Step((float)(gameTime.ElapsedGameTime.TotalMilliseconds * 0.001));
world.Step() is the new update method for the World class. Note that we use milliseconds * 0.001 rather than seconds. This gives us a more accurate timing, which will improve our simulation.
Now we look to creating physics objects. Under 2.1.3, you created a body which managed and reacted to forces on your object, and a geom or geometry which was used for collisions, friction etc.
Under 3.2 you still have these (although a geom is now a Shape) but they are tied together by a Fixture. In fact we can create our object in one go using FixtureFactory. Before we do that however, we need to talk about units.
In 2.1.3, Farseer worked with pixels as its units, so all calculations were done in screen space. Box2D (the physics engine that 3.2 is based on) uses a system called MKS, or Meters, Kilograms, Seconds.
So, if we just punch in the dimensions of the block from the tutorial, the engine tries to simulate a block 64m x 64m, falling about 400m down the screen. As well as that, the engine is optimized for objects with dimensions between 0.1 and 10m, so its unlikely to give us good results.
All of this means we'll need to convert our dimensions from pixels to a scale that works with the engine, and back again when its time to draw. Fear not, help is at hand!
In the Sample project entitled 'Farseer Physics Engine 3.2
SimpleSamples XNA' that you downloaded (or in the source code below) you will find a class called ConvertUnits (in the folder Samples/DemoBaseXNA). This is a helper class designed to solve exactly this problem. Copy this file into your project and change the namespace to FarseerPlatformer (or whatever you called your project).
Now we're ready to create our block. Replace your SetupPhysics method with the following:
protected virtual void SetUpPhysics(World world, Vector2 position, float width, float height, float mass) { fixture = FixtureFactory.CreateRectangle(world, ConvertUnits.ToSimUnits(width), ConvertUnits.ToSimUnits(height), mass, ConvertUnits.ToSimUnits(position)); body = fixture.Body; fixture.Body.BodyType = BodyType.Dynamic; fixture.Restitution = 0.3f; fixture.Friction = 0.5f; }
As you can see, the parameters for FixtureFactory.CreateRectangle are pretty much an amalgamation of those for creating the Body and Geom objects in the tutorial, and replacing the PhysicsSimulator with our World instance.
3.2 calculates mass of your object based on its area and density. For this guide I have just substituted in the values that the original tutorial gave for mass, but you'll need to play with all of the values you use to get the effect you want.
We use ConvertToUnits.ToSimUnits() to convert our parameters to those suitable for use in the simulator. If you have a look inside the source code for this class you'll see that most of its methods are overloaded for doubles, ints, vector2D etc, so we really can just throw this method at our problem.
We also need to make some changes to the draw method. Replace PhysicsObject.draw() with the following method:
Notice that we have to convert all the values that we get from our simulated body back to pixel units, as otherwise we'd have a very, very small box!
You can check out the full source for the new PhysicsObject class below.
Now you just need to tweak the values inside your call to the PhysicsObject constructor like so:
And we're done for this section. You box should be falling down the screen in all it's 3.2 glory.
Laying grounds
We actually don't need to change much in this section for the example to work, we just need to swap out the PhysicsSimulator parameter for a World parameter:
However, when you run your program, you may notice that there is a gap of a couple of pixels between the box and the ground when its at rest. I have two possible explanations for this, but I haven't been able to confirm either. The first is rounding errors when we convert our units, and the second is the fact that Farseer 3.2 adds a 1cm 'buffer' around objects in the simulation, to allow for Continuous Collision Detection (CCD), which is a new feature of the engine.
You can either live with this visual discrepancy, or tweak the spritebatch.draw() call in PhysicsObject to look like this:
I've left the source code with the original that shows the gap, but you can do either.
Joints
RevoluteJoints seem to be much the same in 3.2 as they were in 2.1.3, so we only really need to tweak the CompositePhysicsObject class in a few places. Check out the class below:
I won't painstakingly go through the changes as a quick look at the old and new classes side by side should highlight the differences.
However, one thing to note is that the last parameter in the call to JointFactory.CreateRevoluteJoint() is just relativeJointPosition, rather than physObA.Position + relativeJointPosition (I changed poA to physObA and poB to physObB).
We use ConvertToUnits.ToSimUnits() to convert our parameters to those suitable for use in the simulator. If you have a look inside the source code for this class you'll see that most of its methods are overloaded for doubles, ints, vector2D etc, so we really can just throw this method at our problem.
We also need to make some changes to the draw method. Replace PhysicsObject.draw() with the following method:
public virtual void Draw(SpriteBatch spriteBatch) { spriteBatch.Draw(texture, new Rectangle((int)ConvertUnits.ToDisplayUnits(body.Position.X), (int)ConvertUnits.ToDisplayUnits(body.Position.Y), (int)width, (int)height), null, Color.White, body.Rotation, origin, SpriteEffects.None, 0f); }
Notice that we have to convert all the values that we get from our simulated body back to pixel units, as otherwise we'd have a very, very small box!
You can check out the full source for the new PhysicsObject class below.
Now you just need to tweak the values inside your call to the PhysicsObject constructor like so:
box = new PhysicsObject(world, new Vector2(100, 0), 64, 64, 10, squareTex);
And we're done for this section. You box should be falling down the screen in all it's 3.2 glory.
We actually don't need to change much in this section for the example to work, we just need to swap out the PhysicsSimulator parameter for a World parameter:
public class StaticPhysicsObject : PhysicsObject { public StaticPhysicsObject(World world, Vector2 position, float width, float height, Texture2D texture) : base(world, position, width, height, 1, texture) { fixture.Body.BodyType = BodyType.Static; } }
However, when you run your program, you may notice that there is a gap of a couple of pixels between the box and the ground when its at rest. I have two possible explanations for this, but I haven't been able to confirm either. The first is rounding errors when we convert our units, and the second is the fact that Farseer 3.2 adds a 1cm 'buffer' around objects in the simulation, to allow for Continuous Collision Detection (CCD), which is a new feature of the engine.
You can either live with this visual discrepancy, or tweak the spritebatch.draw() call in PhysicsObject to look like this:
spriteBatch.Draw(texture, new Rectangle((int)ConvertUnits.ToDisplayUnits(body.Position.X), (int)ConvertUnits.ToDisplayUnits(body.Position.Y), (int)width + 1, (int)height + 1), null, Color.White, body.Rotation, origin, SpriteEffects.None, 0f);
I've left the source code with the original that shows the gap, but you can do either.
Joints
RevoluteJoints seem to be much the same in 3.2 as they were in 2.1.3, so we only really need to tweak the CompositePhysicsObject class in a few places. Check out the class below:
public class CompositePhysicsObject { protected PhysicsObject physObA, physObB; protected RevoluteJoint revJoint; public CompositePhysicsObject(World world, PhysicsObject physObA, PhysicsObject physObB, Vector2 relativeJointPosition) { this.physObA = physObA; this.physObB = physObB; revJoint = JointFactory.CreateRevoluteJoint(world, physObA.fixture.Body, physObB.fixture.Body, ConvertUnits.ToSimUnits(relativeJointPosition)); physObA.fixture.CollisionFilter.IgnoreCollisionWith(physObB.fixture); physObB.fixture.CollisionFilter.IgnoreCollisionWith(physObA.fixture); } public virtual void Draw(SpriteBatch spriteBatch) { physObA.Draw(spriteBatch); physObB.Draw(spriteBatch); } }
I won't painstakingly go through the changes as a quick look at the old and new classes side by side should highlight the differences.
However, one thing to note is that the last parameter in the call to JointFactory.CreateRevoluteJoint() is just relativeJointPosition, rather than physObA.Position + relativeJointPosition (I changed poA to physObA and poB to physObB).
This is because in 3.2 this method takes the position of the joint in the local coordinates of the first Body in the parameter list, rather than in world coordinates as in the old method.
Again keep an eye out for when to use pixels as your units, and when you need to use ConvertUnits to convert to meters.
Springs
Unfortunately AngleSpring isn't in 3.2. According to Farseer's creator it shouldn't be too much work to port the AngleSpring from 2.1.3 to 3.2, but I decided not to go down that route (if you decide to give it a go though, be sure to share your results with the community!).
Instead, I decided to 'fake' an AngleSpring with an AngleJoint with a TargetAngle of 0, and play around with it's Softness and BiasFactor properties until I found some values that seemed to work.
The result was similar the original, but not quite the same. If you come up with a better solution please do share it with the community!
Below is my faked SpringPhysicsObject, and the call to create it. The values are just those that I found to work, feel free to play around with them and see if you can get a better result.
Force
Once again we don't need to change much here beyond the PhysicsSimulator/ World change. The only other thing to note is that the value for force used in the tutorial makes your box zip of the screen never to be seen again. Originally I played around with the value until I got one that felt ok.
As you no doubt saw from the original tutorial, impulses seem to work better for this sort of movement anyway, so that's what I've used here instead.
Most of the changes here are ones we have come across before, such as the use of Fixtures. However there are quite a few minor tweaks that need to be made, so I've included the whole of the source here in the text, as well as attaching the file below.
Other than again remembering to keep track of when you need to use pixels and when you need to use meters, the only other thing to note here is that body.LinearVelocity has to be replaced in its entirety. You can't change the X and Y components individually as you could in 2.1.3.
Afterthoughts
Well, hopefully this have given you enough to take what you learned in the original tutorial and carry it on into Farseer 3.2. As I said earlier, you will need to decide for yourself whether the potential confusion with using meters as opposed to pixels as the units for your physics simulation is worth it to benefit from the new features that 3.2 offers.
It may seem tricky, but as long as you keep track of which units you need to use where, its fairly easy to overcome.
Again keep an eye out for when to use pixels as your units, and when you need to use ConvertUnits to convert to meters.
Springs
Unfortunately AngleSpring isn't in 3.2. According to Farseer's creator it shouldn't be too much work to port the AngleSpring from 2.1.3 to 3.2, but I decided not to go down that route (if you decide to give it a go though, be sure to share your results with the community!).
Instead, I decided to 'fake' an AngleSpring with an AngleJoint with a TargetAngle of 0, and play around with it's Softness and BiasFactor properties until I found some values that seemed to work.
The result was similar the original, but not quite the same. If you come up with a better solution please do share it with the community!
Below is my faked SpringPhysicsObject, and the call to create it. The values are just those that I found to work, feel free to play around with them and see if you can get a better result.
public class SpringPhysicsObject : CompositePhysicsObject { protected AngleJoint springJoint; public SpringPhysicsObject(World world, PhysicsObject physObA, PhysicsObject physObB, Vector2 relativeJointPosition, float springSoftness, float springBiasFactor) : base(world, physObA, physObB, relativeJointPosition) { springJoint = JointFactory.CreateAngleJoint(world, physObA.fixture.Body, physObB.fixture.Body); springJoint.TargetAngle = 0; springJoint.Softness = springSoftness; springJoint.BiasFactor = springBiasFactor; } }
Force
As you no doubt saw from the original tutorial, impulses seem to work better for this sort of movement anyway, so that's what I've used here instead.
public class Character : PhysicsObject { public float forcePower; protected KeyboardState keyState; protected KeyboardState oldState; public Character(World world, Vector2 position, float width, float height, float mass, Texture2D texture) : base(world, position, width, height, mass, texture) { } public virtual void Update(GameTime gameTime) { HandleInput(gameTime); } protected virtual void HandleInput(GameTime gameTime) { keyState = Keyboard.GetState(); //Apply force in the arrow key direction Vector2 force = Vector2.Zero; if (keyState.IsKeyDown(Keys.Left)) { force.X -= forcePower * (float)gameTime.ElapsedGameTime.TotalSeconds; } if (keyState.IsKeyDown(Keys.Right)) { force.X += forcePower * (float)gameTime.ElapsedGameTime.TotalSeconds; } if (keyState.IsKeyDown(Keys.Up)) { force.Y -= forcePower * (float)gameTime.ElapsedGameTime.TotalSeconds; } if (keyState.IsKeyDown(Keys.Down)) { force.Y += forcePower * (float)gameTime.ElapsedGameTime.TotalSeconds; } body.ApplyLinearImpulse(force, body.Position); oldState = keyState; } }
Most of the changes here are ones we have come across before, such as the use of Fixtures. However there are quite a few minor tweaks that need to be made, so I've included the whole of the source here in the text, as well as attaching the file below.
Other than again remembering to keep track of when you need to use pixels and when you need to use meters, the only other thing to note here is that body.LinearVelocity has to be replaced in its entirety. You can't change the X and Y components individually as you could in 2.1.3.
public class CompositeCharacter : Character { public Fixture wheel; public FixedAngleJoint fixedAngleJoint; public RevoluteJoint motor; private float centerOffset; public Activity activity; protected Activity oldActivity; private Vector2 jumpForce; private float jumpDelayTime; private const float nextJumpDelayTime = 1f; private const float runSpeed = 8; private const float jumpImpulse = -500; public CompositeCharacter(World world, Vector2 position, float width, float height, float mass, Texture2D texture) : base(world, position, width, height, mass, texture) { if (width > height) { throw new Exception("Error width > height: can't make character because wheel would stick out of body"); } activity = Activity.None; wheel.OnCollision += new OnCollisionEventHandler(OnCollision); } protected override void SetUpPhysics(World world, Vector2 position, float width, float height, float mass) { //Create a fixture with a body almost the size of the entire object //but with the bottom part cut off. float upperBodyHeight = height - (width / 2); fixture = FixtureFactory.CreateRectangle(world, (float)ConvertUnits.ToSimUnits(width), (float)ConvertUnits.ToSimUnits(upperBodyHeight), mass / 2); body = fixture.Body; fixture.Body.BodyType = BodyType.Dynamic; fixture.Restitution = 0.3f; fixture.Friction = 0.5f; //also shift it up a tiny bit to keey the new object's center correct body.Position = ConvertUnits.ToSimUnits(position - (Vector2.UnitY * (width / 4))); centerOffset = position.Y - (float)ConvertUnits.ToDisplayUnits(body.Position.Y); //remember the offset from the center for drawing //Now let's make sure our upperbody is always facing up. fixedAngleJoint = JointFactory.CreateFixedAngleJoint(world, body); //Create a wheel as wide as the whole object wheel = FixtureFactory.CreateCircle(world, (float)ConvertUnits.ToSimUnits(width / 2), mass / 2); //And position its center at the bottom of the upper body wheel.Body.Position = body.Position + ConvertUnits.ToSimUnits(Vector2.UnitY * (upperBodyHeight / 2)); wheel.Body.BodyType = BodyType.Dynamic; wheel.Restitution = 0.3f; wheel.Friction = 0.5f; //These two bodies together are width wide and height high :) //So lets connect them together motor = JointFactory.CreateRevoluteJoint(world, body, wheel.Body, Vector2.Zero); motor.MotorEnabled = true; motor.MaxMotorTorque = 1000f; //set this higher for some more juice motor.MotorSpeed = 0; //Make sure the two fixtures don't collide with each other wheel.CollisionFilter.IgnoreCollisionWith(fixture); fixture.CollisionFilter.IgnoreCollisionWith(wheel); //Set the friction of the wheel to float.MaxValue for fast stopping/starting //or set it higher to make the character slip. wheel.Friction = float.MaxValue; } //Fired when we collide with another object. Use this to stop jumping //and resume normal movement public bool OnCollision(Fixture fix1, Fixture fix2, Contact contact) { //Check if we are both jumping this frame and last frame //so that we ignore the initial collision from jumping away from //the ground if (activity == Activity.Jumping && oldActivity == Activity.Jumping) { activity = Activity.None; } return true; } protected override void HandleInput(GameTime gameTime) { oldActivity = activity; keyState = Keyboard.GetState(); HandleJumping(keyState, oldState, gameTime); if (activity != Activity.Jumping) { HandleRunning(keyState, oldState, gameTime); } if (activity != Activity.Jumping && activity != Activity.Running) { HandleIdle(keyState, oldState, gameTime); } oldState = keyState; } private void HandleJumping(KeyboardState state, KeyboardState oldState, GameTime gameTime) { if (jumpDelayTime < 0) { jumpDelayTime += (float)gameTime.ElapsedGameTime.TotalSeconds; } if (state.IsKeyUp(Keys.Space) && oldState.IsKeyDown(Keys.Space) && activity != Activity.Jumping) { if (jumpDelayTime >= 0) { motor.MotorSpeed = 0; jumpForce.Y = jumpImpulse; body.ApplyLinearImpulse(jumpForce, body.Position); jumpDelayTime = -nextJumpDelayTime; activity = Activity.Jumping; } } if (activity == Activity.Jumping) { if (keyState.IsKeyDown(Keys.Right)) { if (body.LinearVelocity.X < 0) { body.LinearVelocity = new Vector2(-body.LinearVelocity.X * 2, body.LinearVelocity.Y); } } else if (keyState.IsKeyDown(Keys.Left)) { if (body.LinearVelocity.X > 0) { body.LinearVelocity = new Vector2(-body.LinearVelocity.X * 2, body.LinearVelocity.Y); } } } } private void HandleRunning(KeyboardState state, KeyboardState oldState, GameTime gameTime) { if (keyState.IsKeyDown(Keys.Right)) { motor.MotorSpeed = runSpeed; activity = Activity.Running; } else if (keyState.IsKeyDown(Keys.Left)) { motor.MotorSpeed = -runSpeed; activity = Activity.Running; } if (keyState.IsKeyUp(Keys.Left) && keyState.IsKeyUp(Keys.Right)) { motor.MotorSpeed = 0; activity = Activity.None; } } private void HandleIdle(KeyboardState state, KeyboardState oldState, GameTime gameTime) { if (activity == Activity.None) { activity = Activity.Idle; } } public override void Draw(SpriteBatch spriteBatch) { //These first two draw calls draw the upper and lower body independently spriteBatch.Draw(texture, new Rectangle((int)ConvertUnits.ToDisplayUnits(body.Position.X), (int)ConvertUnits.ToDisplayUnits(body.Position.Y), (int)width, (int)(height - (width / 2))), null, Color.White, body.Rotation, origin, SpriteEffects.None, 0f); spriteBatch.Draw(texture, new Rectangle((int)ConvertUnits.ToDisplayUnits(wheel.Body.Position.X), (int)ConvertUnits.ToDisplayUnits(wheel.Body.Position.Y), (int)width, (int)width), null, Color.White, wheel.Body.Rotation, origin, SpriteEffects.None, 0f); //This last draw call shows how to draw these two bodies with one texture (drawn semi-transparent here so you can see the inner workings) spriteBatch.Draw(texture, new Rectangle((int)Position.X, (int)(Position.Y), (int)width, (int)height), null, new Color(1, 1, 1, 0.5f), body.Rotation, origin, SpriteEffects.None, 0f); } public override Vector2 Position { get { return (ConvertUnits.ToDisplayUnits(body.Position) + Vector2.UnitY * centerOffset); } } }
Afterthoughts
Well, hopefully this have given you enough to take what you learned in the original tutorial and carry it on into Farseer 3.2. As I said earlier, you will need to decide for yourself whether the potential confusion with using meters as opposed to pixels as the units for your physics simulation is worth it to benefit from the new features that 3.2 offers.
It may seem tricky, but as long as you keep track of which units you need to use where, its fairly easy to overcome.
Great stuff, will download the sample when I get back to my laptop and start playing.
ReplyDeleteThanks for this, very keen to get to grips with v3x and couldnt find much of use out there.
ReplyDeleteThanks man!
ReplyDeletethanks! this helped me learn alot, although converting it to work in 3.3 was a bitch since I didn't know ANYTHING about XNA, but I eventually figured it out (and learned alot in the process). Thanks for this great tutorial!
ReplyDeleteLooks great! I still need to try it out and get it to work, but thanks in advance!
ReplyDeleteBTW you may need to update the code to the newest version of Farseer as a lot of methods have changed again.
any news on how this works with 3.3?
ReplyDelete