Jump to content

  • Log In with Google      Sign In   
  • Create Account

FREE SOFTWARE GIVEAWAY

We have 4 x Pro Licences (valued at $59 each) for 2d modular animation software Spriter to give away in this Thursday's GDNet Direct email newsletter.


Read more in this forum topic or make sure you're signed up (from the right-hand sidebar on the homepage) and read Thursday's newsletter to get in the running!






A Complete Graphics-less Game #4 - World Map Test

Posted by Squared'D, in A Complete Graphics-less Game 02 June 2013 · 617 views

Beginning on the World Map

I had hoped to release this earlier, but other projects have held me up a bit. Also when I had first envisioned writing this, I thought that I would be able to write about my thought process while I was actively making decisions. That turned out to be too difficult as I change my mind a lot. So I decided that it would be better to make all of the decisions, code something up, and then document my findings here.

Lessons Learned from the last prototype
I learned a lot working on that prototype. I know, it wasn't anything special, but at least I have something working that I can test out. As I said in my last entry, the prototype was written using a more C-style and not C++. Why? I don't know. I guess even though I've been using C++ for years, I'm still more comfortable using C-style functions. So what did I learn from the prototype? If you take a look at the code, I'm sure you'll see a lot of redundancy. Also, as you move from place to place in the game, a new menu is always drawn. To me, I feel like I'm moving between pages. Each page has the following task:
  • draw a menu
  • wait for input
  • process input
Now, how will I implement this into a more robust world map system? In this game, the world map will be a collection of places connected by links. When I think about other aspects of the game, when the user enters towns, a similar system will be used. Because of this, I'll use more generic-style names to describe things.

The Area Map
First, I need to develop a way to story the area information. For this, I'll start by defining a location.
typedef unsigned int LocationIndex;

/// \brief Defines one location in an area and it's links
struct AreaLocation
{
	std::string name;			//!< The name of the location

	std::vector <LocationIndex> links;	//!< The links

	/// \brief Helper function to return the number of links in this location
	inline unsigned int GetNumLinks() const { return links.size(); }
};

I've defined the type LocationIndex because I feel it's clearer than sticking a bunch of "unsigned int"s everywhere. The AreaLocation struct is fairly simple. I contains the area name and a vector of the links. The helper function GetNumLinks() isn't needed, but it will help the code be clearer.

To hold all of this together, I made the CAreaMap class.
/// \brief Class for an area map
/// An Area map can represent all the places in a town or it can be used as a
/// higher level world map.
class CAreaMap
{
	public:
		/// \brief Constructor for CAreaMap
		/// This constructs a new CAreaMap taking the name as the argument
		/// param name[in] The name of the map
		CAreaMap(const std::string &name);

		// ~CAreaMap(void); // we don't need a destructor yet

		/// \brief Returns the name of the map
		const std::string &GetName() const { return m_area_name; }

		/// \brief Returns a location by index
		const AreaLocation &GetLocationByIndex(LocationIndex index) const
		{
			return m_locations[index]; // will crash on invalid index
		}

		/// \brief Returns the number of locations
		inline unsigned int GetNumLocations() const { return m_locations.size(); }

		/// \brief Adds a location to the map
		/// This will add a location to the map. The variable list is a list of ints,
		/// and are the indices for each location that this location links to.
		void AddLocation(std::string name, int num_links, ...);

	private:
		std::string m_area_name;
		std::vector <AreaLocation> m_locations;
};

So far, all of the type are simple so I don't feel that I have use for pointers at the moment. In general, I've been learning to avoid pointers and to use them only when necessary. I'm still in the beginning stages of this project so there's a big possibility for change later. Sometimes when working on projects alone, we want to code everything right the first time. But this isn't always possible. Without a strong framework, you may not even be able to test things properly. And if you make some code and wait for its dependencies to get coded or vice versa before testing, when you run into bugs, you may have a hard time figuring out where the bug is because you've waited so long to do any testing. That's why I'm a huge fan of making temporary code for testing.

Commands
Processing and distinguishing between user commands is very important. This will be a text based game and all commands will come in via a single keystroke. To do this, _getch() is a good function, but it has a problem. _getch() is a blocking function and won't return until the user presses a key. I never want to block the code waiting for a keystroke. Other things may need to be processes such as AI and AI agent should be able to move in and out of an area while you're thinking about your next action. The function _kbhit() can be used to check to see if a key is waiting in the queue. If _kbhit() returns true, then I can get the key using _getch().

I don't want to feed keystrokes directly into the system to be processed. I want them to be translated first. These are the enums and structs that I'll use to define commands.
/// \brief Enum of the possible user commands
enum UserCommandType
{
    Command_Null		= 0,
    Command_Invalid		= 1,
    Command_Move		= 2,
    Command_MoveLink		= 3,
    Command_Cancel		= 4,
    Command_Quit		= 5
};

/// \brief Enum detailing what kind of commands we're listening for
enum SubCommandMode
{
    Mode_Base	= 0,
    Mode_Moving	= 1
};

struct UserCommand
{
    UserCommandType cmd_type;
    int		    cmd_param;
};
The UserCommandType enum details the type of command the user has entered. I've also made the UserCommand struct because some commands need an extra parameter. But why the SubCommandMode enum? Remember before I said that I never want to block waiting for input. This enum will allow me to have sub-menus with different controls. For example, the user should press 'M' to move, but after that, the user should enter the link number or C to cancel. I don't want to block the program while the user thinks about his or her next location and I don't want to introduce another look so I'll use this enum so the program will know what kind of input the user is entering.

