Restructuring

This commit is contained in:
Urban Modig
2025-08-12 16:34:31 +02:00
parent 5ffb77dd00
commit 87b2d8df3c
14 changed files with 246 additions and 203 deletions

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -0,0 +1,57 @@
package se.urmo.game;
import java.awt.Point;
import java.util.Collections;
import java.util.List;
public class CollisionChecker {
private GameMap map;
public CollisionChecker(GameMap map) {
this.map = map;
}
public Point getValidDestination(Direction direction, Point position, int agent_width, int agent_height) {
List<Point> list = switch (direction) {
case RIGHT -> List.of(
new Point(position.x + agent_width, position.y), // TOPRIGHT
new Point(position.x + agent_width, position.y + agent_height)); // BOTTOMRIGHT
case LEFT -> List.of(
position, // TOPLEFT
new Point(position.x, position.y + agent_height)); // BOTTOMLEFT
case UP -> List.of(
position, // TOPLEFT
new Point(position.x + agent_width, position.y)); // TOPRIGHT
case DOWN -> List.of(
new Point(position.x, position.y + agent_height), // BOTTOMLEFT
new Point(position.x + agent_width, position.y + agent_height)); // BOTTOMRIGHT
default -> Collections.EMPTY_LIST;
};
List<Point> list2 = list.stream()
.map(p -> normalizePosition(direction, p, agent_width, agent_height))
.toList();
if (map.isPassable(list2)) {
return normalizePosition(direction, position, agent_width, agent_height);
}
return null; // Blocked
}
public Point normalizePosition(Direction dir, Point pos, int agent_width, int agent_height) {
int x = pos.x;
int y = pos.y;
int width = map.getWidth();
int height = map.getHeight();
// tunnel
if (x < GameMap.OFFSET_X) x = width - agent_width - GameMap.OFFSET_X; // right
if (x >= (width - GameMap.OFFSET_X)) x = GameMap.OFFSET_X; // left
//
// if (y < 0) y = height - 1; // optional vertical wrap
// else if (y >= height) y = 0;
return new Point(x, y);
}
}

View File

@ -0,0 +1,21 @@
package se.urmo.game;
public enum Direction {
// private static final int RIGHT = 0;
// private static final int LEFT = 1;
// private static final int DOWN = 2;
// private static final int UP = 3;
RIGHT(1, 0),
LEFT(-1, 0),
DOWN(0, 1),
UP(0, -1),
NONE(0, 0);
public final int dx;
public final int dy;
Direction(int dx, int dy) {
this.dx = dx;
this.dy = dy;
}
}

View File

