From 7a599ab1e52716385cf267d7ae6a09c1e7dbe581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beatrice=20Dellac=C3=A0?= Date: Fri, 14 Nov 2025 22:35:33 +0100 Subject: [PATCH] refactor world logic --- .../wtf/beatrice/retrorender/GameScreen.java | 10 +- .../beatrice/retrorender/engine/Collider.java | 29 +++ .../retrorender/engine/DayNightCycle.java | 30 ++- .../retrorender/engine/ModelLibrary.java | 68 +++++ .../beatrice/retrorender/engine/World3D.java | 245 +++++++++--------- .../retrorender/engine/WorldObject.java | 37 +++ 6 files changed, 286 insertions(+), 133 deletions(-) create mode 100644 core/src/main/java/wtf/beatrice/retrorender/engine/Collider.java create mode 100644 core/src/main/java/wtf/beatrice/retrorender/engine/ModelLibrary.java create mode 100644 core/src/main/java/wtf/beatrice/retrorender/engine/WorldObject.java diff --git a/core/src/main/java/wtf/beatrice/retrorender/GameScreen.java b/core/src/main/java/wtf/beatrice/retrorender/GameScreen.java index 996b296..37364bf 100644 --- a/core/src/main/java/wtf/beatrice/retrorender/GameScreen.java +++ b/core/src/main/java/wtf/beatrice/retrorender/GameScreen.java @@ -49,6 +49,9 @@ public class GameScreen implements Screen { private TextureRegion frameRegion; private SpriteBatch screenBatch; + // world + private ModelLibrary modelLibrary; + private static final int RETRO_WIDTH = 320; private static final int RETRO_HEIGHT = 240; @@ -94,11 +97,14 @@ public class GameScreen implements Screen { // create the cycle controller dayNightCycle = new DayNightCycle(shadowLight, ambientLight); // optional: start at morning/noon/etc - dayNightCycle.setTimeOfDay(0.25f); // sunrise + dayNightCycle.setTimeOfDay(0.4f); // good sun + dayNightCycle.setAxialTilt(30f); + dayNightCycle.setDayLength(60f * 5f); // 20 minutes } private void initWorld() { - world = new World3D(); + modelLibrary = new ModelLibrary(); + world = new World3D(modelLibrary); } private void initMenus() { diff --git a/core/src/main/java/wtf/beatrice/retrorender/engine/Collider.java b/core/src/main/java/wtf/beatrice/retrorender/engine/Collider.java new file mode 100644 index 0000000..3dbb483 --- /dev/null +++ b/core/src/main/java/wtf/beatrice/retrorender/engine/Collider.java @@ -0,0 +1,29 @@ +package wtf.beatrice.retrorender.engine; + +import com.badlogic.gdx.math.Vector3; + +public class Collider { + + public enum Type { + NONE, + BOX // axis-aligned box + // later: SPHERE, CAPSULE, MESH, etc. + } + + public Type type = Type.NONE; + + // Local-space center + extents relative to ModelInstance transform + public final Vector3 center = new Vector3(0f, 0f, 0f); + public final Vector3 halfExtents = new Vector3(1f, 1f, 1f); + + public static Collider none() { + return new Collider(); + } + + public static Collider box(float hx, float hy, float hz) { + Collider c = new Collider(); + c.type = Type.BOX; + c.halfExtents.set(hx, hy, hz); + return c; + } +} diff --git a/core/src/main/java/wtf/beatrice/retrorender/engine/DayNightCycle.java b/core/src/main/java/wtf/beatrice/retrorender/engine/DayNightCycle.java index 53ea659..6885f82 100644 --- a/core/src/main/java/wtf/beatrice/retrorender/engine/DayNightCycle.java +++ b/core/src/main/java/wtf/beatrice/retrorender/engine/DayNightCycle.java @@ -28,7 +28,7 @@ public class DayNightCycle { // Rotate around Z so the sun path is tilted relative to world up. private final Vector3 tiltAxis = new Vector3(0f, 0f, 1f).nor(); - public float axialTiltDeg = 30f; // tweak to taste + private float axialTilt = 30f; // in degrees /** * If false (default), simple equator-style model: @@ -43,7 +43,7 @@ public class DayNightCycle { * If true, brightness and day length depend on the final tilted direction, * so axialTiltDeg affects how long days are and how high the sun gets. */ - public boolean seasonal = false; + private boolean seasonal = false; /** * 0–1: 0 = midnight, 0.25 = sunrise, 0.5 = noon, 0.75 = sunset. @@ -51,7 +51,7 @@ public class DayNightCycle { private float timeOfDay = 0.25f; /** Seconds for one full 24h cycle. */ - public float dayLengthSeconds = 60 * 5f; + private float dayLengthSeconds = 60 * 5f; /** * Daylight / altitude factor in [0, 1]. @@ -98,7 +98,7 @@ public class DayNightCycle { // Visual direction with axial tilt tmpDir.set(baseX, baseY, baseZ).nor(); - tmpDir.rotate(tiltAxis, axialTiltDeg); + tmpDir.rotate(tiltAxis, axialTilt); sunDirection.set(tmpDir); // Sun color / intensity @@ -116,7 +116,7 @@ public class DayNightCycle { // === Seasonal / physical mode === tmpDir.set(baseX, baseY, baseZ).nor(); - tmpDir.rotate(tiltAxis, axialTiltDeg); + tmpDir.rotate(tiltAxis, axialTilt); sunDirection.set(tmpDir); // True altitude from final direction: 0..1 @@ -166,4 +166,24 @@ public class DayNightCycle { public boolean isSunAboveHorizon() { return sunAltitude > 0f; // strictly > 0; you can use >= 0.01f if you want a tiny cutoff } + + public float getAxialTilt() + { + return axialTilt; + } + + public void setAxialTilt(float degrees) + { + axialTilt = degrees; + } + + public float getDayLength() + { + return dayLengthSeconds; + } + + public void setDayLength(float seconds) + { + dayLengthSeconds = seconds; + } } diff --git a/core/src/main/java/wtf/beatrice/retrorender/engine/ModelLibrary.java b/core/src/main/java/wtf/beatrice/retrorender/engine/ModelLibrary.java new file mode 100644 index 0000000..4bd5ff4 --- /dev/null +++ b/core/src/main/java/wtf/beatrice/retrorender/engine/ModelLibrary.java @@ -0,0 +1,68 @@ +package wtf.beatrice.retrorender.engine; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.VertexAttributes; +import com.badlogic.gdx.graphics.g3d.*; +import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute; +import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute; +import com.badlogic.gdx.graphics.g3d.utils.MeshPartBuilder; +import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder; + +public class ModelLibrary { + + private final ModelBuilder builder = new ModelBuilder(); + + public final Texture groundTexture; + public final Model groundModel; + public final Model unitCubeModel; + + public ModelLibrary() { + // ground texture + groundTexture = new Texture(Gdx.files.internal("textures/paving.png")); + groundTexture.setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest); + groundTexture.setWrap(Texture.TextureWrap.Repeat, Texture.TextureWrap.Repeat); + + // ground model (32x32 plane) + float halfSize = 32f; + float tileScale = 16f; + + builder.begin(); + Material groundMat = new Material( + ColorAttribute.createDiffuse(1f, 1f, 1f, 1f), + TextureAttribute.createDiffuse(groundTexture) + ); + + MeshPartBuilder mpb = builder.part( + "ground", + GL20.GL_TRIANGLES, + VertexAttributes.Usage.Position + | VertexAttributes.Usage.Normal + | VertexAttributes.Usage.TextureCoordinates, + groundMat + ); + mpb.setUVRange(0f, 0f, tileScale, tileScale); + mpb.rect( + -halfSize, 0f, halfSize, + halfSize, 0f, halfSize, + halfSize, 0f, -halfSize, + -halfSize, 0f, -halfSize, + 0f, 1f, 0f + ); + groundModel = builder.end(); + + // a generic 2x2x2 cube model (can be reused for buildings, crates, etc.) + unitCubeModel = builder.createBox( + 2f, 2f, 2f, + new Material(ColorAttribute.createDiffuse(1f, 1f, 1f, 1f)), + VertexAttributes.Usage.Position | VertexAttributes.Usage.Normal + ); + } + + public void dispose() { + groundModel.dispose(); + unitCubeModel.dispose(); + groundTexture.dispose(); + } +} diff --git a/core/src/main/java/wtf/beatrice/retrorender/engine/World3D.java b/core/src/main/java/wtf/beatrice/retrorender/engine/World3D.java index acc3eae..6709ca2 100644 --- a/core/src/main/java/wtf/beatrice/retrorender/engine/World3D.java +++ b/core/src/main/java/wtf/beatrice/retrorender/engine/World3D.java @@ -1,14 +1,8 @@ package wtf.beatrice.retrorender.engine; -import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.graphics.GL20; -import com.badlogic.gdx.graphics.Texture; -import com.badlogic.gdx.graphics.VertexAttributes; -import com.badlogic.gdx.graphics.g3d.*; -import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute; -import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute; -import com.badlogic.gdx.graphics.g3d.utils.MeshPartBuilder; -import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder; +import com.badlogic.gdx.graphics.g3d.Environment; +import com.badlogic.gdx.graphics.g3d.ModelBatch; +import com.badlogic.gdx.math.Matrix4; import com.badlogic.gdx.math.Vector3; import java.util.ArrayList; @@ -16,134 +10,121 @@ import java.util.List; public class World3D { - // textures - private final Texture groundTexture; + private final ModelLibrary models; - // models (base mesh) - private final Model groundModel; - private final Model cubeModel; + private final WorldObject ground; + private final List objects = new ArrayList<>(); - // instances - private final ModelInstance groundInstance; - private final List cubeInstances = new ArrayList<>(); - public World3D() { - ModelBuilder modelBuilder = new ModelBuilder(); + private final Vector3 tmpWorld = new Vector3(); + private final Vector3 tmpFeet = new Vector3(); + private final Vector3 tmpHead = new Vector3(); + private final Vector3 tmpLocalFeet = new Vector3(); + private final Vector3 tmpLocalHead = new Vector3(); - // --- load floor texture - groundTexture = new Texture(Gdx.files.internal("textures/paving.png")); - groundTexture.setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest); - groundTexture.setWrap(Texture.TextureWrap.Repeat, Texture.TextureWrap.Repeat); + private final Matrix4 tmpMat = new Matrix4(); + private final Matrix4 tmpInv = new Matrix4(); - // --- create ground material - Material groundMat = new Material( - ColorAttribute.createDiffuse(1f, 1f, 1f, 1f), - TextureAttribute.createDiffuse(groundTexture) - ); + private final Vector3 tmp = new Vector3(); - // plane size (halfSize in X/Z) - float halfSize = 32f; + public World3D(ModelLibrary models) { + this.models = models; - // times the texture repeats across the whole plane - float tileScale = 16f; - - modelBuilder.begin(); - - MeshPartBuilder mpb = modelBuilder.part( + // --- ground --- + ground = new WorldObject( "ground", - GL20.GL_TRIANGLES, - VertexAttributes.Usage.Position - | VertexAttributes.Usage.Normal - | VertexAttributes.Usage.TextureCoordinates, - groundMat + new com.badlogic.gdx.graphics.g3d.ModelInstance(models.groundModel), + Collider.none() // treat plane as baseHeight = 0, not as a collider ); + ground.staticObject = true; - // set UV range so texture repeats tileScale times - mpb.setUVRange(0f, 0f, tileScale, tileScale); + // --- some cubes (temporary test geometry) --- + addCube("center", 0f, 1f, 0f); + addCube("cube-ne", 8f, 1f, 8f); + addCube("cube-nw", -8f, 1f, 8f); + addCube("cube-se", 8f, 1f, -8f); + addCube("cube-sw", -8f, 1f, -8f); - // build the quad - mpb.rect( - -halfSize, 0f, halfSize, // top-left - halfSize, 0f, halfSize, // top-right - halfSize, 0f, -halfSize, // bottom-right - -halfSize, 0f, -halfSize, // bottom-left - 0f, 1f, 0f // normal up - ); - - groundModel = modelBuilder.end(); - groundInstance = new ModelInstance(groundModel); - - // --- create reusable cube model - cubeModel = modelBuilder.createBox( - 2f, 2f, 2f, - new Material(ColorAttribute.createDiffuse(1f, 1f, 1f, 1f)), - VertexAttributes.Usage.Position | VertexAttributes.Usage.Normal - ); - - // --- place random cubes in the world - // center - cubeInstances.add(new ModelInstance(cubeModel)); // at origin - - // four around - cubeInstances.add(new ModelInstance(cubeModel)); - cubeInstances.get(1).transform.setToTranslation(8f, 1f, 8f); - - cubeInstances.add(new ModelInstance(cubeModel)); - cubeInstances.get(2).transform.setToTranslation(-8f, 1f, 8f); - - cubeInstances.add(new ModelInstance(cubeModel)); - cubeInstances.get(3).transform.setToTranslation(8f, 1f, -8f); - - cubeInstances.add(new ModelInstance(cubeModel)); - cubeInstances.get(4).transform.setToTranslation(-8f, 1f, -8f); - - // one tall pillar in front of you - ModelInstance pillar = new ModelInstance(cubeModel); - pillar.transform + WorldObject pillar = addCube("pillar", 0f, 3f, -10f); + // scale pillar’s transform (purely visual) + pillar.instance.transform .setToScaling(1f, 3f, 1f) .translate(0f, 3f, -10f); - cubeInstances.add(pillar); + // collider is still 1x1x1, you can adjust collider.halfExtents here if needed + } + + private WorldObject addCube(String id, float x, float y, float z) { + var instance = new com.badlogic.gdx.graphics.g3d.ModelInstance(models.unitCubeModel); + instance.transform.setToTranslation(x, y, z); + + // cube is 2x2x2, so half extents = 1 + Collider collider = Collider.box(1f, 1f, 1f); + + WorldObject obj = new WorldObject(id, instance, collider); + obj.staticObject = true; + objects.add(obj); + return obj; } public void update(float delta) { - // rotate cubes around y axis - for (ModelInstance instance : cubeInstances) { - instance.transform.rotate(Vector3.Y, 20f * delta); + // here you can update objects that animate / move + for (WorldObject obj : objects) { + obj.update(delta); } } public void render(ModelBatch batch, Environment environment) { // draw ground - batch.render(groundInstance, environment); - // draw cubes - for (ModelInstance instance : cubeInstances) { - batch.render(instance, environment); + batch.render(ground.instance, environment); + + // draw objects + for (WorldObject obj : objects) { + batch.render(obj.instance, environment); } } + /** * 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 cube. + * Starts from baseHeight (e.g. 0) and checks if you're standing on top of any BOX collider. */ public float getGroundHeightAt(float x, float z, float baseHeight) { - float maxY = baseHeight; // ground plane at y = 0 + float maxY = baseHeight; // plane at y = 0 - // a cube is 2 units tall, from y = 0 to y = 2 - float cubeHalfSize = 1f; - float cubeTopY = 2f; + for (WorldObject obj : objects) { + if (obj.collider.type != Collider.Type.BOX) continue; - Vector3 tmp = new Vector3(); - for (ModelInstance cube : cubeInstances) { - cube.transform.getTranslation(tmp); + Collider col = obj.collider; - float minX = tmp.x - cubeHalfSize; - float maxX = tmp.x + cubeHalfSize; - float minZ = tmp.z - cubeHalfSize; - float maxZ = tmp.z + cubeHalfSize; + // Inverse transform: world → object local + tmpInv.set(obj.instance.transform); + tmpInv.inv(); - if (x >= minX && x <= maxX && z >= minZ && z <= maxZ) { - if (cubeTopY > maxY) { - maxY = cubeTopY; - } + // Player XZ at ground level in world space + tmpWorld.set(x, 0f, z); + tmpWorld.mul(tmpInv); // now in local space + + float px = tmpWorld.x; + float pz = 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; + + // inside horizontal footprint in *local* space + if (px < minX || px > maxX || pz < minZ || pz > maxZ) { + continue; + } + + // Local top Y of the box + float localTopY = col.center.y + col.halfExtents.y; + + // Convert that top point back to world space + tmpWorld.set(col.center.x, localTopY, col.center.z); + tmpWorld.mul(obj.instance.transform); + + if (tmpWorld.y > maxY) { + maxY = tmpWorld.y; } } @@ -151,36 +132,50 @@ public class World3D { } /** - * Simple horizontal collision check between a player and cube sides. + * Simple horizontal collision check between a player and BOX colliders. * Player is approximated as a capsule: radius in XZ, height = eyeHeight. */ public boolean collidesAt(float x, float y, float z, float radius, float eyeHeight) { float feetY = y - eyeHeight; float headY = y; - float cubeHalfSize = 1f; - float cubeBottomY = 0f; - float cubeTopY = 2f; + for (WorldObject obj : objects) { + if (obj.collider.type != Collider.Type.BOX) continue; - Vector3 tmp = new Vector3(); - for (ModelInstance cube : cubeInstances) { - cube.transform.getTranslation(tmp); + Collider col = obj.collider; - float minX = tmp.x - cubeHalfSize; - float maxX = tmp.x + cubeHalfSize; - float minZ = tmp.z - cubeHalfSize; - float maxZ = tmp.z + cubeHalfSize; + // world → local + tmpInv.set(obj.instance.transform); + tmpInv.inv(); - // no vertical overlap -> ignore - if (headY <= cubeBottomY || feetY >= cubeTopY) { + 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) { continue; } - // circle (player) vs AABB (cube) in XZ - boolean overlapX = (x + radius > minX) && (x - radius < maxX); - boolean overlapZ = (z + radius > minZ) && (z - radius < maxZ); + 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; - if (overlapX && overlapZ) { + // 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)); + + float dx = px - closestX; + float dz = pz - closestZ; + + if (dx * dx + dz * dz <= radius * radius) { return true; } } @@ -189,8 +184,6 @@ public class World3D { } public void dispose() { - groundModel.dispose(); - cubeModel.dispose(); - groundTexture.dispose(); + models.dispose(); // or let GameScreen own/dispose the library } } diff --git a/core/src/main/java/wtf/beatrice/retrorender/engine/WorldObject.java b/core/src/main/java/wtf/beatrice/retrorender/engine/WorldObject.java new file mode 100644 index 0000000..8cdc66b --- /dev/null +++ b/core/src/main/java/wtf/beatrice/retrorender/engine/WorldObject.java @@ -0,0 +1,37 @@ +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; + public final ModelInstance instance; + public final Collider collider; + public boolean staticObject = true; + public boolean castsShadow = true; + + // Cached translation for faster queries + private final Vector3 tmpPos = new Vector3(); + + public WorldObject(String id, ModelInstance instance, Collider collider) { + this.id = id; + this.instance = instance; + this.collider = collider; + } + + public void update(float delta) { + instance.transform.rotate(Vector3.Y, delta * 20f); + // default: do nothing + // you can subclass or add strategies later for rotating, animating, AI, etc. + } + + public Vector3 getPosition(Vector3 out) { + instance.transform.getTranslation(out); + return out; + } + + public Vector3 getPosition() { + return getPosition(tmpPos); + } +}