Metallic Void
Project Type: Vertical Scrolling Space Shooter
Duration: 2 years, on and off
Language: C#
Special Tools: Monogame
Credits:
Music: Chris Logsdon
Tweening Engine: Jacob Albano
World system concept: Jacob Albano
Description:
Metallic Void is an ode to the old space shooters I used to play as a kid, such as Gradius, Galaga, R-Type, Life Force, etc… It started out as my final project for my first programming class at NHTI and has gone through a few revamps over the course of it’s development. Currently I have two complete levels, random generation of the enemies every time you play, and freedom to choose which level you want to start with, similar to the Mega Man level selection screen.
MV is also the project I have worked on the longest. Since it is built using Monogame I had to write the systems to make everything work. Building on top of what I learned in school, and concepts I learned from working with friends, I built out a 2d engine to power the game, which was later used in the NHTI AG110 course. See below for some of the major components of the game, and the engine!
The original concept for Metallic Void was done in flash and can be found here: Metallic Void Flash
Code Snippets:
Below are some of the classes / systems I developed while working on the game. I found I spent more time working on the engine and systems than I did working on the actual game!
DE and DEGame
These two classes are the main files of the engine. DE is a static class that is used to initialize all the core components that are used by the engine, and stores them as static references to be able to be used throughout this project. DEGame is the class that inherits from Monogame’s Game class and overrides the main functions that get called by the framework. This approach allowed me to abstract the core engine functionality out to be able to use it as a class library. At which point future developers would inherit DEGame with their own Game file.
namespace Engine { /// <summary> /// The main class of the game that handles initializing the different components, and has static references to them. /// EG. Camera, Tweener, TimerManager, etc... /// </summary> public static class DE { public static Camera Camera; public static DebugConsole DebugConsole; public static Tweener GameTweener; public static float DeltaTime; public static Matrix WorldTransform; public static Vector2 WorldCenter; public static Random Random; public static TimerManager TimerManager; public static SoundSystem SoundSystem; public static int BaseScreenWidth { get; set; } public static int BaseScreenHeight { get; set; } /// <summary> /// Gets the current world, or sets the new world to go to at the end of the next frame /// </summary> public static World World { get { return _world; } set { if (value != _world) _nextWorld = value; } } private static Game _deGame; internal static World _nextWorld; internal static World _world; /// <summary> /// Initializes the components used in the engine /// </summary> /// <param name="game">The game object for this project.</param> /// <param name="gDevice">The graphics device to use</param> /// <param name="content">The content object to use</param> public static void Initialize(Game game, GraphicsDevice gDevice, ContentManager content) { _deGame = game; Random = new Random(Guid.NewGuid().GetHashCode()); //ParticleEngine.Initialize(); Library.Initialize(content); GameTweener = new Tweener(); TimerManager = new TimerManager(); SoundSystem = new SoundSystem(); SoundSystem.Init(100, INITFLAGS.NORMAL); World = new World(); WorldCenter = new Vector2(Camera.Width * .5f, Camera.Height * .5f); WorldTransform = Matrix.CreateTranslation(new Vector3(-WorldCenter, 0.0f))* Matrix.CreateRotationZ(0.0f)* Matrix.CreateTranslation(new Vector3(WorldCenter, 0.0f)); DebugConsole = new DebugConsole(_deGame); #if DEBUG DebugConsole.Enabled = true; _deGame.Components.Add(DebugConsole); #endif } /// <summary> /// Sets the mouse visibility /// </summary> public static bool IsMouseVisible { get { return _deGame.IsMouseVisible; } set { _deGame.IsMouseVisible = value; } } /// <summary> /// Logs a message to the screen. This is only available with the debug version of the engine /// </summary> /// <param name="msg">The message to write</param> /// <param name="ttl">How long to leave the message on the screen.</param> public static void Log(string msg, float ttl = 2) { #if DEBUG DebugConsole.Log(msg, ttl); #endif } /// <summary> /// Logs a message to the screen using string.Format. This is only available with the debug version of the engine /// </summary> /// <param name="msg">The message to write</param> /// <param name="ttl">How long to leave the message on the screen.</param> /// <param name="args">Arguments to use for the string.Format</param> public static void Log(string msg, float ttl = 2, params object[] args) { #if DEBUG DebugConsole.Log(msg, ttl, args); #endif } } }
namespace Engine { public class DEGame : Game { SpriteBatch _spriteBatch; /// <summary> /// Pauses or Un-pauses the game loop /// </summary> public bool IsPaused { get; set; } /// <summary> /// Whether or not the game should be run in full screen /// </summary> public static bool IsFullScreen { get; set; } /// <summary> /// Initializes the game and world. If you override this remember to call base.Initialize() !!!! /// </summary> protected override void Initialize() { DE.Initialize(this, GraphicsDevice, Content); CheckWorld(); base.Initialize(); } /// <summary> /// Initializes the spritebatch to use to draw the game. If you override this remember to call base.LoadContent() !!!! /// </summary> protected override void LoadContent() { _spriteBatch = new SpriteBatch(GraphicsDevice); } /// <summary> /// Unloads content. If you override this remember to call base.UnloadContent() !!!! /// </summary> protected override void UnloadContent() { DE.SoundSystem.Destroy(); base.UnloadContent(); } /// <summary> /// Updates the game. If you override this remember to call base.Update() !!!! /// </summary> /// <param name="gameTime">The built in XNA game time object</param> protected override void Update(GameTime gameTime) { DEInput.Update(); DE.SoundSystem.Update(); // TODO: Change this to Escape to pause and add paused overlay if (DEInput.IsNewKeyPress(Keys.P)) { IsPaused = !IsPaused; if (IsPaused) { MediaPlayer.Pause(); } else { MediaPlayer.Resume(); } } if (IsPaused) { base.Update(gameTime); return; } DE.DeltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds; DE._world.Update(); DE._world.UpdateLists(); if (DE._nextWorld != null) { CheckWorld(); } #if DEBUG if (DE.DebugConsole.Enabled) { if (DEInput.IsNewKeyPress(Keys.F1)) { DE.DebugConsole.IsActive = !DE.DebugConsole.IsActive; } if (DEInput.IsNewKeyPress(Keys.F5)) { Library.ClearLists(); } } #endif DE.GameTweener.Update(DE.DeltaTime); //ParticleEngine.Update(DE.DeltaTime); DE.TimerManager.Update(); if (DEInput.IsNewButtonPress(Buttons.Back, PlayerIndex.One) || DEInput.IsNewKeyPress(Keys.Escape)) { Exit(); } base.Update(gameTime); } /// <summary> /// Draws the current world /// </summary> /// <param name="gameTime">The built in XNA game time object</param> protected override void Draw(GameTime gameTime) { _spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, null, null, null, null, DE.Camera.Get_Transformation()); DE.World.Draw(_spriteBatch); _spriteBatch.End(); base.Draw(gameTime); } private void CheckWorld() { if(DE._nextWorld == null) return; if (DE._world != null) { DE._world.End(); DE._world.UpdateLists(); } DE._world = DE._nextWorld; DE._nextWorld = null; DE._world.UpdateLists(); DE._world.Begin(); DE._world.UpdateLists(); } } }
The Debug Console class handled rendering debug information to the screen, which included the frame rate of the game, the amount of memory the application was using, bounding boxes and collision of objects, and messages created through DebugConsole.log.
/// <summary> /// Class to handle showing the amount of memory being used by the application, and log messages to write to the screen. /// This is only available with the debug version of the engine /// </summary> public class DebugConsole : DrawableGameComponent { FrameRateCounter fps; SpriteBatch spriteBatch; WeakReference gcReference; // for memory numbers long totalMemKB; private SpriteFont _debugFont; private List<DebugMessage> _debugMessages; //private List<GameObject> _objects; private const string MemStringFormat = "Total Memory: {0}KB"; private SpriteFont _font; public bool IsActive; public DebugConsole (Game game) :base(game) { IsActive = false; Enabled = false; _debugMessages = new List<DebugMessage>(); //_objects = new List<GameObject>(); } /// <summary> /// Initializes the console /// </summary> public override void Initialize () { gcReference = new WeakReference(null); totalMemKB = 0; spriteBatch = new SpriteBatch(this.GraphicsDevice); fps = new FrameRateCounter(this.Game.Window.ClientBounds); Library.LoadFonts("Fonts/", "Times12"); base.Initialize(); } /// <summary> /// Loads content needed for drawing to the screen /// </summary> protected override void LoadContent() { _debugFont = Library.GetFont("Times12"); _font = Library.GetFont("Arial20"); base.LoadContent(); } /// <summary> /// Updates the memory display, and messages to write /// </summary> /// <param name="gameTime">The built in XNA GameTime object</param> public override void Update (GameTime gameTime) { if (IsActive) { fps.Update (gameTime); } if (!gcReference.IsAlive) { gcReference = new WeakReference(new object()); totalMemKB = GC.GetTotalMemory(false) / 1024; // still not sure if this is a valid memory tracker... } foreach (var dbm in _debugMessages.ToList()) { dbm.Update(DE.DeltaTime); } base.Update(gameTime); } /// <summary> /// Draws the current memory usage, messages to write, bounding boxes for game objects, and collision boxes for game objects /// </summary> /// <param name="gameTime"></param> public override void Draw(GameTime gameTime) { if (IsActive) { spriteBatch.Begin(); fps.Draw(spriteBatch); foreach (var go in DE.World.GetAllActiveObjects().Where(o => o.Graphic != null && o.Visible)) { LineDrawer.DrawRectangle(spriteBatch, 1.0f, Color.White, go.BoundingBox); LineDrawer.DrawRectangle(spriteBatch, 1.0f, Color.Red, go.CollisionRect); } var memString = string.Format(MemStringFormat, totalMemKB); spriteBatch.DrawString(_font, memString, Vector2.Zero, Color.White); var dbmY = 30; foreach (var dbm in _debugMessages.ToList()) { spriteBatch.DrawString(_debugFont, dbm.Message, new Vector2(10, dbmY), Color.White); dbmY += 20; } spriteBatch.End(); } base.Draw(gameTime); } /// <summary> /// Logs a message to the screen /// </summary> /// <param name="msg">The message to write</param> /// <param name="ttl">How long to leave the message on the screen</param> public void Log(string msg, float ttl = 2) { _debugMessages.Add(new DebugMessage(OnDebugMessageRemoved, msg, ttl)); } /// <summary> /// Logs a message to the screen using string.Format /// </summary> /// <param name="format">The message to write using string.Format</param> /// <param name="ttl">How long to leave the message on the screen</param> /// <param name="args">The arguments to use for the string.Format</param> public void Log(string format, float ttl = 2, params object[] args) { _debugMessages.Add(new DebugMessage(OnDebugMessageRemoved, string.Format(format, args), ttl)); } /// <summary> /// Callback function for when a message is removed from the screen to remove it from the list of messages to write /// </summary> /// <param name="debugMessage">The message to remove</param> public void OnDebugMessageRemoved(DebugMessage debugMessage) { _debugMessages.Remove(debugMessage); } }
Monogame’s implementation of sounds and music was built on top of XNA’s audio system which is pretty limited and fairly buggy. At first I used an mp3 library to play music, and Monogame’s SoundEffect class for my audio, however this proved troublesome as well. At that point I reached out to fellow developers to get ideas for fixing my audio problems. I was told to look into FMOD as he heard they had changed their pricing model and found that indie developers can use the library for free! After a bit of trouble getting things set up, at the time a C# version wasn’t fully supported, I was using FMOD to handle playing all my music and sound effects!
SoundSystem
The SoundSystem class handled initializing FMOD, along with creating and playing sounds.
/// <summary> /// Wrapper around the FMOD class to initialize the system /// </summary> public class SoundSystem { private FMOD.System _fSystem; /// <summary> /// Returns a refernce to the FMOD system /// </summary> protected FMOD.System FSystem { get { return _fSystem; } } /// <summary> /// Creates a new instance of the FMOD system /// </summary> public SoundSystem() { Factory.System_Create(out _fSystem); } /// <summary> /// Initializes the FMOD system /// </summary> /// <param name="numChannels">Number of channels to use for this system</param> /// <param name="initflags">Any FMOD initialization flags to use</param> /// <returns>True if the system is initialized successfully</returns> public bool Init(int numChannels, INITFLAGS initflags) { if (!CheckVersion()) { return false; } CheckHasSoundCard(); return InitInternal(numChannels, initflags); } /// <summary> /// Updates the FMOD system /// </summary> public void Update() { _fSystem.update(); } /// <summary> /// Creates a new channel group /// </summary> /// <param name="name">The name of the channel group to create</param> /// <returns>A new ChannelGroup object, or null if it fails</returns> public ChannelGroup CreateChannelGroup(string name) { ChannelGroup group; return _fSystem.createChannelGroup(name, out group) == RESULT.OK ? group : null; } /// <summary> /// Creates a new sound /// </summary> /// <param name="name">The name of the sound to create</param> /// <param name="mode">The FMOD mode to create the sound in</param> /// <returns>A new Sound object, or throws an exception if the Sound fails to be created</returns> public Sound CreateSound(string name, MODE mode) { Sound sound; var result = _fSystem.createSound(name, mode, out sound); if (result != RESULT.OK) { DE.Log("CreateSound returned result: {0}", 2, result); } if (sound == null) { throw new Exception(string.Format("Failed to create sound. Error Code {0}", result)); } return sound; } /// <summary> /// Creates a new Sound using a stream /// </summary> /// <param name="name">The name of the sound to create</param> /// <param name="mode">The FMOD mode to create the sound in</param> /// <returns>A new Sound object, or throws an exception if the Sound fails to be created</returns> public Sound CreateStream(string name, MODE mode) { Sound sound; var result = _fSystem.createStream(name, mode, out sound); if (result != RESULT.OK) { DE.Log("CreateStream returned result: {0}", 2, result); } if (sound == null) { throw new Exception(string.Format("Failed to create stream. Error Code {0}", result)); } return sound; } /// <summary> /// Plays the sound in the passed in channel group /// </summary> /// <param name="sound">The sound to play</param> /// <param name="channelGroup">The channel group to play the sound in</param> /// <param name="startPaused">Whether or not to start the sound paused</param> /// <returns>The channel the sound is playing in, or throws an exception if the sound fails to play.</returns> public Channel PlaySound(Sound sound, ChannelGroup channelGroup, bool startPaused) { Channel channel; var result = _fSystem.playSound(sound, channelGroup, startPaused, out channel); if (result != RESULT.OK) { DE.Log("PlaySound returned result: {0}", 2, result); } if (channel == null) { throw new Exception(string.Format("Failed to play sound. Error Code {0}", result)); } return channel; } /// <summary> /// Releases the memory taken up from the sound system /// </summary> public virtual void Destroy() { _fSystem.close(); _fSystem.release(); } private bool CheckVersion() { uint version; var result = _fSystem.getVersion(out version); if (result != RESULT.OK || version < VERSION.number) { DE.Log("Fmod version mismatch..."); _fSystem.close(); _fSystem.release(); _fSystem = null; return false; } return true; } private void CheckHasSoundCard() { int numDrivers; _fSystem.getNumDrivers(out numDrivers); if (numDrivers <= 0) { _fSystem.setOutput(OUTPUTTYPE.NOSOUND); } } private bool InitInternal(int numChannels, INITFLAGS initflags) { var result = _fSystem.init(numChannels, initflags, (IntPtr)null); if (result != RESULT.OK) { DE.Log("Failed to init sound system"); _fSystem.close(); _fSystem.release(); _fSystem = null; return false; } return true; } }
DESoundEffect
DESoundEffect is a replacement for Monogame’s SoundEffect class that utilized the SoundSystem, and FMOD, to create, and play sound effects.
/// <summary> /// A wrapper around the FMOD class to handle creating, or playing new sound effects /// </summary> public class DESoundEffect { private readonly Sound _sound; private Channel _channel; /// <summary> /// Gets or Sets the volume for the current channel /// </summary> public float Volume { get { return _channel.GetVolume(); } set { _channel.setVolume(MathHelper.Clamp(value, 0, 1)); } } /// <summary> /// Creates a new instance of a DESoundEffect /// </summary> /// <param name="name">The name of the file, including the path, to create a new instance of.</param> public DESoundEffect(string name) { _sound = DE.SoundSystem.CreateSound(name, MODE.DEFAULT | MODE._2D); } /// <summary> /// Plays the sound effect on the passed in channelGroup /// </summary> /// <param name="channelGroup">The channel group to use to play the sound</param> public void Play(ChannelGroup channelGroup) { _channel = DE.SoundSystem.PlaySound(_sound, channelGroup, false); } /// <summary> /// Releases the memory for this sound effect /// </summary> public void Destroy() { _sound.release(); } }
DESong
DESong is a replacement for Monogame’s Song class that utilized the SoundSystem, and FMOD, to create, play, pause, or stop music.
/// <summary> /// A wrapper around the FMOD class to handle creating, or playing new songs /// </summary> public class DESong { private readonly Sound _sound; private Channel _channel; /// <summary> /// Gets or sets the volume for the song channel /// </summary> public float Volume { get { return _channel.GetVolume(); } set { _channel.setVolume(MathHelper.Clamp(value, 0, 1)); } } /// <summary> /// Creates a new instance of a DESong. /// </summary> /// <param name="name">The name of the file, including the path, to create a new instance for.</param> /// <param name="loop">Whether or not to loop the music</param> /// <param name="stream">Whether or not to stream the file</param> public DESong(string name, bool loop, bool stream) { _sound = stream ? DE.SoundSystem.CreateStream(name, loop ? MODE.LOOP_NORMAL | MODE._2D : MODE.DEFAULT | MODE._2D) : DE.SoundSystem.CreateSound(name, loop ? MODE.LOOP_NORMAL | MODE._2D : MODE.DEFAULT | MODE._2D); } /// <summary> /// Plays the song on the passed in channelGroup /// </summary> /// <param name="channelGroup">The channel group to use to play this song</param> /// <param name="startPaused">Whether or not the song should start paused</param> public void Play(ChannelGroup channelGroup, bool startPaused) { _channel = DE.SoundSystem.PlaySound(_sound, channelGroup, startPaused); } /// <summary> /// Stops the current channel /// </summary> public void Stop() { _channel.stop(); } /// <summary> /// Pauses the current channel /// </summary> public void Pause() { _channel.setPaused(true); } /// <summary> /// Resumes the current channel /// </summary> public void Resume() { _channel.setPaused(false); } /// <summary> /// Releases the memory for the current channel /// </summary> public void Destroy() { _sound.release(); } }
Using a system similar to UDK’s state system I created a state machine to handle the different states of my objects. This made it easier to switch between states and only perform certain logic based off the state the object was in. This was especially handy with the bosses to transition between phases.
Each game object has an instance of the state manager that handles adding, getting, or switching states. As seen below.
public abstract class GameObject : IGameObject { ... public List<string> GetStates() { return _stateManager.GetStatesList(); } protected void AddState(string name, State state) { _stateManager.AddState(name, state); } protected void RemoveState(string name) { _stateManager.RemoveState(name); } public void GoToState(string name) { _currentState = _stateManager.GoToState(name); } public string GetCurrentStateName() { return _stateManager.GetStateName(_currentState); } public void Update(float deltaTime) { if (_currentState == null) return; _currentState.Update(deltaTime); } ... }
Adding States
The state itself is just a class with a name, and a method to call based off it’s state. When a game object is created, I would add the states to it’s state manager and assign a method to the state to be called in the update function of the base Game Object. In the case of my enemies, I had a base class that would add common states, and the object that inherited this class could override them, or add new ones, if needed.
public Enemy() { AddState(ACTIVE, new State(Active)); AddState(EXPLODING, new State(Exploding)); AddState(DEAD, new State(Dead)); AddState(IMMUNE, new State(Immune)); } virtual public void Active(float deltaTime) { } virtual public void Exploding(float deltaTime) { } virtual public void Dead(float deltaTime) { } virtual public void Immune(float deltaTime) { }
The world system I used was created from the concept Jake used in his Indigo project, which was a port of the Flash framework called Flash Punk. The same concepts have been applied, but the code was modified to work with my engine, and the Monogame framework.
World Class
The world class made it easy to add and remove worlds like the Title Menu, Level Selection, or any other level I wanted to create.
public class World { /// <summary> /// List of GameObjects that are to be added to the world at the end of the update event /// </summary> internal List<GameObject> ObjsToAdd; /// <summary> /// List of GameObjects that are to be removed from the world at the end of the update event /// </summary> internal List<GameObject> ObjsToRemove; /// <summary> /// Dictionary that holds a list of game objects for each type of object. EG enemy, boss, player, etc... /// </summary> internal Dictionary<string, List<GameObject>> GObjs; /// <summary> /// Whether or not the world is active /// </summary> public bool IsActive = true; public World() { ObjsToAdd = new List<GameObject>(); ObjsToRemove = new List<GameObject>(); GObjs = new Dictionary<string, List<GameObject>>(); } /// <summary> /// Called when the world is added to the game /// </summary> public virtual void Begin() { } /// <summary> /// Called when the world is being removed from the game /// </summary> public virtual void End() { } /// <summary> /// Update loop to update all the game objects in the world /// </summary> public virtual void Update() { foreach (var o in GObjs.Values.SelectMany(objs => objs.Where(o => o.IsActive))) { o.Update(DE.DeltaTime); } } /// <summary> /// Draws all the game objects that are active /// </summary> /// <param name="spriteBatch"></param> public virtual void Draw(SpriteBatch spriteBatch) { foreach (var o in GObjs.Values.SelectMany(objs => objs.Where(o => o.IsActive))) { o.Draw(spriteBatch); } } ... }