@ -1,9 +1,8 @@
package se.urmo.game; package se.urmo.game;
import se.urmo.game.state.Playing; import se.urmo.game.state.GameStateManager;
import javax.swing.*; import javax.swing.*;
import java.awt.*;
public class Game implements Runnable { public class Game implements Runnable {
public final static int FPS_SET = 120; public final static int FPS_SET = 120;
@ -12,15 +11,20 @@ public class Game implements Runnable {
private final static double timePerUpdate = 1000000000.0 / UPS_SET; private final static double timePerUpdate = 1000000000.0 / UPS_SET;
private final GameStateManager gameStateManager;
private Thread gameThread; private Thread gameThread;
private final JFrame window = new JFrame(); private final JFrame window = new JFrame();
private final GamePanel gamePanel = new GamePanel(this); private final GamePanel gamePanel;
private Playing playing = new Playing(this);
public Game() {
this.gameStateManager = new GameStateManager(this);
this.gamePanel = new GamePanel(this, gameStateManager);
}
public void start() { public void start() {
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setResizable(false); window.setResizable(false);
window.setTitle("2d"); window.setTitle("PacMan");
window.add(gamePanel); window.add(gamePanel);
@ -30,14 +34,9 @@ public class Game implements Runnable {
window.setLocationRelativeTo(null); window.setLocationRelativeTo(null);
window.setVisible(true); window.setVisible(true);
startGameThread();
}
public void startGameThread() {
this.gameThread = new Thread(this); this.gameThread = new Thread(this);
gameThread.start(); gameThread.start();
} }
@Override @Override
public void run() { public void run() {
long previousTime = System.nanoTime(); long previousTime = System.nanoTime();
@ -57,7 +56,7 @@ public class Game implements Runnable {
previousTime = currentTime; previousTime = currentTime;
if (deltaU >= 1) { if (deltaU >= 1) {
update(); gameStateManager.update();
updates++; updates++;
deltaU--; deltaU--;
} }
@ -77,18 +76,7 @@ public class Game implements Runnable {
} }
} }
private void update() {
playing.update();
}
public Playing getPlaying() {
return playing;
}
public GamePanel getGamePanel() { public GamePanel getGamePanel() {
return gamePanel; return gamePanel;
} }
public void render(Graphics g) {
playing.draw(g);
}
} }

View File

@ -73,17 +73,24 @@ public class GameMap {
} }
} }
public boolean isPassable(List<Point> list) {
return list.stream().allMatch(p -> isPassable(p.x, p.y));
}
public boolean isPassable(int x, int y) { public boolean isPassable(int x, int y) {
int row = (y - OFFSET_Y) / MAP_TILESIZE; int row = (y - OFFSET_Y) / MAP_TILESIZE;
int col = (x - OFFSET_X) / MAP_TILESIZE; int col = (x - OFFSET_X) / MAP_TILESIZE;
System.out.println("(x,y)" + x+","+y + " -> col,row: " + col + "," +row);
if (row > mapData.length - 1) row = 0;
if (col > mapData[row].length - 1) col = 0;
if (row < 0) row = mapData.length - 1;
if (col < 0) col = mapData[row].length - 1;
boolean passable = mapData[row][col].isPassable(); boolean passable = mapData[row][col].isPassable();
System.out.println(row + "," + col + "is" +(passable?"":" not") + " passable"); System.out.println(row + "," + col + "is" +(passable?"":" not") + " passable");
return passable; return passable;
} }
public int getWidth() {
return GamePanel.SCREEN_WIDTH;
}
public int getHeight() {
return GamePanel.SCREEN_HEIGHT;
}
} }

View File

@ -1,10 +1,10 @@
package se.urmo.game; package se.urmo.game;
import se.urmo.game.input.KeyHandler; import se.urmo.game.input.KeyHandler;
import se.urmo.game.state.GameStateManager;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.awt.event.KeyEvent;
public class GamePanel extends JPanel { public class GamePanel extends JPanel {
public static final int ORIGINAL_TILE_SIZE = 8; public static final int ORIGINAL_TILE_SIZE = 8;
@ -15,16 +15,15 @@ public class GamePanel extends JPanel {
public static final int SCREEN_WIDTH = MAX_SCREEN_COL * TILE_SIZE; public static final int SCREEN_WIDTH = MAX_SCREEN_COL * TILE_SIZE;
public static final int SCREEN_HEIGHT = MAX_SCREEN_ROW * TILE_SIZE; public static final int SCREEN_HEIGHT = MAX_SCREEN_ROW * TILE_SIZE;
private final Game game; private final Game game;
private final GameStateManager gameStateManager;
public GamePanel(Game game, GameStateManager gameStateManager) {
KeyHandler keyHandler = new KeyHandler(this);
public GamePanel(Game game) {
this.game = game; this.game = game;
this.gameStateManager = gameStateManager;
this.setPreferredSize(new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT)); this.setPreferredSize(new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT));
this.setBackground(Color.decode("#473B78")); this.setBackground(Color.decode("#473B78"));
this.setDoubleBuffered(true); this.setDoubleBuffered(true);
this.addKeyListener(keyHandler); this.addKeyListener(new KeyHandler(gameStateManager));
this.setFocusable(true); this.setFocusable(true);
} }
@ -41,15 +40,6 @@ public class GamePanel extends JPanel {
int border = 16; int border = 16;
g2d.setColor(new Color(20, 20, 28)); // Dark bluish-black like in the image g2d.setColor(new Color(20, 20, 28)); // Dark bluish-black like in the image
g2d.fillRect(border, border, getWidth() - 2 * border, getHeight() - 2 * border); g2d.fillRect(border, border, getWidth() - 2 * border, getHeight() - 2 * border);
game.render(g); gameStateManager.render((Graphics2D) g);
}
public void keyPressed(KeyEvent e) {
game.getPlaying().keyPressed(e);
}
public void keyReleased(KeyEvent e) {
game.getPlaying().keyReleased();
} }
} }

