Updated server-side logic and added better inventory models.

This commit is contained in:
Andrew Lalis 2022-07-18 01:10:29 +02:00
parent 758372108b
commit d83ff8a816
16 changed files with 536 additions and 349 deletions

View File

@ -102,9 +102,9 @@ public class Player {
public Vector3f getRightVector() { public Vector3f getRightVector() {
float x = orientation.x - (float) (Math.PI / 2); float x = orientation.x - (float) (Math.PI / 2);
return new Vector3f( return new Vector3f(
sin(orientation.x), sin(x),
0, 0,
cos(orientation.x) cos(x)
).normalize(); ).normalize();
} }
} }

View File

@ -0,0 +1,11 @@
package nl.andrewl.aos_core.model.item;
/**
* A block item that contains information about what type of block value is
* currently selected.
*/
public class BlockItem extends Item {
public BlockItem(int id) {
super(id, "Block", 100);
}
}

View File

@ -0,0 +1,13 @@
package nl.andrewl.aos_core.model.item;
public class BlockItemStack extends ItemStack {
private int selectedValue = 1;
public BlockItemStack(BlockItem item, int amount) {
super(item, amount);
}
public int getSelectedValue() {
return selectedValue;
}
}

View File

@ -0,0 +1,72 @@
package nl.andrewl.aos_core.model.item;
import nl.andrewl.aos_core.MathUtils;
/**
* The base class for all types of guns.
*/
public class Gun extends Item {
private final int maxClipCount;
private final int maxBulletCount;
private final int bulletsPerRound;
private final float accuracy;
private final float shotCooldownTime;
private final float reloadTime;
private final float baseDamage;
private final float recoil;
public Gun(
int id,
String name,
int maxClipCount,
int maxBulletCount,
int bulletsPerRound,
float accuracy,
float shotCooldownTime,
float reloadTime,
float baseDamage,
float recoil
) {
super(id, name, 1);
this.maxClipCount = maxClipCount;
this.maxBulletCount = maxBulletCount;
this.bulletsPerRound = bulletsPerRound;
this.accuracy = MathUtils.clamp(accuracy, 0, 1);
this.shotCooldownTime = shotCooldownTime;
this.reloadTime = reloadTime;
this.baseDamage = baseDamage;
this.recoil = recoil;
}
public int getMaxClipCount() {
return maxClipCount;
}
public int getMaxBulletCount() {
return maxBulletCount;
}
public int getBulletsPerRound() {
return bulletsPerRound;
}
public float getAccuracy() {
return accuracy;
}
public float getShotCooldownTime() {
return shotCooldownTime;
}
public float getReloadTime() {
return reloadTime;
}
public float getBaseDamage() {
return baseDamage;
}
public float getRecoil() {
return recoil;
}
}

View File

@ -0,0 +1,12 @@
package nl.andrewl.aos_core.model.item;
public class GunItemStack extends ItemStack {
private int bulletCount;
private int clipCount;
public GunItemStack(Gun gun) {
super(gun, 1);
bulletCount = gun.getMaxBulletCount();
clipCount = gun.getMaxClipCount();
}
}

View File

@ -21,6 +21,10 @@ public class Inventory {
this.selectedIndex = selectedIndex; this.selectedIndex = selectedIndex;
} }
public List<ItemStack> getItemStacks() {
return itemStacks;
}
public ItemStack getSelectedItemStack() { public ItemStack getSelectedItemStack() {
return itemStacks.get(selectedIndex); return itemStacks.get(selectedIndex);
} }

View File

