Added everything.
This commit is contained in:
parent
ac49b54192
commit
0cabc4a550
|
@ -0,0 +1,5 @@
|
||||||
|
.idea/
|
||||||
|
client/target/
|
||||||
|
core/target/
|
||||||
|
server/target/
|
||||||
|
/*.iml
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?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">
|
||||||
|
<parent>
|
||||||
|
<artifactId>ace-of-shades</artifactId>
|
||||||
|
<groupId>nl.andrewlalis</groupId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>aos-client</artifactId>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>16</maven.compiler.source>
|
||||||
|
<maven.compiler.target>16</maven.compiler.target>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-assembly-plugin</artifactId>
|
||||||
|
<version>3.3.0</version>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifest>
|
||||||
|
<mainClass>nl.andrewlalis.aos_client.Client</mainClass>
|
||||||
|
</manifest>
|
||||||
|
</archive>
|
||||||
|
<descriptorRefs>
|
||||||
|
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||||
|
</descriptorRefs>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>make-assembly</id>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>single</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>nl.andrewlalis</groupId>
|
||||||
|
<artifactId>aos-core</artifactId>
|
||||||
|
<version>${parent.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
|
@ -0,0 +1,6 @@
|
||||||
|
module aos_client {
|
||||||
|
requires java.logging;
|
||||||
|
requires java.se;
|
||||||
|
|
||||||
|
requires aos_core;
|
||||||
|
}
|
|
@ -0,0 +1,184 @@
|
||||||
|
package nl.andrewlalis.aos_client;
|
||||||
|
|
||||||
|
import nl.andrewlalis.aos_client.view.GameFrame;
|
||||||
|
import nl.andrewlalis.aos_client.view.GamePanel;
|
||||||
|
import nl.andrewlalis.aos_core.model.PlayerControlState;
|
||||||
|
import nl.andrewlalis.aos_core.model.World;
|
||||||
|
import nl.andrewlalis.aos_core.net.ChatMessage;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.ObjectOutputStream;
|
||||||
|
import java.net.DatagramPacket;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main class for the client, which connects to a server to join and play.
|
||||||
|
*/
|
||||||
|
public class Client {
|
||||||
|
public static final int MAX_CHAT_MESSAGES = 10;
|
||||||
|
|
||||||
|
private final int udpPort;
|
||||||
|
private DatagramReceiver datagramReceiver;
|
||||||
|
private MessageTransceiver messageTransceiver;
|
||||||
|
|
||||||
|
private int playerId;
|
||||||
|
private PlayerControlState playerControlState;
|
||||||
|
private World world;
|
||||||
|
|
||||||
|
private final List<String> chatMessages;
|
||||||
|
private boolean chatting = false;
|
||||||
|
private final StringBuilder chatBuffer;
|
||||||
|
|
||||||
|
private final GameRenderer renderer;
|
||||||
|
private final GamePanel gamePanel;
|
||||||
|
private final SoundManager soundManager;
|
||||||
|
|
||||||
|
public Client(int udpPort) {
|
||||||
|
this.udpPort = udpPort;
|
||||||
|
this.chatMessages = new LinkedList<>();
|
||||||
|
this.chatBuffer = new StringBuilder();
|
||||||
|
this.soundManager = new SoundManager();
|
||||||
|
this.gamePanel = new GamePanel(this);
|
||||||
|
this.renderer = new GameRenderer(this, gamePanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void connect(String serverHost, int serverPort, String username) throws IOException, ClassNotFoundException {
|
||||||
|
this.datagramReceiver = new DatagramReceiver(this, this.udpPort);
|
||||||
|
this.datagramReceiver.start();
|
||||||
|
this.messageTransceiver = new MessageTransceiver(this);
|
||||||
|
this.messageTransceiver.connectToServer(serverHost, serverPort, username, this.udpPort);
|
||||||
|
this.messageTransceiver.start();
|
||||||
|
|
||||||
|
while (this.playerControlState == null) {
|
||||||
|
try {
|
||||||
|
System.out.println("Waiting for server response and player registration...");
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GameFrame g = new GameFrame("Ace of Shades - " + serverHost + ":" + serverPort, this, this.gamePanel);
|
||||||
|
g.setVisible(true);
|
||||||
|
this.renderer.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public World getWorld() {
|
||||||
|
return world;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWorld(World world) {
|
||||||
|
this.world = world;
|
||||||
|
for (String sound : this.world.getSoundsToPlay()) {
|
||||||
|
this.soundManager.play(sound);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initPlayerData(int playerId) {
|
||||||
|
this.playerId = playerId;
|
||||||
|
this.playerControlState = new PlayerControlState();
|
||||||
|
this.playerControlState.setPlayerId(playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPlayerId() {
|
||||||
|
return playerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerControlState getPlayerState() {
|
||||||
|
return playerControlState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendPlayerState() {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
ObjectOutputStream oos = new ObjectOutputStream(bos);
|
||||||
|
oos.writeObject(this.playerControlState);
|
||||||
|
byte[] buffer = bos.toByteArray();
|
||||||
|
DatagramPacket packet = new DatagramPacket(buffer, buffer.length, this.messageTransceiver.getRemoteAddress(), this.messageTransceiver.getPort());
|
||||||
|
this.datagramReceiver.getDatagramSocket().send(packet);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void addChatMessage(String text) {
|
||||||
|
this.chatMessages.add(text);
|
||||||
|
this.soundManager.play("chat.wav");
|
||||||
|
while (this.chatMessages.size() > MAX_CHAT_MESSAGES) {
|
||||||
|
this.chatMessages.remove(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getLatestChatMessages() {
|
||||||
|
return this.chatMessages.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isChatting() {
|
||||||
|
return this.chatting;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChatting(boolean chatting) {
|
||||||
|
this.chatting = chatting;
|
||||||
|
if (this.chatting) {
|
||||||
|
this.chatBuffer.setLength(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void appendToChat(char c) {
|
||||||
|
this.chatBuffer.append(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void backspaceChat() {
|
||||||
|
if (this.chatBuffer.length() > 0) {
|
||||||
|
this.chatBuffer.setLength(this.chatBuffer.length() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendChat() {
|
||||||
|
try {
|
||||||
|
this.messageTransceiver.send(new ChatMessage(this.chatBuffer.toString()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
this.setChatting(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCurrentChatBuffer() {
|
||||||
|
return this.chatBuffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
this.datagramReceiver.shutdown();
|
||||||
|
this.messageTransceiver.shutdown();
|
||||||
|
this.renderer.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
// Randomly choose a high-level UDP port that's probably open.
|
||||||
|
int udpPort = 20000 + ThreadLocalRandom.current().nextInt(0, 10000);
|
||||||
|
|
||||||
|
String hostAndPort = JOptionPane.showInputDialog("Enter server host and port (host:port):");
|
||||||
|
if (hostAndPort == null) throw new IllegalArgumentException("A host and port is required.");
|
||||||
|
String[] parts = hostAndPort.split(":");
|
||||||
|
if (parts.length != 2) throw new IllegalArgumentException("Invalid host:port.");
|
||||||
|
String host = parts[0].trim();
|
||||||
|
int port = Integer.parseInt(parts[1]);
|
||||||
|
String username = JOptionPane.showInputDialog("Enter a username:");
|
||||||
|
if (username == null || username.isBlank()) throw new IllegalArgumentException("Username is required.");
|
||||||
|
|
||||||
|
Client client = new Client(udpPort);
|
||||||
|
try {
|
||||||
|
client.connect(host, port, username);
|
||||||
|
} catch (IOException | ClassNotFoundException e) {
|
||||||
|
client.shutdown();
|
||||||
|
e.printStackTrace();
|
||||||
|
JOptionPane.showMessageDialog(null, "Could not connect:\n" + e.getMessage(), "Connection Error", JOptionPane.WARNING_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package nl.andrewlalis.aos_client;
|
||||||
|
|
||||||
|
import nl.andrewlalis.aos_core.model.World;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.ObjectInputStream;
|
||||||
|
import java.net.DatagramPacket;
|
||||||
|
import java.net.DatagramSocket;
|
||||||
|
import java.net.SocketException;
|
||||||
|
|
||||||
|
public class DatagramReceiver extends Thread {
|
||||||
|
private final DatagramSocket datagramSocket;
|
||||||
|
private final Client client;
|
||||||
|
|
||||||
|
private volatile boolean running;
|
||||||
|
|
||||||
|
public DatagramReceiver(Client client, int port) throws SocketException {
|
||||||
|
this.datagramSocket = new DatagramSocket(port);
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DatagramSocket getDatagramSocket() {
|
||||||
|
return datagramSocket;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
this.running = false;
|
||||||
|
this.datagramSocket.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
this.running = true;
|
||||||
|
byte[] buffer = new byte[8192];
|
||||||
|
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
|
||||||
|
while (this.running) {
|
||||||
|
try {
|
||||||
|
this.datagramSocket.receive(packet);
|
||||||
|
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(packet.getData()));
|
||||||
|
Object obj = ois.readObject();
|
||||||
|
if (obj instanceof World) {
|
||||||
|
this.client.setWorld((World) obj);
|
||||||
|
}
|
||||||
|
} catch (IOException | ClassNotFoundException e) {
|
||||||
|
// Ignore any receive exception.d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package nl.andrewlalis.aos_client;
|
||||||
|
|
||||||
|
import nl.andrewlalis.aos_client.view.GamePanel;
|
||||||
|
import nl.andrewlalis.aos_core.model.Bullet;
|
||||||
|
import nl.andrewlalis.aos_core.model.Player;
|
||||||
|
import nl.andrewlalis.aos_core.model.World;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This thread is responsible for updating the client's display periodically,
|
||||||
|
* and performing 'dumb' updates of the model in the interim period between
|
||||||
|
* updates from the server, by continuing objects' velocities.
|
||||||
|
*/
|
||||||
|
public class GameRenderer extends Thread {
|
||||||
|
public static final double FPS = 120.0;
|
||||||
|
public static final double MS_PER_FRAME = 1000.0 / FPS;
|
||||||
|
|
||||||
|
private volatile boolean running = true;
|
||||||
|
|
||||||
|
private final Client client;
|
||||||
|
private final GamePanel gamePanel;
|
||||||
|
|
||||||
|
public GameRenderer(Client client, GamePanel gamePanel) {
|
||||||
|
this.client = client;
|
||||||
|
this.gamePanel = gamePanel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
this.running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
long lastFrame = System.currentTimeMillis();
|
||||||
|
while (this.running) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
long msSinceLastFrame = now - lastFrame;
|
||||||
|
if (msSinceLastFrame >= MS_PER_FRAME) {
|
||||||
|
double elapsedSeconds = msSinceLastFrame / 1000.0;
|
||||||
|
this.gamePanel.repaint();
|
||||||
|
this.updateWorld(elapsedSeconds);
|
||||||
|
lastFrame = now;
|
||||||
|
msSinceLastFrame = 0;
|
||||||
|
}
|
||||||
|
long msUntilNextFrame = (long) (MS_PER_FRAME - msSinceLastFrame);
|
||||||
|
if (msUntilNextFrame > 0) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(msUntilNextFrame);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
System.err.println("Interrupted while waiting for next frame: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateWorld(double t) {
|
||||||
|
World world = this.client.getWorld();
|
||||||
|
for (Player p : world.getPlayers().values()) {
|
||||||
|
p.setPosition(p.getPosition().add(p.getVelocity().mul(t)));
|
||||||
|
}
|
||||||
|
for (Bullet b : world.getBullets()) {
|
||||||
|
b.setPosition(b.getPosition().add(b.getVelocity().mul(t)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
package nl.andrewlalis.aos_client;
|
||||||
|
|
||||||
|
import nl.andrewlalis.aos_core.net.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.ObjectInputStream;
|
||||||
|
import java.io.ObjectOutputStream;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This thread is responsible for handling TCP message communication with the
|
||||||
|
* server.
|
||||||
|
*/
|
||||||
|
public class MessageTransceiver extends Thread {
|
||||||
|
private final Client client;
|
||||||
|
|
||||||
|
private Socket socket;
|
||||||
|
private ObjectOutputStream out;
|
||||||
|
private ObjectInputStream in;
|
||||||
|
|
||||||
|
private volatile boolean running = true;
|
||||||
|
|
||||||
|
public MessageTransceiver(Client client) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void connectToServer(String serverHost, int serverPort, String username, int udpPort) throws IOException, ClassNotFoundException {
|
||||||
|
this.socket = new Socket(serverHost, serverPort);
|
||||||
|
this.out = new ObjectOutputStream(this.socket.getOutputStream());
|
||||||
|
this.in = new ObjectInputStream(this.socket.getInputStream());
|
||||||
|
this.send(new IdentMessage(username, udpPort));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
this.running = false;
|
||||||
|
if (this.socket != null) {
|
||||||
|
try {
|
||||||
|
this.socket.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public InetAddress getRemoteAddress() {
|
||||||
|
return this.socket != null ? this.socket.getInetAddress() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPort() {
|
||||||
|
return this.socket.getPort();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void send(Message message) throws IOException {
|
||||||
|
this.out.writeObject(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (this.running) {
|
||||||
|
try {
|
||||||
|
Message msg = (Message) this.in.readObject();
|
||||||
|
if (msg.getType() == Type.PLAYER_REGISTERED) {
|
||||||
|
PlayerRegisteredMessage prm = (PlayerRegisteredMessage) msg;
|
||||||
|
this.client.initPlayerData(prm.getPlayerId());
|
||||||
|
} else if (msg.getType() == Type.CHAT) {
|
||||||
|
this.client.addChatMessage(((ChatMessage) msg).getText());
|
||||||
|
}
|
||||||
|
} catch (IOException | ClassNotFoundException e) {
|
||||||
|
// Ignore exceptions.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
package nl.andrewlalis.aos_client;
|
||||||
|
|
||||||
|
import javax.sound.sampled.*;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class SoundManager {
|
||||||
|
private final Map<String, byte[]> soundData = new HashMap<>();
|
||||||
|
|
||||||
|
public void play(String sound) {
|
||||||
|
var clip = this.getClip(sound);
|
||||||
|
if (clip != null) {
|
||||||
|
clip.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Clip getClip(String sound) {
|
||||||
|
var soundBytes = this.soundData.get(sound);
|
||||||
|
if (soundBytes == null) {
|
||||||
|
InputStream is = Client.class.getResourceAsStream("/sound/" + sound);
|
||||||
|
if (is == null) {
|
||||||
|
System.err.println("Could not load sound: " + sound);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
try {
|
||||||
|
is.transferTo(bos);
|
||||||
|
soundBytes = bos.toByteArray();
|
||||||
|
this.soundData.put(sound, soundBytes);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
var ais = AudioSystem.getAudioInputStream(new ByteArrayInputStream(soundBytes));
|
||||||
|
var clip = AudioSystem.getClip();
|
||||||
|
clip.addLineListener(event -> {
|
||||||
|
if (event.getType() == LineEvent.Type.STOP) {
|
||||||
|
clip.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
clip.open(ais);
|
||||||
|
return clip;
|
||||||
|
} catch (UnsupportedAudioFileException | IOException | LineUnavailableException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
package nl.andrewlalis.aos_client.control;
|
||||||
|
|
||||||
|
import nl.andrewlalis.aos_client.Client;
|
||||||
|
|
||||||
|
import java.awt.event.KeyAdapter;
|
||||||
|
import java.awt.event.KeyEvent;
|
||||||
|
|
||||||
|
public class PlayerKeyListener extends KeyAdapter {
|
||||||
|
private final Client client;
|
||||||
|
|
||||||
|
public PlayerKeyListener(Client client) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void keyTyped(KeyEvent e) {
|
||||||
|
if (!this.client.isChatting() && (e.getKeyChar() == 't' || e.getKeyChar() == '/')) {
|
||||||
|
this.client.setChatting(true);
|
||||||
|
if (e.getKeyChar() == '/') this.client.appendToChat('/');
|
||||||
|
} else if (this.client.isChatting()) {
|
||||||
|
char c = e.getKeyChar();
|
||||||
|
if (c >= ' ' && c <= '~') {
|
||||||
|
this.client.appendToChat(c);
|
||||||
|
} else if (e.getKeyChar() == 8) {
|
||||||
|
this.client.backspaceChat();
|
||||||
|
} else if (e.getKeyChar() == 10) {
|
||||||
|
this.client.sendChat();
|
||||||
|
} else if (e.getKeyChar() == 27) {
|
||||||
|
this.client.setChatting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void keyPressed(KeyEvent e) {
|
||||||
|
if (client.isChatting()) return;
|
||||||
|
var state = client.getPlayerState();
|
||||||
|
if (e.getKeyCode() == KeyEvent.VK_W) {
|
||||||
|
state.setMovingForward(true);
|
||||||
|
} else if (e.getKeyCode() == KeyEvent.VK_S) {
|
||||||
|
state.setMovingBackward(true);
|
||||||
|
} else if (e.getKeyCode() == KeyEvent.VK_A) {
|
||||||
|
state.setMovingLeft(true);
|
||||||
|
} else if (e.getKeyCode() == KeyEvent.VK_D) {
|
||||||
|
state.setMovingRight(true);
|
||||||
|
}
|
||||||
|
this.client.sendPlayerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void keyReleased(KeyEvent e) {
|
||||||
|
if (client.isChatting()) return;
|
||||||
|
var state = client.getPlayerState();
|
||||||
|
if (e.getKeyCode() == KeyEvent.VK_W) {
|
||||||
|
state.setMovingForward(false);
|
||||||
|
} else if (e.getKeyCode() == KeyEvent.VK_S) {
|
||||||
|
state.setMovingBackward(false);
|
||||||
|
} else if (e.getKeyCode() == KeyEvent.VK_A) {
|
||||||
|
state.setMovingLeft(false);
|
||||||
|
} else if (e.getKeyCode() == KeyEvent.VK_D) {
|
||||||
|
state.setMovingRight(false);
|
||||||
|
}
|
||||||
|
this.client.sendPlayerState();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package nl.andrewlalis.aos_client.control;
|
||||||
|
|
||||||
|
import nl.andrewlalis.aos_client.Client;
|
||||||
|
import nl.andrewlalis.aos_client.view.GamePanel;
|
||||||
|
import nl.andrewlalis.aos_core.geom.Vec2;
|
||||||
|
|
||||||
|
import javax.swing.event.MouseInputAdapter;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.awt.event.MouseWheelEvent;
|
||||||
|
|
||||||
|
public class PlayerMouseListener extends MouseInputAdapter {
|
||||||
|
private final Client client;
|
||||||
|
private final GamePanel gamePanel;
|
||||||
|
|
||||||
|
public PlayerMouseListener(Client client, GamePanel gamePanel) {
|
||||||
|
this.client = client;
|
||||||
|
this.gamePanel = gamePanel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mousePressed(MouseEvent e) {
|
||||||
|
if (e.getButton() == MouseEvent.BUTTON1) {
|
||||||
|
client.getPlayerState().setShooting(true);
|
||||||
|
client.sendPlayerState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseReleased(MouseEvent e) {
|
||||||
|
if (e.getButton() == MouseEvent.BUTTON1) {
|
||||||
|
client.getPlayerState().setShooting(false);
|
||||||
|
client.sendPlayerState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseWheelMoved(MouseWheelEvent e) {
|
||||||
|
if (e.getWheelRotation() > 0) {
|
||||||
|
this.gamePanel.decrementScale();
|
||||||
|
} else if (e.getWheelRotation() < 0) {
|
||||||
|
this.gamePanel.incrementScale();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseMoved(MouseEvent e) {
|
||||||
|
Vec2 c = new Vec2(this.gamePanel.getWidth() / 2.0, this.gamePanel.getHeight() / 2.0);
|
||||||
|
Vec2 centeredMouseLocation = new Vec2(e.getX(), e.getY()).sub(c);
|
||||||
|
client.getPlayerState().setMouseLocation(centeredMouseLocation);
|
||||||
|
client.sendPlayerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseDragged(MouseEvent e) {
|
||||||
|
Vec2 c = new Vec2(this.gamePanel.getWidth() / 2.0, this.gamePanel.getHeight() / 2.0);
|
||||||
|
Vec2 centeredMouseLocation = new Vec2(e.getX(), e.getY()).sub(c);
|
||||||
|
client.getPlayerState().setMouseLocation(centeredMouseLocation);
|
||||||
|
client.getPlayerState().setShooting(true);
|
||||||
|
client.sendPlayerState();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package nl.andrewlalis.aos_client.view;
|
||||||
|
|
||||||
|
import nl.andrewlalis.aos_client.Client;
|
||||||
|
import nl.andrewlalis.aos_client.control.PlayerKeyListener;
|
||||||
|
import nl.andrewlalis.aos_client.control.PlayerMouseListener;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.event.WindowAdapter;
|
||||||
|
import java.awt.event.WindowEvent;
|
||||||
|
|
||||||
|
public class GameFrame extends JFrame {
|
||||||
|
public GameFrame(String title, Client client, GamePanel gamePanel) throws HeadlessException {
|
||||||
|
super(title);
|
||||||
|
this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||||
|
|
||||||
|
this.setResizable(false);
|
||||||
|
gamePanel.setPreferredSize(new Dimension(800, 800));
|
||||||
|
this.setContentPane(gamePanel);
|
||||||
|
gamePanel.setFocusable(true);
|
||||||
|
gamePanel.setRequestFocusEnabled(true);
|
||||||
|
var mouseListener = new PlayerMouseListener(client, gamePanel);
|
||||||
|
gamePanel.addKeyListener(new PlayerKeyListener(client));
|
||||||
|
gamePanel.addMouseListener(mouseListener);
|
||||||
|
gamePanel.addMouseMotionListener(mouseListener);
|
||||||
|
gamePanel.addMouseWheelListener(mouseListener);
|
||||||
|
this.addWindowListener(new WindowAdapter() {
|
||||||
|
@Override
|
||||||
|
public void windowClosing(WindowEvent e) {
|
||||||
|
System.out.println("Closing...");
|
||||||
|
client.shutdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.pack();
|
||||||
|
gamePanel.requestFocusInWindow();
|
||||||
|
this.setLocationRelativeTo(null);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,158 @@
|
||||||
|
package nl.andrewlalis.aos_client.view;
|
||||||
|
|
||||||
|
import nl.andrewlalis.aos_client.Client;
|
||||||
|
import nl.andrewlalis.aos_core.model.*;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.geom.AffineTransform;
|
||||||
|
import java.awt.geom.Ellipse2D;
|
||||||
|
import java.awt.geom.Rectangle2D;
|
||||||
|
|
||||||
|
public class GamePanel extends JPanel {
|
||||||
|
private final Client client;
|
||||||
|
|
||||||
|
private final double[] scales = {1.0, 2.5, 5.0, 10.0, 15.0, 20.0, 25.0, 30.0, 35.0};
|
||||||
|
private int scaleIndex = 3;
|
||||||
|
|
||||||
|
public GamePanel(Client client) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void incrementScale() {
|
||||||
|
if (scaleIndex < scales.length - 1) {
|
||||||
|
scaleIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void decrementScale() {
|
||||||
|
if (scaleIndex > 0) {
|
||||||
|
scaleIndex--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics g) {
|
||||||
|
Graphics2D g2 = (Graphics2D) g;
|
||||||
|
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
|
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
|
||||||
|
|
||||||
|
g2.setColor(Color.BLACK);
|
||||||
|
g2.setBackground(Color.BLACK);
|
||||||
|
g2.clearRect(0, 0, this.getWidth(), this.getHeight());
|
||||||
|
|
||||||
|
World world = client.getWorld();
|
||||||
|
if (world != null) drawWorld(g2, world);
|
||||||
|
drawChat(g2, client.getLatestChatMessages());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawWorld(Graphics2D g2, World world) {
|
||||||
|
Player myPlayer = world.getPlayers().get(this.client.getPlayerId());
|
||||||
|
if (myPlayer == null) return;
|
||||||
|
double scale = this.scales[this.scaleIndex];
|
||||||
|
AffineTransform pre = g2.getTransform();
|
||||||
|
g2.setTransform(this.getWorldTransform(myPlayer, scale));
|
||||||
|
g2.setStroke(new BasicStroke((float) (1 / scale)));
|
||||||
|
|
||||||
|
this.drawField(g2, world);
|
||||||
|
this.drawPlayers(g2, world);
|
||||||
|
this.drawBullets(g2, world);
|
||||||
|
g2.setTransform(pre);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AffineTransform getWorldTransform(Player player, double scale) {
|
||||||
|
AffineTransform tx = new AffineTransform();
|
||||||
|
tx.scale(scale, scale);
|
||||||
|
if (player.getTeam() != null) {
|
||||||
|
var dir = player.getTeam().getOrientation().perp();
|
||||||
|
tx.rotate(dir.x(), dir.y(), (this.getWidth() / scale / 2), (this.getHeight() / scale / 2));
|
||||||
|
}
|
||||||
|
double x = -player.getPosition().x() + (this.getWidth() / scale / 2);
|
||||||
|
double y = -player.getPosition().y() + (this.getHeight() / scale / 2);
|
||||||
|
tx.translate(x, y);
|
||||||
|
return tx;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawField(Graphics2D g2, World world) {
|
||||||
|
g2.setColor(Color.LIGHT_GRAY);
|
||||||
|
g2.fill(new Rectangle2D.Double(0, 0, world.getSize().x(), world.getSize().y()));
|
||||||
|
|
||||||
|
g2.setColor(Color.DARK_GRAY);
|
||||||
|
for (Barricade b : world.getBarricades()) {
|
||||||
|
Rectangle2D.Double barricadeRect = new Rectangle2D.Double(
|
||||||
|
b.getPosition().x(),
|
||||||
|
b.getPosition().y(),
|
||||||
|
b.getSize().x(),
|
||||||
|
b.getSize().y()
|
||||||
|
);
|
||||||
|
g2.fill(barricadeRect);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Team t : world.getTeams()) {
|
||||||
|
g2.setColor(t.getColor());
|
||||||
|
Ellipse2D.Double spawnCircle = new Ellipse2D.Double(
|
||||||
|
t.getSpawnPoint().x() - Player.RADIUS,
|
||||||
|
t.getSpawnPoint().y() - Player.RADIUS,
|
||||||
|
Player.RADIUS * 2,
|
||||||
|
Player.RADIUS * 2
|
||||||
|
);
|
||||||
|
g2.draw(spawnCircle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawPlayers(Graphics2D g2, World world) {
|
||||||
|
for (Player p : world.getPlayers().values()) {
|
||||||
|
AffineTransform pre = g2.getTransform();
|
||||||
|
AffineTransform tx = g2.getTransform();
|
||||||
|
|
||||||
|
tx.translate(p.getPosition().x(), p.getPosition().y());
|
||||||
|
tx.rotate(p.getOrientation().x(), p.getOrientation().y());
|
||||||
|
g2.setTransform(tx);
|
||||||
|
|
||||||
|
Ellipse2D.Double dot = new Ellipse2D.Double(-Player.RADIUS, -Player.RADIUS, Player.RADIUS * 2, Player.RADIUS * 2);
|
||||||
|
Color playerColor = p.getTeam() != null ? p.getTeam().getColor() : Color.BLACK;
|
||||||
|
g2.setColor(playerColor);
|
||||||
|
g2.fill(dot);
|
||||||
|
|
||||||
|
g2.setColor(Color.GRAY);
|
||||||
|
Rectangle2D.Double gun = new Rectangle2D.Double(
|
||||||
|
0,
|
||||||
|
0.5,
|
||||||
|
2,
|
||||||
|
0.25
|
||||||
|
);
|
||||||
|
g2.fill(gun);
|
||||||
|
|
||||||
|
g2.setTransform(pre);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawBullets(Graphics2D g2, World world) {
|
||||||
|
g2.setColor(Color.YELLOW);
|
||||||
|
double bulletSize = 0.5;
|
||||||
|
for (Bullet b : world.getBullets()) {
|
||||||
|
Ellipse2D.Double bulletShape = new Ellipse2D.Double(
|
||||||
|
b.getPosition().x() - bulletSize / 2,
|
||||||
|
b.getPosition().y() - bulletSize / 2,
|
||||||
|
bulletSize,
|
||||||
|
bulletSize
|
||||||
|
);
|
||||||
|
g2.fill(bulletShape);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawChat(Graphics2D g2, String[] messages) {
|
||||||
|
int height = g2.getFontMetrics().getHeight();
|
||||||
|
int y = height;
|
||||||
|
g2.setColor(Color.WHITE);
|
||||||
|
for (String message : messages) {
|
||||||
|
g2.drawString(message, 5, y);
|
||||||
|
y += height;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.client.isChatting()) {
|
||||||
|
g2.drawString("> " + this.client.getCurrentChatBuffer(), 5, height * 11);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,19 @@
|
||||||
|
<?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">
|
||||||
|
<parent>
|
||||||
|
<artifactId>ace-of-shades</artifactId>
|
||||||
|
<groupId>nl.andrewlalis</groupId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>aos-core</artifactId>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>16</maven.compiler.source>
|
||||||
|
<maven.compiler.target>16</maven.compiler.target>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
</project>
|
|
@ -0,0 +1,6 @@
|
||||||
|
module aos_core {
|
||||||
|
requires java.desktop;
|
||||||
|
exports nl.andrewlalis.aos_core.net to aos_server, aos_client;
|
||||||
|
exports nl.andrewlalis.aos_core.model to aos_server, aos_client;
|
||||||
|
exports nl.andrewlalis.aos_core.geom to aos_server, aos_client;
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package nl.andrewlalis.aos_core.geom;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
public record Vec2(double x, double y) implements Serializable {
|
||||||
|
|
||||||
|
public double mag() {
|
||||||
|
return Math.sqrt(x * x + y * y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vec2 add(Vec2 other) {
|
||||||
|
return new Vec2(this.x + other.x, this.y + other.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vec2 sub(Vec2 other) {
|
||||||
|
return new Vec2(this.x - other.x, this.y - other.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vec2 mul(double factor) {
|
||||||
|
return new Vec2(this.x * factor, this.y * factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vec2 unit() {
|
||||||
|
double mag = this.mag();
|
||||||
|
return new Vec2(this.x / mag, this.y / mag);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double dot(Vec2 other) {
|
||||||
|
return this.x * other.x + this.y * other.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vec2 perp() {
|
||||||
|
return new Vec2(-this.y, this.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vec2 perp2() {
|
||||||
|
return new Vec2(this.y, -this.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double dist(Vec2 other) {
|
||||||
|
return other.sub(this).mag();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vec2 rotate(double theta) {
|
||||||
|
return new Vec2(
|
||||||
|
this.x * Math.cos(theta) - this.y * Math.sin(theta),
|
||||||
|
this.x * Math.sin(theta) + this.y * Math.cos(theta)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double angle() {
|
||||||
|
return Math.atan2(this.y, this.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "[ " + x + ", " + y + " ]";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package nl.andrewlalis.aos_core.model;
|
||||||
|
|
||||||
|
import nl.andrewlalis.aos_core.geom.Vec2;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
public class Barricade implements Serializable {
|
||||||
|
/**
|
||||||
|
* The top-left position of this barricade, measured as the distance in
|
||||||
|
* meters from the top-left corner of the map.
|
||||||
|
*/
|
||||||
|
private Vec2 position;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The size of the barricade, in meters.
|
||||||
|
*/
|
||||||
|
private Vec2 size;
|
||||||
|
|
||||||
|
public Barricade(Vec2 position, Vec2 size) {
|
||||||
|
this.position = position;
|
||||||
|
this.size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Barricade(double x, double y, double w, double h) {
|
||||||
|
this(new Vec2(x, y), new Vec2(w, h));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vec2 getPosition() {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vec2 getSize() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package nl.andrewlalis.aos_core.model;
|
||||||
|
|
||||||
|
public class Bullet extends PhysicsObject {
|
||||||
|
public static final double SPEED = 100.0; // Meters per second.
|
||||||
|
|
||||||
|
private final int playerId;
|
||||||
|
|
||||||
|
public Bullet(Player player) {
|
||||||
|
super(player.getPosition().add(player.getOrientation().mul(1.5)), player.getOrientation(), player.getOrientation().mul(SPEED));
|
||||||
|
this.playerId = player.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPlayerId() {
|
||||||
|
return playerId;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package nl.andrewlalis.aos_core.model;
|
||||||
|
|
||||||
|
public class Gun {
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package nl.andrewlalis.aos_core.model;
|
||||||
|
|
||||||
|
import nl.andrewlalis.aos_core.geom.Vec2;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for all objects that have basic movement physics.
|
||||||
|
*/
|
||||||
|
public abstract class PhysicsObject implements Serializable {
|
||||||
|
private Vec2 position;
|
||||||
|
private Vec2 orientation;
|
||||||
|
private Vec2 velocity;
|
||||||
|
|
||||||
|
public PhysicsObject(Vec2 position, Vec2 orientation, Vec2 velocity) {
|
||||||
|
this.position = position;
|
||||||
|
this.orientation = orientation;
|
||||||
|
this.velocity = velocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PhysicsObject() {
|
||||||
|
this(new Vec2(0, 0), new Vec2(0, -1), new Vec2(0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vec2 getPosition() {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPosition(Vec2 position) {
|
||||||
|
this.position = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vec2 getOrientation() {
|
||||||
|
return orientation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOrientation(Vec2 orientation) {
|
||||||
|
this.orientation = orientation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vec2 getVelocity() {
|
||||||
|
return velocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVelocity(Vec2 velocity) {
|
||||||
|
this.velocity = velocity;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package nl.andrewlalis.aos_core.model;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class Player extends PhysicsObject {
|
||||||
|
public static final double SHOT_COOLDOWN = 0.1; // Time between shots, in seconds.
|
||||||
|
public static final double MOVEMENT_SPEED = 10; // Movement speed, in m/s
|
||||||
|
public static final double RADIUS = 0.5; // Collision radius, in meters.
|
||||||
|
|
||||||
|
private final int id;
|
||||||
|
private final String name;
|
||||||
|
private Team team;
|
||||||
|
private PlayerControlState state;
|
||||||
|
|
||||||
|
private transient long lastShot;
|
||||||
|
|
||||||
|
public Player(int id, String name, Team team) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.team = team;
|
||||||
|
this.state = new PlayerControlState();
|
||||||
|
this.state.setPlayerId(this.id);
|
||||||
|
this.updateLastShot();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setState(PlayerControlState state) {
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerControlState getState() {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Team getTeam() {
|
||||||
|
return team;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTeam(Team team) {
|
||||||
|
this.team = team;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLastShot() {
|
||||||
|
return lastShot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateLastShot() {
|
||||||
|
this.lastShot = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
Player player = (Player) o;
|
||||||
|
return getId() == player.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(getId());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
package nl.andrewlalis.aos_core.model;
|
||||||
|
|
||||||
|
import nl.andrewlalis.aos_core.geom.Vec2;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
public class PlayerControlState implements Serializable {
|
||||||
|
private int playerId;
|
||||||
|
|
||||||
|
boolean movingLeft;
|
||||||
|
boolean movingRight;
|
||||||
|
boolean movingForward;
|
||||||
|
boolean movingBackward;
|
||||||
|
|
||||||
|
boolean shooting;
|
||||||
|
|
||||||
|
Vec2 mouseLocation;
|
||||||
|
|
||||||
|
public int getPlayerId() {
|
||||||
|
return playerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlayerId(int playerId) {
|
||||||
|
this.playerId = playerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMovingLeft() {
|
||||||
|
return movingLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMovingLeft(boolean movingLeft) {
|
||||||
|
this.movingLeft = movingLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMovingRight() {
|
||||||
|
return movingRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMovingRight(boolean movingRight) {
|
||||||
|
this.movingRight = movingRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMovingForward() {
|
||||||
|
return movingForward;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMovingForward(boolean movingForward) {
|
||||||
|
this.movingForward = movingForward;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMovingBackward() {
|
||||||
|
return movingBackward;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMovingBackward(boolean movingBackward) {
|
||||||
|
this.movingBackward = movingBackward;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isShooting() {
|
||||||
|
return shooting;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setShooting(boolean shooting) {
|
||||||
|
this.shooting = shooting;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vec2 getMouseLocation() {
|
||||||
|
return mouseLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMouseLocation(Vec2 mouseLocation) {
|
||||||
|
this.mouseLocation = mouseLocation;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package nl.andrewlalis.aos_core.model;
|
||||||
|
|
||||||
|
import nl.andrewlalis.aos_core.geom.Vec2;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Team implements Serializable {
|
||||||
|
private final String name;
|
||||||
|
private final java.awt.Color color;
|
||||||
|
private final Vec2 spawnPoint;
|
||||||
|
private final Vec2 orientation;
|
||||||
|
|
||||||
|
private final List<Player> players;
|
||||||
|
|
||||||
|
public Team(String name, Color color, Vec2 spawnPoint, Vec2 orientation) {
|
||||||
|
this.name = name;
|
||||||
|
this.color = color;
|
||||||
|
this.spawnPoint = spawnPoint;
|
||||||
|
this.orientation = orientation;
|
||||||
|
this.players = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color getColor() {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vec2 getSpawnPoint() {
|
||||||
|
return spawnPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vec2 getOrientation() {
|
||||||
|
return orientation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Player> getPlayers() {
|
||||||
|
return players;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package nl.andrewlalis.aos_core.model;
|
||||||
|
|
||||||
|
import nl.andrewlalis.aos_core.geom.Vec2;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main game world, consisting of all players and other objects in the game.
|
||||||
|
*/
|
||||||
|
public class World implements Serializable {
|
||||||
|
private final Vec2 size;
|
||||||
|
|
||||||
|
private final List<Team> teams;
|
||||||
|
private final Map<Integer, Player> players;
|
||||||
|
private final List<Bullet> bullets;
|
||||||
|
private final List<Barricade> barricades;
|
||||||
|
|
||||||
|
private final List<String> soundsToPlay;
|
||||||
|
|
||||||
|
public World(Vec2 size) {
|
||||||
|
this.size = size;
|
||||||
|
this.teams = new ArrayList<>();
|
||||||
|
this.players = new HashMap<>();
|
||||||
|
this.bullets = new ArrayList<>();
|
||||||
|
this.barricades = new ArrayList<>();
|
||||||
|
this.soundsToPlay = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vec2 getSize() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Team> getTeams() {
|
||||||
|
return teams;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Integer, Player> getPlayers() {
|
||||||
|
return this.players;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Bullet> getBullets() {
|
||||||
|
return bullets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Barricade> getBarricades() {
|
||||||
|
return barricades;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getSoundsToPlay() {
|
||||||
|
return soundsToPlay;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package nl.andrewlalis.aos_core.net;
|
||||||
|
|
||||||
|
public class ChatMessage extends Message {
|
||||||
|
private final String text;
|
||||||
|
|
||||||
|
public ChatMessage(String text) {
|
||||||
|
super(Type.CHAT);
|
||||||
|
this.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getText() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package nl.andrewlalis.aos_core.net;
|
||||||
|
|
||||||
|
public class IdentMessage extends Message {
|
||||||
|
private final String name;
|
||||||
|
private final int datagramPort;
|
||||||
|
|
||||||
|
public IdentMessage(String name, int datagramPort) {
|
||||||
|
super(Type.IDENT);
|
||||||
|
this.name = name;
|
||||||
|
this.datagramPort = datagramPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDatagramPort() {
|
||||||
|
return datagramPort;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package nl.andrewlalis.aos_core.net;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
public class Message implements Serializable {
|
||||||
|
private final Type type;
|
||||||
|
|
||||||
|
public Message(Type type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Message{" +
|
||||||
|
"type=" + type +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package nl.andrewlalis.aos_core.net;
|
||||||
|
|
||||||
|
public class PlayerRegisteredMessage extends Message {
|
||||||
|
private final int playerId;
|
||||||
|
|
||||||
|
public PlayerRegisteredMessage(int playerId) {
|
||||||
|
super(Type.PLAYER_REGISTERED);
|
||||||
|
this.playerId = playerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPlayerId() {
|
||||||
|
return playerId;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package nl.andrewlalis.aos_core.net;
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
IDENT,
|
||||||
|
ACK,
|
||||||
|
PLAYER_REGISTERED,
|
||||||
|
CHAT
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
/**
|
||||||
|
* This is the main package of the core application, which contains all the info
|
||||||
|
* needed for both the client and the server to work together.
|
||||||
|
*/
|
||||||
|
package nl.andrewlalis.aos_core;
|
|
@ -0,0 +1,48 @@
|
||||||
|
package nl.andrewlalis.aos_core.util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
public class ByteUtils {
|
||||||
|
public static byte[] toBytes(int x) {
|
||||||
|
return new byte[] {
|
||||||
|
(byte) (x >> 24),
|
||||||
|
(byte) (x >> 16),
|
||||||
|
(byte) (x >> 8),
|
||||||
|
(byte) x
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int intFromBytes(byte[] bytes) {
|
||||||
|
return ((bytes[0] & 0xFF) << 24) |
|
||||||
|
((bytes[1] & 0xFF) << 16) |
|
||||||
|
((bytes[2] & 0xFF) << 8 ) |
|
||||||
|
((bytes[3] & 0xFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void write(int x, OutputStream os) throws IOException {
|
||||||
|
os.write(toBytes(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int readInt(InputStream is) throws IOException {
|
||||||
|
byte[] bytes = new byte[4];
|
||||||
|
int n = is.read(bytes);
|
||||||
|
if (n < bytes.length) throw new IOException("Could not read enough bytes to read an integer.");
|
||||||
|
return intFromBytes(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void write(String s, OutputStream os) throws IOException {
|
||||||
|
write(s.length(), os);
|
||||||
|
os.write(s.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String readString(InputStream is) throws IOException {
|
||||||
|
int length = readInt(is);
|
||||||
|
byte[] strBytes = new byte[length];
|
||||||
|
int n = is.read(strBytes);
|
||||||
|
if (n != length) throw new IOException("Could not read enough bytes to read string.");
|
||||||
|
return new String(strBytes, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?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.andrewlalis</groupId>
|
||||||
|
<artifactId>ace-of-shades</artifactId>
|
||||||
|
<packaging>pom</packaging>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
<modules>
|
||||||
|
<module>server</module>
|
||||||
|
<module>client</module>
|
||||||
|
<module>core</module>
|
||||||
|
</modules>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>16</maven.compiler.source>
|
||||||
|
<maven.compiler.target>16</maven.compiler.target>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.8.1</version>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?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">
|
||||||
|
<parent>
|
||||||
|
<artifactId>ace-of-shades</artifactId>
|
||||||
|
<groupId>nl.andrewlalis</groupId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>aos-server</artifactId>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>16</maven.compiler.source>
|
||||||
|
<maven.compiler.target>16</maven.compiler.target>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-assembly-plugin</artifactId>
|
||||||
|
<version>3.3.0</version>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifest>
|
||||||
|
<mainClass>nl.andrewlalis.aos_server.Server</mainClass>
|
||||||
|
</manifest>
|
||||||
|
</archive>
|
||||||
|
<descriptorRefs>
|
||||||
|
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||||
|
</descriptorRefs>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>make-assembly</id>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>single</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>nl.andrewlalis</groupId>
|
||||||
|
<artifactId>aos-core</artifactId>
|
||||||
|
<version>${parent.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
|
@ -0,0 +1,5 @@
|
||||||
|
module aos_server {
|
||||||
|
requires java.logging;
|
||||||
|
requires aos_core;
|
||||||
|
requires java.desktop;
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package nl.andrewlalis.aos_server;
|
||||||
|
|
||||||
|
import nl.andrewlalis.aos_core.net.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.ObjectInputStream;
|
||||||
|
import java.io.ObjectOutputStream;
|
||||||
|
import java.net.Socket;
|
||||||
|
|
||||||
|
public class ClientHandler extends Thread {
|
||||||
|
private final Server server;
|
||||||
|
private final Socket socket;
|
||||||
|
private final ObjectOutputStream out;
|
||||||
|
private final ObjectInputStream in;
|
||||||
|
|
||||||
|
private int datagramPort = -1;
|
||||||
|
private int playerId;
|
||||||
|
|
||||||
|
private volatile boolean running = true;
|
||||||
|
|
||||||
|
public ClientHandler(Server server, Socket socket) throws IOException {
|
||||||
|
this.server = server;
|
||||||
|
this.socket = socket;
|
||||||
|
this.out = new ObjectOutputStream(socket.getOutputStream());
|
||||||
|
this.in = new ObjectInputStream(socket.getInputStream());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Socket getSocket() {
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDatagramPort() {
|
||||||
|
return datagramPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPlayerId() {
|
||||||
|
return playerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
this.running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void send(Message message) throws IOException {
|
||||||
|
this.out.writeObject(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
while (this.running) {
|
||||||
|
try {
|
||||||
|
Message msg = (Message) this.in.readObject();
|
||||||
|
if (msg.getType() == Type.IDENT) {
|
||||||
|
IdentMessage ident = (IdentMessage) msg;
|
||||||
|
int id = this.server.registerNewPlayer(ident.getName());
|
||||||
|
this.playerId = id;
|
||||||
|
this.datagramPort = ident.getDatagramPort();
|
||||||
|
this.send(new PlayerRegisteredMessage(id));
|
||||||
|
} else if (msg.getType() == Type.CHAT) {
|
||||||
|
this.server.broadcastPlayerChat(this.playerId, (ChatMessage) msg);
|
||||||
|
}
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Ignore this exception, consider the client disconnected.
|
||||||
|
}
|
||||||
|
this.datagramPort = -1;
|
||||||
|
try {
|
||||||
|
this.socket.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
this.server.clientDisconnected(this);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package nl.andrewlalis.aos_server;
|
||||||
|
|
||||||
|
import nl.andrewlalis.aos_core.model.PlayerControlState;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.ObjectInputStream;
|
||||||
|
import java.net.DatagramPacket;
|
||||||
|
import java.net.DatagramSocket;
|
||||||
|
import java.net.SocketException;
|
||||||
|
|
||||||
|
public class DatagramCommunicationThread extends Thread {
|
||||||
|
private final Server server;
|
||||||
|
private final DatagramSocket socket;
|
||||||
|
|
||||||
|
public DatagramCommunicationThread(Server server, int port) throws SocketException {
|
||||||
|
this.server = server;
|
||||||
|
this.socket = new DatagramSocket(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DatagramSocket getSocket() {
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
byte[] buffer = new byte[8192];
|
||||||
|
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
this.socket.receive(packet);
|
||||||
|
Object obj = new ObjectInputStream(new ByteArrayInputStream(buffer)).readObject();
|
||||||
|
if (obj instanceof PlayerControlState) {
|
||||||
|
this.server.updatePlayerState((PlayerControlState) obj);
|
||||||
|
}
|
||||||
|
} catch (IOException | ClassNotFoundException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,161 @@
|
||||||
|
package nl.andrewlalis.aos_server;
|
||||||
|
|
||||||
|
import nl.andrewlalis.aos_core.geom.Vec2;
|
||||||
|
import nl.andrewlalis.aos_core.model.*;
|
||||||
|
import nl.andrewlalis.aos_core.net.ChatMessage;
|
||||||
|
import nl.andrewlalis.aos_core.net.Message;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.ObjectOutputStream;
|
||||||
|
import java.net.DatagramPacket;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Scanner;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
|
public class Server {
|
||||||
|
public static final int DEFAULT_PORT = 8035;
|
||||||
|
|
||||||
|
private final List<ClientHandler> clientHandlers;
|
||||||
|
private final ServerSocket serverSocket;
|
||||||
|
private final DatagramCommunicationThread datagramCommunicationThread;
|
||||||
|
private final World world;
|
||||||
|
private final WorldUpdater worldUpdater;
|
||||||
|
|
||||||
|
public Server(int port) throws IOException {
|
||||||
|
this.clientHandlers = new ArrayList<>();
|
||||||
|
this.serverSocket = new ServerSocket(port);
|
||||||
|
this.datagramCommunicationThread = new DatagramCommunicationThread(this, port);
|
||||||
|
|
||||||
|
this.world = new World(new Vec2(50, 70));
|
||||||
|
world.getBarricades().add(new Barricade(10, 10, 30, 5));
|
||||||
|
world.getBarricades().add(new Barricade(10, 55, 30, 5));
|
||||||
|
world.getBarricades().add(new Barricade(20, 30, 10, 10));
|
||||||
|
world.getBarricades().add(new Barricade(0, 30, 10, 10));
|
||||||
|
world.getBarricades().add(new Barricade(40, 30, 10, 10));
|
||||||
|
|
||||||
|
world.getTeams().add(new Team("Red", Color.RED, new Vec2(3, 3), new Vec2(0, 1)));
|
||||||
|
world.getTeams().add(new Team("Blue", Color.BLUE, new Vec2(world.getSize().x() - 3, world.getSize().y() - 3), new Vec2(0, -1)));
|
||||||
|
|
||||||
|
this.worldUpdater = new WorldUpdater(this, this.world);
|
||||||
|
System.out.println("Started AOS-Server TCP/UDP on port " + port);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void acceptClientConnection() throws IOException {
|
||||||
|
Socket socket = this.serverSocket.accept();
|
||||||
|
var t = new ClientHandler(this, socket);
|
||||||
|
t.start();
|
||||||
|
synchronized (this.clientHandlers) {
|
||||||
|
this.clientHandlers.add(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int registerNewPlayer(String name) {
|
||||||
|
int id = ThreadLocalRandom.current().nextInt(1, Integer.MAX_VALUE);
|
||||||
|
Team team = null;
|
||||||
|
for (Team t : this.world.getTeams()) {
|
||||||
|
if (team == null) {
|
||||||
|
team = t;
|
||||||
|
} else if (t.getPlayers().size() < team.getPlayers().size()) {
|
||||||
|
team = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Player p = new Player(id, name, team);
|
||||||
|
System.out.println("Client connected: " + p.getId() + ", " + p.getName());
|
||||||
|
this.broadcastMessage(new ChatMessage(name + " connected."));
|
||||||
|
this.world.getPlayers().put(p.getId(), p);
|
||||||
|
p.setPosition(new Vec2(this.world.getSize().x() / 2.0, this.world.getSize().y() / 2.0));
|
||||||
|
if (team != null) {
|
||||||
|
team.getPlayers().add(p);
|
||||||
|
p.setPosition(team.getSpawnPoint());
|
||||||
|
p.setOrientation(team.getOrientation());
|
||||||
|
this.broadcastMessage(new ChatMessage(name + " joined team " + team.getName()));
|
||||||
|
System.out.println("Player joined team " + team.getName());
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clientDisconnected(ClientHandler clientHandler) {
|
||||||
|
Player player = this.world.getPlayers().get(clientHandler.getPlayerId());
|
||||||
|
synchronized (this.clientHandlers) {
|
||||||
|
this.clientHandlers.remove(clientHandler);
|
||||||
|
clientHandler.shutdown();
|
||||||
|
}
|
||||||
|
this.world.getPlayers().remove(player.getId());
|
||||||
|
if (player.getTeam() != null) {
|
||||||
|
player.getTeam().getPlayers().remove(player);
|
||||||
|
}
|
||||||
|
this.broadcastMessage(new ChatMessage(player.getName() + " disconnected."));
|
||||||
|
System.out.println("Client disconnected: " + player.getId() + ", " + player.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendWorldToClients() {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
new ObjectOutputStream(bos).writeObject(this.world);
|
||||||
|
byte[] data = bos.toByteArray();
|
||||||
|
DatagramPacket packet = new DatagramPacket(data, data.length);
|
||||||
|
for (ClientHandler handler : this.clientHandlers) {
|
||||||
|
if (handler.getDatagramPort() == -1) continue;
|
||||||
|
packet.setAddress(handler.getSocket().getInetAddress());
|
||||||
|
packet.setPort(handler.getDatagramPort());
|
||||||
|
this.datagramCommunicationThread.getSocket().send(packet);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updatePlayerState(PlayerControlState state) {
|
||||||
|
Player p = this.world.getPlayers().get(state.getPlayerId());
|
||||||
|
if (p != null) {
|
||||||
|
p.setState(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void broadcastMessage(Message message) {
|
||||||
|
for (ClientHandler handler : this.clientHandlers) {
|
||||||
|
try {
|
||||||
|
handler.send(message);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void broadcastPlayerChat(int playerId, ChatMessage msg) {
|
||||||
|
Player p = this.world.getPlayers().get(playerId);
|
||||||
|
if (p == null) return;
|
||||||
|
this.broadcastMessage(new ChatMessage(p.getName() + ": " + msg.getText()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static void main(String[] args) throws IOException {
|
||||||
|
System.out.println("Enter the port number to start the server on, or blank for default (" + DEFAULT_PORT + "):");
|
||||||
|
Scanner sc = new Scanner(System.in);
|
||||||
|
String input = sc.nextLine();
|
||||||
|
int port = DEFAULT_PORT;
|
||||||
|
if (input != null && !input.isBlank()) {
|
||||||
|
try {
|
||||||
|
port = Integer.parseInt(input.trim());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
System.err.println("Invalid port.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Server server = new Server(port);
|
||||||
|
server.datagramCommunicationThread.start();
|
||||||
|
server.worldUpdater.start();
|
||||||
|
while (true) {
|
||||||
|
server.acceptClientConnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,168 @@
|
||||||
|
package nl.andrewlalis.aos_server;
|
||||||
|
|
||||||
|
import nl.andrewlalis.aos_core.geom.Vec2;
|
||||||
|
import nl.andrewlalis.aos_core.model.Barricade;
|
||||||
|
import nl.andrewlalis.aos_core.model.Bullet;
|
||||||
|
import nl.andrewlalis.aos_core.model.Player;
|
||||||
|
import nl.andrewlalis.aos_core.model.World;
|
||||||
|
import nl.andrewlalis.aos_core.net.ChatMessage;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
|
public class WorldUpdater extends Thread {
|
||||||
|
public static final double TARGET_TPS = 120.0;
|
||||||
|
public static final double MS_PER_TICK = 1000.0 / TARGET_TPS;
|
||||||
|
|
||||||
|
private final Server server;
|
||||||
|
private final World world;
|
||||||
|
private volatile boolean running = true;
|
||||||
|
|
||||||
|
public WorldUpdater(Server server, World world) {
|
||||||
|
this.server = server;
|
||||||
|
this.world = world;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
this.running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
long lastTick = System.currentTimeMillis();
|
||||||
|
while (this.running) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
long msSinceLastTick = now - lastTick;
|
||||||
|
if (msSinceLastTick >= MS_PER_TICK) {
|
||||||
|
double elapsedSeconds = msSinceLastTick / 1000.0;
|
||||||
|
this.tick(elapsedSeconds);
|
||||||
|
lastTick = now;
|
||||||
|
}
|
||||||
|
long msUntilNextTick = (long) (MS_PER_TICK - msSinceLastTick);
|
||||||
|
if (msUntilNextTick > 0) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(msUntilNextTick);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
System.err.println("Interrupted while sleeping until next tick: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tick(double t) {
|
||||||
|
world.getSoundsToPlay().clear();
|
||||||
|
this.updateBullets(t);
|
||||||
|
this.updatePlayers(t);
|
||||||
|
this.server.sendWorldToClients();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePlayers(double t) {
|
||||||
|
for (Player p : this.world.getPlayers().values()) {
|
||||||
|
this.updatePlayerMovement(p, t);
|
||||||
|
this.updatePlayerShooting(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePlayerMovement(Player p, double t) {
|
||||||
|
if (p.getState().getMouseLocation() != null && p.getState().getMouseLocation().mag() > 0) {
|
||||||
|
Vec2 newOrientation = p.getState().getMouseLocation().unit();
|
||||||
|
if (p.getTeam() != null) {
|
||||||
|
double theta = p.getTeam().getOrientation().rotate(Math.PI / 2).angle();
|
||||||
|
newOrientation = newOrientation.rotate(-theta);
|
||||||
|
}
|
||||||
|
p.setOrientation(newOrientation);
|
||||||
|
}
|
||||||
|
double vx = 0;
|
||||||
|
double vy = 0;
|
||||||
|
if (p.getState().isMovingForward()) vy += Player.MOVEMENT_SPEED;
|
||||||
|
if (p.getState().isMovingBackward()) vy -= Player.MOVEMENT_SPEED;
|
||||||
|
if (p.getState().isMovingLeft()) vx -= Player.MOVEMENT_SPEED;
|
||||||
|
if (p.getState().isMovingRight()) vx += Player.MOVEMENT_SPEED;
|
||||||
|
Vec2 forwardVector = p.getOrientation().mul(vy);
|
||||||
|
Vec2 leftVector = p.getOrientation().perp().mul(vx);
|
||||||
|
Vec2 newPos = p.getPosition().add(forwardVector.mul(t)).add(leftVector.mul(t));
|
||||||
|
double nx = newPos.x();
|
||||||
|
double ny = newPos.y();
|
||||||
|
|
||||||
|
for (Barricade b : world.getBarricades()) {
|
||||||
|
// TODO: Improve barricade collision smoothness.
|
||||||
|
double x1 = b.getPosition().x();
|
||||||
|
double x2 = x1 + b.getSize().x();
|
||||||
|
double y1 = b.getPosition().y();
|
||||||
|
double y2 = y1 + b.getSize().y();
|
||||||
|
if (nx + Player.RADIUS > x1 && nx - Player.RADIUS < x2 && ny + Player.RADIUS > y1 && ny - Player.RADIUS < y2) {
|
||||||
|
double distanceLeft = Math.abs(nx - x1);
|
||||||
|
double distanceRight = Math.abs(nx - x2);
|
||||||
|
double distanceTop = Math.abs(ny - y1);
|
||||||
|
double distanceBottom = Math.abs(ny - y2);
|
||||||
|
if (distanceLeft < Player.RADIUS) {
|
||||||
|
nx = x1 - Player.RADIUS;
|
||||||
|
} else if (distanceRight < Player.RADIUS) {
|
||||||
|
nx = x2 + Player.RADIUS;
|
||||||
|
} else if (distanceTop < Player.RADIUS) {
|
||||||
|
ny = y1 - Player.RADIUS;
|
||||||
|
} else if (distanceBottom < Player.RADIUS) {
|
||||||
|
ny = y2 + Player.RADIUS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nx - Player.RADIUS < 0) nx = Player.RADIUS;
|
||||||
|
if (nx + Player.RADIUS > this.world.getSize().x()) nx = this.world.getSize().x() - Player.RADIUS;
|
||||||
|
if (ny - Player.RADIUS < 0) ny = Player.RADIUS;
|
||||||
|
if (ny + Player.RADIUS > this.world.getSize().y()) ny = this.world.getSize().y() - Player.RADIUS;
|
||||||
|
p.setPosition(new Vec2(nx, ny));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePlayerShooting(Player p) {
|
||||||
|
if (p.getState().isShooting() && p.getLastShot() + Player.SHOT_COOLDOWN * 1000 < System.currentTimeMillis()) {
|
||||||
|
this.world.getBullets().add(new Bullet(p));
|
||||||
|
this.world.getSoundsToPlay().add("ak47shot1.wav");
|
||||||
|
p.updateLastShot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateBullets(double t) {
|
||||||
|
List<Bullet> bulletsToRemove = new ArrayList<>();
|
||||||
|
for (Bullet b : this.world.getBullets()) {
|
||||||
|
Vec2 oldPos = b.getPosition();
|
||||||
|
b.setPosition(b.getPosition().add(b.getVelocity().mul(t)));
|
||||||
|
Vec2 pos = b.getPosition();
|
||||||
|
if (pos.x() < 0 || pos.y() < 0 || pos.x() > this.world.getSize().x() || pos.y() > this.world.getSize().y()) {
|
||||||
|
bulletsToRemove.add(b);
|
||||||
|
}
|
||||||
|
for (Barricade bar : this.world.getBarricades()) {
|
||||||
|
if (
|
||||||
|
pos.x() > bar.getPosition().x() && pos.x() < bar.getPosition().x() + bar.getSize().x() &&
|
||||||
|
pos.y() > bar.getPosition().y() && pos.y() < bar.getPosition().y() + bar.getSize().y()
|
||||||
|
) {
|
||||||
|
int n = ThreadLocalRandom.current().nextInt(1, 6);
|
||||||
|
this.world.getSoundsToPlay().add("bullet_impact_" + n + ".wav");
|
||||||
|
bulletsToRemove.add(b);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double x1 = oldPos.x();
|
||||||
|
double x2 = b.getPosition().x();
|
||||||
|
double y1 = oldPos.y();
|
||||||
|
double y2 = b.getPosition().y();
|
||||||
|
double lineDist = oldPos.dist(b.getPosition());
|
||||||
|
for (Player p : this.world.getPlayers().values()) {
|
||||||
|
double n = ((p.getPosition().x() - x1) * (x2 - x1) + (p.getPosition().y() - y1) * (y2 - y1)) / lineDist;
|
||||||
|
n = Math.max(Math.min(n, 1), 0);
|
||||||
|
double dist = p.getPosition().dist(new Vec2(x1 + n * (x2 - x1), y1 + n * (y2 - y1)));
|
||||||
|
if (dist < Player.RADIUS) {
|
||||||
|
Player killer = this.world.getPlayers().get(b.getPlayerId());
|
||||||
|
this.server.broadcastMessage(new ChatMessage(p.getName() + " was shot by " + killer.getName() + "."));
|
||||||
|
world.getSoundsToPlay().add("death.wav");
|
||||||
|
if (p.getTeam() != null) {
|
||||||
|
p.setPosition(p.getTeam().getSpawnPoint());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.world.getBullets().removeAll(bulletsToRemove);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue