Running on Empty

The few things I know, I like to share.

XNA Framework GameEngine Development. (Part 1)

Introduction

Please feel free to visit the rest of the articles in this series.

This series of articles is all about building an Engine using the Microsoft XNA Framework.  I am very excited about this series and hopefully you will be as well.  These articles are not about development practices in C#, coding standards, or even how to use the XNA framework.  Rather, it is my goal to build a small practical engine using the XNA Framework and to have fun.

I am one of the lucky people whose hobbies and work are very closely related.  However, that does not make me an expert.  I simply enjoy reading and learning from other people in the XNA development community and hopefully will have something to give back now.

RoeEngine Diagram

Engine Overview

The XNA Framework is actually very easy to use, so as an Engine developer, we definitely do not want to add any unnecessary complexity.  It is my goal to simplify some already outstanding code in the Framework and to provide some order to the difficult task that is game development.

I would like to briefly point out that this series of articles is designed to be a thank you to the wonderful XNA community.  Without the support and examples given in the open source community I would not be able to put together this series.

Articles in this series will focus on these areas.

  • Creating a BaseEngine, Helper classes, GameSettings classes and the EngineManager class.
  • Camera classes (1st person and 3rd person) and the CameraManager class.
  • GameScreen game component integration (found in the XNA Team website) as well as the GameScreenManager.
  • Material classes, which are comprised of Shaders, Textures, proxies and the Managers associated with Materials.
  • Scene management including SceneGraphs, BSP, OctTrees, QuadTrees and culling, as well as some Managers to help the process.
  • Examples of using the XNA content pipeline to import content into the engine.
  • Using Hoffman, Preetham shaders for real time light scattering in the atmosphere.
  • Reflective and Refractive water.
  • GeoMipMapped QuadTree Terrain with TextureSplatting.
  • Finally, using the engine in a simple game.

This is my goal for now, something in that list caught your eye otherwise you would not have gotten this far into this post.  So you are definitely part of the audience this series of articles is for.  Your feedback is fuel for development, so please feel free to leave comments or suggestions, or even remarks about bugs or improvements. 

I will be using Test Driven Development TDD and will do my best to use YagNi in this series, so please feel free to point out anything that fits into You Aren’t Going to Need It.  I am also a very avid reader of community forums and blogs, so I will do my very best to give credit where it is due.  I am not attempting to sell this engine, so everything here is fair game for you to use.

Creating the RoeEngine2 : Game class  Updated to use Managers namespace.

This is a simplification from the work Benjamin Nitschke has done in the XNA Racer sample.  I found his book and examples to be very elegant, so I am happy to reproduce some of his efforts here.

Start by creating a new XNA for Windows class library project in an empty solution.  Add a new XNA for Windows game project to the solution.  You will now have two projects in the solution.  The whole point of the engine is to abstract the functionality of the engine and reuse its components in the game.  So the base classes from the templates simply will not work.  Delete the Game.cs file from the game project and delete the base class from the class library project.

You will now have a non functioning solution, so we have a ton of work ahead of us to do.  Let’s get started with the code.

 Note:  Thanks to KaBaL, I have updated this class to use the Managers namespace instead of Manager.

using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
using RoeEngine2.Settings;
using RoeEngine2.Managers;

namespace RoeEngine2
{
    public partial class RoeEngine2 : Game
    {
        /// <summary>
        /// Width and height of visible render area.
        /// </summary>
        protected static int width, height;

        /// <summary>
        /// Width of visible render area.
        /// </summary>
        public static int Width
        {
            get { return width; }
        }

        /// <summary>
        /// Height of visible render area.
        /// </summary>
        public static int Height
        {
            get { return height; }
        }

        /// <summary>
        /// Aspect ratio of render area.
        /// </summary>
        private static float aspectRatio = 1.0f;
        /// <summary>
        /// Aspect ratio of render area.
        /// </summary>
        public static float AspectRatio
        {
            get { return aspectRatio; }
        }

        private static Color _backgroundColor = Color.LightBlue;
        /// <summary>
        /// Color used to redraw the background scene.
        /// </summary>
        public static Color BackgroundColor
        {
            get { return _backgroundColor; }
            set { _backgroundColor = value; }
        }

        public static PlatformID CurrentPlatform = Environment.OSVersion.Platform;

        private static string _windowTitle = "";
        /// <summary>
        /// Window title for test cases.
        /// </summary>
        public static string WindowTitle
        {
            get { return _windowTitle; }
        }

        private bool _isAppActive = false;
        /// <summary>
        /// Is the application active.
        /// </summary>
        public bool IsAppActive
        {
            get { return _isAppActive; }
            set { _isAppActive = value; }
        }

        protected static GraphicsDeviceManager _graphicsDeviceManager = null;
        /// <summary>
        /// The graphics device, used to render.
        /// </summary>
        public static GraphicsDevice Device
        {
            get { return _graphicsDeviceManager.GraphicsDevice; }
        }

        protected static ContentManager _contentManager = null;
        /// <summary>
        /// Content Manager
        /// </summary>
        public static ContentManager ContentManager
        {
            get { return _contentManager; }
        }

        private static bool _checkedGraphicsOptions = false;
        private static bool _applyDeviceChanges = false;

