Skill: Use noise
Generate terrain, caves, clouds, and procedural textures with the seedable noise classes (`ValueNoise`, `PerlinNoise`, `SimplexNoise`) and the coordinate...
Written for blit386 1.8.0. The rest of blit386.dev documents the newest engine, so an API you find there may be missing from a game pinned to 1.8.0.
Noise and coordinate hashing answer the question "what is at this spot in the world" without storing a map. Same coordinates and same seed always give the same answer, so a world can be enormous, generated on demand, and still identical every time the player walks back.
When to use
Use for terrain height, cave layouts, clouds, water, procedural textures, tile variation, or scattering trees and rocks across a world larger than memory.
This is the opposite tool to the use-random skill. BT.random is a getter for a shared generator - calling a method
on it (BT.random.int(), ...) draws the next value in a sequence. Noise and hashing are stateless lookups by
coordinate - ask for tile (12, -3) a thousand times and you get the same answer a thousand times.
(Not to be confused with the Noise post-process effect from the add-crt-effect skill - that one is a fullscreen
grain filter on the GPU, nothing to do with world generation.)
Yes or no per tile: coordinate hashing
Use a hash when you want a hard, unrelated answer per coordinate - is there a tree here, which tile variant is this.
import { hash2 } from 'blit386';
const WORLD_SEED = 9001;
hasTreeAt(tileX, tileY) {
return hash2(tileX, tileY, WORLD_SEED) < 0.15; // 15% of tiles, always the same ones
}| Function | Gives you |
|---|---|
hash1(x, seed?), hash2(x, y, seed?), hash3(x, y, z, seed?) | A decimal from 0 up to (not including) 1 |
hash1i(x, seed?), hash2i(x, y, seed?), hash3i(x, y, z, seed?) | A big whole number - good with % to pick a variant |
import { hash2i } from 'blit386';
const variant = hash2i(tileX, tileY, WORLD_SEED) % 4; // 0..3, stable per tileSmooth landscapes: noise
Hashing is jagged - neighboring tiles are unrelated. Noise is smooth, so neighbors resemble each other, which is what makes hills look like hills.
import { PerlinNoise } from 'blit386';
// in init():
this.terrain = new PerlinNoise(9001);
// anywhere:
const height = this.terrain.noise2D(x * 0.05, y * 0.05); // about -1..1The multiplier is the important part. Coordinates go in raw and neighboring whole numbers would land on completely
different values, so scale them down: smaller multiplier means larger, smoother features. Start at 0.05 and adjust
- 0.01 gives broad continents, 0.2 gives tight ripples.
Turning the result into something you can draw. Sample once into an array, then draw from that array every frame:
import { PerlinNoise, Vector2i } from 'blit386';
// in init(): work out the ground height for every screen column, one time only
this.terrain = new PerlinNoise(9001);
this.groundY = [];
for (let x = 0; x < 320; x++) {
const n = this.terrain.noise2D(x * 0.02, 0); // -1..1
this.groundY.push(Math.floor(120 + n * 40)); // round: screen coordinates are whole numbers
}
// in init() as well: two reusable points, so render() allocates nothing
this.top = new Vector2i();
this.bottom = new Vector2i();
// in render(): just read the array back
render() {
for (let x = 0; x < 320; x++) {
this.top.set(x, this.groundY[x]);
this.bottom.set(x, 239);
BT.drawLine(this.top, this.bottom, COLOR_DIRT);
}
}Which class
| Class | Use it for |
|---|---|
ValueNoise | Cheapest. Fine for clouds and soft blobs; can look blocky at large scales |
PerlinNoise | The usual choice for terrain and height maps |
SimplexNoise | Fewer square-grid artifacts; noise2D / noise3D only, no 1D |
All three take a seed in the constructor (new PerlinNoise(9001)) and have a seed(n) method to switch worlds later.
All return roughly -1 to 1.
More detail with fbm
One noise call gives smooth rolling hills. fbm ("fractal Brownian motion") layers several calls at increasing
frequency to add roughness on top of the big shapes - this is what makes terrain look natural instead of like a duvet.
const h = this.terrain.fbm2D(x * 0.02, y * 0.02); // 4 layers by default, still about -1..1
const rougher = this.terrain.fbm2D(x * 0.02, y * 0.02, 6); // more layers, more fine detailExtra arguments are (x, y, octaves, persistence, lacunarity), defaulting to 4, 0.5, and 2. Raising octaves
adds detail and costs more; the other two are worth leaving alone until the shape is right.
Notes
- Generate once, not every frame. Sampling noise for all 320x240 pixels every frame will drop your frame rate. Build the
height map or tile map into an array in
init()(or when the player enters a chunk) and draw from that - see thekeep-it-fastskill. - Keep one world seed in a constant and pass it to every hash call, so the whole world regenerates as a unit when you change it.
- Omit the seed (or pass
0) and you get a fixed default world - handy while experimenting, but two different features will then share a pattern. Give each its own seed once the game is real. - Noise returns decimals around
-1..1. Screen coordinates are whole numbers, soMath.floorthe result before drawing. - Chunked worlds need no bookkeeping: because the answer depends only on coordinates and seed, a chunk regenerates identically when the player returns. Only store what the player changed.
- Needs blit386
^1.5.0.
Skill: Use hot reload
Keep the game running while editing code or assets with the blit386/vite plugin, optional onHotReload state restore, and in-place sprite/audio/font...
Skill: Use the palette (colors)
Set up and change colors with BLIT386's palette-first system of numbered color slots. Use whenever the user sets up colors, mentions colors, themes, or...