Animal Animations - UQcsse3200/2024-studio-1 GitHub Wiki

Animal Animations

Animation Render System

For detailed information on triggering animations, refer to the Animation System page.

Sprite Sheets and Atlas Files

Sprite sheets contain all animation frames. These are split into individual frames and named according to their action ("attack_1", "idle_1") in ATLAS files. The NPCFactory loads these assets and assigns them to NPCs.

Sprite Sheets Generation

The scripts below were created to optimise the workflow of atlas file creation. The usual sets of sprites found online only have one direction. The below script takes place after the spritesheet is split (from example https://ezgif.com/) and plugging the images in you receive zipped split images. The process followed from then is as follows:

  1. Unzip all the folders and rename to their specific action
  2. run the below script (the split names them with tile000.png) but when in a folder attack/tile000.png
  3. It will extract the images to actions so attack/tile000.png -> attack_1.png in the root folder of the script
  4. The below script will do this for all actions and once done, the next script can be used.
import os
import re
import shutil

def rename_and_move_files_to_root(directory):
    # Regex to extract the number from the file name like tile000.png -> 000
    file_number_pattern = re.compile(r'tile(\d+)\.png')

    # Traverse the directory and subdirectories
    for root, dirs, files in os.walk(directory):
        folder_name = os.path.basename(root)  # Get the current folder's name
        
        # Skip the root directory to avoid renaming/moving files already at root
        if root == directory:
            continue
        
        for filename in sorted(files):  # Sorting files to ensure sequential renaming
            if filename.endswith('.png'):
                old_path = os.path.join(root, filename)
                
                # Extract the number from the filename (e.g., tile000.png -> 000)
                match = file_number_pattern.search(filename)
                if match:
                    # Strip leading zeros and convert the number, then add 1 to start from 1
                    file_number = int(match.group(1)) + 1
                    
                    # Generate new filename using folder name as prefix
                    new_filename = f"{folder_name}_{file_number}.png"
                    new_path = os.path.join(directory, new_filename)  # Move to root directory
                    
                    try:
                        # Move the file to the root directory with the new name
                        shutil.move(old_path, new_path)
                        print(f"Renamed and moved: {old_path} -> {new_path}")
                    except Exception as e:
                        print(f"Error moving {old_path}: {e}")

# Replace this with the path of your root folder
root_directory = "Kitsune/"

# Call the function to rename and move files
rename_and_move_files_to_root(root_directory)
  1. Once the first step is done, run the below script to generate the left and right versions with correct names
  2. Script will have to be modified if the sheet initially only had left directions
from PIL import Image, ImageFilter, ImageOps
from os import listdir
from os.path import isfile, join
import os

onlyfiles = [f for f in listdir() if isfile(f)]

for i in onlyfiles:
    name = i.split('.')[0]
    if i.split('.')[1] == 'py':
        continue        
    if name == 'default':
        continue
    else:
        print(name)
        vals = name.split('_')
        nameOnly = vals[0]
        numberOnly = vals[1]
        im = Image.open(i)
        im_mirror = ImageOps.mirror(im)
        im_mirror.save(nameOnly + '_left_' + numberOnly + '.png', quality=100)
        im.save(nameOnly + '_right_' + numberOnly + '.png', quality=100)

These sheets can the be put into GdxTexture (copu one of the sheets and make it default) This will then generate the spritesheets with correct naming

ezgif com-animated-gif-maker(1) ezgif com-animated-gif-maker(2)

Above shows the generated images from the process described above

Spritesheet found online

Attack

Split with correct naming convention

attack_1 attack_2 attack_3 attack_4

Atlas and SpriteSheet

dog

dog.png size:1024,64 repeat:none attack index:4 bounds:52,2,48,48 attack index:3 bounds:352,2,48,48 attack index:2 bounds:552,2,48,48 ...

Animation Configuration and Directions

NPCs' animations are defined in the NPCs.json file, including whether an animal 'isDirectable', which sets up DirectionalComponent to give the current direction an animal is facing. The NPCAnimationController is responsible for synchronizing their movements from entity event triggers.

Here's how directional animations are handled in code:

void animateIdle() {
    if (!dead) {
        if (animator.hasAnimation("idle_right") && animator.hasAnimation("idle_left")) {
            triggerDirectionalAnimation("idle");
        } else if (animator.hasAnimation("idle")) {
            animator.startAnimation("idle");
        } else {
            throw new IllegalStateException("No idle animation found");
        }
    }
}

private void triggerDirectionalAnimation(String baseAnimation) {
    String direction = directionalComponent.getDirection();
    if (direction.equals("right")) {
        animator.startAnimation(baseAnimation + "_right");
    } else {
        animator.startAnimation(baseAnimation + "_left");
    }
}

The tasks then trigger the actions (idle, walk, etc.) based on whether the sprite accepts directions or not.

Simplified Class Diagram

classDiagram
    class Entity {
        +addComponent(Component)
        +getComponent(Class)
    }
    class AnimationRenderComponent {
        -Map animations
        +addAnimation(String, float)
        +startAnimation(String)
    }
    class NPCAnimationController {
        +animateIdle()
        +animateWalk()
        +animateAttack()
    }
    class DirectionalNPCComponent {
        -String direction
        +setDirection(String)
        +getDirection()
    }
    class AITaskComponent {
        -List priorityTasks
        +addTask(PriorityTask)
        +update()
    }

    Entity "1" *-- "1" AnimationRenderComponent
    Entity "1" *-- "1" NPCAnimationController
    Entity "1" *-- "1" DirectionalNPCComponent
    Entity "1" *-- "1" AITaskComponent
    NPCAnimationController --> AnimationRenderComponent : uses
    NPCAnimationController --> DirectionalNPCComponent : uses
    AITaskComponent --> NPCAnimationController : triggers animations

Sequence Diagram

Initialization

sequenceDiagram
    participant NPCFactory
    participant Entity
    participant AnimationRenderComponent
    participant NPCAnimationController
    
    NPCFactory->>Entity: create()
    NPCFactory->>Entity: addComponent(AnimationRenderComponent)
    NPCFactory->>Entity: addComponent(NPCAnimationController)
    NPCFactory->>AnimationRenderComponent: addAnimation("idle", 0.1f)
    NPCFactory->>AnimationRenderComponent: addAnimation("walk", 0.1f)

Triggering an Animation

sequenceDiagram
    participant AITaskComponent
    participant Entity
    participant NPCAnimationController
    participant AnimationRenderComponent
    
    AITaskComponent->>Entity: getEvents().trigger("walk")
    Entity->>NPCAnimationController: animateWalk()
    NPCAnimationController->>AnimationRenderComponent: startAnimation("walk")

Rendering an Animation

sequenceDiagram
    participant GameLoop
    participant Entity
    participant AnimationRenderComponent
    
    GameLoop->>Entity: update()
    Entity->>AnimationRenderComponent: update()
    AnimationRenderComponent->>AnimationRenderComponent: getCurrentFrame()
    AnimationRenderComponent->>GameLoop: draw(SpriteBatch)