        /// <summary>
        /// Create RoeEngine
        /// </summary>
        /// <param name="windowsTitle">Window Title</param>
        protected RoeEngine2(string windowsTitle)
        {
            _graphicsDeviceManager = new GraphicsDeviceManager(this);

            // Set minimum pixel and vertex shader requirements.
            _graphicsDeviceManager.MinimumPixelShaderProfile = ShaderProfile.PS_2_0;
            _graphicsDeviceManager.MinimumVertexShaderProfile = ShaderProfile.VS_2_0;

            _graphicsDeviceManager.PreparingDeviceSettings += new EventHandler<PreparingDeviceSettingsEventArgs>(GraphicsDeviceManager_PreparingDeviceSettings);

            GameSettings.Initialize();

            ApplyResolutionChange();

#if DEBUG
            // Disable vertical retrace to get highest framerates possible for
            // testing performance.
            _graphicsDeviceManager.SynchronizeWithVerticalRetrace = false;
#endif
            // Demand to update as fast as possible, do not use fixed time steps.
            // The whole game is designed this way, if you remove this line
            // the game will not behave normal any longer!
            this.IsFixedTimeStep = false;

            // Init content manager
            _contentManager = new ContentManager(this.Services);

            //TODO include other inits here!
        }

        /// <summary>
        /// Create RoeEngine
        /// </summary>
        protected RoeEngine2()
            : this("Game")
        {

        }

        /// <summary>
        /// Prepare the graphics device.
        /// </summary>
        /// <param name="sender">sender</param>
        /// <param name="e">event args</param>
        void GraphicsDeviceManager_PreparingDeviceSettings(object sender, PreparingDeviceSettingsEventArgs e)
        {
            if (Environment.OSVersion.Platform != PlatformID.Win32NT)
            {
                PresentationParameters presentParams =
                    e.GraphicsDeviceInformation.PresentationParameters;
                if (_graphicsDeviceManager.PreferredBackBufferHeight == 720)
                {
                    presentParams.MultiSampleType = MultiSampleType.FourSamples;
#if !DEBUG
                    presentParams.PresentationInterval = PresentInterval.One;
#endif
                }
                else
                {
                    presentParams.MultiSampleType = MultiSampleType.TwoSamples;
#if !DEBUG
                    presentParams.PresentationInterval = PresentInterval.Two;
#endif
                }
            }
        }

        public static void CheckOptionsAndPSVersion()
        {
            if (Device == null)
            {
                throw new InvalidOperationException("Graphics Device is not created yet!");
            }

            _checkedGraphicsOptions = true;
        }

        public static void ApplyResolutionChange()
        {
            int resolutionWidth = GameSettings.Default.ResolutionWidth;
            int resolutionHeight = GameSettings.Default.ResolutionHeight;

            if (resolutionWidth <= 0 || resolutionWidth <= 0)
            {
                resolutionWidth = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width;
                resolutionHeight = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height;
            }
#if XBOX360
            // Xbox 360 graphics settings are fixed
            _graphicsDeviceManager.IsFullScreen = true;
            _graphicsDeviceManager.PreferredBackBufferWidth =
                GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width;
            _graphicsDeviceManager.PreferredBackBufferHeight =
                GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height;
#else
            _graphicsDeviceManager.PreferredBackBufferWidth = resolutionWidth;
            _graphicsDeviceManager.PreferredBackBufferHeight = resolutionHeight;
            _graphicsDeviceManager.IsFullScreen = GameSettings.Default.Fullscreen;

            _applyDeviceChanges = true;
#endif
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            base.Initialize();

            _graphicsDeviceManager.DeviceReset += new EventHandler(GraphicsDeviceManager_DeviceReset);
            GraphicsDeviceManager_DeviceReset(null, EventArgs.Empty);
        }

        void GraphicsDeviceManager_DeviceReset(object sender, EventArgs e)
        {

        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // TODO: use this.Content to load your game content here
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            // TODO: Add your update logic here

            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            base.Draw(gameTime);

            // Apply device changes
            if (_applyDeviceChanges)
            {
                _graphicsDeviceManager.ApplyChanges();
                _applyDeviceChanges = false;
            }
        }

        protected override void OnActivated(object sender, EventArgs args)
        {
            base.OnActivated(sender, args);
            IsAppActive = true;
        }

        protected override void OnDeactivated(object sender, EventArgs args)
        {
            base.OnDeactivated(sender, args);
            IsAppActive = false;
        }
    }
}

You may notice some of the #if pre-compiler flags in the code.  Those are used to separate XBOX360 code from the Windows code, as well as to mark off some TDD debug areas.

FileHelper class

The aptly named FileHelper class has the function of helping us create files and read files for the project.  I included it this early to give a nice foundation for storing Game and Device settings.

Begin by creating a new folder, I named mine Helpers, and add a FileHelper.cs file to the folder.

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using Microsoft.Xna.Framework.Storage;
using Microsoft.Xna.Framework;

