Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Cubeworld

A voxel sandbox game written from scratch in Java, using LWJGL for windowing and OpenGL for rendering. Procedurally generated terrain, water, trees, wandering pigs, block breaking, frustum culling and a distance-fogged horizon — no game engine involved.

Voxel terrain with trees and a lake

Contents

Features

  • Procedural terrain — multi-octave Perlin noise drives a heightmap across a 16 × 16 grid of chunks.
  • Biome-ish decoration — trees and large trees seeded by probability, with an ocean level that floods low ground.
  • Block breaking — hold left mouse on a block to damage it through six crack stages until it shatters. Different materials have different strengths.
  • Water — translucent liquid blocks, a swimming state with reduced movement speed, and a blue underwater fog effect.
  • Wildlife — pigs wander the world as gravity-affected entities.
  • Lighting — a directional sun with ambient/diffuse/specular terms, plus point lights from lamp blocks.
  • Performance work — frustum culling, hidden-face culling, and a cache of visible surface blocks per chunk so buried geometry is never submitted.
  • Original, generated art — every texture is produced from noise and maths by a script in this repo.

Screenshots

The generated world from above — elevation steps, forests, lakes and a wandering pig:

World overview

Underwater Block targeting
Lit sea floor seen through water Grass and dirt with a targeted block

Getting started

You need JDK 21 and Maven. Nothing else — no database, no server, no API keys.

mvn compile exec:exec

That's the whole thing. Maven detects your platform, pulls the matching LWJGL natives, compiles, and launches the game with the right JVM flags.

Building a standalone jar
mvn package
java -jar target/cubeworld.jar                       # Linux / Windows
java -XstartOnFirstThread -jar target/cubeworld.jar   # macOS

mvn package produces a fat jar with every dependency and native bundled in.

Platform notes

LWJGL natives are selected automatically by Maven profiles that key off os.name / os.arch. macOS (Intel and Apple Silicon), Linux (x86-64 and arm64) and Windows (x86-64) are wired up.

On macOS, GLFW must own the very first thread of the process, so the game has to run with -XstartOnFirstThread. mvn exec:exec supplies that for you. This is also why the project uses exec:exec rather than exec:java — the latter runs inside Maven's own process, where the flag cannot be applied, and the JVM segfaults during GLFW initialisation.

Controls

Input Action
W A S D Move
Mouse Look around
Space Jump
Left Ctrl Sprint
Left Shift Sneak (prevents walking off ledges)
Left mouse button Break the block you are looking at
R Teleport to y = 75 (debug: useful for surveying the world)
Esc Quit

How it works

Frame loop. Game owns the window, the delta-time clock and the FPS counter. Each frame it clears the buffers and calls World.update(), which advances the skybox, refreshes the view frustum, renders the chunks around the player, updates animals, and finally updates the player.

Chunks. A Chunk is a 16 × 16 column of blocks stored in a HashMap<Integer, Block> keyed by a packed integer coordinate. Terrain height comes from NoiseGenerator.generateTerrainHeight(x, z, octaves, persistence, scale). Anything below the ocean level (y = 7) is filled with water; the surface block is grass, everything beneath it is stone.

Render ordering. Chunks are sorted by distance from the player and drawn far-to-near, in three passes — solids, then transparent blocks (leaves), then liquids (water). The player's own chunk is drawn last in each pass. This ordering is what makes alpha blending on water and leaves look correct.

Culling. Two layers. Frustum extracts the six clipping planes from the projection × view matrix and rejects blocks outside them. Separately, Block.isVisible() checks the six neighbours and skips any block that is fully enclosed, so only the surface shell of the terrain is ever submitted to OpenGL. Each chunk caches that visible set and invalidates it when a block is removed.

Block hierarchy. Block is abstract and defines getStrength() and getMaterial(). A Material carries one texture id per cube face, so grass can have a green top, dirt bottom and speckled sides. Three subclasses control which render pass a block belongs to:

Block (abstract)
├── SolidBlock       → Grass, Stone, Wood, Lamp
├── TransparentBlock → Leaves
└── LiquidBlock      → Water

