Initial commit

This commit is contained in:
Urban Modig
2025-08-10 17:01:15 +02:00
commit 5ffb77dd00
29 changed files with 736 additions and 0 deletions

View File

@ -0,0 +1,94 @@
package se.urmo.game;
import se.urmo.game.state.Playing;
import javax.swing.*;
import java.awt.*;
public class Game implements Runnable {
public final static int FPS_SET = 120;
public final static int UPS_SET = 200;
private final static double timePerFrame = 1000000000.0 / FPS_SET;
private final static double timePerUpdate = 1000000000.0 / UPS_SET;
private Thread gameThread;
private final JFrame window = new JFrame();
private final GamePanel gamePanel = new GamePanel(this);
private Playing playing = new Playing(this);
public void start() {
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setResizable(false);
window.setTitle("2d");
window.add(gamePanel);
window.pack();
window.setLocationRelativeTo(null);
window.setVisible(true);
startGameThread();
}
public void startGameThread() {
this.gameThread = new Thread(this);
gameThread.start();
}
@Override
public void run() {
long previousTime = System.nanoTime();
int frames = 0;
int updates = 0;
long lastCheck = System.currentTimeMillis();
double deltaU = 0;
double deltaF = 0;
while (gameThread !=null && gameThread.isAlive()) {
long currentTime = System.nanoTime();
deltaU += (currentTime - previousTime) / timePerUpdate;
deltaF += (currentTime - previousTime) / timePerFrame;
previousTime = currentTime;
if (deltaU >= 1) {
update();
updates++;
deltaU--;
}
if (deltaF >= 1) {
gamePanel.repaint(); // triggers JPanel paintComponent
frames++;
deltaF--;
}
if (System.currentTimeMillis() - lastCheck >= 1000) {
lastCheck = System.currentTimeMillis();
System.out.println("FPS: " + frames + " | UPS: " + updates);
frames = 0;
updates = 0;
}
}
}
private void update() {
playing.update();
}
public Playing getPlaying() {
return playing;
}
public GamePanel getGamePanel() {
return gamePanel;
}
public void render(Graphics g) {
playing.draw(g);
}
}

View File

@ -0,0 +1,89 @@
package se.urmo.game;
import se.urmo.game.map.MapTile;
import se.urmo.game.util.LoadSave;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.IntStream;
public class GameMap {
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_X = MAP_TILESIZE; // 16px from left
private BufferedImage[][] image = new BufferedImage[13][19];
private MapTile[][] mapData;
public GameMap() {
loadMap("maps/map1.csv");
loadSprites();
}
private void loadMap(String path) {
List<int[]> rows = new ArrayList<>();
try (InputStream is = getClass().getClassLoader().getResourceAsStream(path);
BufferedReader br = new BufferedReader(new InputStreamReader(Objects.requireNonNull(is)))) {
String line;
while ((line = br.readLine()) != null) {
String[] tokens = line.split(",");
int[] row = new int[tokens.length];
for (int i = 0; i < tokens.length; i++) {
row[i] = Integer.parseInt(tokens[i].trim());
}
rows.add(row);
}
} catch (IOException | NullPointerException e) {
e.printStackTrace();
}
mapData = rows.stream()
.map(row -> IntStream.of(row)
.mapToObj(MapTile::new)
.toArray(MapTile[]::new))
.toArray(MapTile[][]::new);
}
private void loadSprites() {
BufferedImage img = LoadSave.GetSpriteAtlas("sprites/PacManAssets_Map_TileSet2.png");//473B78
for (int row = 0; row < 13; row++) {
for (int col = 0; col < 19; col++) {
image[row][col] = img.getSubimage(MAP_TILESIZE * col, MAP_TILESIZE * row, MAP_TILESIZE, MAP_TILESIZE);
}
}
}
public void draw(Graphics g) {
for (int row = 0; row < mapData.length; row++) {
for (int col = 0; col < mapData[row].length; col++) {
MapTile tile = mapData[row][col];
BufferedImage tileImage = tile.getImage();
if (tileImage != null) {
int x = OFFSET_X + col * MAP_TILESIZE;
int y = OFFSET_Y + row * MAP_TILESIZE;
g.drawImage(tileImage, x, y, MAP_TILESIZE, MAP_TILESIZE, null);
}
}
}
}
public boolean isPassable(int x, int y) {
int row = (y - OFFSET_Y) / MAP_TILESIZE;
int col = (x - OFFSET_X) / MAP_TILESIZE;
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();
System.out.println(row + "," + col + "is" +(passable?"":" not") + " passable");
return passable;
}
}

View File

@ -0,0 +1,55 @@
package se.urmo.game;
import se.urmo.game.input.KeyHandler;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
public class GamePanel extends JPanel {
public static final int ORIGINAL_TILE_SIZE = 8;
public static final int SCALE = 1;
public static final int TILE_SIZE = ORIGINAL_TILE_SIZE * SCALE;
public static final int MAX_SCREEN_COL = 60;
public static final int MAX_SCREEN_ROW = 80;
public static final int SCREEN_WIDTH = MAX_SCREEN_COL * TILE_SIZE;
public static final int SCREEN_HEIGHT = MAX_SCREEN_ROW * TILE_SIZE;
private final Game game;
KeyHandler keyHandler = new KeyHandler(this);
public GamePanel(Game game) {
this.game = game;
this.setPreferredSize(new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT));
this.setBackground(Color.decode("#473B78"));
this.setDoubleBuffered(true);
this.addKeyListener(keyHandler);
this.setFocusable(true);
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
// Cast to Graphics2D for better control (optional)
Graphics2D g2d = (Graphics2D) g;
// Fill full background with purple border
g2d.setColor(new Color(71, 59, 120)); // #473B78
g2d.fillRect(0, 0, getWidth(), getHeight());
// Fill the inner map background with dark (almost black)
int border = 16;
g2d.setColor(new Color(20, 20, 28)); // Dark bluish-black like in the image
g2d.fillRect(border, border, getWidth() - 2 * border, getHeight() - 2 * border);
game.render(g);
}
public void keyPressed(KeyEvent e) {
game.getPlaying().keyPressed(e);
}
public void keyReleased(KeyEvent e) {
game.getPlaying().keyReleased();
}
}

View File

@ -0,0 +1,9 @@
package se.urmo.game;
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
public class Main {
public static void main(String[] args) {
new Game().start();
}
}

View File

@ -0,0 +1,148 @@
package se.urmo.game;
import se.urmo.game.state.CollisionChecker;
import se.urmo.game.util.LoadSave;
import se.urmo.game.util.MiscUtil;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Arrays;
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;
private final Game game;
private int aniTick = 0;
private int aniIndex = 0;
private static final int ANIMATION_UPDATE_FREQUENCY = 25;
private int speed = 1;
private boolean moving;
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 static final BufferedImage innerBox = MiscUtil.createOutlinedBox(8,8, Color.yellow, 2);
private CollisionChecker collisionChecker;
public PacMan(Game game, CollisionChecker collisionChecker) {
this.game = game;
this.collisionChecker = collisionChecker;
loadAnimation();
}
private void loadAnimation() {
BufferedImage[][] image = new BufferedImage[3][4];
BufferedImage img = LoadSave.GetSpriteAtlas("sprites/PacManAssets-PacMan.png");
for (int row = 0; row < 3; row++) {
for (int col = 0; col < 4; col++) {
image[row][col] = img.getSubimage(32 * col, 32 * row, PACMAN_SIZE, PACMAN_SIZE);
}
}
movmentImages[RIGHT] = image[0];
movmentImages[LEFT] = Arrays.stream(image[0])
.map(i -> LoadSave.rotate(i, 180))
.toArray(BufferedImage[]::new);
movmentImages[DOWN] = Arrays.stream(image[0])
.map(i -> LoadSave.rotate(i, 90))
.toArray(BufferedImage[]::new);
movmentImages[UP] = Arrays.stream(image[0])
.map(i -> LoadSave.rotate(i, 270))
.toArray(BufferedImage[]::new);
}
public void draw(Graphics g) {
g.drawImage(innerBox, xPos, yPos, PACMAN_SIZE, PACMAN_SIZE, null);
//g.setColor(Color.RED);
//g.drawLine(xPos, yPos, xPos, yPos);
//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() {
//System.out.println("Pacman current pos: " + xPos + ", " + yPos);
updateAnimationTick();
int nextTileX = xPos;
int nextTileY = yPos;
switch (direction) {
case RIGHT: {
nextTileX++;
break;
}
case LEFT:
nextTileX--;
break;
case UP:
nextTileY--;
break;
case DOWN:
nextTileY++;
break;
}
if (moving && collisionChecker.canMoveTo(direction, nextTileX, nextTileY, PACMAN_SIZE, PACMAN_SIZE)) {
//boolean b = collisionChecker.canMoveTo(direction, nextTileX, nextTileY, PACMAN_SIZE, PACMAN_SIZE);
//System.out.println("Can" + (b ? " " : "not ") + "move to (" +nextTileX+ "," + nextTileY + ")");
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() {
if (moving) {
aniTick++;
if (aniTick >= ANIMATION_UPDATE_FREQUENCY) {
aniTick = 0;
aniIndex++;
if (aniIndex >= 3) {
aniIndex = 0;
}
}
}
}
public void setMoving(boolean moving) {
this.moving = moving;
}
}

View File

@ -0,0 +1,28 @@
package se.urmo.game.input;
import se.urmo.game.GamePanel;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
public class KeyHandler implements KeyListener {
private final GamePanel gamePanel;
public KeyHandler(GamePanel gamePanel) {
this.gamePanel = gamePanel;
}
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
gamePanel.keyPressed(e);
}
@Override
public void keyReleased(KeyEvent e) {
gamePanel.keyReleased(e);
}
}

View File

@ -0,0 +1,26 @@
package se.urmo.game.map;
import se.urmo.game.util.MiscUtil;
import java.awt.*;
import java.awt.image.BufferedImage;
public class MapTile {
private final int value;
private final BufferedImage image;
private final boolean solid;
public MapTile(int value) {
this.value = value;
this.image = value != 0 ? MiscUtil.createOutlinedBox(16, 16, Color.blue, 2) : null;
this.solid = value != 0;
}
public BufferedImage getImage() {
return this.image;
}
public boolean isPassable() {
return ! this.solid;
}
}

View File

@ -0,0 +1,39 @@
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,44 @@
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,57 @@
package se.urmo.game.util;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
public class LoadSave {
public static BufferedImage GetSpriteAtlas(String fileName) {
BufferedImage img = null;
InputStream is = LoadSave.class.getResourceAsStream("/" + fileName);
try {
img = ImageIO.read(is);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return img;
}
public static BufferedImage rotate(BufferedImage img, double angleDegrees) {
double angleRadians = Math.toRadians(angleDegrees);
// Bildens dimensioner
int w = img.getWidth();
int h = img.getHeight();
// Skapa en ny bild med samma storlek och stöd för transparens
BufferedImage rotated = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = rotated.createGraphics();
// För bästa kvalitet
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// Skapa transform: rotera runt bildens mittpunkt
AffineTransform transform = new AffineTransform();
transform.rotate(angleRadians, w / 2.0, h / 2.0);
// Rita originalbilden med transform
g2d.setTransform(transform);
g2d.drawImage(img, 0, 0, null);
g2d.dispose();
return rotated;
}
}

View File

@ -0,0 +1,31 @@
package se.urmo.game.util;
import java.awt.*;
import java.awt.image.BufferedImage;
public class MiscUtil {
public static BufferedImage createOutlinedBox(int width, int height, Color outlineColor, float lineThickness) {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = image.createGraphics();
// Optional: Disable anti-aliasing for pixel-perfect edges
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
// Set color and stroke
g2d.setColor(outlineColor);
g2d.setStroke(new BasicStroke(lineThickness));
// To avoid clipping, draw inside the bounds.
// Adjust for stroke thickness to stay within image bounds.
float halfStroke = lineThickness / 2f;
g2d.drawRect(
Math.round(halfStroke),
Math.round(halfStroke),
Math.round(width - lineThickness),
Math.round(height - lineThickness)
);
g2d.dispose();
return image;
}
}