refactor world logic

This commit is contained in:
2025-11-14 22:35:33 +01:00
parent 398f1fa26f
commit 7a599ab1e5
6 changed files with 286 additions and 133 deletions

View File

@@ -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() {

View File

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

View File

@@ -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;
/**
* 01: 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;
}
}

View File

@@ -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();
}
}

View File

@@ -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<WorldObject> objects = new ArrayList<>();
// instances
private final ModelInstance groundInstance;
private final List<ModelInstance> 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 pillars 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
}
}

View File

@@ -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);
}
}