namespace RoeEngine2.Helpers
{
    public static class FileHelper
    {
        #region CreateGameContentFile
        /// <summary>
        /// Create game content file, will create file if it does not exist.
        /// Else the existing file should be loaded.
        /// </summary>
        /// <param name="relativeFilename">Relative filename.</param>
        /// <param name="createNew">Create new file.</param>
        /// <returns>FileStream</returns>
        public static FileStream CreateGameContentFile(string relativeFilename, bool createNew)
        {
            string fullPath = Path.Combine(
                StorageContainer.TitleLocation, relativeFilename);
            return File.Open(fullPath,
                createNew ? FileMode.Create : FileMode.OpenOrCreate,
                FileAccess.Write, FileShare.ReadWrite);
        }
        #endregion

        #region LoadGameContentFile
        /// <summary>
        /// Load game content file, returns null if file was not found.
        /// </summary>
        /// <param name="relativeFilename">Relative filename.</param>
        /// <returns>FileStream</returns>
        public static FileStream LoadGameContentFile(string relativeFilename)
        {
            string fullPath = Path.Combine(
                StorageContainer.TitleLocation, relativeFilename);
            if (File.Exists(fullPath) == false)
                return null;
            else
                return File.Open(fullPath,
                    FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
        }
        #endregion

        #region SaveGameContentFile
        /// <summary>
        /// Save game content file, returns the open file.
        /// </summary>
        /// <param name="relativeFilename">Relative filename.</param>
        /// <returns>FileStream</returns>
        public static FileStream SaveGameContentFile(string relativeFilename)
        {
            string fullPath = Path.Combine(
                StorageContainer.TitleLocation, relativeFilename);
            return File.Open(fullPath,
                FileMode.Create, FileAccess.Write);
        }
        #endregion

        #region OpenOrCreateFileForCurrentPlayer

        /// <summary>
        /// Open or create file for current player.  Basically just creates a
        /// FileStream using the specified FileMode flag, but on the Xbox360
        /// we have to ask the user first where he wants to.
        /// Basically used for the GameSettings and the Log class.
        /// </summary>
        /// <param name="filename">Filename</param>
        /// <param name="mode">Mode</param>
        /// <param name="access">Access</param>
        /// <returns>FileStream</returns>
        public static FileStream OpenFileForCurrentPlayer(string filename, FileMode mode, FileAccess access)
        {
            // Open a storage container.
            StorageContainer container = null;

            // Add the container path to our filename.
            string fullFileName = Path.Combine(container.Path, filename);

            // Opens or creates the requested file.
            return new FileStream(
                fullFileName, mode, access, FileShare.ReadWrite);
        }
        #endregion

        #region Get text lines
        /// <summary>
        /// Returns the number of text lines we have in the file.
        /// </summary>
        /// <param name="filename">Filename</param>
        /// <returns>Array of strings.</returns>
        static public string[] GetLines(string filename)
        {
            try
            {
                StreamReader reader = new StreamReader(
                    new FileStream(filename, FileMode.Open, FileAccess.Read),
                    System.Text.Encoding.UTF8);
                // Generic version
                List<string> lines = new List<string>();
                do
                {
                    lines.Add(reader.ReadLine());
                } while (reader.Peek() > -1);
                reader.Close();
                return lines.ToArray();
            }
            catch (FileNotFoundException)
            {
                // Failed to find a file.
                return null;
            }
            catch (DirectoryNotFoundException)
            {
                // Failed to find a directory.
                return null;
            }
            catch (IOException)
            {
                // Something else must have happened.
                return null;
            }
        }
        #endregion

        #region Write Helpers
        /// <summary>
        /// Write Vector3 to stream.
        /// </summary>
        /// <param name="writer">Writer</param>
        /// <param name="vector">Vector3</param>
        public static void WriteVector3(BinaryWriter writer, Vector3 vector)
        {
            if (writer == null)
                throw new ArgumentNullException("writer");
            writer.Write(vector.X);
            writer.Write(vector.Y);
            writer.Write(vector.Z);
        }

        /// <summary>
        /// Write Vector4 to stream.
        /// </summary>
        /// <param name="writer">Writer</param>
        /// <param name="vec">Vector4</param>
        public static void WriteVector4(BinaryWriter writer, Vector4 vector)
        {
            if (writer == null)
                throw new ArgumentNullException("writer");

            writer.Write(vector.X);
            writer.Write(vector.Y);
            writer.Write(vector.Z);
            writer.Write(vector.W);
        }

        /// <summary>
        /// Write Matrix to stream.
        /// </summary>
        /// <param name="writer">Writer</param>
        /// <param name="matrix">Matrix</param>
        public static void WriteMatrix(BinaryWriter writer, Matrix matrix)
        {
            if (writer == null)
                throw new ArgumentNullException("writer");

            writer.Write(matrix.M11);
            writer.Write(matrix.M12);
            writer.Write(matrix.M13);
            writer.Write(matrix.M14);
            writer.Write(matrix.M21);
            writer.Write(matrix.M22);
            writer.Write(matrix.M23);
            writer.Write(matrix.M24);
            writer.Write(matrix.M31);
            writer.Write(matrix.M32);
            writer.Write(matrix.M33);
            writer.Write(matrix.M34);
            writer.Write(matrix.M41);
            writer.Write(matrix.M42);
            writer.Write(matrix.M43);
            writer.Write(matrix.M44);
        }
        #endregion

        #region Read Helpers
        /// <summary>
        /// Read Vector3 from stream.
        /// </summary>
        /// <param name="reader">Reader</param>
        /// <returns>Vector3</returns>
        public static Vector3 ReadVector3(BinaryReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException("reader");

            return new Vector3(
                reader.ReadSingle(),
                reader.ReadSingle(),
                reader.ReadSingle());
        }

        /// <summary>
        /// Read Vector4 from stream.
        /// </summary>
        /// <param name="reader">Reader</param>
        /// <returns>Vector4</returns>
        public static Vector4 ReadVector4(BinaryReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException("reader");

            return new Vector4(
                reader.ReadSingle(),
                reader.ReadSingle(),
                reader.ReadSingle(),
                reader.ReadSingle());
        }

        /// <summary>
        /// Read Matrix from stream.
        /// </summary>
        /// <param name="reader">Reader</param>
        /// <returns>Matrix</returns>
        public static Matrix ReadMatrix(BinaryReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException("reader");

            return new Matrix(
                reader.ReadSingle(),
                reader.ReadSingle(),
                reader.ReadSingle(),
                reader.ReadSingle(),
                reader.ReadSingle(),
                reader.ReadSingle(),
                reader.ReadSingle(),
                reader.ReadSingle(),
                reader.ReadSingle(),
                reader.ReadSingle(),
                reader.ReadSingle(),
                reader.ReadSingle(),
                reader.ReadSingle(),
                reader.ReadSingle(),
                reader.ReadSingle(),
                reader.ReadSingle());
        }
        #endregion
    }
}

 

GameSettings class

The GameSettings class has the singular purpose of storing and retrieving user settings for the game.

using System;
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;
using RoeEngine2.Helpers;
namespace RoeEngine2.Settings
{
    /// <summary>
    /// Game settings, stored in a custom xml file. The reason for this is
    /// we want to be able to store our game data on the Xbox360 too.
    /// On the PC we could just use a Settings/config file and have all the
    /// code autogenerated for us, but this way it works both on PC and Xbox.
    /// Note: The default instance for the game settings is in this class,
    /// this way we get the same behaviour as for normal Settings files!
    /// </summary>
    [Serializable]
    public class GameSettings
    {
        #region Properties
        private string _playerName = "RoE";
        /// <summary>
        /// Player name.
        /// </summary>
        public string PlayerName
        {
            get { return _playerName; }
            set
            {
                if (_playerName != value)
                    _needSave = true;
                _playerName = value;
            }
        }

