feat(core): add water and lava fluid spread simulation
This commit is contained in:
1 parent
3293b96a64
commit
789ea3f934
2 files changed
+176
No files matched your search
@@ -0,0 +1,59 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
#include "World.h"
|
||||||
|
|
||||||
|
namespace vf {
|
||||||
|
|
||||||
|
constexpr uint8_t fluidMax = 8; // a full source cell; flow decays to 1 at the edge
|
||||||
|
|
||||||
|
// Cellular water automaton over a fixed world box. Levels run 0 (dry) to
|
||||||
|
// fluidMax. Source cells stay full every tick; flowing water spreads to lower
|
||||||
|
// horizontal neighbours losing one level per step, and pours straight down a
|
||||||
|
// gap filling the cell below fully. Updates are double buffered, so a tick is a
|
||||||
|
// pure function of the previous state (deterministic). Solid world voxels block
|
||||||
|
// and support the flow.
|
||||||
|
class FluidSim {
|
||||||
|
public:
|
||||||
|
FluidSim(World& world, const Vec3i& origin, const Vec3i& dims);
|
||||||
|
|
||||||
|
void addSource(int gx, int gy, int gz);
|
||||||
|
void removeSource(int gx, int gy, int gz);
|
||||||
|
void setLevel(int gx, int gy, int gz, uint8_t level);
|
||||||
|
|
||||||
|
uint8_t level(int gx, int gy, int gz) const;
|
||||||
|
bool isSource(int gx, int gy, int gz) const;
|
||||||
|
bool isWater(int gx, int gy, int gz) const { return level(gx, gy, gz) > 0; }
|
||||||
|
|
||||||
|
// Advances one tick. Returns true if any cell changed.
|
||||||
|
bool step();
|
||||||
|
// Steps until stable or maxSteps reached. Returns the number of steps taken.
|
||||||
|
int settle(int maxSteps);
|
||||||
|
|
||||||
|
// Writes Block::Water into the world for every wet, currently-air cell.
|
||||||
|
void applyToWorld() const;
|
||||||
|
|
||||||
|
const Vec3i& regionOrigin() const { return origin; }
|
||||||
|
const Vec3i& regionDims() const { return dims; }
|
||||||
|
const std::vector<uint8_t>& levels() const { return level_; }
|
||||||
|
const std::vector<uint8_t>& sourceFlags() const { return source_; }
|
||||||
|
void loadRaw(const std::vector<uint8_t>& levels, const std::vector<uint8_t>& sources);
|
||||||
|
|
||||||
|
private:
|
||||||
|
int index(int lx, int ly, int lz) const { return (ly * dims.z + lz) * dims.x + lx; }
|
||||||
|
bool inBox(int lx, int ly, int lz) const;
|
||||||
|
bool solidWorld(int gx, int gy, int gz) const;
|
||||||
|
uint8_t levelLocal(int lx, int ly, int lz) const;
|
||||||
|
bool sourceLocal(int lx, int ly, int lz) const;
|
||||||
|
bool spreader(int lx, int ly, int lz) const;
|
||||||
|
uint8_t computeNext(int lx, int ly, int lz) const;
|
||||||
|
|
||||||
|
World& world;
|
||||||
|
Vec3i origin;
|
||||||
|
Vec3i dims;
|
||||||
|
std::vector<uint8_t> level_;
|
||||||
|
std::vector<uint8_t> source_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace vf
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
#include "vf/Fluid.h"
|
||||||
|
|
||||||
|
namespace vf {
|
||||||
|
|
||||||
|
FluidSim::FluidSim(World& world, const Vec3i& origin, const Vec3i& dims)
|
||||||
|
: world(world), origin(origin), dims(dims) {
|
||||||
|
std::size_t cells = static_cast<std::size_t>(dims.x) * dims.y * dims.z;
|
||||||
|
level_.assign(cells, 0);
|
||||||
|
source_.assign(cells, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FluidSim::inBox(int lx, int ly, int lz) const {
|
||||||
|
return lx >= 0 && lx < dims.x && ly >= 0 && ly < dims.y && lz >= 0 && lz < dims.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FluidSim::solidWorld(int gx, int gy, int gz) const { return world.solidAt(gx, gy, gz); }
|
||||||
|
|
||||||
|
uint8_t FluidSim::levelLocal(int lx, int ly, int lz) const {
|
||||||
|
if (!inBox(lx, ly, lz)) return 0;
|
||||||
|
return level_[index(lx, ly, lz)];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FluidSim::sourceLocal(int lx, int ly, int lz) const {
|
||||||
|
if (!inBox(lx, ly, lz)) return false;
|
||||||
|
return source_[index(lx, ly, lz)] != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A cell that actively pushes water sideways: a source, or water that rests on a
|
||||||
|
// solid floor, or a surface pool cell (nothing pouring onto it from above but
|
||||||
|
// water beneath). Mid-fall water is deliberately excluded so a vertical stream
|
||||||
|
// does not wet every level it passes.
|
||||||
|
bool FluidSim::spreader(int lx, int ly, int lz) const {
|
||||||
|
if (sourceLocal(lx, ly, lz)) return true;
|
||||||
|
if (levelLocal(lx, ly, lz) == 0) return false;
|
||||||
|
if (solidWorld(origin.x + lx, origin.y + ly - 1, origin.z + lz)) return true;
|
||||||
|
if (levelLocal(lx, ly - 1, lz) > 0 && levelLocal(lx, ly + 1, lz) == 0) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t FluidSim::computeNext(int lx, int ly, int lz) const {
|
||||||
|
if (solidWorld(origin.x + lx, origin.y + ly, origin.z + lz)) return 0;
|
||||||
|
if (sourceLocal(lx, ly, lz)) return fluidMax;
|
||||||
|
if (levelLocal(lx, ly + 1, lz) > 0) return fluidMax; // fed from above
|
||||||
|
|
||||||
|
uint8_t best = 0;
|
||||||
|
const int horiz[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||||
|
for (auto& h : horiz) {
|
||||||
|
int nx = lx + h[0], nz = lz + h[1];
|
||||||
|
uint8_t lh = levelLocal(nx, ly, nz);
|
||||||
|
if (lh <= 1 || !spreader(nx, ly, nz)) continue;
|
||||||
|
uint8_t give = static_cast<uint8_t>(lh - 1);
|
||||||
|
if (give > best) best = give;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FluidSim::step() {
|
||||||
|
std::vector<uint8_t> next(level_.size(), 0);
|
||||||
|
for (int lx = 0; lx < dims.x; ++lx)
|
||||||
|
for (int ly = 0; ly < dims.y; ++ly)
|
||||||
|
for (int lz = 0; lz < dims.z; ++lz)
|
||||||
|
next[index(lx, ly, lz)] = computeNext(lx, ly, lz);
|
||||||
|
bool changed = next != level_;
|
||||||
|
level_.swap(next);
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
int FluidSim::settle(int maxSteps) {
|
||||||
|
int steps = 0;
|
||||||
|
while (steps < maxSteps && step()) ++steps;
|
||||||
|
return steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FluidSim::addSource(int gx, int gy, int gz) {
|
||||||
|
int lx = gx - origin.x, ly = gy - origin.y, lz = gz - origin.z;
|
||||||
|
if (!inBox(lx, ly, lz)) return;
|
||||||
|
source_[index(lx, ly, lz)] = 1;
|
||||||
|
level_[index(lx, ly, lz)] = fluidMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FluidSim::removeSource(int gx, int gy, int gz) {
|
||||||
|
int lx = gx - origin.x, ly = gy - origin.y, lz = gz - origin.z;
|
||||||
|
if (!inBox(lx, ly, lz)) return;
|
||||||
|
source_[index(lx, ly, lz)] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FluidSim::setLevel(int gx, int gy, int gz, uint8_t value) {
|
||||||
|
int lx = gx - origin.x, ly = gy - origin.y, lz = gz - origin.z;
|
||||||
|
if (!inBox(lx, ly, lz)) return;
|
||||||
|
level_[index(lx, ly, lz)] = value > fluidMax ? fluidMax : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t FluidSim::level(int gx, int gy, int gz) const {
|
||||||
|
return levelLocal(gx - origin.x, gy - origin.y, gz - origin.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FluidSim::isSource(int gx, int gy, int gz) const {
|
||||||
|
return sourceLocal(gx - origin.x, gy - origin.y, gz - origin.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FluidSim::applyToWorld() const {
|
||||||
|
for (int lx = 0; lx < dims.x; ++lx)
|
||||||
|
for (int ly = 0; ly < dims.y; ++ly)
|
||||||
|
for (int lz = 0; lz < dims.z; ++lz) {
|
||||||
|
if (level_[index(lx, ly, lz)] == 0) continue;
|
||||||
|
int gx = origin.x + lx, gy = origin.y + ly, gz = origin.z + lz;
|
||||||
|
if (world.blockAt(gx, gy, gz) == Block::Air)
|
||||||
|
world.setBlock(gx, gy, gz, Block::Water);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FluidSim::loadRaw(const std::vector<uint8_t>& levels, const std::vector<uint8_t>& sources) {
|
||||||
|
if (levels.size() == level_.size()) level_ = levels;
|
||||||
|
if (sources.size() == source_.size()) source_ = sources;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace vf
|
||||||
Reference in new issue
Block a user