Ghost movement working - first rev

This commit is contained in:
Urban Modig
2025-08-17 15:03:54 +02:00
parent 1bbba216d2
commit 05285529c5
10 changed files with 95 additions and 63 deletions

19
pom.xml
View File

@ -13,5 +13,24 @@
<maven.compiler.target>21</maven.compiler.target> <maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
<repositories>
<repository>
<id>central</id>
<url>https://repo1.maven.org/maven2/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.13</version>
</dependency>
</dependencies>
</project> </project>

View File

@ -1,5 +1,6 @@
package se.urmo.game.collision; package se.urmo.game.collision;
import lombok.extern.slf4j.Slf4j;
import se.urmo.game.map.GameMap; import se.urmo.game.map.GameMap;
import se.urmo.game.util.Direction; import se.urmo.game.util.Direction;
@ -7,6 +8,7 @@ import java.awt.Point;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@Slf4j
public class GhostCollisionChecker { public class GhostCollisionChecker {
private final GameMap map; private final GameMap map;
@ -32,16 +34,20 @@ public class GhostCollisionChecker {
default -> Collections.EMPTY_LIST; default -> Collections.EMPTY_LIST;
}; };
System.out.println( direction + " bounderies for " + position + " are " + bounderies); log.debug("{} bounderies for {} are {}", direction, position, bounderies);
List<Point> normalizedBoundaries = bounderies.stream() List<Point> normalizedBoundaries = bounderies.stream()
.map(p -> normalizePosition(direction, p, agent_width, agent_height)) .map(p -> normalizePosition(direction, p, agent_width, agent_height))
.toList(); .toList();
if (map.isSolid(normalizedBoundaries)) { if (! map.isSolid(normalizedBoundaries)) {
log.debug("{} is open", direction);
return normalizePosition(direction, position, agent_width, agent_height); return normalizePosition(direction, position, agent_width, agent_height);
}{
log.debug("{} is blocked", direction);
return null;
} }
return null; // Blocked // Blocked
} }
public Point normalizePosition(Direction dir, Point pos, int agent_width, int agent_height) { public Point normalizePosition(Direction dir, Point pos, int agent_width, int agent_height) {
@ -57,9 +63,9 @@ public class GhostCollisionChecker {
return new Point(x, y); return new Point(x, y);
} }
public List<Direction> isIntersection(Point position) { public List<Direction> calculateDirectionAlternatives(Point position) {
List<Direction> intersection = map.isIntersection(position); List<Direction> intersection = map.directionAlternatives(position);
System.out.println("Possible travel directions: " + intersection); log.info("Possible travel directions: {}", intersection);
return intersection; return intersection;
} }
} }

View File

@ -0,0 +1,10 @@
package se.urmo.game.entities;
import java.awt.Point;
public class BlinkyStrategy implements GhostStrategy {
@Override
public Point chooseTarget(PacMan pacman, Ghost self, Ghost blinky) {
return pacman.getTilePosition();
}
}

View File

