Greatly improved bullet collision physics performance and accuracy with true ray tracing.

This commit is contained in:
Andrew Lalis 2022-07-30 10:41:38 +02:00
parent 4aba806610
commit 4198a18a70
3 changed files with 133 additions and 87 deletions

View File

@ -0,0 +1,27 @@
package nl.andrewl.aos_core.model;
/**
* Represents the different modes that a player can be in.
* <ul>
* <li>
* In normal mode, the player acts as a usual competitive player that
* has an inventory, can shoot weapons, and must traverse the world by
* walking around.
* </li>
* <li>
* In creative mode, the player can fly, but still collides with the
* world's objects. The player also has unlimited ammunition and
* blocks.
* </li>
* <li>
* In spectator mode, the player can fly freely throughout the world,
* limited by nothing. The player can't interact with the world in any
* way other than simply observing it through sight and sound.
* </li>
* </ul>
*/
public enum PlayerMode {
NORMAL,
CREATIVE,
SPECTATOR
}

View File

@ -13,6 +13,7 @@ import nl.andrewl.aos_core.net.client.SoundMessage;
import nl.andrewl.aos_core.net.world.ChunkUpdateMessage; import nl.andrewl.aos_core.net.world.ChunkUpdateMessage;
import org.joml.Matrix4f; import org.joml.Matrix4f;
import org.joml.Vector3f; import org.joml.Vector3f;
import org.joml.Vector3fc;
import org.joml.Vector3i; import org.joml.Vector3i;
import java.util.*; import java.util.*;
@ -103,10 +104,9 @@ public class ProjectileManager {
// Check for if the bullet will move close enough to a player to hit them. // 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);
Vector3f direction = new Vector3f(projectile.getVelocity()).normalize();
// Check first to see if we'll hit a player this tick. // 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; Vector3f playerHit = null;
ServerPlayer hitPlayer = null; ServerPlayer hitPlayer = null;
int playerHitType = -1; int playerHitType = -1;
@ -121,23 +121,25 @@ public class ProjectileManager {
) continue; ) continue;
Vector3f headPos = player.getEyePosition(); Vector3f headPos = player.getEyePosition();
Vector3f bodyPos = new Vector3f(player.getPosition()); float headRadius = Player.RADIUS;
bodyPos.y += 1.0f; Vector3f bodyPos = new Vector3f(player.getPosition().x, player.getPosition().y + 1, player.getPosition().z);
float bodyRadius = Player.RADIUS;
// Do a really shitty collision detection... check in 10cm increments if we're close to the player. Vector3f headIntersect = checkSphereIntersection(projectile.getPosition(), direction, headPos, headRadius);
// TODO: Come up with a better collision system. if (headIntersect != null) {
testPos.set(projectile.getPosition()); playerHitType = 1;
while (testPos.distanceSquared(projectile.getPosition()) < movement.lengthSquared() && playerHit == null) { playerHit = headIntersect;
if (testPos.distanceSquared(headPos) < Player.RADIUS * Player.RADIUS) { hitPlayer = player;
playerHitType = 1; break;
playerHit = new Vector3f(testPos); }
hitPlayer = player;
} else if (testPos.distanceSquared(bodyPos) < Player.RADIUS * Player.RADIUS) { // Check body intersection.
playerHitType = 2; Vector3f bodyIntersect = checkSphereIntersection(projectile.getPosition(), direction, bodyPos, bodyRadius);
playerHit = new Vector3f(testPos); if (bodyIntersect != null) {
hitPlayer = player; playerHitType = 2;
} playerHit = bodyIntersect;
testPos.add(testMovement); hitPlayer = player;
break;
} }
} }
@ -168,6 +170,37 @@ public class ProjectileManager {
} }
} }
/**
* Checks for and returns the point at which a ray with origin p and
* normalized direction d intersects with a sphere whose center is c, and
* whose radius is r.
* @param p The ray origin.
* @param d The ray's normalized direction.
* @param c The sphere's center.
* @param r The radius of the sphere.
* @return The point of intersection, if one exists.
*/
private Vector3f checkSphereIntersection(Vector3fc p, Vector3fc d, Vector3fc c, float r) {
Vector3f vpc = new Vector3f(c).sub(p);
if (vpc.dot(d) >= 0) {// Only check for intersections ahead of us.
Vector3f puv = new Vector3f(d).mul(d.dot(vpc) / d.length());
Vector3f pc = new Vector3f(p).add(puv);
Vector3f diff = new Vector3f(c).sub(pc);
if (diff.length() <= r) {
Vector3f pcToC = new Vector3f(pc).sub(c);
float dist = (float) Math.sqrt(r * r - pcToC.lengthSquared());
float distanceToFirstIntersection;
if (vpc.length() > r) {
distanceToFirstIntersection = pcToC.length() - dist;
} else {
distanceToFirstIntersection = pcToC.length() + dist;
}
return new Vector3f(d).mul(distanceToFirstIntersection).add(p);
}
}
return null;
}
private void handleProjectileBlockHit(Hit hit, ServerProjectile projectile, long now) { private void handleProjectileBlockHit(Hit hit, ServerProjectile projectile, long now) {
if (!server.getTeamManager().isProtected(hit.pos())) { if (!server.getTeamManager().isProtected(hit.pos())) {
Gun gun = (Gun) projectile.getSourceItem(); Gun gun = (Gun) projectile.getSourceItem();

View File

@ -102,7 +102,7 @@ public class PlayerActionManager {
} }
tickMovement(dt, server, world, server.getConfig().physics); tickMovement(dt, server, world, server.getConfig().physics);
input.reset(); input.reset(); // Reset our input state after processing this tick's player input.
} }
private void tickGunAction(long now, Server server, GunItemStack g) { private void tickGunAction(long now, Server server, GunItemStack g) {
@ -115,58 +115,17 @@ public class PlayerActionManager {
(gun.isAutomatic() || !gunNeedsReCock) && (gun.isAutomatic() || !gunNeedsReCock) &&
!server.getTeamManager().isProtected(player) // Don't allow players to shoot from within their own team's protected zones. !server.getTeamManager().isProtected(player) // Don't allow players to shoot from within their own team's protected zones.
) { ) {
server.getProjectileManager().spawnBullets(player, gun); shootGun(now, server, gun, g);
g.setBulletCount(g.getBulletCount() - 1);
gunLastShotAt = now;
if (!gun.isAutomatic()) {
gunNeedsReCock = true;
}
server.getPlayerManager().getHandler(player.getId()).sendDatagramPacket(new ItemStackMessage(player.getInventory()));
// Apply recoil!
float recoilFactor = 10f; // Maximum number of degrees to recoil.
if (isScopeEnabled()) recoilFactor *= 0.1f;
float recoil = recoilFactor * gun.getRecoil() + (float) ThreadLocalRandom.current().nextGaussian(0, 0.01);
server.getPlayerManager().getHandler(player.getId()).sendDatagramPacket(new ClientRecoilMessage(0, Math.toRadians(recoil)));
// Play sound!
String shotSound = null;
if (gun instanceof Rifle) {
shotSound = "shot_m1-garand_1";
} else if (gun instanceof Ak47) {
shotSound = "shot_ak-47_1";
} else if (gun instanceof Winchester) {
shotSound = "shot_winchester_1";
}
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. // Check to see if the player is reloading.
input.reloading() && if (input.reloading() && !gunReloading && g.getClipCount() > 0) {
!gunReloading && reloadGun(now, server, g);
g.getClipCount() > 0
) {
g.setClipCount(g.getClipCount() - 1);
gunReloadingStartedAt = now;
gunReloading = true;
reloadingItemStack = g;
server.getPlayerManager().getHandler(player.getId()).sendDatagramPacket(new ItemStackMessage(player.getInventory()));
server.getPlayerManager().broadcastUdpMessage(new SoundMessage("reload", 1, player.getPosition(), player.getVelocity()));
} }
if (// Check to see if reloading is done. // Check to see if reloading is done.
gunReloading && if (gunReloading && reloadingItemStack != null && now - gunReloadingStartedAt > gun.getReloadTime() * 1000) {
reloadingItemStack != null && reloadingComplete(server, gun);
now - gunReloadingStartedAt > gun.getReloadTime() * 1000
) {
reloadingItemStack.setBulletCount(gun.getMaxBulletCount());
int idx = player.getInventory().getIndex(reloadingItemStack);
if (idx != -1) {
server.getPlayerManager().getHandler(player.getId()).sendDatagramPacket(new ItemStackMessage(idx, reloadingItemStack));
}
gunReloading = false;
reloadingItemStack = null;
} }
// 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.
@ -334,26 +293,6 @@ public class PlayerActionManager {
return points; return points;
} }
private boolean isSpaceOccupied(Vector3i pos) {
var playerPos = player.getPosition();
float playerBodyMinZ = playerPos.z - RADIUS;
float playerBodyMaxZ = playerPos.z + RADIUS;
float playerBodyMinX = playerPos.x - RADIUS;
float playerBodyMaxX = playerPos.x + RADIUS;
float playerBodyMinY = playerPos.y;
float playerBodyMaxY = playerPos.y + player.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);
return pos.x >= minX && pos.x <= maxX && pos.y >= minY && pos.y <= maxY && pos.z >= minZ && pos.z <= maxZ;
}
private void checkBlockCollisions(Vector3f movement, Server server, World world) { private void checkBlockCollisions(Vector3f movement, Server server, World world) {
var position = player.getPosition(); var position = player.getPosition();
var velocity = player.getVelocity(); var velocity = player.getVelocity();
@ -501,4 +440,51 @@ public class PlayerActionManager {
} }
} }
} }
private void shootGun(long now, Server server, Gun gun, GunItemStack g) {
server.getProjectileManager().spawnBullets(player, gun);
g.setBulletCount(g.getBulletCount() - 1);
gunLastShotAt = now;
if (!gun.isAutomatic()) {
gunNeedsReCock = true;
}
server.getPlayerManager().getHandler(player.getId()).sendDatagramPacket(new ItemStackMessage(player.getInventory()));
// Apply recoil!
float recoilFactor = 10f; // Maximum number of degrees to recoil.
if (isScopeEnabled()) recoilFactor *= 0.1f;
float recoil = recoilFactor * gun.getRecoil() + (float) ThreadLocalRandom.current().nextGaussian(0, 0.01);
server.getPlayerManager().getHandler(player.getId()).sendDatagramPacket(new ClientRecoilMessage(0, Math.toRadians(recoil)));
// Play sound!
String shotSound = null;
if (gun instanceof Rifle) {
shotSound = "shot_m1-garand_1";
} else if (gun instanceof Ak47) {
shotSound = "shot_ak-47_1";
} else if (gun instanceof Winchester) {
shotSound = "shot_winchester_1";
}
Vector3f soundLocation = new Vector3f(player.getPosition());
soundLocation.y += 1.4f;
soundLocation.add(player.getViewVector());
server.getPlayerManager().broadcastUdpMessage(new SoundMessage(shotSound, 1, soundLocation, player.getVelocity()));
}
private void reloadGun(long now, Server server, GunItemStack g) {
g.setClipCount(g.getClipCount() - 1);
gunReloadingStartedAt = now;
gunReloading = true;
reloadingItemStack = g;
server.getPlayerManager().getHandler(player.getId()).sendDatagramPacket(new ItemStackMessage(player.getInventory()));
server.getPlayerManager().broadcastUdpMessage(new SoundMessage("reload", 1, player.getPosition(), player.getVelocity()));
}
private void reloadingComplete(Server server, Gun gun) {
reloadingItemStack.setBulletCount(gun.getMaxBulletCount());
int idx = player.getInventory().getIndex(reloadingItemStack);
if (idx != -1) {
server.getPlayerManager().getHandler(player.getId()).sendDatagramPacket(new ItemStackMessage(idx, reloadingItemStack));
}
gunReloading = false;
reloadingItemStack = null;
}
} }