Animate Player using Finite State Machine in Ashley ECS

662 Views Asked by At

I have a big school project and I'm developing a Pixel Platformer game using libGDX.

I'm using ECS for the most part, but I've been having trouble rendering my player's animations because I can not distinguish the state of the player or at least the attack ones.

Player State Class

Let me just show you, so I have this "move" states:

public enum PlayerState implements State<PlayerAgent> {

Idle(){
    @Override
    public void update(PlayerAgent agent) {
        agent.moveOnGround();
        if(!agent.isTouchingGround) {
            if (agent.body.getLinearVelocity().y < -0.05)
                agent.stateMachine.changeState(Falling);
            else
                agent.stateMachine.changeState(Jumping);
        }else{
            if (agent.body.getLinearVelocity().x != 0)
                agent.stateMachine.changeState(Walking);
        }
    }
},

Walking() {
    @Override
    public void update(PlayerAgent agent) {
        agent.moveOnGround();
        if(!agent.isTouchingGround) {
            if (agent.body.getLinearVelocity().y < -0.05)
                agent.stateMachine.changeState(Falling);
            else
                agent.stateMachine.changeState(Jumping);
        }else{
            if (agent.body.getLinearVelocity().x == 0)
                agent.stateMachine.changeState(Idle);
        }
    }
},

Jumping() {
    @Override
    public void enter(PlayerAgent agent) {
    }

    @Override
    public void update(PlayerAgent agent) {
        agent.moveOnAir();
        if (agent.body.getLinearVelocity().y < 0)
            agent.stateMachine.changeState(Falling);
        /* else if (agent.jumpOnAir())
            agent.stateMachine.changeState(DoubleJumping);*/
    }
},

DoubleJumping () {
    @Override
    public void update(PlayerAgent agent) {
        agent.moveOnAir();
        if (agent.body.getLinearVelocity().y < 0)
            agent.stateMachine.changeState(Falling);
    }
},

Falling() {

    @Override
    public void update(PlayerAgent agent) {
        agent.moveOnAir();
        if (agent.isTouchingGround) {
            agent.stateMachine.changeState(Idle);
        } else {
            if (agent.stateMachine.getPreviousState() != DoubleJumping) {
                if (agent.jumpOnAir())
                    agent.stateMachine.changeState(DoubleJumping);
            }
        }
    }
};

@Override
public void enter(PlayerAgent agent) {
    // System.out.println(this.toString());
}

@Override
public void update(PlayerAgent agent) {

}

@Override
public void exit(PlayerAgent agent) {
    agent.timer = 0.0f;
}

@Override
public boolean onMessage(PlayerAgent agent, Telegram telegram) {
    return false;
}
}

Player Animations Enum

But I'll need at least this states for player animation:

public enum PlayerAnimations {

 Idle, Walking, Jumping, DoubleJumping, Falling, Attack, JumpAttack, FallingAttack, FallAttack, Hit, Die;

}

And many more has u can see on this packed texture: Player Animations Atlas

PlayerAgent Class:

public class PlayerAgent implements Updateable {

    private Entity player;

    protected Body body;
    private TransformComponent transform;
    SensorCollisionComponent sensors;

    protected static StateMachine<PlayerAgent, PlayerState>  stateMachine;

    public boolean isTouchingGround = true;
    public boolean isTouchingWallLeft = false;
    public boolean isTouchingWallRight = false;

    public static float timer = 0.0f;

    public PlayerAgent(Entity player) {
        this.player = player;
        body = player.getComponent(B2dBodyComponent.class).body;
        transform = player.getComponent(TransformComponent.class);
        sensors = player.getComponent(SensorCollisionComponent.class);

        stateMachine = new DefaultStateMachine<PlayerAgent, PlayerState>(this, PlayerState.Idle);
    }

    @Override
    public void update(float deltaTime) {
        isTouchingGround = (sensors.numFoot > 0);
        isTouchingWallLeft = (sensors.numLeftWall > 0);
        isTouchingWallRight = (sensors.numRightWall > 0);

        stateMachine.update();

        if (!KeyboardController.left && !KeyboardController.right)
            body.setLinearVelocity(MathUtils.lerp(body.getLinearVelocity().x, 0, 0.2f), body.getLinearVelocity().y);

    }


