This has a game development project under itself, but it’s really about coding and mapping data to other pieces of data. This is why I decided to post it here.
The format that I’m using for external inventory item data storage:
[ID:IT_FO_TROUT]
[Name:Trout]
[Description:A raw trout.]
[Value:10]
[3DModel:null]
[InventoryIcon:trout]
[Tag:Consumable]
[Tag:Food]
[Tag:Stackable]
[OnConsume:RestoreHealth(15)]
[OnConsume:RestoreFatigue(15)]
The question is concentrated upon the last 2 OnConsume properties. Basically, the two properties mean that when the item gets consumed, the consumer’s health goes up by 15 points, and his fatigue does so as well. This, in the background, invokes 2 different methods:
void RestoreHealth(Character Subject, int Amount);
void RestoreFatigue(Character Subject, int Amount);
How would you go about mapping the methods to their in-file string counterparts? This is what I thought of:
-
Every time an item gets consumed, a list of strings (the events) gets passed to an Item event manager. The manager parses each string and calls the appropriate methods. Very easy to set up, and since this is not an operation that happens too often, the impact on performance might not be considerable (strings will also be tiny (max 10-15 characters) in size, and parsed in O(n) time).
-
Each inventory item (class) parses the string events once and only once, on initialization. Each string event gets mapped to its appropriate method via a dictionary. This is the most efficient method in terms of performance that I can think of, but it makes it extremely difficult to do other things:
All of the values in the dictionary would have to be delegates of the same kind. This means I cannot keepa) RestoreHealth(int)
b) SummonMonster(Position, Count)
in the same dictionary, and would have to set a new data structure for each kind of callable method. This is a tremendous amount of work to do.
Some ways that came to mind, to improve both methods:
-
I could use some sort of temporary cache inside the Item event
manager, so that an item’s OnConsume events don’t get parsed
twice? I might hit the same issues as the ones I hit during 2)
though, as the cache would have to be amap<InventoryItem,List<delegate>>. -
The hashtable data structure inside the .NET libraries allows
for any kind of object to be a key and/or value at any given time
(unlike the dictionary). I could use this and map string A to
delegate X, while also having mapped string B to delegate Y
inside the same structure. Any reasons why I should not do this? Can
you foresee any trouble that would be brought by this method?
I was also thinking about something in the ways of reflection, but I’m not exactly experienced when it comes to it. And I’m pretty sure parsing the string every time is faster.
EDIT
My final solution, with Alexey Raga’s answer in mind. Using interfaces for each kind of event.
public interface IConsumeEvent
{
void ApplyConsumeEffects(BaseCharacter Consumer);
}
Sample implementer (particular event):
public class RestoreHealthEvent : IConsumeEvent
{
private int Amount = Amount;
public RestoreHealthEvent(int Amount)
{
this.Amount = Amount;
}
public void ApplyConsumeEffects(BaseCharacter Consumer)
{
Consumer.Stats.AlterStat(CharacterStats.CharStat.Health, Amount);
}
}
Inside the parser (the only place where we care about the event’s particularities – because we’re parsing the data files themselves):
RestoreHealthEvent ResHealthEv = new RestoreHealthEvent (Value);
NewItem.ConsumeEvents.Add (ResHealthEv );
When a character consumes an item:
foreach (IConsumeEvent ConsumeEvent in Item.ConsumeEvents)
{
//We're inside a parent method that's inside a parent BaseCharacter class; we're consuming an item right now.
ConsumeEvent.ApplyConsumeEffects(this);
}
Why not “map” them to “command” classes once-and-only-once instead?
For example,
could be mapped to
RestoreHealthandRestoreFatiguecommand classes that can be defined as:Consider commands as just wrappers for your parameters at this point 😉 So instead of passing multiple parameters you always wrap them and pass only one.
It also gives a bit of semantics too.
Now you can map your inventory items to commands that need to be “sent” when each item is consumed.
You can implement a simple “bus” interface like:
and now you just get an instance of
IBusand call itsSendmethod when appropriate.By doing this you separate your “definition” (what needs to be done) and your logic (how to perform an action) concerns.
For the receiving and reacting part you implement the
Subscribemethod to interrogate thesubscriberinstance (again, once and only once) figuring out all its method which can “handle” commands.You can come up with some
IHandle<T> where T: ICommandinterface in your handlers, or just find them by convention (anyHandlemethod that accepts only one argument ofICommandand returnsvoid), or whatever works for you.It is basically the same part of “delegate/action” lists that you were talking about except that now it is per command:
Because all the actions now accept only one parameter (which is
ICommand) you can easily keep them all in the same list.When some command is received, your
IBusimplementation just gets the list of actions for the given command type and simply calls these actions passing the given command as a parameter.Hope it helps.
Advanced: you can do one step further: have a
ConsumeItemcommand:You already have a class that is responsible for holding a map between InventoryItem and Commands, so this class can become a process manager:
ConsumeItemcommand (through the bus)Handlemethod it gets the list of commands for the given inventory itemWell, now we have separated clearly these three concerns:
IBusand send aConsumeItemcommand and we don’t care what happens next.IBus', subscribes forConsumeItem` command and “knows” what needs to be done when each item is consumed (list of commands). It just sends these commands and doesn’t care who and how handle them.RestoreHealth,Die, etc) and don’t care where (and why) they came from.Good luck 🙂