Deathanimation working

This commit is contained in:
Urban Modig
2025-09-02 21:30:08 +02:00
parent 96c89b6598
commit 5ba16402e4
5 changed files with 120 additions and 30 deletions

View File

@ -1,5 +1,6 @@
package se.urmo.game.entities.ghost; package se.urmo.game.entities.ghost;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import se.urmo.game.collision.GhostCollisionChecker; import se.urmo.game.collision.GhostCollisionChecker;
import se.urmo.game.entities.BaseAnimated; import se.urmo.game.entities.BaseAnimated;
@ -13,10 +14,8 @@ import se.urmo.game.map.GameMap;
import se.urmo.game.state.GhostManager; import se.urmo.game.state.GhostManager;
import se.urmo.game.state.LevelManager; import se.urmo.game.state.LevelManager;
import se.urmo.game.util.Direction; import se.urmo.game.util.Direction;
import se.urmo.game.util.MiscUtil;
import se.urmo.game.util.MyPoint; import se.urmo.game.util.MyPoint;
import java.awt.Color;
import java.awt.Graphics; import java.awt.Graphics;
import java.awt.Point; import java.awt.Point;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
@ -49,6 +48,8 @@ public class Ghost extends BaseAnimated {
private final GhostStrategy fearStrategy = new FearStrategy(); private final GhostStrategy fearStrategy = new FearStrategy();
private int frightenedTimer = 0; private int frightenedTimer = 0;
private boolean isBlinking = false; private boolean isBlinking = false;
@Setter
private boolean frozen;
public Ghost(GhostCollisionChecker collisionChecker, GhostStrategy strategy, GhostStrategy scaterStrategy, int animation, LevelManager levelManager) { public Ghost(GhostCollisionChecker collisionChecker, GhostStrategy strategy, GhostStrategy scaterStrategy, int animation, LevelManager levelManager) {
@ -78,7 +79,7 @@ public class Ghost extends BaseAnimated {
} }
public void update(PacMan pacman, GameMap map) { public void update(PacMan pacman, GameMap map) {
//updateAnimationTick(); if (frozen) return;
if (mode == GhostMode.FRIGHTENED) { if (mode == GhostMode.FRIGHTENED) {
updateInFrightendMode(); updateInFrightendMode();
} }
@ -223,4 +224,5 @@ public class Ghost extends BaseAnimated {
public void resetModes() { public void resetModes() {
mode = GhostMode.CHASE; mode = GhostMode.CHASE;
} }
} }

View File

@ -14,10 +14,15 @@ import se.urmo.game.util.MyPoint;
import java.awt.*; import java.awt.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.util.Arrays; import java.util.Arrays;
import java.util.stream.Stream;
@Slf4j @Slf4j
public class PacMan extends BaseAnimated { public class PacMan extends BaseAnimated {
private enum PacmanState {
ALIVE, DYING, DEAD
}
public static final int PACMAN_SIZE = 32; public static final int PACMAN_SIZE = 32;
public static final int PACMAN_OFFSET = PACMAN_SIZE / 2; public static final int PACMAN_OFFSET = PACMAN_SIZE / 2;
private static final int COLLISION_BOX_SIZE = 16; private static final int COLLISION_BOX_SIZE = 16;
@ -26,7 +31,7 @@ public class PacMan extends BaseAnimated {
private static final int ANIMATION_UPDATE_FREQUENCY = 10; private static final int ANIMATION_UPDATE_FREQUENCY = 10;
private static final double BASE_SPEED = 0.40; private static final double BASE_SPEED = 0.40;
private boolean moving = false; private boolean moving = false;
private final BufferedImage[][] spriteSheets; private final BufferedImage[][] spriteSheets;// [row][col]
private MyPoint position; private MyPoint position;
private final CollisionChecker collisionChecker; private final CollisionChecker collisionChecker;
private final LevelManager levelManager; private final LevelManager levelManager;
@ -35,6 +40,16 @@ public class PacMan extends BaseAnimated {
private Direction direction = Direction.NONE; private Direction direction = Direction.NONE;
private double pacmanLevelSpeed; private double pacmanLevelSpeed;
private static BufferedImage[] deathFramesBase; // original, flattened [0..7]
private BufferedImage[] deathFrames; // working copy
private long lastChangeNs;
private static final long FRAME_NS = 80_000_000L; // 80 ms
// animation state
@Setter
private PacmanState state = PacmanState.ALIVE;
private int deathFrameIdx = 0;
public PacMan(CollisionChecker collisionChecker, LevelManager levelManager) { public PacMan(CollisionChecker collisionChecker, LevelManager levelManager) {
super(ANIMATION_UPDATE_FREQUENCY, 4); super(ANIMATION_UPDATE_FREQUENCY, 4);
this.collisionChecker = collisionChecker; this.collisionChecker = collisionChecker;
@ -43,13 +58,19 @@ public class PacMan extends BaseAnimated {
26 * GameMap.MAP_TILESIZE + GameMap.OFFSET_X, 26 * GameMap.MAP_TILESIZE + GameMap.OFFSET_X,
13 * GameMap.MAP_TILESIZE + GameMap.OFFSET_Y + ((double) GameMap.MAP_TILESIZE / 2)); 13 * GameMap.MAP_TILESIZE + GameMap.OFFSET_Y + ((double) GameMap.MAP_TILESIZE / 2));
this.startPosition = this.position; this.startPosition = this.position;
this.spriteSheets = loadAnimation(); Sprites spriteSheets1 = loadAnimation();
this.spriteSheets = spriteSheets1.spriteSheets;
this.deathFramesBase = spriteSheets1.deathFrames;
this.pacmanLevelSpeed = this.levelManager.getPacmanLevelSpeed(); this.pacmanLevelSpeed = this.levelManager.getPacmanLevelSpeed();
} }
private BufferedImage[][] loadAnimation() { record Sprites(BufferedImage[][] spriteSheets, BufferedImage[] deathFrames) {
}
private Sprites loadAnimation() {
BufferedImage[][] image = new BufferedImage[3][4]; BufferedImage[][] image = new BufferedImage[3][4];
BufferedImage[][] spriteMap = new BufferedImage[6][4]; BufferedImage[][] spriteMap = new BufferedImage[6][4];
BufferedImage[] deathFrames;
BufferedImage img = LoadSave.GetSpriteAtlas("sprites/PacManAssets-PacMan.png"); BufferedImage img = LoadSave.GetSpriteAtlas("sprites/PacManAssets-PacMan.png");
for (int row = 0; row < 3; row++) { for (int row = 0; row < 3; row++) {
@ -59,7 +80,7 @@ public class PacMan extends BaseAnimated {
} }
spriteMap[Direction.RIGHT.ordinal()] = image[0]; spriteMap[Direction.RIGHT.ordinal()] = image[0];
spriteMap[Direction.LEFT.ordinal()] = Arrays.stream(image[0]) spriteMap[Direction.LEFT.ordinal()] = Arrays.stream(image[0])
.map(i -> LoadSave.rotate(i, 180)) .map(i -> LoadSave.rotate(i, Direction.LEFT.angel))
.toArray(BufferedImage[]::new); .toArray(BufferedImage[]::new);
spriteMap[Direction.DOWN.ordinal()] = Arrays.stream(image[0]) spriteMap[Direction.DOWN.ordinal()] = Arrays.stream(image[0])
.map(i -> LoadSave.rotate(i, 90)) .map(i -> LoadSave.rotate(i, 90))
@ -67,12 +88,22 @@ public class PacMan extends BaseAnimated {
spriteMap[Direction.UP.ordinal()] = Arrays.stream(image[0]) spriteMap[Direction.UP.ordinal()] = Arrays.stream(image[0])
.map(i -> LoadSave.rotate(i, 270)) .map(i -> LoadSave.rotate(i, 270))
.toArray(BufferedImage[]::new); .toArray(BufferedImage[]::new);
spriteMap[4] = image[1]; deathFrames = Stream.concat(Arrays.stream(image[1]), Arrays.stream(image[2]))
spriteMap[5] = image[2]; .toArray(BufferedImage[]::new);
return spriteMap;
return new Sprites(spriteMap, deathFrames);
} }
public void draw(Graphics g) { public void draw(Graphics g) {
switch (state) {
case ALIVE -> drawAlive(g);
case DYING -> drawDead(g);
}
}
private void drawAlive(Graphics g) {
if (state != PacmanState.ALIVE) return; // ignore if not dying/dead
g.drawImage( g.drawImage(
spriteSheets[direction == Direction.NONE ? 0 : direction.ordinal()][aniIndex], spriteSheets[direction == Direction.NONE ? 0 : direction.ordinal()][aniIndex],
(int) position.x - PACMAN_OFFSET, (int) position.x - PACMAN_OFFSET,
@ -81,7 +112,39 @@ public class PacMan extends BaseAnimated {
PACMAN_SIZE, null); PACMAN_SIZE, null);
} }
private void drawDead(Graphics g) {
if (state == PacmanState.ALIVE) return; // ignore if not dying/dead
g.drawImage(
deathFrames[deathFrameIdx],
(int) position.x - PACMAN_OFFSET,
(int) position.y - PACMAN_OFFSET,
PACMAN_SIZE,
PACMAN_SIZE,
null
);
}
public void update() { public void update() {
switch (state) {
case ALIVE -> updateAlive();
case DYING -> updateDead();
}
}
private void updateDead() {
if (state != PacmanState.DYING) return;
long now = System.nanoTime();
while (now - lastChangeNs >= FRAME_NS && deathFrameIdx < deathFramesBase.length - 1) {
deathFrameIdx++;
lastChangeNs += FRAME_NS; // carry over exact cadence
}
}
private void updateAlive() {
if (state != PacmanState.ALIVE) return;
if (moving) { if (moving) {
MyPoint mpoint = switch (direction) { MyPoint mpoint = switch (direction) {
case RIGHT -> new MyPoint(position.x + getSpeed(), position.y); case RIGHT -> new MyPoint(position.x + getSpeed(), position.y);
@ -99,6 +162,21 @@ public class PacMan extends BaseAnimated {
} }
} }
// called by PlayingState when collision (non-frightened)
public void startDeathAnimation() {
log.info("Starting death animation");
state = PacmanState.DYING;
deathFrameIdx = 0;
lastChangeNs = System.nanoTime(); // reset stopwatch right now
deathFrames = Arrays.stream(deathFramesBase)
.map(img -> LoadSave.rotate(img, direction.angel))
.toArray(BufferedImage[]::new);
}
public boolean isDeathDone() {
return state == PacmanState.DEAD;
}
private double getSpeed() { private double getSpeed() {
return BASE_SPEED * pacmanLevelSpeed; return BASE_SPEED * pacmanLevelSpeed;
} }
@ -114,6 +192,8 @@ public class PacMan extends BaseAnimated {
public void reset() { public void reset() {
resetPosition(); resetPosition();
aniIndex = 0; // reset animation to start aniIndex = 0; // reset animation to start
state = PacmanState.ALIVE;
deathFrameIdx = 0;
} }
public Image getLifeIcon() { public Image getLifeIcon() {

View File

@ -1,6 +1,7 @@
package se.urmo.game.state; package se.urmo.game.state;
import lombok.Getter; import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import se.urmo.game.collision.GhostCollisionChecker; import se.urmo.game.collision.GhostCollisionChecker;
import se.urmo.game.entities.ghost.strategy.BlinkyStrategy; import se.urmo.game.entities.ghost.strategy.BlinkyStrategy;
@ -41,6 +42,7 @@ public class GhostManager {
5000, 20000, // scatter 5s, chase 20s 5000, 20000, // scatter 5s, chase 20s
5000, Integer.MAX_VALUE // scatter 5s, then chase forever 5000, Integer.MAX_VALUE // scatter 5s, then chase forever
}; };
private boolean frozen;
public GhostManager(GhostCollisionChecker ghostCollisionChecker, AnimationManager animationManager, LevelManager levelManager) { public GhostManager(GhostCollisionChecker ghostCollisionChecker, AnimationManager animationManager, LevelManager levelManager) {
this.levelManager = levelManager; this.levelManager = levelManager;
@ -64,6 +66,7 @@ public class GhostManager {
} }
public void update(PacMan pacman, GameMap map) { public void update(PacMan pacman, GameMap map) {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
if (phaseIndex < cycleDurations.length) { if (phaseIndex < cycleDurations.length) {
int duration = cycleDurations[phaseIndex]; int duration = cycleDurations[phaseIndex];
@ -97,4 +100,8 @@ public class GhostManager {
setMode(GhostMode.SCATTER); setMode(GhostMode.SCATTER);
ghosts.forEach(Ghost::resetPosition); ghosts.forEach(Ghost::resetPosition);
} }
public void setFrozen(boolean frozen) {
this.ghosts.forEach(ghost -> ghost.setFrozen(frozen));
}
} }

View File

@ -88,22 +88,19 @@ public class PlayingState implements GameState {
case LIFE_LOST -> { case LIFE_LOST -> {
// Freeze, then reset round (keep dot state) // Freeze, then reset round (keep dot state)
if (phaseElapsed() >= LIFE_LOST_MS) { if (phaseElapsed() >= LIFE_LOST_MS) {
pacman.reset();
deathInProgress = false; deathInProgress = false;
resetAfterLifeLost(); ghostManager.setFrozen(false);
setPhase(RoundPhase.READY); setPhase(RoundPhase.READY);
if (lives <= 0) { if (lives <= 0) {
endGame(); endGame();
} }
} }
pacman.update();
} }
} }
} }
private void resetAfterLifeLost() {
pacman.reset(); // to start tile, direction stopped
ghostManager.reset(); // to house
}
private void advanceLevel() { private void advanceLevel() {
levelManager.nextLevel(); levelManager.nextLevel();
map.reset(); map.reset();
@ -142,8 +139,8 @@ public class PlayingState implements GameState {
@Override @Override
public void render(Graphics2D g) { public void render(Graphics2D g) {
map.draw(g); map.draw(g);
pacman.draw(g);
ghostManager.draw(g); ghostManager.draw(g);
pacman.draw(g);
fruitManager.draw(g); fruitManager.draw(g);
drawUI(g); drawUI(g);
@ -215,6 +212,8 @@ public class PlayingState implements GameState {
ghost.resetPosition(); ghost.resetPosition();
ghost.setMode(GhostMode.CHASE); // end frightend ghost.setMode(GhostMode.CHASE); // end frightend
} else { } else {
ghostManager.setFrozen(true);
pacman.startDeathAnimation();
deathInProgress = true; deathInProgress = true;
// Pac-Man loses a life // Pac-Man loses a life
lives--; lives--;

View File

@ -1,18 +1,20 @@
package se.urmo.game.util; package se.urmo.game.util;
public enum Direction { public enum Direction {
RIGHT(1, 0), RIGHT(1, 0, 0),
LEFT(-1, 0), LEFT(-1, 0 , 180),
DOWN(0, 1), DOWN(0, 1, 90),
UP(0, -1), UP(0, -1, 270),
NONE(0, 0); NONE(0, 0, 0);
public final int dx; public final int dx;
public final int dy; public final int dy;
public final int angel;
Direction(int dx, int dy) { Direction(int dx, int dy, int angel) {
this.dx = dx; this.dx = dx;
this.dy = dy; this.dy = dy;
this.angel = angel;
} }
public Direction opposite() { public Direction opposite() {