Spring 2010 Week4

Notes:

I was planning on doing a tutorial on the Farseer Physics Engine. However, the most recent version of Farseer requires XNA 3.1 and the computers in Old Main 407 only have XNA 3.0. I tried using an older version of Farseer but the differences were pretty significant.
Instead we went over the AI for Tetris and a few other miscellaneous things.

Anyway, I'm doing the tutorial for Farseer here.

Start by downloading the latest version of Farseer, unzip it, open the folder, and load the .sln file.

The only files we'll be changing (besides the ones that we create) will be in the SimpleSamplesXNA project. See all the folders that say "DemoX"? We will create our own demo so create a new folder called "SIGXNADemo" or something like that by right-clicking on the SimpleSamplesXNA project, select Add->New Folder and name it "SIGXNADemo".
In this new folder we will create our demo. Right-click on the new folder and select Add->Class, give it the name "SIGXNADemoScreen" and click on Add.
In the new file "SIGXNADemoScreen" that opened, make the class public and have it inherit from GameScreen:

public class SIGXNADemoScreen : GameScreen

Also add these using statements if they're not already there:
using DemoBaseXNA;
using DemoBaseXNA.DrawingSystem;
using DemoBaseXNA.ScreenSystem;
using FarseerGames.FarseerPhysics;
using FarseerGames.FarseerPhysics.Dynamics;
using FarseerGames.FarseerPhysics.Factories;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

Next, add the following methods to SIGXNADemoScreen. If you want to take the time to understand what it does that's fine, but this code doesn't really have anything to do the with actual engine - it's just setting things up to run:
public override void HandleInput(InputState input)
{
 
    if (input.PauseGame)
    {
        ScreenManager.AddScreen(new PauseScreen(GetTitle(), GetDetails()));
    }
 
    base.HandleInput(input);
}
 
public static string GetTitle()
{
    return "SIGXNA Demo";
}
 
private static string GetDetails()
{
    return "Learning to use some stuff in Farseer.";
}

The first method HandleInput() handles when the player pressed the Esc key to pause the demo. The next method GetTitle() just returns the name of this demo. The final method GetDetails() returns a detailed description of the demo.
Open FarseerPhysicsGame.cs and go to the bottom of the constructor where it has a bunch of lines of code that say:

ScreenManager.MainMenuScreen.AddMainMenuItem(DemoXScreen.GetTitle(), new DemoXScreen());

Add this line right above all those lines:
ScreenManager.MainMenuScreen.AddMainMenuItem(SIGXNADemoScreen.GetTitle(), new SIGXNADemoScreen());

This will add our demo to the options in the main menu. Feel free to run the game now and see the "SIGXNA Demo" option at the top. Go into it and notice nothing is going on. Now Press Esc to pause the demo and see the demo description "Learning to use some stuff in Farseer."

Now we're ready to get some physics stuff running. We'll start by Initializing the physics engine.
Add the following method at the top of SIGXNADemoScreen:

public override void Initialize()
{
    PhysicsSimulator = new PhysicsSimulator(new Vector2(0, 0));
    PhysicsSimulatorView = new PhysicsSimulatorView(PhysicsSimulator);
 
    base.Initialize();
}

The first line of code initializes the PhysicalSimulator object (inherited from GameScreen). Note that it is passing a Vector3 object initialized to (0, 0). This is setting the gravity component to no gravity. You can set it to something like (0, 50) to make it fall at a noticible rate if you want, but I won't (for now, at least).
Next, we'll add a bunch of circles. At the top of SIGXNADemoScreen declare the following variables:
private const int circleRadius = 32;
private const int circleCount = 24;
private Body[] circles;
private CircleBrush circleBrush;

The first two variables are constants - the first one, circleRadius, is the radius of the circles we will create. The second one, circleCount, is the number of circles we will create. The Body array circles will hold the logical physics objects and the CircleBrush object is used for drawing the circles on the screen (and therefore is not part of the Farseer Physics Engine).
Now that we have some physics objects created, we need to initialize them. Add the following method to the code:
public override void LoadContent()
{
    // Create the brush that will be used to draw the circle
    circleBrush = new CircleBrush(circleRadius, Color.White, Color.Black);
    circleBrush.Load(ScreenManager.GraphicsDevice);
 
    circles = new Body[circleCount];
    for (int i = 0; i < circleCount; i++)
    {
        circles[i] = BodyFactory.Instance.CreateCircleBody(PhysicsSimulator, circleRadius, 2);
        circles[i].Position = new Vector2(150 + 150 * (i % (circleCount / 4)), 150 + 150 * (i / (circleCount / 4)));
        GeomFactory.Instance.CreateCircleGeom(PhysicsSimulator, circles[i], circleRadius, 20);
    }
 
    base.LoadContent();
}

