State Design

Started by
12 comments, last by Kylotan 6 years, 12 months ago

States are often used to manage game logic complexity. When used with objects, they help to break everything these objects can do up into manageable chunks. I quite like the idea of states but there is something I'm not quite sure about when using them.

When designing states for your Player object, such as Movement or Attacking states, no matter how general and universally applicable they may seem, on some level they need to deal with Player specific operations. This becomes reflected in their implementation details. Since in most games the Player is what handles user input, if the Player is utilising states to manage how it handles input, then those states must deal with Player specific input-related members. This is a problem if you want the states to be usable by any type of object.

If the states used by the Player need to utilise Player specific members, their general applicability is lost and they can no longer be used by other types of objects which may share the need for some or all of the same states as the Player. Such a kind of object might be an Enemy. An Enemy might also have need of states such as Movement or Attacking states. The type of state would be the same, but what they do for the object would be different. Enemy states for example would not respond to user input, but some kind of AI to perform their actions.

Due to this, is it considered reasonable to duplicate sets of states for different types of objects, where the type of state is largely the same, but how they enable the object using them is different?

Advertisement
If I understand you right, you want both player entities and enemy entities to use similar states like for movement or attacking. The difficulty you're facing is that even though both types of entities would use the same set of states, the states for players would have to handle the game's input while the states for enemies would have to handle AI.
First, how similar are the player entities and enemy entities themselves? Say, are they two separate classes, two subclasses of another class, or the same class?
Second, it sounds like the states for both players and enemies deal with "input", and they only differ in where the input comes from (either game input or AI). Is it possible to wrap the game's input and enemy AI in some kind of "input source" that can be assigned to or otherwise accessed from your states? So you could reuse your states between players and enemies while setting them up with the right type of input source.

It would be more concise for states to be usable by any kind of entity, but I am somewhat ambivalent about creating duplicate states if necessary. What I would like to know is if it is unreasonable to do so.

First, how similar are the player entities and enemy entities themselves? Say, are they two separate classes, two subclasses of another class, or the same class?

Player currently is its own abstract class with derived types defining the specifics.

This has not yet been implemented, but I imagine Enemy would be its own abstract class with different types of enemies derived from it as well.

Second, it sounds like the states for both players and enemies deal with "input"

I am currently only working with Player types, but yes the Player class currently is responsible for handling input events by taking the results of an event driven InputProcessor and setting flags to determine which keys are down and which are up, based on those results. There are other flags whose state is derived from the key state and timings of key presses and releases. This is all information the states the Player uses currently need.

Is it possible to wrap the game's input and enemy AI in some kind of "input source" that can be assigned to or otherwise accessed from your states?

I don't know. How might something like that be done?

Is it possible to wrap the game's input and enemy AI in some kind of "input source" that can be assigned to or otherwise accessed from your states?

I don't know. How might something like that be done?

When it is done, often there is a base class for the character controller, and a base class for the character to be controlled.

The character controller accepts a reference to any character. It doesn't matter what the character is. The character controller calls functions in the character to make it move, jump, interact, whatever you need. The character controller might accept input from the keyboard, from the mouse, from a VR controller, from across the network, from assorted AI systems, or wherever else.

The actual interfaces will be unique to your game.

Done well, this allows games to easily swap out players. An AI friendly unit can be replaced with a second human, or with a remote player joining the game. A human player that drops out can be replaced with a friendly AI. Character controllers for AI can be more easily developed for specialized situations, like a water-based controller for AI, a land-based controller for AI, an airship-based controller for AI, and so on. You can do similar situational controls and adapters for the human inputs.

Thanks frob, but could you give an example? I'm struggling to see how this would tie in with the states.

Since in most games the Player is what handles user input

You may have a loose definition of "player" or maybe you have a class used in the right place and is for some reason called "Player", but for the purpose of this reply I will assume the most likely case in which you mean to say that a character gets input from the human and uses that to move itself.
This is absolutely not what any game does, and I have written extensively about it.

https://www.gamedev.net/topic/650640-is-using-static-variables-for-input-engine-evil/#entry5113267

Additionally, letting the player character handle its own input is flawed; the character then needs to know more about its surroundings than it otherwise should. For example, if the character is in the air it should not be able to jump again. Now the character class needs to know about the physics engine to get information as to whether or not it is on the ground to decide if jumping is possible.