@ -3,12 +3,23 @@ package nl.andrewl.aos_core.model.item;
/** /**
* Represents a type of item that a player can have. * Represents a type of item that a player can have.
*/ */
public class ItemType { public class Item {
private final int id; /**
private final String name; * The item's unique id.
private final int maxAmount; */
protected final int id;
public ItemType(int id, String name, int maxAmount) { /**
* The item's unique name.
*/
protected final String name;
/**
* The maximum amount of this item that can be in a stack at once.
*/
protected final int maxAmount;
public Item(int id, String name, int maxAmount) {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.maxAmount = maxAmount; this.maxAmount = maxAmount;

View File

@ -5,15 +5,15 @@ package nl.andrewl.aos_core.model.item;
* a type of item, and the amount of it. * a type of item, and the amount of it.
*/ */
public class ItemStack { public class ItemStack {
private final ItemType type; private final Item type;
private int amount; private int amount;
public ItemStack(ItemType type, int amount) { public ItemStack(Item type, int amount) {
this.type = type; this.type = type;
this.amount = amount; this.amount = amount;
} }
public Object getType() { public Item getType() {
return type; return type;
} }

View File

@ -1,5 +1,7 @@
package nl.andrewl.aos_core.model.item; package nl.andrewl.aos_core.model.item;
import nl.andrewl.aos_core.model.item.gun.Rifle;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -7,14 +9,26 @@ import java.util.Map;
* Global constant set of registered item types. * Global constant set of registered item types.
*/ */
public final class ItemTypes { public final class ItemTypes {
public static final Map<Integer, ItemType> TYPES_MAP = new HashMap<>(); private static final Map<Integer, Item> TYPES_BY_ID = new HashMap<>();
private static final Map<String, Item> TYPES_BY_NAME = new HashMap<>();
static { static {
registerType(new ItemType(1, "Rifle", 1)); registerType(new BlockItem(1));
registerType(new ItemType(2, "Block", 100)); registerType(new Rifle(2));
} }
public static void registerType(ItemType type) { public static void registerType(Item type) {
TYPES_MAP.put(type.getId(), type); TYPES_BY_ID.put(type.getId(), type);
TYPES_BY_NAME.put(type.getName(), type);
}
@SuppressWarnings("unchecked")
public static <T extends Item> T get(int id) {
return (T) TYPES_BY_ID.get(id);
}
@SuppressWarnings("unchecked")
public static <T extends Item> T get(String name) {
return (T) TYPES_BY_NAME.get(name);
} }
} }

View File

@ -0,0 +1,20 @@
package nl.andrewl.aos_core.model.item.gun;
import nl.andrewl.aos_core.model.item.Gun;
public class Rifle extends Gun {
public Rifle(int id) {
super(
id,
"Rifle",
5,
8,
1,
0.97f,
0.8f,
2.5f,
80f,
50f
);
}
}

View File

@ -12,7 +12,8 @@ public record PlayerUpdateMessage(
float px, float py, float pz, float px, float py, float pz,
float vx, float vy, float vz, float vx, float vy, float vz,
float ox, float oy, float ox, float oy,
boolean crouching boolean crouching,
int selectedItemId
) implements Message { ) implements Message {
public void apply(Player p) { public void apply(Player p) {

View File

@ -4,7 +4,6 @@ import nl.andrewl.aos_core.Net;
import nl.andrewl.aos_core.net.PlayerJoinMessage; import nl.andrewl.aos_core.net.PlayerJoinMessage;
import nl.andrewl.aos_core.net.PlayerLeaveMessage; import nl.andrewl.aos_core.net.PlayerLeaveMessage;
import nl.andrewl.aos_core.net.udp.DatagramInit; import nl.andrewl.aos_core.net.udp.DatagramInit;
import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage;
import nl.andrewl.record_net.Message; import nl.andrewl.record_net.Message;
import org.joml.Vector3f; import org.joml.Vector3f;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -32,13 +31,7 @@ public class PlayerManager {
log.info("Registered player \"{}\" with id {}", player.getUsername(), player.getId()); log.info("Registered player \"{}\" with id {}", player.getUsername(), player.getId());
player.setPosition(new Vector3f(0, 64, 0)); player.setPosition(new Vector3f(0, 64, 0));
broadcastTcpMessageToAllBut(new PlayerJoinMessage(player), player); broadcastTcpMessageToAllBut(new PlayerJoinMessage(player), player);
broadcastUdpMessage(new PlayerUpdateMessage( broadcastUdpMessage(player.getUpdateMessage());
player.getId(),
player.getPosition().x, player.getPosition().y, player.getPosition().z,
player.getVelocity().x, player.getVelocity().y, player.getVelocity().z,
player.getOrientation().x, player.getOrientation().y,
player.getLastInputState().crouching()
));
return player; return player;
} }

View File

@ -1,6 +1,7 @@
package nl.andrewl.aos2_server; package nl.andrewl.aos2_server;
import nl.andrewl.aos2_server.config.ServerConfig; import nl.andrewl.aos2_server.config.ServerConfig;
import nl.andrewl.aos2_server.logic.WorldUpdater;
import nl.andrewl.aos_core.config.Config; import nl.andrewl.aos_core.config.Config;
import nl.andrewl.aos_core.model.world.World; import nl.andrewl.aos_core.model.world.World;
import nl.andrewl.aos_core.model.world.Worlds; import nl.andrewl.aos_core.model.world.Worlds;
@ -8,7 +9,6 @@ import nl.andrewl.aos_core.net.UdpReceiver;
import nl.andrewl.aos_core.net.udp.ClientInputState; import nl.andrewl.aos_core.net.udp.ClientInputState;
import nl.andrewl.aos_core.net.udp.ClientOrientationState; import nl.andrewl.aos_core.net.udp.ClientOrientationState;
import nl.andrewl.aos_core.net.udp.DatagramInit; import nl.andrewl.aos_core.net.udp.DatagramInit;
import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage;
import nl.andrewl.record_net.Message; import nl.andrewl.record_net.Message;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -66,26 +66,14 @@ public class Server implements Runnable {
} else if (msg instanceof ClientInputState inputState) { } else if (msg instanceof ClientInputState inputState) {
ServerPlayer player = playerManager.getPlayer(inputState.clientId()); ServerPlayer player = playerManager.getPlayer(inputState.clientId());
if (player != null) { if (player != null) {
player.setLastInputState(inputState); player.getActionManager().setLastInputState(inputState);
playerManager.broadcastUdpMessage(new PlayerUpdateMessage( playerManager.broadcastUdpMessage(player.getUpdateMessage());
player.getId(),
player.getPosition().x, player.getPosition().y, player.getPosition().z,
player.getVelocity().x, player.getVelocity().y, player.getVelocity().z,
player.getOrientation().x, player.getOrientation().y,
player.getLastInputState().crouching()
));
} }
} else if (msg instanceof ClientOrientationState orientationState) { } else if (msg instanceof ClientOrientationState orientationState) {
ServerPlayer player = playerManager.getPlayer(orientationState.clientId()); ServerPlayer player = playerManager.getPlayer(orientationState.clientId());
if (player != null) { if (player != null) {
player.setOrientation(orientationState.x(), orientationState.y()); player.setOrientation(orientationState.x(), orientationState.y());
playerManager.broadcastUdpMessageToAllBut(new PlayerUpdateMessage( playerManager.broadcastUdpMessageToAllBut(player.getUpdateMessage(), player);
player.getId(),
player.getPosition().x, player.getPosition().y, player.getPosition().z,
player.getVelocity().x, player.getVelocity().y, player.getVelocity().z,
player.getOrientation().x, player.getOrientation().y,
player.getLastInputState().crouching()
), player);
} }
} }
} }

View File

@ -1,14 +1,10 @@
package nl.andrewl.aos2_server; package nl.andrewl.aos2_server;
import nl.andrewl.aos2_server.config.ServerConfig; import nl.andrewl.aos2_server.logic.PlayerActionManager;
import nl.andrewl.aos_core.model.Player; import nl.andrewl.aos_core.model.Player;
import nl.andrewl.aos_core.model.world.World; import nl.andrewl.aos_core.model.item.*;
import nl.andrewl.aos_core.net.udp.ChunkUpdateMessage; import nl.andrewl.aos_core.model.item.gun.Rifle;
import nl.andrewl.aos_core.net.udp.ClientInputState; import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage;
import org.joml.Math;
import org.joml.Vector2i;
import org.joml.Vector3f;
import org.joml.Vector3i;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -25,303 +21,34 @@ public class ServerPlayer extends Player {
public static final float WIDTH = 0.75f; public static final float WIDTH = 0.75f;
public static final float RADIUS = WIDTH / 2f; public static final float RADIUS = WIDTH / 2f;
private ClientInputState lastInputState; private final PlayerActionManager actionManager;
private long lastBlockRemovedAt = 0; private final Inventory inventory;
private long lastBlockPlacedAt = 0;
private boolean updated = false;
public ServerPlayer(int id, String username) { public ServerPlayer(int id, String username) {
super(id, username); super(id, username);
// Initialize with a default state of no input. this.actionManager = new PlayerActionManager(this);
lastInputState = new ClientInputState(id, false, false, false, false, false, false, false, false, false); this.inventory = new Inventory(new ArrayList<>(), 0);
inventory.getItemStacks().add(new GunItemStack(ItemTypes.get("Rifle")));
inventory.getItemStacks().add(new BlockItemStack(ItemTypes.get("Block"), 50));
} }
public ClientInputState getLastInputState() { public PlayerActionManager getActionManager() {
return lastInputState; return actionManager;
}
public void setLastInputState(ClientInputState inputState) {
this.lastInputState = inputState;
}
public boolean isUpdated() {
return updated;
}
public void tick(float dt, World world, Server server) {
long now = System.currentTimeMillis();
// Check for breaking blocks.
if (lastInputState.hitting() && now - lastBlockRemovedAt > server.getConfig().actions.blockRemoveCooldown * 1000) {
Vector3f eyePos = new Vector3f(position);
eyePos.y += getEyeHeight();
var hit = world.getLookingAtPos(eyePos, viewVector, 10);
if (hit != null) {
world.setBlockAt(hit.pos().x, hit.pos().y, hit.pos().z, (byte) 0);
lastBlockRemovedAt = now;
server.getPlayerManager().broadcastUdpMessage(ChunkUpdateMessage.fromWorld(hit.pos(), world));
}
}
// Check for placing blocks.
if (lastInputState.interacting() && now - lastBlockPlacedAt > server.getConfig().actions.blockPlaceCooldown * 1000) {
Vector3f eyePos = new Vector3f(position);
eyePos.y += getEyeHeight();
var hit = world.getLookingAtPos(eyePos, viewVector, 10);
if (hit != null) {
Vector3i placePos = new Vector3i(hit.pos());
placePos.add(hit.norm());
world.setBlockAt(placePos.x, placePos.y, placePos.z, (byte) 1);
lastBlockPlacedAt = now;
server.getPlayerManager().broadcastUdpMessage(ChunkUpdateMessage.fromWorld(placePos, world));
}
}
tickMovement(dt, world, server.getConfig().physics);
}
private void tickMovement(float dt, World world, ServerConfig.PhysicsConfig config) {
updated = false; // Reset the updated flag. This will be set to true if the player was updated in this tick.
boolean grounded = isGrounded(world);
tickHorizontalVelocity(config, grounded);
if (isGrounded(world)) {
if (lastInputState.jumping()) {
velocity.y = config.jumpVerticalSpeed * (lastInputState.sprinting() ? 1.25f : 1f);
updated = true;
}
} else {
velocity.y -= config.gravity * dt;
updated = true;
}
// Apply updated velocity to the player.
if (velocity.lengthSquared() > 0) {
Vector3f movement = new Vector3f(velocity).mul(dt);
// Check for collisions if we try to move according to what the player wants.
checkBlockCollisions(movement, world);
position.add(movement);
updated = true;
}
}
private void tickHorizontalVelocity(ServerConfig.PhysicsConfig config, boolean doDeceleration) {
Vector3f horizontalVelocity = new Vector3f(
velocity.x == velocity.x ? velocity.x : 0f,
0,
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 (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()) {
maxSpeed = config.crouchingSpeed;
} else if (lastInputState.sprinting()) {
maxSpeed = config.sprintingSpeed;
} else {
maxSpeed = config.walkingSpeed;
}
if (horizontalVelocity.length() > maxSpeed) {
horizontalVelocity.normalize(maxSpeed);
}
updated = true;
} else if (doDeceleration && horizontalVelocity.lengthSquared() > 0) {
Vector3f deceleration = new Vector3f(horizontalVelocity)
.negate().normalize()
.mul(Math.min(horizontalVelocity.length(), config.movementDeceleration));
horizontalVelocity.add(deceleration);
if (horizontalVelocity.length() < 0.1f) {
horizontalVelocity.set(0);
}
updated = true;
}
// Update the player's velocity with what we've computed.
velocity.x = horizontalVelocity.x;
velocity.z = horizontalVelocity.z;
}
private boolean isGrounded(World world) {
// Player must be flat on the top of a block.
if (Math.floor(position.y) != position.y) return false;
// Check to see if there's a block under any of the spaces the player is over.
return getHorizontalSpaceOccupied(position).stream()
.anyMatch(point -> world.getBlockAt(point.x, position.y - 0.1f, point.y) != 0);
} }
/** /**
* Gets the list of all spaces occupied by a player's position, in the * Helper method to build an update message for this player, to be sent to
* horizontal XZ plane. This can be between 1 and 4 spaces, depending on * various clients.
* if the player's position is overlapping with a few blocks. * @return The update message.
* @param pos The position.
* @return The list of 2d positions occupied.
*/ */
private List<Vector2i> getHorizontalSpaceOccupied(Vector3f pos) { public PlayerUpdateMessage getUpdateMessage() {
// Get the list of 2d x,z coordinates that we overlap with. return new PlayerUpdateMessage(
List<Vector2i> points = new ArrayList<>(4); // Due to the size of radius, there can only be a max of 4 blocks. id,
int minX = (int) Math.floor(pos.x - RADIUS); position.x, position.y, position.z,
int minZ = (int) Math.floor(pos.z - RADIUS); velocity.x, velocity.y, velocity.z,
int maxX = (int) Math.floor(pos.x + RADIUS); orientation.x, orientation.y,
int maxZ = (int) Math.floor(pos.z + RADIUS); actionManager.getLastInputState().crouching(),
for (int x = minX; x <= maxX; x++) { inventory.getSelectedItemStack().getType().getId()
for (int z = minZ; z <= maxZ; z++) { );
points.add(new Vector2i(x, z));
} }
} }
return points;
}
private void checkBlockCollisions(Vector3f movement, World world) {
final Vector3f nextTickPosition = new Vector3f(position).add(movement);
// System.out.printf("Pos:\t\t%.3f, %.3f, %.3f%nmov:\t\t%.3f, %.3f, %.3f%nNexttick:\t%.3f, %.3f, %.3f%n",
// position.x, position.y, position.z,
// movement.x, movement.y, movement.z,
// nextTickPosition.x, nextTickPosition.y, nextTickPosition.z
// );
float height = getCurrentHeight();
float delta = 0.00001f;
final Vector3f stepSize = new Vector3f(movement).normalize(1.0f);
// The number of steps we'll make towards the next tick position.
int stepCount = (int) Math.ceil(movement.length());
if (stepCount == 0) return; // No movement, so exit.
final Vector3f nextPos = new Vector3f(position);
final Vector3f lastPos = new Vector3f(position);
for (int i = 0; i < stepCount; i++) {
lastPos.set(nextPos);
nextPos.add(stepSize);
// If we shot past the next tick position, clamp it to that.
if (new Vector3f(nextPos).sub(position).length() > movement.length()) {
nextPos.set(nextTickPosition);
}
// Check if we collide with anything at this new position.
float playerBodyPrevMinZ = lastPos.z - RADIUS;
float playerBodyPrevMaxZ = lastPos.z + RADIUS;
float playerBodyPrevMinX = lastPos.x - RADIUS;
float playerBodyPrevMaxX = lastPos.x + RADIUS;
float playerBodyPrevMinY = lastPos.y;
float playerBodyPrevMaxY = lastPos.y + height;
float playerBodyMinZ = nextPos.z - RADIUS;
float playerBodyMaxZ = nextPos.z + RADIUS;
float playerBodyMinX = nextPos.x - RADIUS;
float playerBodyMaxX = nextPos.x + RADIUS;
float playerBodyMinY = nextPos.y;
float playerBodyMaxY = nextPos.y + height;
// Compute the bounds of all blocks the player is intersecting with.
int minX = (int) Math.floor(playerBodyMinX);
int minZ = (int) Math.floor(playerBodyMinZ);
int minY = (int) Math.floor(playerBodyMinY);
int maxX = (int) Math.floor(playerBodyMaxX);
int maxZ = (int) Math.floor(playerBodyMaxZ);
int maxY = (int) Math.floor(playerBodyMaxY);
for (int x = minX; x <= maxX; x++) {
for (int z = minZ; z <= maxZ; z++) {
for (int y = minY; y <= maxY; y++) {
byte block = world.getBlockAt(x, y, z);
if (block <= 0) continue; // We're not colliding with this block.
float blockMinY = (float) y;
float blockMaxY = (float) y + 1;
float blockMinX = (float) x;
float blockMaxX = (float) x + 1;
float blockMinZ = (float) z;
float blockMaxZ = (float) z + 1;
/*
To determine if the player is moving into the -Z side of a block:
- The player's max z position went from < blockMinZ to >= blockMinZ.
- The block to the -Z direction is air.
*/
boolean collidingWithNegativeZ = playerBodyPrevMaxZ < blockMinZ && playerBodyMaxZ >= blockMinZ && world.getBlockAt(x, y, z - 1) <= 0;
if (collidingWithNegativeZ) {
position.z = blockMinZ - RADIUS - delta;
velocity.z = 0;
movement.z = 0;
}
/*
To determine if the player is moving into the +Z side of a block:
- The player's min z position went from >= blockMaxZ to < blockMaxZ.
- The block to the +Z direction is air.
*/
boolean collidingWithPositiveZ = playerBodyPrevMinZ >= blockMaxZ && playerBodyMinZ < blockMaxZ && world.getBlockAt(x, y, z + 1) <= 0;
if (collidingWithPositiveZ) {
position.z = blockMaxZ + RADIUS + delta;
velocity.z = 0;
movement.z = 0;
}
/*
To determine if the player is moving into the -X side of a block:
- The player's max x position went from < blockMinX to >= blockMinX
- The block to the -X direction is air.
*/
boolean collidingWithNegativeX = playerBodyPrevMaxX < blockMinX && playerBodyMaxX >= blockMinX && world.getBlockAt(x - 1, y, z) <= 0;
if (collidingWithNegativeX) {
position.x = blockMinX - RADIUS - delta;
velocity.x = 0;
movement.x = 0;
}
/*
To determine if the player is moving into the +X side of a block:
- The player's min x position went from >= blockMaxX to < blockMaxX.
- The block to the +X direction is air.
*/
boolean collidingWithPositiveX = playerBodyPrevMinX >= blockMaxX && playerBodyMinX < blockMaxX && world.getBlockAt(x + 1, y, z) <= 0;
if (collidingWithPositiveX) {
position.x = blockMaxX + RADIUS + delta;
velocity.x = 0;
movement.x = 0;
}
/*
To determine if the player is moving down onto a block:
- The player's min y position went from >= blockMaxY to < blockMaxY
- The block above the current one is air.
*/
boolean collidingWithFloor = playerBodyPrevMinY >= blockMaxY && playerBodyMinY < blockMaxY && world.getBlockAt(x, y + 1, z) <= 0;
if (collidingWithFloor) {
position.y = blockMaxY;
velocity.y = 0;
movement.y = 0;
}
/*
To determine if the player is moving up into a block:
- The player's y position went from below blockMinY to >= blockMinY
- The block below the current one is air.
*/
boolean collidingWithCeiling = playerBodyPrevMaxY < blockMinY && playerBodyMaxY >= blockMinY && world.getBlockAt(x, y - 1, z) <= 0;
if (collidingWithCeiling) {
position.y = blockMinY - height - delta;
velocity.y = 0;
movement.y = 0;
}
updated = true;
}
}
}
}
}
public float getCurrentHeight() {
return lastInputState.crouching() ? HEIGHT_CROUCH : HEIGHT;
}
public float getEyeHeight() {
return lastInputState.crouching() ? EYE_HEIGHT_CROUCH : EYE_HEIGHT;
}
}

View File

@ -0,0 +1,329 @@
package nl.andrewl.aos2_server.logic;
import nl.andrewl.aos2_server.Server;
import nl.andrewl.aos2_server.ServerPlayer;
import nl.andrewl.aos2_server.config.ServerConfig;
import nl.andrewl.aos_core.model.world.World;
import nl.andrewl.aos_core.net.udp.ChunkUpdateMessage;
import nl.andrewl.aos_core.net.udp.ClientInputState;
import org.joml.Math;
import org.joml.Vector2i;
import org.joml.Vector3f;
import org.joml.Vector3i;
import java.util.ArrayList;
import java.util.List;
import static nl.andrewl.aos2_server.ServerPlayer.*;
/**
* Component that manages a server player's current actions and movement.
*/
public class PlayerActionManager {
private final ServerPlayer player;
private ClientInputState lastInputState;
private long lastBlockRemovedAt = 0;
private long lastBlockPlacedAt = 0;
private boolean updated = false;
public PlayerActionManager(ServerPlayer player) {
this.player = player;
lastInputState = new ClientInputState(player.getId(), false, false, false, false, false, false, false, false, false);
}
public ClientInputState getLastInputState() {
return lastInputState;
}
public void setLastInputState(ClientInputState lastInputState) {
this.lastInputState = lastInputState;
}
public boolean isUpdated() {
return updated;
}
public void tick(float dt, World world, Server server) {
long now = System.currentTimeMillis();
// Check for breaking blocks.
if (lastInputState.hitting() && now - lastBlockRemovedAt > server.getConfig().actions.blockRemoveCooldown * 1000) {
Vector3f eyePos = new Vector3f(player.getPosition());
eyePos.y += getEyeHeight();
var hit = world.getLookingAtPos(eyePos, player.getViewVector(), 10);
if (hit != null) {
world.setBlockAt(hit.pos().x, hit.pos().y, hit.pos().z, (byte) 0);
lastBlockRemovedAt = now;
server.getPlayerManager().broadcastUdpMessage(ChunkUpdateMessage.fromWorld(hit.pos(), world));
}
}
// Check for placing blocks.
if (lastInputState.interacting() && now - lastBlockPlacedAt > server.getConfig().actions.blockPlaceCooldown * 1000) {
Vector3f eyePos = new Vector3f(player.getPosition());
eyePos.y += getEyeHeight();
var hit = world.getLookingAtPos(eyePos, player.getViewVector(), 10);
if (hit != null) {
Vector3i placePos = new Vector3i(hit.pos());
placePos.add(hit.norm());
world.setBlockAt(placePos.x, placePos.y, placePos.z, (byte) 1);
lastBlockPlacedAt = now;
server.getPlayerManager().broadcastUdpMessage(ChunkUpdateMessage.fromWorld(placePos, world));
}
}
tickMovement(dt, world, server.getConfig().physics);
}
private void tickMovement(float dt, World world, ServerConfig.PhysicsConfig config) {
updated = false; // Reset the updated flag. This will be set to true if the player was updated in this tick.
var velocity = player.getVelocity();
var position = player.getPosition();
boolean grounded = isGrounded(world);
tickHorizontalVelocity(config, grounded);
if (isGrounded(world)) {
if (lastInputState.jumping()) {
velocity.y = config.jumpVerticalSpeed * (lastInputState.sprinting() ? 1.25f : 1f);
updated = true;
}
} else {
velocity.y -= config.gravity * dt;
updated = true;
}
// Apply updated velocity to the player.
if (velocity.lengthSquared() > 0) {
Vector3f movement = new Vector3f(velocity).mul(dt);
// Check for collisions if we try to move according to what the player wants.
checkBlockCollisions(movement, world);
position.add(movement);
updated = true;
}
}
private void tickHorizontalVelocity(ServerConfig.PhysicsConfig config, boolean doDeceleration) {
var velocity = player.getVelocity();
var orientation = player.getOrientation();
Vector3f horizontalVelocity = new Vector3f(
velocity.x == velocity.x ? velocity.x : 0f,
0,
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 (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()) {
maxSpeed = config.crouchingSpeed;
} else if (lastInputState.sprinting()) {
maxSpeed = config.sprintingSpeed;
} else {
maxSpeed = config.walkingSpeed;
}
if (horizontalVelocity.length() > maxSpeed) {
horizontalVelocity.normalize(maxSpeed);
}
updated = true;
} else if (doDeceleration && horizontalVelocity.lengthSquared() > 0) {
Vector3f deceleration = new Vector3f(horizontalVelocity)
.negate().normalize()
.mul(Math.min(horizontalVelocity.length(), config.movementDeceleration));
horizontalVelocity.add(deceleration);
if (horizontalVelocity.length() < 0.1f) {
horizontalVelocity.set(0);
}
updated = true;
}
// Update the player's velocity with what we've computed.
velocity.x = horizontalVelocity.x;
velocity.z = horizontalVelocity.z;
}
private boolean isGrounded(World world) {
var position = player.getPosition();
// Player must be flat on the top of a block.
if (Math.floor(position.y) != position.y) return false;
// Check to see if there's a block under any of the spaces the player is over.
return getHorizontalSpaceOccupied(position).stream()
.anyMatch(point -> world.getBlockAt(point.x, position.y - 0.1f, point.y) != 0);
}
/**
* Gets the list of all spaces occupied by a player's position, in the
* horizontal XZ plane. This can be between 1 and 4 spaces, depending on
* if the player's position is overlapping with a few blocks.
* @param pos The position.
* @return The list of 2d positions occupied.
*/
private List<Vector2i> getHorizontalSpaceOccupied(Vector3f pos) {
// Get the list of 2d x,z coordinates that we overlap with.
List<Vector2i> points = new ArrayList<>(4); // Due to the size of radius, there can only be a max of 4 blocks.
int minX = (int) Math.floor(pos.x - RADIUS);
int minZ = (int) Math.floor(pos.z - RADIUS);
int maxX = (int) Math.floor(pos.x + RADIUS);
int maxZ = (int) Math.floor(pos.z + RADIUS);
for (int x = minX; x <= maxX; x++) {
for (int z = minZ; z <= maxZ; z++) {
points.add(new Vector2i(x, z));
}
}
return points;
}
private void checkBlockCollisions(Vector3f movement, World world) {
var position = player.getPosition();
var velocity = player.getVelocity();
final Vector3f nextTickPosition = new Vector3f(position).add(movement);
// System.out.printf("Pos:\t\t%.3f, %.3f, %.3f%nmov:\t\t%.3f, %.3f, %.3f%nNexttick:\t%.3f, %.3f, %.3f%n",
// position.x, position.y, position.z,
// movement.x, movement.y, movement.z,
// nextTickPosition.x, nextTickPosition.y, nextTickPosition.z
// );
float height = getCurrentHeight();
float delta = 0.00001f;
final Vector3f stepSize = new Vector3f(movement).normalize(1.0f);
// The number of steps we'll make towards the next tick position.
int stepCount = (int) Math.ceil(movement.length());
if (stepCount == 0) return; // No movement, so exit.
final Vector3f nextPos = new Vector3f(position);
final Vector3f lastPos = new Vector3f(position);
for (int i = 0; i < stepCount; i++) {
lastPos.set(nextPos);
nextPos.add(stepSize);
// If we shot past the next tick position, clamp it to that.
if (new Vector3f(nextPos).sub(position).length() > movement.length()) {
nextPos.set(nextTickPosition);
}
// Check if we collide with anything at this new position.
float playerBodyPrevMinZ = lastPos.z - RADIUS;
float playerBodyPrevMaxZ = lastPos.z + RADIUS;
float playerBodyPrevMinX = lastPos.x - RADIUS;
float playerBodyPrevMaxX = lastPos.x + RADIUS;
float playerBodyPrevMinY = lastPos.y;
float playerBodyPrevMaxY = lastPos.y + height;
float playerBodyMinZ = nextPos.z - RADIUS;
float playerBodyMaxZ = nextPos.z + RADIUS;
float playerBodyMinX = nextPos.x - RADIUS;
float playerBodyMaxX = nextPos.x + RADIUS;
float playerBodyMinY = nextPos.y;
float playerBodyMaxY = nextPos.y + height;
// Compute the bounds of all blocks the player is intersecting with.
int minX = (int) Math.floor(playerBodyMinX);
int minZ = (int) Math.floor(playerBodyMinZ);
int minY = (int) Math.floor(playerBodyMinY);
int maxX = (int) Math.floor(playerBodyMaxX);
int maxZ = (int) Math.floor(playerBodyMaxZ);
int maxY = (int) Math.floor(playerBodyMaxY);
for (int x = minX; x <= maxX; x++) {
for (int z = minZ; z <= maxZ; z++) {
for (int y = minY; y <= maxY; y++) {
byte block = world.getBlockAt(x, y, z);
if (block <= 0) continue; // We're not colliding with this block.
float blockMinY = (float) y;
float blockMaxY = (float) y + 1;
float blockMinX = (float) x;
float blockMaxX = (float) x + 1;
float blockMinZ = (float) z;
float blockMaxZ = (float) z + 1;
/*
To determine if the player is moving into the -Z side of a block:
- The player's max z position went from < blockMinZ to >= blockMinZ.
- The block to the -Z direction is air.
*/
boolean collidingWithNegativeZ = playerBodyPrevMaxZ < blockMinZ && playerBodyMaxZ >= blockMinZ && world.getBlockAt(x, y, z - 1) <= 0;
if (collidingWithNegativeZ) {
position.z = blockMinZ - RADIUS - delta;
velocity.z = 0;
movement.z = 0;
}
/*
To determine if the player is moving into the +Z side of a block:
- The player's min z position went from >= blockMaxZ to < blockMaxZ.
- The block to the +Z direction is air.
*/
boolean collidingWithPositiveZ = playerBodyPrevMinZ >= blockMaxZ && playerBodyMinZ < blockMaxZ && world.getBlockAt(x, y, z + 1) <= 0;
if (collidingWithPositiveZ) {
position.z = blockMaxZ + RADIUS + delta;
velocity.z = 0;
movement.z = 0;
}
/*
To determine if the player is moving into the -X side of a block:
- The player's max x position went from < blockMinX to >= blockMinX
- The block to the -X direction is air.
*/
boolean collidingWithNegativeX = playerBodyPrevMaxX < blockMinX && playerBodyMaxX >= blockMinX && world.getBlockAt(x - 1, y, z) <= 0;
if (collidingWithNegativeX) {
position.x = blockMinX - RADIUS - delta;
velocity.x = 0;
movement.x = 0;
}
/*
To determine if the player is moving into the +X side of a block:
- The player's min x position went from >= blockMaxX to < blockMaxX.
- The block to the +X direction is air.
*/
boolean collidingWithPositiveX = playerBodyPrevMinX >= blockMaxX && playerBodyMinX < blockMaxX && world.getBlockAt(x + 1, y, z) <= 0;
if (collidingWithPositiveX) {
position.x = blockMaxX + RADIUS + delta;
velocity.x = 0;
movement.x = 0;
}
/*
To determine if the player is moving down onto a block:
- The player's min y position went from >= blockMaxY to < blockMaxY
- The block above the current one is air.
*/
boolean collidingWithFloor = playerBodyPrevMinY >= blockMaxY && playerBodyMinY < blockMaxY && world.getBlockAt(x, y + 1, z) <= 0;
if (collidingWithFloor) {
position.y = blockMaxY;
velocity.y = 0;
movement.y = 0;
}
/*
To determine if the player is moving up into a block:
- The player's y position went from below blockMinY to >= blockMinY
- The block below the current one is air.
*/
boolean collidingWithCeiling = playerBodyPrevMaxY < blockMinY && playerBodyMaxY >= blockMinY && world.getBlockAt(x, y - 1, z) <= 0;
if (collidingWithCeiling) {
position.y = blockMinY - height - delta;
velocity.y = 0;
movement.y = 0;
}
updated = true;
}
}
}
}
}
public float getCurrentHeight() {
return lastInputState.crouching() ? HEIGHT_CROUCH : HEIGHT;
}
public float getEyeHeight() {
return lastInputState.crouching() ? EYE_HEIGHT_CROUCH : EYE_HEIGHT;
}
}

View File

@ -1,9 +1,7 @@
package nl.andrewl.aos2_server; package nl.andrewl.aos2_server.logic;
import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage; import nl.andrewl.aos2_server.Server;
import org.joml.Math; import org.joml.Math;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -56,14 +54,8 @@ public class WorldUpdater implements Runnable {
private void tick() { private void tick() {
for (var player : server.getPlayerManager().getPlayers()) { for (var player : server.getPlayerManager().getPlayers()) {
player.tick(secondsPerTick, server.getWorld(), server); player.getActionManager().tick(secondsPerTick, server.getWorld(), server);
if (player.isUpdated()) server.getPlayerManager().broadcastUdpMessage(new PlayerUpdateMessage( if (player.getActionManager().isUpdated()) server.getPlayerManager().broadcastUdpMessage(player.getUpdateMessage());
player.getId(),
player.getPosition().x, player.getPosition().y, player.getPosition().z,
player.getVelocity().x, player.getVelocity().y, player.getVelocity().z,
player.getOrientation().x, player.getOrientation().y,
player.getLastInputState().crouching()
));
} }
} }
} }