Skip to main content
GameDev.net gamedev.net
🔒 Locked

A question of design.

Started by Silent Dragon Aug 25, 2008 at 4:51 PM 4 replies 900+ views
Original Post
Silent Dragon
Silent Dragon
Hi all, I've been working on an RPG in C# and XNA (just so you understand the syntax) and have been storing my game details in XML files. The main problem I have is that my design doesn't seem all that great. Basically id numbers are used to associate everything, sort of like a database, but then anything that uses the ID's need access to the 'database'. I think an example will best suit this. stats.xml

<?xml version="1.0" encoding="utf-8" ?>
<stats>
	<stat id="1">
		<name>Health Points</name>
		<shortname>HP</shortname>
		<description>The amount of health you have. Reaching 0 will cause death.</description>
	</stat>
	<stat id="2">
		<name>Mana Points</name>
		<shortname>MP</shortname>
		<description>The amount of magic you can cast.</description>
	</stat>
	<stat id="3">
		<name>Strength</name>
		<shortname>Str</shortname>
		<description>Your physical condition.</description>
	</stat>
</stats>

players.xml

<?xml version="1.0" encoding="utf-8" ?>
<players>
	<player id="1">
		<name>Jim Amaranthine</name>
		<menuimage file="maps/tiles/tile2" />
		<animations file="sprites/knt1">
			<animation name="UP">
				<frame x="0" y="0" width="32" height="32" />
				<frame x="32" y="0" width="32" height="32" />
			</animation>
			<animation name="DOWN">
				<frame x="64" y="0" width="32" height="32" />
				<frame x="96" y="0" width="32" height="32" />
			</animation>
			<animation name="LEFT">
				<frame x="128" y="0" width="32" height="32" />
				<frame x="160" y="0" width="32" height="32" />
			</animation>
			<animation name="RIGHT">
				<frame x="192" y="0" width="32" height="32" />
				<frame x="224" y="0" width="32" height="32" />
			</animation>
		</animations>
		<levels>
			<level id="1">
				<stats>
					<stat id="1" increaseby="50" />
					<stat id="2" increaseby="25" />
				</stats>
			</level>
		</levels>
		<currentlevel>1</currentlevel>
	</player>
</players>