        public const int MinimumResolutionWidth = 640;

        private int _resolutionWidth = 0;
        /// <summary>
        /// Resolution width.
        /// </summary>
        public int ResolutionWidth
        {
            get { return _resolutionWidth; }
            set
            {
                if (_resolutionWidth != value)
                    _needSave = true;
                _resolutionWidth = value;
            }
        }

        public const int MinimumResolutionHeight = 480;

        private int _resolutionHeight = 0;
        /// <summary>
        /// Resolution height.
        /// </summary>
        public int ResolutionHeight
        {
            get { return _resolutionHeight; }
            set
            {
                if (_resolutionHeight != value)
                    _needSave = true;
                _resolutionHeight = value;
            }
        }

        private bool _fullscreen = false;
        /// <summary>
        /// Fullscreen.
        /// </summary>
        public bool Fullscreen
        {
            get { return _fullscreen; }
            set
            {
                if (_fullscreen != value)
                    _needSave = true;
                _fullscreen = value;
            }
        }

        private bool _postScreenEffects = true;
        /// <summary>
        /// All the shiney stuff after rendering is done.
        /// </summary>
        public bool PostScreenEffects
        {
            get { return _postScreenEffects; }
            set
            {
                if (_postScreenEffects != value)
                    _needSave = true;
                _postScreenEffects = value;
            }
        }

        private bool _reflections = true;
        /// <summary>
        /// Show reflections off reflective surfaces.
        /// </summary>
        public bool Reflections
        {
            get { return _reflections; }
            set
            {
                if (_reflections != value)
                    _needSave = true;
                _reflections = value;
            }
        }

        private bool _refractions = true;
        /// <summary>
        /// Show refractions on see through surfaces.
        /// </summary>
        public bool Refractions
        {
            get { return _refractions; }
            set
            {
                if (_refractions != value)
                    _needSave = true;
                _refractions = value;
            }
        }

        private bool _shadowMapping = true;
        /// <summary>
        /// Cast shadows in the environment.
        /// </summary>
        public bool ShadowMapping
        {
            get { return _shadowMapping; }
            set
            {
                if (_shadowMapping != value)
                    _needSave = true;
                _shadowMapping = value;
            }
        }

        private bool _highDetail = true;
        /// <summary>
        /// Use high detail rendering, textures and lighting.
        /// </summary>
        public bool HighDetail
        {
            get { return _highDetail; }
            set
            {
                if (_highDetail != value)
                    _needSave = true;
                _highDetail = value;
            }
        }

        private float _soundVolume = 0.8f;
        /// <summary>
        /// Sound volume.
        /// </summary>
        public float SoundVolume
        {
            get { return _soundVolume; }
            set
            {
                if (_soundVolume != value)
                    _needSave = true;
                _soundVolume = value;
            }
        }

        private float _musicVolume = 0.6f;
        /// <summary>
        /// Music volume.
        /// </summary>
        public float MusicVolume
        {
            get { return _musicVolume; }
            set
            {
                if (_musicVolume != value)
                    _needSave = true;
                _musicVolume = value;
            }
        }

        private float _controllerSensitivity = 0.5f;
        /// <summary>
        /// Controller sensitivity.
        /// </summary>
        public float ControllerSensitivity
        {
            get { return _controllerSensitivity; }
            set
            {
                if (_controllerSensitivity != value)
                    _needSave = true;
                _controllerSensitivity = value;
            }
        }
        #endregion

        #region Default
        /// <summary>
        /// Filename used to store the game settings.
        /// </summary>
        const string SettingsFilename = "GameSetting.xml";

        private static GameSettings _defaultInstance = null;
        /// <summary>
        /// Default instance of the game settings.
        /// </summary>
        public static GameSettings Default
        {
            get { return _defaultInstance; }
        }  
 
        /// <summary>
        /// Need to save the game settings file only if true.
        /// </summary>
        private static bool _needSave = false;
        #endregion

        #region Constructor
        /// <summary>
        /// No public constructor! Create the game settings.
        /// </summary>
        private GameSettings()
        {
        }

        /// <summary>
        /// Create game settings.  This constructor helps us to only load the
        /// GameSetting once, not again if GameSetting is recreated by
        /// the Deserialization process.
        /// </summary>
        public static void Initialize()
        {
            _defaultInstance = new GameSettings();
            Load();
        }       
        #endregion

        #region Load
        /// <summary>
        /// Load
        /// </summary>
        public static void Load()
        {
            _needSave = false;

            FileStream file = FileHelper.LoadGameContentFile(
                SettingsFilename);

            if (file == null)
            {
                // We need a save, but wait to create the file after quitting.
                _needSave = true;
                return;
            }

            // If the file is empty, just create a new file with the default
            // settings.
            if (file.Length == 0)
            {
                // Close the file first.
                file.Close();

                // Check if there is a file in the game directory
                // to load the default game settings.
                file = FileHelper.LoadGameContentFile(SettingsFilename);
                if (file != null)
                {
                    // Load everything into this class.
                    GameSettings loadedGameSetting =
                        (GameSettings)new XmlSerializer(typeof(GameSettings)).Deserialize(file);
                    if (loadedGameSetting != null)
                        _defaultInstance = loadedGameSetting;

                    // Close the file.
                    file.Close();
                }

                // Save the user settings.
                _needSave = true;
                Save();
            }
            else
            {
                // Else load everything into this class with help of the
                // XmlSerializer.
                GameSettings loadedGameSetting =
                    (GameSettings)new XmlSerializer(typeof(GameSettings)).Deserialize(file);
                if (loadedGameSetting != null)
                    _defaultInstance = loadedGameSetting;

                // Close the file.
                file.Close();
            }
        }
        #endregion       

        #region Save
        /// <summary>
        /// Save
        /// </summary>
        public static void Save()
        {
            // No need to save if everything is up to date.
            if (!_needSave)
                return;

            _needSave = false;

            FileStream file = FileHelper.SaveGameContentFile(
                SettingsFilename);

            // Save everything in this class with help from the XmlSerializer.
            new XmlSerializer(typeof(GameSettings)).Serialize(file, _defaultInstance);

            // Close the file.
            file.Close();
        }

        /// <summary>
        /// Set the minimum graphics capabilities.
        /// </summary>
        public static void SetMinimumGraphics()
        {
            GameSettings.Default.ResolutionWidth = GameSettings.MinimumResolutionWidth;
            GameSettings.Default.ResolutionHeight = GameSettings.MinimumResolutionHeight;
            GameSettings.Default.Reflections = false;
            GameSettings.Default.Refractions = false;
            GameSettings.Default.ShadowMapping = false;
            GameSettings.Default.HighDetail = false;
            GameSettings.Default.PostScreenEffects = false;
            GameSettings.Save();
        }
        #endregion
    }
}

EngineManager class  

Updated to use the Managers namespace instead of Manager.The EngineManager class is the first and most important Manager class in the Engine.  It will be the way all other classes in the Engine retrieve Devices and Content in the Engine.  It will also maintain the Game object itself, this is a very important step and also puts in place a foundation of how the rest of the engine will be designed.

using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
namespace RoeEngine2.Managers
{
    public class EngineManager : RoeEngine2
    {
        private static Game _game;
        /// <summary>
        /// The XNA game.
        /// </summary>
        public static Game Game
        {
            get { return _game; }
            set { _game = value; }
        }
        public EngineManager(string unitTestName)
            : base(unitTestName)
        {
        }
        public EngineManager()
            : base( "Engine" )
        {
        }
        protected override void Draw(GameTime gameTime)
        {
            base.Draw(gameTime);
            Device.Clear(BackgroundColor);
        }
    }
}

Program.cs, the makings of your first game.  

Updated to use the Managers namespace instead of Manager.Add a reference to your game project and reference the Engine project in this solution.  Open the Program.cs file and replace the code you find with the following.

using System;
using RoeEngine2.Managers;
using Microsoft.Xna.Framework;

#if !XBOX360
using System.Windows.Forms;
using Microsoft.Xna.Framework.Graphics;
#endif

namespace Kynskavion
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
#if !XBOX360
        [STAThread]
#endif
        static void Main(string[] args)
        {
#if DEBUG
            StartUnitTests();
#else
            StartGame();
#endif
        }