The World Map State
Even though this test project doesn't support multiple states yet, I'm going to start using this terminology now. Remember, I'm still in the early phases and don't want to introduce inheritance yet.
/// \brief Class for the world state
/// This class allows a user to be able to explore a world map as
/// defined in the CAreaMap class.
class CWorldMapState
{
    public:
        CWorldMapState(void);

        /// \brief Runs the main loop for this state
        void DoMainLoop();

    private:
        /// \brief Sets the world map to the default value
        void SetUpWorldMap();

        /// \brief Gets the next user command
        UserCommand GetNextUserCommand();

        /// \brief Show an error message on an invalid command
        void OnInvalidCommand();

        /// \brief process the current message
        /// \return true to redraw the current location
        bool OnProcessCommand(const UserCommand &user_cmd);

        /// \brief shows an area display on std::cout
        void DisplayAreaLocation(const AreaLocation &) const;

        /// \brief returns true if the world map is looking at a valid location
        bool isCurrentLocationValid() const;

        /// \brief returns the current location
        const AreaLocation &GetCurrentLocation() const;

        CAreaMap m_WorldMap;
        unsigned int m_CurLocation;

        SubCommandMode m_CommandMode;

};
Most of the methods are private, but may end up being protected after I add a state base class. The really class only has one public method and that is to run the main loop. The SetUpWorldMap() method is temporary and will be removed or changed drastically after I add loading the map from a file.

Here's the main loop. Sorry for the lack of comments:
void CWorldMapState::DoMainLoop()
{
    UserCommand user_cmd;

    if(isCurrentLocationValid())
    {
        // dislay the current location
        DisplayAreaLocation(GetCurrentLocation());

        do
        {
            // query for the next user command
            user_cmd = GetNextUserCommand();

            if(user_cmd.cmd_type != Command_Null)
            {
                // check for an invalid input message
                if(user_cmd.cmd_type == Command_Invalid)
                {
                    OnInvalidCommand();
                }
                else if (user_cmd.cmd_type != Command_Quit)
                {
                    if(OnProcessCommand(user_cmd))
                    {
                        DisplayAreaLocation(GetCurrentLocation());
                    }
                }
            }
        } while (user_cmd.cmd_type != Command_Quit);
    }
}
Every time through the loop the program queries for new input. If there's new input, it is sent to be processed.

The GetNextUserCommand() method checks to see if a key has been pressed and then turns that keystroke into a command.
UserCommand CWorldMapState::GetNextUserCommand()
{
    UserCommand new_command;
    new_command.cmd_type = Command_Invalid;

    if(_kbhit())
    {
        int key = _getch();
        if(m_CommandMode == Mode_Base)
        {
            switch(key)
            {
                case 'q':
                case 'Q':
                    new_command.cmd_type = Command_Quit;
                    break;
                case 'm':
                case 'M':
                    new_command.cmd_type = Command_Move;
                    break;
            }
        }
        else if(m_CommandMode == Mode_Moving)
        {
            if((key == 'c') || (key == 'C'))
            {
                // the user has pressed cancel
                new_command.cmd_type = Command_Cancel;
            }
            int max_link = (int)GetCurrentLocation().GetNumLinks();
            int link = key - '0'; // convert to an int
            if((link >= 0) && (link < max_link))
            {
                new_command.cmd_type = Command_MoveLink;
                // set the index as the param
                new_command.cmd_param = (int)GetCurrentLocation().links[link];
            }
        }
    }
    else
    {
        new_command.cmd_type = Command_Null;
    }

    return new_command;
}



I'll just show the two other important methods here just so you can take a look. You can download the source to see the complete project.
bool CWorldMapState::OnProcessCommand(const UserCommand &user_cmd)
{
    if(user_cmd.cmd_type == Command_Move)
    {
        std::cout << "Enter link (0 - " << GetCurrentLocation().GetNumLinks() - 1 << ") (C - Cancel)" << std::endl;

        // change the command mode so we'll process user commands as links
        m_CommandMode = Mode_Moving;
    }
    else if(user_cmd.cmd_type == Command_MoveLink)
    {
        m_CommandMode = Mode_Base;
        m_CurLocation = (unsigned int)user_cmd.cmd_param;
        std::cout << "Moving to " << GetCurrentLocation().name << std::endl;
        return true; // redraw the current location
    }
    else if(user_cmd.cmd_type == Command_Cancel)
    {
        m_CommandMode = Mode_Base;
        return true; // redraw the current location
    }

    // do not redraw the current location
    return false;
}

void CWorldMapState::DisplayAreaLocation(const AreaLocation &location) const
{
    std::cout << std::endl << "==========================================" << std::endl;
    std::cout << "==========================================" << std::endl;
    // write the name
    std::cout << "Now in " << location.name << std::endl;

    // write the links
    std::cout << std::endl << "Links:------------------------------------" << std::endl;
    for(unsigned int i = 0; i < location.GetNumLinks(); ++i)
    {
        unsigned int link_index = location.links[i];

        if(link_index < m_WorldMap.GetNumLocations()) // make sure the location is valid
        std::cout << i << ") " << m_WorldMap.GetLocationByIndex(link_index).name << std::endl;
    }

    // write the menu
    std::cout << std::endl << "Commands:---------------------------------" << std::endl;
    std::cout << "M) Move" << std::endl;
    std::cout << "Q) Quit" << std::endl;
    std::cout << "==========================================" << std::endl;
}

If you have any questions about this entry you can leave a comment.

Note about the source
I develop using Microsoft Visual Studio 2010 Professional so if you have any problems compiling the code on your system let me know.

Attached Files






Recent Comments

December 2014 »

S M T W T F S
 123456
78910111213
14151617181920
2122232425 26 27
28293031   
PARTNERS