diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b4a5277 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea/ +target/ +*.iml diff --git a/design/icon.svg b/design/icon.svg new file mode 100644 index 0000000..a1f0762 --- /dev/null +++ b/design/icon.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..1af812c --- /dev/null +++ b/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + nl.andrewl + starship-arena + 1.0.0-SNAPSHOT + + + 17 + 17 + UTF-8 + + + + + + com.google.code.gson + gson + 2.9.0 + + + + + + + maven-assembly-plugin + + + + nl.andrewl.starship_arena.StarshipArena + + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + + \ No newline at end of file diff --git a/src/main/java/nl/andrewl/starship_arena/StarshipArena.java b/src/main/java/nl/andrewl/starship_arena/StarshipArena.java new file mode 100644 index 0000000..08b94a6 --- /dev/null +++ b/src/main/java/nl/andrewl/starship_arena/StarshipArena.java @@ -0,0 +1,21 @@ +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; + +/** + * The main executable class which starts the program. + */ +public class StarshipArena { + public static void main(String[] args) { + ShipModel corvette = ShipModel.load(ResourceUtils.getString("/ships/corvette.json")); + Ship s = new Ship(corvette); + Arena arena = new Arena(); + arena.getShips().add(s); + var window = new ArenaWindow(arena); + window.setVisible(true); + } +} diff --git a/src/main/java/nl/andrewl/starship_arena/model/Arena.java b/src/main/java/nl/andrewl/starship_arena/model/Arena.java new file mode 100644 index 0000000..2dd240e --- /dev/null +++ b/src/main/java/nl/andrewl/starship_arena/model/Arena.java @@ -0,0 +1,21 @@ +package nl.andrewl.starship_arena.model; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * Represents the top-level model containing all objects in an arena that can + * interact with each other. + */ +public class Arena { + private final Collection ships = new ArrayList<>(); + private final Camera camera = new Camera(); + + public Collection getShips() { + return ships; + } + + public Camera getCamera() { + return camera; + } +} diff --git a/src/main/java/nl/andrewl/starship_arena/model/Camera.java b/src/main/java/nl/andrewl/starship_arena/model/Camera.java new file mode 100644 index 0000000..827c3c4 --- /dev/null +++ b/src/main/java/nl/andrewl/starship_arena/model/Camera.java @@ -0,0 +1,37 @@ +package nl.andrewl.starship_arena.model; + +import java.awt.geom.Point2D; + +public class Camera { + public static final double SCALE_INTERVAL = 50.0; + + private Object focus; + + private Point2D.Float position = new Point2D.Float(); + + private int scaleIncrement = 1; + + public Object getFocus() { + return focus; + } + + public void setFocus(Object focus) { + this.focus = focus; + } + + public Point2D.Float getPosition() { + return position; + } + + public void setPosition(Point2D.Float position) { + this.position = position; + } + + public int getScaleIncrement() { + return scaleIncrement; + } + + public void setScaleIncrement(int scaleIncrement) { + this.scaleIncrement = scaleIncrement; + } +} diff --git a/src/main/java/nl/andrewl/starship_arena/model/PhysicsObject.java b/src/main/java/nl/andrewl/starship_arena/model/PhysicsObject.java new file mode 100644 index 0000000..fa4b73d --- /dev/null +++ b/src/main/java/nl/andrewl/starship_arena/model/PhysicsObject.java @@ -0,0 +1,58 @@ +package nl.andrewl.starship_arena.model; + +import java.awt.geom.Point2D; + +public 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. + */ + private final Point2D.Float position = new Point2D.Float(); + + /** + * The object's rotation in radians, from 0 to 2 PI. + */ + private float rotation; + + /** + * The object's velocity, in meters per second. + */ + private final Point2D.Float velocity = new Point2D.Float(); + + /** + * The object's rotational speed, in radians per second. + */ + private float rotationSpeed; + + public Point2D.Float getPosition() { + return position; + } + + public float getRotation() { + return rotation; + } + + public void setRotation(float rotation) { + while (rotation < 0) rotation += 2 * Math.PI; + while (rotation > 2 * Math.PI) rotation -= 2 * Math.PI; + this.rotation = rotation; + } + + public Point2D.Float getVelocity() { + return velocity; + } + + public float getRotationSpeed() { + return rotationSpeed; + } + + public void setRotationSpeed(float rotationSpeed) { + this.rotationSpeed = rotationSpeed; + } + + public void update(double delta) { + position.x += velocity.x * delta; + position.y += velocity.y * delta; + setRotation((float) (rotation + rotationSpeed * delta)); + } +} diff --git a/src/main/java/nl/andrewl/starship_arena/model/Ship.java b/src/main/java/nl/andrewl/starship_arena/model/Ship.java new file mode 100644 index 0000000..347190e --- /dev/null +++ b/src/main/java/nl/andrewl/starship_arena/model/Ship.java @@ -0,0 +1,56 @@ +package nl.andrewl.starship_arena.model; + +import nl.andrewl.starship_arena.model.ship.Gun; +import nl.andrewl.starship_arena.model.ship.Panel; +import nl.andrewl.starship_arena.model.ship.ShipComponent; +import nl.andrewl.starship_arena.util.ResourceUtils; + +import java.awt.*; +import java.util.ArrayList; +import java.util.Collection; + +public class Ship extends PhysicsObject { + private final String modelName; + private final Collection components; + + private final Collection panels; + private final Collection guns; + + private Color primaryColor = Color.GRAY; + + public Ship(ShipModel model) { + this.modelName = model.getName(); + this.components = model.getComponents(); + this.panels = new ArrayList<>(); + this.guns = 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); + } + } + + public Ship(String modelResource) { + this(ShipModel.load(ResourceUtils.getString(modelResource))); + } + + public String getModelName() { + return modelName; + } + + public Collection getComponents() { + return components; + } + + public Color getPrimaryColor() { + return primaryColor; + } + + public float getMass() { + float m = 0; + for (var c : components) { + m += c.getMass(); + } + return m; + } +} diff --git a/src/main/java/nl/andrewl/starship_arena/model/ShipModel.java b/src/main/java/nl/andrewl/starship_arena/model/ShipModel.java new file mode 100644 index 0000000..39db86f --- /dev/null +++ b/src/main/java/nl/andrewl/starship_arena/model/ShipModel.java @@ -0,0 +1,70 @@ +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 java.util.Collection; + +public class ShipModel { + private String name; + private Collection components; + + public String getName() { + return name; + } + + public Collection getComponents() { + return components; + } + + /** + * Normalizes the geometric properties of the components such that the ship + * model's components are centered around (0, 0). + * TODO: Consider scaling? + */ + private void normalizeComponents() { + float minX = Float.MAX_VALUE; + float maxX = Float.MIN_VALUE; + float minY = Float.MAX_VALUE; + float maxY = Float.MIN_VALUE; + for (var c : components) { + if (c instanceof GeometricComponent g) { + for (var p : g.getPoints()) { + minX = Math.min(minX, p.x); + maxX = Math.max(maxX, p.x); + minY = Math.min(minY, p.y); + maxY = Math.max(maxY, p.y); + } + } + } + final float width = maxX - minX; + final float height = maxY - minY; + final float offsetX = -minX - width / 2; + final float offsetY = -minY - height / 2; + // Shift all components to the top-left. + for (var c : components) { + if (c instanceof GeometricComponent g) { + for (var p : g.getPoints()) { + p.x += offsetX; + p.y += offsetY; + } + } else if (c instanceof Gun g) { + g.getLocation().x += offsetX; + g.getLocation().y += offsetY; + } + } + } + + public static ShipModel load(String json) { + Gson gson = new GsonBuilder() + .registerTypeAdapter(ShipComponent.class, new ComponentDeserializer()) + .create(); + ShipModel model = gson.fromJson(json, ShipModel.class); + model.normalizeComponents(); + return model; + } +} diff --git a/src/main/java/nl/andrewl/starship_arena/model/ship/Cockpit.java b/src/main/java/nl/andrewl/starship_arena/model/ship/Cockpit.java new file mode 100644 index 0000000..716d20e --- /dev/null +++ b/src/main/java/nl/andrewl/starship_arena/model/ship/Cockpit.java @@ -0,0 +1,7 @@ +package nl.andrewl.starship_arena.model.ship; + +/** + * A cockpit represents the control point of the ship. + */ +public class Cockpit extends GeometricComponent { +} diff --git a/src/main/java/nl/andrewl/starship_arena/model/ship/ComponentDeserializer.java b/src/main/java/nl/andrewl/starship_arena/model/ship/ComponentDeserializer.java new file mode 100644 index 0000000..8b30dd6 --- /dev/null +++ b/src/main/java/nl/andrewl/starship_arena/model/ship/ComponentDeserializer.java @@ -0,0 +1,24 @@ +package nl.andrewl.starship_arena.model.ship; + +import com.google.gson.*; + +import java.lang.reflect.Type; + +/** + * Custom deserializer that's used to deserialize components based on their + * "type" property. + */ +public class ComponentDeserializer implements JsonDeserializer { + @Override + public ShipComponent deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext ctx) throws JsonParseException { + JsonObject obj = jsonElement.getAsJsonObject(); + String componentTypeName = obj.get("type").getAsString(); + Type componentType = switch (componentTypeName) { + case "panel" -> Panel.class; + case "cockpit" -> Cockpit.class; + case "gun" -> Gun.class; + default -> throw new JsonParseException("Invalid ship component type: " + componentTypeName); + }; + return ctx.deserialize(obj, componentType); + } +} diff --git a/src/main/java/nl/andrewl/starship_arena/model/ship/GeometricComponent.java b/src/main/java/nl/andrewl/starship_arena/model/ship/GeometricComponent.java new file mode 100644 index 0000000..20eefc7 --- /dev/null +++ b/src/main/java/nl/andrewl/starship_arena/model/ship/GeometricComponent.java @@ -0,0 +1,15 @@ +package nl.andrewl.starship_arena.model.ship; + +import java.awt.geom.Point2D; +import java.util.List; + +/** + * Represents a component of a ship that can be drawn as a geometric primitive. + */ +public abstract class GeometricComponent extends ShipComponent { + private List points; + + public List getPoints() { + return points; + } +} 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 new file mode 100644 index 0000000..49231f2 --- /dev/null +++ b/src/main/java/nl/andrewl/starship_arena/model/ship/Gun.java @@ -0,0 +1,16 @@ +package nl.andrewl.starship_arena.model.ship; + +import java.awt.geom.Point2D; + +public class Gun extends ShipComponent { + private String name; + private Point2D.Float location; + + public String getName() { + return name; + } + + public Point2D.Float getLocation() { + return location; + } +} diff --git a/src/main/java/nl/andrewl/starship_arena/model/ship/Panel.java b/src/main/java/nl/andrewl/starship_arena/model/ship/Panel.java new file mode 100644 index 0000000..d360e00 --- /dev/null +++ b/src/main/java/nl/andrewl/starship_arena/model/ship/Panel.java @@ -0,0 +1,8 @@ +package nl.andrewl.starship_arena.model.ship; + +/** + * A simple structural panel that makes up all or part of a ship's body. + */ +public class Panel extends GeometricComponent { + private String name; +} diff --git a/src/main/java/nl/andrewl/starship_arena/model/ship/ShipComponent.java b/src/main/java/nl/andrewl/starship_arena/model/ship/ShipComponent.java new file mode 100644 index 0000000..852161f --- /dev/null +++ b/src/main/java/nl/andrewl/starship_arena/model/ship/ShipComponent.java @@ -0,0 +1,27 @@ +package nl.andrewl.starship_arena.model.ship; + +import nl.andrewl.starship_arena.model.Ship; + +/** + * Represents the top-level component information for any part of a ship. + */ +public class ShipComponent { + /** + * The ship that this component belongs to. + */ + private transient Ship ship; + + private float mass; + + public Ship getShip() { + return ship; + } + + public void setShip(Ship ship) { + this.ship = ship; + } + + public float getMass() { + return mass; + } +} diff --git a/src/main/java/nl/andrewl/starship_arena/util/ResourceUtils.java b/src/main/java/nl/andrewl/starship_arena/util/ResourceUtils.java new file mode 100644 index 0000000..6d30fb0 --- /dev/null +++ b/src/main/java/nl/andrewl/starship_arena/util/ResourceUtils.java @@ -0,0 +1,21 @@ +package nl.andrewl.starship_arena.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; + +public class ResourceUtils { + public static InputStream get(String name) { + InputStream in = ResourceUtils.class.getResourceAsStream(name); + if (in == null) throw new UncheckedIOException(new IOException("Could not load resource: " + name)); + return in; + } + + public static String getString(String name) { + try (var in = get(name)) { + return new String(in.readAllBytes()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/src/main/java/nl/andrewl/starship_arena/view/ArenaPanel.java b/src/main/java/nl/andrewl/starship_arena/view/ArenaPanel.java new file mode 100644 index 0000000..a151877 --- /dev/null +++ b/src/main/java/nl/andrewl/starship_arena/view/ArenaPanel.java @@ -0,0 +1,64 @@ +package nl.andrewl.starship_arena.view; + +import nl.andrewl.starship_arena.model.Arena; +import nl.andrewl.starship_arena.model.Camera; + +import javax.swing.*; +import java.awt.*; +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 + protected void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g; + g2.setColor(Color.BLACK); + 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); + + for (var s : arena.getShips()) { + shipRenderer.render(s, g2); + } + + g2.setTransform(originalTx); + + g2.setColor(Color.GREEN); + g2.fillRect(0, 0, 20, 20); + g2.setColor(Color.BLUE); + g2.fillRect(getWidth() - 20, getHeight() - 20, 20, 20); + } +} diff --git a/src/main/java/nl/andrewl/starship_arena/view/ArenaWindow.java b/src/main/java/nl/andrewl/starship_arena/view/ArenaWindow.java new file mode 100644 index 0000000..ca3e92b --- /dev/null +++ b/src/main/java/nl/andrewl/starship_arena/view/ArenaWindow.java @@ -0,0 +1,39 @@ +package nl.andrewl.starship_arena.view; + +import nl.andrewl.starship_arena.model.Arena; +import nl.andrewl.starship_arena.util.ResourceUtils; + +import javax.imageio.ImageIO; +import javax.swing.*; +import java.awt.*; +import java.io.IOException; +import java.io.InputStream; + +public class ArenaWindow extends JFrame { + private final ArenaPanel arenaPanel; + + public ArenaWindow(Arena arena) { + super("Starship Arena"); + setDefaultCloseOperation(EXIT_ON_CLOSE); + setUndecorated(true); + try { + GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(); + device.setFullScreenWindow(this); + setPreferredSize(new Dimension(device.getDisplayMode().getWidth(), device.getDisplayMode().getHeight())); + } catch (HeadlessException e) { + System.err.println("Cannot start the program on systems without a screen."); + System.exit(1); + } + try { + InputStream in = ResourceUtils.get("/img/icon.png"); + setIconImage(ImageIO.read(in)); + in.close(); + } catch (IOException e) { + e.printStackTrace(); + } + + arenaPanel = new ArenaPanel(arena); + add(arenaPanel); + pack(); + } +} diff --git a/src/main/java/nl/andrewl/starship_arena/view/Renderer.java b/src/main/java/nl/andrewl/starship_arena/view/Renderer.java new file mode 100644 index 0000000..1f30f88 --- /dev/null +++ b/src/main/java/nl/andrewl/starship_arena/view/Renderer.java @@ -0,0 +1,7 @@ +package nl.andrewl.starship_arena.view; + +import java.awt.*; + +public interface Renderer { + void render(T obj, Graphics2D g); +} diff --git a/src/main/java/nl/andrewl/starship_arena/view/ShipRenderer.java b/src/main/java/nl/andrewl/starship_arena/view/ShipRenderer.java new file mode 100644 index 0000000..d17b085 --- /dev/null +++ b/src/main/java/nl/andrewl/starship_arena/view/ShipRenderer.java @@ -0,0 +1,31 @@ +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 java.awt.*; +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); + } + } + } +} diff --git a/src/main/resources/img/icon.png b/src/main/resources/img/icon.png new file mode 100644 index 0000000..b21f0b7 Binary files /dev/null and b/src/main/resources/img/icon.png differ diff --git a/src/main/resources/ships/corvette.json b/src/main/resources/ships/corvette.json new file mode 100644 index 0000000..f7270ab --- /dev/null +++ b/src/main/resources/ships/corvette.json @@ -0,0 +1,52 @@ +{ + "name": "Corvette", + "components": [ + { + "type": "panel", + "name": "Main Fuselage", + "mass": 5000, + "points": [ + {"x": 0.3, "y": 0.6}, + {"x": 0.2, "y": 0.1}, + {"x": 0.1, "y": 0.5}, + {"x": 0.2, "y": 0.8}, + {"x": 0.8, "y": 0.8}, + {"x": 0.9, "y": 0.5}, + {"x": 0.8, "y": 0.1}, + {"x": 0.7, "y": 0.6} + ] + }, + { + "type": "panel", + "name": "Front Cargo Bay", + "mass": 1000, + "points": [ + {"x": 0.4, "y": 0.2}, + {"x": 0.35, "y": 0.6}, + {"x": 0.65, "y": 0.6}, + {"x": 0.6, "y": 0.2} + ] + }, + { + "type": "cockpit", + "mass": 800, + "points": [ + {"x": 0.5, "y": 0.0}, + {"x": 0.4, "y": 0.2}, + {"x": 0.6, "y": 0.2} + ] + }, + { + "type": "gun", + "name": "Port-Side Machine Gun", + "mass": 500, + "location": {"x": 0.15, "y": 0.4} + }, + { + "type": "gun", + "name": "Starboard-Side Machine Gun", + "mass": 500, + "location": {"x": 0.85, "y": 0.4} + } + ] +} \ No newline at end of file