fix collisions and movement

This commit is contained in:
2025-11-15 00:25:06 +01:00
parent 0c726ed416
commit 94fefa6d2c
10 changed files with 220 additions and 84 deletions

View File

@@ -53,7 +53,7 @@ public class GameScreen implements Screen {
private ModelLibrary modelLibrary;
private static final int RETRO_WIDTH = 320;
private static final int RETRO_WIDTH = 430;
private static final int RETRO_HEIGHT = 240;
public GameScreen(Main game) {
@@ -61,7 +61,7 @@ public class GameScreen implements Screen {
}
private void initCamera() {
camera = new PerspectiveCamera(67f, RETRO_WIDTH, RETRO_HEIGHT);
camera = new PerspectiveCamera(60f, RETRO_WIDTH, RETRO_HEIGHT);
settings = new GameSettings();
settings.fov = camera.fieldOfView;
cameraController = new FpsCameraController(camera, settings);

View File

@@ -60,19 +60,37 @@ public class DebugHud {
float y = camera.position.y;
float z = camera.position.z;
String posText = String.format("X: %.2f Y: %.2f Z: %.2f", x, y, z);
String posText = String.format("X: %.3f Y: %.3f Z: %.3f", x, y, z);
font.draw(batch, posText, 5f, height - 15f);
String angText = String.format("Yaw: %.1f Pitch: %.1f", yaw, pitch);
String angText = String.format("Yaw: %.2f Pitch: %.2f", yaw, pitch);
font.draw(batch, angText, 5f, height - 25f);
String dayText = String.format("Time of day: %.2f Sun height: %.2f", dayNightCycle.getTimeOfDay(), dayNightCycle.getSunAltitude());
String dayText = String.format("Time of day: %.3f [%s] Sun height: %.3f", dayNightCycle.getTimeOfDay(), formatTimeOfDay(dayNightCycle.getTimeOfDay()), dayNightCycle.getSunAltitude());
font.draw(batch, dayText, 5f, height - 35f);
}
batch.end();
}
private String formatTimeOfDay(float timeOfDay) {
// keep it in [0,1)
float t = timeOfDay - (float)Math.floor(timeOfDay);
float hoursF = t * 24f;
int hours = (int)Math.floor(hoursF);
float minutesF = (hoursF - hours) * 60f;
int minutes = (int)Math.floor(minutesF);
// clamp just in case of float weirdness
if (hours < 0) hours = 0;
if (hours > 23) hours = 23;
if (minutes < 0) minutes = 0;
if (minutes > 59) minutes = 59;
return String.format("%02d:%02d", hours, minutes);
}
public void dispose() {
batch.dispose();
font.dispose();

View File

@@ -104,20 +104,18 @@ public class FpsCameraController {
/** Update each frame with delta time */
public void update(float delta, World3D world) {
// if we don't own the mouse, just don't move camera / player
if (!mouseCaptured) {
return;
}
// skip a few frames after capture to avoid mouse skew
// teleport cursor to center
// warmup: ignore mouse for a few frames after capture
if (captureWarmupFrames > 0) {
captureWarmupFrames--;
Gdx.input.setCursorPosition(centerX, centerY);
return;
}
// --- mouse look: keep cursor centred
// --- mouse look ---
int mouseX = Gdx.input.getX();
int mouseY = Gdx.input.getY();
@@ -130,22 +128,17 @@ public class FpsCameraController {
float deltaX = -dx * settings.mouseSensitivity;
float deltaY = -dy * settings.mouseSensitivity;
yaw += deltaX;
yaw += deltaX;
pitch += deltaY;
// clamp pitch to avoid flipping
pitch = Math.max(-89f, Math.min(89f, pitch));
// keep yaw in [0, 360)
if (yaw >= 360f) yaw -= 360f;
if (yaw < 0f) yaw += 360f;
if (yaw < 0f) yaw += 360f;
updateCameraDirection();
// --- keyboard movement
// forward vector (flattened on XZ)
// --- build movement vectors ---
tmpForward.set(camera.direction.x, 0f, camera.direction.z).nor();
// right vector (perpendicular)
tmpRight.set(tmpForward.z, 0f, -tmpForward.x).nor();
camera.fieldOfView = settings.fov;
@@ -171,48 +164,143 @@ public class FpsCameraController {
moveZ -= tmpRight.z * moveAmount;
}
float playerRadius = 0.4f;
// --- try to move in X/Z with collision
float newX = camera.position.x + moveX;
float newZ = camera.position.z + moveZ;
float curY = camera.position.y;
if (!world.collidesAt(newX, curY, camera.position.z, playerRadius, eyeHeight)) {
camera.position.x = newX;
}
if (!world.collidesAt(camera.position.x, curY, newZ, playerRadius, eyeHeight)) {
camera.position.z = newZ;
}
boolean jumpHeld = Gdx.input.isKeyPressed(Input.Keys.SPACE);
// apply gravity
float playerRadius = 0.4f;
// =========================================================
// 1) VERTICAL INTEGRATION + GROUND SNAP (capsule-based)
// =========================================================
// Apply gravity
velocityY += gravity * delta;
float proposedY = camera.position.y + velocityY * delta;
// feet position if we accept this Y
float feetY = proposedY - eyeHeight;
float oldY = camera.position.y;
float proposedY = oldY + velocityY * delta;
// find ground at current X/Z
float groundY = world.getGroundHeightAt(camera.position.x, camera.position.z, 0f);
// Capsule ground at current XZ
float groundY = world.getCapsuleGroundHeight(
camera.position.x,
camera.position.z,
playerRadius,
0f
);
if (feetY <= groundY) {
// ground or other surface below feet
grounded = true;
velocityY = 0f;
float feetNow = oldY - eyeHeight;
float feetNext = proposedY - eyeHeight;
final float GROUND_SNAP_EPS = 0.15f; // max distance to snap down
if (velocityY <= 0f && feetNext <= groundY + GROUND_SNAP_EPS) {
// Falling or resting, and we reached (or slightly passed) the ground
grounded = true;
velocityY = 0f;
camera.position.y = groundY + eyeHeight;
} else {
grounded = false;
grounded = false;
camera.position.y = proposedY;
}
// start jump again if holding down key
// Handle jump AFTER we've updated grounded state
if (grounded && jumpHeld) {
velocityY = jumpSpeed;
grounded = false;
grounded = false;
}
// snap position to quantization steps
// =========================================================
// 2) HORIZONTAL MOVEMENT WITH COLLISION + OPTIONAL STEP UP
// =========================================================
// We re-query ground at the start of the frame for step logic
float currentGroundY = world.getCapsuleGroundHeight(
camera.position.x,
camera.position.z,
playerRadius,
0f
);
float curY = camera.position.y;
// Max height we auto-step; must be < crate height, < house walls, etc
final float maxStepHeight = 0.6f; // your crate is 1.0 high, so you must jump
final float stepSnapEps = 0.05f; // tolerance when landing on step
// --- X axis ---
if (moveX != 0f) {
float targetX = camera.position.x + moveX;
float targetZ = camera.position.z;
boolean collides = world.collidesAt(
targetX,
curY,
targetZ,
playerRadius,
eyeHeight
);
if (!collides) {
camera.position.x = targetX;
} else if (grounded && velocityY <= 0.01f) {
// Only try to step up if we are actually on the ground (walking),
// NOT while jumping or falling.
float newGroundY = world.getCapsuleGroundHeight(
targetX,
targetZ,
playerRadius,
0f
);
float step = newGroundY - currentGroundY;
if (step > 0f && step <= maxStepHeight) {
// Step up: teleport onto the higher surface
camera.position.x = targetX;
camera.position.y = newGroundY + eyeHeight + stepSnapEps;
curY = camera.position.y;
currentGroundY = newGroundY;
grounded = true;
velocityY = 0f;
}
// else: it's a real wall or too high → ignore X movement
}
}
// --- Z axis ---
if (moveZ != 0f) {
float targetX = camera.position.x;
float targetZ = camera.position.z + moveZ;
boolean collides = world.collidesAt(
targetX,
curY,
targetZ,
playerRadius,
eyeHeight
);
if (!collides) {
camera.position.z = targetZ;
} else if (grounded && velocityY <= 0.01f) {
float newGroundY = world.getCapsuleGroundHeight(
targetX,
targetZ,
playerRadius,
0f
);
float step = newGroundY - currentGroundY;
if (step > 0f && step <= maxStepHeight) {
camera.position.z = targetZ;
camera.position.y = newGroundY + eyeHeight + stepSnapEps;
curY = camera.position.y;
currentGroundY = newGroundY;
grounded = true;
velocityY = 0f;
}
}
}
// =========================================================
// 3) RETRO QUANTIZATION
// =========================================================
if (retroQuantization) {
float q = positionQuantize;
camera.position.x = Math.round(camera.position.x * q) / q;
@@ -220,7 +308,6 @@ public class FpsCameraController {
camera.position.z = Math.round(camera.position.z * q) / q;
}
}
private void updateCameraDirection() {
// build direction vector from yaw/pitch
float useYaw = yaw;

View File

@@ -2,7 +2,7 @@ package wtf.beatrice.retrorender.engine;
public class GameSettings {
public float fov = 67f; // degrees
public float fov = 60f; // degrees
public float mouseSensitivity = 0.15f;
public float moveSpeed = 5f;

View File

@@ -57,7 +57,7 @@ public class ModelLibrary {
// generic 2x2x2 cube
unitCubeModel = builder.createBox(
2f, 2f, 2f,
1f, 1f, 1f,
new Material(ColorAttribute.createDiffuse(1f, 1f, 1f, 1f)),
VertexAttributes.Usage.Position | VertexAttributes.Usage.Normal
);

View File

@@ -62,7 +62,7 @@ public class SettingsMenu {
pm.dispose();
// default FOV
fov = 67f;
fov = 60f;
}
public void setFov(float fov) {

View File

@@ -1,7 +1,6 @@
package wtf.beatrice.retrorender.engine;
import com.badlogic.gdx.graphics.g3d.ModelInstance;
public class TownFactory {
private final ModelLibrary models;
@@ -15,32 +14,39 @@ public class TownFactory {
// houseBlockModel is 4x3x4 → center at y=1.5
inst.transform.setToTranslation(x, 1.5f, z);
// collider roughly matches the visual: halfExtents = (2,1.5,2)
Collider col = Collider.box(2f, 1.5f, 2f);
WorldObject obj = new WorldObject(id, inst, col);
obj.staticObject = true;
obj.castsShadow = true;
obj.staticObject = true;
obj.castsShadow = true;
obj.walkableSurface = false; // you don't walk on roofs
obj.collidable = true; // blocks movement
return obj;
}
public WorldObject createCrate(String id, float x, float z) {
ModelInstance inst = new ModelInstance(models.unitCubeModel);
// unit cube is 2x2x2 → center at y=1
inst.transform.setToTranslation(x, 1f, z);
// unit cube is 1x1x1 → center at y=0.5
inst.transform.setToTranslation(x, 0.5f, z);
Collider col = Collider.box(0.5f, 0.5f, 0.5f);
Collider col = Collider.box(1f, 1f, 1f);
WorldObject obj = new WorldObject(id, inst, col);
obj.staticObject = true;
obj.staticObject = true;
obj.castsShadow = true;
obj.walkableSurface = true; // you can stand on it
obj.collidable = true; // also blocks sides
return obj;
}
public WorldObject createPathTile(String id, float x, float z) {
ModelInstance inst = new ModelInstance(models.pathTileModel);
inst.transform.setToTranslation(x, 0f, z);
// purely visual, no collider
WorldObject obj = new WorldObject(id, inst, Collider.none());
obj.staticObject = true;
obj.staticObject = true;
obj.walkableSurface = false; // ground plane already gives height 0
obj.collidable = false;
return obj;
}
}

View File

@@ -119,35 +119,44 @@ public class World3D {
}
/**
* Returns the ground height (y) at a given XZ position.
* Starts from baseHeight (e.g. 0) and checks if you're standing on top of any BOX collider.
* Returns the ground height (y) for a capsule with given radius whose center
* is at (x, z) in world space. Starts from baseHeight (e.g. 0) and checks
* walkable BOX colliders using a circle-vs-AABB test in XZ.
*/
public float getGroundHeightAt(float x, float z, float baseHeight) {
float maxY = baseHeight; // plane at y = 0
public float getCapsuleGroundHeight(float x, float z, float radius, float baseHeight) {
float maxY = baseHeight; // plane at y = baseHeight
for (WorldObject obj : objects) {
if (!obj.walkableSurface) continue;
if (obj.collider.type != Collider.Type.BOX) continue;
Collider col = obj.collider;
// Inverse transform: world → object local
// world → local
tmpInv.set(obj.instance.transform);
tmpInv.inv();
// Player XZ at ground level in world space
// capsule center projected to XZ in local space
tmpWorld.set(x, 0f, z);
tmpWorld.mul(tmpInv); // now in local space
tmpWorld.mul(tmpInv);
float px = tmpWorld.x;
float pz = tmpWorld.z;
float cx = tmpWorld.x;
float cz = tmpWorld.z;
float minX = col.center.x - col.halfExtents.x;
float maxX = col.center.x + col.halfExtents.x;
float minZ = col.center.z - col.halfExtents.z;
float maxZ = col.center.z + col.halfExtents.z;
float boxMinX = col.center.x - col.halfExtents.x;
float boxMaxX = col.center.x + col.halfExtents.x;
float boxMinZ = col.center.z - col.halfExtents.z;
float boxMaxZ = col.center.z + col.halfExtents.z;
// inside horizontal footprint in *local* space
if (px < minX || px > maxX || pz < minZ || pz > maxZ) {
// circle (capsule footprint) vs AABB in local XZ
float closestX = Math.max(boxMinX, Math.min(cx, boxMaxX));
float closestZ = Math.max(boxMinZ, Math.min(cz, boxMaxZ));
float dx = cx - closestX;
float dz = cz - closestZ;
if (dx * dx + dz * dz > radius * radius) {
// no overlap in XZ with this walkable surface
continue;
}
@@ -166,6 +175,13 @@ public class World3D {
return maxY;
}
/**
* Backwards-compatible point-sample version (radius = 0).
*/
public float getGroundHeightAt(float x, float z, float baseHeight) {
return getCapsuleGroundHeight(x, z, 0f, baseHeight);
}
/**
* Simple horizontal collision check between a player and BOX colliders.
* Player is approximated as a capsule: radius in XZ, height = eyeHeight.
@@ -174,36 +190,41 @@ public class World3D {
float feetY = y - eyeHeight;
float headY = y;
final float EPS_TOP = 0.05f;
final float EPS_BOTTOM = 0.01f;
for (WorldObject obj : objects) {
if (!obj.collidable) continue;
if (obj.collider.type != Collider.Type.BOX) continue;
Collider col = obj.collider;
// world → local
tmpInv.set(obj.instance.transform);
tmpInv.inv();
tmpFeet.set(x, feetY, z).mul(tmpInv);
tmpHead.set(x, headY, z).mul(tmpInv);
// For horizontal test, use the "average" x/z of feet/head
float px = 0.5f * (tmpFeet.x + tmpHead.x);
float pz = 0.5f * (tmpFeet.z + tmpHead.z);
float boxMinY = col.center.y - col.halfExtents.y;
float boxMaxY = col.center.y + col.halfExtents.y;
// vertical overlap in local space
if (tmpHead.y <= boxMinY || tmpFeet.y >= boxMaxY) {
if (tmpFeet.y >= boxMaxY - EPS_TOP) {
// on / just above top → let groundHeight handle it
continue;
}
if (tmpHead.y <= boxMinY + EPS_BOTTOM || tmpFeet.y >= boxMaxY) {
continue;
}
float px = 0.5f * (tmpFeet.x + tmpHead.x);
float pz = 0.5f * (tmpFeet.z + tmpHead.z);
float minX = col.center.x - col.halfExtents.x;
float maxX = col.center.x + col.halfExtents.x;
float minZ = col.center.z - col.halfExtents.z;
float maxZ = col.center.z + col.halfExtents.z;
// circle (player) vs AABB in local XZ
float closestX = Math.max(minX, Math.min(px, maxX));
float closestZ = Math.max(minZ, Math.min(pz, maxZ));

View File

@@ -2,7 +2,6 @@ package wtf.beatrice.retrorender.engine;
import com.badlogic.gdx.graphics.g3d.ModelInstance;
import com.badlogic.gdx.math.Vector3;
public class WorldObject {
public final String id;
@@ -11,6 +10,10 @@ public class WorldObject {
public boolean staticObject = true;
public boolean castsShadow = true;
// NEW: gameplay flags
public boolean walkableSurface = false; // contributes to getGroundHeightAt
public boolean collidable = true; // used by collidesAt
// Cached translation for faster queries
private final Vector3 tmpPos = new Vector3();
@@ -22,7 +25,8 @@ public class WorldObject {
public void update(float delta) {
// default: do nothing
// you can subclass or add strategies later for rotating, animating, AI, etc.
// For rotating decoration, set collidable=false + walkableSurface=false
// and rotate instance.transform here if you want.
}
public Vector3 getPosition(Vector3 out) {

View File

@@ -28,7 +28,7 @@ public class Lwjgl3Launcher {
//// useful for testing performance, but can also be very stressful to some hardware.
//// You may also need to configure GPU drivers to fully disable Vsync; this can cause screen tearing.
configuration.setWindowedMode(640, 480);
configuration.setWindowedMode(860, 480);
//// You can change these files; they are in lwjgl3/src/main/resources/ .
//// They can also be loaded from the root of assets/ .