Added primitive player health and killing system.

This commit is contained in:
Andrew Lalis 2022-07-25 14:55:41 +02:00
parent 4ef355f707
commit e1456707dd
9 changed files with 187 additions and 18 deletions

View File

@ -1,10 +1,13 @@
package nl.andrewl.aos_core; package nl.andrewl.aos_core;
import org.joml.Vector3f;
import org.joml.Vector3fc;
import org.joml.Vector3i; import org.joml.Vector3i;
import org.joml.Vector3ic; import org.joml.Vector3ic;
public class Directions { public class Directions {
public static final Vector3ic UP = new Vector3i(0, 1, 0); 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 DOWN = new Vector3i(0, -1, 0);
public static final Vector3ic NEGATIVE_X = new Vector3i(-1, 0, 0); public static final Vector3ic NEGATIVE_X = new Vector3i(-1, 0, 0);
public static final Vector3ic POSITIVE_X = new Vector3i(1, 0, 0); public static final Vector3ic POSITIVE_X = new Vector3i(1, 0, 0);

View File

@ -44,6 +44,7 @@ public final class Net {
serializer.registerType(15, InventorySelectedStackMessage.class); serializer.registerType(15, InventorySelectedStackMessage.class);
serializer.registerType(16, SoundMessage.class); serializer.registerType(16, SoundMessage.class);
serializer.registerType(17, ProjectileMessage.class); serializer.registerType(17, ProjectileMessage.class);
serializer.registerType(18, ClientHealthMessage.class);
} }
public static ExtendedDataInputStream getInputStream(InputStream in) { public static ExtendedDataInputStream getInputStream(InputStream in) {

View File

@ -1,8 +1,13 @@
package nl.andrewl.aos_core.model; package nl.andrewl.aos_core.model;
import nl.andrewl.aos_core.MathUtils; import nl.andrewl.aos_core.MathUtils;
import org.joml.Math;
import org.joml.Vector2f; import org.joml.Vector2f;
import org.joml.Vector3f; import org.joml.Vector3f;
import org.joml.Vector3i;
import java.util.ArrayList;
import java.util.List;
import static org.joml.Math.*; import static org.joml.Math.*;
@ -162,4 +167,35 @@ public class Player {
public float getCurrentHeight() { public float getCurrentHeight() {
return crouching ? HEIGHT_CROUCH : HEIGHT; return crouching ? HEIGHT_CROUCH : HEIGHT;
} }
public List<Vector3i> 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<Vector3i> 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);
}
} }

View File

@ -1,5 +1,6 @@
package nl.andrewl.aos_core.model.world; package nl.andrewl.aos_core.model.world;
import org.joml.Vector3f;
import org.joml.Vector3i; import org.joml.Vector3i;
import org.joml.Vector3ic; import org.joml.Vector3ic;
@ -9,5 +10,6 @@ import org.joml.Vector3ic;
*/ */
public record Hit ( public record Hit (
Vector3i pos, Vector3i pos,
Vector3ic norm Vector3ic norm,
Vector3f rawPos
) {} ) {}

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.00001f; private static final float DELTA = 0.0001f;
protected final Map<Vector3ic, Chunk> chunkMap = new HashMap<>(); protected final Map<Vector3ic, Chunk> chunkMap = new HashMap<>();
protected ColorPalette palette; protected ColorPalette palette;
@ -160,7 +160,7 @@ public class World {
System.err.println("Invalid hit!"); System.err.println("Invalid hit!");
} }
return new Hit(hitPos, hitNorm); return new Hit(hitPos, hitNorm, pos);
} }
} }
return null; return null;
@ -189,7 +189,7 @@ public class World {
} }
private static float factorToNextValue(float n, float dir) { private static float factorToNextValue(float n, float dir) {
if (dir == 0) return 0; if (dir == 0) return Float.MAX_VALUE;
float nextValue; float nextValue;
if (dir > 0) { if (dir > 0) {
nextValue = (Math.ceil(n) == n) ? n + 1 : Math.ceil(n); nextValue = (Math.ceil(n) == n) ? n + 1 : Math.ceil(n);
@ -197,6 +197,11 @@ public class World {
nextValue = Math.floor(n) - DELTA; nextValue = Math.floor(n) - DELTA;
} }
float diff = nextValue - n; 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); return Math.abs(diff / dir);
} }

View File

@ -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 {}

View File

