fix collisions and movement
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -62,7 +62,7 @@ public class SettingsMenu {
|
||||
pm.dispose();
|
||||
|
||||
// default FOV
|
||||
fov = 67f;
|
||||
fov = 60f;
|
||||
}
|
||||
|
||||
public void setFov(float fov) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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/ .
|
||||
|
||||
Reference in New Issue
Block a user