The first two lines initialize and load the CircleBrush object. Remember, this is just used to draw circles in the demo and don't really have anything to do with the physics engine. The next line initializes the circles array to hold circleCount circles.
Now for the for loop: The first line creates the circle bodies with a radius circleRadius and a mass 2 and assigns the circle to our PhysicsSimulator. The second line sets their positions based on the index. Don't worry about all the magic numbers on that line - all it does is space out the circles nicely. The third line sets the circles up to interact with each other (collision detection).
In the last line we call base.LoadContent() to run its base class' LoadContent() method.
There's one more method we need to override to get things running. Add the following method to the class:
public override void Draw(GameTime gameTime)
{
    ScreenManager.SpriteBatch.Begin();
    foreach (Body circle in circles)
    {
        circleBrush.Draw(ScreenManager.SpriteBatch, circle.Position);
    }
    ScreenManager.SpriteBatch.End();
 
    base.Draw(gameTime);
}

Obviously we're overloading the Draw() method. ScreenManager.SpriteBatch is exactly the same as the spriteBatch object in Game1.cs when you start an XNA project. So just like spriteBatch, we have a ScreenManager.SpriteBatch.Begin() and ScreenManager.SpriteBatch.End(). In between the beginning and end, we have a foreach loop that draws each of the circles using circleBrush. Finally at the end of the method we call base.Draw() which calls the base class Draw() method.
Go ahead and run the code now - it should show a bunch of circles. You can use your mouse to grab the circles and move them around. At this point, you can also change the PhysicsSimulator initialization to have gravity:
PhysicsSimulator = new PhysicsSimulator(new Vector2(0, 50));

All of this is fine, but wouldn't it be nice to move on from a simple simulation/demo to something more game-like? Well, we won't really do this, but we will create a spaceship object that we can use to interact with all the circles. Go ahead and remove the gravity from PhysicsSimulator:
PhysicsSimulator = new PhysicsSimulator(Vector2.Zero);

In your SolutionExplorer, right-click on the SIGXNADemo folder and click Add->Class. Name it Ship.cs and click on Add. The ship class should have the following using statements:
using System;
using FarseerGames.FarseerPhysics;
using FarseerGames.FarseerPhysics.Dynamics;
using FarseerGames.FarseerPhysics.Factories;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

and remember to make the class public:
public class Ship

Add these variables to the top of the Ship class:
public Body Body;
private readonly Texture2D _texture;
private readonly Vector2 _origin;
private readonly float _scale;

The ship will need a Body object that will hold the position and rotation. Body also has a bunch of variables like Velocity and DragCoefficient that we'll access. In addition to Body, we'll need a Texture2D to hold the ship image, an origin variable so the image will rotate properly, and a scale variable to make the texture the correct size.
Now that we have those variables, we need to set them in the constructor. Add this method:
public Ship(Texture2D texture, PhysicsSimulator physicsSimulator)
{
    _texture = texture;
    _origin = new Vector2(texture.Width / 2f, texture.Height / 2f);
    _scale = 0.2f;
    Body = BodyFactory.Instance.CreateEllipseBody(physicsSimulator, _texture.Width * _scale / 2, _texture.Height * _scale / 2, 10);
    Body.RotationalDragCoefficient = 0.01f;
    GeomFactory.Instance.CreateEllipseGeom(physicsSimulator, Body, _texture.Width * _scale / 2, _texture.Height * _scale / 2, 20);
}

The constructor takes a Texture2D and our PhysicsSimulator variable as parameters. The first thing we do is set the _texture and _origin variables. The _scale variable is set to 0.2f so that the texture will be the proper size.
Next we initialize the Body variable. Not that the shape will be an Ellipse and not a Circle. We pass it physicsSimulator so that it can be added to the simulator. We also set the width and height of the ellipse based on the texture size and scale. Next line of code sets the RotationalDragCoefficient to slightly higher than the default (0.001f). This is just a personal choice and doesn't really make a huge difference. The last line of code registers this object for collision detection with the circles.