Use frob's post to fill in some implementational details. Conceptually, a higher-level object (an object controller) examines input and decides what to do with it, using information it has about the input itself, the game world, the game rules, and the character. Obviously something with this level of access to information is very high-level.

Where do you put states? Wherever you need them.
I get the feeling you are thinking, "Should I put states here, or should I put them there?", when you should be thinking, "Would states be helpful here? Would they also be helpful there?"

You are expected to have many sets of states for different groups of purposes. Your character might have a set of states that determine some of its overall properties such as being alive or dead, poisoned or normal, etc.
You may have a group of states to determine which animation to play (keep in mind that states used specifically for rendering should never be used for logic).
You may have another group of states on your character controller to help determine how to process input (and again a character controller has access to more information than just its own states, so decisions can be made based off its own states and combinations of character states and game states).

An example of where the character controller might source data from various places:
Jump button has been pressed.
- Check character controller state. Are we receiving input? We might be in a no-input state for cinematics. But we are not so we continue.
- Check character's state. Is it alive? It is.
- Check character's properties. Is it on the ground? The physics engine should apply this property once-per-frame so that it can be queried efficiently. Character is on the ground.
- Etc.


States go where you need them.


L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

One design suggestion for you about all that said :
- ActionManager : It contains AddAction, IsActionActive and ProcessEvent which is called to update the state of the action manager based on the event loop.
- Action : ActionManager store an array of action which is an ID + [gamepad, mouse, key, modifiers]
You can have a function to convert string to input to set the input from a configuration file.
As said before, the input must not be done in the character class but in the game state update for exemple or a script which does the game logic.
If you have multiple character you should do a base class Character with some virtual and inherit from it for each character.
Then you can set what is the current character and call the good virtual, as said previously.
As L. Spiro said, you have to check multiple things : cut-scene, alive, touch the ground ...

I appreciate all the feedback, but I don't have much experience with the concepts being introduced and so things are becoming too abstract for me to follow. I would like to get more concrete and show you exactly where I'm at currently with my code. Hopefully you'll bear with me :)

I wrote this code, in Java using LibGDX, last summer and have been restructuring it over the week, so please excuse the incoherent mess in parts (or throughout :P ). There is quite a lot of the code that I wouldn't mind being reviewed, but to try to keep things focused I'll only paste what I think are the relevant parts here. For anyone interested in having a look at the full source here's a link to the Github repository https://github.com/RustedBot/DragonballZRPG/tree/refactoring/core/src/com/dragonballzrpg

I'll start off by showing how user input is processed.


public class GameInputProcessor implements InputProcessor
{
    private List<InputHandler> inputHandlers;

    public GameInputProcessor()
    {
        inputHandlers = new ArrayList<InputHandler>();
    }

    public void addAll(Collection<? extends InputHandler> inputHandlers)
    {
        this.inputHandlers.addAll(inputHandlers);
    }

    public void add(InputHandler inputHandler)
    {
        if(!inputHandlers.contains(inputHandler)) inputHandlers.add(inputHandler);
    }

    public void remove(InputHandler inputHandler)
    {
        if(inputHandlers.contains(inputHandler)) inputHandlers.remove(inputHandler);
    }

    @Override
    public boolean keyDown(int keycode)
    {
        for(InputHandler inputHandler : inputHandlers)
        {
            inputHandler.handleKeyDown(keycode);
        }

        return false;
    }

    @Override
    public boolean keyUp(int keycode)
    {
        for(InputHandler inputHandler : inputHandlers)
        {
            inputHandler.handleKeyUp(keycode);
        }

        return false;
    }
    
    // Other types of input handling methods below. Not relevant.
}

Here I have a GameInputProcessor class which holds a List of InputHandlers. InputHandler is an interface with the methods handleKeyDown(int keyCode) and handleKeyUp(int keyCode). GameInputProcessor receives its InputHandlers from the PlayScreen class, which is where the game objects are created, updated and rendered currently.

To handle key presses and releases, the GameInputProcessor passes on the keycode in the respective key handling methods to the InputHandlers. This way any InputHandler can deal with input in the way that is most appropriate for it.

What are your thoughts on handling input this way?

Next are the Player classes.