Stats ('Database' for the stats.xml - I realise it doesn't load in the XML file atm, but its just as a test)

using System;
using System.Collections.Generic;
using System.Text;

namespace TileEngine
{
    public class Stats
    {
        private Dictionary<int, Stat> stats;

        public Stats(String statXMLFilename)
        {
            stats = new Dictionary<int, Stat>();

            // TODO: Load stats from the XML File
            stats.Add(1, new Stat("Health", "HP", "HEALTH YEA"));
            stats.Add(2, new Stat("Mana", "MP", "OH It's MANAAAA"));
        }

        public Stat getStat(int id)
        {
            if (stats.ContainsKey(id))
                return stats[id];

            return null;
        }

        public int getId(Stat stat)
        {
            return 0;
        }
    }
}

Stat.cs

using System;
using System.Collections.Generic;
using System.Text;

namespace TileEngine
{
    public class Stat
    {
        private String name;
        private String shortname;
        private String description;

        public String Name
        {
            get
            {
                return name;
            }
        }

        public String ShortName
        {
            get
            {
                return shortname;
            }
        }

        public String Description
        {
            get
            {
                return description;
            }
        }

        public Stat(String name, String shortname, String description)
        {
            this.name = name;
            this.shortname = shortname;
            this.description = description;
        }
    }
}

Players.cs ('Database' for the players)

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace TileEngine
{
    public class Players
    {
        private Dictionary<int, Player> players;
        private Stats stats;

        public Players(ContentManager content, String XMLFilename, Stats stats)
        {
            // TODO: Load players from the XML File
            players = new Dictionary<int, Player>();
            this.stats = stats;

            XmlTextReader xmlReader;

            try
            {
                xmlReader = new XmlTextReader(XMLFilename);

                while (xmlReader.Read())
                {
                    switch (xmlReader.NodeType)
                    {
                        case XmlNodeType.Element:
                            {
                                if (xmlReader.Name.Equals("player"))
                                {
                                    Player player = new Player(stats);
                                    int id = int.Parse(xmlReader.GetAttribute("id"));

                                    //String name = "";
                                    //Dictionary<String, FrameAnimation> animations = new Dictionary<string,FrameAnimation>();
                                    //Texture2D animationTexture = null;
                                    //Texture2D menuImage = null;

                                    while (xmlReader.Read())
                                    {
                                        if (xmlReader.NodeType == XmlNodeType.EndElement && xmlReader.Name.Equals("player"))
                                        {
                                            break;
                                        }

                                        if (xmlReader.NodeType == XmlNodeType.Element)
                                        {
                                            if (xmlReader.Name.Equals("name"))
                                            {
                                                player.Name = xmlReader.ReadString();
                                            }
                                            else if (xmlReader.Name.Equals("menuimage"))
                                            {
                                                String filename = xmlReader.GetAttribute("file");

                                                if (!String.IsNullOrEmpty(filename))
                                                    player.MenuImage = content.Load<Texture2D>(filename);
                                            }
                                            else if (xmlReader.Name.Equals("animation"))
                                            {
                                                String animationName = xmlReader.GetAttribute("name");
                                                List<Rectangle> rects = new List<Rectangle>();

                                                while (xmlReader.Read())
                                                {
                                                    if (xmlReader.NodeType == XmlNodeType.EndElement && xmlReader.Name.Equals("animation"))
                                                    {
                                                        break;
                                                    }

                                                    if (xmlReader.Name.Equals("frame"))
                                                    {
                                                        int posX, posY, width, height;

                                                        posX = int.Parse(xmlReader.GetAttribute("x"));
                                                        posY = int.Parse(xmlReader.GetAttribute("y"));
                                                        width = int.Parse(xmlReader.GetAttribute("width"));
                                                        height = int.Parse(xmlReader.GetAttribute("height"));

                                                        rects.Add(new Rectangle(posX, posY, width, height));
                                                    }

                                                } 

                                                player.AddAnimation(animationName, new FrameAnimation(rects));
                                            }
                                            else if (xmlReader.Name.Equals("animations"))
                                            {
                                                String filename = xmlReader.GetAttribute("file");

                                                if (!String.IsNullOrEmpty(filename))
                                                    player.AnimationTexture = content.Load<Texture2D>(filename);
                                            }
                                            else if (xmlReader.Name.Equals("level"))
                                            {
                                                String level = xmlReader.GetAttribute("id");
                                                List<StatValue> levelStats = new List<StatValue>();

                                                while (xmlReader.Read())
                                                {
                                                    if (xmlReader.NodeType == XmlNodeType.EndElement && xmlReader.Name.Equals("level"))
                                                    {
                                                        break;
                                                    }

                                                    if (xmlReader.Name.Equals("stat"))
                                                    {
                                                        int statId = int.Parse(xmlReader.GetAttribute("id"));
                                                        int value = int.Parse(xmlReader.GetAttribute("increaseby"));

                                                        levelStats.Add(new StatValue(statId, value));
                                                    }

                                                }

                                                player.AddLevel(int.Parse(level), levelStats);
                                            }
                                            else if (xmlReader.Name.Equals("currentlevel"))
                                            {
                                                player.setLevel(int.Parse(xmlReader.ReadString()));
                                            }
                                        }
                                    }

                                    players.Add(id, player);
                                }

                                break;
                            }
                    }
                }
            }
            catch (XmlException xmle)
            {
                Console.WriteLine(xmle.Message);
            }
        }

        public Player getPlayer(int id)
        {
            if (players.ContainsKey(id))
                return players[id];
            else 
                return null;
        }
    }
}

As you can see if you bothered to read through all that jibberish is that each 'Player' is given the pointer to the 'Stats' database. It feels to me like this is a somewhat bad design, can anyone suggest any kind of improvement? Thank you for even just reading all this, let alone helping -SD
Shakedown
Shakedown
First off, you probably want to separate anything regarding XML from the Player class. What if you decide you don't want to support XML anymore? You're forced to change your Player class when really the Player class should be concerned with interacting with the world, not with dealing with XML. To solve this, remove all things XML from Player and make some sort of creation process that will read XML and create Players from the XML.

Now, with your XML, I'd suggest combining your stat and name elements, like so:

<stats> <stat name="Health" id="1">  <shortname>HP</shortname>  <description>...</description> </stat></stats>


This way, the XML makes a bit more sense and removes an extra layer of indirection when searching for a particular stat.

Quote:
...each 'Player' is given the pointer to the 'Stats' database. It feels to me like this is a somewhat bad design...


You're right. What does a Player care about a database? Like I said earlier, remove the Stats database from the Player class and instead give it to a creation class. This creation class can deal with the XML and databases to create the Players.

That should point you in the right direction, good luck!
Silent Dragon
Silent Dragon
Thank you for your reply!

Quote:
Original post by Shakedown
First off, you probably want to separate anything regarding XML from the Player class. What if you decide you don't want to support XML anymore? You're forced to change your Player class when really the Player class should be concerned with interacting with the world, not with dealing with XML. To solve this, remove all things XML from Player and make some sort of creation process that will read XML and create Players from the XML.

I half did that already, with the 'Players' class which loads in the XML and creates the 'Player' class instances, but I do see your point, and I should think about that further

Quote:

Now, with your XML, I'd suggest combining your stat and name elements, like so:

*** Source Snippet Removed ***

This way, the XML makes a bit more sense and removes an extra layer of indirection when searching for a particular stat.

Totally agree. Actually I could probably simply remove 'id' and have the 'name' act as the unique identifier for the Stat.

Quote:

You're right. What does a Player care about a database? Like I said earlier, remove the Stats database from the Player class and instead give it to a creation class. This creation class can deal with the XML and databases to create the Players.

That should point you in the right direction, good luck!


You're right, the Player shouldn't care about the database. The reason this whole 'oh no my design sucks' thought came about when I was trying to design my in-game menu, where it will display the stats of each player. Now, the way it currently works is that it gets the 'Stat' instance from the 'Stats' (the database) that is contained in the 'Player' class. The only other way I can see of it working is passing the 'Stats' (the database) to the menu, and only store the id's of the stats inside the player. Does that make any sense? Haha.

This still doesn't seem like a decent design really, but I guess it's probably better than it is right now.

Thanks,
SD
Shakedown
Shakedown
Quote:

You're right, the Player shouldn't care about the database. The reason this whole 'oh no my design sucks' thought came about when I was trying to design my in-game menu, where it will display the stats of each player. Now, the way it currently works is that it gets the 'Stat' instance from the 'Stats' (the database) that is contained in the 'Player' class. The only other way I can see of it working is passing the 'Stats' (the database) to the menu, and only store the id's of the stats inside the player. Does that make any sense? Haha.


Well, first things first: minimize database queries as much as possible. You should be able to reduce database queries to this: once when your game begins to load up any saved data (as well as static information) and once when your game ends to save any in-game data.

If your menu is to display the stats particular to a player, there is no need to query the database if the player contains their stats (which they should, and the database should contain their stats as of the last save). If your in-game menus are to display information that is specific to the current player, then your menus should ask the player for any values the menu cares about.
Silent Dragon
Silent Dragon
Quote:
Original post by Shakedown
Well, first things first: minimize database queries as much as possible. You should be able to reduce database queries to this: once when your game begins to load up any saved data (as well as static information) and once when your game ends to save any in-game data.


Well, that sounds reasonable. So it should work something like this..?

LoadPlayers(Stats statsDatabase){...     Player newPlayer = new Player()     while(LoadingPlayerFromXML)     {          ...          foreach(int newStatId in StatsInXML)          {               ...               newPlayer.AddStat(statsDatabase.getStat(newStatId), statValue);               ...          }          ...     }...}


Quote:

If your menu is to display the stats particular to a player, there is no need to query the database if the player contains their stats (which they should, and the database should contain their stats as of the last save). If your in-game menus are to display information that is specific to the current player, then your menus should ask the player for any values the menu cares about.


This would then allow the menu to get to the Stat (the data structure, containing the name, shortname, description etc..) through the Player. I suppose this would be better because neither the Menu, nor the Player would need access to the 'database' class.

Is this that you meant?

-SD

Shakedown
Shakedown
Quote:

Well, that sounds reasonable. So it should work something like this..?


Yep, that's the idea. Make sure that you're able to load only a single player as well so when a player dies you don't have to load up everybody.

Quote:

This would then allow the menu to get to the Stat (the data structure, containing the name, shortname, description etc..) through the Player. I suppose this would be better because neither the Menu, nor the Player would need access to the 'database' class.

Is this that you meant?


I would suggest hiding the Stat structure from the menu as well. All the menu wants to do is display some numbers next to some strings. Perhaps simply adding methods in the Player class like getHealth(). This way the Stat structure will be relevant only to a Player, which is logically correct (Menus and Stats? More like Menus and numbers). This will also remove coupling between Menus and Stats. If you wanted to change the Stat structure you would only have to touch the Player class, not the Menu class.

Plus, if you've got an RPG where players have 100 stats, do you really want to be passing around that large structure only to get the HP and Mana?

I realize that there's also problems with having to create explicit methods (i.e. getHealth(), getMana(), getTheOther98Attributes()), but I do think you should stay away from the Menu class pulling out the Stat class from the Player.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.