Added chat, improved player input responses, and rebalanced weapons.

This commit is contained in:
Andrew Lalis 2022-07-28 11:02:09 +02:00
parent bf0982fbd9
commit e909b90457
18 changed files with 241 additions and 104 deletions

6
.gitignore vendored
View File

@ -1,7 +1,13 @@
.idea/
target/
client-builds/
client.yaml
server.yaml
# Ignore the ./config directory so that developers can put their config files
# there for server and client apps.
config
# Ignore compiled tool executables
build-clients
setversion

View File

@ -8,45 +8,22 @@ _Read this guide to get started and join a server in just a minute or two!_
1. Make sure you've got at least Java 17 installed. You can get it [here](https://adoptium.net/temurin/releases).
2. Download the `aos2-client` JAR file from the [releases page](https://github.com/andrewlalis/ace-of-shades-2/releases) that's compatible with your system.
3. Run the game by double-clicking the JAR file, or entering `java -jar <jarfile>` in a terminal. This should generate a `config.yaml` file.
3. Run the game by double-clicking the JAR file, or entering `java -jar <jarfile>` in a terminal. This should generate a `client.yaml` file.
4. Set the `serverHost`, `serverPort`, and `username` properties accordingly for the server you want to join.
5. Run the game again to join the server and start playing!
### Controls
- `WASD` to move, `SPACE` to jump, `LEFT-CONTROL` to crouch.
- `LEFT-CLICK` to use your held item (shoot, destroy blocks, etc.).
- `RIGHT-CLICK` to interact with things using your held item (place blocks, etc.).
- `R` to reload.
- `T` to chat. Press `ENTER` to send the chat.
- `ESCAPE` to exit the game.
- Numbers are used for selecting different items.
- `F3` toggles showing debug info.
## Setting up a Server
Setting up a server is quite easy. Just go to the [releases page](https://github.com/andrewlalis/ace-of-shades-2/releases) and download the latest `aos2-server` JAR file. Similar to the client, it's best if you provide a `config.yaml` file to the server, in the same directory. The following snippet shows the structure and default values of a server's configuration.
```yaml
port: 25565
connectionBacklog: 5
ticksPerSecond: 20.0
world: worlds.redfort
teams:
- name: Red
color: [0.8, 0, 0]
spawnPoint: A
- name: Blue
color: [0, 0, 0.8]
spawnPoint: B
physics:
gravity: 29.43
walkingSpeed: 4
crouchingSpeed: 1.5
sprintingSpeed: 9
movementAcceleration: 2
movementDeceleration: 1
jumpVerticalSpeed: 8
actions:
blockBreakCooldown: 0.25
blockPlaceCooldown: 0.1
blockBreakReach: 5
blockPlaceReach: 5
blockBulletDamageResistance: 3
blockBulletDamageCooldown: 10
resupplyCooldown: 30
resupplyRadius: 3
teamSpawnProtection: 10
movementAccuracyDecreaseFactor: 0.01
friendlyFire: false
```
Setting up a server is quite easy. Just go to the [releases page](https://github.com/andrewlalis/ace-of-shades-2/releases) and download the latest `aos2-server` JAR file, and run it. It'll create a `server.yaml` configuration file if you don't provide one.
## Configuration
Both the client and server use a similar style of YAML-based configuration, where upon booting up, the program will look for a configuration file in the current working directory with one of the following names: `configuration`, `config`, `cfg`, ending in either `.yaml` or `.yml`. Alternatively, you can provide the path to a configuration file at a different location via a single command-line argument. For example:

View File

@ -83,14 +83,7 @@ public class Client implements Runnable {
return;
}
gameRenderer = new GameRenderer(this);
gameRenderer.setupWindow(
inputHandler,
new PlayerViewCursorCallback(config.input, this, gameRenderer.getCamera(), communicationHandler),
new PlayerInputKeyCallback(inputHandler),
new PlayerInputMouseClickCallback(inputHandler),
new PlayerInputMouseScrollCallback(this, communicationHandler)
);
gameRenderer = new GameRenderer(this, inputHandler);
soundManager = new SoundManager();
log.debug("Sound system initialized.");
@ -207,6 +200,13 @@ public class Client implements Runnable {
if (soundManager != null) {
soundManager.play("chat", 1, myPlayer.getEyePosition(), myPlayer.getVelocity());
}
} else if (msg instanceof ClientOrientationUpdateMessage orientationUpdateMessage) {
runLater(() -> {
myPlayer.setOrientation(orientationUpdateMessage.x(), orientationUpdateMessage.y());
if (gameRenderer != null) {
gameRenderer.getCamera().setOrientationToPlayer(myPlayer);
}
});
}
}
@ -222,6 +222,10 @@ public class Client implements Runnable {
return inputHandler;
}
public CommunicationHandler getCommunicationHandler() {
return communicationHandler;
}
public Map<Integer, Team> getTeams() {
return teams;
}

View File

@ -6,6 +6,7 @@ import nl.andrewl.aos2_client.model.ClientPlayer;
import nl.andrewl.aos_core.model.item.BlockItemStack;
import nl.andrewl.aos_core.model.world.Hit;
import nl.andrewl.aos_core.net.client.BlockColorMessage;
import nl.andrewl.aos_core.net.client.ChatWrittenMessage;
import nl.andrewl.aos_core.net.client.ClientInputState;
import static org.lwjgl.glfw.GLFW.*;
@ -35,6 +36,9 @@ public class InputHandler {
private boolean debugEnabled;
private boolean chatting;
private StringBuffer chatText = new StringBuffer();
public InputHandler(Client client, CommunicationHandler comm) {
this.client = client;
@ -66,6 +70,7 @@ public class InputHandler {
}
public void setForward(boolean forward) {
if (chatting) return;
this.forward = forward;
updateInputState();
}
@ -75,6 +80,7 @@ public class InputHandler {
}
public void setBackward(boolean backward) {
if (chatting) return;
this.backward = backward;
updateInputState();
}
@ -84,6 +90,7 @@ public class InputHandler {
}
public void setLeft(boolean left) {
if (chatting) return;
this.left = left;
updateInputState();
}
@ -93,6 +100,7 @@ public class InputHandler {
}
public void setRight(boolean right) {
if (chatting) return;
this.right = right;
updateInputState();
}
@ -102,6 +110,7 @@ public class InputHandler {
}
public void setJumping(boolean jumping) {
if (chatting) return;
this.jumping = jumping;
updateInputState();
}
@ -111,6 +120,7 @@ public class InputHandler {
}
public void setCrouching(boolean crouching) {
if (chatting) return;
this.crouching = crouching;
updateInputState();
}
@ -120,6 +130,7 @@ public class InputHandler {
}
public void setSprinting(boolean sprinting) {
if (chatting) return;
this.sprinting = sprinting;
updateInputState();
}
@ -129,6 +140,7 @@ public class InputHandler {
}
public void setHitting(boolean hitting) {
if (chatting) return;
this.hitting = hitting;
updateInputState();
}
@ -138,6 +150,7 @@ public class InputHandler {
}
public void setInteracting(boolean interacting) {
if (chatting) return;
this.interacting = interacting;
updateInputState();
}
@ -147,6 +160,7 @@ public class InputHandler {
}
public void setReloading(boolean reloading) {
if (chatting) return;
this.reloading = reloading;
updateInputState();
}
@ -156,6 +170,7 @@ public class InputHandler {
}
public void setSelectedInventoryIndex(int selectedInventoryIndex) {
if (chatting) return;
this.selectedInventoryIndex = selectedInventoryIndex;
updateInputState();
}
@ -168,6 +183,57 @@ public class InputHandler {
this.debugEnabled = !debugEnabled;
}
public void enableChatting() {
if (chatting) return;
setForward(false);
setBackward(false);
setLeft(false);
setRight(false);
setJumping(false);
setCrouching(false);
setSprinting(false);
setReloading(false);
chatting = true;
chatText = new StringBuffer();
}
public boolean isChatting() {
return chatting;
}
public void cancelChatting() {
chatting = false;
chatText.delete(0, chatText.length());
}
public void appendToChat(int codePoint) {
if (!chatting || chatText.length() + 1 > 120) return;
chatText.appendCodePoint(codePoint);
}
public void appendToChat(String s) {
if (!chatting || chatText.length() + s.length() > 120) return;
chatText.append(s);
}
public void deleteFromChat() {
if (!chatting || chatText.length() == 0) return;
chatText.deleteCharAt(chatText.length() - 1);
}
public String getChatText() {
return chatText.toString();
}
public void sendChat() {
if (!chatting) return;
String text = chatText.toString().trim();
cancelChatting();
if (!text.isBlank()) {
client.getCommunicationHandler().sendMessage(new ChatWrittenMessage(text));
}
}
public void pickBlock() {
var player = client.getMyPlayer();
if (player.getInventory().getSelectedItemStack() instanceof BlockItemStack stack) {

View File

@ -0,0 +1,18 @@
package nl.andrewl.aos2_client.control;
import org.lwjgl.glfw.GLFWCharCallbackI;
public class PlayerCharacterInputCallback implements GLFWCharCallbackI {
private final InputHandler inputHandler;
public PlayerCharacterInputCallback(InputHandler inputHandler) {
this.inputHandler = inputHandler;
}
@Override
public void invoke(long window, int codepoint) {
if (inputHandler.isChatting()) {
inputHandler.appendToChat(codepoint);
}
}
}

View File

@ -24,12 +24,23 @@ public class PlayerInputKeyCallback implements GLFWKeyCallbackI {
case GLFW_KEY_LEFT_SHIFT -> inputHandler.setSprinting(true);
case GLFW_KEY_R -> inputHandler.setReloading(true);
case GLFW_KEY_BACKSPACE -> inputHandler.deleteFromChat();
case GLFW_KEY_ENTER -> inputHandler.sendChat();
case GLFW_KEY_1 -> inputHandler.setSelectedInventoryIndex(0);
case GLFW_KEY_2 -> inputHandler.setSelectedInventoryIndex(1);
case GLFW_KEY_3 -> inputHandler.setSelectedInventoryIndex(2);
case GLFW_KEY_4 -> inputHandler.setSelectedInventoryIndex(3);
case GLFW_KEY_F3 -> inputHandler.toggleDebugEnabled();
case GLFW_KEY_ESCAPE -> {
if (inputHandler.isChatting()) {
inputHandler.cancelChatting();
} else {
glfwSetWindowShouldClose(window, true);
}
}
}
} else if (action == GLFW_RELEASE) {
switch (key) {
@ -42,7 +53,15 @@ public class PlayerInputKeyCallback implements GLFWKeyCallbackI {
case GLFW_KEY_LEFT_SHIFT -> inputHandler.setSprinting(false);
case GLFW_KEY_R -> inputHandler.setReloading(false);
case GLFW_KEY_ESCAPE -> glfwSetWindowShouldClose(window, true);
case GLFW_KEY_T -> inputHandler.enableChatting();
case GLFW_KEY_SLASH -> {
inputHandler.enableChatting();
inputHandler.appendToChat("/");
}
}
} else if (action == GLFW_REPEAT) {
switch (key) {
case GLFW_KEY_BACKSPACE -> inputHandler.deleteFromChat();
}
}
}

View File

@ -10,9 +10,9 @@ public class PlayerInputMouseScrollCallback implements GLFWScrollCallbackI {
private final Client client;
private final CommunicationHandler comm;
public PlayerInputMouseScrollCallback(Client client, CommunicationHandler comm) {
public PlayerInputMouseScrollCallback(Client client) {
this.client = client;
this.comm = comm;
this.comm = client.getCommunicationHandler();
}
@Override

View File

@ -30,11 +30,11 @@ public class PlayerViewCursorCallback implements GLFWCursorPosCallbackI {
private float lastMouseCursorY;
private long lastOrientationUpdateSentAt = 0L;
public PlayerViewCursorCallback(ClientConfig.InputConfig config, Client client, Camera cam, CommunicationHandler comm) {
this.config = config;
public PlayerViewCursorCallback(Client client, Camera cam) {
this.config = client.getConfig().input;
this.client = client;
this.camera = cam;
this.comm = comm;
this.comm = client.getCommunicationHandler();
}
@Override

View File

@ -3,7 +3,7 @@ package nl.andrewl.aos2_client.render;
import nl.andrewl.aos2_client.Camera;
import nl.andrewl.aos2_client.Client;
import nl.andrewl.aos2_client.config.ClientConfig;
import nl.andrewl.aos2_client.control.InputHandler;
import nl.andrewl.aos2_client.control.*;
import nl.andrewl.aos2_client.model.ClientPlayer;
import nl.andrewl.aos2_client.render.chunk.ChunkRenderer;
import nl.andrewl.aos2_client.render.gui.GuiRenderer;
@ -36,42 +36,36 @@ public class GameRenderer {
private static final float Z_FAR = 500f;
private final ClientConfig.DisplayConfig config;
private ChunkRenderer chunkRenderer;
private GuiRenderer guiRenderer;
private ModelRenderer modelRenderer;
private final ChunkRenderer chunkRenderer;
private final GuiRenderer guiRenderer;
private final ModelRenderer modelRenderer;
private final Camera camera;
private final Client client;
// Standard models for various game components.
private Model playerModel;
private Model rifleModel;
private Model blockModel;
private Model bulletModel;
private Model smgModel;
private Model shotgunModel;
private Model flagModel;
private final Model playerModel;
private final Model rifleModel;
private final Model blockModel;
private final Model bulletModel;
private final Model smgModel;
private final Model shotgunModel;
private final Model flagModel;
private long windowHandle;
private int screenWidth = 800;
private int screenHeight = 600;
private final long windowHandle;
private final int screenWidth;
private final int screenHeight;
private final Matrix4f perspectiveTransform;
public GameRenderer(Client client) {
public GameRenderer(Client client, InputHandler inputHandler) {
this.config = client.getConfig().display;
this.client = client;
this.camera = new Camera();
camera.setToPlayer(client.getMyPlayer());
this.perspectiveTransform = new Matrix4f();
}
public void setupWindow(
InputHandler inputHandler,
GLFWCursorPosCallbackI viewCursorCallback,
GLFWKeyCallbackI inputKeyCallback,
GLFWMouseButtonCallbackI mouseButtonCallback,
GLFWScrollCallbackI scrollCallback
) {
// Initialize window!
GLFWErrorCallback.createPrint(System.err).set();
if (!glfwInit()) throw new IllegalStateException("Could not initialize GLFW.");
glfwDefaultWindowHints();
@ -96,10 +90,11 @@ public class GameRenderer {
log.debug("Initialized GLFW window.");
// Setup callbacks.
glfwSetKeyCallback(windowHandle, inputKeyCallback);
glfwSetCursorPosCallback(windowHandle, viewCursorCallback);
glfwSetMouseButtonCallback(windowHandle, mouseButtonCallback);
glfwSetScrollCallback(windowHandle, scrollCallback);
glfwSetKeyCallback(windowHandle, new PlayerInputKeyCallback(inputHandler));
glfwSetCursorPosCallback(windowHandle, new PlayerViewCursorCallback(client, camera));
glfwSetMouseButtonCallback(windowHandle, new PlayerInputMouseClickCallback(inputHandler));
glfwSetScrollCallback(windowHandle, new PlayerInputMouseScrollCallback(client));
glfwSetCharCallback(windowHandle, new PlayerCharacterInputCallback(inputHandler));
if (config.captureCursor) {
glfwSetInputMode(windowHandle, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
}
@ -282,15 +277,15 @@ public class GameRenderer {
}
public void freeWindow() {
if (rifleModel != null) rifleModel.free();
if (smgModel != null) smgModel.free();
if (flagModel != null) flagModel.free();
if (bulletModel != null) bulletModel.free();
if (playerModel != null) playerModel.free();
if (blockModel != null) blockModel.free();
if (modelRenderer != null) modelRenderer.free();
if (guiRenderer != null) guiRenderer.free();
if (chunkRenderer != null) chunkRenderer.free();
rifleModel.free();
smgModel.free();
flagModel.free();
bulletModel.free();
playerModel.free();
blockModel.free();
modelRenderer.free();
guiRenderer.free();
chunkRenderer.free();
GL.destroy();
Callbacks.glfwFreeCallbacks(windowHandle);
glfwSetErrorCallback(null);

View File

@ -223,7 +223,7 @@ public class GuiRenderer {
nvgSave(vgId);
drawCrosshair(width, height);
drawChat(width, height, client.getChat());
drawChat(width, height, client);
drawHealthBar(width, height, client.getMyPlayer());
drawHeldItemStackInfo(width, height, client.getMyPlayer());
if (client.getInputHandler().isDebugEnabled()) {
@ -269,18 +269,18 @@ public class GuiRenderer {
private void drawHealthBar(float w, float h, ClientPlayer player) {
nvgFillColor(vgId, GuiUtils.rgba(1, 0, 0, 1, colorA));
nvgBeginPath(vgId);
nvgRect(vgId, 20, h - 60, 100, 20);
nvgRect(vgId, w - 170, h - 110, 100, 20);
nvgFill(vgId);
nvgFillColor(vgId, GuiUtils.rgba(0, 1, 0, 1, colorA));
nvgBeginPath(vgId);
nvgRect(vgId, 20, h - 60, 100 * player.getHealth(), 20);
nvgRect(vgId, w - 170, h - 110, 100 * player.getHealth(), 20);
nvgFill(vgId);
nvgFillColor(vgId, GuiUtils.rgba(1, 1, 1, 1, colorA));
nvgFontSize(vgId, 12f);
nvgFontFaceId(vgId, jetbrainsMonoFont);
nvgTextAlign(vgId, NVG_ALIGN_LEFT | NVG_ALIGN_TOP);
nvgText(vgId, 20, h - 30, String.format("%.2f / 1.00 HP", player.getHealth()));
nvgText(vgId, w - 170, h - 80, String.format("%.2f / 1.00 HP", player.getHealth()));
}
private void drawHeldItemStackInfo(float w, float h, ClientPlayer player) {
@ -324,19 +324,20 @@ public class GuiRenderer {
nvgText(vgId, w - 140, h - 14, String.format("Selected value: %d", stack.getSelectedValue()));
}
private void drawChat(float w, float h, Chat chat) {
private void drawChat(float w, float h, Client client) {
float chatWidth = w / 3;
float chatHeight = h / 4;
nvgFillColor(vgId, GuiUtils.rgba(0, 0, 0, 0.25f, colorA));
nvgBeginPath(vgId);
nvgRect(vgId, 0, 0, chatWidth, chatHeight);
nvgRect(vgId, 0, h - chatHeight - 16, chatWidth, chatHeight);
nvgFill(vgId);
var chat = client.getChat();
nvgFontSize(vgId, 12f);
nvgFontFaceId(vgId, jetbrainsMonoFont);
nvgTextAlign(vgId, NVG_ALIGN_LEFT | NVG_ALIGN_TOP);
float y = chatHeight - 12;
float y = h - 16 - 12;
for (var msg : chat.getMessages()) {
if (msg.author().equals("_ANNOUNCE")) {
nvgFillColor(vgId, GuiUtils.rgba(0.7f, 0, 0, 1, colorA));
@ -348,9 +349,17 @@ public class GuiRenderer {
nvgFillColor(vgId, GuiUtils.rgba(1, 1, 1, 1, colorA));
nvgText(vgId, 5, y, msg.author() + ": " + msg.message());
}
y -= 16;
}
var input = client.getInputHandler();
if (input.isChatting()) {
nvgFillColor(vgId, GuiUtils.rgba(0, 0, 0, 0.5f, colorA));
nvgBeginPath(vgId);
nvgRect(vgId, 0, h - 16, w, 16);
nvgFill(vgId);
nvgFillColor(vgId, GuiUtils.rgba(1, 1, 1, 1, colorA));
nvgText(vgId, 5, h - 14, "> " + input.getChatText() + "_");
}
}
private void drawDebugInfo(float w, float h, Client client) {

View File

@ -48,6 +48,7 @@ public final class Net {
serializer.registerType(19, BlockColorMessage.class);
serializer.registerType(20, ChatMessage.class);
serializer.registerType(21, ChatWrittenMessage.class);
serializer.registerType(22, ClientOrientationUpdateMessage.class);
}
public static ExtendedDataInputStream getInputStream(InputStream in) {

View File

@ -14,7 +14,7 @@ public class Ak47 extends Gun {
0.1f,
1.2f,
0.4f,
30f,
0.1f,
true
);
}

View File

@ -7,14 +7,14 @@ public class Rifle extends Gun {
super(
id,
"Rifle",
5,
6,
8,
1,
0.97f,
0.98f,
0.8f,
2.5f,
0.8f,
50f,
0.2f,
false
);
}

View File

@ -7,14 +7,14 @@ public class Winchester extends Gun {
super(
id,
"Winchester",
10,
6,
4,
4,
0.85f,
0.75f,
2.5f,
0.3f,
60f,
0.33f,
false
);
}

View File

@ -4,6 +4,6 @@ appender.console.name = STDOUT
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = [%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n
rootLogger.level = debug
rootLogger.level = info
rootLogger.appenderRefs = stdout
rootLogger.appenderRef.stdout.ref = STDOUT

View File

@ -7,6 +7,8 @@ import nl.andrewl.aos_core.model.item.ItemStack;
import nl.andrewl.aos_core.model.world.Chunk;
import nl.andrewl.aos_core.model.world.WorldIO;
import nl.andrewl.aos_core.net.TcpReceiver;
import nl.andrewl.aos_core.net.client.ChatMessage;
import nl.andrewl.aos_core.net.client.ChatWrittenMessage;
import nl.andrewl.aos_core.net.connect.ConnectAcceptMessage;
import nl.andrewl.aos_core.net.connect.ConnectRejectMessage;
import nl.andrewl.aos_core.net.connect.ConnectRequestMessage;
@ -77,6 +79,25 @@ public class ClientCommunicationHandler {
if (chunk != null && hashMessage.hash() != chunk.blockHash()) {
sendTcpMessage(new ChunkDataMessage(chunk));
}
} else if (msg instanceof ChatWrittenMessage chatWrittenMessage) {
if (chatWrittenMessage.message().startsWith("/t ")) {
if (player.getTeam() != null) {
var chat = new ChatMessage(
System.currentTimeMillis(),
player.getUsername(),
chatWrittenMessage.message().substring(3)
);
for (var teamPlayer : server.getTeamManager().getPlayers(player.getTeam())) {
server.getPlayerManager().getHandler(teamPlayer).sendTcpMessage(chat);
}
}
} else {
server.getPlayerManager().broadcastTcpMessage(new ChatMessage(
System.currentTimeMillis(),
player.getUsername(),
chatWrittenMessage.message()
));
}
}
}
@ -110,6 +131,10 @@ public class ClientCommunicationHandler {
log.debug("Sent connect accept message.");
sendInitialData();
log.debug("Sent initial data.");
sendTcpMessage(ChatMessage.privateMessage("Welcome to the server, " + player.getUsername() + "."));
if (player.getTeam() != null) {
sendTcpMessage(ChatMessage.privateMessage("You've joined the " + player.getTeam().getName() + " team."));
}
// Initiate a TCP receiver thread to accept incoming messages from the client.
TcpReceiver tcpReceiver = new TcpReceiver(in, this::handleTcpMessage)
.withShutdownHook(() -> server.getPlayerManager().deregister(this.player));
@ -135,10 +160,12 @@ public class ClientCommunicationHandler {
public void sendTcpMessage(Message msg) {
ForkJoinPool.commonPool().submit(() -> {
try {
Net.write(msg, out);
} catch (IOException e) {
e.printStackTrace();
synchronized (out) {
try {
Net.write(msg, out);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}

View File

@ -111,6 +111,16 @@ public class PlayerActionManager {
gunNeedsReCock = true;
}
server.getPlayerManager().getHandler(player.getId()).sendDatagramPacket(new ItemStackMessage(player.getInventory()));
// Apply recoil!
float recoilFactor = 10f; // Maximum number of degrees to recoil.
float recoil = recoilFactor * gun.getRecoil() + (float) ThreadLocalRandom.current().nextGaussian(0, 0.01);
player.getOrientation().y += Math.toRadians(recoil);
server.getPlayerManager().getHandler(player.getId()).sendDatagramPacket(new ClientOrientationUpdateMessage(
player.getOrientation().x(),
player.getOrientation().y()
));
server.getPlayerManager().broadcastUdpMessageToAllBut(player.getUpdateMessage(now), player);
// Play sound!
String shotSound = null;
if (gun instanceof Rifle) {
shotSound = "shot_m1-garand_1";

View File

@ -2,6 +2,11 @@ package nl.andrewl.aos2_server.logic;
import nl.andrewl.aos_core.net.client.ClientInputState;
/**
* Component that keeps track of any impulses the player has made since the
* last tick. This way, if a player activates an input for even a fraction of
* a tick, it'll register.
*/
public class PlayerImpulses {
public boolean forward;
public boolean backward;