View File

@ -1,6 +1,5 @@
package se.urmo.game; package se.urmo.game;
import se.urmo.game.state.CollisionChecker;
import se.urmo.game.util.LoadSave; import se.urmo.game.util.LoadSave;
import se.urmo.game.util.MiscUtil; import se.urmo.game.util.MiscUtil;
@ -10,10 +9,6 @@ import java.util.Arrays;
public class PacMan { public class PacMan {
private static final int RIGHT = 0;
private static final int LEFT = 1;
private static final int DOWN = 2;
private static final int UP = 3;
public static final int PACMAN_SIZE = GamePanel.TILE_SIZE; public static final int PACMAN_SIZE = GamePanel.TILE_SIZE;
private final Game game; private final Game game;
private int aniTick = 0; private int aniTick = 0;
@ -22,14 +17,16 @@ public class PacMan {
private int speed = 1; private int speed = 1;
private boolean moving; private boolean moving;
private final BufferedImage[][] movmentImages = new BufferedImage[4][4]; private final BufferedImage[][] movmentImages = new BufferedImage[4][4];
private int direction = 0; //private int xPos = 14 * 16 + 8, yPos = 29 * 16; // top left of object
private int xPos = 14*16 + 8, yPos = 29*16; // top left of object private Point position;
private static final BufferedImage innerBox = MiscUtil.createOutlinedBox(8,8, Color.yellow, 2); private static final BufferedImage innerBox = MiscUtil.createOutlinedBox(8, 8, Color.yellow, 2);
private CollisionChecker collisionChecker; private CollisionChecker collisionChecker;
private Direction direction = Direction.NONE;
public PacMan(Game game, CollisionChecker collisionChecker) { public PacMan(Game game, CollisionChecker collisionChecker) {
this.game = game; this.game = game;
this.collisionChecker = collisionChecker; this.collisionChecker = collisionChecker;
position = new Point(26 * 16 + 8 + GameMap.OFFSET_X, 13 * 16 + GameMap.OFFSET_Y);
loadAnimation(); loadAnimation();
} }
@ -42,91 +39,46 @@ public class PacMan {
image[row][col] = img.getSubimage(32 * col, 32 * row, PACMAN_SIZE, PACMAN_SIZE); image[row][col] = img.getSubimage(32 * col, 32 * row, PACMAN_SIZE, PACMAN_SIZE);
} }
} }
movmentImages[RIGHT] = image[0]; movmentImages[Direction.RIGHT.ordinal()] = image[0];
movmentImages[LEFT] = Arrays.stream(image[0]) movmentImages[Direction.LEFT.ordinal()] = Arrays.stream(image[0])
.map(i -> LoadSave.rotate(i, 180)) .map(i -> LoadSave.rotate(i, 180))
.toArray(BufferedImage[]::new); .toArray(BufferedImage[]::new);
movmentImages[DOWN] = Arrays.stream(image[0]) movmentImages[Direction.DOWN.ordinal()] = Arrays.stream(image[0])
.map(i -> LoadSave.rotate(i, 90)) .map(i -> LoadSave.rotate(i, 90))
.toArray(BufferedImage[]::new); .toArray(BufferedImage[]::new);
movmentImages[UP] = Arrays.stream(image[0]) movmentImages[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);
} }
public void draw(Graphics g) { public void draw(Graphics g) {
g.drawImage(innerBox, xPos, yPos, PACMAN_SIZE, PACMAN_SIZE, null); g.drawImage(innerBox, position.x, position.y, PACMAN_SIZE, PACMAN_SIZE, null);
//g.setColor(Color.RED); //g.setColor(Color.RED);
//g.drawLine(xPos, yPos, xPos, yPos); //g.drawLine(xPos, yPos, xPos, yPos);
//g.drawImage(movmentImages[direction][aniIndex], xPos, yPos, PACMAN_SIZE, PACMAN_SIZE, null); //g.drawImage(movmentImages[direction][aniIndex], xPos, yPos, PACMAN_SIZE, PACMAN_SIZE, null);
} }
public void setLeft() {
direction = LEFT;
}
public void setRight() {
direction = RIGHT;
}
public void setUp() {
direction = UP;
}
public void setDown() {
direction = DOWN;
}
public void update() { public void update() {
//System.out.println("Pacman current pos: " + xPos + ", " + yPos);
updateAnimationTick(); updateAnimationTick();
int nextTileX = xPos; if(direction == Direction.NONE) return;
int nextTileY = yPos;
switch (direction) { Point newPosition = switch (direction){
case RIGHT: { case RIGHT -> new Point(position.x + speed, position.y);
nextTileX++; case LEFT -> new Point(position.x - speed, position.y);
break; case UP -> new Point(position.x , position.y - speed);
} case DOWN -> new Point(position.x, position.y + speed);
case LEFT: default -> throw new IllegalStateException("Unexpected value: " + direction);
nextTileX--; };
break;
case UP:
nextTileY--;
break;
case DOWN:
nextTileY++;
break;
}
Point destination = collisionChecker.getValidDestination(direction, newPosition, PACMAN_SIZE, PACMAN_SIZE);
if (moving && collisionChecker.canMoveTo(direction, nextTileX, nextTileY, PACMAN_SIZE, PACMAN_SIZE)) { if(destination != null) {
//boolean b = collisionChecker.canMoveTo(direction, nextTileX, nextTileY, PACMAN_SIZE, PACMAN_SIZE); position = destination;
//System.out.println("Can" + (b ? " " : "not ") + "move to (" +nextTileX+ "," + nextTileY + ")"); System.out.println("Position: + " + position);
updatePosition();
} }
} }
private void updatePosition() {
if(moving) {
switch (direction) {
case RIGHT -> {
if(xPos + PACMAN_SIZE < GamePanel.SCREEN_WIDTH) xPos += speed;
}
case LEFT -> {
if(xPos > 0) xPos -= speed;
else xPos = 0;
}
case DOWN -> {
if(yPos + PACMAN_SIZE < GamePanel.SCREEN_HEIGHT) yPos += speed;
}
case UP ->{
if(yPos > 0) yPos -= speed;
}
}
}
}
private void updateAnimationTick() { private void updateAnimationTick() {
if (moving) { if (moving) {
@ -145,4 +97,9 @@ public class PacMan {
public void setMoving(boolean moving) { public void setMoving(boolean moving) {
this.moving = moving; this.moving = moving;
} }
public void setDirection(Direction direction) {
this.moving = true;
this.direction = direction;
}
} }

View File

@ -1,28 +1,29 @@
package se.urmo.game.input; package se.urmo.game.input;
import se.urmo.game.GamePanel; import se.urmo.game.state.GameStateManager;
import java.awt.event.KeyEvent; import java.awt.event.KeyEvent;
import java.awt.event.KeyListener; import java.awt.event.KeyListener;
public class KeyHandler implements KeyListener { public class KeyHandler implements KeyListener {
private final GamePanel gamePanel; private final GameStateManager gameStateManager;
public KeyHandler(GamePanel gamePanel) { public KeyHandler(GameStateManager gameStateManager) {
this.gamePanel = gamePanel; this.gameStateManager = gameStateManager;
} }
@Override @Override
public void keyTyped(KeyEvent e) { public void keyTyped(KeyEvent e) {
//not in use
} }
@Override @Override
public void keyPressed(KeyEvent e) { public void keyPressed(KeyEvent e) {
gamePanel.keyPressed(e); gameStateManager.getCurrentState().keyPressed(e);
} }
@Override @Override
public void keyReleased(KeyEvent e) { public void keyReleased(KeyEvent e) {
gamePanel.keyReleased(e); gameStateManager.getCurrentState().keyReleased(e);
} }
} }

View File

@ -1,39 +0,0 @@
package se.urmo.game.state;
import se.urmo.game.GameMap;
public class CollisionChecker {
private GameMap map;
public CollisionChecker(GameMap map) {
this.map = map;
}
public boolean canMoveTo(int dir, int objectLeft, int objectTop, int width, int height) {
int objectRight = objectLeft + width;
int objectBotton = objectTop + height;
return switch (dir){
case 0 -> isPassibleRight(objectRight, objectTop, objectBotton);// right
case 1 -> isPassibleLeft(objectLeft, objectTop, objectBotton);// right
case 2 -> isPassibleBottom(objectLeft, objectRight, objectBotton);
case 3 -> isPassibleTop(objectLeft, objectRight, objectTop);// right
default -> throw new IllegalArgumentException("Invalid dir: " + dir);
};
}
private boolean isPassibleBottom(int objectLeft, int objectRight, int objectBotton) {
return map.isPassable(objectLeft, objectBotton) && map.isPassable(objectRight, objectBotton);
}
private boolean isPassibleTop(int objectLeft, int objectRight, int objectTop) {
return map.isPassable(objectLeft, objectTop) && map.isPassable(objectRight, objectTop);
}
private boolean isPassibleLeft(int objectLeft, int objectTop, int objectBotton) {
return map.isPassable(objectLeft, objectTop) && map.isPassable(objectLeft, objectBotton);
}
private boolean isPassibleRight(int objectRight, int objectTop, int objectBotton) {
return map.isPassable(objectRight, objectTop) && map.isPassable(objectRight, objectBotton);
}
}

View File

@ -0,0 +1,13 @@
package se.urmo.game.state;
import java.awt.*;
import java.awt.event.KeyEvent;
public interface GameState {
void update();
void render(Graphics2D g);
void keyPressed(KeyEvent e);
void keyReleased(KeyEvent e);
}

View File

@ -0,0 +1,35 @@
package se.urmo.game.state;
import se.urmo.game.Game;
import java.awt.*;
import java.util.HashMap;
import java.util.Map;
public class GameStateManager {
private final Game game;
private Map<GameStateType, GameState> states = new HashMap<>();
private GameState currentState;
public GameStateManager(Game game) {
this.game = game;
states.put(GameStateType.PLAYING, new PlayingState(game, this));
setState(GameStateType.PLAYING);
}
private void setState(GameStateType type) {
currentState = states.get(type);
}
public void update() {
currentState.update();
}
public void render(Graphics2D g) {
currentState.render(g);
}
public GameState getCurrentState() {
return currentState;
}
}

View File

@ -0,0 +1,5 @@
package se.urmo.game.state;
public enum GameStateType {
PLAYING,
}

View File

@ -1,44 +0,0 @@
package se.urmo.game.state;
import se.urmo.game.Game;
import se.urmo.game.GameMap;
import se.urmo.game.PacMan;
import java.awt.*;
import java.awt.event.KeyEvent;
public class Playing {
private PacMan pacMan;
private GameMap map = new GameMap();
private final CollisionChecker collisionChecker = new CollisionChecker(map);
public Playing(Game game) {
this.pacMan = new PacMan(game, collisionChecker);
}
public void update() {
pacMan.update();
}
public void keyPressed(KeyEvent e) {
pacMan.setMoving(true);
switch (e.getKeyCode()) {
case KeyEvent.VK_A -> pacMan.setLeft();
case KeyEvent.VK_D -> pacMan.setRight();
case KeyEvent.VK_W -> pacMan.setUp();
case KeyEvent.VK_S -> pacMan.setDown();
}
}
public void draw(Graphics g) {
map.draw(g);
pacMan.draw(g);
}
public void keyReleased() {
pacMan.setMoving(false);
}
public GameMap getMap() {
return map;
}
}

View File

@ -0,0 +1,46 @@
package se.urmo.game.state;
import se.urmo.game.*;
import java.awt.*;
import java.awt.event.KeyEvent;
public class PlayingState implements GameState {
private final Game game;
private final GameStateManager gameStateManager;
private PacMan pacman;
private GameMap map;
public PlayingState(Game game, GameStateManager gameStateManager) {
this.game = game;
this.gameStateManager = gameStateManager;
this.map = new GameMap();
this.pacman = new PacMan(game, new CollisionChecker(map));
}
@Override
public void update() {
pacman.update();
}
@Override
public void render(Graphics2D g) {
map.draw(g);
pacman.draw(g);
}
@Override
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_W -> pacman.setDirection(Direction.UP);
case KeyEvent.VK_S -> pacman.setDirection(Direction.DOWN);
case KeyEvent.VK_A -> pacman.setDirection(Direction.LEFT);
case KeyEvent.VK_D -> pacman.setDirection(Direction.RIGHT);
}
}
@Override
public void keyReleased(KeyEvent e) {
pacman.setMoving(false);
pacman.setDirection(Direction.NONE);
}
}