refactor world logic
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user