Added starter components.

This commit is contained in:
Andrew Lalis 2022-03-31 22:08:51 +02:00
parent 4873a308ee
commit 300a62e621
22 changed files with 740 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.idea/
target/
*.iml

111
design/icon.svg Normal file
View File

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="256"
height="256"
viewBox="0 0 67.733332 67.733335"
version="1.1"
id="svg5"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
sodipodi:docname="icon.svg"
inkscape:export-filename="A:\Programming\GitHub-andrewlalis\StarshipArena\design\icon.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:document-units="px"
showgrid="false"
units="px"
showguides="true"
inkscape:guide-bbox="true"
inkscape:snap-page="true"
inkscape:zoom="2"
inkscape:cx="61.5"
inkscape:cy="141.75"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<sodipodi:guide
position="33.866667,54.715007"
orientation="1,0"
id="guide824" />
<sodipodi:guide
position="2.5765869,33.866667"
orientation="0,-1"
id="guide826" />
<sodipodi:guide
position="2.778125,2.5016983e-07"
orientation="0,-1"
id="guide828" />
</sodipodi:namedview>
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient2822">
<stop
style="stop-color:#00023d;stop-opacity:1;"
offset="0"
id="stop2818" />
<stop
style="stop-color:#8158ac;stop-opacity:1"
offset="1"
id="stop2820" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2822"
id="linearGradient3304"
gradientUnits="userSpaceOnUse"
x1="33.866665"
y1="44.908512"
x2="33.866665"
y2="0.92604166"
gradientTransform="matrix(1.0004146,0,0,0.92621082,-0.01400114,2.308488)" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:#ce0900;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 37.120775,63.000637 C 37.208969,42.098554 21.012326,39.151829 11.972396,42.569364 l 3.373437,2.645833 c 9.37066,-5.335764 20.407928,7.113912 21.774942,17.78544 z"
id="path4745"
sodipodi:nodetypes="cccc" />
<path
style="fill:#05e898;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 30.775166,63.000637 C 30.686972,42.098554 46.883615,39.151829 55.923545,42.569364 l -3.373437,2.645833 c -9.37066,-5.335764 -20.407928,7.113912 -21.774942,17.78544 z"
id="path8544"
sodipodi:nodetypes="cccc" />
<path
style="fill:url(#linearGradient3304);fill-opacity:1;stroke:#ffb800;stroke-width:1.01874;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 12.530819,65.043834 33.866706,3.1661977 55.008802,65.043833 33.866706,40.170249 Z"
id="path863"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#ce0900;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 11.972396,42.569364 C -2.2722264,48.32423 20.397365,66.043094 41.63193,42.657014 62.866491,19.270926 45.943565,5.5747177 45.943565,5.5747177 51.46268,17.143022 53.619707,20.974556 38.278435,40.494141 25.055936,55.054458 6.5096703,50.64227 15.345833,45.215197 Z"
id="path6747"
sodipodi:nodetypes="cscccc" />
<path
style="fill:#05e898;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 55.923545,42.569364 C 70.168167,48.32423 47.498576,66.043094 26.264011,42.657014 5.0294498,19.270926 21.952376,5.5747177 21.952376,5.5747177 16.433261,17.143022 14.276234,20.974556 29.617506,40.494141 42.840005,55.054458 61.38627,50.64227 52.550108,45.215197 Z"
id="path13334"
sodipodi:nodetypes="cscccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

52
pom.xml Normal file
View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>nl.andrewl</groupId>
<artifactId>starship-arena</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.9.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>nl.andrewl.starship_arena.StarshipArena</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id> <!-- this is used for inheritance merges -->
<phase>package</phase> <!-- bind to the packaging phase -->
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -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);
}
}

View File

@ -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<Ship> ships = new ArrayList<>();
private final Camera camera = new Camera();
public Collection<Ship> getShips() {
return ships;
}
public Camera getCamera() {
return camera;
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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<ShipComponent> components;
private final Collection<Panel> panels;
private final Collection<Gun> 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<ShipComponent> getComponents() {
return components;
}
public Color getPrimaryColor() {
return primaryColor;
}
public float getMass() {
float m = 0;
for (var c : components) {
m += c.getMass();
}
return m;
}
}

View File

@ -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<ShipComponent> components;
public String getName() {
return name;
}
public Collection<ShipComponent> 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;
}
}

View File

@ -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 {
}

View File

@ -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<ShipComponent> {
@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);
}
}

View File

@ -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<Point2D.Float> points;
public List<Point2D.Float> getPoints() {
return points;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,7 @@
package nl.andrewl.starship_arena.view;
import java.awt.*;
public interface Renderer<T> {
void render(T obj, Graphics2D g);
}

View File

@ -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<Ship> {
@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);
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -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}
}
]
}