public abstract class Player extends Entity implements InputHandler
{
    protected OrthographicCamera camera;
    public Bool isUpKeyPressed;
    public Bool isDownKeyPressed;
    public Bool isLeftKeyPressed;
    public Bool isRightKeyPressed;
    public Bool isMeleeKeyPressed;
    public Bool isReadyToRunUp;
    public Bool isReadyToRunDown;
    public Bool isReadyToRunLeft;
    public Bool isReadyToRunRight;
    public Bool runWindowOpen;
    public Bool canAttack;
    private double elapsedRunWindowTime;
    private double runWindowDuration;
    private double runSpeed;
    private int up, down, left, right, melee;

    public Player(Vector2 position, double speed, Map<AnimationName, Animation> animations, Animation currentAnimation,
                  Map<SoundName, Sound> sounds, int up, int down, int left, int right, int melee)
    {
        super(position, speed, animations, currentAnimation, sounds);
        runSpeed = speed * 2;

        isUpKeyPressed = new Bool();
        isDownKeyPressed = new Bool();
        isLeftKeyPressed = new Bool();
        isRightKeyPressed = new Bool();
        isMeleeKeyPressed = new Bool();
        isReadyToRunUp = new Bool();
        isReadyToRunDown = new Bool();
        isReadyToRunLeft = new Bool();
        isReadyToRunRight = new Bool();
        runWindowOpen = new Bool();
        canAttack = new Bool(true);

        this.up = up;
        this.down = down;
        this.left = left;
        this.right = right;
        this.melee = melee;

        initialiseStates();

        elapsedRunWindowTime = 0.0d;
        runWindowDuration = .3d;
    }

    private void initialiseStates()
    {
        states.put(StateName.STANDING, new StandingState());
        states.put(StateName.WALKING_UP, new WalkingUpState());
        states.put(StateName.WALKING_DOWN, new WalkingDownState());
        states.put(StateName.WALKING_LEFT, new WalkingLeftState());
        states.put(StateName.WALKING_RIGHT, new WalkingRightState());
        states.put(StateName.WALKING_UP_LEFT, new WalkingUpLeftState());
        states.put(StateName.WALKING_UP_RIGHT, new WalkingUpRightState());
        states.put(StateName.WALKING_DOWN_LEFT, new WalkingDownLeftState());
        states.put(StateName.WALKING_DOWN_RIGHT, new WalkingDownRightState());
        states.put(StateName.RUNNING_UP, new RunningUpState());
        states.put(StateName.RUNNING_DOWN, new RunningDownState());
        states.put(StateName.RUNNING_LEFT, new RunningLeftState());
        states.put(StateName.RUNNING_RIGHT, new RunningRightState());
        states.put(StateName.RUNNING_UP_LEFT, new RunningUpLeftState());
        states.put(StateName.RUNNING_UP_RIGHT, new RunningUpRightState());
        states.put(StateName.RUNNING_DOWN_LEFT, new RunningDownLeftState());
        states.put(StateName.RUNNING_DOWN_RIGHT, new RunningDownRightState());
        states.put(StateName.MELEEING_UP, new MeleeingUpState());
        states.put(StateName.MELEEING_DOWN, new MeleeingDownState());
        states.put(StateName.MELEEING_LEFT, new MeleeingLeftState());
        states.put(StateName.MELEEING_RIGHT, new MeleeingRightState());

        for(State state : states.values())
        {
            state.initialiseTransitions(this);
        }

        currentState = states.get(StateName.STANDING);
    }

    protected void setKeys(int keyCode)
    {
        if(keyCode == up)
        {
            isUpKeyPressed.set(true);
        }
        else if(keyCode == down)
        {
            isDownKeyPressed.set(true);
        }
        else if(keyCode == left)
        {
            isLeftKeyPressed.set(true);
        }
        else if(keyCode == right)
        {
            isRightKeyPressed.set(true);
        }
        else if(keyCode == melee)
        {
            isMeleeKeyPressed.set(true);
        }
        else
        {
            return;
        }

        if(!runWindowOpen.value()) runWindowOpen.set(true);
    }

    protected void unsetKeys(int keyCode)
    {
        if(keyCode == up)
        {
            isUpKeyPressed.set(false);
            if(runWindowOpen.value()) isReadyToRunUp.set(true);
        }
        else if(keyCode == down)
        {
            isDownKeyPressed.set(false);
            if(runWindowOpen.value()) isReadyToRunDown.set(true);
        }
        else if(keyCode == left)
        {
            isLeftKeyPressed.set(false);
            if(runWindowOpen.value()) isReadyToRunLeft.set(true);
        }
        else if(keyCode == right)
        {
            isRightKeyPressed.set(false);
            if(runWindowOpen.value()) isReadyToRunRight.set(true);
        }
        else if(keyCode == melee)
        {
            isMeleeKeyPressed.set(false);
            canAttack.set(true);
        }
    }