@ -3,6 +3,7 @@ package nl.andrewl.aos2_server;
import nl.andrewl.aos2_server.model.ServerPlayer; import nl.andrewl.aos2_server.model.ServerPlayer;
import nl.andrewl.aos_core.Net; import nl.andrewl.aos_core.Net;
import nl.andrewl.aos_core.model.Team; 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.PlayerJoinMessage;
import nl.andrewl.aos_core.net.client.PlayerLeaveMessage; import nl.andrewl.aos_core.net.client.PlayerLeaveMessage;
import nl.andrewl.aos_core.net.connect.DatagramInit; 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)); 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) { public void handleUdpInit(DatagramInit init, DatagramPacket packet) {
var handler = getHandler(init.clientId()); var handler = getHandler(init.clientId());
if (handler != null) { if (handler != null) {

View File

@ -2,9 +2,12 @@ package nl.andrewl.aos2_server;
import nl.andrewl.aos2_server.model.ServerPlayer; import nl.andrewl.aos2_server.model.ServerPlayer;
import nl.andrewl.aos2_server.model.ServerProjectile; 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.Projectile;
import nl.andrewl.aos_core.model.world.Hit; import nl.andrewl.aos_core.model.world.Hit;
import nl.andrewl.aos_core.net.world.ChunkUpdateMessage; import nl.andrewl.aos_core.net.world.ChunkUpdateMessage;
import org.joml.Matrix4f;
import org.joml.Vector3f; import org.joml.Vector3f;
import java.util.HashMap; import java.util.HashMap;
@ -12,7 +15,13 @@ import java.util.LinkedList;
import java.util.Map; import java.util.Map;
import java.util.Queue; 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 class ProjectileManager {
public static final float MOVEMENT_FACTOR = 1f;
private final Server server; private final Server server;
private int nextProjectileId = 1; private int nextProjectileId = 1;
private final Map<Integer, ServerProjectile> projectiles; private final Map<Integer, ServerProjectile> projectiles;
@ -27,8 +36,15 @@ public class ProjectileManager {
public void spawnBullet(ServerPlayer player) { public void spawnBullet(ServerPlayer player) {
int id = nextProjectileId++; int id = nextProjectileId++;
if (nextProjectileId == Integer.MAX_VALUE) nextProjectileId = 1; if (nextProjectileId == Integer.MAX_VALUE) nextProjectileId = 1;
Vector3f pos = new Vector3f(player.getEyePosition()); Vector3f pos = new Vector3f();
Vector3f vel = new Vector3f(player.getViewVector()).normalize().mul(300); 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); ServerProjectile bullet = new ServerProjectile(id, pos, vel, Projectile.Type.BULLET, player);
projectiles.put(bullet.getId(), bullet); projectiles.put(bullet.getId(), bullet);
server.getPlayerManager().broadcastUdpMessage(bullet.toMessage(false)); server.getPlayerManager().broadcastUdpMessage(bullet.toMessage(false));
@ -45,21 +61,82 @@ public class ProjectileManager {
} }
private void tickProjectile(ServerProjectile projectile, float dt) { private void tickProjectile(ServerProjectile projectile, float dt) {
projectile.getVelocity().y -= server.getConfig().physics.gravity * dt; projectile.getVelocity().y -= server.getConfig().physics.gravity * dt * MOVEMENT_FACTOR;
// TODO: Check if bullet will hit anything, like blocks or players, if it follows current velocity.
// Check for if the bullet will move close enough to a player to hit them.
Vector3f movement = new Vector3f(projectile.getVelocity()).mul(dt); 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(); Vector3f movementDir = new Vector3f(movement).normalize();
// Hit hit = server.getWorld().getLookingAtPos(projectile.getPosition(), movementDir, movement.length()); Hit hit = server.getWorld().getLookingAtPos(projectile.getPosition(), movementDir, movement.length());
projectile.getPosition().add(movement);
if (projectile.getDistanceTravelled() > 500) { float playerHitDist = Float.MAX_VALUE;
// if (hit != null) { if (playerHit != null) playerHitDist = projectile.getPosition().distanceSquared(playerHit);
// server.getWorld().setBlockAt(hit.pos().x, hit.pos().y, hit.pos().z, (byte) 0); float worldHitDist = Float.MAX_VALUE;
// server.getPlayerManager().broadcastUdpMessage(ChunkUpdateMessage.fromWorld(hit.pos(), server.getWorld())); if (hit != null) worldHitDist = projectile.getPosition().distanceSquared(hit.rawPos());
// }
removalQueue.add(projectile); // If we hit the world before the player,
server.getPlayerManager().broadcastUdpMessage(projectile.toMessage(true)); 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 { } 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));
}
} }

View File

@ -22,9 +22,16 @@ public class ServerPlayer extends Player {
private final PlayerActionManager actionManager; private final PlayerActionManager actionManager;
private final Inventory inventory; 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) { public ServerPlayer(int id, String username) {
super(id, username); super(id, username);
this.inventory = new Inventory(new ArrayList<>(), 0); this.inventory = new Inventory(new ArrayList<>(), 0);
this.health = 1f;
this.actionManager = new PlayerActionManager(this); this.actionManager = new PlayerActionManager(this);
inventory.getItemStacks().add(new GunItemStack(ItemTypes.RIFLE)); inventory.getItemStacks().add(new GunItemStack(ItemTypes.RIFLE));
inventory.getItemStacks().add(new BlockItemStack(ItemTypes.BLOCK, 50, (byte) 1)); inventory.getItemStacks().add(new BlockItemStack(ItemTypes.BLOCK, 50, (byte) 1));
@ -39,6 +46,16 @@ public class ServerPlayer extends Player {
return inventory; 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 * Helper method to build an update message for this player, to be sent to
* various clients. * various clients.