1

Let's say I'm designing an RPG type game, and this game has a turn-based combat system. There are some things that the player character/non-player characters can do inside and outside of combat. For example, while they are not in combat, they can move around the world, use items, talk to other npcs, and the player (not their character) can open certain game menus. While the characters are in combat, they can also use items, and use certain combat abilities, but cannot move around the game world or talk to other npcs.

This question is intended to be language-agnostic, but the examples code will be in Java.

A first implementation of this might be something like

public class Character {

    public void use(Item item) { /* ... */ }

    public void talkTo(Npc npc) {
        if (inCombat) {
            throw new IllegalStateException("Unable to talk to npcs while in combat");
        }
        /* ... */
    }

    public void useAbility(Ability ability) {
        if (!inCombat) {
            throw new IllegalStateException("Unable to use ability while not in combat");
        }
        /* ... */
    }
}

My question is targeted to the useAbility and talkTo method. Should these methods be callable all of the time, or should I restrict this in some manner, perhaps something like OverworldContext or CombatContext or an adapter/proxy of some sort to separate things that can be done? Perhaps something like

public class OverworldCharacter extends Character {

    private Character character;

    public OverworldCharacter(Character character) {
        this.character = character;
    }

    public void talkTo(Npc npc) {
        // do some stuff
    }

    // proxy character methods to field
}

And

public class InCombatCharacter {

    // Same as other class

    public void useAbility(Ability ability) {
        // do some more stuff
    }
}

This game is intended to allow plugins/mods to be created by third parties, so that's another things to consider in this design.

4 Answers 4

2

You could have methods on your character that represent the actions the character takes and that lead to a 'context' in which the actions are limited. That way, the actions can only be called when they are relevant.

The problem with not limiting the actions in any way from the domain is that your check logic will be duplicated a lot. Every action you add will need to check what 'context' the character is in. A single forgotten check can lead to a character that can use an out-of-combat ability while in combat. And even if you never forget to add checks, you face a large amount of code that is not checked at compile-time but at runtime instead.

public class Character 
{
    public BattleRound fight(Enemy npc) 
    {
        return new BattleRound(this, npc);
    }
}

public class BattleRound
{
    public void useAbility(CombatAbility ability)
    {
        /*...*/
    }
}

The above example has your character enter a round of combat when they fight an enemy. A character can use an ability within a round, attack, cast a spell... and these actions are enforced by the BattleRound (or BattleTurn if you prefer that term). Another advantage of this design is that you can even limit the actions according to the class: the WizardCharacter could return a WizardBattleRound which does not allow a hit but does allow a cast or a heal. The effects of the actions on round could perform actions on character such as reduce hitpoints, add hitpoints, add certain spell effects, etc...

2
  • You make a good point here, although it's important to note that fight must check whether there is a currently active BattleRound and respond sensibly. Also useAbility (and hit and cast and heal) must check whether the battleround is already over or not.
    – back2dos
    Mar 7, 2016 at 9:14
  • 1
    You have to validate your state transitions, yes. And it's a lot easier to do if you clearly separate your states into separate classes.
    – JDT
    Mar 7, 2016 at 9:52
1

Why throw errors when you can just ignore these "invalid" calls.

Unless your objective is to teach the developer, the user of your interface, proper protocol, you might want to take a leave out of the direct user interface book.

A direct user interface (the steering wheel and the accelerator in a car are examples of direct user interfaces) would just ignore invalid use - do nothing if the context isn't correct.

You can press down on the accelerator in a car all you want, but unless the car's engine is on, nothing much will happen (apart from you drowning the engine in fuel). When the car's engine is on and the car is in gear and the clutch is engaged, you may go somewhere. Unless someone is also standing on the break.

Most people deal pretty effectively with this kind of interface.

3
  • 1
    A lot of people also crash into walls because they panic and mistake the gas for the brake pedal. Mechanical systems, unlike virtual system, tend to have very little leeway when it comes to dealing with 'bad' inputs that might harm the system or the user. With the complexity of software being what it is, 'forcing' developers to do (or not do) something is very much worth considering. That being said, in a simple application (CRUD-like systems) it's often acceptable to enforce constraints only from the UI. But those systems are very simple at heart...
    – JDT
    Mar 7, 2016 at 8:54
  • 1
    @JDT Your analogy is off. Mistakenly accelerating your car into a wall is by all means a valid user interaction, detrimental as it may be. Throwing exceptions would be more like ejecting the driver if they step on the acceleration while the engine is off. It does not really help anyone. It is a great idea to have safety measures that prevent the car from hitting obstacles, but that is a different mechanism and one that must be decoupled from user input.
    – back2dos
    Mar 7, 2016 at 9:12
  • @JDT, they don't mistake. They panic and just push one of them regardless. But... I get your point. Then again, I didn't say to just enforce constraints from the UI. I am dead set against that. Just like validation, the server should always validate / enforce constraints. Any UI validation/enforcing of constraints is only a UI-acceleration issue and cannot be trusted by the server. It may after all be accessed directly. Mar 7, 2016 at 9:13
0

You are trying to decide what actions are allowed based on the current state of the system.

One way to model this is using a State Diagram or State Chart.

A way of representing a state diagram in code exists using the Finite State Machine or using the State Design Pattern.

In this case, you probably want to look at the State Diagram/Chart first, it will give you an idea of how to come up with your States, how to transition from one state to the next and how to incorporate actions. With this knowledge, you can build this into your program using the State Design Pattern.

0

I don't see these concerns as something to be enforced by your game entities like Character. Who calls these functions anyway? That's an important consideration.

If your game engine is such that your battle system/state restricts what functions it calls, then any kind of validation here in something like Character is just to make sure your one battle system implementation is working correctly (and that's a lot of extra code and checking and dependencies on external application state at a decentralized level to make sure it doesn't call the wrong functions in battle).

This game is intended to allow plugins/mods to be created by third parties, so that's another things to consider in this design.

Do the third parties implement their own battle systems or not? Because if not, then your battle system implementation is centralized to one place, and it's not going to be that difficult to make sure it doesn't call functions to talk to NPCs in battle.

If they can implement their own battle systems and can reprogram your entire battle system with that open of an architecture, then why restrict them from calling such functions if they can provide correct behavior to talk to NPCs mid-battle?

[...] perhaps something like OverworldContext or CombatContext or an adapter/proxy of some sort to separate things that can be done?

To me, if possible (and I could come up with examples for how to enforce these restrictions centrally at context-level without digging into game entity implementations that now have to be made aware of these contexts even if the contexts are overridable by third parties to close off that section of your architecture), and in the interest of decoupling, the context enforces the restrictions of what functions it can call on a game entity, not every single game entity type making sure its functions are called in the proper context(s). Put restrictions outside on the caller if possible, not try to detect misuse from the callee (which would then have to be made aware of so much more information to determine when it can and can't be called).

I find decoupling one of the most beneficial SE concepts to simplify and reduce the cost of changes while yielding more practical designs that are easier to test, and put very crudely you can understand it as minimizing the amount of information something needs to know in order to work. Making entities aware of the contexts in which they are used is making them depend on a whole lot more external information, even if the contexts are very abstract.

If you absolutely can't avoid this, then I'd at least recommend NVI (non-virtual interface): https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Non-Virtual_Interface ... to minimize the places you have to enforce such checks. But I really don't think you should be checking for this stuff personally and just enforce the appropriate constraints in your contexts.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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