Welcome, Guest. Please login or register.

Login with username, password and session length

 
Advanced search

1411581 Posts in 69386 Topics- by 58445 Members - Latest Member: Mansreign

May 05, 2024, 05:46:56 PM

Need hosting? Check out Digital Ocean
(more details in this thread)
TIGSource ForumsDeveloperTechnical (Moderator: ThemsAllTook)Is using c++ maps to store data sane?
Pages: [1] 2
Print
Author Topic: Is using c++ maps to store data sane?  (Read 3606 times)
Massena
Level 4
****


Satisfied.


View Profile
« on: July 10, 2009, 03:28:32 PM »

For long I have struggled on how to keep all my game data in a place the whole program can access it, without succumbing to the inherent evils of global variables.

A few days ago, I had an idea. I have a game class. This game class has a lot of maps. A map for ints, a map for floats, a map for strings, a map for sprites, etc.

Every map consists of a key which is a string, and a piece of data.

There is also a map that contains pointers to functions, each of which is a state. Every of these functions is of the type: string Example_state(Game* game); and when they are called they are given as arguments a pointer to the game class that called them (this) and they return a string that is the key for the next state to be called.

I'm using all those maps so every time I start a new project I don't have to create a new "game" class with the data that project needs.

I guess my question is whether this abusing of maps will cause any significant slowdown in my program or whether there is any better way of doing what I'm doing (creating a flexible, robust state machine that can handle data).
Logged

Overkill
Level 3
***


Andrew G. Crowell


View Profile WWW
« Reply #1 on: July 10, 2009, 03:52:05 PM »

For one, global state is a sign of bad design, and should be avoided at all costs. Having std::maps of all this data, which is accessed throughout the entire program, is just as bad, if not worse.

And arguably, you're still making the data "global". Just now, you're doing it through a god class (also bad), which provides the same features that global variables would have been able to provide.

Coupling is a serious code smell. Making your code rely on global state (whether through language mechanism or through something that "gets around" global variables) means that your code is bound to be unmaintainable. This is because there's probably no knowing where the global state will be accessed and manipulated. This means it'll be hell to debug, and will most likely not be threadsafe.

In addition, std::map is SLOW compared to globals, giving O(log n) complexity for all operations (meaning that time to find a particular element in the map it increases logarithmically as it grows). When you compare it with normal global variables, which are O(1) (constant time) lookup, you run into some serious overhead. Especially if you ever use these std::map variables in a tight loop.

In addition, using string lookups, means the compiler can't help you in any way if you make a typo. You are now making runtime errors when you make a mistake. Why not just use a scripted language if you're throwing away the most meaningful things: compile-time checking, and speed.

What REALLY needs to be done is something much better, which is making your code modular. This means designating specific responsibilities to certain chunks of code, and they adhere to a well designed interface to reduce coupling, making your code more cohesive. Modular code is much more reusable.

Global state sometimes can't be fully avoided in games, but you want to avoid it as much as possible so that you can delegate tasks to other components of your code, and not have to worry about some completely unrelated segment of your game blowing it up.

Sorry for the rant, I hope this helps you.
Logged

Massena
Level 4
****


Satisfied.


View Profile
« Reply #2 on: July 10, 2009, 04:03:30 PM »

Realizing that I couldn't get rid of global state I at least tried making a god class that wouldn't need rewriting every time I decide to change the name of some variable, for example if god class is as such:
Code:
class Game {
   private:
      int player_x, player_y, player_health;
   public:
      functions
      functions
};

If my next awesome hypothetical game needs no variable player_health I'll have to go down in to my framework and change code around. What I mean is that I would like to completely isolate the generic engine part and the specific game part of the code. Is this possible?
Logged

Zaphos
Guest
« Reply #3 on: July 10, 2009, 04:09:30 PM »

You could encapsulate the game specific data in an object of some generic class, which you subclass for each new game ...

edit: or, similarly, you could template the game class on the custom data type.
Logged
BorisTheBrave
Level 10
*****


