Cursed Hunters: A 2D Side-Scrolling Fighter (XNA | C#)


Task: Create a fully functional game demo demonstrating separation of game engine and game code.

I opted to create a side-scrolling fighting game with artwork by my close friend and artist, Kenic Yu.

The game uses many external files to create the levels. The level itself is stored as a text file, with various characters meaning different things on the level. The characters have their own sprite sheet, and to go along with it, an XML file containing all the parameters for each animation per character. This allows for easy customisation as well as tweaking when something goes wrong.

High scores are also stored in an XML file. It contains both the player's name and their achieved score. The HighScore class keeps track of all this, and provides functions to add entries, which sorts and restricts the high scores list to the best 10 scores.

The part I'm most happy with is my implementation of dynamic key bindings, detailed below.


Dynamic Key Bindings

I want to highlight this feature I implemented because it shows a very clear separation between game engine and game code.

The player is able to change the key bindings for the game to anything that is on the keyboard. Any changes are stored to an XML file, and loaded when the game starts. The XML file also stores default key bindings, which are set with the "Reset Defaults" button in case anything goes wrong.

Shown below is the KeyBindingsMenu class, which also contains code to display the menu. It's quite long, but it works really well. I hope there's a better way of doing this instead of using an enum for the possible actions, it ended up more messy than I would like.

                    using System;
                    using System.Collections.Generic;
                    using System.Xml.Serialization;
                    using System.IO;
                    using Microsoft.Xna.Framework;
                    using Microsoft.Xna.Framework.Content;
                    using Microsoft.Xna.Framework.Graphics;
                    using Microsoft.Xna.Framework.Input;

                    namespace cw
                    {
                        class KeyBindingsMenu
                        {
                            List<Button> menuButtons;
                            List<Button> bindingButtons;

                            public KeyBindings keyBindings;

                            ContentManager content;
                            GraphicsDeviceManager graphics;
                            SpriteBatch spriteBatch;

                            GameController gameController;

                            public enum KeybindPlayerActions
                            {
                                up, down, left, right, attack
                            }

                            CommandManager commandManager; //to detect keyboard input for setting keybindings

                            SpriteFont font;
                            SpriteFont titleFont;

                            string titleText;
                            Vector2 titleSize;
                            Vector2 titlePosition;

                            public Popup popup { get; set; }

                            float screenWidth;
                            float screenHeight;

                            public KeyBindingsMenu(ContentManager content, GraphicsDeviceManager graphics, GameController gameController)
                            {
                                popup = null;
                                this.content = content;
                                this.graphics = graphics;
                                this.gameController = gameController;

                                keyBindings = ReadBindingsXML();

                                menuButtons = new List<Button>();
                                bindingButtons = new List<Button>();

                                commandManager = new CommandManager();

                                screenWidth = graphics.GraphicsDevice.Viewport.Width;
                                screenHeight = graphics.GraphicsDevice.Viewport.Height;

                                LoadContent();
                            }

                            public void LoadContent()
                            {
                                font = content.Load<SpriteFont>("menuFont");
                                Button backButton = new Button("Back", "Back", font, new Vector2(screenWidth / 2, screenHeight - (font.MeasureString("Back").Y)), Button.Align.Center);
                                backButton.mouseClick += clickBack;
                                menuButtons.Add(backButton);

                                Button saveButton = new Button("Save Bindings", "Save Bindings", font, new Vector2(screenWidth - 10, screenHeight - (font.MeasureString("Save Bindings").Y)), Button.Align.Right);
                                saveButton.mouseClick += clickSave;
                                menuButtons.Add(saveButton);

                                Button defaultsButton = new Button("Restore Defaults", "Restore Defaults", font, new Vector2(10, screenHeight - (font.MeasureString("RestoreDefaults").Y)), Button.Align.Left);
                                defaultsButton.mouseClick += clickDefaults;
                                menuButtons.Add(defaultsButton);

                                float height = 30f;
                                Vector2 position = new Vector2(screenWidth / 2, screenHeight / 3);
                                Button upButton = new Button("Move Up", "Move Up = " + keyBindings.up, font, position + new Vector2(0, height), Button.Align.Center);
                                upButton.mouseClick += upButtonClick;
                                bindingButtons.Add(upButton);

                                Button downButton = new Button("Move Down", "Move Down = " + keyBindings.down, font, position + new Vector2(0, 2 * height), Button.Align.Center);
                                downButton.mouseClick += downButtonClick;
                                bindingButtons.Add(downButton);

                                Button leftButton = new Button("Move Left", "Move Left = " + keyBindings.left, font, position + new Vector2(0, 3 * height), Button.Align.Center);
                                leftButton.mouseClick += leftButtonClick;
                                bindingButtons.Add(leftButton);

                                Button rightButton = new Button("Move Right", "Move Right = " + keyBindings.right, font, position + new Vector2(0, 4 * height), Button.Align.Center);
                                rightButton.mouseClick += rightButtonClick;
                                bindingButtons.Add(rightButton);

                                Button attackButton = new Button("Attack", "Attack = " + keyBindings.attack, font, position + new Vector2(0, 6 * height), Button.Align.Center);
                                attackButton.mouseClick += attackButtonClick;
                                bindingButtons.Add(attackButton);

                                titleFont = content.Load<SpriteFont>("titleFont");
                                titleText = "Key Bindings";
                                titleSize = titleFont.MeasureString(titleText);
                                titlePosition = new Vector2((screenWidth - titleSize.X) / 2, titleSize.Y);

                            }

                            public void clickBack(object sender, MouseButtonArgs e)
                            {
                                if (popup == null)
                                {
                                    GameController.PlayMenuSound();

                                    //set keybindings back to ones in xml file when return to main menu, this undos changes after user changes bindings without saving
                                    ReadBindingsXML();
                                    gameController.SetKeyBindings();
                                    gameController.currentScreen = GameController.ScreenStates.mainMenu;
                                }
                            }

                            public void clickSave(object sender, MouseButtonArgs e)
                            {
                                if (popup == null)
                                {
                                    GameController.PlayMenuSound();

                                    //saves bindings to xml, then loads it back and applies keybindings to game
                                    SaveBindingsXML();
                                    ReadBindingsXML();
                                    gameController.SetKeyBindings();
                                    gameController.currentScreen = GameController.ScreenStates.mainMenu;
                                }
                            }

                            public void clickDefaults(object sender, MouseButtonArgs e)
                            {
                                if (popup == null)
                                {
                                    GameController.PlayMenuSound();

                                    keyBindings.up = keyBindings.defaultUp;
                                    keyBindings.down = keyBindings.defaultDown;
                                    keyBindings.left = keyBindings.defaultLeft;
                                    keyBindings.right = keyBindings.defaultRight;
                                    keyBindings.attack = keyBindings.defaultAttack;

                                    RefreshBindingsText();
                                    SaveBindingsXML();
                                }
                            }

                            public void upButtonClick(object sender, MouseButtonArgs e)
                            {
                                createPopup("Move Up");
                            }

                            public void downButtonClick(object sender, MouseButtonArgs e)
                            {
                                createPopup("Move Down");
                            }

                            public void leftButtonClick(object sender, MouseButtonArgs e)
                            {
                                createPopup("Move Left");
                            }

                            public void rightButtonClick(object sender, MouseButtonArgs e)
                            {
                                createPopup("Move Right");
                            }

                            public void attackButtonClick(object sender, MouseButtonArgs e)
                            {
                                 createPopup("Attack");
                            }

                            public void createPopup(string text)
                            {
                                if (popup == null)
                                {
                                    popup = new Popup(text, font, spriteBatch, graphics.GraphicsDevice, (int)screenWidth, (int)screenHeight, (int)screenHeight / 5, new Color(0, 0, 0, 100), new Color(255, 255, 255, 255), this);
                                    GameController.PlayMenuSound();
                                }
                            }

                            public void setBinding(KeybindPlayerActions action, Keys key)
                            {
                                //check if binding isn't being used already
                                if (key == keyBindings.up && action != KeybindPlayerActions.up)
                                    popup.setText(key.ToString() + " is being used for 'Move Up");
                                else if (key == keyBindings.down && action != KeybindPlayerActions.down)
                                    popup.setText(key.ToString() + " is being used for 'Move Down'");
                                else if (key == keyBindings.left && action != KeybindPlayerActions.left)
                                    popup.setText(key.ToString() + " is being used for 'Move Left'");
                                else if (key == keyBindings.right && action != KeybindPlayerActions.right)
                                    popup.setText(key.ToString() + " is being used for 'Move Right'");
                                else if (key == keyBindings.attack && action != KeybindPlayerActions.attack)
                                    popup.setText(key.ToString() + " is being used for 'Attack'");
                                else if (key == Keys.Escape)
                                    popup.setText(key.ToString() + " cannot be used");
                                else
                                {
                                    switch (action)
                                    {
                                        case KeybindPlayerActions.up:
                                            keyBindings.up = key;
                                            break;
                                        case KeybindPlayerActions.down:
                                            keyBindings.down = key;
                                            break;
                                        case KeybindPlayerActions.left:
                                            keyBindings.left = key;
                                            break;
                                        case KeybindPlayerActions.right:
                                            keyBindings.right = key;
                                            break;
                                        case KeybindPlayerActions.attack:
                                            keyBindings.attack = key;
                                            break;
                                    }
                                    popup = null;
                                    GameController.PlayMenuSound();
                                }
                                RefreshBindingsText();
                            }

                            public void RefreshBindingsText()
                            {
                                foreach (Button button in bindingButtons)
                                {
                                    switch (button.name)
                                    {
                                        case "Move Up":
                                            button.setText("Move Up = " + keyBindings.up);
                                            break;
                                        case "Move Down":
                                            button.setText("Move Down = " + keyBindings.down);
                                            break;
                                        case "Move Left":
                                            button.setText("Move Left = " + keyBindings.left);
                                            break;
                                        case "Move Right":
                                            button.setText("Move Right = " + keyBindings.right);
                                            break;
                                        case "Attack":
                                            button.setText("Attack = " + keyBindings.attack);
                                            break;
                                    }
                                }
                            }

                            public void SaveBindingsXML()
                            {
                                try
                                {
                                    StreamWriter sw = new StreamWriter("Content/KeyBindings.xml");
                                    XmlSerializer xml = new XmlSerializer(typeof(KeyBindings));
                                    xml.Serialize(sw, keyBindings);
                                } catch (Exception e)
                                {
                                    Console.WriteLine("EXCEPTION: " + e.Message);
                                }
                            }

                            public KeyBindings ReadBindingsXML()
                            {
                                try
                                {
                                    using (StreamReader reader = new StreamReader("Content/KeyBindings.xml"))
                                        return (KeyBindings)new XmlSerializer(typeof(KeyBindings)).Deserialize(reader.BaseStream);
                                }
                                catch (Exception e)
                                {
                                    Console.WriteLine("ERROR: File could not be read!");
                                    Console.WriteLine("Exception Message: " + e.Message);
                                }
                                return null;
                            }

                            public void UnloadContent()
                            {
                                content.Unload();
                            }

                            public void Update(GameTime gameTime, MouseState mouse)
                            {
                                foreach (Button button in menuButtons)
                                    button.Update();
                                foreach (Button button in bindingButtons)
                                    button.Update();
                                if (popup != null)
                                    popup.Update(Keyboard.GetState());
                            }

                            public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
                            {
                                this.spriteBatch = spriteBatch;
                                //title
                                spriteBatch.DrawString(titleFont, titleText, titlePosition, Color.Black);

                                foreach (Button button in menuButtons)
                                    button.Draw(spriteBatch);
                                foreach (Button button in bindingButtons)
                                    button.Draw(spriteBatch);

                                if (popup != null)
                                    popup.Draw(spriteBatch);
                            }


                        }
                    }

                

What happens is quite simple. The player clicks on the action they want to rebind, and a "popup" appears telling them to press a key to bind that action. They aren't allowed to have a particular key be bound to more than one action, an error would be displayed if it is attempted. Once a suitable key is pressed, the system takes note of the key and displays it accordingly. It isn't saved until the player clicks "Save Bindings". If "Main Menu" or "Restore Defaults" is pressed, the changes aren't saved and would be reverted to what they were before, or the defaults depending on the button pressed.