Some guidelines:
A) Let's favorite composition over inheritance: Inheritence will be used to generalize effects into one kind of interface, IEffect -> IItemEffect.
B) Favrotie dependency injection for services, not for information.
C) Finding a good metaphore for solving the issue: For my metaphore I'll use buying a soda from a vending machine.
Lets start:
The metaphore goes like this: You press a button and the right product goes out.
Our button is the item being used and the product is the effect. (Actually the product going out is the effect).
Let's start by defining a simple interface IEffect. It has nothing because it's a generlized effect for every god damn effect.
The second one is IUseableEffect, which is an effect which can be used.
It has a single method Use();
So:
class IItemEffect: public IEffect
{ virtual void Use() =0; }
So let's ask ourselves, does the vending machine asks the customer where is the bottle? GOD DAMN NO. So the effect system does not ask us what kind of door we need or where is the god damn door.
However it does need information, this will be internal and won't concern our use method.
like the customer, it does not know how the vending machine gets the product. (Probably a hash)
Now we define a simple effect, let's do "Give player an apple".
We'll start by our current interfaces, I belive something like an inventory will suite us:
interface IPlayerInventory
{
/*..many unrelevant functions*/
Add(IItem item);
}
class PlayerInventory: public IPlayerInventory
{
/*Implementation does not concern us.*/
}
Then we write our effect:
class AppleGiverEffect: public IItemEffect
{
private:
IPlayerInventory mInventory;
public:
AppleGiverEffect(IPlayerInventory playerInventory)
{
mInventory= playerInventory;
}
void Use()
{
mInventory.Add(new AppleItem(Color.Green))
}
}
Why is it good to implement it this way:
A) The effect is generellized, I can use the effect on anything that has an inventory.
B) Information is not passed, it is retriven by services. Any behavior is a service which can be injected into the effect.
C) The Use method is not concerned with any implementation since the interface is dumb. It only knows how to use the efffect.
D) Implementation is hidden behind the interface which is what SOLID principles state, you want to know what the interface does and that's it.
Of course it is more simplified than real gaming effect systems,
Take the guidelines and the principles from my example and try to design something more robust for your solution.
Good luck!
Edit: Haegarr, You've over-complicated the system purpose. Don't try to perdict the future or your needs,
Write extensible code and you won't be stuck 10 minutes on question of "why", "how", "When", "Why" again...
+ Managing "world-state" from bunch of services is not a good idea. Fix me if I got you wrong.
First of, there a many possible ways to solve such a problem. Their respective suitability depends on the game mechanics, player control and AI, need for extensibility, overall structure of the game engine, and possibly more. In short, working out a solution without knowing all aspects may give a sub-optimal result.
Nevertheless, coming to your question about "services": This is in fact a totally fine approach! You should just think of generalizing some stuff shown in your example above. Think of a service (in my implementation is this one called SpatialServices) that can be queried for the current placement (i.e. position and orientation) of any game object, queried for a list of game objects that are in a given vicinity range, colliding with a given volume, and so on, optionally done with a filter on the game object's type. Such a functionality is not special to ItemEffects; it is commonly useable. Notice that this kind of approach is one incarnation of the widely used component based entities architectures.
Let's come back to the generalization. A key can in principle be used by the player or any agent of AI. So let's generalize the "player" parameter to be an "actor" parameter and being of the Agent class type. I prefer to use this term here because I think "that agent that performs the action that has this effect is the actor". Next, using a key is not restricted to a door. Let's say that your game object can implement the Lockable interface, and a key is defined to be a game object that can be used to lock and unlock such a Lockable object, or, in other words, the actions "lock object" and "unlock object" can be applied to a Lockable type of object. Next, passing the Level as argument is fine if it is understood mainly as the current collection of services.
For me, the above formulation leads to the question "why does ItemEffect::use(…) check for applicability?" The anatomy of an action shows IMHO at least the distinct parts pre-condition and outcome. For a more elaborate system, more parts are needed, of course. Let's say we have a class Action with the methods Action::checkApplicability(…) and Action::perform(…). So the player controller and AI agent can invoke checkApplicability to determine whether to take that action into account. E.g. the player controller may adapt its UI to reflect that that action is not available, so that the player just has no ability to trigger e.g. "unlock object" if s/he hasn't a key or is too far away. Notice please that we have a game mechanics related option here: Do we allow the agent to try to use an obviously senseless action and then react with "haha, stupid one, you are too far away", or else do we prevent this kind of situation? (You remember, I already wrote earlier that such things have an influence.) If checkApplicability allows the action and the action selection mechanism has selected this action, then Action::perform(…) is invoked. Nothing hinders you to do some checks herein, too. This is nonetheless the place where the "try and possibly fail" style of mechanics can be implemented. However, in general the outcome of the action is coded in that routine.
The above is already some stuff to think about. However, we still have neglected some aspects. For example, an action may have an immediate or a deferred outcome. For example, the action "open object" applied to a chest should change the state from closed to opened (so the outcome is "is opened"), but the transition should be garnished with an animation. So the state changes from (stable) closed to (transient) opening to (again stable) opened. The state is controlled first by the action, then by the animation sub-system. Another aspect is the enhancement: What happens to "open object" if the chest has a lock, and … perhaps a trap?
From all this we can deduce what kind of system I have in mind: A separate world state, a couple of actions as the basis for agents to alter the world state, and the possibility for literally any sub-systems and services to work with them. I'm currently on my way to implement such a system, so you should not be surprised that my explanations go in exactly that direction ;) But, maybe, those thoughts are of interest to you, too.