    protected void checkRunWindow()
    {
        if(runWindowOpen.value())
        {
            elapsedRunWindowTime += Gdx.graphics.getDeltaTime();

            if(elapsedRunWindowTime >= runWindowDuration)
            {
                elapsedRunWindowTime = 0.0d;
                runWindowOpen.set(false);
                isReadyToRunUp.set(false);
                isReadyToRunDown.set(false);
                isReadyToRunLeft.set(false);
                isReadyToRunRight.set(false);
            }
        }
    }

    public double getRunSpeed()
    {
        return runSpeed;
    }
}

public class TeenFutureTrunks extends Player
{
    public TeenFutureTrunks(OrthographicCamera camera, Vector2 position, double speed,
                            Map<AnimationName, Animation> animations, Animation currentAnimation,
                            Map<SoundName, Sound> sounds, int up, int down, int left, int right, int melee)
    {
        super(position, speed, animations, currentAnimation, sounds, up, down, left, right, melee);
        this.camera = camera;
    }

    @Override
    public void update()
    {
        currentState.update(this);
        currentAnimation.update();
        checkRunWindow();

        camera.position.x = (int)position.x + width / 2.0f;
        camera.position.y = (int)position.y + height / 2.0f;
    }

    @Override
    public void render(SpriteBatch batch)
    {
        batch.draw(currentAnimation.getCurrentFrame(), (int)position.x, (int)position.y);
    }

    @Override
    public void handleKeyDown(int keyCode)
    {
        setKeys(keyCode);
    }

    @Override
    public void handleKeyUp(int keyCode)
    {
        unsetKeys(keyCode);
    }
}

When input is received TeenFutureTrunks tells the Player class to set and unset the relevant keys. This translates to a set of key related Bools being made true or false. Bool is a wrapper which holds a boolean. I'll get into why I had to do this in a bit.

TeenFutureTrunks is responsible for updating the current state and animation and adjusting the camera. This is something I did when I was writing the code last year and I'm not so sure getting the player to control the camera like this is the right way to go. More appropriate I think would be to give the camera the current Players position and have the camera update itself with that position, wherever it's located in the code. I've left it like this for now though since it works. Any advice on better managing the camera would be great.

Okay, on to the states.


public abstract class State
{
    protected List<Transition> transitions;
    protected double currentStateDuration;

    public State()
    {
        transitions = new ArrayList<Transition>();
        currentStateDuration = 0.0d;
    }

    public abstract void initialiseTransitions(Player player);
    public abstract void enter(Entity entity);
    public abstract void exit(Entity entity);
    public abstract void update(Entity entity);
    public abstract void render(Entity entity, SpriteBatch batch);

    protected <T> T getRandomValue(T[] values)
    {
        Random random = new Random();
        int value = random.nextInt(values.length);

        return values[value];
    }
} 

All states derive from an abstract State class. The State holds a List of Transitions each state can cycle through to go to other states. The State class has enter(), exit(), update() and render() methods, and currently an initialiseTransitions() method which takes in a Player. Not good of course, as it limits the scope of the Transitions. It's something I'm hoping to improve.

The Player has a set of states, consisting of states such as StandingState, WalkingDownState, WalkingRightState, RunningUpState, MeleeingLeftState and so on. The set of states you see in the Player class are not all the states the Player can be in. I also have in mind states such as TransformingState, FiringEnergyBlastState and UnleashingSpecialAttackState, in the various directions.

I have not yet implemented all of the states, as I wanted to know what the best way to do so might be, based on feedback here. However, I have partially implemented StandingState, WalkingRightState, RunningRightState and MeleeingRightState, to test out Transitions between them. The problem as mentioned is that the Transitions are Player member dependent, locking the states into being usable only by the Player.

The partially implemented states look like this.


public class StandingState extends State
{
    @Override
    public void initialiseTransitions(Player player)
    {
        transitions.add(new Transition(player.states.get(StateName.WALKING_RIGHT), new AnimationName[]{AnimationName.WALK_RIGHT},
        new TransitionCondition[]
        {
            new TransitionCondition(player.isRightKeyPressed, true)
        }));

        transitions.add(new Transition(player.states.get(StateName.RUNNING_RIGHT), new AnimationName[]{AnimationName.RUN_RIGHT},
        new TransitionCondition[]
        {
            new TransitionCondition(player.isRightKeyPressed, true),
            new TransitionCondition(player.isReadyToRunRight, true)
        }));

        transitions.add(new Transition(player.states.get(StateName.MELEEING_RIGHT),
                        new AnimationName[]{AnimationName.PUNCH_RIGHT_1, AnimationName.PUNCH_RIGHT_2, AnimationName.KICK_RIGHT},
        new TransitionCondition[]
        {
            new TransitionCondition(player.isMeleeKeyPressed, true),
            new TransitionCondition(player.canAttack, true)
        }));
    }

    @Override
    public void enter(Entity entity)
    {

    }

    @Override
    public void exit(Entity entity)
    {

    }

    @Override
    public void update(Entity entity)
    {
        for(Transition transition : transitions)
        {
            transition.update((Player)entity);
        }
    }

    @Override
    public void render(Entity entity, SpriteBatch batch)
    {

    }
}

public class WalkingRightState extends State
{
    @Override
    public void initialiseTransitions(Player player)
    {
        transitions.add(new Transition(player.states.get(StateName.STANDING), new AnimationName[]{AnimationName.FACE_RIGHT},
        new TransitionCondition[]
        {
            new TransitionCondition(player.isRightKeyPressed, false)
        }));

        transitions.add(new Transition(player.states.get(StateName.MELEEING_RIGHT),
                        new AnimationName[]{AnimationName.PUNCH_RIGHT_1, AnimationName.PUNCH_RIGHT_2, AnimationName.KICK_RIGHT},
        new TransitionCondition[]
        {
            new TransitionCondition(player.isMeleeKeyPressed, true),
            new TransitionCondition(player.canAttack, true)
        }));
    }

    @Override
    public void enter(Entity entity)
    {

    }

    @Override
    public void exit(Entity entity)
    {

    }

    @Override
    public void update(Entity entity)
    {
        for(Transition transition : transitions)
        {
            transition.update(entity);
        }

        entity.position.x += entity.getSpeed() * Gdx.graphics.getDeltaTime();
    }

    @Override
    public void render(Entity entity, SpriteBatch batch)
    {

    }
}

public class RunningRightState extends State
{
    @Override
    public void initialiseTransitions(Player player)
    {
        transitions.add(new Transition(player.states.get(StateName.STANDING), new AnimationName[]{AnimationName.FACE_RIGHT},
        new TransitionCondition[]
        {
            new TransitionCondition(player.isRightKeyPressed, false)
        }));

        transitions.add(new Transition(player.states.get(StateName.MELEEING_RIGHT),
                        new AnimationName[]{AnimationName.PUNCH_RIGHT_1, AnimationName.PUNCH_RIGHT_2, AnimationName.KICK_RIGHT},
        new TransitionCondition[]
        {
            new TransitionCondition(player.isMeleeKeyPressed, true),
            new TransitionCondition(player.canAttack, true)
        }));
    }

    @Override
    public void enter(Entity entity)
    {
        entity.sounds.get(SoundName.RUNNING).loop();
    }

    @Override
    public void exit(Entity entity)
    {
        entity.sounds.get(SoundName.RUNNING).stop();
    }

    @Override
    public void update(Entity entity)
    {
        for(Transition transition : transitions)
        {
            transition.update(entity);
        }

        ((Player)entity).position.x += ((Player)entity).getRunSpeed() * Gdx.graphics.getDeltaTime();
    }

    @Override
    public void render(Entity entity, SpriteBatch batch)
    {

    }
}

public class MeleeingRightState extends State
{
    @Override
    public void initialiseTransitions(Player player)
    {
        transitions.add(new Transition(player.states.get(StateName.STANDING), new AnimationName[]{AnimationName.FACE_RIGHT},
        new TransitionCondition[]
        {
            new TransitionCondition(player.isRightKeyPressed, false)
        }));

        transitions.add(new Transition(player.states.get(StateName.WALKING_RIGHT), new AnimationName[]{AnimationName.WALK_RIGHT},
        new TransitionCondition[]
        {
            new TransitionCondition(player.isRightKeyPressed, true)
        }));
    }

