From e1456707ddad4a3261a93781c935bd45aea5d832 Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Mon, 25 Jul 2022 14:55:41 +0200 Subject: [PATCH] Added primitive player health and killing system. --- .../java/nl/andrewl/aos_core/Directions.java | 3 + .../main/java/nl/andrewl/aos_core/Net.java | 1 + .../nl/andrewl/aos_core/model/Player.java | 36 ++++++ .../nl/andrewl/aos_core/model/world/Hit.java | 4 +- .../andrewl/aos_core/model/world/World.java | 11 +- .../net/client/ClientHealthMessage.java | 12 ++ .../nl/andrewl/aos2_server/PlayerManager.java | 16 +++ .../aos2_server/ProjectileManager.java | 105 +++++++++++++++--- .../aos2_server/model/ServerPlayer.java | 17 +++ 9 files changed, 187 insertions(+), 18 deletions(-) create mode 100644 core/src/main/java/nl/andrewl/aos_core/net/client/ClientHealthMessage.java diff --git a/core/src/main/java/nl/andrewl/aos_core/Directions.java b/core/src/main/java/nl/andrewl/aos_core/Directions.java index f5efcd7..906cff5 100644 --- a/core/src/main/java/nl/andrewl/aos_core/Directions.java +++ b/core/src/main/java/nl/andrewl/aos_core/Directions.java @@ -1,10 +1,13 @@ package nl.andrewl.aos_core; +import org.joml.Vector3f; +import org.joml.Vector3fc; import org.joml.Vector3i; import org.joml.Vector3ic; public class Directions { public static final Vector3ic UP = new Vector3i(0, 1, 0); + public static final Vector3fc UPf = new Vector3f(0, 1, 0); public static final Vector3ic DOWN = new Vector3i(0, -1, 0); public static final Vector3ic NEGATIVE_X = new Vector3i(-1, 0, 0); public static final Vector3ic POSITIVE_X = new Vector3i(1, 0, 0); diff --git a/core/src/main/java/nl/andrewl/aos_core/Net.java b/core/src/main/java/nl/andrewl/aos_core/Net.java index b49d007..d2c2414 100644 --- a/core/src/main/java/nl/andrewl/aos_core/Net.java +++ b/core/src/main/java/nl/andrewl/aos_core/Net.java @@ -44,6 +44,7 @@ public final class Net { serializer.registerType(15, InventorySelectedStackMessage.class); serializer.registerType(16, SoundMessage.class); serializer.registerType(17, ProjectileMessage.class); + serializer.registerType(18, ClientHealthMessage.class); } public static ExtendedDataInputStream getInputStream(InputStream in) { diff --git a/core/src/main/java/nl/andrewl/aos_core/model/Player.java b/core/src/main/java/nl/andrewl/aos_core/model/Player.java index 54b1e1e..7ba55c8 100644 --- a/core/src/main/java/nl/andrewl/aos_core/model/Player.java +++ b/core/src/main/java/nl/andrewl/aos_core/model/Player.java @@ -1,8 +1,13 @@ package nl.andrewl.aos_core.model; import nl.andrewl.aos_core.MathUtils; +import org.joml.Math; import org.joml.Vector2f; import org.joml.Vector3f; +import org.joml.Vector3i; + +import java.util.ArrayList; +import java.util.List; import static org.joml.Math.*; @@ -162,4 +167,35 @@ public class Player { public float getCurrentHeight() { return crouching ? HEIGHT_CROUCH : HEIGHT; } + + public List getBlockSpaceOccupied() { + float playerBodyMinZ = position.z - RADIUS; + float playerBodyMaxZ = position.z + RADIUS; + float playerBodyMinX = position.x - RADIUS; + float playerBodyMaxX = position.x + RADIUS; + float playerBodyMinY = position.y; + float playerBodyMaxY = position.y + getCurrentHeight(); + + // 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); + + List vectors = new ArrayList<>(8); + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + for (int z = minZ; z <= maxZ; z++) { + vectors.add(new Vector3i(x, y, z)); + } + } + } + return vectors; + } + + public boolean isSpaceOccupied(Vector3i pos) { + return getBlockSpaceOccupied().contains(pos); + } } diff --git a/core/src/main/java/nl/andrewl/aos_core/model/world/Hit.java b/core/src/main/java/nl/andrewl/aos_core/model/world/Hit.java index cdbe354..90176fb 100644 --- a/core/src/main/java/nl/andrewl/aos_core/model/world/Hit.java +++ b/core/src/main/java/nl/andrewl/aos_core/model/world/Hit.java @@ -1,5 +1,6 @@ package nl.andrewl.aos_core.model.world; +import org.joml.Vector3f; import org.joml.Vector3i; import org.joml.Vector3ic; @@ -9,5 +10,6 @@ import org.joml.Vector3ic; */ public record Hit ( Vector3i pos, - Vector3ic norm + Vector3ic norm, + Vector3f rawPos ) {} diff --git a/core/src/main/java/nl/andrewl/aos_core/model/world/World.java b/core/src/main/java/nl/andrewl/aos_core/model/world/World.java index fd9cdf2..fef4eb3 100644 --- a/core/src/main/java/nl/andrewl/aos_core/model/world/World.java +++ b/core/src/main/java/nl/andrewl/aos_core/model/world/World.java @@ -17,7 +17,7 @@ import java.util.Map; * that players can interact in. */ public class World { - private static final float DELTA = 0.00001f; + private static final float DELTA = 0.0001f; protected final Map chunkMap = new HashMap<>(); protected ColorPalette palette; @@ -160,7 +160,7 @@ public class World { System.err.println("Invalid hit!"); } - return new Hit(hitPos, hitNorm); + return new Hit(hitPos, hitNorm, pos); } } return null; @@ -189,7 +189,7 @@ public class World { } private static float factorToNextValue(float n, float dir) { - if (dir == 0) return 0; + if (dir == 0) return Float.MAX_VALUE; float nextValue; if (dir > 0) { nextValue = (Math.ceil(n) == n) ? n + 1 : Math.ceil(n); @@ -197,6 +197,11 @@ public class World { nextValue = Math.floor(n) - DELTA; } float diff = nextValue - n; + // 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 Math.abs(diff / dir); } diff --git a/core/src/main/java/nl/andrewl/aos_core/net/client/ClientHealthMessage.java b/core/src/main/java/nl/andrewl/aos_core/net/client/ClientHealthMessage.java new file mode 100644 index 0000000..bbdf31f --- /dev/null +++ b/core/src/main/java/nl/andrewl/aos_core/net/client/ClientHealthMessage.java @@ -0,0 +1,12 @@ +package nl.andrewl.aos_core.net.client; + +import nl.andrewl.record_net.Message; + +/** + * A message that's sent to update a client with their player's latest health + * information. + * @param health The player's health. + */ +public record ClientHealthMessage( + float health +) implements Message {} diff --git a/server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java b/server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java index c0408bb..8b33d26 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java +++ b/server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java @@ -3,6 +3,7 @@ package nl.andrewl.aos2_server; import nl.andrewl.aos2_server.model.ServerPlayer; import nl.andrewl.aos_core.Net; import nl.andrewl.aos_core.model.Team; +import nl.andrewl.aos_core.net.client.ClientHealthMessage; import nl.andrewl.aos_core.net.client.PlayerJoinMessage; import nl.andrewl.aos_core.net.client.PlayerLeaveMessage; import nl.andrewl.aos_core.net.connect.DatagramInit; @@ -121,6 +122,21 @@ public class PlayerManager { return server.getWorld().getSpawnPoints().values().stream().findAny().orElse(new Vector3f(0, 0, 0)); } + /** + * This method is invoked by the server's logic if a player has been + * determined to be killed somehow. We will reset their inventory, health, + * and respawn them. + * @param player The player that died. + */ + public void playerKilled(ServerPlayer player) { + player.setPosition(getBestSpawnPoint(player)); + player.setVelocity(new Vector3f(0)); + player.setHealth(1); + getHandler(player.getId()).sendDatagramPacket(new ClientHealthMessage(player.getHealth())); + broadcastUdpMessage(player.getUpdateMessage(System.currentTimeMillis())); + // TODO: Team points or something. + } + public void handleUdpInit(DatagramInit init, DatagramPacket packet) { var handler = getHandler(init.clientId()); if (handler != null) { diff --git a/server/src/main/java/nl/andrewl/aos2_server/ProjectileManager.java b/server/src/main/java/nl/andrewl/aos2_server/ProjectileManager.java index bd92755..a2cb643 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/ProjectileManager.java +++ b/server/src/main/java/nl/andrewl/aos2_server/ProjectileManager.java @@ -2,9 +2,12 @@ package nl.andrewl.aos2_server; import nl.andrewl.aos2_server.model.ServerPlayer; import nl.andrewl.aos2_server.model.ServerProjectile; +import nl.andrewl.aos_core.Directions; +import nl.andrewl.aos_core.model.Player; import nl.andrewl.aos_core.model.Projectile; import nl.andrewl.aos_core.model.world.Hit; import nl.andrewl.aos_core.net.world.ChunkUpdateMessage; +import org.joml.Matrix4f; import org.joml.Vector3f; import java.util.HashMap; @@ -12,7 +15,13 @@ import java.util.LinkedList; import java.util.Map; import java.util.Queue; +/** + * Component that manages the set of all active projectiles in the world, and + * performs tick updates for them. + */ public class ProjectileManager { + public static final float MOVEMENT_FACTOR = 1f; + private final Server server; private int nextProjectileId = 1; private final Map projectiles; @@ -27,8 +36,15 @@ public class ProjectileManager { public void spawnBullet(ServerPlayer player) { int id = nextProjectileId++; if (nextProjectileId == Integer.MAX_VALUE) nextProjectileId = 1; - Vector3f pos = new Vector3f(player.getEyePosition()); - Vector3f vel = new Vector3f(player.getViewVector()).normalize().mul(300); + Vector3f pos = new Vector3f(); + Matrix4f bulletTransform = new Matrix4f() + .translate(player.getEyePosition()) + .rotate(player.getOrientation().x + (float) Math.PI, Directions.UPf) + .translate(-0.35f, -0.4f, 0.35f); + bulletTransform.transformPosition(pos); + Vector3f vel = new Vector3f(player.getViewVector()).normalize() + .mul(200 * MOVEMENT_FACTOR) + .add(player.getVelocity()); ServerProjectile bullet = new ServerProjectile(id, pos, vel, Projectile.Type.BULLET, player); projectiles.put(bullet.getId(), bullet); server.getPlayerManager().broadcastUdpMessage(bullet.toMessage(false)); @@ -45,21 +61,82 @@ public class ProjectileManager { } private void tickProjectile(ServerProjectile projectile, float dt) { - projectile.getVelocity().y -= server.getConfig().physics.gravity * dt; - // TODO: Check if bullet will hit anything, like blocks or players, if it follows current velocity. + projectile.getVelocity().y -= server.getConfig().physics.gravity * dt * MOVEMENT_FACTOR; + + // Check for if the bullet will move close enough to a player to hit them. Vector3f movement = new Vector3f(projectile.getVelocity()).mul(dt); + + // Check first to see if we'll hit a player this tick. + Vector3f testPos = new Vector3f(); + Vector3f testMovement = new Vector3f(movement).normalize(0.1f); + Vector3f playerHit = null; + ServerPlayer hitPlayer = null; + int playerHitType = -1; + for (ServerPlayer player : server.getPlayerManager().getPlayers()) { + // Don't allow players to shoot themselves. + if (projectile.getPlayer() != null && projectile.getPlayer().equals(player)) continue; + + Vector3f headPos = player.getEyePosition(); + Vector3f bodyPos = new Vector3f(player.getPosition()); + bodyPos.y += 1.0f; + + // Do a really shitty collision detection... check in 10cm increments if we're close to the player. + // TODO: Come up with a better collision system. + testPos.set(projectile.getPosition()); + while (testPos.distanceSquared(projectile.getPosition()) < movement.lengthSquared() && playerHit == null) { + if (testPos.distanceSquared(headPos) < Player.RADIUS * Player.RADIUS) { + playerHitType = 1; + playerHit = new Vector3f(testPos); + hitPlayer = player; + } else if (testPos.distanceSquared(bodyPos) < Player.RADIUS * Player.RADIUS) { + playerHitType = 2; + playerHit = new Vector3f(testPos); + hitPlayer = player; + } + testPos.add(testMovement); + } + } + + // Then check to see if we'll hit the world during this tick. Vector3f movementDir = new Vector3f(movement).normalize(); -// Hit hit = server.getWorld().getLookingAtPos(projectile.getPosition(), movementDir, movement.length()); - projectile.getPosition().add(movement); - if (projectile.getDistanceTravelled() > 500) { -// if (hit != null) { -// server.getWorld().setBlockAt(hit.pos().x, hit.pos().y, hit.pos().z, (byte) 0); -// server.getPlayerManager().broadcastUdpMessage(ChunkUpdateMessage.fromWorld(hit.pos(), server.getWorld())); -// } - removalQueue.add(projectile); - server.getPlayerManager().broadcastUdpMessage(projectile.toMessage(true)); + Hit hit = server.getWorld().getLookingAtPos(projectile.getPosition(), movementDir, movement.length()); + + float playerHitDist = Float.MAX_VALUE; + if (playerHit != null) playerHitDist = projectile.getPosition().distanceSquared(playerHit); + float worldHitDist = Float.MAX_VALUE; + if (hit != null) worldHitDist = projectile.getPosition().distanceSquared(hit.rawPos()); + + // If we hit the world before the player, + if (hit != null && (playerHit == null || worldHitDist < playerHitDist)) { + // Bullet struck the world first. + server.getWorld().setBlockAt(hit.pos().x, hit.pos().y, hit.pos().z, (byte) 0); + server.getPlayerManager().broadcastUdpMessage(ChunkUpdateMessage.fromWorld(hit.pos(), server.getWorld())); + deleteProjectile(projectile); + } else if (playerHit != null && (hit == null || playerHitDist < worldHitDist)) { + // Bullet struck the player first. + System.out.println("Player hit: " + playerHitType); + float damage = 0.4f; + if (playerHitType == 1) damage *= 2; + hitPlayer.setHealth(hitPlayer.getHealth() - damage); + System.out.println(hitPlayer.getHealth()); + if (hitPlayer.getHealth() == 0) { + System.out.println("Player killed!!!"); + server.getPlayerManager().playerKilled(hitPlayer); + } + deleteProjectile(projectile); } else { - server.getPlayerManager().broadcastUdpMessage(projectile.toMessage(false)); + // Bullet struck nothing. + projectile.getPosition().add(movement); + if (projectile.getDistanceTravelled() > 500) { + deleteProjectile(projectile); + } else { + server.getPlayerManager().broadcastUdpMessage(projectile.toMessage(false)); + } } } + + private void deleteProjectile(ServerProjectile p) { + removalQueue.add(p); + server.getPlayerManager().broadcastUdpMessage(p.toMessage(true)); + } } diff --git a/server/src/main/java/nl/andrewl/aos2_server/model/ServerPlayer.java b/server/src/main/java/nl/andrewl/aos2_server/model/ServerPlayer.java index f7a9f4b..2182618 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/model/ServerPlayer.java +++ b/server/src/main/java/nl/andrewl/aos2_server/model/ServerPlayer.java @@ -22,9 +22,16 @@ public class ServerPlayer extends Player { private final PlayerActionManager actionManager; private final Inventory inventory; + /** + * The player's health, from 0 to 1, where <= 0 means death, and 1 means + * full health. + */ + private float health; + public ServerPlayer(int id, String username) { super(id, username); this.inventory = new Inventory(new ArrayList<>(), 0); + this.health = 1f; this.actionManager = new PlayerActionManager(this); inventory.getItemStacks().add(new GunItemStack(ItemTypes.RIFLE)); inventory.getItemStacks().add(new BlockItemStack(ItemTypes.BLOCK, 50, (byte) 1)); @@ -39,6 +46,16 @@ public class ServerPlayer extends Player { return inventory; } + public float getHealth() { + return health; + } + + public void setHealth(float health) { + if (health > 1) health = 1; + if (health < 0) health = 0; + this.health = health; + } + /** * Helper method to build an update message for this player, to be sent to * various clients.