How do I design this kind of feature?

Started by
9 comments, last by SYJourney 7 years, 11 months ago

There is this kind of design problem that I seem to encounter with every game I try to make. Basically, I will try to take into account some feature that just doesn't seem to fit into how I coded so far. Most often it's an item or special ability that a player can use.

For example, I want different items which can do the following:

- Transport the player to a different level

- Increase some stat of the player

- A key which opens a door if you are standing in front of it

- Change the player's sprite

The question is where in my code do I put all this stuff? The kind of design I'd actually want is something like this:


class ItemEffect
{
 public:
  virtual void use() const = 0;
}

class WarpItemEffect : public ItemEffect
{
 WarpItemEffect(int target_level_id);

 void use() const override;
}

class KeyItemEffect : public ItemEffect
{
 KeyItemEffect(KeyType key_type);

 void use() const override;
}

So suppose I have some "Item" class. When I create a new instance of the class I will then simply plug in the subclass of ItemEffect I need. To me as an amateur this seems pretty good so far. But there's an obvious problem. If we take the key example, I most likely will need the "Player", to know which direction he is facing, then I need the "Level", to check if there is a door in front of him, and then I need to open the "Door". All these are things which I cannot just code within my "use" method. If I tried implementing it I'd most likely end up with something like this:


void KeyItemEffect::use(const IPlayer& player, ILevel& level)
{
 Direction direction = player.getDirection();
 Position position = player.getPosition();
 IDoor* door = level.findDoor(position, direction);
 if (door)
  door->unlock(key_type);
}

So what's happened is that half of the code which actually does something is now in the other classes. I guess that for Level::findDoor(), I can probably find other uses but the main problem is: If I continue this, I will end up with more and more methods in "Level" (or other classes) which just execute services specific to ItemEffects.

So how can I design this so that it makes sense?

Advertisement

Same problem here, sir. I wait with you for some wise reply :cool:

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.

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.

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.

I'm not sure what your critics are about. Do you mean thinking 10 minutes on a problem is already a waste of time? Really? In my experience, planning what to do is far away from being a waste of time, especially when dealing with a complexity like this. The aspects I mentioned are not coming from a look into a glass ball but from explicit needs. Effects do not happen by themselves. They need to be triggered. They need to be selected first (or, to be exact, an action need to be selected first); action selection mechanism is a well known term. They need to be controlled. All these things work together. Looking just on a single aspect isolated from its surroundings will cause problems. Moreover, decoupling systems is a real world requirement, too. If you ignore needs, in the end you will waste more time on refactoring than 10 minutes, for sure.

Then, why exactly is managing world state from a couple of services not a good idea? For example, all component based entity solutions with sub-systems do it. It allows for a higher performance due to better cache coherency. It allows for a clear distinction of stages when running the game loop (i.e. what happens when in which order). When interested in an explanation with greater depth, have a look at Game Engine Architecture by Jason Gregory.

No offense. I already mentioned that many ways can be gone. The way I'm going is not my first trial. I already learned it the hard way, so to say.

For our game prototype we were on the same situation. We first builded a logical inventory that means that this isnt a graphical inventory but a list of items stored as

ItemID, Ammount, State of Attrition

and our character got a second ivnentory with fixed item slots for those items equiped holding the original inventory slot reference. Inventory could be accessed via interface on the character to query items, add and remove items. Items were stored in a database with item prototypes and

Fixed unique ID, Name, Description, BuffID

so evvery part of the game nows what kind of item is behind certain item id. Buff is an extra database that contains some buffs. Inventory adds buffs to a list when an item is dragged and removes them when an item is dropped.

We then added a general action interface to the game. Every object in a scene could have a list of actions that could be triggered when 'activating' the object. When activating, the character instance is passed to the action function and the interface specialisation does anything else so for example the door would check if characters inventory contains a key item id that matches to <someone>'s key and decides then to swing open or stays closed.

When opeening the door, it will set an internal state to itselfe where the lock flag is removed and removes the key item from inventory.

We also have some actions require certain item to be aquipped or certain skill to have, function is always the same passing character instance and let the action object decide what to do. (Same system also let us trade for merchants/use storage items e.g. chests by the way)

The interface or 'base class' approach is usefull when you have different kinds of actions what I attemp is the case in your question

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.

I'm not sure what your critics are about. Do you mean thinking 10 minutes on a problem is already a waste of time? Really? In my experience, planning what to do is far away from being a waste of time, especially when dealing with a complexity like this. The aspects I mentioned are not coming from a look into a glass ball but from explicit needs. Effects do not happen by themselves. They need to be triggered. They need to be selected first (or, to be exact, an action need to be selected first); action selection mechanism is a well known term. They need to be controlled. All these things work together. Looking just on a single aspect isolated from its surroundings will cause problems. Moreover, decoupling systems is a real world requirement, too. If you ignore needs, in the end you will waste more time on refactoring than 10 minutes, for sure.

Then, why exactly is managing world state from a couple of services not a good idea? For example, all component based entity solutions with sub-systems do it. It allows for a higher performance due to better cache coherency. It allows for a clear distinction of stages when running the game loop (i.e. what happens when in which order). When interested in an explanation with greater depth, have a look at Game Engine Architecture by Jason Gregory.

