#include "world.h" #include #include #include //--- // Jump-flood Voronoi diagram generation //--- static void jumpFlood(int N, WorldCoord *seeds, int8_t *world, int px, int py, int qx, int qy) { if((unsigned)qx >= (unsigned)N || (unsigned)qy >= (unsigned)N) return; int pi = N * py + px; int qi = N * qy + qx; int sp = world[pi]; int sq = world[qi]; if(sp < 0 && sq >= 0) { world[pi] = sq; } else if(world[pi] >= 0 && world[qi] >= 0) { WorldCoord spc = seeds[sp]; WorldCoord sqc = seeds[sq]; int dist_sp_2 = (py-spc.y) * (py-spc.y) + (px-spc.x) * (px-spc.x); int dist_sq_2 = (py-sqc.y) * (py-sqc.y) + (px-sqc.x) * (px-sqc.x); if(dist_sq_2 < dist_sp_2) world[pi] = sq; } } /* This assumes [world] to be of size N*N */ static void voronoi(int8_t *world, int N, WorldCoord *seeds) { int k = N / 2; while(k > 0) { for(int py = 0; py < N; py++) for(int px = 0; px < N; px++) { jumpFlood(N, seeds, world, px, py, px - k, py - k); jumpFlood(N, seeds, world, px, py, px, py - k); jumpFlood(N, seeds, world, px, py, px + k, py - k); jumpFlood(N, seeds, world, px, py, px - k, py); jumpFlood(N, seeds, world, px, py, px, py); jumpFlood(N, seeds, world, px, py, px + k, py); jumpFlood(N, seeds, world, px, py, px - k, py + k); jumpFlood(N, seeds, world, px, py, px, py + k); jumpFlood(N, seeds, world, px, py, px + k, py + k); } k >>= 1; } } //--- // Perlin noise //--- static vec2 perlinGradients[16]; GCONSTRUCTOR static void precomputeGradients(void) { for(int i = 0; i < 16; i++) { float alpha = 2 * 3.14159 * i / 16; perlinGradients[i].x = num(cosf(alpha)); perlinGradients[i].y = num(sinf(alpha)); } } static vec2 randomUnitVector(void) { return perlinGradients[rand() % 16]; } static num smooth(num t) { return t * t * t * (t * (6 * t - num(15)) + num(10)); } static num lerp(num x, num y, num t) { return x + t * (y - x); } static num dot(vec2 *grid, int GRID_N, int ix, int iy, num fx, num fy) { /* Wrap the grid around to get a torus effect, so the noise does not appear discontinuous when we repeat it during fractal sampling */ vec2 g = grid[(iy % GRID_N) * GRID_N + (ix % GRID_N)]; return (fx - num(ix)) * g.x + (fy - num(iy)) * g.y; } static int8_t *perlinNoise(int N, int PERLIN_CELL_SIZE) { int8_t *noise = new int8_t[N * N]; if(!noise) return nullptr; int GRID_N = N / PERLIN_CELL_SIZE; vec2 *grid = new vec2[GRID_N * GRID_N]; if(!grid) { delete[] noise; return nullptr; } for(int i = 0; i < GRID_N * GRID_N; i++) grid[i] = randomUnitVector(); for(int y = 0; y < N; y++) for(int x = 0; x < N; x++) { num fx = num(x) / PERLIN_CELL_SIZE; num fy = num(y) / PERLIN_CELL_SIZE; int ix = (int)fx; int iy = (int)fy; num d0 = dot(grid, GRID_N, ix, iy, fx, fy); num d1 = dot(grid, GRID_N, ix+1, iy, fx, fy); num d2 = dot(grid, GRID_N, ix, iy+1, fx, fy); num d3 = dot(grid, GRID_N, ix+1, iy+1, fx, fy); num rx = smooth(fx - num(ix)); num ry = smooth(fy - num(iy)); num v = lerp(lerp(d0, d1, rx), lerp(d2, d3, rx), ry); noise[y * N + x] = (int)(v * num(128)); } delete[] grid; return noise; } static int8_t samplePerlin(int8_t *noise, int N, int x, int y, int levels) { int8_t result = 0; for(int i = 0; i < levels; i++) { /* Add something depending on i to decorrelate octaves */ int wy = ((y << i) + (17 * i + 5)) % N; int wx = ((x << i) + (13 * i + 1)) % N; int tmp = result + (noise[wy * N + wx] >> i); result = max(-128, min(tmp, 127)); } return result; } #include #include void perlinTest(int8_t *noise, int N) { /* int8_t *noise; int N = 128; int PERLIN_CELL_SIZE = 16; noise = perlinNoise(N, PERLIN_CELL_SIZE); dclear(C_RED); */ if(noise) for(int y = 0; y < N; y++) for(int x = 0; x < N; x++) { int v = samplePerlin(noise, N, x, y, 1); int gray = 16 + (v >> 3); dpixel(x, y, C_RGB(gray, gray, gray)); // dpixel(x, y, (v >= -16 && v < 16) ? C_WHITE : C_BLACK); } dupdate(); getkey(); } //--- // World object API //--- WorldCoord WorldCoord::Up(0, -1); WorldCoord WorldCoord::Down(0, 1); WorldCoord WorldCoord::Left(-1, 0); WorldCoord WorldCoord::Right(1, 0); bool World::validSpawn(int x, int y) const { if(!this->limits.contains(WorldCoord(x, y))) return false; BlockInfo *info = Nooncraft::getBlockInfo(this->cellAt(x, y)); return info && info->walkable; } WorldCoord World::findSpawnPoint() { int max_dist_x = max(-this->limits.xmin, this->limits.xmax); int max_dist_y = max(-this->limits.ymin, this->limits.ymax); int max_dist = max(max_dist_x, max_dist_y); for(int d = 0; d <= max_dist; d++) { /* Look for blocks at distance `d` of (0,0) for somewhere spawnable */ for(int x = -d; x <= d; x++) { if(this->validSpawn(x, -d)) return WorldCoord(x, -d); if(this->validSpawn(x, +d)) return WorldCoord(x, +d); } for(int y = -d; y <= d; y++) { if(this->validSpawn(-d, y)) return WorldCoord(-d, y); if(this->validSpawn(+d, y)) return WorldCoord(+d, y); } } return WorldCoord(0, 0); } BlockInfo *World::blockInfoAt(WorldCoord p) const { if(!this->limits.contains(p)) return nullptr; return Nooncraft::getBlockInfo(this->cellAt(p.x, p.y)); } bool World::canBreakBlock(WorldCoord p) const { BlockInfo *info = blockInfoAt(p); return info && info->breakability != Breakability::Fluid && info->breakability != Breakability::Unbreakable; } bool World::canPlaceAt(WorldCoord p) const { BlockInfo *info = this->blockInfoAt(p); return info && info->replaceable; } namespace Nooncraft { World *mkWorld(int width, int height) { WorldRect l, br; World *w = (World *)malloc(sizeof *w); if(!w) goto fail; w->cells = (Block *)malloc(width * height * sizeof *w->cells); if(!w->cells) goto fail; l = WorldRect( -width / 2, width - 1 - width / 2, -height / 2, height - 1 - height / 2); br = WorldRect( l.xmin - 1, l.xmax + 1, l.ymin - 1, l.ymax + 1); w->limits = l; w->worldBorder = br; return w; fail: if(w) free(w->cells); free(w); return nullptr; } static vec2 biomeSort_caveDir; static WorldCoord *biomeSort_biomeCenters; static int biomeSort_compare(void const *b1, void const *b2) { int seed1 = *(int const *)b1; int seed2 = *(int const *)b2; vec2 caveDir = biomeSort_caveDir; WorldCoord *biomeCenters = biomeSort_biomeCenters; /* Largest dot product with caveDir wins */ num dot1 = caveDir.x * biomeCenters[seed1].x + caveDir.y * biomeCenters[seed1].y; num dot2 = caveDir.x * biomeCenters[seed2].x + caveDir.y * biomeCenters[seed2].y; return (dot1 - dot2).v; } static Biome randomNonCaveBiome(void) { Biome options[3] = { Biome::Plains, Biome::Forest, Biome::Ocean }; return options[rand() % 3]; } static void generateCluster(World *w, int xt, int yt, Block b, int count) { int x = xt + w->limits.xmin; int y = yt + w->limits.ymin; /* Very bruteforcey solution. We iterate over a square of size `2*radius` and generate an average of `count` blocks there. We want it relatively compact so we arrange it so that `4*radius^2 ≈ N*count` with N ≥ 1 larger for small clusters. */ int radius = count; int N = (count < 10) ? 4 : (count < 30) ? 2 : 1; while((4 / N) * radius * radius > count) radius--; int x0 = max(x - radius, w->limits.xmin); int x1 = min(x + radius - 1, w->limits.xmax); int y0 = max(y - radius, w->limits.ymin); int y1 = min(y + radius - 1, w->limits.ymax); for(int y = y0; y <= y1; y++) for(int x = x0; x <= x1; x++) { if(rand() % (4 * radius * radius) < count) w->cellAt(x, y) = b; } } static void generateTree(World *w, int xt, int yt) { int x = xt + w->limits.xmin; int y = yt + w->limits.ymin; Block log = BlockID(OAK_LOG); Block leaves = BlockID(OAK_LEAVES); w->trySetCellAt(x - 1, y - 4, leaves); w->trySetCellAt(x, y - 4, leaves); w->trySetCellAt(x + 1, y - 4, leaves); w->trySetCellAt(x - 2, y - 3, leaves); w->trySetCellAt(x - 1, y - 3, leaves); w->trySetCellAt(x + 1, y - 3, leaves); w->trySetCellAt(x + 2, y - 3, leaves); w->trySetCellAt(x - 2, y - 2, leaves); w->trySetCellAt(x - 1, y - 2, leaves); w->trySetCellAt(x + 1, y - 2, leaves); w->trySetCellAt(x + 2, y - 2, leaves); w->trySetCellAt(x - 1, y - 1, leaves); w->trySetCellAt(x + 1, y - 1, leaves); w->trySetCellAt(x, y - 3, log); w->trySetCellAt(x, y - 2, log); w->trySetCellAt(x, y - 1, log); w->trySetCellAt(x, y, log); w->trySetCellAt(x - 1, y - 2, log); w->trySetCellAt(x + 1, y - 2, log); } void genWorld(World *w) { int WIDTH = w->limits.width(); int HEIGHT = w->limits.height(); // --== Generate a Perlin noise map for various random things --== int PERLIN_N = 128; int PERLIN_CELL_SIZE = 16; int8_t *noise = perlinNoise(PERLIN_N, PERLIN_CELL_SIZE); // --== Generate biome diagram ==-- int8_t *biomeMap = new int8_t[WIDTH * HEIGHT]; if(!biomeMap) return; memset(biomeMap, 0xff, WIDTH * HEIGHT); /* Generate random biome centers */ int BIOME_COUNT = 16; WorldCoord biomeCenters[BIOME_COUNT]; for(int i = 0; i < BIOME_COUNT; i++) { biomeCenters[i] = WorldCoord(rand() % WIDTH, rand() % HEIGHT); biomeMap[WIDTH * biomeCenters[i].y + biomeCenters[i].x] = i; } /* This will no longer work if we change Block to be a 16-bit value */ static_assert(sizeof(Block) == 1); voronoi(biomeMap, min(WIDTH, HEIGHT), biomeCenters); /* Move the biome boundaries to have irregular shapes */ for(int y = 0; y < HEIGHT; y++) for(int x = 0; x < WIDTH; x++) { /* Shake biomes around by copying the value of a nearby cell, with distance on x/y given by some Perlin map. We reuse the same map */ int8_t dx = samplePerlin(noise, PERLIN_N, x, y, 3); int8_t dy = samplePerlin(noise, PERLIN_N, PERLIN_N-x, PERLIN_N-y, 3); /* Restrict the displacement from ±128 to ±4 */ dx >>= 5; dy >>= 5; WorldCoord neighbor(x+dx + w->limits.xmin, y+dy + w->limits.ymin); if(w->limits.contains(neighbor)) { biomeMap[y * WIDTH + x] = biomeMap[(y+dy) * WIDTH + (x+dx)]; } } // --== Fill in the biome types ==-- Biome biomeTypes[BIOME_COUNT]; for(int i = 0; i < BIOME_COUNT; i++) { biomeTypes[i] = randomNonCaveBiome(); } // --== Fill in some cave biomes ==-- /* Select a random direction where cave biomes will be */ vec2 caveDir = randomUnitVector(); /* Sort biomes by distance to caveDir */ int biomeSort[BIOME_COUNT]; for(int i = 0; i < BIOME_COUNT; i++) biomeSort[i] = i; biomeSort_caveDir = caveDir; biomeSort_biomeCenters = biomeCenters; qsort(biomeSort, BIOME_COUNT, sizeof biomeSort[0], biomeSort_compare); /* The top 6 biomes in this direction become cave biomes */ for(int i = BIOME_COUNT - 1; i >= BIOME_COUNT - 6; i--) { biomeTypes[biomeSort[i]] = Biome::Cave; } // --== Ensure a spawn point near plains ==-- int zero_zero_ix = -w->limits.ymin * WIDTH - w->limits.xmin; biomeTypes[biomeMap[zero_zero_ix]] = Biome::Plains; // --== Generate biomes' base layer ==-- for(int y = 0; y <= HEIGHT; y++) for(int x = 0; x <= WIDTH; x++) { int id = biomeMap[y * WIDTH + x]; Biome type = biomeTypes[id]; Block *cell = &w->cells[y * WIDTH + x]; if(type == Biome::Plains) { /* Use the noise map to distinguish grass and dirt areas */ int8_t v = samplePerlin(noise, PERLIN_N, x, y, 2); *cell = (v >= 0) ? BlockID(GRASS) : BlockID(DIRT); } else if(type == Biome::Forest) { /* Small regions of grass */ int8_t v = samplePerlin(noise, PERLIN_N, x, y, 2); *cell = (v >= 64) ? BlockID(GRASS) : BlockID(DIRT); } else if(type == Biome::Ocean) { *cell = BlockID(WATER_SOURCE); } else if(type == Biome::Cave) { *cell = BlockID(STONE); } else *cell = 0xff; } // --== Generate clusters of sub-blocks ==-- for(int i = 0; i < WIDTH * HEIGHT / 64; i++) { int x = rand() % WIDTH; int y = rand() % HEIGHT; Biome b = biomeTypes[biomeMap[y * WIDTH + x]]; if(b == Biome::Plains) { if(rand() % 2 == 0) generateCluster(w, x, y, BlockID(FLOWERS), 5); } if(b == Biome::Forest) { if(rand() % 2 == 0) w->trySetCellAt(x, y, BlockID(OAK_LEAVES)); } if(b == Biome::Cave) { int r = rand() % 12; if(r >= 0 && r <= 2) generateCluster(w, x, y, BlockID(COAL_ORE), 8); else if(r >= 3 && r <= 5) generateCluster(w, x, y, BlockID(IRON_ORE), 8); else if(r == 6) generateCluster(w, x, y, BlockID(GOLD_ORE), 6); else if(r == 7) generateCluster(w, x, y, BlockID(DIAMOND_ORE), 3); } } // --== Carve caves ==-- for(int y = 0; y <= HEIGHT; y++) for(int x = 0; x <= WIDTH; x++) { Biome b = biomeTypes[biomeMap[y * WIDTH + x]]; if(b == Biome::Cave) { int v = samplePerlin(noise, PERLIN_N, x, y, 1); if(v >= -16 && v < 16) w->cells[y * WIDTH + x] = (rand() % 16 == 0) ? BlockID(COBBLESTONE) : BlockID(AIR); } } // --== Generate basic structures ==-- for(int i = 0; i < WIDTH * HEIGHT / 100; i++) { int x = rand() % WIDTH; int y = rand() % HEIGHT; Biome b = biomeTypes[biomeMap[y * WIDTH + x]]; if((b == Biome::Plains && rand() % 4 == 0) || b == Biome::Forest) { generateTree(w, x, y); } } // --== Clean up ==-- delete[] biomeMap; } void freeWorld(World *w) { free(w->cells); free(w); } } /* namespace Nooncraft */