Running on Empty

The few things I know, I like to share.

XNA Framework GameEngine Development. (Part 14, Adding Physics)

Introduction

Welcome to Part 14 of the XNA Framework GameEngine Development series.  This is the first article in the Physics sub-section and will deal mainly on integrating physics into the exisiting engine.  I am not a physics expert and will not go into the details of how dynamic objects interact.  Rather my goal is to integrate an existing physics engine into up until now can only be described as a wrapper for the XNA rendering engine.

part14.jpg

Reader Feedback

I had intended to use the Newton Physics Wrapper written by TamedTornado, but thanks to feedback from (kt) I moved over to the BulletX physics engine.  I do read your comments and want them, take the time to make a suggestion and I will work to make it a part of the engine.  I do want to give everybody an oppurtunity to get the best start for making games, I don’t know all the answers, so I find your insights very valuable.  We all get better games in return.

Release

Here is the source code for this article.  Please feel free to use this code, make improvements if you like, but share what you do.  Drop me a line or comment if you do make something with this engine, I would love to see it.

Shapes

The only major new concept that we have to pay attention to for physics is the concept of shapes.  Every object that we want to have in the physics engine must define a shape.  The BulletX engine goes out of its way to provide many useful shapes for our game objects.  In this example I have solely used the Box shape, but in future demos I will use others.

PhysicsManager

The PhysicsManager is the first internal class GameComponent of the engine.  That means the “game” will never be able to use its classes.  This is an encapsulation technique that will protect a very busy class from being misused.  As a result, the PhysicsManager is tied very closly to the SceneGraphManager (and will be tied to other scene managers in the future).  So what does the physics manager do for us?

First, it defines the World using properties such as gravity, the axis aligned bounding box (aabb) algorithm, as well as collision data.  Quite a lot of information here, most of which is just intializing the stuff that makes BulletX work.

Second, it contains the CreateRigidBody method.  This is the single most important method in the class and the one you will use most often as a game developer.  This method takes an IRoeBulletPhysics object as a parameter adds it to the world and returns a world index to the object passed in as an out parameter.  This method does all the setup work for getting objects into the BulletX engine, not that it is hard, it just isn’t code I want to write all the time.

Finally, the GetObject method allows you to grab the CollisionObject from the world by index.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using RoeEngine2.GameComponents;
using RoeEngine2.Interfaces;
using XnaDevRu.BulletX;
using XnaDevRu.BulletX.Dynamics;
using RoeEngine2.SceneObject;

namespace RoeEngine2.Managers
{
    internal class PhysicsManager : GameComponent
    {
        private static int _collisionIndex = 0;
        private static CollisionDispatcher _collisionDispatcher;
        private static OverlappingPairCache _broadPhase;
        private static SequentialImpulseConstraintSolver _solver;
        private static DiscreteDynamicsWorld _world;
        public static DiscreteDynamicsWorld World
        {
            get { return _world; }
        }

        private static bool _useSweepAndPrune;
        public static bool UseSweepAndPrune
        {
            get { return _useSweepAndPrune; }
            set { _useSweepAndPrune = value; }
        }

        private static bool _drawDebugger;
        public static bool DrawDebugger
        {
            get { return _drawDebugger; }
            set { _drawDebugger = value; }
        }

        private static bool _initialized = false;
        /// 
        /// Is the PhysicsManagers Initialized, used for test cases and setup of Effects.
        /// 
        public static bool Initialized
        {
            get { return _initialized; }
        }

        /// 
        /// Create the Physics Managers.
        /// 
        /// 
        public PhysicsManager(Game game)
            : base(game)
        {
            Enabled = true;
            ResetPhysics();
        }

        public static void ResetPhysics()
        {
            _collisionDispatcher = new CollisionDispatcher();

            if (_useSweepAndPrune)
            {
                Vector3 worldAabbMin = new Vector3(-10000, -10000, -10000);
                Vector3 worldAabbMax = new Vector3(10000, 10000, 10000);
                const int maxProxies = 32766;
                _broadPhase = new AxisSweep3(worldAabbMin, worldAabbMax, maxProxies);
            }
            else
            {
                _broadPhase = new SimpleBroadphase();
            }
            _solver = new SequentialImpulseConstraintSolver();
            _world = new DiscreteDynamicsWorld(_collisionDispatcher, _broadPhase, _solver);

            _world.Gravity = new Vector3(0, -10.0f, 0);

            GC.Collect();
        }

