Jump to content
  • Advertisement
  • entries
  • comments
  • views

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

Sign in to follow this  



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.

    [/font]typedef unsigned int LocationIndex;/// \brief Defines one location in an area and it's linksstruct AreaLocation{ std::string name; //!< The name of the location std::vector 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 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.

    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 commandsenum 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 forenum 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; 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.
Sign in to follow this  


Recommended Comments

There are no comments to display.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Advertisement

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!