    public static PlayerState getCurrentState(){
        return stateMachine.getCurrentState();
    }
    public static PlayerState getLastState(){
        return stateMachine.getPreviousState();
    }
    public static boolean isInState(PlayerState state){
        return stateMachine.isInState(state);
    }

    public boolean moveOnGround() {
        if (KeyboardController.left) {
            body.setLinearVelocity(MathUtils.lerp(body.getLinearVelocity().x, -3f, 0.1f), body.getLinearVelocity().y);
            transform.flipX = true;
        }
        if (KeyboardController.right){
            body.setLinearVelocity(MathUtils.lerp(body.getLinearVelocity().x, 3f, 0.1f), body.getLinearVelocity().y);
            transform.flipX = false;
        }
        if (KeyboardController.up) {
            body.applyLinearImpulse(0, 2.7f, body.getWorldCenter().x, body.getWorldCenter().y, true);
            KeyboardController.up = false;
        }
            return (KeyboardController.left || KeyboardController.right);
    }

    public boolean moveOnAir(){
        if (KeyboardController.left){
            body.setLinearVelocity(MathUtils.lerp(body.getLinearVelocity().x, -1.5f, 0.1f), body.getLinearVelocity().y);
            transform.flipX = true;
        }if (KeyboardController.right){
            transform.flipX = false;
            body.setLinearVelocity(MathUtils.lerp(body.getLinearVelocity().x, 1.5f, 0.1f), body.getLinearVelocity().y);
        }
            return (KeyboardController.left || KeyboardController.right);
    }

    public boolean jumpOnAir(){
        if (KeyboardController.up) {
            body.applyLinearImpulse(0, 2.5f, body.getWorldCenter().x, body.getWorldCenter().y, true);
            KeyboardController.up = false;
            return true;
        }
        return false;
    }

}

Animation Code

Code in the AnimationSystem that takes care of the player:

PlayerComponent pc = pm.get(entity);
                if (pc.attacking) {

                    switch (PlayerAgent.getCurrentState()) {
                        case Idle:
                        case Walking:
                            if(PlayerAgent.getLastState() == PlayerState.Falling) {
                                key = PlayerAnimations.FallAttack.ordinal();
                                if(ani.animations.get(key).isAnimationFinished(PlayerAgent.timer)) pc.attacking = false;
                            }else
                                key = PlayerAnimations.Attack.ordinal();
                            break;
                        case Jumping:
                        case DoubleJumping:
                            key = PlayerAnimations.JumpAttack.ordinal();
                            break;
                        case Falling:
                            key = PlayerAnimations.FallingAttack.ordinal();
                            break;
                        default:
                            key = PlayerAnimations.Idle.ordinal();
                            break;
                    }
                } else {
                    switch (PlayerAgent.getCurrentState()) {
                        case Idle:
                            key = PlayerAnimations.Idle.ordinal();
                            break;
                        case Walking:
                            key = PlayerAnimations.Walking.ordinal();
                            break;
                        case Jumping:
                            key = PlayerAnimations.Jumping.ordinal();
                            break;
                        case DoubleJumping:
                            key = PlayerAnimations.DoubleJumping.ordinal();
                            break;
                        case Falling:
                            key = PlayerAnimations.Falling.ordinal();
                            break;
                        default:
                            key = PlayerAnimations.Idle.ordinal();
                            break;
                    }

                }
                PlayerAgent.timer += deltaTime;
            }

I was thinking of having states inside a state like Grounded -> Idle, Walking. But I still don't know how I can make the player AttackStates.

1

There are 1 best solutions below

0
On

after some digging i found a solution, at least for now, so for anyone that has the same problem here is how i solved it.

PlayerAttackState.java

I started by creating a new attack state called PlayerAttackState.

public enum PlayerAttackState implements State<PlayerAgent> {


        NONE(){
            @Override
            public void enter(PlayerAgent agent) {
                agent.attacking = false;
                agent.lastAttack = System.currentTimeMillis();
            }

            @Override
            public void update(PlayerAgent agent) {
                if (KeyboardController.attack && !agent.attacking) {
                    agent.attacking = true;
                    KeyboardController.attack = false;
                    agent.attackStateMachine.changeState(Attacking);
                    //agent.playerComp.attack();
                }
            }
        },

