0
\$\begingroup\$

I've got a class, WeaponFrame, that has an int member, weapon_frame_id, that I intend to use as a way to identify the weapon, and I use that compare against other WeaponFrame instances.

The problem is that I want to pull the weapon from the global list of WeaponFrames and equip it on an Actor. As the designer, I might know that weapon_frame_id #2 is the bow weapon I want, but what is the best way to programmatically identify them?

If I assign an ID to the WeaponFrame on instantiation, I am unable to rely on the value being identical the next time the game is run, unless I assign the value to an enum or int then. That might work though.

I'm using C++11, would I just create a namespace somewhere, and enumerate all the weapon types,

enum class WeaponFrameIDs { 
  Sword = 1,
  Bow = 2, //repeated a hundred-plus times
};

or would I make a ton of const integers in a header somewhere, even if effectively an enum that way too:

namespace WeaponFrameIDs {
  const int Sword = 1;
  const int Bow = 2;
}

I could also use strings mapped to an integer, but I'm losing compile-time verification (and I can't use constexprs with VS2013), and run the risk of duplicating entries. However it supports dynamic assignment to the list:

std::unordered_map<std::string, int> WeaponFrameIDs {
  {"Sword", 1},
  {"Bow", 2},
};

Is there an alternative that I'm missing? What is the best way to write a system for identifying weapons in a game?

If it's relevant, here's an example situation I'm trying to solve via code. I am not using a visual editor where I can drag a weapon onto an actor.

void set_up_actor()
{
  Actor actor;
  WeaponFrame* weapon_frame = AllWeaponFrames.at(WeaponFrameIDs::Bow);
  actor.weapon_frame = weapon_frame;
};

Points I'm looking to avoid:

  • Filling an entire header with enums
  • Needing to recompile the entire project because I changed that header
  • Losing the ability to verify the id at compile time
  • Eventually add user-generated ids. This conflicts a lot with the compile-time verification.

I'm trying to solve the best way to pull the Weapon from a list, whether it's by ID, index, specially mapped key or something else.

  • If I use a literal ID, actor->weapon_id = 1, I don't know what weapon is tied to the ID of 1.
  • If I use an enum, or a const int, to represent the 1, I need to recompile the game for each of the new weapons I add.
  • If I change the id type to a string, I lose the ability to programmatically validate the string against the existing list of weapons.
\$\endgroup\$
  • \$\begingroup\$ I had a hard time verbalizing the problem but I am just looking for a good way of assigning these WeaponFrames to a reliable identifier that I can use. \$\endgroup\$ – TankorSmash Nov 24 '18 at 1:48
  • 1
    \$\begingroup\$ The bounds of this question are not clear. You say you want "compile type[sic] verification", but that doesn't make sense if you have hundreds of these things. You don't want to have to change source files and recompile your entire program just to add a new weapon to the game, do you? \$\endgroup\$ – Nicol Bolas Nov 24 '18 at 2:33
  • \$\begingroup\$ @NicolBolas That's what I'm struggling to define. I want to allow for a system to be extendable without being re-compiled, but I would also like something to easily identify the weapons. It doesn't seem like I can have my cake and eat it too, so I'm wondering what the best option is. I corrected the typo, thank you! \$\endgroup\$ – TankorSmash Nov 24 '18 at 2:58
3
\$\begingroup\$

Persist your weapon data seperately. You probably want to use a database, but you could also use a file and load it into memory. Using constants creates (at least) two problems, the first is adding or changing data requires you to recompile the binary, and the second is it inflates the size of the binary.

Let's consider a simple example where we use database.

Suppose we have a table for weapons which has the following a columns, id (pkey), name (varchar), max damage (int). Our weapons do a random amount of damage between 0 and max damage.

We also need a table to store player data, it will have the following columns, id (pkey), name (varchar), and weapon_id (fkey).

Our program defines the following structures (for simplicity).

struct Weapon {
    int id;
    char *name;
    int max_damage;
}

struct Player {
    int id;
    char *name;
    Weapon *weapon;
};

Suppose a player with an id of 1 loads into our game.

SELECT * FROM player INNER JOIN weapon ON weapon.id = player.weapon_id WHERE player.id = 1

We can use the data returned from the database to fill in our structs and the rest of the program can use them.

Do not get bogged down in the specifics of this example as it is mearly that.

