PlayerAnimationSystem - UQcsse3200/2024-studio-3 GitHub Wiki

Introduction

The Player Animation System in the game is designed to handle various animations associated with the player's movement and state transitions. It comprises several components that work together to detect player input, trigger the appropriate animations, and render them on the screen. This system is key to providing visual feedback and enhancing the player's immersion in the game by making character movements responsive and dynamic.

The system is split into several key classes, such as PlayerFactory, PlayerAnimationController, KeyboardPlayerInputComponent, and AnimationRenderComponent. Together, these classes handle everything from detecting input to rendering the right animations.

Key Features

  • Input Handling: Detects player input (keyboard or other) and triggers the appropriate animation event.
  • Event-Driven Animation: Listens to specific player events (e.g., walking in various directions) and responds by triggering the corresponding animations.
  • State-Based Animation: Switches animations based on the player's state (walking, standing still, attacking, etc.).
  • Smooth Transitions: Manages the smooth transition between animations when player actions change.
  • Rendering: Responsible for rendering the current animation frame based on the player’s movement.
  • Resource Management: Ensures that all animation resources are properly handled, avoiding memory leaks.

Classes Involved

PlayerFactory

The PlayerFactory class is responsible for creating the player entity and attaching all necessary components, such as input handling, animations, combat stats, and interaction mechanisms. It ensures that all components required for player functionality are initialized and properly configured.

Key Responsibilities

  • Player Creation: Initialises a new player entity and assigns essential components to it.
  • Component Initialisation: Adds components like PlayerAnimationController, KeyboardPlayerInputComponent, AnimationRenderComponent, and others to the player entity.

Code Breakdown

public static Entity createPlayer(PlayerConfig config) {
    InputComponent inputComponent =
        ServiceLocator.getInputService().getInputFactory().createForPlayer();

    AnimationRenderComponent animator =
        new AnimationRenderComponent(ServiceLocator.getResourceService().getAsset("images/player.atlas", TextureAtlas.class));

    // Add animations for different movement directions
    animator.addAnimation("Character_StandDown", 0.2f);
    animator.addAnimation("Character_StandUp", 0.2f);
    animator.addAnimation("Character_StandLeft", 0.2f);
    animator.addAnimation("Character_StandRight", 0.2f);
    animator.addAnimation("Character_DownLeft", 0.2f, Animation.PlayMode.LOOP);
    animator.addAnimation("Character_UpRight", 0.2f, Animation.PlayMode.LOOP);
    animator.addAnimation("Character_Up", 0.2f, Animation.PlayMode.LOOP);
    animator.addAnimation("Character_Left", 0.2f, Animation.PlayMode.LOOP);
    animator.addAnimation("Character_DownRight", 0.2f, Animation.PlayMode.LOOP);
    animator.addAnimation("Character_Right", 0.2f, Animation.PlayMode.LOOP);

    Entity player =
        new Entity()
            .addComponent(new PhysicsComponent())
            .addComponent(new ColliderComponent())
            .addComponent(new HitboxComponent().setLayer(PhysicsLayer.PLAYER))
            .addComponent(new PlayerActions())
            .addComponent(new CombatStatsComponent(config.health, config.baseAttack))
            .addComponent(new InventoryComponent(config.inventorySize))
            .addComponent(new InventoryDisplay())
            .addComponent(inputComponent)
            .addComponent(animator)
            .addComponent(new PlayerAnimationController())
            .addComponent(new TooltipsDisplay())
            .addComponent(new PlayerStatsDisplay())
            .addComponent(new InteractionComponent(PhysicsLayer.INTERACTABLE))
            .addComponent(new SensorComponent(PhysicsLayer.INTERACTABLE, 10f));

    animator.startAnimation("Character_StandUp");
    return player;
}

PlayerAnimationController

The PlayerAnimationController class manages the player's animations, listening for specific events that correspond to the player's actions (e.g., walking, standing). Based on these events, it triggers the appropriate animations by interacting with the AnimationRenderComponent.

Key Responsibilities

  • Event Listeners: Registers event listeners for movement events (e.g., "walkLeft", "walkRight") and triggers corresponding animations.
  • State-Based Animation: Transitions between animations based on the player's current movement state (e.g., standing or walking).

Code Breakdown

@Override
public void create() {
    super.create();
    animator = this.entity.getComponent(AnimationRenderComponent.class);
    entity.getEvents().addListener("walkLeft", this::animateLeft);
    entity.getEvents().addListener("walkRight", this::animateRight);
    entity.getEvents().addListener("walkUp", this::animateUp);
    entity.getEvents().addListener("walkDown", this::animateDown);
    entity.getEvents().addListener("walkUpLeft", this::animateUpLeft);
    entity.getEvents().addListener("walkUpRight", this::animateUpRight);
    entity.getEvents().addListener("walkDownLeft", this::animateDownLeft);
    entity.getEvents().addListener("walkDownRight", this::animateDownRight);
    entity.getEvents().addListener("walkStopAnimation", this::animateStop);
}

void animateLeft() {animator.startAnimation("Character_Left");}
void animateRight() {animator.startAnimation("Character_Right");}
void animateUp() {animator.startAnimation("Character_Up");}
void animateDown() {animator.startAnimation("Character_Down");}
void animateStop(Vector2 lastDirection) {
    if (lastDirection.x < -0.1) {
        animator.startAnimation("Character_StandLeft");
    } else if (lastDirection.x > 0.1) {
        animator.startAnimation("Character_StandRight");
    } else if (lastDirection.y < -0.1) {
        animator.startAnimation("Character_StandDown");
    } else if (lastDirection.y > 0.1) {
        animator.startAnimation("Character_StandUp");
    }
}

KeyboardPlayerInputComponent

The KeyboardPlayerInputComponent class detects player input from the keyboard and translates it into movement or action commands for the player entity. This component listens for key presses (e.g., W, A, S, D) and triggers the appropriate movement events, which the PlayerAnimationController uses to animate the player.

Key Responsibilities

  • Input Handling: Detects keyboard input and translates it into movement commands.
  • Event Triggering: Triggers events like "walkLeft", "walkRight", and "walkStopAnimation" that the animation controller listens for.
  • Smooth Transitions: It ensures smooth transitions between walking and standing animations when the player stops moving.

Code Breakdown

@Override
public boolean keyDown(int keycode) {
    if (!isInteracting) {
        switch (keycode) {
            case Keys.W:
                walkDirection.add(Vector2Utils.UP);
                triggerWalkEvent();
                return true;
            case Keys.A:
                walkDirection.add(Vector2Utils.LEFT);
                triggerWalkEvent();
                return true;
            case Keys.S:
                walkDirection.add(Vector2Utils.DOWN);
                triggerWalkEvent();
                return true;
            case Keys.D:
                walkDirection.add(Vector2Utils.RIGHT);
                triggerWalkEvent();
                return true;
        }
    }
    return false;
}


private void triggerWalkEvent() {
    // Store the last movement direction to use when the player stops
    Vector2 lastDir = this.walkDirection.cpy();
    
    // Recalculate the walk direction based on the keys currently pressed
    this.walkDirection = keysToVector().scl(WALK_SPEED);

    // Convert the vector to a specific direction (UP, DOWN, LEFT, RIGHT)
    Directions dir = keysToDirection();
    
    // If no keys are pressed, trigger the stop event
    if (dir == Directions.NONE) {
        entity.getEvents().trigger("walkStop");
        entity.getEvents().trigger("walkStopAnimation", lastDir);
        return;
    }

    // Based on the calculated direction, trigger the appropriate walk event
    switch (dir) {
        case UP -> entity.getEvents().trigger("walkUp");
        case DOWN -> entity.getEvents().trigger("walkDown");
        case LEFT -> entity.getEvents().trigger("walkLeft");
        case RIGHT -> entity.getEvents().trigger("walkRight");
        case UPLEFT -> entity.getEvents().trigger("walkUpLeft");
        case UPRIGHT -> entity.getEvents().trigger("walkUpRight");
        case DOWNLEFT -> entity.getEvents().trigger("walkDownLeft");
        case DOWNRIGHT -> entity.getEvents().trigger("walkDownRight");
    }
}


AnimationRenderComponent

The AnimationRenderComponent is responsible for rendering the player's animations. It stores various animations (e.g., walking up, down, left, right) and renders the appropriate one based on the state triggered by the PlayerAnimationController.

Key Responsibilities

  • Rendering: Draws the current animation frame based on the player's state.
  • Animation Management: Manages different animations for various player actions and ensures smooth transitions between them.

Code Breakdown

public void startAnimation(String animationName) {
    this.currentAnimation = animations.get(animationName);
    if (currentAnimation != null) {
        this.currentAnimation.start();
    }
}

UML Diagram

Conclusion

The Player Animation System in this game involves a well-coordinated interaction between several components: input handling, event-driven animation control, and rendering. The system detects player input, triggers the relevant events, and ensures that the appropriate animations are played. The PlayerFactory handles the initialization of the player entity and its associated components, while the PlayerAnimationController manages the transitions between different animations based on the player’s state. The AnimationRenderComponent ensures that the animations are displayed smoothly on the screen.

By using an event-driven architecture, the system allows for flexibility and modularity, making it easier to extend or modify player behavior or animations in the future.