        Attacking(){
            @Override
            public void enter(PlayerAgent agent) {

                agent.timer  = 0f;
                agent.attackCombo++;
                if (agent.attackCombo > 3 || System.currentTimeMillis() - agent.lastAttack > 100) agent.attackCombo = 1;
                if (agent.stateMachine.isInState(PlayerState.Jumping) || agent.stateMachine.isInState(PlayerState.DoubleJumping))
                    agent.attackStateMachine.changeState(AirAttack);

            }

            @Override
            public void update(PlayerAgent agent) {
                super.update(agent);
                if (agent.stateMachine.isInState(PlayerState.Jumping) || agent.stateMachine.isInState(PlayerState.DoubleJumping))
                    agent.attackStateMachine.changeState(AirAttack);

            }
        },

        AirAttack(){
            @Override
            public void enter(PlayerAgent agent) {
                if (agent.attackCombo >= 2) {
                    agent.attackCombo = 1;
                    agent.attackStateMachine.changeState(FallingAttack);
                }
            }

            @Override
            public void update(PlayerAgent agent) {
                super.update(agent);
                if (agent.attackCombo == 3) {
                    agent.attackCombo = 1;
                    agent.attackStateMachine.changeState(FallingAttack);
                }
            }
        },

        FallingAttack(){
            @Override
            public void enter(PlayerAgent agent) {

            }

            @Override
            public void update(PlayerAgent agent) {
                if (agent.stateMachine.isInState(PlayerState.Idle) || agent.stateMachine.isInState(PlayerState.Walking))
                    agent.attackStateMachine.changeState(FallAttack);
                else if (agent.stateMachine.isInState(PlayerState.DoubleJumping))
                    agent.attackStateMachine.changeState(AirAttack);
            }
        },

        FallAttack(){
            @Override
            public void update(PlayerAgent agent) {
                super.update(agent);
            }
        },

    ;

        @Override
        public void enter(PlayerAgent agent) {

        }

        @Override
        public void update(PlayerAgent agent) {
            if (agent.animation.isAnimationFinished(agent.timer))
                agent.attackStateMachine.changeState(NONE);
        }

        @Override
        public void exit(PlayerAgent agent) {
        }

        @Override
        public boolean onMessage(PlayerAgent agent, Telegram telegram) {
            return false;
        }
    }

PlayerAgent.java

So after this I needed to add a state machine for it in the PlayerAgent class and some other variables.

public class PlayerAgent implements Updateable {

    private Entity player;

    protected Body body;
    private TransformComponent transform;
    public SensorCollisionComponent sensors;
    public PlayerComponent playerComp;
    public AttackComponent attackComp;

    protected static StateMachine<PlayerAgent, PlayerState>  stateMachine;
    protected static StateMachine<PlayerAgent, PlayerAttackState> attackStateMachine;

    public boolean isTouchingGround = true;
    public boolean isTouchingWallLeft = false;
    public boolean isTouchingWallRight = false;
    public static boolean attacking = false;

    public static Animation animation = null;

    public static float timer = 0.0f;
    public static int attackCombo = 0;
    public long lastAttack = 0l;


    public PlayerAgent(Entity player) {
        this.player = player;
        body = player.getComponent(B2dBodyComponent.class).body;
        transform = player.getComponent(TransformComponent.class);
        sensors = player.getComponent(SensorCollisionComponent.class);
        playerComp = player.getComponent(PlayerComponent.class);
        attackComp = player.getComponent(AttackComponent.class);


        stateMachine = new DefaultStateMachine<PlayerAgent, PlayerState>(this, PlayerState.Idle);
        attackStateMachine = new DefaultStateMachine<PlayerAgent, PlayerAttackState>(this, PlayerAttackState.NONE);
    }

    @Override
    public void update(float deltaTime) {

        isTouchingGround = (sensors.numFoot > 0);
        isTouchingWallLeft = (sensors.numLeftWall > 0);
        isTouchingWallRight = (sensors.numRightWall > 0);

        stateMachine.update();
        attackStateMachine.update();

        if (!KeyboardController.left && !KeyboardController.right)
            body.setLinearVelocity(MathUtils.lerp(body.getLinearVelocity().x, 0, 0.2f), body.getLinearVelocity().y);

    }


    public static PlayerState getCurrentState(){
        return stateMachine.getCurrentState();
    }
    public static PlayerAttackState getAttackState(){return attackStateMachine.getCurrentState();}
    public static PlayerState getLastState(){
        return stateMachine.getPreviousState();
    }
    public static boolean isInState(PlayerState state){
        return stateMachine.isInState(state);
    }