@ -1,5 +1,6 @@
package se.urmo.game.entities; package se.urmo.game.entities;
import lombok.extern.slf4j.Slf4j;
import se.urmo.game.collision.GhostCollisionChecker; import se.urmo.game.collision.GhostCollisionChecker;
import se.urmo.game.main.Game; import se.urmo.game.main.Game;
import se.urmo.game.map.GameMap; import se.urmo.game.map.GameMap;
@ -13,6 +14,7 @@ import java.awt.Point;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.util.List; import java.util.List;
@Slf4j
public class Ghost { public class Ghost {
private static final int COLLISION_BOX_SIZE = 16; private static final int COLLISION_BOX_SIZE = 16;
private static final int GHOST_SPEED = 1; private static final int GHOST_SPEED = 1;
@ -25,19 +27,20 @@ public class Ghost {
private final Game game; private final Game game;
private final GhostCollisionChecker collisionChecker; private final GhostCollisionChecker collisionChecker;
private final GhostStrategy strategy;
private Point position; private Point position;
private boolean moving = true; private boolean moving = true;
private int aniTick = 0; private int aniTick = 0;
private int aniIndex = 0; private int aniIndex = 0;
private BufferedImage[] animation; private BufferedImage[] animation;
private Direction direction = Direction.LEFT;
private int movementTick = 0; private int movementTick = 0;
public Ghost(Game game, GhostCollisionChecker collisionChecker, GhostStrategy strategy) { public Ghost(Game game, GhostCollisionChecker collisionChecker, GhostStrategy strategy) {
this.game = game; this.game = game;
this.collisionChecker = collisionChecker; this.collisionChecker = collisionChecker;
this.strategy = strategy;
position = new Point(13 * 16 + 8 + GameMap.OFFSET_X, 4 * 16 + GameMap.OFFSET_Y); position = new Point(13 * 16 + 8 + GameMap.OFFSET_X, 4 * 16 + GameMap.OFFSET_Y);
loadAnimation(); loadAnimation();
} }
@ -64,36 +67,25 @@ public class Ghost {
g.drawImage(COLLISION_BOX, position.x, position.y, COLLISION_BOX_SIZE, COLLISION_BOX_SIZE, null); g.drawImage(COLLISION_BOX, position.x, position.y, COLLISION_BOX_SIZE, COLLISION_BOX_SIZE, null);
} }
public void update() { public void update(PacMan pacman) {
updateAnimationTick(); updateAnimationTick();
if(movementTick >= GHOST_MOVEMENT_UPDATE_FREQUENCY) { updatePosition(pacman);
// if intersection - decide direction
// else if direction isPassible
List<Direction> i = collisionChecker.isIntersection(position);
if(!i.isEmpty()){
// Change direction
if(i.contains(Direction.DOWN)) direction = Direction.DOWN;
else if(i.contains(Direction.RIGHT)) direction = Direction.RIGHT;
else if(i.contains(Direction.UP)) direction = Direction.UP;
else direction = Direction.LEFT;
} }
//Point target = strategy.chooseTarget(pacman, this, blinky); private void updatePosition(PacMan pacman) {
if(movementTick >= GHOST_MOVEMENT_UPDATE_FREQUENCY) {
log.info("Evaluating possible directions");
Direction intendedDirection = chooseDirection(
collisionChecker.calculateDirectionAlternatives(position),
strategy.chooseTarget(pacman, null, null));
Point newPosition = switch (direction){
case RIGHT -> new Point(position.x += GHOST_SPEED, position.y);
case LEFT -> new Point(position.x -= GHOST_SPEED, position.y);
case DOWN -> new Point(position.x, position.y += GHOST_SPEED);
case UP -> new Point(position.x, position.y -= GHOST_SPEED);
default -> throw new IllegalStateException("Illegal direction");
};
Point destination = collisionChecker.getValidDestination(direction, newPosition, GHOST_SIZE, GHOST_SIZE); log.info("selecting direction {}", intendedDirection);
Point newPosition = new Point(
position.x += intendedDirection.dx * GHOST_SPEED,
position.y += intendedDirection.dy * GHOST_SPEED);
// if (position.x + direction.dx * GHOST_SPEED < GameMap.OFFSET_X) direction = direction.opposite(); Point destination = collisionChecker.getValidDestination(intendedDirection, newPosition, COLLISION_BOX_SIZE, COLLISION_BOX_SIZE);
// if (position.x + GHOST_SIZE + (direction.dx * GHOST_SPEED) > GamePanel.SCREEN_WIDTH - GameMap.OFFSET_X)
// direction = direction.opposite();
if(destination != null) { if(destination != null) {
position = destination; position = destination;

View File

@ -0,0 +1,7 @@
package se.urmo.game.entities;
import java.awt.Point;
public interface GhostStrategy {
Point chooseTarget(PacMan pacman, Ghost self, Ghost blinky);
}

View File

@ -109,4 +109,8 @@ public class PacMan {
public void setDirection(Direction direction) { public void setDirection(Direction direction) {
this.direction = direction; this.direction = direction;
} }
public Point getTilePosition() {
return position;
}
} }

View File

@ -1,5 +1,6 @@
package se.urmo.game.map; package se.urmo.game.map;
import lombok.extern.slf4j.Slf4j;
import se.urmo.game.main.GamePanel; import se.urmo.game.main.GamePanel;
import se.urmo.game.util.Direction; import se.urmo.game.util.Direction;
import se.urmo.game.util.LoadSave; import se.urmo.game.util.LoadSave;
@ -13,6 +14,7 @@ import java.util.Objects;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import java.util.stream.Stream; import java.util.stream.Stream;
@Slf4j
public class GameMap { public class GameMap {
public static final int MAP_TILESIZE = 16;// 16px from left public static final int MAP_TILESIZE = 16;// 16px from left
public static final int OFFSET_Y = 7 * MAP_TILESIZE; // 160px from top public static final int OFFSET_Y = 7 * MAP_TILESIZE; // 160px from top
@ -169,22 +171,28 @@ public class GameMap {
if(tile.getValue() == 0) tile.setImage(null); if(tile.getValue() == 0) tile.setImage(null);
} }
public List<Direction> isIntersection(Point position) { public List<Direction> directionAlternatives(Point position) {
int row = (position.y - OFFSET_Y) / MAP_TILESIZE; int row = (position.y - OFFSET_Y) / MAP_TILESIZE;
int col = (position.x - OFFSET_X) / MAP_TILESIZE; int col = (position.x - OFFSET_X) / MAP_TILESIZE;
record DirectionCheck(int rowOffset, int colOffset, Direction direction) {} record DirectionCheck(int rowOffset, int colOffset, Direction direction) {}
log.debug("At [{}][{}]", row, col);
return Stream.of( return Stream.of(
new DirectionCheck(0, 1, Direction.RIGHT), new DirectionCheck(0, 1, Direction.RIGHT),
new DirectionCheck(0, -1, Direction.LEFT), new DirectionCheck(0, -1, Direction.LEFT),
new DirectionCheck(1, 0, Direction.DOWN), new DirectionCheck(1, 0, Direction.DOWN),
new DirectionCheck(-1, 0, Direction.UP) new DirectionCheck(-1, 0, Direction.UP)
) )
.filter(dc -> !mapData[row + dc.rowOffset][col + dc.colOffset].isSolid()) .filter(dc -> {
int r = row + dc.rowOffset;
int c = col + dc.colOffset;
MapTile mapTile = mapData[r][c];
boolean solid = mapTile.isSolid();
log.debug("[{}][{}] {} is {} ({})", r, c, dc.direction, solid ? "solid" : " not solid", mapTile.getValue());
return !solid;
})
.map(DirectionCheck::direction) .map(DirectionCheck::direction)
.toList(); .toList();
} }
public boolean isSolid(List<Point> points) { public boolean isSolid(List<Point> points) {
@ -196,7 +204,7 @@ public class GameMap {
int col = (x - OFFSET_X) / MAP_TILESIZE; int col = (x - OFFSET_X) / MAP_TILESIZE;
MapTile mapTile = mapData[row][col]; MapTile mapTile = mapData[row][col];
boolean solid = mapTile.isSolid(); boolean solid = mapTile.isSolid();
//System.out.println("["+row+"]["+col+"] is " + (solid?"solid":" not solid") + " (" + mapTile.getValue() + ")"); log.debug("["+row+"]["+col+"] is " + (solid?"solid":" not solid") + " (" + mapTile.getValue() + ")");
return solid; return solid;
} }
} }

View File

@ -1,7 +1,12 @@
package se.urmo.game.map; package se.urmo.game.map;
import lombok.Getter;
import lombok.Setter;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
@Getter
@Setter
public class MapTile { public class MapTile {
private final int value; private final int value;
private BufferedImage image; private BufferedImage image;
@ -11,14 +16,10 @@ public class MapTile {
public MapTile(BufferedImage image, int value) { public MapTile(BufferedImage image, int value) {
this.value = value; this.value = value;
this.image = image; this.image = image;
this.solid = value != 0; this.solid = value != 0 && value != 99;
this.collisionMask = value != 0 ? createCollisionMask(image) : null; this.collisionMask = value != 0 ? createCollisionMask(image) : null;
} }
public boolean[][] getCollisionMask() {
return collisionMask;
}
private boolean[][] createCollisionMask(BufferedImage img) { private boolean[][] createCollisionMask(BufferedImage img) {
if(img == null) return null; if(img == null) return null;
int w = img.getWidth(); int w = img.getWidth();
@ -33,19 +34,4 @@ public class MapTile {
return mask; return mask;
} }
public BufferedImage getImage() {
return this.image;
}
public boolean isSolid() {
return this.solid;
}
public void setImage(BufferedImage img) {
this.image = img;
}
public int getValue() {
return this.value;
}
} }

View File

@ -1,7 +1,9 @@
package se.urmo.game.state; package se.urmo.game.state;
import lombok.Getter;
import se.urmo.game.collision.CollisionChecker; import se.urmo.game.collision.CollisionChecker;
import se.urmo.game.collision.GhostCollisionChecker; import se.urmo.game.collision.GhostCollisionChecker;
import se.urmo.game.entities.BlinkyStrategy;
import se.urmo.game.entities.Ghost; import se.urmo.game.entities.Ghost;
import se.urmo.game.entities.PacMan; import se.urmo.game.entities.PacMan;
import se.urmo.game.main.Game; import se.urmo.game.main.Game;
@ -16,19 +18,20 @@ public class PlayingState implements GameState {
private final GameStateManager gameStateManager; private final GameStateManager gameStateManager;
private final Ghost ghost; private final Ghost ghost;
private PacMan pacman; private PacMan pacman;
@Getter
private GameMap map; private GameMap map;
public PlayingState(Game game, GameStateManager gameStateManager) { public PlayingState(Game game, GameStateManager gameStateManager) {
this.game = game; this.game = game;
this.gameStateManager = gameStateManager; this.gameStateManager = gameStateManager;
this.map = new GameMap(); this.map = new GameMap();
this.pacman = new PacMan(game, new CollisionChecker(map)); this.pacman = new PacMan(game, new CollisionChecker(map));
this.ghost = new Ghost(game, new GhostCollisionChecker(map), null); this.ghost = new Ghost(game, new GhostCollisionChecker(map), new BlinkyStrategy());
} }
@Override @Override
public void update() { public void update() {
pacman.update(); pacman.update();
ghost.update(); ghost.update(pacman);
} }
@Override @Override
@ -58,8 +61,4 @@ public class PlayingState implements GameState {
pacman.setMoving(false); pacman.setMoving(false);
pacman.setDirection(Direction.NONE); pacman.setDirection(Direction.NONE);
} }
public GameMap getMap() {
return map;
}
} }

View File

@ -0,0 +1 @@
org.slf4j.simpleLogger.defaultLogLevel=debug