XNA Framework GameEngine Development. (Part 12, Culling and Chase Camera)
Introduction
Welcome to Part 12 of the XNA Framework GameEngine Development series. This article is a slight departure from my original plan of showing a SkyDome game object. Rather, I decided to go back and clean up several of the classes I have already created as well as include some culling in the SceneGraph.
Interfaces
I removed the IRoeDrawable Interface from the project. I figure all RoeSceneObjects should be drawable.
IRoeSimplePhysics.cs – allows an object to maintain some simple physical updates.
using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; namespace RoeEngine2.Interfaces { /// <summary> /// Adds simple physics and reset to an RoeSceneObject /// </summary> public interface IRoeSimplePhysics : IRoeSceneObject { Vector3 Up { get; } Vector3 Right { get; } float RotationRate { get; set; } float Mass { get; set; } float ThrustForce { get; set; } float DragFactor { get; set; } Vector3 Velocity { get; set; } void Reset(); } }
IRoeAcceptInput.cs – each object that inherits this interface will be able to Handle game input.
using System; using System.Collections.Generic; using System.Text; using RoeEngine2.GameComponents; using Microsoft.Xna.Framework; namespace RoeEngine2.Interfaces { /// <summary> /// Allows a RoeSceneObject to handle input. /// </summary> public interface IRoeAcceptInput : IRoeSceneObject { void HandleInput(GameTime gameTime, Input input); } }
IRoeCullable.cs – allows an object to be culled by the SceneManager.
using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; namespace RoeEngine2.Interfaces { public interface IRoeCullable : IRoeSceneObject { bool BoundingBoxCreated { get; } BoundingBox BoundingBox { get; } BoundingBox GetBoundingBoxTransformed(); } }
IRoeUpdateable.cs – Added GameTime to the update method
using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; namespace RoeEngine2.Interfaces { /// <summary> /// Allows an RoeSceneObject to be Updated /// </summary> public interface IRoeUpdateable : IRoeSceneObject { void Update(GameTime gameTime); } }
Class changesNode.cs – Added HandleInputMethod also clar nodes after they are unloaded
using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; using RoeEngine2.GameComponents; namespace RoeEngine2.SceneObject.SceneGraph { public class Node { protected NodeList _nodes; public NodeList Nodes { get { return _nodes; } } public Node() { _nodes = new NodeList(); } public void AddNode(Node newNode) { _nodes.Add(newNode); } public virtual void HandleInput(GameTime gameTime, Input input) { _nodes.ForEach( delegate(Node node) { node.HandleInput(gameTime, input); }); } public virtual void Update(GameTime gameTime) { _nodes.ForEach( delegate(Node node) { node.Update(gameTime); }); } public virtual void UnloadContent() { _nodes.ForEach( delegate(Node node) { node.UnloadContent(); }); _nodes.Clear(); } public virtual void LoadContent() { _nodes.ForEach( delegate(Node node) { node.LoadContent(); }); } public virtual void Draw(GameTime gameTime) { _nodes.ForEach( delegate(Node node) { node.Draw(gameTime); }); } } }
SceneObjectNode.cs – Added HandleInput and Culling
using System; using System.Collections.Generic; using System.Text; using RoeEngine2.Interfaces; using Microsoft.Xna.Framework; using RoeEngine2.GameComponents; using RoeEngine2.Managers; using Microsoft.Xna.Framework.Graphics; namespace RoeEngine2.SceneObject.SceneGraph { public class SceneObjectNode : Node { private RoeSceneObject _sceneObject; public RoeSceneObject SceneObject { get { return _sceneObject; } set { _sceneObject = value; } } public SceneObjectNode(RoeSceneObject newObject) { _sceneObject = newObject; } public override void HandleInput(GameTime gameTime, Input input) { if (SceneObject is IRoeAcceptInput) ((IRoeAcceptInput)SceneObject).HandleInput(gameTime, input); } public override void Update(GameTime gameTime) { if (SceneObject is IRoeUpdateable) ((IRoeUpdateable)SceneObject).Update(gameTime); } public override void UnloadContent() { if (SceneObject is IRoeLoadable) ((IRoeLoadable)SceneObject).UnloadContent(); } public override void LoadContent() { if (SceneObject is IRoeLoadable) ((IRoeLoadable)SceneObject).LoadContent(); } public override void Draw(GameTime gameTime) { if (SceneObject is IRoeCullable) { if (CameraManager.ActiveCamera.Frustum.Contains(((IRoeCullable)SceneObject).GetBoundingBoxTransformed()) == ContainmentType.Disjoint) { SceneGraphManager.culled++; } else { SceneObject.Draw(gameTime); SceneObject.DrawNodesBoundingBox(gameTime); } } else SceneObject.Draw(gameTime); } } }
RoeSceneObject.cs – added bounding box drawing and World matrix
using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; using RoeEngine2.Interfaces; using RoeEngine2.Managers; using Microsoft.Xna.Framework.Graphics; namespace RoeEngine2.SceneObject { public class RoeSceneObject : IRoeSceneObject { public VertexPositionColor[] points = new VertexPositionColor[8]; public int[] index = new int[24]; private bool _readyToRender = false; /// <summary> /// Is this object ready to render? /// </summary> public bool ReadyToRender { get { return _readyToRender; } set { _readyToRender = value; } } private Material _material; public Material Material { get { return _material; } set { _material = value; } } private string _modelName; public string ModelName { get { return _modelName; } set { _modelName = value; } } private Vector3 _position = Vector3.Zero; /// <summary> /// The position of this object in 3d space. /// </summary> public Vector3 Position { get { return _position; } set { _position = value; } } private Vector3 _scale = Vector3.One; /// <summary> /// Scale of the object. /// </summary> public Vector3 Scale { get { return _scale; } set { _scale = value; } } private Quaternion _rotation = Quaternion.Identity; /// <summary> /// Yaw, pitch and roll of the object. /// </summary> public Quaternion Rotation { get { return _rotation; } set { _rotation = value; } } public virtual Matrix World { get { return Matrix.CreateScale(this.Scale) * Matrix.CreateFromQuaternion(this.Rotation) * Matrix.CreateTranslation(this.Position); } } public virtual void Draw(GameTime gameTime) { if (ReadyToRender) { IRoeModel model = ModelManager.GetModel(_modelName); 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(); } } } } public void DrawNodesBoundingBox(GameTime gameTime) { if(ReadyToRender) { 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<VertexPositionColor>( PrimitiveType.LineList, points, 0, 8, index, 0, 12); pass.End(); } effect.End(); } } } } } }
CameraManager.cs – Now supports FirstPersonCamera specifically.
using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; using System.Collections; using RoeEngine2.GameComponents; namespace RoeEngine2.Managers { public class CameraManager : GameComponent { private static Hashtable _cameras = new Hashtable(); public enum CameraNumber { _default = 1, _dolly = 2, _3 = 3, _4 = 4, _5 = 5, _6 = 6, _7 = 7, _8 = 8, _9 = 9, _10 = 10 } private static bool _initialized = false; /// <summary> /// Is the CameraManagers Initialized, used for test cases and setup of Effects. /// </summary> public static bool Initialized { get { return _initialized; } } private static Camera _activeCamera; /// <summary> /// The camera where all the action takes place. /// </summary> public static Camera ActiveCamera { get { return _activeCamera; } } /// <summary> /// Create the camera Managers. /// </summary> /// <param name="game"></param> public CameraManager(Game game) : base(game) { Enabled = true; } /// <summary> /// Create the cameras. /// </summary> public override void Initialize() { base.Initialize(); AddCamera(new FirstPersonCamera(), CameraNumber._default); SetActiveCamera(CameraNumber._default); AddCamera(new FirstPersonCamera(), CameraNumber._dolly); _initialized = true; } /// <summary> /// Update the active camera. /// </summary> /// <param name="gameTime"></param> public override void Update(GameTime gameTime) { base.Update(gameTime); _activeCamera.Update(gameTime); } /// <summary> /// Adds a new camera to the CameraManagers /// </summary> /// <param name="newCamera"></param> /// <param name="cameraLabel"></param> public static void AddCamera(Camera newCamera, CameraNumber cameraNumber) { if (!_cameras.Contains(cameraNumber)) { _cameras.Add(cameraNumber, newCamera); } } /// <summary> /// Change the projection matrix of all cameras. /// </summary> /// <param name="aspectRatio"></param> public static void SetAllCamerasProjectionMatrix(float aspectRatio) { foreach (Camera camera in _cameras.Values) { camera.Projection = Matrix.CreatePerspectiveFieldOfView( camera.FieldOfView, aspectRatio, camera.NearPlane, camera.FarPlane); } } /// <summary> /// Changes the active camera by label /// </summary> /// <param name="cameraLabel"></param> public static void SetActiveCamera(CameraNumber cameraNumber) { if (_cameras.ContainsKey(cameraNumber)) { _activeCamera = _cameras[cameraNumber] as Camera; } } } }
SceneGraphManager.cs – Added support for HandleInput
using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; using RoeEngine2.SceneObject.SceneGraph; using RoeEngine2.SceneObject; using RoeEngine2.GameComponents; namespace RoeEngine2.Managers { public class SceneGraphManager : GameComponent { public static int culled = 0; private static Node _root; /// <summary> /// The root of the scene graph /// </summary> public static Node Root { get { return _root; } } /// <summary> /// Create the scenegraph Managers. /// </summary> /// <param name="game"></param> public SceneGraphManager(Game game) : base(game) { Enabled = true; _root = new Node(); } /// <summary> /// Draw objects /// </summary> /// <param name="gameTime"></param> public static void Draw(GameTime gameTime) { culled = 0; _root.Draw(gameTime); } public static void HandleInput(GameTime gameTime, Input input) { _root.HandleInput(gameTime, input); } public override void Update(GameTime gameTime) { base.Update(gameTime); _root.Update(gameTime); } /// <summary> /// Load the content of all the objects in the scenegraph /// </summary> public static void LoadContent() { _root.LoadContent(); } /// <summary> /// Unload the content of all the objects in the scenegraph /// </summary> public static void UnloadContent() { _root.UnloadContent(); } /// <summary> /// Add an object to the scenegraph. /// </summary> /// <param name="newObject"></param> public static void AddObject(RoeSceneObject newObject) { SceneObjectNode node = new SceneObjectNode(newObject); _root.AddNode(node); } } }
New ClassesShip.cs – This is just for the test case in the example
using System; using System.Collections.Generic; using System.Text; using RoeEngine2.SceneObject; using RoeEngine2.Interfaces; using Microsoft.Xna.Framework; using RoeEngine2.Models; using RoeEngine2.Managers; using Microsoft.Xna.Framework.Graphics; namespace Kynskavion.GameObjects.Ship { public class Ship : RoeSceneObject, IRoeLoadable, IRoeUpdateable, IRoeSimplePhysics, IRoeCullable { public Vector3 Up { get { return Vector3.Up; } } public Vector3 Right { get { return Vector3.Right; } } private float _rotationRate = 1.0f; public float RotationRate { get { return _rotationRate; } set { _rotationRate = value; } } public float Mass { get { throw new Exception("The method or operation is not implemented."); } set { throw new Exception("The method or operation is not implemented."); } } public float ThrustForce { get { throw new Exception("The method or operation is not implemented."); } set { throw new Exception("The method or operation is not implemented."); } } public float DragFactor { get { throw new Exception("The method or operation is not implemented."); } set { throw new Exception("The method or operation is not implemented."); } } public Vector3 Velocity { get { throw new Exception("The method or operation is not implemented."); } set { throw new Exception("The method or operation is not implemented."); } } private bool _boundingBoxCreated; public bool BoundingBoxCreated { get { return _boundingBoxCreated; } } public BoundingBox _boundingBox; public BoundingBox BoundingBox { get { return _boundingBox; } } public Ship() { } public Ship(Vector3 newPosition) { Position = newPosition; } public BoundingBox GetBoundingBoxTransformed() { Vector3 min, max; min = _boundingBox.Min; max = _boundingBox.Max; min = Vector3.Transform(_boundingBox.Min, Matrix.CreateTranslation(Position)); max = Vector3.Transform(_boundingBox.Max, Matrix.CreateTranslation(Position)); return new BoundingBox(min, max); } public void Update(GameTime gameTime) { float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds; Rotation = Quaternion.Multiply(Rotation, Quaternion.CreateFromAxisAngle(Vector3.UnitZ, _rotationRate * elapsed)); IRoeModel model = ModelManager.GetModel(ModelName); if (model != null && model.ReadyToRender && !ReadyToRender) { Matrix[] transforms = new Matrix[model.BaseModel.Bones.Count]; _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; max = BoundingBox.Max; _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); ReadyToRender = true; } } public void LoadContent() { RoeModel model = new RoeModel("Content/Models/Ship"); ModelManager.AddModel(model, "ship"); this.ModelName = "ship"; 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 void Reset() { RotationRate = 0.0f; } } }
ShipMain.cs – Same Thing as Ship, but this also accepts input
using System; using System.Collections.Generic; using System.Text; using RoeEngine2.SceneObject; using RoeEngine2.Interfaces; using Microsoft.Xna.Framework; using RoeEngine2.Models; using RoeEngine2.Managers; using Microsoft.Xna.Framework.Graphics; using RoeEngine2.GameComponents; using Microsoft.Xna.Framework.Input; namespace Kynskavion.GameObjects.ShipMain { public class ShipMain : RoeSceneObject, IRoeLoadable, IRoeUpdateable, IRoeSimplePhysics, IRoeAcceptInput, IRoeCullable { public Vector3 Direction; private Vector3 _up = Vector3.Up; public Vector3 Up { get { return _up; } } private Vector3 _right; public Vector3 Right { get { return _right; } } private float _rotationRate = 1.0f; public float RotationRate { get { return _rotationRate; } set { _rotationRate = value; } } private float _mass = 1.0f; public float Mass { get { return _mass; } set { _mass = value; } } private float _thrustForce = 24000.0f; public float ThrustForce { get { return _thrustForce; } set { _thrustForce = value; } } private float _dragFactor = 0.97f; public float DragFactor { get { return _dragFactor; } set { _dragFactor = value; } } private Vector3 _velocity; public Vector3 Velocity { get { return _velocity; } set { _velocity = value; } } private Matrix _world; public override Matrix World { get { return _world; } } private bool _boundingBoxCreated; public bool BoundingBoxCreated { get { return _boundingBoxCreated; } } public BoundingBox _boundingBox; public BoundingBox BoundingBox { get { return _boundingBox; } } public ShipMain() { Reset(); } public ShipMain(Vector3 newPosition) { Position = newPosition; Reset(); } public BoundingBox GetBoundingBoxTransformed() { Vector3 min, max; min = _boundingBox.Min; max = _boundingBox.Max; min = Vector3.Transform(_boundingBox.Min, Matrix.CreateTranslation(Position)); max = Vector3.Transform(_boundingBox.Max, Matrix.CreateTranslation(Position)); 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 = Vector3.Transform(BoundingBox.Min, Matrix.CreateTranslation(Position)); max = Vector3.Transform(BoundingBox.Max, Matrix.CreateTranslation(Position)); _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); ReadyToRender = true; } } public void LoadContent() { RoeModel model = new RoeModel("Content/Models/Ship"); ModelManager.AddModel(model, "ship"); this.ModelName = "ship"; 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 void Reset() { Position = new Vector3(0, 0, 0); Direction = Vector3.Forward; _up = Vector3.Up; _right = Vector3.Right; Velocity = Vector3.Zero; } public void HandleInput(GameTime gameTime, Input input) { float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds; if (ReadyToRender) { Vector2 rotationAmount = -input.CurrentGamePadState.ThumbSticks.Left; if (input.CurrentKeyboardState.IsKeyDown(Keys.W)) rotationAmount.Y = -1.0f; if (input.CurrentKeyboardState.IsKeyDown(Keys.S)) rotationAmount.Y = 1.0f; if (input.CurrentKeyboardState.IsKeyDown(Keys.D)) rotationAmount.X = -1.0f; if (input.CurrentKeyboardState.IsKeyDown(Keys.A)) rotationAmount.X = 1.0f; // Determine thrust amount from input float thrustAmount = input.CurrentGamePadState.Triggers.Right; if (input.CurrentKeyboardState.IsKeyDown(Keys.Space)) thrustAmount = 1.0f; // Scale rotation amount to radians per second rotationAmount = rotationAmount * RotationRate * elapsed; // Correct the X axis steering when the ship is upside down if (Up.Y < 0) rotationAmount.X = -rotationAmount.X; // Create rotation matrix from rotation amount Matrix rotationMatrix = Matrix.CreateFromAxisAngle(Right, rotationAmount.Y) * Matrix.CreateRotationY(rotationAmount.X); // Rotate orientation vectors Direction = Vector3.TransformNormal(Direction, rotationMatrix); _up = Vector3.TransformNormal(Up, rotationMatrix); // Re-normalize orientation vectors // Without this, the matrix transformations may introduce small rounding // errors which add up over time and could destabilize the ship. Direction.Normalize(); Up.Normalize(); // Re-calculate Right _right = Vector3.Cross(Direction, Up); // The same instability may cause the 3 orientation vectors may // also diverge. Either the Up or Direction vector needs to be // re-computed with a cross product to ensure orthagonality _up = Vector3.Cross(Right, Direction); // Calculate force from thrust amount Vector3 force = Direction * thrustAmount * ThrustForce; // Apply acceleration Vector3 acceleration = force / Mass; Velocity += acceleration * elapsed; // Apply psuedo drag Velocity *= DragFactor; // Apply velocity Position += Velocity * elapsed; // Reconstruct the ship's world matrix _world = Matrix.Identity; _world.Forward = Direction; _world.Up = Up; _world.Right = Right; _world.Translation = Position; } } } }[/sourcecode] FirstPersonCamera.cs - The original camera, just now encapsulated in its own class [sourcecode language='csharp']using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; namespace RoeEngine2.GameComponents { public class FirstPersonCamera : Camera { private Vector3 _position = Vector3.Zero; /// <summary> /// Postition of the camera. /// </summary> public Vector3 Position { get { return _position; } } private Vector3 _cameraReference = new Vector3(0, 0, 10); /// <summary> /// The spot in 3d space where the camera is looking. /// </summary> public Vector3 CameraReference { get { return _cameraReference; } } private float _yaw = 0.0f; private float _pitch = 0.0f; /// <summary> /// Set the position in 3d space. /// </summary> /// <param name="newPosition"></param> public override void SetPosition(Vector3 newPosition) { _position = newPosition; } /// <summary> /// Set the point in 3d space where the camera is looking. /// </summary> /// <param name="newReference"></param> public void SetCameraReference(Vector3 newReference) { _cameraReference = newReference; } /// <summary> /// Move the camera in 3d space. /// </summary> /// <param name="move"></param> public override void Translate(Vector3 move) { Matrix forwardMovement = Matrix.CreateRotationY(_yaw); Vector3 v = new Vector3(0, 0, 0); v = Vector3.Transform(move, forwardMovement); _position.Z += v.Z; _position.X += v.X; _position.Y += v.Y; } /// <summary> /// Rotate around the Y, Default Up axis. Usually called Yaw. /// </summary> /// <param name="angle">Angle in degrees to rotate the camera.</param> public override void RotateY(float angle) { angle = MathHelper.ToRadians(angle); if (_yaw >= MathHelper.Pi * 2) _yaw = MathHelper.ToRadians(0.0f); else if (_yaw <= -MathHelper.Pi * 2) _yaw = MathHelper.ToRadians(0.0f); _yaw += angle; } /// <summary> /// Rotate the camera around the X axis. Usually called pitch. /// </summary> /// <param name="angle">Angle in degrees to rotate the camera.</param> public override void RotateX(float angle) { angle = MathHelper.ToRadians(angle); _pitch += angle; if (_pitch >= MathHelper.ToRadians(75)) _pitch = MathHelper.ToRadians(75); else if (_pitch <= MathHelper.ToRadians(-75)) _pitch = MathHelper.ToRadians(-75); } public override void Update(GameTime gameTime) { Vector3 cameraPosition = _position; Matrix rotationMatrix = Matrix.CreateRotationY(_yaw); Matrix pitchMatrix = Matrix.Multiply(Matrix.CreateRotationX(_pitch), rotationMatrix); Vector3 transformedReference = Vector3.Transform(_cameraReference, pitchMatrix); Vector3 cameraLookat = cameraPosition + transformedReference; View = Matrix.CreateLookAt(cameraPosition, cameraLookat, Vector3.Up); Frustum = new BoundingFrustum(Matrix.Multiply(View, Projection)); ReflectedFrustum = new BoundingFrustum(Matrix.Multiply(ReflectedView, Projection)); } } }[/sourcecode] ChaseCamera.cs - A camera that chases an object in an over the should view. [sourcecode language='csharp']using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; namespace RoeEngine2.GameComponents { public class ChaseCamera : Camera { private bool _springEnabled = true; public bool SpringEnabled { get { return _springEnabled; } set { _springEnabled = value; } } private Vector3 _position; public Vector3 Position { get { return _position; } } private Vector3 _velocity; public Vector3 Velocity { get { return _velocity; } } private float _stiffness = 1800.0f; public float Stiffness { get { return _stiffness; } set { _stiffness = value; } } private float _damping = 600.0f; public float Damping { get { return _damping; } set { _damping = value; } } private float _mass = 50.0f; public float Mass { get { return _mass; } set { _mass = value; } } private Vector3 _chasePosition; public Vector3 ChasePosition { get { return _chasePosition; } set { _chasePosition = value; } } private Vector3 _chaseDirection; public Vector3 ChaseDirection { get { return _chaseDirection; } set { _chaseDirection = value; } } private Vector3 _up = Vector3.Up; public Vector3 Up { get { return _up; } set { _up = value; } } private Vector3 _desiredPositionOffset = new Vector3(0, 2.0f, 2.0f); public Vector3 DesiredPositionOffset { get { return _desiredPositionOffset; } set { _desiredPositionOffset = value; } } private Vector3 _desiredPosition; public Vector3 DesiredPosition { get { return _desiredPosition; } set { _desiredPosition = value; } } private Vector3 _lookAtOffset = new Vector3(0, 2.8f, 0); public Vector3 LookAtOffset { get { return _lookAtOffset; } set { _lookAtOffset = value; } } private Vector3 _lookAt; public Vector3 LookAt { get { return _lookAt; } } public override void Update(GameTime gameTime) { UpdatePosition(); float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds; Vector3 stretch = _position - _desiredPosition; Vector3 force = -_stiffness * stretch - _damping * _velocity; Vector3 acceleration = force / _mass; _velocity += acceleration * elapsed; _position += _velocity * elapsed; UpdateMatrices(); } private void UpdatePosition() { // Construct a matrix to transform from object space to worldspace Matrix transform = Matrix.Identity; transform.Forward = ChaseDirection; transform.Up = Up; transform.Right = Vector3.Cross(Up, ChaseDirection); // Calculate desired camera properties in world space _desiredPosition = ChasePosition + Vector3.TransformNormal(DesiredPositionOffset, transform); _lookAt = ChasePosition + Vector3.TransformNormal(LookAtOffset, transform); } private void UpdateMatrices() { View = Matrix.CreateLookAt(Position, LookAt, Vector3.Up); Frustum = new BoundingFrustum(Matrix.Multiply(View, Projection)); ReflectedFrustum = new BoundingFrustum(Matrix.Multiply(ReflectedView, Projection)); } public override void Reset() { UpdatePosition(); _velocity = Vector3.Zero; _position = _desiredPosition; UpdateMatrices(); } } }[/sourcecode] <strong>Updates to GameScreens</strong>GameplayScreen.cs using System; using System.Collections.Generic; using System.Text; using RoeEngine2.GameComponents; using RoeEngine2.Managers; using Microsoft.Xna.Framework; using System.Threading; using RoeEngine2.Models; using RoeEngine2.SceneObject; using Microsoft.Xna.Framework.Input; using RoeEngine2.Shaders; using RoeEngine2.Texures; using Kynskavion.GameObjects.Ship; using Microsoft.Xna.Framework.Graphics; using Kynskavion.GameObjects.ShipMain; namespace Kynskavion.GameScreens { class GameplayScreen : GameScreen { private static double delta = 0.0; private static ShipMain shipMain = new ShipMain(); public GameplayScreen() { TransitionOnTime = TimeSpan.FromSeconds(1.5); TransitionOffTime = TimeSpan.FromSeconds(0.5); ChaseCamera camera = new ChaseCamera(); camera.DesiredPositionOffset = new Vector3(0.0f, 2000.0f, 3500.0f); camera.LookAtOffset = new Vector3(0.0f, 150.0f, 0.0f); camera.NearPlane = 10.0f; camera.FarPlane = 1000000.0f; CameraManager.AddCamera(camera, CameraManager.CameraNumber._3); CameraManager.SetActiveCamera(CameraManager.CameraNumber._3); CameraManager.SetAllCamerasProjectionMatrix((float)EngineManager.Device.Viewport.Width / EngineManager.Device.Viewport.Height); } /// <summary> /// Load graphics content for the game. /// </summary> public override void LoadContent() { Random rnd = new Random(); for (int i = -10000; i <= 10000; i += 5000) { for (int j = -10000; j <= 10000; j += 5000) { for (int k = -10000; k <= 10000; k += 5000) { Ship ship = new Ship(new Vector3(i, j, k)); ship.RotationRate = rnd.Next(-10, 10); SceneGraphManager.AddObject(ship); } } } SceneGraphManager.AddObject(shipMain); 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(); } /// <summary> /// Unload graphics content used by the game. /// </summary> public override void UnloadContent() { SceneGraphManager.UnloadContent(); } public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen) { base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen); delta = gameTime.ElapsedGameTime.TotalSeconds; ChaseCamera camera = (ChaseCamera)CameraManager.ActiveCamera; camera.ChasePosition = shipMain.Position; camera.ChaseDirection = shipMain.Direction; camera.Up = shipMain.Up; } public override void HandleInput(GameTime gameTime, Input input) { if (input.PauseGame) { ScreenManager.AddScreen(new PauseMenuScreen()); } else { SceneGraphManager.HandleInput(gameTime, input); if (input.CurrentKeyboardState.IsKeyDown(Keys.Q)) { CameraManager.ActiveCamera.Translate(new Vector3(0, 3000f * (float)delta, 0.0f)); } if (input.CurrentKeyboardState.IsKeyDown(Keys.Z)) { CameraManager.ActiveCamera.Translate(new Vector3(0, -3000f * (float)delta, 0.0f)); } if (input.CurrentKeyboardState.IsKeyDown(Keys.W)) { CameraManager.ActiveCamera.Translate(new Vector3(0, 0, 1000f * (float)delta)); } if (input.CurrentKeyboardState.IsKeyDown(Keys.S)) { CameraManager.ActiveCamera.Translate(new Vector3(0, 0, -1000f * (float)delta)); } if (input.CurrentKeyboardState.IsKeyDown(Keys.D)) { CameraManager.ActiveCamera.Translate(new Vector3(-1000f * (float)delta, 0, 0)); } if (input.CurrentKeyboardState.IsKeyDown(Keys.A)) { CameraManager.ActiveCamera.Translate(new Vector3(1000f * (float)delta, 0, 0)); } if (input.CurrentMouseState.RightButton == ButtonState.Pressed) { CameraManager.ActiveCamera.RotateX(input.MouseMoved.Y); CameraManager.ActiveCamera.RotateY(input.MouseMoved.X); } } } public override void Draw(GameTime gameTime) { base.Draw(gameTime); SceneGraphManager.Draw(gameTime); } public override void PostUIDraw(GameTime gameTime) { base.PostUIDraw(gameTime); string message = "FPS: " + EngineManager.FpsCounter.FPS.ToString() + " Culled: " + SceneGraphManager.culled.ToString(); // Center the text in the viewport. Vector2 textPosition = new Vector2(10, 20); Color color = new Color(255, 255, 255, TransitionAlpha); ScreenManager.SpriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Deferred, SaveStateMode.SaveState); ScreenManager.SpriteBatch.DrawString(ScreenManager.Font, message, textPosition, color); ScreenManager.SpriteBatch.End(); } } }
ConclusionIn this article I introduced many new concepts. First, I introduced the chase camera. This camera option is an over the shoulder view camera, it will follow an object around at a nice viewing distance. Next, I demonstrated culling in the SceneGraphManager. Culling is accomplished view bounds checking with the camera frustum and the bounding box of an object. Finally, I demonstrated all of these concepts in a simple fly through game.
Looking forward to your comments and suggestions.