        public override void Initialize()
        {
            base.Initialize();

            XnaDebugDraw debugDrawer = new XnaDebugDraw(EngineManager.Device);
            debugDrawer.DebugMode = DebugDrawModes.DrawAabb | DebugDrawModes.DrawContactPoints;
            _world.DebugDrawer = debugDrawer;

            _initialized = true;
        }

        public static void Draw(GameTime gameTime)
        {
            if (_drawDebugger)
                ((XnaDebugDraw)_world.DebugDrawer).Update(CameraManager.ActiveCamera.View, CameraManager.ActiveCamera.Projection);
            _world.StepSimulation(1.0f / 60.0f, 1);
        }

        /// 
        /// Add a new shape to the physics manager.
        /// 
        /// Body mass, if 0 body is static.
        /// Starting body matrix transform.
        /// Body shape
        /// Index into the Physics World collection
        /// Created Rigid Body
        private static void CreateRigidBody(float mass, Matrix startMatrix,
                                                 CollisionShape newShape, CollisionOptions options,
                                                 out int worldIndex)
        {
            bool isDynamic = (mass > 0.0f);

            Vector3 localInertia = new Vector3();
            if (isDynamic)
                newShape.CalculateLocalInertia(mass, out localInertia);

            //using motionstate is recommended, it provides interpolation capabilities, and only synchronizes 'active' objects
            DefaultMotionState myMotionState = new DefaultMotionState(startMatrix, Matrix.Identity);
            RigidBody body = new RigidBody(mass, myMotionState, newShape, localInertia, 0.0f, 0.0f, 0.5f, 0.0f);

            _world.AddRigidBody(body);

            worldIndex = _collisionIndex;
            _collisionIndex++;

            body.CollisionFlags |= options;
        }

        /// 
        /// Add a new shape to the physics manager.
        /// 
        /// Body mass, if 0 body is static.
        public static void CreateRigidBody(IRoeBulletPhysics iRoeBulletPhysics)
        {
            int index;
            RoeSceneObject obj = (RoeSceneObject)iRoeBulletPhysics;
            CreateRigidBody(iRoeBulletPhysics.Mass, Matrix.CreateTranslation(obj.Position) * Matrix.CreateFromQuaternion(obj.Rotation),
                            iRoeBulletPhysics.CollisionShape, iRoeBulletPhysics.CollisionOptions,
                            out index);
            iRoeBulletPhysics.WorldIndex = index;
        }

        public static CollisionObject GetObject(int index)
        {
            if (_world.CollisionObjectsCount > index)
            {
                return _world.CollisionObjects[index];
            }
            return null;
        }
    }
}

Interface

As usual I like to use interfaces to define base classes for game objects.  Here is the new IRoeBulletPhysics interface.

using System;
using System.Collections.Generic;
using System.Text;
using XnaDevRu.BulletX.Dynamics;
using XnaDevRu.BulletX;

namespace RoeEngine2.Interfaces
{
    /// 
    /// Every RoeSceneObject that implements this interface will be added to the physics manager
    /// 
    public interface IRoeBulletPhysics : IRoeSceneObject
    {
        CollisionOptions CollisionOptions
        {
            get;
            set;
        }

        int WorldIndex
        {
            get;
            set;
        }
       
        CollisionShape CollisionShape
        {
            get;
            set;
        }

        float Mass
        {
            get;
            set;
        }
    }
}

RoeSceneObject

Just some small changes here, mainly to do with the World matrix.  We pretty much need to give control over world transform to the physics engine, here is how to do it.

        public virtual Matrix World
        {
            get
            {
                if (ReadyToRender)
                {
                    if (this is IRoeBulletPhysics)
                    {
                        Matrix world = Matrix.Identity;
                        CollisionObject obj = PhysicsManager.World.CollisionObjects[((IRoeBulletPhysics)this).WorldIndex];
                        RigidBody body = RigidBody.Upcast(obj);

                        if (body != null && body.MotionState != null)
                        {
                            DefaultMotionState myMotionState = (DefaultMotionState)body.MotionState;
                            return Matrix.CreateScale(Scale) * myMotionState.GraphicsWorldTransform;
                        }
                        else
                        {
                            return Matrix.CreateScale(Scale) * obj.WorldTransform;
                        }
                    }
                }
                return Matrix.CreateScale(_scale) * Matrix.CreateFromQuaternion(_rotation) * Matrix.CreateTranslation(_position);
            }
        }

Box.cs

I know this isn’t very interesting, but it is a start.  In any good physics demo, you get to see spheres or boxes falling and colliding.  So we need to make a Box class.

