Added better client input handling.

This commit is contained in:
Andrew Lalis 2022-07-28 09:01:16 +02:00
parent 7df8120c16
commit e321979c3c
10 changed files with 212 additions and 56 deletions

View File

@ -7,7 +7,6 @@ import nl.andrewl.aos2_client.model.ClientPlayer;
import nl.andrewl.aos2_client.model.OtherPlayer;
import nl.andrewl.aos2_client.render.GameRenderer;
import nl.andrewl.aos2_client.sound.SoundManager;
import nl.andrewl.aos_core.FileUtils;
import nl.andrewl.aos_core.config.Config;
import nl.andrewl.aos_core.model.Player;
import nl.andrewl.aos_core.model.Projectile;
@ -137,7 +136,7 @@ public class Client implements Runnable {
gameRenderer.getCamera().setToPlayer(myPlayer);
}
if (soundManager != null) {
soundManager.updateListener(myPlayer.getPosition(), myPlayer.getVelocity());
soundManager.updateListener(myPlayer.getEyePosition(), myPlayer.getVelocity());
}
lastPlayerUpdate = playerUpdate.timestamp();
} else {
@ -206,7 +205,7 @@ public class Client implements Runnable {
} else if (msg instanceof ChatMessage chatMessage) {
chat.chatReceived(chatMessage);
if (soundManager != null) {
soundManager.play("chat", 1, myPlayer.getPosition(), myPlayer.getVelocity());
soundManager.play("chat", 1, myPlayer.getEyePosition(), myPlayer.getVelocity());
}
}
}
@ -268,10 +267,11 @@ public class Client implements Runnable {
public static void main(String[] args) throws IOException {
List<Path> configPaths = Config.getCommonConfigPaths();
configPaths.add(0, Path.of("client.yaml")); // Add this first so we create client.yaml if needed.
if (args.length > 0) {
configPaths.add(Path.of(args[0].trim()));
}
ClientConfig clientConfig = Config.loadConfig(ClientConfig.class, configPaths, new ClientConfig(), FileUtils.readClasspathFile("default-config.yaml"));
ClientConfig clientConfig = Config.loadConfig(ClientConfig.class, configPaths, new ClientConfig(), "default-config.yaml");
Client client = new Client(clientConfig);
client.run();
}

View File

@ -1,5 +1,6 @@
package nl.andrewl.aos_core.config;
import nl.andrewl.aos_core.FileUtils;
import org.yaml.snakeyaml.Yaml;
import java.io.IOException;
@ -19,7 +20,7 @@ public final class Config {
* @param paths The paths to load from.
* @param fallback A default configuration object to use if no config could
* be loaded from any of the paths.
* @param defaultConfigFile The default config file to save.
* @param defaultConfigFile The default config file resource to save.
* @return The configuration object.
* @param <T> The type of the configuration object.
*/
@ -35,25 +36,13 @@ public final class Config {
}
Path outputPath = paths.size() > 0 ? paths.get(0) : Path.of("config.yaml");
try (var writer = Files.newBufferedWriter(outputPath)) {
writer.write(defaultConfigFile);
writer.write(FileUtils.readClasspathFile(defaultConfigFile));
} catch (IOException e) {
e.printStackTrace();
}
return fallback;
}
public static <T> T loadConfig(Class<T> configType, List<Path> paths, String defaultConfigFile) {
var cfg = loadConfig(configType, paths, null, defaultConfigFile);
if (cfg == null) {
throw new RuntimeException("Could not load config from any of the supplied paths.");
}
return cfg;
}
public static <T> T loadConfig(Class<T> configType, T fallback, String defaultConfigFile, Path... paths) {
return loadConfig(configType, List.of(paths), fallback, defaultConfigFile);
}
public static List<Path> getCommonConfigPaths() {
List<Path> paths = new ArrayList<>();
paths.add(Path.of("config.yaml"));

View File

@ -17,7 +17,7 @@ import java.util.Map;
* that players can interact in.
*/
public class World {
private static final float DELTA = 0.01f;
private static final float DELTA = 0.001f;
protected final Map<Vector3ic, Chunk> chunkMap = new HashMap<>();
protected ColorPalette palette;
@ -164,8 +164,12 @@ public class World {
public Hit getLookingAtPos(Vector3f eyePos, Vector3f eyeDir, float limit) {
if (eyeDir.lengthSquared() == 0 || limit <= 0) return null;
Vector3f pos = new Vector3f(eyePos);
Vector3f previousPos = new Vector3f();
while (pos.distance(eyePos) < limit) {
previousPos.set(pos);
stepToNextBlock(pos, eyeDir);
// If for some reason we couldn't advance to the next block, exit null, so we don't infinitely loop.
if (pos.equals(previousPos)) return null;
if (getBlockAt(pos) > 0) {
Vector3i hitPos = new Vector3i(
(int) Math.floor(pos.x),
@ -234,7 +238,7 @@ public class World {
// Testing code!
if (diff == 0) {
System.out.printf("n = %.8f, nextValue = %.8f, floor(n) - DELTA = %.8f%n", n, nextValue, Math.floor(n) - DELTA);
throw new RuntimeException("EEK");
return Float.MAX_VALUE;
}
return Math.abs(diff / dir);
}

View File

@ -0,0 +1,11 @@
package nl.andrewl.aos_core.net.client;
import nl.andrewl.record_net.Message;
/**
* A message that the server sends to clients, to tell them to update their
* player's orientation to the specified values.
*/
public record ClientOrientationUpdateMessage(
float x, float y
) implements Message {}

View File

@ -177,10 +177,11 @@ public class Server implements Runnable {
public static void main(String[] args) throws IOException {
List<Path> configPaths = Config.getCommonConfigPaths();
configPaths.add(0, Path.of("server.yaml"));
if (args.length > 0) {
configPaths.add(Path.of(args[0].trim()));
}
ServerConfig cfg = Config.loadConfig(ServerConfig.class, configPaths, new ServerConfig(), null);
ServerConfig cfg = Config.loadConfig(ServerConfig.class, configPaths, new ServerConfig(), "default-config.yaml");
Server server = new Server(cfg);
new Thread(server).start();
ServerCli.start(server);

View File

@ -29,8 +29,8 @@ import static nl.andrewl.aos2_server.model.ServerPlayer.RADIUS;
*/
public class PlayerActionManager {
private final ServerPlayer player;
private final PlayerInputTracker input;
private ClientInputState lastInputState;
private long lastBlockRemovedAt = 0;
private long lastBlockPlacedAt = 0;
@ -45,23 +45,15 @@ public class PlayerActionManager {
public PlayerActionManager(ServerPlayer player) {
this.player = player;
lastInputState = new ClientInputState(
player.getId(),
false, false, false, false,
false, false, false,
false, false, false,
player.getInventory().getSelectedIndex()
);
this.input = new PlayerInputTracker(player);
}
public ClientInputState getLastInputState() {
return lastInputState;
public PlayerInputTracker getInput() {
return input;
}
public boolean setLastInputState(ClientInputState lastInputState) {
boolean change = !lastInputState.equals(this.lastInputState);
if (change) this.lastInputState = lastInputState;
return change;
return input.setLastInputState(lastInputState);
}
public boolean isUpdated() {
@ -70,8 +62,8 @@ public class PlayerActionManager {
public void tick(long now, float dt, World world, Server server) {
updated = false; // Reset the updated flag. This will be set to true if the player was updated in this tick.
if (player.getInventory().getSelectedIndex() != lastInputState.selectedInventoryIndex()) {
player.getInventory().setSelectedIndex(lastInputState.selectedInventoryIndex());
if (player.getInventory().getSelectedIndex() != input.selectedInventoryIndex()) {
player.getInventory().setSelectedIndex(input.selectedInventoryIndex());
// Tell the client that their inventory slot has been updated properly.
server.getPlayerManager().getHandler(player.getId()).sendDatagramPacket(new InventorySelectedStackMessage(player.getInventory().getSelectedIndex()));
updated = true; // Tell everyone else that this player's selected item has changed.
@ -81,7 +73,7 @@ public class PlayerActionManager {
if (selectedStack instanceof BlockItemStack b) {
tickBlockAction(now, server, world, b);
} else if (selectedStack instanceof GunItemStack g) {
tickGunAction(now, server, world, g);
tickGunAction(now, server, g);
}
if (
@ -93,18 +85,19 @@ public class PlayerActionManager {
lastResupplyAt = now;
}
if (player.isCrouching() != lastInputState.crouching()) {
player.setCrouching(lastInputState.crouching());
if (player.isCrouching() != input.crouching()) {
player.setCrouching(input.crouching());
updated = true;
}
tickMovement(dt, server, world, server.getConfig().physics);
input.reset();
}
private void tickGunAction(long now, Server server, World world, GunItemStack g) {
private void tickGunAction(long now, Server server, GunItemStack g) {
Gun gun = (Gun) g.getType();
if (// Check to see if the player is shooting.
lastInputState.hitting() &&
input.hitting() &&
g.getBulletCount() > 0 &&
!gunReloading &&
now - gunLastShotAt > gun.getShotCooldownTime() * 1000 &&
@ -126,11 +119,14 @@ public class PlayerActionManager {
} else if (gun instanceof Winchester) {
shotSound = "shot_winchester_1";
}
server.getPlayerManager().broadcastUdpMessage(new SoundMessage(shotSound, 1, player.getPosition(), player.getVelocity()));
Vector3f soundLocation = new Vector3f(player.getPosition());
soundLocation.y += 1.4f;
soundLocation.add(player.getViewVector());
server.getPlayerManager().broadcastUdpMessage(new SoundMessage(shotSound, 1, soundLocation, player.getVelocity()));
}
if (// Check to see if the player is reloading.
lastInputState.reloading() &&
input.reloading() &&
!gunReloading &&
g.getClipCount() > 0
) {
@ -151,7 +147,7 @@ public class PlayerActionManager {
}
// Check to see if the player released the trigger, for non-automatic weapons.
if (!gun.isAutomatic() && gunNeedsReCock && !lastInputState.hitting()) {
if (!gun.isAutomatic() && gunNeedsReCock && !input.hitting()) {
gunNeedsReCock = false;
}
}
@ -159,7 +155,7 @@ public class PlayerActionManager {
private void tickBlockAction(long now, Server server, World world, BlockItemStack stack) {
// Check for breaking blocks.
if (
lastInputState.hitting() &&
input.hitting() &&
stack.getAmount() < stack.getType().getMaxAmount() &&
now - lastBlockRemovedAt > server.getConfig().actions.blockBreakCooldown * 1000
) {
@ -175,7 +171,7 @@ public class PlayerActionManager {
}
// Check for placing blocks.
if (
lastInputState.interacting() &&
input.interacting() &&
stack.getAmount() > 0 &&
now - lastBlockPlacedAt > server.getConfig().actions.blockPlaceCooldown * 1000
) {
@ -202,8 +198,8 @@ public class PlayerActionManager {
tickHorizontalVelocity(config, grounded);
if (isGrounded(world)) {
if (lastInputState.jumping()) {
velocity.y = config.jumpVerticalSpeed * (lastInputState.sprinting() ? 1.25f : 1f);
if (input.jumping()) {
velocity.y = config.jumpVerticalSpeed * (input.sprinting() ? 1.25f : 1f);
updated = true;
}
} else {
@ -239,19 +235,19 @@ public class PlayerActionManager {
velocity.z == velocity.z ? velocity.z : 0f
);
Vector3f acceleration = new Vector3f(0);
if (lastInputState.forward()) acceleration.z -= 1;
if (lastInputState.backward()) acceleration.z += 1;
if (lastInputState.left()) acceleration.x -= 1;
if (lastInputState.right()) acceleration.x += 1;
if (input.forward()) acceleration.z -= 1;
if (input.backward()) acceleration.z += 1;
if (input.left()) acceleration.x -= 1;
if (input.right()) acceleration.x += 1;
if (acceleration.lengthSquared() > 0) {
acceleration.normalize();
acceleration.rotateAxis(orientation.x, 0, 1, 0);
acceleration.mul(config.movementAcceleration);
horizontalVelocity.add(acceleration);
final float maxSpeed;
if (lastInputState.crouching()) {
if (input.crouching()) {
maxSpeed = config.crouchingSpeed;
} else if (lastInputState.sprinting()) {
} else if (input.sprinting()) {
maxSpeed = config.sprintingSpeed;
} else {
maxSpeed = config.walkingSpeed;

View File

@ -0,0 +1,42 @@
package nl.andrewl.aos2_server.logic;
import nl.andrewl.aos_core.net.client.ClientInputState;
public class PlayerImpulses {
public boolean forward;
public boolean backward;
public boolean left;
public boolean right;
public boolean jumping;
public boolean crouching;
public boolean sprinting;
public boolean hitting;
public boolean interacting;
public boolean reloading;
public void update(ClientInputState s) {
forward = forward || s.forward();
backward = backward || s.backward();
left = left || s.left();
right = right || s.right();
jumping = jumping || s.jumping();
crouching = crouching || s.crouching();
sprinting = sprinting || s.sprinting();
hitting = hitting || s.hitting();
interacting = interacting || s.interacting();
reloading = reloading || s.reloading();
}
public void reset() {
forward = false;
backward = false;
left = false;
right = false;
jumping = false;
crouching = false;
sprinting = false;
hitting = false;
interacting = false;
reloading = false;
}
}

View File

@ -0,0 +1,82 @@
package nl.andrewl.aos2_server.logic;
import nl.andrewl.aos2_server.model.ServerPlayer;
import nl.andrewl.aos_core.net.client.ClientInputState;
/**
* Wrapper around the various information we have about a player's input state,
* including their last known state, and any impulses they've made since the
* last tick.
*/
public class PlayerInputTracker {
private ClientInputState lastInputState;
private final PlayerImpulses impulsesSinceLastTick;
public PlayerInputTracker(ServerPlayer player) {
lastInputState = new ClientInputState(
player.getId(),
false, false, false, false,
false, false, false,
false, false, false,
player.getInventory().getSelectedIndex()
);
this.impulsesSinceLastTick = new PlayerImpulses();
}
public boolean setLastInputState(ClientInputState lastInputState) {
boolean updated = !lastInputState.equals(this.lastInputState);
if (updated) {
this.lastInputState = lastInputState;
impulsesSinceLastTick.update(lastInputState);
}
return updated;
}
public void reset() {
impulsesSinceLastTick.reset();
}
public boolean forward() {
return lastInputState.forward() || impulsesSinceLastTick.forward;
}
public boolean backward() {
return lastInputState.backward() || impulsesSinceLastTick.backward;
}
public boolean left() {
return lastInputState.left() || impulsesSinceLastTick.left;
}
public boolean right() {
return lastInputState.right() || impulsesSinceLastTick.right;
}
public boolean jumping() {
return lastInputState.jumping() || impulsesSinceLastTick.jumping;
}
public boolean crouching() {
return lastInputState.crouching() || impulsesSinceLastTick.crouching;
}
public boolean sprinting() {
return lastInputState.sprinting() || impulsesSinceLastTick.sprinting;
}
public boolean hitting() {
return lastInputState.hitting() || impulsesSinceLastTick.hitting;
}
public boolean interacting() {
return lastInputState.interacting() || impulsesSinceLastTick.interacting;
}
public boolean reloading() {
return lastInputState.reloading() || impulsesSinceLastTick.reloading;
}
public int selectedInventoryIndex() {
return lastInputState.selectedInventoryIndex();
}
}

View File

@ -6,7 +6,6 @@ import nl.andrewl.aos_core.model.item.BlockItemStack;
import nl.andrewl.aos_core.model.item.GunItemStack;
import nl.andrewl.aos_core.model.item.Inventory;
import nl.andrewl.aos_core.model.item.ItemTypes;
import nl.andrewl.aos_core.model.item.gun.Winchester;
import nl.andrewl.aos_core.net.client.PlayerUpdateMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -69,7 +68,7 @@ public class ServerPlayer extends Player {
position.x, position.y, position.z,
velocity.x, velocity.y, velocity.z,
orientation.x, orientation.y,
actionManager.getLastInputState().crouching(),
actionManager.getInput().crouching(),
inventory.getSelectedItemStack().getType().getId()
);
}

View File

@ -0,0 +1,32 @@
# Ace of Shades 2 Server Configuration
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