View Profile WWW
« Reply #4 on: July 10, 2009, 04:11:51 PM »

Stuff like player health should be encapsulated into a player object. Encapsulation (making things only visible to the smallest logical scope), is they key to not breaking things when something changes.

Frequently there is a base Object (aka Actor, sometimes) that represents a game object. Then you'd subclass it for different sorts, including one for Player, and it's there you'd put health.

But it's a huge debate over how to organize code for games - a lot of commonly held truths don't apply for games. In answer to your original question, maps themselves are not really evil. But if you aren't using the fact that the keys of the map are sorted, swap the map for a hash_map (aka unordered_map), which has more or less O(1) performance, and practical improvements too.
Logged
Overkill
Level 3
***


Andrew G. Crowell


View Profile WWW
« Reply #5 on: July 10, 2009, 04:13:38 PM »

Realizing that I couldn't get rid of global state I at least tried making a god class that wouldn't need rewriting every time I decide to change the name of some variable, for example if god class is as such:
Code:
class Game {
   private:
      int player_x, player_y, player_health;
   public:
      functions
      functions
};

If my next awesome hypothetical game needs no variable player_health I'll have to go down in to my framework and change code around. What I mean is that I would like to completely isolate the generic engine part and the specific game part of the code. Is this possible?

Ohhh. I see what you mean. Sorry for being abrasive.

I am not sure that's easy to do in C++. std::map would be slowish though. In that particular hypothetical example, I'd have a Player class which whould keep all of the data regarding a player in one nice spot. And meanwhile, the framework would just hold onto one "Player" object, that it generally needs to know very little about. Sadly since your game changes, I'd say the easiest option is really to just update code. Then if you need a player health attribute you add the code in that uses that.

Granted, I realize this can't be done for everything, but I dunno. I'd say adding onto the data definitions in your code is still easier than tacking on "optional" functionality to a framework you're making. It's quicker to debug, and although tedious, I doubt you'll find too much savings with a string table instead.

These are just my experiences though.
Logged

Average Software
Level 10
*****

Fleeing all W'rkncacnter


View Profile WWW
« Reply #6 on: July 10, 2009, 04:56:27 PM »

Realizing that I couldn't get rid of global state I at least tried making a god class that wouldn't need rewriting every time I decide to change the name of some variable, for example if god class is as such:
Code:
class Game {
   private:
      int player_x, player_y, player_health;
   public:
      functions
      functions
};

If my next awesome hypothetical game needs no variable player_health I'll have to go down in to my framework and change code around. What I mean is that I would like to completely isolate the generic engine part and the specific game part of the code. Is this possible?

I have an approach to this problem that seems to work pretty well.  I have class that represents each state, and they all have a method called Process() that does whatever that state does, and then returns a pointer to the next state (most often they just return this;).  I have a tiering of these logic objects that goes like so:

At the top is a class called Logic.  Logic contains everything a state will need that is NOT specific to any one game.  This is all your generic stuff that every game needs, this is also where the Process method is declared.  It looks somethin like this:

Code:
class Logic
{
public:
    virtual Logic *Process() = 0;

private:
    // Stuff every logic state in every possible game needs to have.
};

Under that is a logic class that contains all the specific stuff that every state in this particular game needs to have, this is where you would store your game wide data as statics.  Assume I'm making a game called Donut King, I might have this:

Code:
class DonutKingLogic : public Logic
{
public:
    // Stuff

protected:
    // Stuff for every state in Donut King, "globals" if you will.
    static int player_health;
    static const volatile unsigned long *const volatile long_declaration;
};

Then you have your actual states for the game, like this:

Code:
class TitleScreen : public DonutKingLogic
{
public:
    Logic *Process();  // Magic happens here!

private:
    // State specific data.
};

This scheme avoids most of the problems you're having, and allows me to keep a nice generic framework that I don't have to keep rewriting.
Logged



What would John Carmack do?
nihilocrat
Level 10
*****


Full of stars.


