diff --git a/src/main/java/nl/andrewl/starship_arena/StarshipArena.java b/src/main/java/nl/andrewl/starship_arena/StarshipArena.java index 08b94a6..2aead94 100644 --- a/src/main/java/nl/andrewl/starship_arena/StarshipArena.java +++ b/src/main/java/nl/andrewl/starship_arena/StarshipArena.java @@ -2,8 +2,6 @@ package nl.andrewl.starship_arena; import nl.andrewl.starship_arena.model.Arena; import nl.andrewl.starship_arena.model.Ship; -import nl.andrewl.starship_arena.model.ShipModel; -import nl.andrewl.starship_arena.util.ResourceUtils; import nl.andrewl.starship_arena.view.ArenaWindow; /** @@ -11,10 +9,17 @@ import nl.andrewl.starship_arena.view.ArenaWindow; */ public class StarshipArena { public static void main(String[] args) { - ShipModel corvette = ShipModel.load(ResourceUtils.getString("/ships/corvette.json")); - Ship s = new Ship(corvette); + Ship s1 = new Ship("/ships/corvette.json"); + s1.setVelocity(0, -0.5f); + s1.setRotationSpeed(0.5f); Arena arena = new Arena(); - arena.getShips().add(s); + arena.getShips().add(s1); + Ship s2 = new Ship("/ships/corvette.json"); + s2.setRotation((float) (Math.PI / 6)); + s2.getPosition().x = 3; + s2.getPosition().y = -5; + arena.getShips().add(s2); + arena.getCamera().setFocus(s1); var window = new ArenaWindow(arena); window.setVisible(true); } diff --git a/src/main/java/nl/andrewl/starship_arena/control/CameraController.java b/src/main/java/nl/andrewl/starship_arena/control/CameraController.java new file mode 100644 index 0000000..0e43d7f --- /dev/null +++ b/src/main/java/nl/andrewl/starship_arena/control/CameraController.java @@ -0,0 +1,50 @@ +package nl.andrewl.starship_arena.control; + +import nl.andrewl.starship_arena.model.Camera; +import nl.andrewl.starship_arena.model.PhysicsObject; + +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; + +public class CameraController implements MouseWheelListener, KeyListener { + private final Camera camera; + + public CameraController(Camera camera) { + this.camera = camera; + } + + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + if (e.getWheelRotation() > 0) { + camera.zoomOut(); + } else { + camera.zoomIn(); + } + } + + @Override + public void keyTyped(KeyEvent e) {} + + @Override + public void keyPressed(KeyEvent e) { + // Reset view. + if (e.getKeyCode() == KeyEvent.VK_SPACE && e.isControlDown()) { + System.out.println("Resetting view"); + camera.setRotation(0); + camera.setPosition(0, 0); + camera.resetScale(); + } + if (e.getKeyCode() == KeyEvent.VK_ESCAPE && camera.getFocus() != null) { + System.out.println("Leaving focus!"); + PhysicsObject f = camera.getFocus(); + camera.setRotation(0); + camera.setPosition(f.getPosition().x, f.getPosition().y); + camera.setFocus(null); + } + } + + @Override + public void keyReleased(KeyEvent e) {} +} diff --git a/src/main/java/nl/andrewl/starship_arena/control/GameUpdater.java b/src/main/java/nl/andrewl/starship_arena/control/GameUpdater.java new file mode 100644 index 0000000..2097762 --- /dev/null +++ b/src/main/java/nl/andrewl/starship_arena/control/GameUpdater.java @@ -0,0 +1,65 @@ +package nl.andrewl.starship_arena.control; + +import nl.andrewl.starship_arena.model.Arena; +import nl.andrewl.starship_arena.view.ArenaPanel; + +import javax.swing.*; + +public class GameUpdater extends Thread { + public static final double PHYSICS_FPS = 60.0; + public static final double MILLISECONDS_PER_PHYSICS_TICK = 1000.0 / PHYSICS_FPS; + public static final double PHYSICS_SPEED = 1.0; + + public static final double DISPLAY_FPS = 60.0; + public static final double MILLISECONDS_PER_DISPLAY_FRAME = 1000.0 / DISPLAY_FPS; + + private final Arena arena; + private final ArenaPanel arenaPanel; + private volatile boolean running = true; + + public GameUpdater(Arena arena, ArenaPanel arenaPanel) { + this.arena = arena; + this.arenaPanel = arenaPanel; + } + + public void setRunning(boolean running) { + this.running = running; + } + + @Override + public void run() { + long lastPhysicsUpdate = System.currentTimeMillis(); + long lastDisplayUpdate = System.currentTimeMillis(); + while (running) { + long currentTime = System.currentTimeMillis(); + long timeSinceLastPhysicsUpdate = currentTime - lastPhysicsUpdate; + long timeSinceLastDisplayUpdate = currentTime - lastDisplayUpdate; + if (timeSinceLastPhysicsUpdate >= MILLISECONDS_PER_PHYSICS_TICK) { + double elapsedSeconds = timeSinceLastPhysicsUpdate / 1000.0; + updateArena(elapsedSeconds * PHYSICS_SPEED); + lastPhysicsUpdate = currentTime; + timeSinceLastPhysicsUpdate = 0L; + } + if (timeSinceLastDisplayUpdate >= MILLISECONDS_PER_DISPLAY_FRAME) { + SwingUtilities.invokeLater(arenaPanel::repaint); + lastDisplayUpdate = currentTime; + timeSinceLastDisplayUpdate = 0L; + } + long timeUntilNextPhysicsUpdate = (long) (MILLISECONDS_PER_PHYSICS_TICK - timeSinceLastPhysicsUpdate); + long timeUntilNextDisplayUpdate = (long) (MILLISECONDS_PER_DISPLAY_FRAME - timeSinceLastDisplayUpdate); + + // Sleep to reduce CPU usage. + try { + Thread.sleep(Math.min(timeUntilNextPhysicsUpdate, timeUntilNextDisplayUpdate)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + private void updateArena(double t) { + for (var s : arena.getShips()) { + s.update(t); + } + } +} diff --git a/src/main/java/nl/andrewl/starship_arena/model/Camera.java b/src/main/java/nl/andrewl/starship_arena/model/Camera.java index 827c3c4..37274b0 100644 --- a/src/main/java/nl/andrewl/starship_arena/model/Camera.java +++ b/src/main/java/nl/andrewl/starship_arena/model/Camera.java @@ -3,19 +3,19 @@ package nl.andrewl.starship_arena.model; import java.awt.geom.Point2D; public class Camera { - public static final double SCALE_INTERVAL = 50.0; + private static final float[] SCALE_FACTORS = {500, 200, 100, 50, 25, 10, 5, 1, 0.5f, 0.25f, 0.1f, 0.05f, 0.01f}; + public static final byte DEFAULT_SCALE_FACTOR_INDEX = 5; - private Object focus; + private PhysicsObject focus; + private final Point2D.Float position = new Point2D.Float(); + private float rotation; + private byte scaleIndex = DEFAULT_SCALE_FACTOR_INDEX; - private Point2D.Float position = new Point2D.Float(); - - private int scaleIncrement = 1; - - public Object getFocus() { + public PhysicsObject getFocus() { return focus; } - public void setFocus(Object focus) { + public void setFocus(PhysicsObject focus) { this.focus = focus; } @@ -23,15 +23,32 @@ public class Camera { return position; } - public void setPosition(Point2D.Float position) { - this.position = position; + public void setPosition(float x, float y) { + this.position.x = x; + this.position.y = y; } - public int getScaleIncrement() { - return scaleIncrement; + public float getRotation() { + return rotation; } - public void setScaleIncrement(int scaleIncrement) { - this.scaleIncrement = scaleIncrement; + public void setRotation(float rotation) { + this.rotation = rotation; + } + + public float getScaleFactor() { + return SCALE_FACTORS[scaleIndex]; + } + + public void zoomOut() { + if (scaleIndex < SCALE_FACTORS.length - 1) scaleIndex++; + } + + public void zoomIn() { + if (scaleIndex > 0) scaleIndex--; + } + + public void resetScale() { + scaleIndex = DEFAULT_SCALE_FACTOR_INDEX; } } diff --git a/src/main/java/nl/andrewl/starship_arena/model/PhysicsObject.java b/src/main/java/nl/andrewl/starship_arena/model/PhysicsObject.java index fa4b73d..45bca0b 100644 --- a/src/main/java/nl/andrewl/starship_arena/model/PhysicsObject.java +++ b/src/main/java/nl/andrewl/starship_arena/model/PhysicsObject.java @@ -2,7 +2,7 @@ package nl.andrewl.starship_arena.model; import java.awt.geom.Point2D; -public class PhysicsObject { +public abstract class PhysicsObject { /** * The position of this object in the scene, in meters from the origin. * Positive x-axis goes to the right, and positive y-axis goes down. @@ -28,6 +28,11 @@ public class PhysicsObject { return position; } + public void setPosition(float x, float y) { + this.position.x = x; + this.position.y = y; + } + public float getRotation() { return rotation; } @@ -42,6 +47,11 @@ public class PhysicsObject { return velocity; } + public void setVelocity(float x, float y) { + this.velocity.x = x; + this.velocity.y = y; + } + public float getRotationSpeed() { return rotationSpeed; } diff --git a/src/main/java/nl/andrewl/starship_arena/model/Ship.java b/src/main/java/nl/andrewl/starship_arena/model/Ship.java index 347190e..128a6c5 100644 --- a/src/main/java/nl/andrewl/starship_arena/model/Ship.java +++ b/src/main/java/nl/andrewl/starship_arena/model/Ship.java @@ -1,5 +1,6 @@ package nl.andrewl.starship_arena.model; +import nl.andrewl.starship_arena.model.ship.Cockpit; import nl.andrewl.starship_arena.model.ship.Gun; import nl.andrewl.starship_arena.model.ship.Panel; import nl.andrewl.starship_arena.model.ship.ShipComponent; @@ -15,18 +16,21 @@ public class Ship extends PhysicsObject { private final Collection panels; private final Collection guns; + private final Collection cockpits; private Color primaryColor = Color.GRAY; - public Ship(ShipModel model) { + private Ship(ShipModel model) { this.modelName = model.getName(); this.components = model.getComponents(); this.panels = new ArrayList<>(); this.guns = new ArrayList<>(); + this.cockpits = new ArrayList<>(); for (var c : components) { c.setShip(this); if (c instanceof Panel p) panels.add(p); if (c instanceof Gun g) guns.add(g); + if (c instanceof Cockpit cp) cockpits.add(cp); } } @@ -42,6 +46,18 @@ public class Ship extends PhysicsObject { return components; } + public Collection getPanels() { + return panels; + } + + public Collection getGuns() { + return guns; + } + + public Collection getCockpits() { + return cockpits; + } + public Color getPrimaryColor() { return primaryColor; } diff --git a/src/main/java/nl/andrewl/starship_arena/model/ShipModel.java b/src/main/java/nl/andrewl/starship_arena/model/ShipModel.java index 39db86f..36cccd5 100644 --- a/src/main/java/nl/andrewl/starship_arena/model/ShipModel.java +++ b/src/main/java/nl/andrewl/starship_arena/model/ShipModel.java @@ -2,10 +2,7 @@ package nl.andrewl.starship_arena.model; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import nl.andrewl.starship_arena.model.ship.ComponentDeserializer; -import nl.andrewl.starship_arena.model.ship.GeometricComponent; -import nl.andrewl.starship_arena.model.ship.Gun; -import nl.andrewl.starship_arena.model.ship.ShipComponent; +import nl.andrewl.starship_arena.model.ship.*; import java.util.Collection; @@ -62,6 +59,7 @@ public class ShipModel { public static ShipModel load(String json) { Gson gson = new GsonBuilder() .registerTypeAdapter(ShipComponent.class, new ComponentDeserializer()) + .registerTypeAdapter(Gun.class, new GunDeserializer()) .create(); ShipModel model = gson.fromJson(json, ShipModel.class); model.normalizeComponents(); diff --git a/src/main/java/nl/andrewl/starship_arena/model/ship/Gun.java b/src/main/java/nl/andrewl/starship_arena/model/ship/Gun.java index 49231f2..a079b6b 100644 --- a/src/main/java/nl/andrewl/starship_arena/model/ship/Gun.java +++ b/src/main/java/nl/andrewl/starship_arena/model/ship/Gun.java @@ -5,6 +5,23 @@ import java.awt.geom.Point2D; public class Gun extends ShipComponent { private String name; private Point2D.Float location; + private float rotation; + private float maxRotation; + private float minRotation; + private float barrelWidth; + private float barrelLength; + + public Gun() {} + + public Gun(String name, Point2D.Float location, float rotation, float maxRotation, float minRotation, float barrelWidth, float barrelLength) { + this.name = name; + this.location = location; + this.rotation = rotation; + this.maxRotation = maxRotation; + this.minRotation = minRotation; + this.barrelWidth = barrelWidth; + this.barrelLength = barrelLength; + } public String getName() { return name; @@ -13,4 +30,24 @@ public class Gun extends ShipComponent { public Point2D.Float getLocation() { return location; } + + public float getRotation() { + return rotation; + } + + public float getMaxRotation() { + return maxRotation; + } + + public float getMinRotation() { + return minRotation; + } + + public float getBarrelWidth() { + return barrelWidth; + } + + public float getBarrelLength() { + return barrelLength; + } } diff --git a/src/main/java/nl/andrewl/starship_arena/model/ship/GunDeserializer.java b/src/main/java/nl/andrewl/starship_arena/model/ship/GunDeserializer.java new file mode 100644 index 0000000..d292d17 --- /dev/null +++ b/src/main/java/nl/andrewl/starship_arena/model/ship/GunDeserializer.java @@ -0,0 +1,22 @@ +package nl.andrewl.starship_arena.model.ship; + +import com.google.gson.*; + +import java.awt.geom.Point2D; +import java.lang.reflect.Type; + +public class GunDeserializer implements JsonDeserializer { + @Override + public Gun deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext ctx) throws JsonParseException { + JsonObject obj = jsonElement.getAsJsonObject(); + return new Gun( + obj.get("name").getAsString(), + ctx.deserialize(obj.get("location"), Point2D.Float.class), + (float) Math.toRadians(obj.get("rotation").getAsFloat()), + (float) Math.toRadians(obj.get("maxRotation").getAsDouble()), + (float) Math.toRadians(obj.get("minRotation").getAsFloat()), + obj.get("barrelWidth").getAsFloat(), + obj.get("barrelLength").getAsFloat() + ); + } +} diff --git a/src/main/java/nl/andrewl/starship_arena/view/ArenaPanel.java b/src/main/java/nl/andrewl/starship_arena/view/ArenaPanel.java index a151877..f1d5e0b 100644 --- a/src/main/java/nl/andrewl/starship_arena/view/ArenaPanel.java +++ b/src/main/java/nl/andrewl/starship_arena/view/ArenaPanel.java @@ -1,7 +1,9 @@ package nl.andrewl.starship_arena.view; +import nl.andrewl.starship_arena.control.CameraController; import nl.andrewl.starship_arena.model.Arena; import nl.andrewl.starship_arena.model.Camera; +import nl.andrewl.starship_arena.model.PhysicsObject; import javax.swing.*; import java.awt.*; @@ -9,15 +11,10 @@ import java.awt.geom.AffineTransform; public class ArenaPanel extends JPanel { private final Arena arena; - private final ShipRenderer shipRenderer = new ShipRenderer(); public ArenaPanel(Arena arena) { this.arena = arena; - this.addMouseWheelListener(e -> { - arena.getCamera().setScaleIncrement(arena.getCamera().getScaleIncrement() + e.getWheelRotation()); - repaint(); - }); } @Override @@ -28,37 +25,44 @@ public class ArenaPanel extends JPanel { g2.fillRect(0, 0, getWidth(), getHeight()); AffineTransform originalTx = g2.getTransform(); - AffineTransform tx = new AffineTransform(); - - Camera cam = arena.getCamera(); - - double translateX = (double) getWidth() / 2; - double translateY = (double) getHeight() / 2; - if (cam.getFocus() == null) { - translateX += cam.getPosition().x; - translateY += cam.getPosition().y; - } - - double scale = 1 * Camera.SCALE_INTERVAL; - if (cam.getScaleIncrement() > 0) { - scale = cam.getScaleIncrement() * Camera.SCALE_INTERVAL; - } else if (cam.getScaleIncrement() < 0) { - scale = 1.0 / Math.abs(cam.getScaleIncrement() * Camera.SCALE_INTERVAL); - } - - tx.translate(translateX, translateY); - tx.scale(scale, scale); - g2.setTransform(tx); - + AffineTransform camTx = getCameraTransform(); for (var s : arena.getShips()) { + AffineTransform shipTx = new AffineTransform(camTx); + shipTx.translate(s.getPosition().x, s.getPosition().y); + shipTx.rotate(s.getRotation()); + g2.setTransform(shipTx); shipRenderer.render(s, g2); } - g2.setTransform(originalTx); + // Testing indicators. g2.setColor(Color.GREEN); g2.fillRect(0, 0, 20, 20); g2.setColor(Color.BLUE); g2.fillRect(getWidth() - 20, getHeight() - 20, 20, 20); + g2.setColor(Color.MAGENTA); + g2.fillOval(getWidth() / 2 - 5, getHeight() / 2 - 5, 10, 10); + } + + private AffineTransform getCameraTransform() { + AffineTransform tx = new AffineTransform(); + Camera cam = arena.getCamera(); + // Start by translating such that 0, 0 is in the center of the screen instead of top-left. + tx.translate((double) getWidth() / 2, (double) getHeight() / 2); + tx.scale(cam.getScaleFactor(), cam.getScaleFactor()); + + double rotation = -cam.getRotation(); + double x = -cam.getPosition().x; + double y = -cam.getPosition().y; + if (cam.getFocus() != null) { + PhysicsObject f = cam.getFocus(); + rotation -= f.getRotation(); + x -= f.getPosition().x; + y -= f.getPosition().y; + } + + tx.rotate(rotation); + tx.translate(x, y); + return tx; } } diff --git a/src/main/java/nl/andrewl/starship_arena/view/ArenaWindow.java b/src/main/java/nl/andrewl/starship_arena/view/ArenaWindow.java index ca3e92b..138f3b9 100644 --- a/src/main/java/nl/andrewl/starship_arena/view/ArenaWindow.java +++ b/src/main/java/nl/andrewl/starship_arena/view/ArenaWindow.java @@ -1,5 +1,7 @@ package nl.andrewl.starship_arena.view; +import nl.andrewl.starship_arena.control.CameraController; +import nl.andrewl.starship_arena.control.GameUpdater; import nl.andrewl.starship_arena.model.Arena; import nl.andrewl.starship_arena.util.ResourceUtils; @@ -11,6 +13,7 @@ import java.io.InputStream; public class ArenaWindow extends JFrame { private final ArenaPanel arenaPanel; + private final GameUpdater updater; public ArenaWindow(Arena arena) { super("Starship Arena"); @@ -33,7 +36,13 @@ public class ArenaWindow extends JFrame { } arenaPanel = new ArenaPanel(arena); - add(arenaPanel); + setContentPane(arenaPanel); pack(); + + var camCtl = new CameraController(arena.getCamera()); + addKeyListener(camCtl); + addMouseWheelListener(camCtl); + updater = new GameUpdater(arena, arenaPanel); + updater.start(); } } diff --git a/src/main/java/nl/andrewl/starship_arena/view/ShipRenderer.java b/src/main/java/nl/andrewl/starship_arena/view/ShipRenderer.java index d17b085..53fb5a3 100644 --- a/src/main/java/nl/andrewl/starship_arena/view/ShipRenderer.java +++ b/src/main/java/nl/andrewl/starship_arena/view/ShipRenderer.java @@ -3,29 +3,49 @@ package nl.andrewl.starship_arena.view; import nl.andrewl.starship_arena.model.Ship; import nl.andrewl.starship_arena.model.ship.Cockpit; import nl.andrewl.starship_arena.model.ship.GeometricComponent; +import nl.andrewl.starship_arena.model.ship.Gun; import java.awt.*; +import java.awt.geom.AffineTransform; import java.awt.geom.Path2D; public class ShipRenderer implements Renderer { @Override public void render(Ship ship, Graphics2D g) { - for (var c : ship.getComponents()) { - if (c instanceof GeometricComponent geo) { - Path2D.Float path = new Path2D.Float(); - var first = geo.getPoints().get(0); - path.moveTo(first.x, first.y); - for (int i = 0; i < geo.getPoints().size(); i++) { - var point = geo.getPoints().get(i); - path.lineTo(point.x, point.y); - } - if (geo instanceof Cockpit) { - g.setColor(new Color(0f, 0f, 0.5f, 0.5f)); - } else { - g.setColor(ship.getPrimaryColor()); - } - g.fill(path); - } + for (var p : ship.getPanels()) renderGeometricComponent(p, g); + for (var c : ship.getCockpits()) renderGeometricComponent(c, g); + for (var gun : ship.getGuns()) renderGun(gun, g); + } + + private void renderGeometricComponent(GeometricComponent geo, Graphics2D g) { + Path2D.Float path = new Path2D.Float(); + var first = geo.getPoints().get(0); + path.moveTo(first.x, first.y); + for (int i = 0; i < geo.getPoints().size(); i++) { + var point = geo.getPoints().get(i); + path.lineTo(point.x, point.y); } + if (geo instanceof Cockpit) { + g.setColor(new Color(0f, 0f, 0.5f, 0.5f)); + } else { + g.setColor(geo.getShip().getPrimaryColor()); + } + g.fill(path); + } + + private void renderGun(Gun gun, Graphics2D g) { + AffineTransform originalTx = g.getTransform(); + AffineTransform tx = new AffineTransform(originalTx); + tx.rotate(gun.getRotation() + Math.PI, gun.getLocation().x, gun.getLocation().y); + tx.translate(gun.getLocation().x, gun.getLocation().y); + g.setTransform(tx); + Path2D.Float path = new Path2D.Float(); + path.moveTo(-gun.getBarrelWidth() / 2, 0); + path.lineTo(gun.getBarrelWidth() / 2, 0); + path.lineTo(gun.getBarrelWidth() / 2, gun.getBarrelLength()); + path.lineTo(-gun.getBarrelWidth() / 2, gun.getBarrelLength()); + g.setColor(Color.DARK_GRAY); + g.fill(path); + g.setTransform(originalTx); } } diff --git a/src/main/resources/ships/corvette.json b/src/main/resources/ships/corvette.json index f7270ab..bc181c8 100644 --- a/src/main/resources/ships/corvette.json +++ b/src/main/resources/ships/corvette.json @@ -40,13 +40,23 @@ "type": "gun", "name": "Port-Side Machine Gun", "mass": 500, - "location": {"x": 0.15, "y": 0.4} + "location": {"x": 0.15, "y": 0.35}, + "rotation": 0, + "minRotation": -160, + "maxRotation": 5, + "barrelWidth": 0.02, + "barrelLength": 0.2 }, { "type": "gun", "name": "Starboard-Side Machine Gun", "mass": 500, - "location": {"x": 0.85, "y": 0.4} + "location": {"x": 0.85, "y": 0.35}, + "rotation": 0, + "minRotation": -5, + "maxRotation": 160, + "barrelWidth": 0.02, + "barrelLength": 0.2 } ] } \ No newline at end of file