Advertisement

What do you guys use as a key in RPG data structures?

Started by August 25, 2015 01:01 AM
27 comments, last by ankhd 9 years, 4 months ago


- There are "attack" objects, which all inherit from a base attack class (and may be composed of some other things in the future)—they do all the calculations given a character's stats and some damage multiplier. This will make it easier to give specific characters specific abilities. When performing an attack, one of these objects is created, passed to an attack-event, and an attack-handler applies all the appropriate damage.

Sounds reasonable -- just beware the urge to derive a new object for each kind of attack. As a rule of thumb, strive to keep the number (and especially the depth) of derivations small. The basic approach in this case would be to only make a new kind of attack object when the attack formula is different -- by this I mean the actual shape of the formula, not just having different constant factors ("weights" or "multipliers") -- those can be passed into the constructor of the Attack.


- Items have the same attribute-map as characters. When items are equipped, an iterator just adds the appropriate item stats to the character stats by key (equipping, picking up items, etc. is actually handled by a handler, too, since it requires things like removing an item from a room and putting it in the character's inventory.)

Again, the idea is reasonable. There are a couple ways to approach this that immediately jump to mind. If most equipment/status-effects effect only one stat, then it might be better to have a list of such things (e.g. built up by looking at currently-equiped items and active status effects) and playing their effect on a copy of the player status object. This is fairly simple (its something like the Command Pattern). If many equipable items/status-effects effect multiple stats, then it might be better to have a list of 'filters' which derive from the stat object and "wrap" another stat object (the player stats at the root, or another stat filter) to present a filtered view -- in that way they can be chained together to apply stacking status modifiers (its something like the Decorator Pattern). In either case, an attacker might be allowed to have their own modifiers (say, a certain enemy type removes or reduces magic resistance, even that due to equipped items).

I still say that a map of strings-to-integers is both overkill (provides dynamism of stat names and number of stats you're not using and don't seem to need), and too limiting (all stats can only be integers), but you might have reasons that aren't clear, or might just be more comfortable with what you have. If it were me, I'd have a struct/class representing stats (or stat-packs, if different entity types or characters might have different sets of stats). You might be aiming for the flexibility to add unknown stats at a later time, but unless you want to be able to invent new stats at runtime (e.g. your 'game' itself is just configuration/data) the unused flexibility probably imposes more headache than you gain back in utility.

throw table_exception("(? ???)? ? ???");

I've been using a lot of std::map<std::string, int> or something. An example would be:

std::map<std::string, int> attributes_;
attributes_["str"] = 5
attributes_["def"] = 5

I'm not sure I'm good enough at C++ yet to know, but using "std::string" as a key too much feels like it could go incredibly wrong in the future. What do you guys suggest for readable/usable code?

I considered a global enum, but that strikes me as worse than the string idea.


I use uint32_t for keys, and yes, I use std::unordered_map.

However!

In this case, I'd have the attributes just be regular variables, like this:


struct Attributes
{
    int strength = 0;
    int defense = 0;
    int maxHealth = 0;
    int maxMana = 0;
    int endurance = 0;
};

- There are "attack" objects, which all inherit from a base attack class (and may be composed of some other things in the future)—they do all the calculations given a character's stats and some damage multiplier. This will make it easier to give specific characters specific abilities. When performing an attack, one of these objects is created, passed to an attack-event, and an attack-handler applies all the appropriate damage.


Sounds reasonable -- just beware the urge to derive a new object for each kind of attack. As a rule of thumb, strive to keep the number (and especially the depth) of derivations small. The basic approach in this case would be to only make a new kind of attack object when the attack formula is different -- by this I mean the actual shape of the formula, not just having different constant factors ("weights" or "multipliers") -- those can be passed into the constructor of the Attack.


Exactly. Inheritance can be used for logic, but for merely different data (damage amounts and particle effects and such like that), just pass in different data to the same class constructor.

An important revelation is when you realize logic is also data, and unlock the ability to pass in logic as parameters. smile.png

Advertisement

You should use a map when you want to store relatively small subset of many possible "key" values that you are not able to determine before runtime. That will allow you to not waste space reserved for every possible key or allow you to have keys that you can't determine beforehand. This is not the case of your question. The stats you need are both finite and known before runtime.


Would creating a separate object for each "set" of stats really be less overhead than just using a map? Considering that attacks use these stats, items affect these stats, etc. I didn't really have dynamism in mind when I started using maps, I just thought it was the easiest way to store a base-attribute integer.

I'm not sure what overhead do you have in mind? That's true that sword will have "damage" but not "capacity" and bag will have "capacity" but not "damage". But you really shouldn't create different stat structs for each. Just for the sword it will be damage = 5, capacity = 0 and for bag damage = 0, capacity = 10.

The strongly typed struct is the easiest and most error resistant way to handle stats for your objects.


There are "attack" objects, which all inherit from a base attack class (and may be composed of some other things in the future)—they do all the calculations given a character's stats and some damage multiplier. This will make it easier to give specific characters specific abilities. When performing an attack, one of these objects is created, passed to an attack-event, and an attack-handler applies all the appropriate damage.

An attack is an action, a verb. You should not model verbs as nouns, or else you end up with anathemata like attack.do(hero, enemy) instead of a more natural hero.attack(enemy). Attack handlers, attack events, attack objects: what do those look like in real life?

Stephen M. Webb
Professional Free Software Developer

I do have a character.attack(target) function for my characters, actually!

This is the function that prompts the creation of an attack object. The closest parallels to real life I can think of for attack objects and handlers are: an attack object is the act of swinging the sword towards your target (with more strength, your attack object has an increase in damage). Attack handlers are the physics behind said swing, what happens when the swing hits an armored target vs. what happens when it hits an unarmored target, etc (if an attack handler doesn't consider strength, then it doesn't matter how high one's strength stat is, it won't affect what happens).


Perhaps it's my naiveté speaking, but, while programatically modelling things after their respective nouns or verbs is a good starting point—I think at some point more complexity is needed (i.e. - it seems that the easiest way to maintain a lot of freedom in creating different attacks is to actually have an attack object with some standardized structure).

Maybe this is the equivalent of asking a question like: "Is an attack the [more verb-y] action of swinging a sword, or do I consider it the [more noun-y] aggregation of causes-and-effects. In the latter case, I can model a branch falling from a tree and hitting a player as an attack without necessarily having to give the tree a target, or add an attackWithFallingBranch(target) method to the tree.

An attack is an action, a verb. You should not model verbs as nouns, or else you end up with anathemata like [font='courier new', courier, monospace]attack.do(hero, enemy)[/font] instead of a more natural [font='courier new', courier, monospace]hero.attack(enemy)[/font]. Attack handlers, attack events, attack objects: what do those look like in real life?


Modeling your code components after real life, physical objects is, in my view, an antipattern. In a good design some components may conceptually align with an object in the physical world or the simulation you are creating, but whether or not a component correlates with one of those objects should not be a requirement. After all , what does a database transaction look like? What does a file stream look like? What does a dependency injection container look like?
Advertisement

Another question (pretty unrelated to the title of this thread:

I've been reading a lot of the decorator and component patterns (I'd like to implement it for items. For example, while an inheritance pattern would look something like Item -> Equipment -> Weapon and Item -> Equipment -> Armor, I'd like to just wrap an item in an equipment wrapper, giving it stats, and then in a weapon wrapper, maybe giving it a damage-boost stat or something like this.

I'm not actually sure what wrapping entails, though. All the tutorials I see are... not very helpful. From what I've seen, it looks like putting an item object inside of a weapon object? Then accessing a weapon method would be something like item->weapon->method()? I was under the impression that decoration should actually add the method, like an item wrapped in a weapon decoration would just be item->method()?. I'm having a hard time figuring out how to do this without inheritance.

Wrapping usually means creating a class that exposes at least one of the same interfaces as the wrapped class does. That wrapper class will hold a reference to an instance of the wrapped class. When you call a method on the wrapper it will perform some additional behavior or modification to the arguments, then call the wrapped class method. The result will be passed back to the original caller, perhaps even being modified also. Wrapping is a way to add behavior to a class without modifying that class. You can pass around the wrapper instead of the wrapped class and consumers of the class will not know any different as long as the wrapped class still behaves in a way that is rational.

A contrived example would be a logging wrapper. You could wrap some class with your logging wrapper and pass that around instead. Your logging wrapper will log before and after any method is called. It doesn't change the result of the call as far as the consumer is concerned, except that it will slow the call down.
One thing to be aware of when wrapping is that if you are introducing the possibility of additional types of errors (as in my logging example) you need to ensure that those errors don't leak to the caller in a way that it cannot handle.

So the wrapper only modifies the already-existing methods of the wrapped? That is, it doesn't add or remove any actual members?

This topic is closed to new replies.

Advertisement