        private static void StartUnitTests()
        {
            StartGame();
        }

        private static void StartGame()
        {
#if !XBOX360
            try
            {
#endif
                using (EngineManager game = new EngineManager())
                {
                    EngineManager.Game = game;
                    SetupScene();
                    game.Run();
                }
#if !XBOX360
            }
            catch (NoSuitableGraphicsDeviceException)
            {
                MessageBox.Show("Pixel and vertex shaders 2.0 or greater are required.",
                    "Kynskavion",
                    MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            catch (OutOfVideoMemoryException)
            {
                //GameSettings.SetMinimumGraphics();

                MessageBox.Show("Insufficent video memory.\n\n" +
                    "The graphics settings have been reconfigured to the minimum. " +
                    "Please restart the application. \n\nIf you continue to receive " +
                    "this error message, your system may not meet the " +
                    "minimum requirements.  \n\nCheck documentation for minimum requirements.",
                    "Kynskavion",
                    MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString(), "Kynskavion", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
#endif
        }

        private static void SetupScene()
        {
            //throw new Exception("The method or operation is not implemented.");
        }
    }
}

Excellent, we have just created a fine starting point for the rest of the game engine.  Not too bad for 30 minutes of work.  You can now set the startup project to be your game project and press F5 to see a nice blue screen…Conclusion

I realize this seems like a lot of code just to have a baby blue screen, you could have done baby blue just with the game template out of the XNA box.  Remember though, we are not trying to make a simple sample, we want a robust engine.  That means we have to abstract and encapsulate a bit, there is a lot more code to write before we make the next Crysis engine.

I look forward to your comments and suggestions, your feedback is fuel for new development.

January 9, 2008 - Posted by | C#, XBOX360, XNA

49 Comments »

  1. […] RoeCode over at Running on Empty has started a series of Articles that go through the process of creating a Game Engine using the XNA Framework. […]

    Pingback by Tutorial Series - Creating a Game Engine in XNA - Mykres Space | January 9, 2008 | Reply

  2. This looks promising.

    The only thing I’m not sure of is the Scenegraph manager since scenegraph could turn out to be not as robust as many developers may think, but YMMV.

    Comment by Pete | January 9, 2008 | Reply

  3. Thank you for the comment Pete. I totally understand what you mean about SceneGraphs. I have used them before with good success, but only in small demos. The SceneGraph class that I am going to show in a later article is definitly just to get objects to render in a very basic sense.

    I plan to explore other scene management principles such as portals.

    Comment by roecode | January 9, 2008 | Reply

  4. Looks like a great start, I’ll be watching, and hopefully be able make some suggestions when appropriate.

    Comment by KaBaL | January 9, 2008 | Reply

  5. > I plan to explore other scene management principles such as portals.

    A portals system is a quite interesting task …

    Comment by Pete | January 9, 2008 | Reply

  6. Thank you for your feedback KaBal. Suggestions are always welcome.

    Comment by roecode | January 9, 2008 | Reply

  7. How about physics?

    Comment by selinux | January 10, 2008 | Reply

  8. selinux, thank you for your interest.

    Yes, I have a planned series for implementing physics in the Engine. I figured this series would be for the base engine. Physics, AI, an engine editor and AI will come as addons.

    Comment by roecode | January 10, 2008 | Reply

  9. I made some updates to include the GraphicDeviceManager_DeviceReset event. I decided even though I am using YagNi, it wasn’t worth leaving this vital piece out.

    Also, I cleaned up the ugly white space in the code.

    Comment by roecode | January 10, 2008 | Reply

  10. You’ve made a mistake in updating this page, the GameManager.cs code is the same as the GameSettings.cs code. ( I noticed as I came back to check something after making changes )

    Comment by KaBaL | January 10, 2008 | Reply

  11. sorry, meant “EngineManager.cs”

    Comment by KaBaL | January 10, 2008 | Reply

  12. Wow, thank you KaBaL. Fixed the code now.

    Comment by roecode | January 10, 2008 | Reply

  13. […] Part I: Basic engine skeleton […]

    Pingback by Getting up with XNA GS 2.0 - Kartones Blog | February 4, 2008 | Reply

  14. […] a Game Engine in XNA No, not me. Blogger roecode has just started writing this series, XNA Game Engine Development. I’m in awe. I’ve fantasized about writing a game engine ever since I knew what such a […]

    Pingback by Building a Game Engine in XNA « No Free Time | February 6, 2008 | Reply

  15. it could be a rookie question..but i saw you declaring events in your classes, events works on xbox?

    if so, thats a very good news(at least to me) , cos until now i did’nt see a code using event driven arquitecture…wich is far superior …letting someone to hook a delegate..without having to change the general purpose engine library..

    my question is..if it works on xbox..it would be a good architecture practive..but a bad performance practice?

    would’nt perfomance be compromised with this practice??
    (i really hopes not :))

    Comment by Kaminski | February 24, 2008 | Reply

  16. Kaminski,
    That is a great question, I assume the event in question is the GraphicsDeviceManager_PreparingDeviceSettings event used in the base engine. I have not heard of any issues with using Event driven architecture on the XBOX360, I would be very interested in seeing an article or blog where events are brought into question on the XBOX.

    As for performance, this particular event is not performance intensive, rather it is a callback event for the graphics device initializes. In the ScreenManager, I use events for user interaction, the example is a swipe from the XNA team site.

    So I am left with two things to think about.

    1) If events do not work on the XBOX I will need to replace the code concerning events.
    2) If events perform poorly then I will have to be very careful not to use them in performance intensive sections of code.

    Comment by roecode | February 25, 2008 | Reply

  17. Hi what bout the inputmanager? do you plan to model something similar?

    i dont see an input manager on your initial design

    Comment by Armando | February 28, 2008 | Reply

  18. No, I don’t have immediate plans for an Input Manager. I do have plans for an Input Mapper, but right now input is handled as a GameComponent. See some of the later articles, namely Part 11 and Part 12, as well as the ScreenManager class. Each of those handles input.

    Comment by roecode | February 28, 2008 | Reply

  19. Hey there!

    Very good work!

    I’ve got an interesting question: what’s the reason for making most of the member fields protected static? Why not leave them as private since you’re using properties anyway… no?

    I’m a newb 😛

    Comment by Jsmaycotte | March 25, 2008 | Reply

  20. I made them protected so that I would not be tempted to use them in the inherited child class. Protected is just a way of encapsulating a member variable and is useful for protection when you inherit the parent member.

    Comment by roecode | March 25, 2008 | Reply

  21. Hi. This article is very great!!!
    I found some typographical errors.

    RoeEngine2 : 189
    if (resolutionWidth <= 0 || resolutionWidth
    if (resolutionWidth <= 0 || resolutionHeight <= 0)

    Right???

    Comment by Joey | March 25, 2008 | Reply

  22. Yup, that is an error, I will correct it in a future release. Thank you for pointing it out.

    Be aware, I have rewritten many of these base classes recently and will be depreciating some of this older code.

    Comment by roecode | March 26, 2008 | Reply

  23. Hi,

    I’m a realy big newby at programming.
    your site really looks nice and it’s easy to understand but
    at the end i don’t understand how you can ad a refernce of the gameEngine to your test project.

    I tried to do references–>add–>browse
    and in the map where i saved the roelengine project i could find any of the files that are a reference file

    please help.

    thanks

    srry for my bad english

    Comment by Bart | April 14, 2008 | Reply

  24. Right click on the test project, references, projects tab, then pick the game project.

    Comment by roecode | April 14, 2008 | Reply

  25. thanks I got it to work.

    Comment by Bart | April 15, 2008 | Reply

  26. Hey Dude thx for the tutorial… but i keep gettings some errors… Which i cant seem to correct.. Maybe you can help me??? 🙂

    Sry im such a noob to this XNA…

    /MJ

    Error 4 Identifier expected P:\ECS Game Engine\GameEngine\Kynskavion\Program.cs 79 33 Kynskavion

    Error 5 Expected class, delegate, enum, interface, or struct P:\ECS Game Engine\GameEngine\Kynskavion\Program.cs 79 35 Kynskavion

    Error 7 Type or namespace definition, or end-of-file expected P:\ECS Game Engine\GameEngine\Kynskavion\Program.cs 85 9 Kynskavion

    Error 10 Metadata file ‘P:\ECS Game Engine\GameEngine\RoeEngine2\bin\x86\Debug\RoeEngine2.dll’ could not be found Kynskavion

    Comment by Morten | May 7, 2008 | Reply

  27. […] RoeCode over at Running on Empty has started a series of Articles that go through the process of creating a Game Engine using the XNA Framework. […]

    Pingback by Tutorial Series - Creating a Game Engine in XNA : Virtual Realm | May 10, 2008 | Reply

  28. Hi there 😀
    i have problems compiling the code cuz at line 22 in the EngineManger class it’s an “}” expected. Same in RoeEngine line 144. Can figure out whats the problem is :-/
    Could anyone help please?
    ty

    Comment by J2T | May 15, 2008 | Reply

  29. J2T, in more recent posts I actually release source code. Just copying the source code from these windows has not proved to be too helpful. From time to time I’ve linked out my project source.

    If you still have problems compiling you can send me your source and I might be able to give you some better hints.

    Comment by roecode | May 15, 2008 | Reply

  30. […] Part 1, Engine Base Classes, Helpers and EngineManager […]

    Pingback by UH COSC Interactive Game Development » XNA Game Engine Development | May 21, 2008 | Reply

  31. I am the most amateur of amateurs but is there not some syntax errors going on at and around line 141 of your RoeEngine? I know my compiler wont eat it, in fact about half of it ends up being commented out after the double forward slashes after http:. Did some html get mixed into your coding as well? Here is what I am talking about.

    protected RoeEngine2()
    : this(“Game&quot
    {

    }

    Comment by Justin Hoffman | July 7, 2008 | Reply

  32. Much of the code was cut from my comment, but If you refer to your source you from lines 140 to 144 you should see what I am talking about.

    Comment by Justin Hoffman | July 7, 2008 | Reply

  33. Yes, this is due to the code posting template I am using. Sometimes certain constructor logic looks like quotation fields to word press.

    The protected constructor should look something like this

    protected RoeEngine2()
    : this(“GameEngine”)
    {

    }

    Please, do not just copy and paste code from the windows, rather it makes more sense to try to understand the concepts at work here and make some of your own changes and tweeks to the code.

    Not much learning can be done by simply copying and pasting code.

    Comment by roecode | July 7, 2008 | Reply

  34. Hi,

    Thanks for the tutorials so far; I’m looking forward to exploring large-scale C# design as well as some more advanced graphics concepts down the road! It’s interesting to see how someone else approaches implementing solutions to similar problems I’ve faced in my own designs.

    Keep up the great work!
    Nathan

    Comment by Nathan | August 5, 2008 | Reply

  35. Actually, I was hoping to ask a question about your Load() method; specifically, in the “if (file.Length == 0)” block. The way I’m reading it, you close the file descriptor only to open it again right away, and load from it if the descriptor isn’t null. But we know the descriptor can’t be null, since we were able to read the length of the file back in the conditional! Further, since the file is known to be empty, there’s no point in deserializing it. since there’s nothing there..am I conceptually thinking this through right, or is there something I’m missing?

    Thanks,
    Nathan

    Comment by Nathan | August 5, 2008 | Reply

  36. That block is used to determine if we need to save a new settings file or if we have found one and can use the settings we discovered in the file.

    The block isn’t used for an empty file, it is used for a file that does not exist.

    Comment by roecode | August 5, 2008 | Reply

  37. Sorry for the questions, but if the file doesn’t exist, shouldn’t LoadGameContentFile() return null, and not pass through to the (Length == 0) conditional? What is the difference between the file descriptor being null and the length being null, if the latter means that the file doesn’t exist? Also, if the block is used for cases when the file doesn’t exist, why do we do the close descriptor, reopen, and serialize from it? There’s probably something obvious that I’m just not seeing.

    Comment by Nathan | August 5, 2008 | Reply

  38. In OpenFileForCurrentPlayer, the container is null and the path property is being accessed resulting in a null exception. Should it be the Title Path?

    Comment by Shane | August 27, 2008 | Reply

  39. I recently ran across this series and have found the information you’re providing very helpful to a project I am currently working on. One thing I am wondering is what program you used to generate the diagram at the top of this article.
    Thank you for your time!

    Comment by Ari | October 3, 2008 | Reply

  40. That is Microsoft Visio.

    Comment by roecode | October 3, 2008 | Reply

  41. Hi,
    I just ran into this series and wanted to say thanks for the “effort for the public” and the great work.
    I didn’t find a link to entire source code project.
    Is it availabe?
    If so, where/how can I get?

    Thanks, and keep it up!
    Guy.

    BTW,
    Can I subscribe to recieve email-notifications when someone replies to my comments?

    Comment by guyguy | October 11, 2008 | Reply

  42. You can take a look at one of the later sections to find the source code to download.

    Not certain about the subscription to replys to your comments though. Glad you enjoy the series though.

    Comment by roecode | October 13, 2008 | Reply

  43. To pay tribute to you!
    I will always follow the course you go on learning.
    Thanks again!

    Comment by TianYu | November 17, 2008 | Reply

  44. I have two error that i don’t know how to fix,(i’m a noob)
    can some one help me?

    error one -‘ReoEngine2’ is a ‘namespace’ but is used like a ‘type’ | EngineManager.cs |line 6 |column 34

    error two – Metadata file ‘C:\Users\Rseifert\Documents\Visual Studio 2008\Projects\ReoEngine\ReoEngine\bin\x86\Debug\ReoEngine.dll’ could not be found

    Comment by Bob | November 25, 2008 | Reply

  45. I’m using XNA 3.0 GSE and i have a the error

    “The type or namespace name ‘Windows’ does not exist in the namespace ‘System’ (are you missing an assembly reference”

    this error comes up in my Program.cs

    any suggestions

    Comment by Acedia | November 30, 2008 | Reply

  46. I have fixed this issue by adding the reference to System.Windows.Forms in the solution explorer.

    Comment by Acedia | November 30, 2008 | Reply

  47. Does anyone know anything about visual studio freaking out when attempting to add second xna projects to a solution? For some reason, when I do it, visual studio says there was a corrupt memory problem. I know my ram and hard drives are in decent condition. When I restart the solution, both projects are there, but I’m missing the content portion of the second project that was added. I’m using visual studio 2k5 professional with sp1.

    Comment by Steven | August 31, 2009 | Reply

  48. If I’m not mistaken:

    if (resolutionWidth <= 0 || resolutionWidth <= 0)
    Has to be
    if (resolutionWidth <= 0 || resolutionHeight <= 0)

    On line 189 of the first code part.

    Comment by Iltar van der Berg | October 15, 2009 | Reply

  49. why does the container is null in “OpenFileForCurrentPlayer” method?
    is it not supposed to be something else?

    Comment by Aviad | May 31, 2010 | Reply


Leave a reply to KaBaL Cancel reply