\$\endgroup\$
  • \$\begingroup\$ Pulling it out of the DB would have the same issue, wouldn't it? I'd still need to know which ID to select for. I don't know much at all about code with respect to the size of binaries, but surely even if it was thousands of instances, it wouldn't be more than a few MB right? Each frame is about 8 ints. Thanks for the answer, I'll end up putting the weapons outside of the code, but I'm still looking for a way to ID them. \$\endgroup\$ – TankorSmash Nov 24 '18 at 3:23
  • \$\begingroup\$ You would also want to store the NPC and PC data in a similar manner, e.g. { main_hand: 123, off_hand: 456 } where those are id's for weapons in the database. \$\endgroup\$ – user3730788 Nov 24 '18 at 3:30
  • \$\begingroup\$ Right, but that would be after I've assigned the weapons to the PCs, I'm trying to give a PC a weapon, but I don't know how to connect them. With the DB, I'd need to know the literal ID, or select for the attributes I want from the weapon. \$\endgroup\$ – TankorSmash Nov 24 '18 at 3:37
  • \$\begingroup\$ See updated post. \$\endgroup\$ – user3730788 Nov 24 '18 at 3:53
  • \$\begingroup\$ I'm sorry, I think I haven't been clear at all, thanks for bearing with me as I sort myself out here. The updated answer lays out (very well, I might add) how to set up your game to load in the weapon from the database, but the issue I'm having is how to set the player's weapon.id to be a value in the first place. Your answer presupposes that the weapon ID is already assigned to the player, whereas I'm trying to add the weapon ID to the player in the first place. I'll do my best to edit the question to include that. \$\endgroup\$ – TankorSmash Nov 24 '18 at 4:13
2
\$\begingroup\$

The simplest and easiest way to handle this is to:

  1. Store weapons in a file to be loaded at runtime.
  2. Use fixed-length strings as identifiers for the items. Your code references them by using string literals.

Fixed-length strings make for great identifiers. They are:

  1. Hard to overlap. You need the identifiers to be assigned by hand in the weapon files themselves, not at load time (this allows the identifiers to be persistent). But it's really easy to accidentally reuse an integer value, especially if you're counting from 1. It's a lot harder to repeat a fixed-length string by accident. Even a 16-character string has plenty of space, and a 32-character one is long enough for you to have a naming convention with groups of names partitioned by prefixes and such.
  2. Fast to compare. When doing comparisons with fixed-length identifier strings, you don't care about any specific ordering. You don't specifically need ASCII, lexicographical, or Unicode ordering. What you need is a single, specific, unchanging ordering. This can be achieved by comparing the strings as though they were arrays of integers rather than bytes. A 16-character fixed-length string can be compared with 2 64-bit integer compares rather than 16 character compares.

    This also makes lookups in associative containers faster.

  3. Not particularly large. 16 character strings are probably big enough for a unique identifier, but if you want to get really descriptive, you can use 32. Oh sure, that's bigger than the 4 or 8 bytes a single integer would take up, but it's not a huge amount.

    • And if their size does become a legitimate problem (which it almost certainly won't), you can always have non-debugging builds have the fixed-length string class store a hash rather than a string. You'll lose #5 on this list, but you'll get back the storage space from your strings.
  4. Legible in code. When you read your code, you see string names, so you have an idea what it's referencing.

  5. Legible in the debugger. This is a very big deal. At some point in your game's development, you're going to be looking at some data structure and trying to figure out which weapon this data structure is referencing. An integer value is pretty opaque; a fixed-length string can be read in the debugger and tells you everything you need to know about what is being referenced.

Yes, there's no compile-time protection in your source code references, but you lost that the moment the weapons stopped being part of your C++ code. After all, if the weapons are not part of your code, there's nothing that ensures that the integer 1 or an enumerator maps to the actual weapon that it appears to (or any weapon at all). That's determined by what is in your weapon file, not what's in your C++ code.

\$\endgroup\$
  • \$\begingroup\$ Here's a real life example of how that may look (please note these codes may be out of date, but were at one time live). gmstemple.com/Diablo2/itemcodes.html \$\endgroup\$ – user3730788 Nov 24 '18 at 15:24
  • \$\begingroup\$ @user3730788: I don't know; an integer seems better than a 3-letter code. The whole point of using a string is that it's legible; it has a clear and obvious meaning. And it's easier to keep from accidental overlap. Just look at that list: ne1, ne2, etc. That's impenetrable and offers little that 0x23000001 wouldn't (where 0x23 represents the basic type, and 0x000001 is the item index within that type). \$\endgroup\$ – Nicol Bolas Nov 24 '18 at 15:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.