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:
- Unzip all the folders and rename to their specific action
- run the below script (the split names them with tile000.png) but when in a folder attack/tile000.png
- It will extract the images to actions so attack/tile000.png -> attack_1.png in the root folder of the script
- 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)
- Once the first step is done, run the below script to generate the left and right versions with correct names
- 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
Above shows the generated images from the process described above
Spritesheet found online
Split with correct naming convention
Atlas and SpriteSheet
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)