    @Override
    public void enter(Entity entity)
    {
        Player player = (Player)entity;

        player.canAttack.set(false);
        player.sounds.get(getRandomValue(new SoundName[]{SoundName.MELEE_1, SoundName.MELEE_2})).play();
    }

    @Override
    public void exit(Entity entity)
    {
        
    }

    @Override
    public void update(Entity entity)
    {
        currentStateDuration += Gdx.graphics.getDeltaTime();

        if(currentStateDuration >= entity.currentAnimation.getDuration())
        {
            currentStateDuration = 0.0d;
            entity.currentAnimation.reset();

            for(Transition transition : transitions)
            {
                transition.update(entity);
            }
        }
    }

    @Override
    public void render(Entity entity, SpriteBatch batch)
    {

    }
}

Transitions between these states works, mostly. A problem is going from StandingState to a specific MeleeingState. Either I'd need to come up with some flag based solution to get these transitions working correctly or just go with FacingUp/Down/Left/RightStates to make sure that when standing the Player transitions to the correct MeleeingState.

To show you how the states transition between each other, here are Transition and TransitionCondition.


public class Transition
{
    private State state;
    private AnimationName[] animationNames;
    private List<TransitionCondition> transitionConditions;

    public Transition(State state, AnimationName[] animationNames, TransitionCondition[] transitionConditions)
    {
        this.state = state;
        this.animationNames = animationNames;
        this.transitionConditions = new ArrayList<TransitionCondition>();

        for(TransitionCondition transitionCondition : transitionConditions)
        {
            this.transitionConditions.add(transitionCondition);
        }
    }

    public void update(Entity entity)
    {
        for(TransitionCondition transitionCondition : transitionConditions)
        {
            if(!transitionCondition.isValid()) return;
        }

        entity.currentAnimation = entity.animations.get(getRandomAnimationName(animationNames));
        entity.currentState.exit(entity);
        entity.currentState = state;
        entity.currentState.enter(entity);
    }

    private AnimationName getRandomAnimationName(AnimationName[] animationNames)
    {
        Random random = new Random();

        return animationNames[random.nextInt(animationNames.length)];
    }
}

public class TransitionCondition
{
    private Bool condition;
    private boolean requiredValue;

    public TransitionCondition(Bool condition, boolean requiredValue)
    {
        this.condition = condition;
        this.requiredValue = requiredValue;
    }

    public boolean isValid()
    {
        return condition.equals(requiredValue);
    }
}

Every Transition takes in a State to transition to, an array of AnimationName enums in the case of needing a random Animation from a set and an array of TransitionConditions. In update(), it goes through all of its TransitionConditions, checking if they are valid. If all are valid, it proceeds to setting the entities current state to the state passed in on construction. If any are not valid, it returns and no transition takes place.

I said earlier that I had to make the conditionals Bools. The reason is because each Transitions set of TransitionConditions is set on construction and so for the validation checks to be meaningful, the condition each TransitionCondition holds must be a reference to the original source, so that it changes along with source changes. I don't think using a wrapper like this is particularly nice, but it's what I could think of to get Transitions working.

My main question after all this is:

  • How can the code be improved so that the States, and Transitions between them, are entity independent and AI based transitions can be easily incorporated?

I hope what I've written makes some kind of sense. If anything is unclear let me know and I'll do my best to clarify. Thank you.

Would someone please mind helping me with this? I'm just looking for an example of the controller thingy that was mentioned and a possible way in which I might incorporate it into my code. Thanks.

You posted far too much code for us, unfortunately. I'm not going to read it all. All I can do is summarise what has been said before and maybe rephrase it:

  • Input is not something in your game world, and therefore shouldn't really be part of an entity.
  • Therefore, handle input in a separate object, which can reference an in-game entity and tell it which state changes to make. Such an object is often called a 'controller'.
  • (Generally speaking, try to avoid 'Player' objects because they often end up being dumping grounds for game logic, character logic, view logic, and input logic. A player is usually just another character, maybe with some special abilities.)
  • Using state objects for handling entity states is fine.
  • Since a controller doesn't need a 'Player' object and could control any Entity, you don't need to "reach out" into the 'Player' object to implement your transitions via checking inputs
  • Postpone resolving a state name until the transition actually happens - that way, you don't need to hardcode the "player.states.get" stuff inside initialiseTransitions. Alternatively, states could be inside an entity, not a player.

This topic is closed to new replies.

Advertisement