Refactor animations

This commit is contained in:
2025-07-21 13:50:17 +02:00
parent 8a2a5511f4
commit 1ce0810cc2
11 changed files with 386 additions and 106 deletions

View File

@@ -6,7 +6,6 @@ import com.bartlomiejpluta.base.engine.core.gl.object.material.Material;
import com.bartlomiejpluta.base.engine.world.object.Sprite;
import com.bartlomiejpluta.base.util.math.MathUtil;
import lombok.EqualsAndHashCode;
import org.joml.Vector2fc;
@EqualsAndHashCode(callSuper = true)
public abstract class AnimatedSprite extends Sprite implements Animated {
@@ -22,9 +21,51 @@ public abstract class AnimatedSprite extends Sprite implements Animated {
protected abstract boolean shouldAnimate();
protected abstract Vector2fc[] getSpriteAnimationFramesPositions();
/**
* Allows subclasses to restrict animation to a specific subset of frames.
*
* <p>By default, this method returns {@code null}, which means all frames in the texture
* are available for animation. In this case, the frame IDs used by {@link #setAnimationFrame(int)}
* correspond directly to the global frame indices in the texture, as shown below:
*
* <pre>
* +----+----+----+----+
* | 00 | 01 | 02 | 03 |
* +----+----+----+----+
* | 04 | 05 | 06 | 07 |
* +----+----+----+----+
* | 08 | 09 | 10 | 11 |
* +----+----+----+----+
* | 12 | 13 | 14 | 15 |
* +----+----+----+----+
* </pre>
*
* <p>However, if this method returns a specific array of frame indices (e.g., {@code {8, 9, 10, 11, 13, 15}}),
* only those frames will be used for animation. The frame IDs are then remapped, where index 0
* corresponds to the first frame in the subset, index 1 to the second, and so on:
*
* <pre>
* +----+----+----+----+
* | | | | |
* +----+----+----+----+
* | | | | |
* +----+----+----+----+
* | 00 | 01 | 02 | 03 |
* +----+----+----+----+
* | | 04 | | 05 |
* +----+----+----+----+
* </pre>
*
* <p><strong>Important:</strong> When this method returns {@code null}, calling
* {@link #setAnimationFrame(int)} has the same effect as calling {@code setFrame(int)}
* directly, since the frame IDs map directly to the global texture frame indices.
* When a subset is defined, {@link #setAnimationFrame(int)} uses local indices
* within that subset.
*
* @return an array of global frame IDs to use for animation, or {@code null} to use all frames
*/
protected int[] getAvailableFrames() {
protected int[] getAvailableFramesSubset() {
return null;
}
@@ -40,7 +81,7 @@ public abstract class AnimatedSprite extends Sprite implements Animated {
@Override
public void setAnimationFrame(int frame) {
var availableFrames = getAvailableFrames();
var availableFrames = getAvailableFramesSubset();
if (availableFrames == null) {
setFrame(frame % getTextureCoordinates().length);
@@ -55,11 +96,9 @@ public abstract class AnimatedSprite extends Sprite implements Animated {
if (shouldAnimate()) {
time += dt * 1000;
setAnimationFrame(time / intervalInMilliseconds * intervalInMilliseconds);
// var maxFrames = getTextureCoordinates().length;
// currentAnimationFrame = ((time % (maxFrames * intervalInMilliseconds)) / intervalInMilliseconds);
// setSprite(currentAnimationFrame);
} else {
time = 0;
return;
}
time = 0;
}
}

View File

@@ -6,7 +6,6 @@ import com.bartlomiejpluta.base.api.map.layer.object.ObjectLayer;
import com.bartlomiejpluta.base.api.move.AnimationMovement;
import com.bartlomiejpluta.base.api.move.Direction;
import com.bartlomiejpluta.base.api.move.Movement;
import com.bartlomiejpluta.base.engine.core.gl.object.material.Material;
import com.bartlomiejpluta.base.engine.core.gl.object.texture.Texture;
import com.bartlomiejpluta.base.engine.world.movement.MovableSprite;
import com.bartlomiejpluta.base.util.path.Path;
@@ -90,11 +89,6 @@ public class DefaultAnimation extends MovableSprite implements Animation {
return enabled;
}
@Override
protected Vector2fc[] getSpriteAnimationFramesPositions() {
return frames;
}
@Override
protected void setDefaultAnimationFrame() {
// do nothing

View File

@@ -1,9 +1,8 @@
package com.bartlomiejpluta.base.engine.world.character.manager;
import com.bartlomiejpluta.base.engine.common.manager.AssetManager;
import com.bartlomiejpluta.base.engine.core.gl.object.material.Material;
import com.bartlomiejpluta.base.engine.core.gl.object.texture.Texture;
import com.bartlomiejpluta.base.engine.world.character.asset.CharacterSetAsset;
import com.bartlomiejpluta.base.engine.world.character.model.CharacterSet;
public interface CharacterSetManager extends AssetManager<CharacterSetAsset, Texture> {
public interface CharacterSetManager extends AssetManager<CharacterSetAsset, CharacterSet> {
}

View File

@@ -1,44 +1,21 @@
package com.bartlomiejpluta.base.engine.world.character.manager;
import com.bartlomiejpluta.base.api.character.Character;
import com.bartlomiejpluta.base.api.move.Direction;
import com.bartlomiejpluta.base.engine.util.mesh.MeshManager;
import com.bartlomiejpluta.base.engine.world.character.config.CharacterSpriteConfiguration;
import com.bartlomiejpluta.base.engine.world.character.model.DefaultCharacter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.joml.Vector2f;
import org.joml.Vector2fc;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Map.Entry;
import static java.util.stream.Collectors.toUnmodifiableMap;
@Slf4j
@Component
@RequiredArgsConstructor(onConstructor_ = @__(@Autowired))
public class DefaultCharacterManager implements CharacterManager {
private final MeshManager meshManager;
private final CharacterSetManager characterSetManager;
private final int defaultSpriteColumn;
private final Map<Direction, Integer> spriteDirectionRows;
private final Map<Direction, Vector2fc> spriteDefaultRows;
@Autowired
public DefaultCharacterManager(MeshManager meshManager, CharacterSetManager characterSetManager, CharacterSpriteConfiguration configuration) {
this.meshManager = meshManager;
this.characterSetManager = characterSetManager;
this.spriteDirectionRows = configuration.getSpriteDirectionRows();
defaultSpriteColumn = configuration.getDefaultSpriteColumn();
this.spriteDefaultRows = spriteDirectionRows
.entrySet()
.stream()
.collect(toUnmodifiableMap(Entry::getKey, entry -> new Vector2f(defaultSpriteColumn, entry.getValue())));
}
private final CharacterSpriteConfiguration configuration;
@Override
public void init() {
@@ -47,7 +24,7 @@ public class DefaultCharacterManager implements CharacterManager {
@Override
public Character createCharacter(String characterSetUid) {
return new DefaultCharacter(characterSetManager, defaultSpriteColumn, spriteDirectionRows, spriteDefaultRows, characterSetUid);
return new DefaultCharacter(characterSetManager, characterSetUid, configuration.getDefaultSpriteColumn());
}
@Override

View File

@@ -1,11 +1,11 @@
package com.bartlomiejpluta.base.engine.world.character.manager;
import com.bartlomiejpluta.base.engine.core.gl.object.material.Material;
import com.bartlomiejpluta.base.engine.core.gl.object.texture.Texture;
import com.bartlomiejpluta.base.engine.core.gl.object.texture.TextureManager;
import com.bartlomiejpluta.base.engine.error.AppException;
import com.bartlomiejpluta.base.engine.project.config.ProjectConfiguration;
import com.bartlomiejpluta.base.engine.world.character.asset.CharacterSetAsset;
import com.bartlomiejpluta.base.engine.world.character.config.CharacterSpriteConfiguration;
import com.bartlomiejpluta.base.engine.world.character.model.CharacterSet;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@@ -20,6 +20,8 @@ import java.util.Map;
public class DefaultCharacterSetManager implements CharacterSetManager {
private final TextureManager textureManager;
private final Map<String, CharacterSetAsset> assets = new HashMap<>();
private final Map<String, CharacterSet> charSets = new HashMap<>();
private final CharacterSpriteConfiguration charsetConfiguration;
private final ProjectConfiguration configuration;
@Override
@@ -34,15 +36,24 @@ public class DefaultCharacterSetManager implements CharacterSetManager {
}
@Override
public Texture loadObject(String uid) {
var asset = assets.get(uid);
public CharacterSet loadObject(String uid) {
var charSet = charSets.get(uid);
if (asset == null) {
throw new AppException("The character set asset with UID: [%s] does not exist", uid);
if (charSet == null) {
var asset = assets.get(uid);
if (asset == null) {
throw new AppException("The character set asset with UID: [%s] does not exist", uid);
}
var source = configuration.projectFile("charsets", asset.getSource());
var texture = textureManager.loadTexture(source, asset.getRows(), asset.getColumns());
charSet = CharacterSet.from(texture, charsetConfiguration.getSpriteDirectionRows());
log.info("Loading character set from assets to cache under the key: [{}]", uid);
charSets.put(uid, charSet);
}
var source = configuration.projectFile("charsets", asset.getSource());
return textureManager.loadTexture(source, asset.getRows(), asset.getColumns());
return charSet;
}
}

View File

@@ -0,0 +1,74 @@
package com.bartlomiejpluta.base.engine.world.character.model;
import com.bartlomiejpluta.base.api.move.Direction;
import com.bartlomiejpluta.base.engine.core.gl.object.texture.Texture;
import lombok.Getter;
import lombok.NonNull;
import java.util.Map;
import java.util.function.Function;
import static com.bartlomiejpluta.base.api.move.Direction.values;
import static java.util.Arrays.stream;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toUnmodifiableMap;
import static java.util.stream.IntStream.range;
@Getter
public class CharacterSet {
private final Texture texture;
private final Map<Direction, int[]> frames;
private CharacterSet(@NonNull Texture texture, @NonNull Map<Direction, Integer> charsetRowsByDirections) {
this.texture = texture;
this.frames = stream(values())
.collect(toUnmodifiableMap(identity(), calculateFrames(charsetRowsByDirections)));
}
private Function<Direction, int[]> calculateFrames(Map<Direction, Integer> charsetRowsByDirections) {
return d -> framesForRow(charsetRowsByDirections.get(d));
}
private int[] framesForRow(int row) {
var cols = texture.getColumns();
return range(row * cols, (row + 1) * cols).toArray();
}
/**
* Creates a new CharacterSet instance from the specified texture and direction-to-row mapping.
*
* <p>This factory method constructs a CharacterSet by associating each facing direction
* with its corresponding row in the character set texture. The method automatically
* calculates frame indices for each direction based on the texture dimensions and
* row mappings.
*
* <p>The character set texture should be organized as a grid where:
* <ul>
* <li>Each row represents graphics for a specific facing direction</li>
* <li>Each column represents either an animation frame or object variant</li>
* <li>Row indices are zero-based (0 = first row, 1 = second row, etc.)</li>
* </ul>
*
* <p>Example mapping:
* <pre>{@code
* Map<Direction, Integer> mapping = Map.of(
* Direction.DOWN, 0, // First row (index 0) for DOWN direction
* Direction.LEFT, 1, // Second row (index 1) for LEFT direction
* Direction.RIGHT, 2, // Third row (index 2) for RIGHT direction
* Direction.UP, 3 // Fourth row (index 3) for UP direction
* );
* CharacterSet charset = CharacterSet.from(texture, mapping);
* }</pre>
*
* @param texture the character set texture containing sprite graphics organized in rows and columns
* @param charsetRowsByDirections a mapping that defines which texture row corresponds to each facing direction,
* where the key is the direction and the value is the zero-based row index
* @return a new CharacterSet instance configured with the specified texture and direction mappings
* @throws NullPointerException if either texture or charsetRowsByDirections is null
* @throws IllegalArgumentException if the charsetRowsByDirections map is empty or contains invalid row indices
* @throws IndexOutOfBoundsException if any row index in the mapping exceeds the texture's row count
*/
public static CharacterSet from(Texture texture, Map<Direction, Integer> charsetRowsByDirections) {
return new CharacterSet(texture, charsetRowsByDirections);
}
}

View File

@@ -7,8 +7,6 @@ import com.bartlomiejpluta.base.api.map.layer.object.ObjectLayer;
import com.bartlomiejpluta.base.api.move.CharacterMovement;
import com.bartlomiejpluta.base.api.move.Direction;
import com.bartlomiejpluta.base.api.move.Movement;
import com.bartlomiejpluta.base.engine.core.gl.object.material.Material;
import com.bartlomiejpluta.base.engine.core.gl.object.texture.Texture;
import com.bartlomiejpluta.base.engine.error.AppException;
import com.bartlomiejpluta.base.engine.world.character.manager.CharacterSetManager;
import com.bartlomiejpluta.base.engine.world.movement.MovableSprite;
@@ -21,7 +19,6 @@ import org.joml.Vector2f;
import org.joml.Vector2fc;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
@@ -30,24 +27,15 @@ import static java.util.Objects.requireNonNull;
@EqualsAndHashCode(callSuper = true)
public class DefaultCharacter extends MovableSprite implements Character {
private static final Map<Direction, int[]> CHARSET_FRAMES = Map.of(
Direction.DOWN, new int[]{0, 1, 2, 3},
Direction.LEFT, new int[]{4, 5, 6, 7},
Direction.RIGHT, new int[]{8, 9, 10, 11},
Direction.UP, new int[]{12, 13, 14, 15}
);
private static final int DEFAULT_CHARSET_FRAME_COLUMN = 0;
private final int defaultSpriteColumn;
private final CharacterSetManager characterSetManager;
private final Map<Direction, Integer> spriteDirectionRows;
private final Map<Direction, Vector2fc> spriteDefaultRows;
private final Vector2f characterScale = new Vector2f(1, 1);
private Texture texture;
private Vector2fc characterSetSize;
private final EventHandler eventHandler = new EventHandler();
private final Queue<CharacterInstantAnimation> instantAnimations = new LinkedList<>();
@Setter
private int defaultSpriteColumn;
private CharacterSet characterSet;
private Vector2fc characterSetSize;
@Getter
@Setter
@@ -65,22 +53,19 @@ public class DefaultCharacter extends MovableSprite implements Character {
private boolean animationEnabled = true;
private final Queue<CharacterInstantAnimation> instantAnimations = new LinkedList<>();
public DefaultCharacter(CharacterSetManager characterSetManager, int defaultSpriteColumn, @NonNull Map<Direction, Integer> spriteDirectionRows, Map<Direction, Vector2fc> spriteDefaultRows, @NonNull String characterSetUid) {
this(characterSetManager.loadObject(characterSetUid), characterSetManager, defaultSpriteColumn, spriteDirectionRows, spriteDefaultRows);
public DefaultCharacter(CharacterSetManager characterSetManager, @NonNull String characterSetUid, int defaultSpriteColumn) {
this(characterSetManager.loadObject(characterSetUid), characterSetManager, defaultSpriteColumn);
}
private DefaultCharacter(@NonNull Texture texture, @NonNull CharacterSetManager characterSetManager, int defaultSpriteColumn, @NonNull Map<Direction, Integer> spriteDirectionRows, @NonNull Map<Direction, Vector2fc> spriteDefaultRows) {
super(texture);
private DefaultCharacter(@NonNull CharacterSet characterSet, @NonNull CharacterSetManager characterSetManager, int defaultSpriteColumn) {
super(characterSet.getTexture());
this.defaultSpriteColumn = defaultSpriteColumn;
this.characterSetManager = characterSetManager;
this.spriteDirectionRows = spriteDirectionRows;
this.faceDirection = Direction.DOWN;
this.spriteDefaultRows = spriteDefaultRows;
this.characterSet = characterSet;
this.characterSetSize = characterSet.getTexture().getSpriteSize();
this.texture = texture;
this.characterSetSize = texture.getSpriteSize();
super.setScale(characterSetSize.x() * characterScale.x, characterSetSize.y() * characterScale.y);
setDefaultAnimationFrame();
@@ -117,35 +102,21 @@ public class DefaultCharacter extends MovableSprite implements Character {
}
@Override
protected Vector2fc[] getSpriteAnimationFramesPositions() {
var row = spriteDirectionRows.get(faceDirection);
var frames = getMaterial().getColumns();
var array = new Vector2f[frames];
for (int column = 0; column < frames; ++column) {
array[column] = new Vector2f(column, row);
}
return array;
}
@Override
protected int[] getAvailableFrames() {
return CHARSET_FRAMES.get(faceDirection);
protected int[] getAvailableFramesSubset() {
return characterSet.getFrames().get(faceDirection);
}
@Override
protected void setDefaultAnimationFrame() {
// getMaterial().setSpritePosition(spriteDefaultRows.get(faceDirection));
setAnimationFrame(CHARSET_FRAMES.get(faceDirection)[DEFAULT_CHARSET_FRAME_COLUMN]);
setAnimationFrame(characterSet.getFrames().get(faceDirection)[defaultSpriteColumn]);
}
@Override
public void changeCharacterSet(String characterSetUid) {
this.texture = characterSetManager.loadObject(characterSetUid);
this.characterSetSize = texture.getSpriteSize();
this.characterSet = characterSetManager.loadObject(characterSetUid);
this.characterSetSize = characterSet.getTexture().getSpriteSize();
super.setScale(characterSetSize.x() * characterScale.x, characterSetSize.y() * characterScale.y);
setMaterial(texture);
setMaterial(characterSet.getTexture());
}
@Override