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.