Every face maps texture U to the face's horizontal axis and V to world Y, so directional art such as bark grain runs the same way on all four sides of a block.

Entities. Entity handles gravity (−30 u/s², terminal velocity −50 u/s), AABB collision against the block map, and swimming/underwater state. Player extends it with camera control and input; Animal extends it with a textured cube body and wandering behaviour.

Picking. Camera.getLookingAtBlock() marches a ray forward from the camera in 0.1-unit steps up to 5 units, returning the first non-liquid block it hits. That block gets a white wireframe outline and receives damage while the mouse is held.

Project layout

src/main/java/com/clouddrop/
├── Main.java              entry point
├── Game.java              window, frame loop, GL setup
├── Window.java            GLFW window wrapper
├── Camera.java            view matrix, fog, crosshair, ray picking
├── Chunk.java             16×16 block column, generation and render passes
├── Skybox.java            sun direction and sky colour over the day cycle
├── Material.java          per-face texture ids
├── Textures.java          texture id registry
├── Location.java          x/y/z plus owning world and chunk
├── worlds/World.java      chunk grid, animals, render ordering
├── objects/               Block hierarchy, PointLight
│   ├── blocks/            Grass, Stone, Wood, Leaves, Water, Lamp
│   ├── templates/         Tree, LargeTree structure generators
│   └── entities/          Entity, Player, Animal, animals/Pig
└── utils/
    ├── NoiseGenerator.java  Perlin noise with octave support
    ├── Frustum.java         six-plane frustum extraction
    └── TextureLoader.java   classpath → stb → GL texture

tools/TextureGenerator.java  generates every PNG in src/main/resources
src/main/resources/          the generated 16×16 textures
docs/screenshots/            images used by this README

Textures

Every texture is generated, not drawn by hand or copied from anywhere. tools/TextureGenerator.java produces all fifteen PNGs from seeded value noise and simple maths — mottled grass, tiling bark grain, concentric end grain on logs, sine-ripple water, radial lamp glow, and a six-stage crack overlay. The noise is sampled with wraparound so each tile meets its own edges seamlessly when blocks repeat.

The generated files are committed, so you do not need to run this to play. To re-roll the art:

mvn exec:exec@textures
# or, without Maven:
java tools/TextureGenerator.java src/main/resources

Change a SEED constant in the generator and re-run to get a different variation in the same style. All generated assets are covered by this project's MIT license.

Tuning the world

Most of the interesting knobs are constants you can edit directly:

What Where
World size (16 × 16 chunks) World.init()
Render distance (3 chunks) World.renderChunksAroundPlayer()
Ocean level, tree/pig spawn rates Chunk.generateChunk()
Terrain octaves, persistence, scale Chunk.generateChunk()generateTerrainHeight
Gravity, terminal velocity Entity
Walk / sprint / sneak / swim speeds Player.processInput()
Fog density and range Camera.initFog()
Day length (20 min) World.dayDurationMillis

Known limitations

Worth knowing before you dig in — these are all open for contribution:

  • The world is a fixed 16 × 16 chunk grid. There is no chunk streaming, so you can walk off the edge.
  • The day/night cycle is implemented in Skybox but pinned to midday — World.update() overwrites the computed timeOfDayFactor with a hard-coded 0.6f. Remove that line to let time run.
  • Rendering uses the OpenGL 1.x fixed-function pipeline (glBegin/glEnd immediate mode) rather than VBOs and shaders. It is simple to read but caps the achievable frame rate.
  • Blocks can be broken but not placed.
  • There is no world persistence; every launch generates a fresh world from a time-based seed.
  • Lamp point lighting exists (Chunk.renderLighting()) but is not called from the render loop. The generator also uses lamp blocks to pave the sea floor, which is why the shallows glow amber.

Contributing

Contributions are welcome. See CONTRIBUTING.md.

License

MIT — code and generated assets alike.

Cubeworld is an independent, non-commercial learning project, not affiliated with or endorsed by any other game or its publisher.

About

A voxel sandbox game written from scratch in Java with LWJGL and OpenGL — procedural terrain, water, wildlife and block breaking. Runs with one command.

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages