commit e905477c8df0ac6649475e8c8255be633e666b0c Author: m-mun Date: Fri Sep 4 12:52:46 2026 +0900 init diff --git a/TODO b/TODO new file mode 100644 index 0000000..55d851a --- /dev/null +++ b/TODO @@ -0,0 +1,14 @@ +タイルの当たり判定の実装 - DONE +└バグ修正 - DONE +敵の生成 - WIP +└複数をランダムの位置に生成 - DONE +└キャラクタ同士の当たり判定 - WIP +└敵の行動AI - WIP +UIの実装 +┗ステータスの表示 - DONE +┗ボタンの実装 - WIP +┗セリフの表示 - NONE + +使用素材 +https://absuredmrsteel.itch.io/16x16-home-interior-pixel-art +https://opengameart.org/content/rpg-asset-character-prince-nes \ No newline at end of file diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..57e9cdb --- /dev/null +++ b/build.bat @@ -0,0 +1 @@ +javac -Xlint:deprecation -Xdiags:verbose -Xlint:unchecked -deprecation -d bin -encoding utf8 -classpath res -sourcepath src src/Launcher.java diff --git a/res/character/Full_SpriteSheet.png b/res/character/Full_SpriteSheet.png new file mode 100644 index 0000000..3b4d9dd Binary files /dev/null and b/res/character/Full_SpriteSheet.png differ diff --git a/res/default.png b/res/default.png new file mode 100644 index 0000000..5d84e0e Binary files /dev/null and b/res/default.png differ diff --git a/res/tileset/House_Tileset.png b/res/tileset/House_Tileset.png new file mode 100644 index 0000000..dca5854 Binary files /dev/null and b/res/tileset/House_Tileset.png differ diff --git a/res/tileset/House_Tileset.tsx b/res/tileset/House_Tileset.tsx new file mode 100644 index 0000000..2c681e5 --- /dev/null +++ b/res/tileset/House_Tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/res/tileset/map1.tmx b/res/tileset/map1.tmx new file mode 100644 index 0000000..61543e3 --- /dev/null +++ b/res/tileset/map1.tmx @@ -0,0 +1,100 @@ + + + + + +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,19,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,20,0,0,0,27,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,36,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,52,0,0,0,0, +0,0,0,0,0,0,0,49,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,40,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,37,38,39,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,46,47,48,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,23,0,0,0,0,0,0,0,0,0,0, +0,0,0,28,29,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + + +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,0,1,1,1,0,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1, +1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + + + diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..1084c40 --- /dev/null +++ b/run.bat @@ -0,0 +1 @@ +java -classpath ./res;./bin Launcher diff --git a/src/Launcher.java b/src/Launcher.java new file mode 100644 index 0000000..ddeec83 --- /dev/null +++ b/src/Launcher.java @@ -0,0 +1,12 @@ +import game.GameLoop; + +public class Launcher { + private Launcher() { + Thread thread = new Thread(new GameLoop()); + thread.start(); + } + + public static void main(String[] args) { + new Launcher(); + } +} diff --git a/src/ai/AICondition.java b/src/ai/AICondition.java new file mode 100644 index 0000000..9368dec --- /dev/null +++ b/src/ai/AICondition.java @@ -0,0 +1,8 @@ +package ai; + +import entity.Entity; +import state.State; + +public interface AICondition { + boolean isMet(State state, Entity entity); +} diff --git a/src/ai/AIManager.java b/src/ai/AIManager.java new file mode 100644 index 0000000..aefcbe6 --- /dev/null +++ b/src/ai/AIManager.java @@ -0,0 +1,38 @@ +package ai; + +import ai.state.AIState; +import ai.state.Stand; +import ai.state.Wander; +import entity.Entity; +import state.State; + +public class AIManager { + private AIState currentAIState; + + public AIManager() { + transitionTo("stand"); + } + + public AIState getCurrentAIState() { + return currentAIState; + } + + private void transitionTo(String nextState) { + switch (nextState) { + case "wander": + currentAIState = new Wander(); + break; + case "stand": + currentAIState = new Stand(); + break; + default: + } + } + + public void update(State state, Entity entity) { + currentAIState.update(state, entity); + if (currentAIState.shouldTransition(state, entity)) { + transitionTo(currentAIState.getNextState()); + } + } +} diff --git a/src/ai/AITransition.java b/src/ai/AITransition.java new file mode 100644 index 0000000..02dbedf --- /dev/null +++ b/src/ai/AITransition.java @@ -0,0 +1,22 @@ +package ai; + +import entity.Entity; +import state.State; + +public class AITransition { + private String nextState; + private AICondition condition; + + public AITransition(String nextState, AICondition condition) { + this.nextState = nextState; + this.condition = condition; + } + + public boolean shouldTransition(State state, Entity entity) { + return condition.isMet(state, entity); + } + + public String getNextState() { + return nextState; + } +} diff --git a/src/ai/state/AIState.java b/src/ai/state/AIState.java new file mode 100644 index 0000000..32de0c0 --- /dev/null +++ b/src/ai/state/AIState.java @@ -0,0 +1,25 @@ +package ai.state; + +import ai.AITransition; +import entity.Entity; +import state.State; + +public abstract class AIState { + private AITransition transition; + + public AIState() { + transition = initTransition(); + } + + protected abstract AITransition initTransition(); + + public String getNextState() { + return transition.getNextState(); + } + + public boolean shouldTransition(State state, Entity entity) { + return transition.shouldTransition(state, entity); + } + + public abstract void update(State state, Entity entity); +} diff --git a/src/ai/state/Stand.java b/src/ai/state/Stand.java new file mode 100644 index 0000000..2795e61 --- /dev/null +++ b/src/ai/state/Stand.java @@ -0,0 +1,24 @@ +package ai.state; + +import ai.AITransition; +import entity.Entity; +import game.GameLoop; +import state.State; + +public class Stand extends AIState { + private int alive; + + public Stand() { + this.alive = 0; + } + + @Override + public AITransition initTransition() { + return new AITransition("wander", ((state, eneity) -> alive >= GameLoop.ups * 3)); + } + + @Override + public void update(State state, Entity entity) { + alive++; + } +} diff --git a/src/ai/state/Wander.java b/src/ai/state/Wander.java new file mode 100644 index 0000000..67e1013 --- /dev/null +++ b/src/ai/state/Wander.java @@ -0,0 +1,38 @@ +package ai.state; + +import ai.AITransition; +import controller.EnemyController; +import core.Position; +import entity.Entity; +import state.State; + +public class Wander extends AIState { + private Position target; + + public Wander() { + super(); + } + + private boolean arrived(Entity entity) { + return target != null && entity.getPosition().isInRangeOf(target); + } + + @Override + protected AITransition initTransition() { + return new AITransition("stand", ((state, entity) -> arrived(entity))); + } + + @Override + public void update(State state, Entity entity) { + EnemyController controller = (EnemyController)entity.getController(); + if (target == null) { + target = state.getGameMap().findRandomPosition(); + } + if (arrived(entity)) { + controller.stop(); + } + if (target != null) { + controller.moveToTarget(target, entity.getPosition()); + } + } +} diff --git a/src/controller/EnemyController.java b/src/controller/EnemyController.java new file mode 100644 index 0000000..3718035 --- /dev/null +++ b/src/controller/EnemyController.java @@ -0,0 +1,43 @@ +package controller; + +import core.Position; + +public class EnemyController implements EntityController { + private boolean up; + private boolean down; + private boolean right; + private boolean left; + + public boolean isRequestKeyAction() { + return false; + } + + public boolean isRequestKeyDown() { + return down; + } + + public boolean isRequestKeyLeft() { + return left; + } + + public boolean isRequestKeyRight() { + return right; + } + + public boolean isRequestKeyUp() { + return up; + } + + public void moveToTarget(Position target, Position current) { + double deltaX = target.getX() - current.getX(); + double deltaY = target.getY() - current.getY(); + up = deltaY < 0 && Math.abs(deltaY) > Position.PROXIMITY_RANGE; + right = deltaX > 0 && Math.abs(deltaX) > Position.PROXIMITY_RANGE; + down = deltaY > 0 && Math.abs(deltaY) > Position.PROXIMITY_RANGE; + left = deltaX < 0 && Math.abs(deltaX) > Position.PROXIMITY_RANGE; + } + + public void stop() { + up = down = left = right = false; + } +} diff --git a/src/controller/EntityController.java b/src/controller/EntityController.java new file mode 100644 index 0000000..5545ae5 --- /dev/null +++ b/src/controller/EntityController.java @@ -0,0 +1,9 @@ +package controller; + +public interface EntityController { + boolean isRequestKeyAction(); + boolean isRequestKeyDown(); + boolean isRequestKeyLeft(); + boolean isRequestKeyRight(); + boolean isRequestKeyUp(); +} diff --git a/src/core/Position.java b/src/core/Position.java new file mode 100644 index 0000000..c328c70 --- /dev/null +++ b/src/core/Position.java @@ -0,0 +1,58 @@ +package core; + +public class Position { + public static final int PROXIMITY_RANGE = 3; + + private double x; + private double y; + + public Position() { + x = 0; + y = 0; + } + + public Position(int x, int y) { + this.x = x; + this.y = y; + } + + public Position(double x, double y) { + this.x = x; + this.y = y; + } + + public Position(Position position) { + x = position.getX(); + y = position.getY(); + } + + public void add(Vector2D vector) { + x += vector.getX(); + y += vector.getY(); + } + + public boolean isInRangeOf(Position position) { + return Math.abs(x - position.getX()) < PROXIMITY_RANGE && Math.abs(y - position.getY()) < PROXIMITY_RANGE; + } + + public double getX() { + return x; + } + + public double getY() { + return y; + } + + public void setX(double x) { + this.x = x; + } + + public void setY(double y) { + this.y = y; + } + + @Override + public String toString() { + return "POSITION: (X: " + x + ", Y: " + y + ")"; + } +} diff --git a/src/core/Size.java b/src/core/Size.java new file mode 100644 index 0000000..bebbe99 --- /dev/null +++ b/src/core/Size.java @@ -0,0 +1,40 @@ +package core; + +public class Size { + private int height; + private int width; + + public Size() { + width = 0; + height = 0; + } + + public Size(int width, int height) { + this.width = width; + this.height = height; + } + + public Size(Size size) { + } + + public int getHeight() { + return height; + } + + public int getWidth() { + return width; + } + + public void setHeight(int height) { + this.height = height; + } + + public void setWidth(int width) { + this.width = width; + } + + @Override + public String toString() { + return "SIZE: (W: " + width + ", H: " + height + ")"; + } +} diff --git a/src/core/Vector2D.java b/src/core/Vector2D.java new file mode 100644 index 0000000..5cf8f59 --- /dev/null +++ b/src/core/Vector2D.java @@ -0,0 +1,72 @@ +package core; + +public class Vector2D { + private double x; + private double y; + + public Vector2D() { + this(0, 0); + } + + public Vector2D(double x, double y) { + this.x = x; + this.y = y; + } + + public static Vector2D copyOf(Vector2D vector) { + return new Vector2D(vector.getX(), vector.getY()); + } + + public static Vector2D directionBetweenPositions(Position start, Position end) { + Vector2D direction = new Vector2D(start.getX() - end.getX(), start.getY() - end.getY()); + direction.normalize(); + return direction; + } + + public static double dotProduct(Vector2D v1, Vector2D v2) { + return v1.getX() * v2.getX() + v1.getY() * v2.getY(); + } + + public double getX() { + return x; + } + + public double getY() { + return y; + } + + public double length() { + return Math.sqrt(x * x + y * y); + } + + public void multiply(double speed) { + x *= speed; + y *= speed; + } + + public void normalize() { + double length = length(); + x = x == 0 ? 0 : x/length; + y = y == 0 ? 0 : y/length; + } + + public void rotateByDegrees(int degrees) { + double radians = Math.toRadians(degrees); + double cosAngle = Math.cos(radians); + double sinAngle = Math.sin(radians); + double rotatedX = cosAngle * x - sinAngle * y; + double rotatedY = sinAngle * x + cosAngle * y; + x = rotatedX; + y = rotatedY; + } + + public void setVector(double x, double y) { + this.x = x; + this.y = y; + } + + @Override + public String toString() { + return "VECTOR: (X: " + x + ", Y: " + y + ")"; + } +} diff --git a/src/display/DebugRenderer.java b/src/display/DebugRenderer.java new file mode 100644 index 0000000..f0da31b --- /dev/null +++ b/src/display/DebugRenderer.java @@ -0,0 +1,74 @@ +package display; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; + +import core.Position; +import core.Size; +import entity.creature.Enemy; +import entity.creature.Player; +import game.GameLoop; +import map.GameMap; +import state.PlayState; +import state.State; + +public class DebugRenderer { + public void renderDebug(Graphics2D g, State state) { + renderMapDataCollisionBox(g, state); + renderDebugCollisionBox(g, state); + } + + private void renderDebugCollisionBox(Graphics2D g, State state) { + g.setColor(Color.RED); + g.setStroke(new BasicStroke(2)); + if (state instanceof PlayState playState) { + Position cp = state.getGameCamera().getPosition(); + Player p = playState.getPlayer(); + int scale = state.getGameMap().getScale(); + playState.getEntities().stream().forEach(entity -> { + g.drawRect( + (int)(entity.getCollisionBox().getX() - cp.getX()), (int)(entity.getCollisionBox().getY() - cp.getY()), + (int)entity.getCollisionBox().getWidth(), (int)entity.getCollisionBox().getHeight()); + if (entity instanceof Enemy enemy) { + g.setFont(new Font(Font.MONOSPACED, Font.BOLD, 30)); + g.setColor(Color.GRAY); + g.drawString(enemy.getCurrentState(), (int)(entity.getX() - cp.getX() + 2), (int)(entity.getY() - 8 - cp.getY())); + g.setColor(Color.WHITE); + g.drawString(enemy.getCurrentState(), (int)(entity.getX() - cp.getX()), (int)(entity.getY() - 10 - cp.getY())); + g.setColor(Color.RED); + } + }); + g.drawRect( + (int)(p.getCollisionBox().getX() - cp.getX()), (int)(p.getCollisionBox().getY() - cp.getY()), + (int)p.getCollisionBox().getWidth(), (int)p.getCollisionBox().getHeight()); + } + } + + private void renderMapDataCollisionBox(Graphics2D g, State state) { + Color color = Color.RED; + GameMap gm = state.getGameMap(); + Size tileSize = gm.getTileSize(); + Position cp = state.getGameCamera().getPosition(); + int scale = gm.getScale(); + int[][] mapData = gm.getMapData().get(GameMap.MAP_LAYER_4); + for (int y = 0; y < mapData.length; y++) { + for (int x = 0; x < mapData[y].length; x++) { + color = mapData[y][x] == 0 ? Color.YELLOW : Color.GREEN; + g.setColor(color); + g.drawRect(x * tileSize.getWidth() * scale - (int) cp.getX(), y * tileSize.getHeight() * scale - (int) cp.getY(), + tileSize.getWidth() * scale, tileSize.getHeight() * scale); + } + } + if (state instanceof PlayState playState) { + if (gm.getTileRect().x != -1) { + Player player = playState.getPlayer(); + g.setColor(player.isWalkable() ? Color.BLUE : Color.RED); + g.setStroke(new BasicStroke(3)); + g.drawRect(gm.getTileRect().x - (int) cp.getX(), gm.getTileRect().y - (int) cp.getY(), tileSize.getWidth() * scale, tileSize.getHeight() * scale); + g.drawRect(gm.getTileRect2().x - (int) cp.getX(), gm.getTileRect2().y - (int) cp.getY(), tileSize.getWidth() * scale, tileSize.getHeight() * scale); + } + } + } +} diff --git a/src/display/Display.java b/src/display/Display.java new file mode 100644 index 0000000..5fb7124 --- /dev/null +++ b/src/display/Display.java @@ -0,0 +1,82 @@ +package display; + +import java.awt.Canvas; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsEnvironment; +import java.awt.image.BufferStrategy; + +import javax.swing.JFrame; + +import core.Size; +import input.Keyboard; +import input.Mouse; +import state.State; + +public class Display extends JFrame { + public static boolean debug; + private Canvas canvas; + private Keyboard keyboard; + private Mouse mouse; + private Renderer renderer; + private DebugRenderer debugRenderer; + + public Display(Keyboard keyboard, Mouse mouse, String title, Size windowSize) { + super(title); + this.keyboard = keyboard; + this.mouse = mouse; + renderer = new Renderer(); + debugRenderer = new DebugRenderer(); + createFrame(windowSize); + createCanvas(windowSize); + add(canvas); + pack(); + setVisible(true); + canvas.createBufferStrategy(2); + addKeyListener(this.keyboard); + canvas.addKeyListener(this.keyboard); + canvas.addMouseListener(this.mouse); + canvas.addMouseMotionListener(this.mouse); + canvas.setFocusable(true); + } + + private void createCanvas(Size windowSize) { + GraphicsConfiguration config = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); + canvas = new Canvas(config); + Dimension dimension = new Dimension(windowSize.getWidth(), windowSize.getHeight()); + canvas.setPreferredSize(dimension); + canvas.setMaximumSize(dimension); + canvas.setMinimumSize(dimension); + canvas.setBackground(Color.BLACK); + } + + private void createFrame(Size windowSize) { + setSize(windowSize.getWidth(), windowSize.getHeight()); + setUndecorated(true); + setResizable(false); + setDefaultCloseOperation(EXIT_ON_CLOSE); + setLocationRelativeTo(null); + } + + public Size getCanvasSize() { + return new Size(canvas.getWidth(), canvas.getHeight()); + } + + public void render(State state) { + BufferStrategy bs = canvas.getBufferStrategy(); + if (bs == null) { + canvas.createBufferStrategy(2); + return; + } + Graphics2D g = (Graphics2D) bs.getDrawGraphics(); + g.clearRect(0, 0, canvas.getWidth(), canvas.getHeight()); + renderer.render(g, state); + if (debug) { + debugRenderer.renderDebug(g, state); + } + g.dispose(); + bs.show(); + } +} diff --git a/src/display/GameCamera.java b/src/display/GameCamera.java new file mode 100644 index 0000000..6fdbf7c --- /dev/null +++ b/src/display/GameCamera.java @@ -0,0 +1,55 @@ +package display; + +import java.awt.Rectangle; +import java.util.Optional; + +import core.Position; +import core.Size; +import entity.creature.Player; +import state.State; + +public class GameCamera { + private Optional focusObject; + private Position position; + private Size windowSize; + private Rectangle viewBounds; + + public GameCamera(Size windowSize) { + this.windowSize = windowSize; + position = new Position(); + focusObject = Optional.empty(); + calculateViewBounds(); + } + + private void calculateViewBounds() { + viewBounds = new Rectangle((int) position.getX(), (int) position.getY(), windowSize.getWidth(), windowSize.getHeight()); + } + + public void focusOn(Player player) { + focusObject = Optional.of(player); + } + + public boolean isInView(Player player) { + return viewBounds.intersects(player.getX(), player.getY(), player.getWidth(), player.getHeight()); + } + + public Position getPosition() { + return position; + } + + public Size getWindowSize() { + return windowSize; + } + + public void setPosition(Position position) { + this.position = position; + } + + public void update(State state) { + if (focusObject.isPresent()) { + Position objectPosition = focusObject.get().getPosition(); + position.setX(objectPosition.getX() - windowSize.getWidth() / 2); + position.setY(objectPosition.getY() - windowSize.getHeight() / 2); + } + } +} diff --git a/src/display/Renderer.java b/src/display/Renderer.java new file mode 100644 index 0000000..e3c4d8c --- /dev/null +++ b/src/display/Renderer.java @@ -0,0 +1,86 @@ +package display; + +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.util.List; + +import core.Position; +import core.Size; +import entity.creature.Player; +import map.GameMap; +import state.PlayState; +import state.State; + +public class Renderer { + public void render(Graphics2D g, State state) { + renderGameMap(g, state); + renderGameObjects(g, state); + renderUI(g, state); + } + + private void renderGameMap(Graphics2D g, State state) { + GameMap gameMap = state.getGameMap(); + List tileset = gameMap.getTileSet(); + Size stageSize = gameMap.getStageSize(); + GameCamera camera = state.getGameCamera(); + for (int i = 0; i < GameMap.NUMBER_OF_MAP_LAYER - 1; i++) { + for (int y = 0; y < stageSize.getHeight(); y++) { + for (int x = 0; x < stageSize.getWidth(); x++) { + int id = gameMap.getMapDataCell(x, y, i); + if (id >= 1) { + Size tileSize = gameMap.getTileSize(); + int scale = gameMap.getScale(); + Position cp = camera.getPosition(); + g.drawImage( + tileset.get(id - 1), + x * tileSize.getWidth() * scale - (int) cp.getX(), y * tileSize.getHeight() * scale - (int) cp.getY(), + tileSize.getWidth() * scale, tileSize.getHeight() * scale, + null); + } + } + } + } + } + + private void renderGameObjects(Graphics2D g, State state) { + GameMap gameMap = state.getGameMap(); + if (state instanceof PlayState playState) { + GameCamera camera = state.getGameCamera(); + Position cp = camera.getPosition(); + Player player = playState.getPlayer(); + playState.getEntities().stream().forEach(entity -> { + g.drawImage( + entity.getCurrentImage(), + (int) entity.getX() - (int) cp.getX(), + (int) entity.getY() - (int) cp.getY(), + gameMap.getTileSize().getWidth() * gameMap.getScale(), + gameMap.getTileSize().getHeight() * gameMap.getScale(), + null); + }); + g.drawImage( + player.getCurrentImage(), + (int) player.getX() - (int) cp.getX(), + (int) player.getY() - (int) cp.getY(), + gameMap.getTileSize().getWidth() * gameMap.getScale(), + gameMap.getTileSize().getHeight() * gameMap.getScale(), + null); + } + } + + private void renderUI(Graphics2D g, State state) { + state.getUiContainers().forEach(uiContainer -> + g.drawImage( + uiContainer.getSprite(), + (int)uiContainer.getRelativePosition().getX(), + (int)uiContainer.getRelativePosition().getY(), + null)); + if (Display.debug) { + state.getUiDebugContainers().forEach(uiContainer -> + g.drawImage( + uiContainer.getSprite(), + (int)uiContainer.getRelativePosition().getX(), + (int)uiContainer.getRelativePosition().getY(), + null)); + } + } +} diff --git a/src/entity/Entity.java b/src/entity/Entity.java new file mode 100644 index 0000000..fe237f1 --- /dev/null +++ b/src/entity/Entity.java @@ -0,0 +1,199 @@ +package entity; + +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import controller.EnemyController; +import controller.EntityController; +import core.Position; +import core.Size; +import core.Vector2D; +import entity.creature.Direction; +import gfx.Animation; +import gfx.SpriteSheet; +import input.Keyboard; +import map.GameMap; +import state.PlayState; +import state.State; + +public abstract class Entity { + private static final int DISTANCE_TO_ENTITY = 100; + + protected GameMap gameMap; + protected Position position; + protected Size size; + protected Vector2D vector; + protected Keyboard keyboard; + protected Map animations; + protected Animation currentAnimation; + protected Direction direction; + protected Rectangle collisionBox; + protected double walkSpeed; + protected EntityController controller; + + public Entity(GameMap gameMap, Keyboard keyboard, Position position, Size size, String fileName, int tileWidth, int tileHeight) { + this.gameMap = gameMap; + this.position = position; + this.size = size; + this.keyboard = keyboard; + vector = new Vector2D(); + SpriteSheet spriteSheet = new SpriteSheet(fileName, tileWidth, tileHeight); + registerAnimations(spriteSheet); + currentAnimation = animations.get(Direction.DOWN); + direction = Direction.STOP; + collisionBox = new Rectangle((int)position.getX(), (int)position.getY(), + size.getWidth() * gameMap.getScale(), size.getHeight() * gameMap.getScale()); + walkSpeed = 6; + } + + protected void changeAnimation(Direction direction) { + currentAnimation.start(); + if (!currentAnimation.equals(animations.get(direction))) { + currentAnimation = animations.get(direction); + } + } + + public BufferedImage getCurrentImage() { + return currentAnimation.getCurrentImage(); + } + + public Direction getDirection() { + return direction; + } + + public Map getAnimations() { + return animations; + } + + public Animation getCurrentAnimation() { + return currentAnimation; + } + + public Rectangle getCollisionBox() { + return collisionBox; + } + + public EntityController getController() { + return controller; + } + + public Keyboard getKeyboard() { + return keyboard; + } + + public int getHeight() { + return size.getHeight(); + } + + public Position getPosition() { + return position; + } + + public Size getSize() { + return size; + } + + public Vector2D getVector() { + return vector; + } + + public int getWidth() { + return size.getWidth(); + } + + public double getX() { + return position.getX(); + } + + public double getY() { + return position.getY(); + } + + private void handleAnimation() { + switch (direction) { + case UP: + changeAnimation(Direction.UP); + break; + case LEFT: + changeAnimation(Direction.LEFT); + break; + case DOWN: + changeAnimation(Direction.DOWN); + break; + case RIGHT: + changeAnimation(Direction.RIGHT); + break; + case STOP: + currentAnimation.stop(); + default: + break; + } + currentAnimation.update(); + } + + protected void handleCollision(State state) { + GameMap map = state.getGameMap(); + collisionBox.x += vector.getX(); + collisionBox.y += vector.getY(); + switch (direction) { + case UP: + vector.setVector(0, -walkSpeed); + break; + case LEFT: + vector.setVector(-walkSpeed, 0); + break; + case DOWN: + vector.setVector(0, walkSpeed); + break; + case RIGHT: + vector.setVector(walkSpeed, 0); + break; + case STOP: + default: + vector.setVector(0, 0); + break; + } + if (!map.isWalkable(this) || !isWalkable(state)) { + vector.setVector(0, 0); + } + position.add(vector); + } + + private boolean isCollision(Entity entity) { + Rectangle rect = new Rectangle( + collisionBox.x + (int)vector.getX(), collisionBox.y + (int)vector.getY(), + collisionBox.width, collisionBox.height); + return entity.getCollisionBox().intersects(rect); + } + + public boolean isWalkable() { + return (vector.getX() != 0 || vector.getY() != 0); + } + + protected boolean isWalkable(State state) { + if (state instanceof PlayState playState) { + List filterList = playState.getEntities().stream() + .filter(entity -> Math.abs(getX() - entity.getX()) < DISTANCE_TO_ENTITY && Math.abs(getY() - entity.getY()) < DISTANCE_TO_ENTITY) + .collect(Collectors.toList()); + for (int i = 0; i < filterList.size(); i++) { + if (isCollision(filterList.get(i))) { + return false; + } + } + } + return true; + } + + protected void registerAnimations(SpriteSheet spriteSheet) { + animations = new HashMap<>(); + } + + public void update(State state) { + handleAnimation(); + handleCollision(state); + } +} diff --git a/src/entity/creature/Creature.java b/src/entity/creature/Creature.java new file mode 100644 index 0000000..98a3fec --- /dev/null +++ b/src/entity/creature/Creature.java @@ -0,0 +1,138 @@ +package entity.creature; + +import core.Position; +import core.Size; +import entity.Entity; +import input.Keyboard; +import map.GameMap; + +/* + * NESS LEVEL UP MAP + * LV | NEED EXP + * 1 | 0 + * 2 | 4 + * 3 | 17 + * 4 | 44 + * 5 | 109 + * 6 | 236 + * 7 | 449 + * 8 | 772 + * 9 | 1,229 + * 10 | 1,884 + * 11 | 2,611 + * 12 | 3,644 + * 13 | 4,877 + * 14 | 6,364 + * 15 | 8,129 + * 16 | 10,703 + * 17 | 13,241 + * 18 | 16,214 + * 19 | 19,673 + * 20 | 23,699 + * 21 | 28,253 + * 22 | 33,476 + * 23 | 39,389 + * 24 | 46,043 + * 25 | 53,489 + * 26 | 61,778 + * 27 | 70,961 + * 28 | 81,089 + * 29 | 92,213 + * 30 | 104,384 + * 31 | 117,653 + * 32 | 132,071 + * 33 | 147,689 + * 34 | 164,558 + * 35 | 182,729 + * 36 | 202,270 + * 37 | 223,249 + * 38 | 245,734 + * 39 | 269,793 + * 40 | 295,494 + * 41 | 322,905 + * 42 | 352,094 + * 43 | 383,129 + * 44 | 416,078 + * 45 | 451,009 + * 46 | 487,990 + * 47 | 527,089 + * 48 | 568,374 + * 49 | 611,913 + * 50 | 657,774 + * 51 | 706,025 + * 52 | 756,734 + * 53 | 809,969 + * 54 | 865,798 + * 55 | 924,289 + * 56 | 985,510 + * 57 | 1,049,529 + * 58 | 1,116,414 + * 59 | 1,186,233 + * 60 | 1,259,054 + * 61 | 1,335,030 + * 62 | 1,414,314 + * 63 | 1,497,059 + * 64 | 1,583,418 + * 65 | 1,673,544 + * 66 | 1,767,590 + * 67 | 1,865,709 + * 68 | 1,968,054 + * 69 | 2,074,778 + * 70 | 2,186,034 + * 71 | 2,301,975 + * 72 | 2,422,754 + * 73 | 2,548,524 + * 74 | 2,679,438 + * 75 | 2,815,649 + * 76 | 2,957,310 + * 77 | 3,104,574 + * 78 | 3,257,594 + * 79 | 3,416,523 + * 80 | 3,581,514 + * 81 | 3,752,720 + * 82 | 3,930,294 + * 83 | 4,114,389 + * 84 | 4,305,158 + * 85 | 4,502,754 + * 86 | 4,707,330 + * 87 | 4,919,039 + * 88 | 5,138,034 + * 89 | 5,364,468 + * 90 | 5,598,494 + * 91 | 5,840,265 + * 92 | 6,089,934 + * 93 | 6,347,654 + * 94 | 6,613,578 + * 95 | 6,887,859 + * 96 | 7,170,650 + * 97 | 7,462,104 + * 98 | 7,762,374 + * 99 | 8,071,613 + */ +public abstract class Creature extends Entity { + protected String name; + protected int level; + protected int maxhp; + protected int maxmp; + protected int hp; + protected int mp; + protected int exp; + protected int nextlevel; + protected int offens; + protected int defens; + protected int speed; + protected int guts; + protected int vitality; + protected int iq; + protected int luck; + // TODO protected List psis; + + public Creature(GameMap gameMap, String name, Keyboard keyboard, Position position, Size size, String fileName, int tileWidth, int tileHeight) { + super(gameMap, keyboard, position, size, fileName, tileWidth, tileHeight); + this.name = name; + this.level = 1; + determiningStatus(); + } + + protected abstract void determiningStatus(); +} diff --git a/src/entity/creature/Direction.java b/src/entity/creature/Direction.java new file mode 100644 index 0000000..21ba861 --- /dev/null +++ b/src/entity/creature/Direction.java @@ -0,0 +1,9 @@ +package entity.creature; + +public enum Direction { + STOP, + UP, + DOWN, + LEFT, + RIGHT, +} diff --git a/src/entity/creature/Enemy.java b/src/entity/creature/Enemy.java new file mode 100644 index 0000000..525b6c1 --- /dev/null +++ b/src/entity/creature/Enemy.java @@ -0,0 +1,79 @@ +package entity.creature; + +import java.awt.Rectangle; + +import ai.AIManager; +import controller.EnemyController; +import core.Position; +import core.Size; +import display.DebugRenderer; +import entity.creature.Direction; +import game.GameLoop; +import gfx.Animation; +import gfx.SpriteSheet; +import input.Keyboard; +import map.GameMap; +import state.State; + +public class Enemy extends Creature { + private AIManager aiManager; + + public Enemy(GameMap gameMap, String name, Keyboard keyboard, Position position, Size size, String fileName, int tileWidth, int tileHeight) { + super(gameMap, name, keyboard, position, size, fileName, tileWidth, tileHeight); + aiManager = new AIManager(); + collisionBox = new Rectangle((int)position.getX() + 25, (int)position.getY() + 25, 4 * gameMap.getScale(), 8 * gameMap.getScale()); + controller = new EnemyController(); + } + + @Override + protected void determiningStatus() { + } + + public String getCurrentState() { + return aiManager.getCurrentAIState().getClass().getSimpleName(); + } + + @Override + protected void handleCollision(State state) { + GameMap map = state.getGameMap(); + collisionBox.x += vector.getX(); + collisionBox.y += vector.getY(); + vector.setVector(0, 0); + if (controller.isRequestKeyUp()) { + changeAnimation(Direction.UP); + vector.setVector(0, -walkSpeed); + } + if (controller.isRequestKeyDown()) { + changeAnimation(Direction.DOWN); + vector.setVector(0, walkSpeed); + } + if (controller.isRequestKeyLeft()) { + changeAnimation(Direction.LEFT); + vector.setVector(-walkSpeed, 0); + } + if (controller.isRequestKeyRight()) { + changeAnimation(Direction.RIGHT); + vector.setVector(walkSpeed, 0); + } + // TODO + // if (!map.isWalkable(this) || !isWalkable(state)) { + // vector.setVector(0, 0); + // } + position.add(vector); + } + + @Override + protected void registerAnimations(SpriteSheet spriteSheet) { + super.registerAnimations(spriteSheet); + animations.put(Direction.DOWN , new Animation(spriteSheet.getTextures(0, 4 ), (long) (0.3 * GameLoop.ups), true)); + animations.put(Direction.RIGHT, new Animation(spriteSheet.getTextures(4, 8 ), (long) (0.3 * GameLoop.ups), true)); + animations.put(Direction.LEFT , new Animation(spriteSheet.getTextures(8, 12), (long) (0.3 * GameLoop.ups), true)); + animations.put(Direction.UP , new Animation(spriteSheet.getTextures(12, 16), (long) (0.3 * GameLoop.ups), true)); + } + + @Override + public void update(State state) { + super.update(state); + aiManager.update(state, this); + } +} diff --git a/src/entity/creature/Player.java b/src/entity/creature/Player.java new file mode 100644 index 0000000..06e7a0e --- /dev/null +++ b/src/entity/creature/Player.java @@ -0,0 +1,68 @@ +package entity.creature; + +import java.awt.Rectangle; +import java.awt.event.KeyEvent; +import java.util.HashMap; +import java.util.Map; + +import core.Position; +import core.Size; +import core.Vector2D; +import game.GameLoop; +import gfx.Animation; +import gfx.SpriteSheet; +import input.Keyboard; +import map.GameMap; +import state.State; + +public class Player extends Creature { + public Player(GameMap gameMap, String name, Keyboard keyboard, Position position, Size size, String fileName, int tileWidth, int tileHeight) { + super(gameMap, name, keyboard, position, size, fileName, tileWidth, tileHeight); + collisionBox = new Rectangle((int)position.getX() + 25, (int)position.getY() + 25, 4 * gameMap.getScale(), 8 * gameMap.getScale()); + } + + @Override + protected void determiningStatus() { + maxhp = 30; + maxmp = 10; + hp = maxhp; + mp = maxmp; + // TODO nextlevel; + offens = 2; + defens = 2; + speed = 2; + guts = 2; + vitality = 2; + iq = 2; + luck = 2; + } + + private void handleInput() { + if (keyboard.isPressedKey(KeyEvent.VK_W)) { + direction = Direction.UP; + } else if (keyboard.isPressedKey(KeyEvent.VK_A)) { + direction = Direction.LEFT; + } else if (keyboard.isPressedKey(KeyEvent.VK_S)) { + direction = Direction.DOWN; + } else if (keyboard.isPressedKey(KeyEvent.VK_D)) { + direction = Direction.RIGHT; + } else { + direction = Direction.STOP; + } + } + + @Override + protected void registerAnimations(SpriteSheet spriteSheet) { + super.registerAnimations(spriteSheet); + animations.put(Direction.DOWN , new Animation(spriteSheet.getTextures(4, 7 ), (long) (0.3 * GameLoop.ups), true)); + animations.put(Direction.RIGHT, new Animation(spriteSheet.getTextures(48, 51), (long) (0.3 * GameLoop.ups), true)); + animations.put(Direction.LEFT , new Animation(spriteSheet.getTextures(63, 66), (long) (0.3 * GameLoop.ups), true)); + animations.put(Direction.UP , new Animation(spriteSheet.getTextures(19, 22), (long) (0.3 * GameLoop.ups), true)); + } + + @Override + public void update(State state) { + super.update(state); + handleInput(); + } +} diff --git a/src/game/Game.java b/src/game/Game.java new file mode 100644 index 0000000..032c3fc --- /dev/null +++ b/src/game/Game.java @@ -0,0 +1,57 @@ +package game; + +import java.awt.event.KeyEvent; + +import core.Size; +import display.Display; +import input.Keyboard; +import input.Mouse; +import state.StartState; +import state.State; + +public class Game { + private GameLoop gameLoop; + private Display display; + private Keyboard keyboard; + private Mouse mouse; + private State state; + + public Game(GameLoop gameLoop) { + this.gameLoop = gameLoop; + keyboard = new Keyboard(); + mouse = new Mouse(); + display = new Display(keyboard, mouse, "2D Game Engine Example", new Size(1280, 720)); + state = new StartState(display.getCanvasSize(), this); + } + + public Display getDisplay() { + return display; + } + + public Keyboard getKeyboard() { + return keyboard; + } + + public Mouse getMouse() { + return mouse; + } + + public void render() { + display.render(state); + } + + public void setState(State state) { + this.state = state; + } + + public void stop() { + gameLoop.stop(); + } + + public void update() { + if (keyboard.isJustPressedKey(KeyEvent.VK_ESCAPE)) { + stop(); + } + state.update(); + } +} diff --git a/src/game/GameLoop.java b/src/game/GameLoop.java new file mode 100644 index 0000000..db1ee0d --- /dev/null +++ b/src/game/GameLoop.java @@ -0,0 +1,67 @@ +package game; + +public class GameLoop implements Runnable { + public static int fpsCount = 0; + public static int upsCount = 0; + public static long loopCount = 0; + public static double fps = 30.0; + public static double ups = 30.0; + private boolean running; + private double targetUpsFrame = 1000000000.0 / ups; + private double targetFpsFrame = 1000000000.0 / fps; + private Game game; + + public GameLoop() { + running = true; + game = new Game(this); + } + + private void render() { + game.render(); + } + + @Override + public void run() { + int fpsCnt = 0; + double fpsDelta = 0; + long timer = 0; + long last = System.nanoTime(); + long loopCnt = 0; + int upsCnt = 0; + double upsDelta = 0; + while (running) { + long now = System.nanoTime(); + upsDelta += (now - last) / targetUpsFrame; + fpsDelta += (now - last) / targetFpsFrame; + timer += now - last; + last = now; + while (upsDelta >= 1) { + update(); + upsDelta--; + upsCnt++; + } + while (fpsDelta >= 1) { + render(); + fpsDelta--; + fpsCnt++; + } + loopCnt++; + if (timer >= 1000000000) { + fpsCount = fpsCnt; + upsCount = upsCnt; + loopCount = loopCnt; + timer = fpsCnt = upsCnt = 0; + loopCnt = 0; + } + } + System.exit(0); + } + + public void stop() { + running = false; + } + + private void update() { + game.update(); + } +} \ No newline at end of file diff --git a/src/gfx/Animation.java b/src/gfx/Animation.java new file mode 100644 index 0000000..1e231f8 --- /dev/null +++ b/src/gfx/Animation.java @@ -0,0 +1,53 @@ +package gfx; + +import java.awt.image.BufferedImage; +import java.util.List; + +public class Animation { + private BufferedImage currentImage; + private List images; + private long currentFrame; + private long nextFrame; + private int idx; + private boolean loop; + private boolean running; + + public Animation(List images, long nextFrame, boolean loop) { + this.images = images; + this.nextFrame = nextFrame; + this.loop = loop; + currentImage = images.get(0); + } + + public BufferedImage getCurrentImage() { + return currentImage; + } + + public void start() { + running = true; + } + + public void stop() { + running = false; + } + + public void update() { + if (running) { + currentFrame++; + } else { + if (currentFrame != 0 && idx != 0) { + currentFrame = 0; + idx = 0; + } + } + if (currentFrame == nextFrame) { + currentFrame = 0; + if (loop && idx == images.size() - 1) { + idx = 0; + } else { + idx++; + } + } + currentImage = images.get(idx); + } +} diff --git a/src/gfx/SpriteSheet.java b/src/gfx/SpriteSheet.java new file mode 100644 index 0000000..6c2e998 --- /dev/null +++ b/src/gfx/SpriteSheet.java @@ -0,0 +1,53 @@ +package gfx; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import util.ImageUtil; + +public class SpriteSheet { + private int tileWidth; + private int tileHeight; + private BufferedImage sheet; + private List sprites; + + public SpriteSheet(String fileName, int tileWidth, int tileHeight) { + this.tileWidth = tileWidth; + this.tileHeight = tileHeight; + sprites = new ArrayList<>(); + loadImage(fileName); + loadTextures(); + } + + private void loadImage(String fileName) { + try { + sheet = ImageUtil.loadImage(fileName); + } catch (IOException | IllegalArgumentException e) { + throw new IllegalStateException("スプライトシートの読み込みに失敗しました。", e); + } + } + + private void loadTextures() { + for (int y = 0; y < sheet.getHeight(null); y += tileHeight) { + for (int x = 0; x < sheet.getWidth(null); x += tileWidth) { + if (x + tileWidth <= sheet.getWidth() && y + tileHeight <= sheet.getHeight()) { + sprites.add(sheet.getSubimage(x, y, tileWidth, tileHeight)); + } + } + } + } + + public BufferedImage getSprite(int idx) { + return sprites.get(idx); + } + + public List getTextures(int start, int end) { + List textures = new ArrayList<>(); + for (int i = start; i < end; i++) { + textures.add(getSprite(i)); + } + return textures; + } +} diff --git a/src/input/Keyboard.java b/src/input/Keyboard.java new file mode 100644 index 0000000..801a580 --- /dev/null +++ b/src/input/Keyboard.java @@ -0,0 +1,63 @@ +package input; + +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; + +public class Keyboard implements KeyListener { + private boolean[] isKeyPressed; + private byte[] keyPressedTime; + + public Keyboard() { + isKeyPressed = new boolean[0xFFFF]; + keyPressedTime = new byte[0xFFFF]; + } + + @Override + public void keyTyped(KeyEvent e) { + } + + @Override + public void keyPressed(KeyEvent e) { + isKeyPressed[e.getKeyCode()] = true; + } + + @Override + public void keyReleased(KeyEvent e) { + isKeyPressed[e.getKeyCode()] = false; + } + + private void handleKeyPressTime(int keyCode) { + if (isKeyPressed[keyCode]) { + if (keyPressedTime[keyCode] < Byte.MAX_VALUE) { + keyPressedTime[keyCode]++; + } + } else { + keyPressedTime[keyCode] = 0; + } + } + + public boolean isJustPressedKey(int keyCode) { + return keyPressedTime[keyCode] == 1; + } + + public boolean isPressedKey(int keyCode) { + return keyPressedTime[keyCode] > 0; + } + + public boolean isPressedKey(int... keyCodes) { + for (int keyCode : keyCodes) { + if (keyPressedTime[keyCode] == 0) { + return false; + } + } + return true; + } + + public void update() { + handleKeyPressTime(KeyEvent.VK_A); + handleKeyPressTime(KeyEvent.VK_D); + handleKeyPressTime(KeyEvent.VK_S); + handleKeyPressTime(KeyEvent.VK_W); + handleKeyPressTime(KeyEvent.VK_ESCAPE); + } +} diff --git a/src/input/Mouse.java b/src/input/Mouse.java new file mode 100644 index 0000000..cf42bbf --- /dev/null +++ b/src/input/Mouse.java @@ -0,0 +1,78 @@ +package input; + +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; + +import core.Position; + +public class Mouse implements MouseListener, MouseMotionListener { + private Position mousePosition; + private boolean mouseClicked; + private boolean mouseReleased; + private boolean mousePressed; + + public Mouse() { + mousePosition = new Position(-1, -1); + } + + public void cleanUpInputEvents() { + mouseClicked = false; + mouseReleased = false; + mousePressed = false; + } + + public Position getMousePosition() { + return mousePosition; + } + + public boolean isMouseClicked() { + return mouseClicked; + } + + public boolean isMouseReleased() { + return mouseReleased; + } + + public boolean isMousePressed() { + return mousePressed; + } + + @Override + public void mouseClicked(MouseEvent e) { + } + + @Override + public void mouseDragged(MouseEvent e) { + mousePosition.setX(e.getPoint().getX()); + mousePosition.setY(e.getPoint().getY()); + } + + @Override + public void mouseEntered(MouseEvent e) { + } + + @Override + public void mouseExited(MouseEvent e) { + } + + @Override + public void mouseMoved(MouseEvent e) { + mousePosition.setX(e.getPoint().getX()); + mousePosition.setY(e.getPoint().getY()); + } + + @Override + public void mousePressed(MouseEvent e) { + mousePressed = e.getButton() == MouseEvent.BUTTON1; + } + + @Override + public void mouseReleased(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + mouseClicked = true; + mousePressed = false; + mouseReleased = true; + } + } +} diff --git a/src/input/MouseConsumer.java b/src/input/MouseConsumer.java new file mode 100644 index 0000000..9614ca4 --- /dev/null +++ b/src/input/MouseConsumer.java @@ -0,0 +1,9 @@ +package input; + +import state.State; + +public interface MouseConsumer { + void onClick(State state); + void onDrag(State state); + void onRelease(State state); +} diff --git a/src/map/GameMap.java b/src/map/GameMap.java new file mode 100644 index 0000000..af62c8d --- /dev/null +++ b/src/map/GameMap.java @@ -0,0 +1,213 @@ +package map; + +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import core.Position; +import core.Size; +import core.Vector2D; +import entity.Entity; +import util.ImageUtil; +import util.MapUtil; + +public class GameMap { + public static final int MAP_LAYER_1 = 0; + public static final int MAP_LAYER_2 = 1; + public static final int MAP_LAYER_3 = 2; + public static final int MAP_LAYER_4 = 3; + public static final int NUMBER_OF_MAP_LAYER = 4; + public static final int UNWALKABLE_TILE = 0; + public static final int WALKABLE_TILE = 1; + + private List mapData; + private List tileset; + private String tileSetFileName; + private Size stageSize; + private Size tileSize; + private int scale; + private Rectangle tileRect; + private Rectangle tileRect2; + private Random random; + + public GameMap() { + mapData = new ArrayList<>(); + tileset = new ArrayList<>(); + scale = 4; + tileRect = new Rectangle(-1, -1, 0, 0); + tileRect2 = new Rectangle(-1, -1, 0, 0); + random = new Random(); + buildGameMap(); + loadTileSet(); + } + + private void buildGameMap() { + try { + Map map = MapUtil.loadMapFromFileName("map1.tmx"); + tileSetFileName = map.get(MapUtil.MAP_KEY_SOURCE); + for (int i = 1; i <= NUMBER_OF_MAP_LAYER; i++) { + // TODO 今IDは使われていない + int id = Integer.parseInt(map.get(MapUtil.MAP_KEY_ID + i)); + int width = Integer.parseInt(map.get(MapUtil.MAP_KEY_WIDTH + i)); + int height = Integer.parseInt(map.get(MapUtil.MAP_KEY_HEIGHT + i)); + int tileWidth = Integer.parseInt(map.get(MapUtil.MAP_KEY_TILE_WIDTH)); + int tileHeight = Integer.parseInt(map.get(MapUtil.MAP_KEY_TILE_HEIGHT)); + stageSize = new Size(width, height); + tileSize = new Size(tileWidth, tileHeight); + String data = map.get(MapUtil.MAP_KEY_DATA + i); + int[][] arrData = new int[height][width]; + String[] splitData = data.replace(" ", "").replace("\n", "").split(","); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + arrData[y][x] = Integer.parseInt(splitData[y * width + x]); + } + } + mapData.add(arrData); + } + } catch (Exception e) { + throw new IllegalStateException("ゲームマップの構築に失敗しました。", e); + } + } + + public Position findRandomPosition() { + return new Position( + random.nextInt(stageSize.getWidth() * (tileSize.getWidth() * scale)), + random.nextInt(stageSize.getHeight() * (tileSize.getHeight() * scale))); + } + + public int getHeight() { + return stageSize.getHeight(); + } + + public List getMapData() { + return mapData; + } + + public int getMapDataCell(int x, int y, int layer) { + return mapData.get(layer)[y][x]; + } + + private Points getCollisionPoints(Entity entity) { + Vector2D vec = entity.getVector(); + Rectangle rect = entity.getCollisionBox(); + Points pos = new Points(-1); + switch (entity.getDirection()) { + case UP: + pos.p1.setX(rect.x); + pos.p1.setY(rect.y + vec.getY()); + pos.p2.setX(rect.x + rect.width); + pos.p2.setY(rect.y + vec.getY()); + break; + case DOWN: + pos.p1.setX(rect.x); + pos.p1.setY(rect.y + rect.height + vec.getY()); + pos.p2.setX(rect.x + rect.width); + pos.p2.setY(rect.y + rect.height + vec.getY()); + break; + case LEFT: + pos.p1.setX(rect.x + vec.getX()); + pos.p1.setY(rect.y); + pos.p2.setX(rect.x + vec.getX()); + pos.p2.setY(rect.y + rect.height); + break; + case RIGHT: + pos.p1.setX(rect.x + rect.width + vec.getX()); + pos.p1.setY(rect.y); + pos.p2.setX(rect.x + rect.width + vec.getX()); + pos.p2.setY(rect.y + rect.height); + break; + } + return pos; + } + + public Rectangle getTileRect() { + return tileRect; + } + + public Rectangle getTileRect2() { + return tileRect2; + } + + public List getTileSet() { + return tileset; + } + + public int getScale() { + return scale; + } + + public Size getStageSize() { + return stageSize; + } + + public Size getTileSize() { + return tileSize; + } + + public int getWidth() { + return stageSize.getWidth(); + } + + public boolean isOutBoundsOfMap(int x, int y) { + return x < 0 || x >= stageSize.getWidth() || y < 0 || y >= stageSize.getHeight(); + } + + public boolean isWalkable(Entity entity) { + Points pos = getCollisionPoints(entity); + double gridX1d = pos.p1.getX() / (tileSize.getWidth() * scale); + double gridY1d = pos.p1.getY() / (tileSize.getHeight() * scale); + double gridX2d = pos.p2.getX() / (tileSize.getWidth() * scale); + double gridY2d = pos.p2.getY() / (tileSize.getHeight() * scale); + int gridX1 = gridX1d < 0 ? -1 : (int)gridX1d; + int gridY1 = gridY1d < 0 ? -1 : (int)gridY1d; + int gridX2 = gridX2d < 0 ? -1 : (int)gridX2d; + int gridY2 = gridY2d < 0 ? -1 : (int)gridY2d; + if (isOutBoundsOfMap(gridX1, gridY1) && isOutBoundsOfMap(gridX2, gridY2)) { + return false; + } + tileRect = new Rectangle(gridX1 * (tileSize.getWidth() * scale), gridY1 * (tileSize.getHeight() * scale), tileSize.getWidth() * scale, tileSize.getHeight() * scale); + tileRect2 = new Rectangle(gridX2 * (tileSize.getWidth() * scale), gridY2 * (tileSize.getHeight() * scale), tileSize.getWidth() * scale, tileSize.getHeight() * scale); + if (mapData.get(MAP_LAYER_4)[gridY1][gridX1] == UNWALKABLE_TILE || + mapData.get(MAP_LAYER_4)[gridY2][gridX2] == UNWALKABLE_TILE) { + return false; + } + return true; + } + + private void loadTileSet() { + try { + BufferedImage image = ImageUtil.loadImage(MapUtil.TILE_SET_PATH + tileSetFileName); + for (int y = 0; y < image.getHeight(null); y += tileSize.getHeight()) { + for (int x = 0; x < image.getWidth(null); x += tileSize.getWidth()) { + tileset.add(image.getSubimage(x, y, tileSize.getWidth(), tileSize.getHeight())); + } + } + } catch (IOException e) { + throw new IllegalStateException("指定されたパスから正しくタイルセットが読み込めませんでした。 path: " + MapUtil.TILE_SET_PATH + tileSetFileName, e); + } + } + + private class Points { + public Position p1; + public Position p2; + + public Points() { + p1 = new Position(); + p2 = new Position(); + } + + public Points(int p) { + p1 = new Position(p, p); + p2 = new Position(p, p); + } + + public Points(int x1, int y1, int x2, int y2) { + p1 = new Position(x1, y1); + p2 = new Position(y1, y2); + } + } +} diff --git a/src/map/Tile.java b/src/map/Tile.java new file mode 100644 index 0000000..5a1f7d6 --- /dev/null +++ b/src/map/Tile.java @@ -0,0 +1,44 @@ +package map; + +import java.awt.image.BufferedImage; + +import gfx.Animation; + +public class Tile { + private Animation animation; + private int tileWidth; + private int tileHeight; + private boolean walkable; + + public Tile() { + walkable = true; + } + + private Tile(Animation animation, int tileWidth, int tileHeight, boolean walkable) { + this.animation = animation; + this.tileWidth = tileWidth; + this.tileHeight = tileHeight; + this.walkable = walkable; + } + + // FIXME 必要なければ削除 + public static Tile copyOf(Tile tile) { + return new Tile(tile.getSprite(), tile.getTileIndex(), tile.getTileName(), tile.isWalkable()); + } + + public BufferedImage getSprite() { + return sprite; + } + + public int getTileIndex() { + return tileIndex; + } + + public String getTileName() { + return tileName; + } + + public boolean isWalkable() { + return walkable; + } +} diff --git a/src/state/PlayState.java b/src/state/PlayState.java new file mode 100644 index 0000000..8dd2f49 --- /dev/null +++ b/src/state/PlayState.java @@ -0,0 +1,56 @@ +package state; + +import java.awt.Color; + +import core.Position; +import core.Size; +import display.Display; +import entity.Entity; +import entity.creature.Player; +import game.Game; +import game.GameLoop; +import ui.Alignment; +import ui.UIContainer; +import ui.VerticalContainer; +import ui.clickable.UIButton; +import ui.debug.UIDebugInfo; + +public class PlayState extends State { + private Player player; + + public PlayState(Size windowSize, Game game) { + super(windowSize, game); + player = new Player( + gameMap, + "Tester", + keyboard, + new Position(320, 192), + new Size(gameMap.getTileSize().getWidth(), gameMap.getTileSize().getHeight()), + "/character/Full_SpriteSheet.png", + 16, 32); + generateEntity(10); + gameCamera.focusOn(player); + } + + @Override + public void initializeUI(Size windowSize) { + UIContainer container = new VerticalContainer(windowSize); + container.setAlignment(Alignment.Position.END, Alignment.Position.START); + container.addUIComponent(new UIButton("デバッグ", () -> Display.debug = !Display.debug)); + container.addUIComponent(new UIButton("終了", () -> game.stop())); + uiContainers.add(container); + UIDebugInfo uiDebugInfo = new UIDebugInfo(windowSize); + uiDebugContainers.add(uiDebugInfo); + } + + public Player getPlayer() { + return player; + } + + @Override + public void update() { + super.update(); + player.update(this); + entities.forEach(entity -> entity.update(this)); + } +} diff --git a/src/state/StartState.java b/src/state/StartState.java new file mode 100644 index 0000000..4ef5070 --- /dev/null +++ b/src/state/StartState.java @@ -0,0 +1,17 @@ +package state; + +import core.Size; +import game.Game; +import ui.state.start.UIStartStateMenu; + +public class StartState extends State { + public StartState(Size windowSize, Game game) { + super(windowSize, game); + } + + @Override + public void initializeUI(Size windowSize) { + UIStartStateMenu menu = new UIStartStateMenu(windowSize, game); + uiContainers.add(menu); + } +} diff --git a/src/state/State.java b/src/state/State.java new file mode 100644 index 0000000..efba5d3 --- /dev/null +++ b/src/state/State.java @@ -0,0 +1,106 @@ +package state; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import core.Position; +import core.Size; +import display.Display; +import display.GameCamera; +import entity.Entity; +import entity.creature.Enemy; +import game.Game; +import input.Keyboard; +import input.Mouse; +import input.MouseConsumer; +import map.GameMap; +import ui.UIContainer; +import ui.debug.UIDebugInfo; + +public abstract class State { + protected Game game; + protected Keyboard keyboard; + protected Mouse mouse; + protected GameMap gameMap; + protected GameCamera gameCamera; + protected MouseConsumer activeConsumer; + protected Random random; + protected List entities; + protected List uiContainers; + protected List uiDebugContainers; + protected UIDebugInfo uiDebugInfo; + + public State(Size windowSize, Game game) { + this.game = game; + keyboard = game.getKeyboard(); + mouse = game.getMouse(); + gameMap = new GameMap(); + gameCamera = new GameCamera(game.getDisplay().getCanvasSize()); + random = new Random(); + entities = new ArrayList<>(); + uiContainers = new ArrayList<>(); + uiDebugContainers = new ArrayList<>(); + initializeUI(windowSize); + } + + protected void generateEntity(int num) { + for (int i = 0; i < num; i++) { + int randomX = random.nextInt(gameMap.getWidth()) * gameMap.getTileSize().getWidth() * gameMap.getScale(); + int randomY = random.nextInt(gameMap.getHeight()) * gameMap.getTileSize().getHeight() * gameMap.getScale(); + entities.add( + new Enemy( + gameMap, + "Enemy" + i, + keyboard, + new Position(randomX, randomY), + new Size(gameMap.getTileSize().getWidth(), gameMap.getTileSize().getHeight()), + "/character/Full_SpriteSheet.png", + 16, 16 + ) + ); + } + } + + public List getEntities() { + return entities; + } + + public GameCamera getGameCamera() { + return gameCamera; + } + + public GameMap getGameMap() { + return gameMap; + } + + public Mouse getMouse() { + return mouse; + } + + public List getUiContainers() { + return uiContainers; + } + + public List getUiDebugContainers() { + return uiDebugContainers; + } + + public abstract void initializeUI(Size windowSize); + + public void setActiveConsumer(MouseConsumer activeConsumer) { + this.activeConsumer = activeConsumer; + } + + public void update() { + keyboard.update(); + entities.forEach(entity -> entity.update(this)); + uiContainers.forEach(uiContainer -> uiContainer.update(this)); + if (Display.debug) { + uiDebugContainers.forEach(uiContainer -> uiContainer.update(this)); + } + gameCamera.update(this); + mouse.cleanUpInputEvents(); + } +} diff --git a/src/ui/Alignment.java b/src/ui/Alignment.java new file mode 100644 index 0000000..d2dc77c --- /dev/null +++ b/src/ui/Alignment.java @@ -0,0 +1,25 @@ +package ui; + +public class Alignment { + public enum Position { + START, + CENTER, + END + } + + private final Position horizontal; + private final Position vertical; + + public Alignment(Position horizontal, Position vertical) { + this.vertical = vertical; + this.horizontal = horizontal; + } + + public Position getHorizontal() { + return horizontal; + } + + public Position getVertical() { + return vertical; + } +} diff --git a/src/ui/HorizontalContainer.java b/src/ui/HorizontalContainer.java new file mode 100644 index 0000000..0928859 --- /dev/null +++ b/src/ui/HorizontalContainer.java @@ -0,0 +1,35 @@ +package ui; + +import core.Position; +import core.Size; + +public class HorizontalContainer extends UIContainer { + public HorizontalContainer(Size windowSize) { + super(windowSize); + } + + @Override + protected void calculateChildrenPosition() { + int currentX = padding.getLeft(); + for (UIComponent child : children) { + currentX += child.getMargin().getLeft(); + child.setRelativePosition(new Position(currentX, padding.getTop())); + child.setAbsolutePosition(new Position(currentX + (int)absolutePosition.getX(), padding.getTop() + (int)absolutePosition.getY())); + currentX += child.getSize().getWidth(); + currentX += child.getMargin().getRight(); + } + } + + @Override + protected Size calculateChildrenSize() { + int combinedChildW = 0; + int tallestChildH = 0; + for (UIComponent child : children) { + combinedChildW += child.getSize().getWidth() + child.getMargin().getHorizontal(); + if (child.getSize().getHeight() > tallestChildH) { + tallestChildH = child.getSize().getHeight(); + } + } + return new Size(combinedChildW, tallestChildH); + } +} diff --git a/src/ui/Space.java b/src/ui/Space.java new file mode 100644 index 0000000..dd4fada --- /dev/null +++ b/src/ui/Space.java @@ -0,0 +1,47 @@ +package ui; + +public class Space { + private int top; + private int bottom; + private int left; + private int right; + + public Space(int space) { + this(space, space); + } + + public Space(int horizontal, int vertical) { + this(vertical, vertical, horizontal, horizontal); + } + + public Space(int top, int bottom, int left, int right) { + this.top = top; + this.bottom = bottom; + this.left = left; + this.right = right; + } + + public int getBottom() { + return bottom; + } + + public int getHorizontal() { + return left + right; + } + + public int getLeft() { + return left; + } + + public int getRight() { + return right; + } + + public int getTop() { + return top; + } + + public int getVertical() { + return top + bottom; + } +} diff --git a/src/ui/UIComponent.java b/src/ui/UIComponent.java new file mode 100644 index 0000000..b3a8ed2 --- /dev/null +++ b/src/ui/UIComponent.java @@ -0,0 +1,67 @@ +package ui; + +import java.awt.image.BufferedImage; + +import core.Position; +import core.Size; +import state.State; + +public abstract class UIComponent { + protected Position relativePosition; + protected Position absolutePosition; + protected Size size; + protected Space margin; + protected Space padding; + + public UIComponent() { + relativePosition = new Position(0, 0); + absolutePosition = new Position(0, 0); + size = new Size(1, 1); + margin = new Space(0); + padding = new Space(0); + } + + public Space getMargin() { + return margin; + } + + public Space getPadding() { + return padding; + } + + public Position getAbsolutePosition() { + return absolutePosition; + } + + public Position getRelativePosition() { + return relativePosition; + } + + public Size getSize() { + return size; + } + + public abstract BufferedImage getSprite(); + + public void setMargin(Space margin) { + this.margin = margin; + } + + public void setPadding(Space padding) { + this.padding = padding; + } + + public void setAbsolutePosition(Position absolutePosition) { + this.absolutePosition = absolutePosition; + } + + public void setRelativePosition(Position relativePosition) { + this.relativePosition = relativePosition; + } + + public void setSize(Size size) { + this.size = size; + } + + public abstract void update(State state); +} diff --git a/src/ui/UIContainer.java b/src/ui/UIContainer.java new file mode 100644 index 0000000..3c04289 --- /dev/null +++ b/src/ui/UIContainer.java @@ -0,0 +1,124 @@ +package ui; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Transparency; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.List; + +import core.Position; +import core.Size; +import state.State; +import ui.Space; +import util.ImageUtil; + +public abstract class UIContainer extends UIComponent { + protected Color backgroundColor; + protected List children; + protected Size windowSize; + protected Alignment alignment; + protected Size fixedSize; + + public UIContainer(Size windowSize) { + super(); + this.windowSize = windowSize; + backgroundColor = new Color(20, 20, 20); + alignment = new Alignment(Alignment.Position.START, Alignment.Position.START); + margin = new Space(5); + padding = new Space(5); + children = new ArrayList<>(); + calculateSize(); + calculatePosition(); + } + + public void addUIComponent(UIComponent uiComponent) { + children.add(uiComponent); + } + + public void addUIComponentAll(UIComponent... uiComponents) { + for (UIComponent uiComponent : uiComponents) { + children.add(uiComponent); + } + } + + protected abstract Size calculateChildrenSize(); + + protected abstract void calculateChildrenPosition(); + + private void calculatePosition() { + int x = padding.getLeft(); + if (alignment.getHorizontal() == Alignment.Position.CENTER) { + x = windowSize.getWidth() / 2 - size.getWidth() / 2; + } + if (alignment.getHorizontal() == Alignment.Position.END) { + x = windowSize.getWidth() - size.getWidth() - margin.getRight(); + } + int y = padding.getTop(); + if (alignment.getVertical() == Alignment.Position.CENTER) { + y = windowSize.getHeight() / 2 - size.getHeight() / 2; + } + if (alignment.getVertical() == Alignment.Position.END) { + y = windowSize.getHeight() - size.getHeight() - margin.getBottom(); + } + relativePosition = new Position(x, y); + absolutePosition = new Position(x, y); + calculateChildrenPosition(); + } + + private void calculateSize() { + Size calculateChildrenSize = calculateChildrenSize(); + if (fixedSize != null) { + size = fixedSize; + } else { + size = new Size( + padding.getHorizontal() + calculateChildrenSize.getWidth(), + padding.getVertical() + calculateChildrenSize.getHeight()); + } + } + + protected void clearChildren() { + children.clear(); + } + + public Color getBackgroundColor() { + return backgroundColor; + } + + @Override + public BufferedImage getSprite() { + BufferedImage image = ImageUtil.createCompatibleImage(size, Transparency.BITMASK); + Graphics2D g = image.createGraphics(); + g.setColor(backgroundColor); + g.fillRect(0, 0, size.getWidth(), size.getHeight()); + for (UIComponent child : children) { + g.drawImage( + child.getSprite(), + (int)child.getRelativePosition().getX(), + (int)child.getRelativePosition().getY(), + null + ); + } + g.dispose(); + return image; + } + + public void setAlignment(Alignment.Position vertical, Alignment.Position horizontal) { + alignment = new Alignment(vertical, horizontal); + } + + public void setBackgroundColor(Color backgroundColor) { + this.backgroundColor = backgroundColor; + } + + public void setFixedSize(Size fixedSize) { + this.fixedSize = fixedSize; + } + + @Override + public void update(State state) { + children.forEach(component -> component.update(state)); + calculateSize(); + calculatePosition(); + } +} diff --git a/src/ui/UIText.java b/src/ui/UIText.java new file mode 100644 index 0000000..24ae325 --- /dev/null +++ b/src/ui/UIText.java @@ -0,0 +1,73 @@ +package ui; + +import java.awt.Canvas; +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.Transparency; +import java.awt.image.BufferedImage; + +import core.Size; +import state.State; +import util.ImageUtil; + +public class UIText extends UIComponent { + private String text; + private int fontSize; + private int fontStyle; + private String fontFamily; + private Color color; + private boolean dropShadow; + private int dropShadowOffset; + private Color shadowColor; + private Font font; + + public UIText(String text) { + this.text = text; + fontSize = 24; + fontStyle = Font.BOLD; + fontFamily = Font.MONOSPACED; + color = Color.WHITE; + dropShadow = true; + dropShadowOffset = 2; + shadowColor = new Color(140, 140, 140); + } + + private void createFont() { + font = new Font(fontFamily, fontStyle, fontSize); + } + + private void calculateSize() { + FontMetrics fontMetrics = new Canvas().getFontMetrics(font); + size = new Size( + fontMetrics.stringWidth(text) + padding.getHorizontal(), + fontMetrics.getHeight() + padding.getVertical() + ); + } + + @Override + public BufferedImage getSprite() { + BufferedImage image = (BufferedImage)ImageUtil.createCompatibleImage(size, Transparency.BITMASK); + Graphics2D g = image.createGraphics(); + g.setFont(font); + if (dropShadow) { + g.setColor(shadowColor); + g.drawString(text, padding.getLeft() + dropShadowOffset, fontSize + padding.getTop() + dropShadowOffset); + } + g.setColor(color); + g.drawString(text, padding.getLeft(), fontSize + padding.getTop()); + g.dispose(); + return image; + } + + @Override + public void update(State state) { + createFont(); + calculateSize(); + } + + public void setText(String text) { + this.text = text; + } +} diff --git a/src/ui/VerticalContainer.java b/src/ui/VerticalContainer.java new file mode 100644 index 0000000..d3bb008 --- /dev/null +++ b/src/ui/VerticalContainer.java @@ -0,0 +1,35 @@ +package ui; + +import core.Position; +import core.Size; + +public class VerticalContainer extends UIContainer { + public VerticalContainer(Size windowSize) { + super(windowSize); + } + + @Override + protected void calculateChildrenPosition() { + int currentY = padding.getTop(); + for (UIComponent child : children) { + currentY += child.getMargin().getTop(); + child.setRelativePosition(new Position(padding.getLeft(), currentY)); + child.setAbsolutePosition(new Position(padding.getLeft() + (int)absolutePosition.getX(), currentY + (int)absolutePosition.getY())); + currentY += child.getSize().getHeight(); + currentY += child.getMargin().getBottom(); + } + } + + @Override + protected Size calculateChildrenSize() { + int combinedChildH = 0; + int widestChildW = 0; + for (UIComponent child : children) { + combinedChildH += child.getSize().getHeight() + child.getMargin().getVertical(); + if (child.getSize().getWidth() > widestChildW) { + widestChildW = child.getSize().getWidth(); + } + } + return new Size(widestChildW, combinedChildH); + } +} diff --git a/src/ui/clickable/UIButton.java b/src/ui/clickable/UIButton.java new file mode 100644 index 0000000..4a823f0 --- /dev/null +++ b/src/ui/clickable/UIButton.java @@ -0,0 +1,49 @@ +package ui.clickable; + +import java.awt.Color; +import java.awt.image.BufferedImage; + +import core.Size; +import ui.UIContainer; +import ui.UIText; +import ui.VerticalContainer; +import state.State; + +public class UIButton extends UIClickable { + private UIContainer container; + private UIText label; + private Runnable clickEvent; + + public UIButton(String label, Runnable clickEvent) { + this.label = new UIText(label); + this.clickEvent = clickEvent; + container = new VerticalContainer(new Size(0, 0)); + container.addUIComponent(this.label); + container.setFixedSize(new Size(150, 30)); + } + + @Override + public BufferedImage getSprite() { + return container.getSprite(); + } + + @Override + public void onClick() { + clickEvent.run(); + } + + @Override + public void update(State state) { + super.update(state); + container.update(state); + size = container.getSize(); + Color color = Color.GRAY; + if (hasFocus) { + color = Color.LIGHT_GRAY; + } + if (isPressed) { + color = Color.DARK_GRAY; + } + container.setBackgroundColor(color); + } +} diff --git a/src/ui/clickable/UIClickable.java b/src/ui/clickable/UIClickable.java new file mode 100644 index 0000000..7780bc4 --- /dev/null +++ b/src/ui/clickable/UIClickable.java @@ -0,0 +1,33 @@ +package ui.clickable; + +import java.awt.Rectangle; + +import core.Position; +import state.State; +import ui.UIComponent; + +public abstract class UIClickable extends UIComponent { + protected boolean hasFocus; + protected boolean isPressed; + + @Override + public void update(State state) { + Position mousePosition = state.getMouse().getMousePosition(); + hasFocus = getBounds().contains((int)mousePosition.getX(), (int)mousePosition.getY()); + isPressed = hasFocus && state.getMouse().isMousePressed(); + if (hasFocus && state.getMouse().isMouseClicked()) { + onClick(); + } + } + + private Rectangle getBounds() { + return new Rectangle( + (int)absolutePosition.getX(), + (int)absolutePosition.getY(), + size.getWidth(), + size.getHeight() + ); + } + + protected abstract void onClick(); +} diff --git a/src/ui/debug/UIDebugInfo.java b/src/ui/debug/UIDebugInfo.java new file mode 100644 index 0000000..e914426 --- /dev/null +++ b/src/ui/debug/UIDebugInfo.java @@ -0,0 +1,48 @@ +package ui.debug; + +import java.awt.Color; + +import core.Size; +import entity.creature.Player; +import game.GameLoop; +import map.GameMap; +import state.PlayState; +import state.State; +import ui.VerticalContainer; +import ui.UIText; + +public class UIDebugInfo extends VerticalContainer { + private UIText ram = new UIText("RAM"); + private UIText fps = new UIText("FPS"); + private UIText ups = new UIText("UPS"); + private UIText loop = new UIText("LOOP"); + private UIText pos = new UIText("POS"); + private UIText grid = new UIText("GRID"); + private UIText dir = new UIText("DIR"); + + public UIDebugInfo(Size windowSize) { + super(windowSize); + setBackgroundColor(new Color(0, 0, 0, 0)); + addUIComponentAll(ram, fps, ups, loop, pos, grid, dir); + } + + public void update(State state) { + super.update(state); + ram.setText(String.format("RAM: %.3fMB", + (((double)(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1000) / 1000))); + fps.setText(String.format("FPS: %d", GameLoop.fpsCount)); + ups.setText(String.format("UPS: %d", GameLoop.upsCount)); + loop.setText(String.format("1S : %d", GameLoop.loopCount)); + if (state instanceof PlayState pState) { + Player p = pState.getPlayer(); + GameMap gm = state.getGameMap(); + Size tileSize = gm.getTileSize(); + int scale = gm.getScale(); + pos.setText(String.format("%s", "Player: " + p.getPosition())); + int gridX = ((int) p.getCollisionBox().getX() / (tileSize.getWidth() * scale)); + int gridY = ((int) p.getCollisionBox().getY() / (tileSize.getHeight() * scale)); + grid.setText(String.format("%s", "(X: " + gridX + " Y: " + gridY + ")")); + dir.setText(String.format("%s", p.getDirection())); + } + } +} diff --git a/src/ui/state/start/UIStartStateMenu.java b/src/ui/state/start/UIStartStateMenu.java new file mode 100644 index 0000000..eaab6a4 --- /dev/null +++ b/src/ui/state/start/UIStartStateMenu.java @@ -0,0 +1,28 @@ +package ui.state.start; + +import core.Size; +import game.Game; +import state.PlayState; +import ui.Alignment; +import ui.UIText; +import ui.VerticalContainer; +import ui.clickable.UIButton; + +public class UIStartStateMenu extends VerticalContainer { + private Game game; + private UIText title; + private UIButton start; + private UIButton setting; + private UIButton exit; + + public UIStartStateMenu(Size windowSize, Game game) { + super(windowSize); + this.game = game; + setAlignment(Alignment.Position.CENTER, Alignment.Position.CENTER); + title = new UIText("2D Game Engine Example"); + start = new UIButton("開始", () -> game.setState(new PlayState(game.getDisplay().getCanvasSize(), game))); + setting = new UIButton("設定", () -> {}); + exit = new UIButton("終了", () -> game.stop()); + addUIComponentAll(title, start, setting, exit); + } +} diff --git a/src/util/ImageUtil.java b/src/util/ImageUtil.java new file mode 100644 index 0000000..da2f363 --- /dev/null +++ b/src/util/ImageUtil.java @@ -0,0 +1,36 @@ +package util; + +import java.awt.Graphics2D; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsEnvironment; +import java.awt.Image; +import java.awt.Transparency; +import java.awt.image.BufferedImage; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import core.Size; + +public class ImageUtil { + public static BufferedImage createCompatibleImage(Size size, int transparency) { + GraphicsConfiguration config = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); + return config.createCompatibleImage(size.getWidth(), size.getHeight(), transparency); + } + + public static BufferedImage loadImage(String path) throws IOException, IllegalArgumentException { + try { + Image imageFromDisk = ImageIO.read(ImageUtil.class.getResource(path)); + BufferedImage compatibleImage = createCompatibleImage( + new Size(imageFromDisk.getWidth(null), imageFromDisk.getHeight(null)), Transparency.BITMASK); + Graphics2D graphics = compatibleImage.createGraphics(); + graphics.drawImage(imageFromDisk, 0, 0, null); + graphics.dispose(); + return compatibleImage; + } catch (IOException e) { + throw new IOException("指定されたパスからイメージを読み込む事が出来ませんでした。 path: " + path, e); + } catch (IllegalArgumentException e) { + throw new IOException("指定されたパスが存在しませんでした。 path: " + path, e); + } + } +} diff --git a/src/util/MapUtil.java b/src/util/MapUtil.java new file mode 100644 index 0000000..ce28d30 --- /dev/null +++ b/src/util/MapUtil.java @@ -0,0 +1,98 @@ +package util; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +public class MapUtil { + public static final int NUMBER_OF_TILE_SIZE = 2; + public static final String MAP_KEY_DATA = "data"; + public static final String MAP_KEY_HEIGHT = "height"; + public static final String MAP_KEY_ID = "id"; + public static final String MAP_KEY_WIDTH = "width"; + public static final String MAP_KEY_SOURCE = "source"; + public static final String MAP_KEY_TILE_HEIGHT = "tileheight"; + public static final String MAP_KEY_TILE_WIDTH = "tilewidth"; + public static final String TILE_SET_PATH = "/tileset/"; + + private static void loadMapInfo(Map mapData, String fileName) + throws ParserConfigurationException, SAXException, IOException { + DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document document = builder.parse(MapUtil.class.getResourceAsStream(TILE_SET_PATH + fileName)); + document.getDocumentElement().normalize(); + Element elementList = document.getDocumentElement(); + NodeList nodeList = elementList.getElementsByTagName("tileset"); + Element element = (Element) nodeList.item(0); + mapData.put(MAP_KEY_SOURCE, element.getAttribute(MAP_KEY_SOURCE).replace("tsx", "png")); + nodeList = elementList.getElementsByTagName("layer"); + for (int i = 0; i < nodeList.getLength(); i++) { + element = (Element) nodeList.item(i); + int id = Integer.parseInt(element.getAttribute(MAP_KEY_ID)); + mapData.put(MAP_KEY_ID + id, element.getAttribute(MAP_KEY_ID)); + mapData.put(MAP_KEY_WIDTH + id, element.getAttribute(MAP_KEY_WIDTH)); + mapData.put(MAP_KEY_HEIGHT + id, element.getAttribute(MAP_KEY_HEIGHT)); + mapData.put(MAP_KEY_DATA + id, element.getTextContent()); + } + } + + // FIXME TileSizeの値を取得する方法を考える必要がある。 + private static void loadTileSizeInfo(Map mapData, String fileName) throws IOException, URISyntaxException { + try (BufferedReader br = new BufferedReader( + new FileReader(new File(MapUtil.class.getResource(TILE_SET_PATH + fileName).toURI())))) { + String line = ""; + int idx = 0; + while ((line = br.readLine()) != null) { + idx++; + if (idx == NUMBER_OF_TILE_SIZE) { + String beginStr = "tilewidth="; + String endStr = "tileheight="; + int beginIndex = line.indexOf(beginStr); + int endIndex = line.indexOf(endStr); + String tileWidth = line.substring(beginIndex, endIndex).replace(beginStr, "").replace("\"", "").replace(" ", ""); + beginStr = "tileheight="; + endStr = "infinite="; + beginIndex = line.indexOf(beginStr); + endIndex = line.indexOf(endStr); + String tileHeight = line.substring(beginIndex, endIndex).replace(beginStr, "").replace("\"", "").replace(" ", ""); + mapData.put(MAP_KEY_TILE_WIDTH, tileWidth); + mapData.put(MAP_KEY_TILE_HEIGHT, tileHeight); + } + } + } catch (IOException e) { + throw new IOException("タイルサイズの読み込みに失敗しました。 fileName: " + fileName, e); + } catch (URISyntaxException e) { + System.err.println("不正なURIが設定されたため例外が発生しました。 fileName: " + fileName); + throw e; + } + } + + public static Map loadMapFromFileName(String fileName) + throws ParserConfigurationException, SAXException, IOException, URISyntaxException { + try { + Map mapData = new HashMap<>(); + loadMapInfo(mapData, fileName); + loadTileSizeInfo(mapData, fileName); + return mapData; + } catch (ParserConfigurationException e) { + System.err.println("XMLの構成に致命的なミスがあります。 fileName: " + fileName); + throw e; + } catch (SAXException e) { + throw new SAXException("SAXでの処理で例外が発生しました。fileName: " + fileName, e); + } catch (IOException e) { + throw new IOException("入出力処理で例外が発生しました。 fileName: " + fileName, e); + } + } +}