Let's create the Draw() method for Ship as it's really easy. Put this at the bottom of the file:

public void Draw(SpriteBatch spriteBatch)
{
    spriteBatch.Draw(
        _texture,
        Body.Position,
        null,
        Color.White,
        Body.Rotation,
        _origin,
        _scale,
        SpriteEffects.None,
        0.0f
        );
}

We've done this exact same method many times in previous tutorials but I'll go over it really quickly. All we are doing is calling spriteBatch.Draw() with these parameters: our texture, the position to draw it, the part of the texture we want to draw - we want to draw the whole thing so we pass it null, the tint color - we don't want to tint the texture so we pass it white, the rotation, the origin, the scale, the sprite effects - again we don't want to flip it around, so we pass it none, and finally the layer, which we don't care about so we just pass it 0.

There are three more methods to add - turning left and right, and adding thrust. We'll start with turning left and right. Add these two methods to the Ship class:

public void TurnLeft()
{
    Body.Rotation -= MathHelper.ToRadians(2);
    Body.AngularVelocity = 0;
}
 
public void TurnRight()
{
    Body.Rotation += MathHelper.ToRadians(2);
    Body.AngularVelocity = 0;
}

These methods will be called in SIGXNADemoScreen's HandleInput() method whenever the player presses left or right. All we do in the methods is change the rotation by 2 degrees each update. I noticed that I didn't really like it when the ship kept on rotating after I turned it, so I remove the angular velocity any time I turn.
Last method in Ship is the Thrust() method:
public void Thrust()
{
    const float forceAmount = 10000;
    Vector2 force = new Vector2(
        (float)Math.Sin(Body.Rotation) * forceAmount,
        (float)-Math.Cos(Body.Rotation) * forceAmount
    );
    Body.AngularVelocity = 0;
 
    Body.ApplyForce(force);
}

First thing we do is create a constant force of 10000. Next thing we do is create a force vector based on the rotation and the force amount. We set angular velocity to zero, just like we did in TurnLeft() and TurnRight(). Finally we apply the force to the ship's Body attribute.
We've spent enough time creating this class, let's get back to SIGXNADemoScreen.cs and put the ship in the demo.

At the top of SIGXNADemoScreen where all the other variables are declared, add this one:

private Ship ship;

Next, in LoadContent(), add these three lines of code:
ship = new Ship(ScreenManager.ContentManager.Load<Texture2D>("Content/spaceship"), PhysicsSimulator);
ship.Body.Position = new Vector2(500);
ship.Body.IgnoreGravity = true;

The first line initializes the ship object with our ship texture that we should load and the PhysicsSimulator object. Speaking of the ship texture, let's put it in the Content directory. You can find the texture here. Right-click on the Content folder in your SimpleSamplesXNA project and click Add->Existing Item, browse to where you saved spaceship.png, select it, and click Add.
Now, back to the code. The next line sets the initial position and the final line has the ship not be affected by gravity.
Scroll down to the Draw() method. Right after the foreach loop but before the ScreenManager.SpriteBatch.End(); add this line:
ship.Draw(ScreenManager.SpriteBatch);

so the final Draw() method should look like this:
public override void Draw(GameTime gameTime)
{
    ScreenManager.SpriteBatch.Begin();
    foreach (Body circle in circles)
    {
        circleBrush.Draw(ScreenManager.SpriteBatch, circle.Position);
    }
    ship.Draw(ScreenManager.SpriteBatch);
    ScreenManager.SpriteBatch.End();
    base.Draw(gameTime);
}

Go ahead and run the code and you should have a ship appear in the middle of the screen that you can drag around using the mouse just like the circles.

Last thing we want to do is get the ship to fly around using the keyboard.
Add these lines of code to the HandleInput() method:

if (input.CurrentKeyboardState.IsKeyDown(Keys.Up))
    ship.Thrust();
 
if (input.CurrentKeyboardState.IsKeyDown(Keys.Left))
    ship.TurnLeft();
 
if (input.CurrentKeyboardState.IsKeyDown(Keys.Right))
    ship.TurnRight();

It should be pretty obvious what is going on here. If the player presses Keys.Up, the ship will apply a force in the direction the ship is facing. If the player presses Keys.Left or Keys.Right, the ship should rotate.
Run the demo and you can now control the ship!

back to SIGXNA

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License