    public boolean moveOnGround() {
        if (KeyboardController.left) {
            body.setLinearVelocity(MathUtils.lerp(body.getLinearVelocity().x, -3f, 0.1f), body.getLinearVelocity().y);
            transform.flipX = true;
        }
        if (KeyboardController.right){
            body.setLinearVelocity(MathUtils.lerp(body.getLinearVelocity().x, 3f, 0.1f), body.getLinearVelocity().y);
            transform.flipX = false;
        }
        if (KeyboardController.up) {
            body.applyLinearImpulse(0, 2.7f, body.getWorldCenter().x, body.getWorldCenter().y, true);
            KeyboardController.up = false;
        }
            return (KeyboardController.left || KeyboardController.right);
    }

    public boolean moveOnAir(){
        if (KeyboardController.left){
            body.setLinearVelocity(MathUtils.lerp(body.getLinearVelocity().x, -1.5f, 0.1f), body.getLinearVelocity().y);
            transform.flipX = true;
        }if (KeyboardController.right){
            transform.flipX = false;
            body.setLinearVelocity(MathUtils.lerp(body.getLinearVelocity().x, 1.5f, 0.1f), body.getLinearVelocity().y);
        }
            return (KeyboardController.left || KeyboardController.right);
    }

    public boolean jumpOnAir(){
        if (KeyboardController.up) {
            body.applyLinearImpulse(0, 2.5f, body.getWorldCenter().x, body.getWorldCenter().y, true);
            KeyboardController.up = false;
            return true;
        }
        return false;
    }

}

PlayerAnimations.java

Finally I needed to set up the animations for the player so i created a enum called PlayerAnimations.

public enum PlayerAnimations {

    Idle,
    Walking,
    Jumping, DoubleJumping,
    Falling,
    Attack_1, Attack_2, Attack_3,
    AirAttack_1, AirAttack_2,
    FallingAttack, FallAttack,
    Hit,
    Die

}

AnimationSystem.java

And then render them using the AniamtionSystem.

     switch (PlayerAgent.getAttackState()) {
                case NONE:
                    switch (PlayerAgent.getCurrentState()) {
                        case Idle:
                            key = PlayerAnimations.Idle.ordinal();
                            break;
                        case Walking:
                            key = PlayerAnimations.Walking.ordinal();
                            break;
                        case Jumping:
                            key = PlayerAnimations.Jumping.ordinal();
                            break;
                        case DoubleJumping:
                            key = PlayerAnimations.DoubleJumping.ordinal();
                            break;
                        case Falling:
                            key = PlayerAnimations.Falling.ordinal();
                            break;
                        default:
                            key = PlayerAnimations.Idle.ordinal();
                            break;
                    }
                    break;
                case Attacking:
                    switch (PlayerAgent.attackCombo) {
                        case 1:
                            key = PlayerAnimations.Attack_1.ordinal();
                            break;
                        case 2:
                            key = PlayerAnimations.Attack_2.ordinal();
                            break;
                        case 3:
                            key = PlayerAnimations.Attack_3.ordinal();
                            break;
                        default:
                            key = PlayerAnimations.Attack_1.ordinal();
                            System.out.println("Attack Combo Error");
                            break;
                    }
                    break;
                case AirAttack:
                    switch (PlayerAgent.attackCombo) {
                        case 1:
                            key = PlayerAnimations.AirAttack_1.ordinal();
                            break;
                        case 2:
                            key = PlayerAnimations.AirAttack_2.ordinal();
                            break;
                        default:
                            key = PlayerAnimations.AirAttack_1.ordinal();
                            System.out.println("Air Attack Combo Error");
                            break;
                    }
                    break;
                case FallingAttack:
                    key = PlayerAnimations.FallingAttack.ordinal();
                    break;
                case FallAttack:
                    key = PlayerAnimations.FallAttack.ordinal();
                    break;
                default:
                    key = PlayerAnimations.Idle.ordinal();
                    break;
            }
            timer = PlayerAgent.timer;
            PlayerAgent.timer += deltaTime;
            PlayerAgent.animation = ani.animations.get(key);
        }

        tex.region = ani.animations.get(key).getKeyFrame(timer);

Hope this can help some one and, once again, sorry for my poor english. If you have any question feel free to ask me.