Added better camera transforms and whatnot.

This commit is contained in:
Andrew Lalis 2022-04-01 11:23:43 +02:00
parent 300a62e621
commit 27f61f67bc
13 changed files with 335 additions and 72 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Panel> panels;
private final Collection<Gun> guns;
private final Collection<Cockpit> 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<Panel> getPanels() {
return panels;
}
public Collection<Gun> getGuns() {
return guns;
}
public Collection<Cockpit> getCockpits() {
return cockpits;
}
public Color getPrimaryColor() {
return primaryColor;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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