View Profile WWW
« Reply #7 on: July 10, 2009, 06:25:01 PM »

The developer time you save using an std::map versus using something else will outweigh any performance loss. Premature optimization is the yadda yadda yadda Wizard
Logged

Zaphos
Guest
« Reply #8 on: July 10, 2009, 07:46:01 PM »

This isn't so much about optimization as it is about structuring your code in a way that suits the language and the type system.  Writing code in a way such that the compiler can verify details at compile time, instead of at run time, could save you developer time in the long run.  And if all your variables were accessed by globalMap["varname"] or similar, that could be pretty hard to go back and fix later, so it's not premature to worry about at the start of a project.

Also, hash_map is as easy to use as map is, if your compiler supports it.  (gcc and msvc do, I think, but it's not in the official standard?)
Logged
yesfish
Guest
« Reply #9 on: July 10, 2009, 10:48:16 PM »

Maps are quite fast but, "A map for ints, a map for floats, a map for strings, a map for sprites, etc." - that's crazy. Why would you want to do that?

I like Average Software's solution.

Also there's a design pattern called singleton, for like the graphics manager or global game functionality.

excuse the bad code.
Code:
class singleton
{
private:
singleton();
singleton(&singleton); //um whatever the copy constructor is I haven't used c++ properly in a while.
static singleton* pinst = 0;
public:
static singleton* instance()
{
if(pinst == 0)
pinst = new singleton();
return pinst;
}
};
So you use it by calling

singleton* s = singleton::instance();
s->somevar = 123;
s->somemethod();

or if you're lazy

singleton::instance()->somemethod();
Logged
Average Software
Level 10
*****

Fleeing all W'rkncacnter


View Profile WWW
« Reply #10 on: July 11, 2009, 07:32:41 AM »

Also, hash_map is as easy to use as map is, if your compiler supports it.  (gcc and msvc do, I think, but it's not in the official standard?)

I believe it's part of TR1, but I'm pretty sure it will be an official part of C++09, but I think they're giving it a different name.

Quote from: yesfish
Also there's a design pattern called singleton, for like the graphics manager or global game functionality.

Unless you need something that a class provides (inheritance, polymorphism, lazy instantiation) I still think that singletons are silly in C++.   I continue to advocate a simple namespace with local data.  This is my "sprite manager" for my current project.  It's in Ada, but the idea still applies.

Spec file:
Code:
with Drawables;

use Drawables;

-- Package for managing active Drawables.
package Renderer is
    -- The main window has three drawing areas, the main
    -- play area, the map, and the status display.
    type Area is (Main, Map, Status);

    -- Add a Drawable to the specified area.
    procedure Add_Drawable(D: in Drawable'Class;
                           Location: in Area := Main);
    -- Remove a Drawable from the specified area.
    procedure Remove_Drawable(D: in Drawable'Class;
                              Location: in Area := Main);
    -- Draw all registered Drawables in all three areas.
    procedure Draw;
    -- Delete all Drawables.
    procedure Purge_Drawables;
end Renderer;

Body file:
Code:
with Ada.Containers.Doubly_Linked_Lists;
with Main_Window;
with Graphics;

use Ada.Containers;

package body Renderer is
    -- Internal lists of Drawables.
    package Drawable_Lists is new Doubly_Linked_Lists(NN_Drawable_Access);
    use Drawable_Lists;

    -- There is one list of Drawables for each area.
    Drawables: array (Area) of List;

    -- Insert a Drawable into an area.
    procedure Add_Drawable(D: in Drawable'Class;
                           Location: in Area := Main) is
    begin
        Drawables(Location).Append(D'Unchecked_Access);
    end Add_Drawable;

    -- Remove a Drawable from an area.
    procedure Remove_Drawable(D: in Drawable'Class;
                              Location: in Area := Main) is
        Position: Cursor := Drawables(Location).Find(D'Unchecked_Access);
    begin
        if Position /= No_Element then
            Drawables(Location).Delete(Position);
        end if;
    end Remove_Drawable;

    -- Draw all three areas.
    procedure Draw is
        procedure Iterator(Position: in Cursor) is
        begin
            Element(Position).Draw;
        end Iterator;
    begin
        -- First draw the main play area.
        Graphics.Set_Target(Main_Window.Play_Area_Buffer,
                            Main_Window.Play_Area_Context);
        Drawables(Main).Iterate(Iterator'Access);
       
        -- Then draw the map area.
        Graphics.Set_Target(Main_Window.Map_Area_Buffer,
                            Main_Window.Map_Area_Context);
        Drawables(Map).Iterate(Iterator'Access);
       
        -- Finally draw the status display.
        Graphics.Set_Target(Main_Window.Status_Area_Buffer,
                            Main_Window.Status_Area_Context);
        Drawables(Status).Iterate(Iterator'Access);

        -- Swap the backbuffers.
        Main_Window.Swap_Buffers;
    end Draw;

    -- Delete all registered Drawables.
    procedure Purge_Drawables is
    begin
        for X in Area loop
            Drawables(X).Clear;
        end loop;
    end Purge_Drawables;
end Renderer;

No need for extra classes or that GetInstance crap.  Don't make things more complicated than they need to be.
Logged



What would John Carmack do?
mcc
Level 10
*****


glitch


View Profile WWW
« Reply #11 on: July 11, 2009, 12:06:23 PM »

So... I think of the global hash idea as not particularly so bad-- I'd worry whether it would introduce coding-awkwardness or performance issues once the game became very complex, but I wouldn't dismiss it out of hand. I personally would go about it in a different way though.

You discuss having a "bunch of" maps, distinguished by type. So a map<int> intmap, a map<float> floatmap, etc. This is of course the way that the C++ template system makes easiest. It's not necessary, though. Why not just have one big map that contains everything? So you could have a map<Item>, where Item is some sort of union.

The nice thing about this approach would be that it opens up the possibility of having a key in your map<Item> whose return value is another map<Item>. If you do this, you have just re-introduced the idea of local state-- because you can think of each nested map<Item> as a state scope. So for example instead of saying global_int_map["player_health"] you might say something like global_map["player"].m["health"].i. That's a little ugly, but it allows for you to do things like write a function that operates on a map of the sort stored in global_map["player"], then later train that function on say global_map["player2"].

Now given I'm not sure any of this really superior to traditional object-oriented approaches? But the thing that stands out to me as intriguing about the "keep everything in maps" approach is that because it would mean your data store maps well to the kind of "JSON-y" model used in most dynamic languages, it seems like it would be almost trivial later to plug your code into integration with python or javascript or some other scripting language, or XML or JSON or Couch or any number of other serialization methods, or a web service, or...
Logged

My projects:<br />Games: Jumpman Retro-futuristic platforming iJumpman iPhone version Drumcircle PC+smartphone music toy<br />More: RUN HELLO
mcc
Level 10
*****


glitch


View Profile WWW
« Reply #12 on: July 11, 2009, 12:20:43 PM »

Incidentally I'll note that where needed I personally prefer to use hash_map rather than map, although I'm not quite sure if I have a good reason for this or whether it's superstition.

What exactly is STL "map" using internally, anyway? Something like a balanced tree? Or does it vary depending on implementation? I've never been able to figure that out.
Logged

My projects:<br />Games: Jumpman Retro-futuristic platforming iJumpman iPhone version Drumcircle PC+smartphone music toy<br />More: RUN HELLO
Zaphos
Guest
« Reply #13 on: July 11, 2009, 12:58:01 PM »

Wiki says: "The usual implementation is a self-balancing binary search tree (but any other data structure that respects the complexity constraints can be used, like a skip list)."

(since the standard doesn't require a specific data structure, just complexity constraints, in case some better data structure comes along in the future.)
Logged
Snakey
Level 2
**


View Profile WWW
« Reply #14 on: July 11, 2009, 01:30:53 PM »

It seems like you need to consider structuring your key data components in more ways than one. It's entirely possible that you may need several structures which represent your objects in different ways.

You may need a tree like structure which represents the heirarchy of your game objects, who belongs to who and so forth.

You may need a spatial structure which represents the spatial positioning of your game objects, where things are in the world.

You may need a flat structure which represents all of the game objects in the world.

You may need a combination of two or even all of them. All of these structures simply organise data in a way that makes sense to the functionality you need. Tree structures are great for calculating relational data between game objects, such as relative positions if something is attached to another thing. Spatial structures are great for when you need to test if things are near each other, say when you need to do splash damage from a rocket. Flat structures are good when you need to search the entire world for something, or if you need to group common game objects together.

I really don't think having a class which contains all of the games variables will help make anything better. It'll be slower since you will need to search, find and get / set a value. Cleaning up will also be slow, say if dozens of rockets explode and are removed from the game, garbage clean up could jilt the frame rate.
Logged

I like turtles.
Massena
Level 4
****


Satisfied.


View Profile
« Reply #15 on: July 12, 2009, 03:30:19 AM »

After careful deliberation I have decided to use unordered_maps, probably from the Boost library. If it gets too slow or buggy in the future I'll just toss it in the bin and use something wiser.

This has lead me to think about some deeper concepts of programming. Why isn't creating a variable just a function? What I'm doing with my admittedly moronic code is simulating the "remote" creation of variables. With this system I can call game.create_int("name_of_the_int", 5); and have an int. Why is this not a standard feature in C++.

On a deeper level, why can't I tell my program to execute any string, for example:
Code:
string test;
cin >> test;
execute(test);
And if test is "std::cout << "Hello world";" the program should output 'Hello world'.

Why can't I treat data as code and vice-versa?

Is this a feature in other languages? I have heard of similar things in LISP and other such mythical languages.
Logged

Zaphos
Guest
« Reply #16 on: July 12, 2009, 03:59:26 AM »

C++ is a compiled language; a C++ program does not know enough to 'understand' C++ code at run time, as all that understanding is in the compiler, which is more heavy-weight than many programs created using C++.

The advantages of a compiled language are typically better performance, better static checking of program validity, and less stuff to distribute with the program ...
Logged
BorisTheBrave
Level 10
*****


View Profile WWW
« Reply #17 on: July 12, 2009, 04:09:18 AM »

Wow, I've never met anyone asking this question. Interpreted languages (where you can frequently swap code with data) are now so commonplace that C++ is being castigated for not being one.

Short answer: C++ compiles down to very efficient machine code. The compiler is a big powerful tool, so isn't available to the end user. The compiled code doesn't contain much information about the original source, so it would anyway be impossible to compile extra lines without access to the original header files, i.e. releasing half of your source.

OTOH, C++ is absurdly fast compared with everything else (and its speed competitors also have similar restrictions). Compiled code is the default state of being for code. With the exception of Lisp, interpreted languages came much later. Hence my surprise at your question.

It sounds like what you are doing would be better suited to an interpreted, dynamic language like you are used to. You either need to change your habits for C++, or introduce bindings to an interpreted language for parts where you need this (I'm told this is easiest to do in Lua). If you continue with your present mindset, you'll be continuously frustrated by the "lack" of capabilities of C++.
Logged
ஒழுக்கின்மை (Paul Eres)
Level 10
*****


Also known as रिंकू.


View Profile WWW
« Reply #18 on: July 12, 2009, 04:51:46 AM »

in my experience there's nothing wrong with using global variables for small game projects: the reason people warn against them is because it gets confusing after a program gets to be a sufficient size, but for small programs (say, less than 10,000 lines) using global variables is often the best and fastest way to do something
Logged

moi
Level 10
*****


DILF SANTA


View Profile WWW
« Reply #19 on: July 12, 2009, 06:55:07 AM »

With all that stuff in your heads, when does the game get done  Concerned
Logged

subsystems   subsystems   subsystems
Pages: [1] 2
Print
Jump to:  

Theme orange-lt created by panic