Sign in to follow this  

State Design

Recommended Posts

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?

Share this post


Link to post
Share on other sites
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.

Share this post


Link to post
Share on other sites

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?

Edited by Seer

Share this post


Link to post
Share on other sites

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.

Share this post


Link to post
Share on other sites

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

Share this post


Link to post
Share on other sites

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 ...

Edited by Alundra

Share this post


Link to post
Share on other sites

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.

Share this post


Link to post
Share on other sites

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.

Share this post


Link to post
Share on other sites

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.

Share this post


Link to post
Share on other sites

Thanks very much Kylotan for getting back to me and sorry about posting too much code, I was just trying to be as detailed about the problem as possible.

 

From what you say, I can picture an abstract base Controller class that holds a reference to an Entity, something like:

public abstract class Controller
{
    protected Entity entity;

    protected Controller(Entity entity)
    {
        this.entity = entity;
    }

    public abstract void execute();
} 

 From this, specific types of Controllers might be derived such as, as discussed, an InputController or an AIController.

 

Now the thing with LibGDX, is if you want to do event driven input handling, you have to use its InputProcessor in some way. As I've shown, the way I'm using it is by sub-classing it with GameInputProcessor. Since you are constrained in how you can handle input with the InputProcessor (you can't directly handle multiple key down or key up events in one frame), the only way I've been able to work out how to deal with multiple keys at once is by setting flags on key events to reflect which keys have been set and unset.

 

Eventually dealing with input this way has led to the Bools I talked about such as isRightKeyPressed or isMeleeKeyPressed.

 

If instead of having the Player respond to events from the InputProcessor with handlekeyDown(int keyCode) and handleKeyUp(int keyCode), should the InputController hold those functions and respond to the input events? If so, should it hold flags as the Player currently does, instead of the Player, and set and unset them to reflect the current state of the keys?

 

...and tell it which state changes to make.

This is where I'm really lost. How exactly should the Controller tell the Entity which state to change to? Should it check combinations of all the flags with if-else's and in a top down way from there change the states? With flags (which currently are the only way I can see how to handle multiple keys at once), that's the only way I could see that possibly working and it would be really messy.

 

It would really help if I could see some example code showing how this might work. Currently I can't imagine a good approach.

Share this post


Link to post
Share on other sites

"you have to use its InputProcessor in some way. As I've shown, the way I'm using it is by sub-classing it with GameInputProcessor" - That's fine.

"you can't directly handle multiple key down or key up events in one frame" - usually you wouldn't need to. Usually you can just react to events as they happen. But sure, cache the results if you want, for some reason.

"the Bools I talked about such as isRightKeyPressed or isMeleeKeyPressed" - but this is bad because you've let your game logic (e.g. move right, Melee) leak into your input handling code. Those bools should be in the input system and should just reflect the state of input devices. They don't need to know about the player or the game. That comes later, via whatever code links the input to the entity.

"How exactly should the Controller tell the Entity which state to change to?" - Well, it probably shouldn't. The controller needs to convert low level input events (key presses) into game logic events (move left, jump). It could simply call the relevant function on the entity, e.g. this.entity.Jump(). That function would handle any relevant state changes.

To summarise:

  1. Input handler should only care about input devices and know nothing about the game or entities. It may need to buffer input events so that it can remember the current state.
  2. A Controller is the link between a source of input and a game logic entity. This is a good candidate for where key bindings live; it can examine the input handler and call game-specific code accordingly.
  3. Only the entity should know and care about its internal states and how to transition between them.

Share this post


Link to post
Share on other sites
Those bools should be in the input system

Are you referring to GameInputprocessor or InputController?

 

LibGDX has a set of what I think are enums which reflect the keyboard keys in Input.Keys. For example, it has Keys.UP, Keys.DOWN, Keys.DPAD_UP. Is this what you are referring to when you say the bools should be in the input system?

 

I'm just wondering where the 'things' that tell you what keys were pressed should be or if you even need to have a set of individual bools to reflect that.

 

I don't understand why methods like moveLeft() or jump() would change the entities state. Wouldn't they just be responsible for adjusting entity data, and perhaps handling logic around the data they're manipulating if necessary? I feel like one of the elegant things about states is that once set up, they take care of themselves, from within. I don't think they should be manipulated like that from without.

Edited by Seer

Share this post


Link to post
Share on other sites

It turns out that you don't need to maintain these bools anyway because LibGDX offers functions like isButtonPressed. So that can be incorporated in your input handler/processor.

If you want your states to handle everything about an entity then instead of explicit methods like Jump, WalkLeft, etc, just create messages for each of those and pass them in to the state, and let the state decide what to do. Or, ditch the events and let the states continue to poll for input (ideally via an intermediate controller object that isolates your game logic from low level key handling).

"I feel like one of the elegant things about states is that once set up, they take care of themselves, from within." Sure, that's elegant, from the perspective of the state. It's not elegant for the code as a whole if each of those states needs to know a lot about the rest of the code in order to do anything useful, such as whether a key is down, or whether it's operating as part of a player, etc.

Besides, as it stands your states aren't particularly re-usable as they have hard-coded transitions. To make this system really flexible you'd want to set up transitions externally so that different entities could have different conditions for transitioning between states.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this