No offense. I already mentioned that many ways can be gone. The way I'm going is not my first trial. I already learned it the hard way, so to say.

That's the whole beauty of planning interfaces, he has already defined the effect, if he comes to planning the trigerring mechanism, all he has to do is use the interface because the triggering mechanism does not care about how the effect is being used.

Therefore the meaning of my sentence " and you won't be stuck 10 minutes on question of "why", "how", "When", "Why" again" is not caring about other sub-systems when you cover another sub-system.

If you do look into the implementation at the end of another sub-system, it is a sign of a poor interface design.

I do agree with you that it needed to be controlled and other sub-systems should be in concern. But as I said, the imlpementation is not important for another sub-system.

Or else it won't be a sub-system anymore, just one big messy system.

About the world state changes, I did not get you completly there, all I understood is you want to alter the global world state from services, which is not good.

There is another manner to look at this which may be a variation of haegarr's response. At the top level, your problem is generally known as cross cutting where a design works for 90% but 10% doesn't fit the same pattern and causes issues such as this. Usually this is caused by a failure in the design to separate concepts properly and your current design suffers from this. Consider your ItemEffect, it only supplies a single function so how could it be breaking SRP? Well, in the example provided it conflates the concept of 'usable' with the concept of 'use'. I.e. it checks that it is in a usable condition before attempting to trigger a state change. Separation of the 'is it usable' from the 'use it' concerns would be a first step to solving much of the design issue and as haegarr suggests it is similar in behavior to how a component entity model works. So, reworking your example, you could do something like the following:


class Action {public: virtual void do(...) = 0;}
class UseKey {
public:
  virtual void do(...) override
  {
    target->unlock();
  }
};


It has no condition checks, it just performs the action with the assumption that something else has validated that everything is ready to go. The thing that makes the checks could be broken down into many conditions (generally a good idea) but I'll be lazy and outline it in a single class and also assume you have a services oriented design around things:


// This assumes it is an inventory item, the same pattern holds for other variations.
class ConditionCheck {
public:
  virtual void addToInventory(...) = 0;
  virtual void removeFromInventory(...) = 0;
};
class KeyConditions {
public:
  virtual void addToInventory(...) override
  {
    // Assume a 'smart world' which supplies various services.
    // The primary service of concern here is an awareness system.
    mQuery = GetWorld().SpatialAwareness().Query()
      .Center(owner->GetTransform())
      .Radius(2.5) // keys pay attention to things within 2.5 meters.
      .Filter(Door::ObjectType)
      .OnChange(std::bind(&KeyConditions::onVisible, this, std::placeholder::_1));
  }

  virtual void removeFromInventory(...) override
  {
    mQuery = nullptr;
  }

private:
  void onVisible(const std::vector<ObjectHandle>& doors)
  {
    for(door : doors)
    {
      if (haveKeyFor(door) && facing(door))
        validCondition(usable(door));
    }
  }

  SpatialAwareness::QueryHandle mQuery;
};
This is not a perfect example but hopefully shows the goals and direction such an architecture would take. It combines a more complete following of SRP with a reactive design to prevent the behavioral lock in you are finding.

Perhaps this is too much of a change, you could still borrow some concepts and fix the SRP issue to be in a better position at a minimum. Of course, the issue a lot of folks have with something like this is that it feels (is) pretty abstract and takes a bit to get used to. Additionally, WoopsASword, does have a point, unless this is a relatively major portion of your gameplay, a simplified solution may be better. I would tend to use this solution if I were creating a huge RPG style game, but if it were a fairly simple game, I'd keep it simple.

There is another manner to look at this which may be a variation of haegarr's response. ...

Yes, that is definitely what I meant except that you're going the refactoring way (which, of course, is totally fine) where I already have been trapped once back in time and hence know of that particular necessity we're talking about here.

Interestingly enough, there is many good stuff to learn from TA / IF engines, where the interaction of the player is focused on performing such kind of actions.

There is another manner to look at this which may be a variation of haegarr's response. ...

Yes, that is definitely what I meant except that you're going the refactoring way (which, of course, is totally fine) where I already have been trapped once back in time and hence know of that particular necessity we're talking about here.

Interestingly enough, there is many good stuff to learn from TA / IF engines, where the interaction of the player is focused on performing such kind of actions.

My primary motivation for separation of usable from use in this case is that the results can now be exposed to script systems considerably easier. I generally dump most of these items into a behavior tree since the simple 'use a key' example can be extended to include a lot more checks and becomes the basis of a puzzle system also. I.e.:

sequence

- haveItem(key, "Some door")

- haveItem(scroll, "Nasty Green Ritual")

- isDay("Tuesday")

- isHour("Noon")

- actorInVicinity(Player, "Nasty Green Altar", 5)

- actorHasPerformed(Player, "Sacrifice", "Chicken Feet")

- makeActionAvailable("Trigger Nasty Green Apocalypse")

Now the player can destroy the world by reusing prior work. With enough generic actions, even the final line can be pushed off to script such that none of this requires custom code. Maybe the OP is not making a game with puzzles, maybe he is, either way though fixing SRP allows throwing this stuff in script where it is easier to reconfigure and experiment.

This topic is closed to new replies.

Advertisement