Skip to content

Player Animations

sbaks2 edited this page Oct 6, 2026 · 2 revisions

Purpose

This feature allows the player to see a visual representation in an aesthetically pleasing manner using the assets from https://szadiart.itch.io/2d-soulslike-character?download which are free to use.

How it works

The sprite sheets and atlas's within the LevelGameArea class

 private static final String[] entityAtlases = {
    "images/ghost.atlas", "images/ghostKing.atlas", "images/gold_coin/gold_coin.atlas",
          "images/knight.atlas", "images/LeftKnight.atlas"
  };

The assets are then put into the PlayerRenderComponent inside of the Player entity done inside Player Factory

PlayerRenderComponent animator =
            new PlayerRenderComponent(
                    ServiceLocator.getResourceService()
                            .getAsset("images/knight.atlas", TextureAtlas.class),
                    ServiceLocator.getResourceService()
                            .getAsset("images/LeftKnight.atlas", TextureAtlas.class)
            );

From this the animations are added to the PlayerRenderComponent, with Loop if the animation is intended to loop or normal if it should stop after going through the animation once.

    animator.addAnimation("Idle", 0.1f, Animation.PlayMode.LOOP);
    animator.addAnimation("Jump", 0.1f, Animation.PlayMode.NORMAL);
    animator.addAnimation("Attacks", 0.1f, Animation.PlayMode.NORMAL);
    animator.addAnimation("crouchidle", 0.1f, Animation.PlayMode.LOOP);
    animator.addAnimation("Roll", 0.1f, Animation.PlayMode.NORMAL);
    animator.addAnimation("Slide", 0.1f, Animation.PlayMode.NORMAL);
    animator.addAnimation("Run", 0.1f, Animation.PlayMode.LOOP);
    animator.addAnimation("LeftIdle", 0.1f, Animation.PlayMode.LOOP);
    animator.addAnimation("LeftJump", 0.1f, Animation.PlayMode.NORMAL);
    animator.addAnimation("LeftAttacks", 0.1f, Animation.PlayMode.NORMAL);
    animator.addAnimation("Leftcrouchidle", 0.1f, Animation.PlayMode.LOOP);
    animator.addAnimation("LeftRoll", 0.1f, Animation.PlayMode.NORMAL);
    animator.addAnimation("LeftSlide", 0.1f, Animation.PlayMode.NORMAL);
    animator.addAnimation("LeftRun", 0.1f, Animation.PlayMode.LOOP);
    player.addComponent(animator).addComponent(new PlayerAnimationController());

The initial idle animation is caused by the following code inside of PlayerFactory:

animator.startAnimation("Idle");

Inside of PlayerAnimation Controller listeners are added for every movement

        animator = this.entity.getComponent(PlayerRenderComponent.class);
        entity.getEvents().addListener("idle", this::animateIdle);
        entity.getEvents().addListener("run", this::animateRun);
        entity.getEvents().addListener("attacking", this::animateAttack);
        entity.getEvents().addListener("sliding", this::animateSlide);
        entity.getEvents().addListener("crouchidle", this::animateCrouch);
        entity.getEvents().addListener("jumping", this::animateJump);
        entity.getEvents().addListener("rolling", this::animateRoll);

The animate functions contain similar code to the following with Left and Right distinguished to make sure that the direction of the character is clear

  void animateIdle(String direction) {
        if ("Right".equals(direction)) {
            animator.startAnimation("Idle");
        } else {
            animator.startAnimation("LeftIdle");
        }
    }

These listeners are called inside of KeyboardPlayerInputComponent whenever the corresponding key is pressed inside of KeyDown.

case Keys.A:
        direction = "Left";
        walkDirection.add(Vector2Utils.LEFT);
        if (crouch) {
          entity.getEvents().trigger("crouchidle", direction);
        } else {
          entity.getEvents().trigger("run", direction);
        }
        triggerWalkEvent();
        return true;

Then if the animation is a looped animation such as run or crouch the following code or similar is used to stop it inside of the keyUp function:

case Keys.A:
        walkDirection.sub(Vector2Utils.LEFT);
        if (walkDirection.isZero()) {
          entity.getEvents().trigger("idle", direction);
        }
        triggerWalkEvent();
        return true;

However if the animation is not looped then the PlayerActions class stops it using an animationtimer function which identifies the direction from a getter function.

 private void animationtimer(String direction) {
    PlayerRenderComponent animator = entity.getComponent(PlayerRenderComponent.class);
    if (animator.isFinished()
            &! animator.getCurrentAnimation().equals("crouchidle")
            &! animator.getCurrentAnimation().equals("Leftcrouchidle")
    ) {
      entity.getEvents().trigger("idle", direction);
    }
  }

This function is called inside the update function

  @Override
  public void update() {
    playMovementSound();
    if (!dead && (moving || platformerComponent.getJumpingBool())) {
      updateSpeed();
    }
    timerforslide();
    String direction = entity.getComponent(KeyboardPlayerInputComponent.class).getDirection();
    animationtimer(direction);
  }

Scaling the Entity

The Entity is given an initial scale by:

static Texture size = new Texture("images/knight_default.png");
player.setScale(0.75f, (float) size.getHeight() / size.getWidth());

However the proportions of each individual frame is different and thus were kept consistent by PlayerRenderComponent which overides the inherited draw function from the AnimationRenderComponent to ensure the player animations kept consistent proportions.

  @Override
  protected void draw(SpriteBatch batch) {
    if (currentAnimation == null) {
      return;
    }
    TextureRegion region = currentAnimation.getKeyFrame(animationPlayTime);
    Vector2 pos = entity.getPosition();
    float pixel = 1f / 42f;
    float width = pixel * region.getRegionWidth();
    float height = pixel * region.getRegionHeight();
    batch.draw(region, pos.x, pos.y, width, height);
    animationPlayTime += timeSource.getDeltaTime();
  }

Testing

PlayerRenderComponentTest was created to test the player render component using the same tests as the AnimationRenderComponentTests however accounting for the 3 atlas's required for PlayerRenderComponent and the difference in the draw function to create the sprite.

Clone this wiki locally