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.model.OtherPlayer;
import nl.andrewl.aos2_client.render.GameRenderer; import nl.andrewl.aos2_client.render.GameRenderer;
import nl.andrewl.aos2_client.sound.SoundManager; 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.config.Config;
import nl.andrewl.aos_core.model.Player; import nl.andrewl.aos_core.model.Player;
import nl.andrewl.aos_core.model.Projectile; import nl.andrewl.aos_core.model.Projectile;
@ -137,7 +136,7 @@ public class Client implements Runnable {
gameRenderer.getCamera().setToPlayer(myPlayer); gameRenderer.getCamera().setToPlayer(myPlayer);
} }
if (soundManager != null) { if (soundManager != null) {
soundManager.updateListener(myPlayer.getPosition(), myPlayer.getVelocity()); soundManager.updateListener(myPlayer.getEyePosition(), myPlayer.getVelocity());
} }
lastPlayerUpdate = playerUpdate.timestamp(); lastPlayerUpdate = playerUpdate.timestamp();
} else { } else {
@ -206,7 +205,7 @@ public class Client implements Runnable {
} else if (msg instanceof ChatMessage chatMessage) { } else if (msg instanceof ChatMessage chatMessage) {
chat.chatReceived(chatMessage); chat.chatReceived(chatMessage);
if (soundManager != null) { 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 { public static void main(String[] args) throws IOException {
List<Path> configPaths = Config.getCommonConfigPaths(); 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) { if (args.length > 0) {
configPaths.add(Path.of(args[0].trim())); 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 client = new Client(clientConfig);
client.run(); client.run();
} }

View File

@ -1,5 +1,6 @@
package nl.andrewl.aos_core.config; package nl.andrewl.aos_core.config;
import nl.andrewl.aos_core.FileUtils;
import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.Yaml;
import java.io.IOException; import java.io.IOException;
@ -19,7 +20,7 @@ public final class Config {
* @param paths The paths to load from. * @param paths The paths to load from.
* @param fallback A default configuration object to use if no config could * @param fallback A default configuration object to use if no config could
* be loaded from any of the paths. * 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. * @return The configuration object.
* @param <T> The type of 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"); Path outputPath = paths.size() > 0 ? paths.get(0) : Path.of("config.yaml");
try (var writer = Files.newBufferedWriter(outputPath)) { try (var writer = Files.newBufferedWriter(outputPath)) {
writer.write(defaultConfigFile); writer.write(FileUtils.readClasspathFile(defaultConfigFile));
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
return fallback; 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() { public static List<Path> getCommonConfigPaths() {
List<Path> paths = new ArrayList<>(); List<Path> paths = new ArrayList<>();
paths.add(Path.of("config.yaml")); paths.add(Path.of("config.yaml"));

View File

@ -17,7 +17,7 @@ import java.util.Map;
* that players can interact in. * that players can interact in.
*/ */
public class World { 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 final Map<Vector3ic, Chunk> chunkMap = new HashMap<>();
protected ColorPalette palette; protected ColorPalette palette;
@ -164,8 +164,12 @@ public class World {
public Hit getLookingAtPos(Vector3f eyePos, Vector3f eyeDir, float limit) { public Hit getLookingAtPos(Vector3f eyePos, Vector3f eyeDir, float limit) {
if (eyeDir.lengthSquared() == 0 || limit <= 0) return null; if (eyeDir.lengthSquared() == 0 || limit <= 0) return null;
Vector3f pos = new Vector3f(eyePos); Vector3f pos = new Vector3f(eyePos);
Vector3f previousPos = new Vector3f();
while (pos.distance(eyePos) < limit) { while (pos.distance(eyePos) < limit) {
previousPos.set(pos);
stepToNextBlock(pos, eyeDir); 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) { if (getBlockAt(pos) > 0) {
Vector3i hitPos = new Vector3i( Vector3i hitPos = new Vector3i(
(int) Math.floor(pos.x), (int) Math.floor(pos.x),
@ -234,7 +238,7 @@ public class World {
// Testing code! // Testing code!
if (diff == 0) { if (diff == 0) {
System.out.printf("n = %.8f, nextValue = %.8f, floor(n) - DELTA = %.8f%n", n, nextValue, Math.floor(n) - DELTA); 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); 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 { public static void main(String[] args) throws IOException {
List<Path> configPaths = Config.getCommonConfigPaths(); List<Path> configPaths = Config.getCommonConfigPaths();
configPaths.add(0, Path.of("server.yaml"));
if (args.length > 0) { if (args.length > 0) {
configPaths.add(Path.of(args[0].trim())); 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); Server server = new Server(cfg);
new Thread(server).start(); new Thread(server).start();
ServerCli.start(server); ServerCli.start(server);

View File

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