using System;
using System.Collections.Generic;
using System.Text;
using RoeEngine2.Interfaces;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using XnaDevRu.BulletX;
using RoeEngine2.Managers;
using RoeEngine2.Models;
using XnaDevRu.BulletX.Dynamics;

namespace RoeEngine2.SceneObject.StandardObjects
{
    public class Box : RoeSceneObject, IRoeLoadable, IRoeUpdateable, IRoeOcclusion, IRoeBulletPhysics
    {
        public VertexPositionColor[] points = new VertexPositionColor[8];
        public int[] index = new int[24];

        private string _occlusionModelName;
        public string OcclusionModelName
        {
            get { return _occlusionModelName; }
            set { _occlusionModelName = value; }
        }

        private OcclusionQuery _query = new OcclusionQuery(EngineManager.Device);
        public OcclusionQuery Query
        {
            get { return _query; }
        }

        private bool _occluded = false;
        public bool Occluded
        {
            get { return _occluded; }
            set { _occluded = value; }
        }

        private bool _culled = false;
        public bool Culled
        {
            get { return _culled; }
            set { _culled = value; }
        }

        private CollisionOptions _collisionOptions;
        public CollisionOptions CollisionOptions
        {
            get { return _collisionOptions; }
            set { _collisionOptions = value; }
        }

        private int _worldIndex;
        public int WorldIndex
        {
            get { return _worldIndex; }
            set { _worldIndex = value; }
        }

        private CollisionShape _collisionShape;
        public CollisionShape CollisionShape
        {
            get { return _collisionShape; }
            set { _collisionShape = value; }
        }

        private float _mass = 0.0f;
        public float Mass
        {
            get { return _mass; }
            set { _mass = value; }
        }

        private bool _boundingBoxCreated;
        public bool BoundingBoxCreated
        {
            get { return _boundingBoxCreated; }
        }

        public BoundingBox _boundingBox;
        public BoundingBox BoundingBox
        {
            get { return _boundingBox; }
        }

        public Box()
        {
           
        }

        public Box(Vector3 newPosition)
        {
            Position = newPosition;
        }

        public BoundingBox GetBoundingBoxTransformed()
        {
            Vector3 min, max;
            min = _boundingBox.Min;
            max = _boundingBox.Max;
           
            min = Vector3.Transform(_boundingBox.Min, World);
            max = Vector3.Transform(_boundingBox.Max, World);

            return new BoundingBox(min, max);
        }

        public void Update(GameTime gameTime)
        {
            float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

            IRoeModel model = ModelManager.GetModel(ModelName);
            if (model != null && model.ReadyToRender && !ReadyToRender)
            {
                Matrix[] transforms = new Matrix[model.BaseModel.Bones.Count];
                model.BaseModel.CopyAbsoluteBoneTransformsTo(transforms);

                _boundingBox = new BoundingBox();

                foreach (ModelMesh mesh in model.BaseModel.Meshes)
                {
                    if (!BoundingBoxCreated)
                    {
                        _boundingBox = BoundingBox.CreateMerged(_boundingBox, BoundingBox.CreateFromSphere(mesh.BoundingSphere));
                    }
                }
                _boundingBoxCreated = true;

                Vector3 min, max;
                min = BoundingBox.Min * new Vector3(0.6f, 0.6f, 0.6f);
                max = BoundingBox.Max * new Vector3(0.6f, 0.6f, 0.6f);

                _boundingBox = new BoundingBox(min, max);

                points[0].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Min.Y, _boundingBox.Min.Z);
                points[1].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Min.Y, _boundingBox.Min.Z);
                points[2].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Min.Y, _boundingBox.Max.Z);
                points[3].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Min.Y, _boundingBox.Max.Z);

                points[4].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Max.Y, _boundingBox.Min.Z);
                points[5].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Max.Y, _boundingBox.Min.Z);
                points[6].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Max.Y, _boundingBox.Max.Z);
                points[7].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Max.Y, _boundingBox.Max.Z);

                _collisionShape = new BoxShape(Scale / 2.0f);
                SceneGraphManager.AddPhysics(this);
               
                ReadyToRender = true;
            }
        }

        public void LoadContent()
        {
            RoeModel model = new RoeModel("Content/Models/Box");

            ModelManager.AddModel(model, "boxmodel");
            this.ModelName = "boxmodel";
            this.OcclusionModelName = "boxmodel";

            index[0] = 0;
            index[1] = 1;
            index[2] = 1;
            index[3] = 2;
            index[4] = 2;
            index[5] = 3;
            index[6] = 3;
            index[7] = 0;

            index[8] = 4;
            index[9] = 5;
            index[10] = 5;
            index[11] = 6;
            index[12] = 6;
            index[13] = 7;
            index[14] = 7;
            index[15] = 4;

            index[16] = 0;
            index[17] = 4;
            index[18] = 1;
            index[19] = 5;
            index[20] = 2;
            index[21] = 6;
            index[22] = 3;
            index[23] = 7;

        }

        public void UnloadContent()
        {
           
        }

        public override void Draw(GameTime gameTime)
        {
            base.Draw(gameTime);

            using (VertexDeclaration declaration = new VertexDeclaration(EngineManager.Device, VertexPositionColor.VertexElements))
            {
                EngineManager.Device.RenderState.PointSize = 1.0f;
                EngineManager.Device.VertexDeclaration = declaration;

                IRoeShader shader = ShaderManager.GetShader("basic");

                if (shader.ReadyToRender)
                {
                    BasicEffect effect = shader.BaseEffect as BasicEffect;
                    effect.DiffuseColor = Color.Red.ToVector3();
                    effect.View = CameraManager.ActiveCamera.View;
                    effect.Projection = CameraManager.ActiveCamera.Projection;
                    effect.World = World;

                    effect.Begin();

                    foreach (EffectPass pass in effect.CurrentTechnique.Passes)
                    {
                        pass.Begin();
                        EngineManager.Device.DrawUserIndexedPrimitives(
                                            PrimitiveType.LineList, points,
                                            0,
                                            8,
                                            index,
                                            0,
                                            12);
                        pass.End();
                    }
                    effect.End();
                }
            }
        }

        public override void DrawCulling(GameTime gameTime)
        {
            Occluded = false;
            if (ReadyToRender && !Culled)
            {
                _query.Begin();
                IRoeModel model = ModelManager.GetModel(_occlusionModelName);
                if (model != null && model.ReadyToRender)
                {
                    Matrix[] transforms = new Matrix[model.BaseModel.Bones.Count];
                    model.BaseModel.CopyAbsoluteBoneTransformsTo(transforms);

                    foreach (ModelMesh mesh in model.BaseModel.Meshes)
                    {
                        foreach (BasicEffect effect in mesh.Effects)
                        {
                            effect.EnableDefaultLighting();
                            effect.PreferPerPixelLighting = true;
                            effect.World = World;
                            effect.View = CameraManager.ActiveCamera.View;
                            effect.Projection = CameraManager.ActiveCamera.Projection;
                        }
                        mesh.Draw();
                    }
                }
                _query.End();

                while (!_query.IsComplete)
                {

                }

                if (_query.IsComplete && _query.PixelCount == 0)
                {
                    Occluded = true;
                }
            }
        }
    }
}

The Rest

Pretty much now you have the basics of physics in the engine.  Now all you need to do is add some logic to the game to add some boxes and objects and watch them collide.

Here is my gameclass Initalizer to get you started.

        /// 
        /// Load graphics content for the game.
        /// 
        public override void LoadContent()
        {
            SceneGraphManager.DrawDebugger = true;

            Box box = new Box();
            box.Position = new Vector3(0, -10, 0);
            box.Scale = new Vector3(10, 10, 10);
            box.Mass = 0.0f;
            SceneGraphManager.AddObject(box);

            for (int j = -2; j < 3; j++)
            {
                box = new Box(new Vector3(j * 2, 0, 0));
                box.Scale = new Vector3(2, 2, 2);
                box.Mass = 1.0f;
                SceneGraphManager.AddObject(box);

                for (int i = 1; i < 10; i++)
                {
                    box = new Box(new Vector3(j * 2f, i * 3.0f, 0));
                    box.Scale = new Vector3(0.5f, 0.5f, 0.5f);
                    box.Mass = 10.0f;
                    SceneGraphManager.AddObject(box);
                }
            }
            SceneGraphManager.LoadContent();

            // once the load has finished, we use ResetElapsedTime to tell the game's
            // timing mechanism that we have just finished a very long frame, and that
            // it should not try to catch up.
            EngineManager.Game.ResetElapsedTime();
        }

Conclusion

In this article I introduced physics into the engine.  This is a point of demarcation between a rendering engine and a true to life game engine, by including multiple core components working in unison.  From now on this series can truely be called a game engine.

Please leave a suggestion or comment.  I value what you have to say and read every comment, and sometimes make improvements based on what you have to say.

February 26, 2008 Posted by | C#, GameComponent, XBOX360, XNA | 6 Comments

   

Follow

Get every new post delivered to your Inbox.