#pragma once #include "StarVariant.hpp" #include "StarRect.hpp" #include "StarMultiArray.hpp" #include "StarMap.hpp" #include "StarOrderedSet.hpp" #include "StarRandom.hpp" #include "StarBlockAllocator.hpp" namespace Star { struct CellularLiquidCollisionCell {}; template struct CellularLiquidFlowCell { Maybe liquid; float level; float pressure; }; template struct CellularLiquidSourceCell { LiquidId liquid; float pressure; }; template using CellularLiquidCell = Variant, CellularLiquidSourceCell>; template struct CellularLiquidWorld { virtual ~CellularLiquidWorld(); virtual Vec2I uniqueLocation(Vec2I const& location) const; virtual CellularLiquidCell cell(Vec2I const& location) const = 0; // Should return an amount between 0.0 and 1.0 as a percentage of liquid // drain at this position virtual float drainLevel(Vec2I const& location) const; // Will be called only on cells which for which the cell method returned a // flow cell, to update the flow cell. virtual void setFlow(Vec2I const& location, CellularLiquidFlowCell const& flow) = 0; // Called once for every active liquid <-> liquid interaction of different // liquid types each update. Will be called AFTER pushing all the flow // values back out so modifications to liquids are sensible. virtual void liquidInteraction(Vec2I const& a, LiquidId aLiquid, Vec2I const& b, LiquidId bLiquid); // Called once for every liquid collision each update. Also called after // pushing all the flow values out, so changes to liquids can sensibly be // performed here. virtual void liquidCollision(Vec2I const& pos, LiquidId liquid, Vec2I const& collisionPos); }; struct LiquidCellEngineParameters { float lateralMoveFactor; float spreadOverfillUpFactor; float spreadOverfillLateralFactor; float spreadOverfillDownFactor; float pressureEqualizeFactor; float pressureMoveFactor; float maximumPressureLevelImbalance; float minimumLivenPressureChange; float minimumLivenLevelChange; float minimumLiquidLevel; float interactTransformationLevel; }; template class LiquidCellEngine { public: typedef shared_ptr> CellularLiquidWorldPtr; LiquidCellEngine(LiquidCellEngineParameters parameters, CellularLiquidWorldPtr cellWorld); unsigned liquidTickDelta(LiquidId liquid); void setLiquidTickDelta(LiquidId liquid, unsigned tickDelta); void setProcessingLimit(Maybe processingLimit); List noProcessingLimitRegions() const; void setNoProcessingLimitRegions(List noProcessingLimitRegions); void visitLocation(Vec2I const& location); void visitRegion(RectI const& region); void update(); size_t activeCells() const; size_t activeCells(LiquidId liquid) const; bool isActive(Vec2I const& pos) const; private: enum class Adjacency { Left, Right, Bottom, Top }; struct WorkingCell { Vec2I position; Maybe liquid; bool sourceCell; float level; float pressure; WorkingCell* leftCell; WorkingCell* rightCell; WorkingCell* topCell; WorkingCell* bottomCell; }; template using BAHashMap = StableHashMap, std::equal_to, BlockAllocator, 4096>>; template using BAHashSet = HashSet, std::equal_to>; template using BAOrderedHashSet = OrderedHashSet, std::equal_to, BlockAllocator>; void setup(); void applyPressure(); void spreadPressure(); void limitPressure(); void pressureMove(); void spreadOverfill(); void levelMove(); void findInteractions(); void finish(); WorkingCell* workingCell(Vec2I p); WorkingCell* adjacentCell(WorkingCell* cell, Adjacency adjacency); void setPressure(float pressure, WorkingCell& cell); void transferPressure(float amount, WorkingCell& source, WorkingCell& dest, bool allowReverse); void transferLevel(float amount, WorkingCell& source, WorkingCell& dest, bool allowReverse); void setLevel(float level, WorkingCell& cell); RandomSource m_random; LiquidCellEngineParameters m_engineParameters; CellularLiquidWorldPtr m_cellWorld; BAHashMap> m_activeCells; BAHashMap m_liquidTickDeltas; Maybe m_processingLimit; List m_noProcessingLimitRegions; uint64_t m_step; BAHashMap> m_workingCells; List m_currentActiveCells; BAHashSet m_nextActiveCells; BAHashSet> m_liquidInteractions; BAHashSet> m_liquidCollisions; }; template CellularLiquidWorld::~CellularLiquidWorld() {} template Vec2I CellularLiquidWorld::uniqueLocation(Vec2I const& location) const { return location; } template float CellularLiquidWorld::drainLevel(Vec2I const&) const { return 0.0f; } template void CellularLiquidWorld::liquidInteraction(Vec2I const&, LiquidId, Vec2I const&, LiquidId) {} template void CellularLiquidWorld::liquidCollision(Vec2I const&, LiquidId, Vec2I const&) {} template LiquidCellEngine::LiquidCellEngine(LiquidCellEngineParameters parameters, CellularLiquidWorldPtr cellWorld) : m_engineParameters(parameters), m_cellWorld(cellWorld), m_step(0) {} template unsigned LiquidCellEngine::liquidTickDelta(LiquidId liquid) { return m_liquidTickDeltas.value(liquid, 1); } template void LiquidCellEngine::setLiquidTickDelta(LiquidId liquid, unsigned tickDelta) { m_liquidTickDeltas[liquid] = tickDelta; } template void LiquidCellEngine::setProcessingLimit(Maybe processingLimit) { m_processingLimit = processingLimit; } template List LiquidCellEngine::noProcessingLimitRegions() const { return m_noProcessingLimitRegions; } template void LiquidCellEngine::setNoProcessingLimitRegions(List noProcessingLimitRegions) { m_noProcessingLimitRegions = noProcessingLimitRegions; } template void LiquidCellEngine::visitLocation(Vec2I const& p) { m_nextActiveCells.add(p); } template void LiquidCellEngine::visitRegion(RectI const& region) { for (int x = region.xMin(); x < region.xMax(); ++x) { for (int y = region.yMin(); y < region.yMax(); ++y) m_nextActiveCells.add({x, y}); } } template void LiquidCellEngine::update() { setup(); applyPressure(); spreadPressure(); limitPressure(); pressureMove(); spreadOverfill(); levelMove(); findInteractions(); finish(); ++m_step; } template size_t LiquidCellEngine::activeCells() const { size_t totalSize = 0; for (auto const& p : m_activeCells) totalSize += p.second.size(); return totalSize; } template size_t LiquidCellEngine::activeCells(LiquidId liquid) const { return m_activeCells.value(liquid).size(); } template bool LiquidCellEngine::isActive(Vec2I const& pos) const { for (auto const& p : m_activeCells) { if (p.second.contains(pos)) return true; } return false; } template void LiquidCellEngine::setup() { // In case an exception occurred during the last update, clear potentially // stale data here m_workingCells.clear(); m_currentActiveCells.clear(); for (auto& activeCellsPair : m_activeCells) { unsigned tickDelta = liquidTickDelta(activeCellsPair.first); if (tickDelta == 0 || m_step % tickDelta != 0) continue; size_t limitedCellNumber = 0; for (auto const& pos : activeCellsPair.second.values()) { if (m_processingLimit) { bool foundInUnlimitedRegion = false; for (auto const& region : m_noProcessingLimitRegions) { if (region.contains(pos)) { foundInUnlimitedRegion = true; break; } } if (!foundInUnlimitedRegion) { if (limitedCellNumber < *m_processingLimit) ++limitedCellNumber; else continue; } } auto cell = workingCell(pos); if (!cell || cell->liquid != activeCellsPair.first) { activeCellsPair.second.remove(pos); } else { m_currentActiveCells.append(cell); activeCellsPair.second.remove(pos); } } } sort(m_currentActiveCells, [](WorkingCell* lhs, WorkingCell* rhs) { return lhs->position[1] < rhs->position[1]; }); } template void LiquidCellEngine::applyPressure() { for (auto const& selfCell : m_currentActiveCells) { if (!selfCell->liquid || selfCell->sourceCell) continue; auto topCell = adjacentCell(selfCell, Adjacency::Top); if (topCell && selfCell->liquid == topCell->liquid) setPressure(max(selfCell->pressure, topCell->pressure + min(topCell->level, 1.0f)), *selfCell); } } template void LiquidCellEngine::spreadPressure() { for (auto const& selfCell : m_currentActiveCells) { if (!selfCell->liquid) continue; auto spreadPressure = [&](Adjacency adjacency, float bias) { auto targetCell = adjacentCell(selfCell, adjacency); if (targetCell && !targetCell->sourceCell) transferPressure((selfCell->pressure + bias - targetCell->pressure) * m_engineParameters.pressureEqualizeFactor, *selfCell, *targetCell, true); }; if (m_random.randb()) { spreadPressure(Adjacency::Left, 0.0f); spreadPressure(Adjacency::Right, 0.0f); } else { spreadPressure(Adjacency::Right, 0.0f); spreadPressure(Adjacency::Left, 0.0f); } spreadPressure(Adjacency::Bottom, 1.0f); spreadPressure(Adjacency::Top, -1.0f); } } template void LiquidCellEngine::limitPressure() { for (auto const& selfCell : m_currentActiveCells) { float level = min(selfCell->level, 1.0f); auto topCell = adjacentCell(selfCell, Adjacency::Top); // Force the pressure to the cell level if there is empty space above, // otherwise simply make sure the pressure is at least the level if (topCell && !topCell->liquid) setPressure(level, *selfCell); else setPressure(max(selfCell->pressure, level), *selfCell); } } template void LiquidCellEngine::pressureMove() { for (auto const& selfCell : m_currentActiveCells) { if (!selfCell->liquid) continue; auto pressureMove = [&](Adjacency adjacency) { auto targetCell = adjacentCell(selfCell, adjacency); if (targetCell && !targetCell->sourceCell && targetCell->level >= selfCell->level) { float amount = (selfCell->pressure - targetCell->pressure) * m_engineParameters.pressureMoveFactor; amount = min(amount, selfCell->level - (1.0f - m_engineParameters.maximumPressureLevelImbalance)); amount = min(amount, (1.0f + m_engineParameters.maximumPressureLevelImbalance) - targetCell->level); transferLevel(amount, *selfCell, *targetCell, false); } }; if (m_random.randb()) { pressureMove(Adjacency::Left); pressureMove(Adjacency::Right); } else { pressureMove(Adjacency::Right); pressureMove(Adjacency::Left); } } } template void LiquidCellEngine::spreadOverfill() { for (auto const& selfCell : m_currentActiveCells) { if (!selfCell->liquid || selfCell->sourceCell) continue; auto spreadOverfill = [&](Adjacency adjacency, float factor) { float overfill = selfCell->level - 1.0f; if (overfill > 0.0f) { auto targetCell = adjacentCell(selfCell, adjacency); if (targetCell) transferLevel(min(overfill, (selfCell->level - targetCell->level)) * factor, *selfCell, *targetCell, false); } }; spreadOverfill(Adjacency::Top, m_engineParameters.spreadOverfillUpFactor); if (m_random.randb()) { spreadOverfill(Adjacency::Left, m_engineParameters.spreadOverfillLateralFactor); spreadOverfill(Adjacency::Right, m_engineParameters.spreadOverfillLateralFactor); } else { spreadOverfill(Adjacency::Right, m_engineParameters.spreadOverfillLateralFactor); spreadOverfill(Adjacency::Left, m_engineParameters.spreadOverfillLateralFactor); } spreadOverfill(Adjacency::Bottom, m_engineParameters.spreadOverfillDownFactor); } } template void LiquidCellEngine::levelMove() { for (auto const& selfCell : m_currentActiveCells) { if (!selfCell->liquid) continue; auto belowCell = adjacentCell(selfCell, Adjacency::Bottom); if (belowCell) transferLevel(min(1.0f - belowCell->level, selfCell->level), *selfCell, *belowCell, false); setLevel(selfCell->level * (1.0f - m_cellWorld->drainLevel(selfCell->position)), *selfCell); auto lateralMove = [&](Adjacency adjacency) { auto targetCell = adjacentCell(selfCell, adjacency); if (targetCell) transferLevel((selfCell->level - targetCell->level) * m_engineParameters.lateralMoveFactor, *selfCell, *targetCell, false); }; if (m_random.randb()) { lateralMove(Adjacency::Left); lateralMove(Adjacency::Right); } else { lateralMove(Adjacency::Right); lateralMove(Adjacency::Left); } } } template void LiquidCellEngine::findInteractions() { for (auto const& selfCell : m_currentActiveCells) { if (!selfCell->liquid) continue; for (auto adjacency : {Adjacency::Bottom, Adjacency::Top, Adjacency::Left, Adjacency::Right}) { auto targetCell = adjacentCell(selfCell, adjacency); if (!targetCell) { Vec2I adjacentPos = selfCell->position; if (adjacency == Adjacency::Left) adjacentPos += Vec2I(-1, 0); else if (adjacency == Adjacency::Right) adjacentPos += Vec2I(1, 0); else if (adjacency == Adjacency::Bottom) adjacentPos += Vec2I(0, -1); else if (adjacency == Adjacency::Top) adjacentPos += Vec2I(0, 1); m_liquidCollisions.add(make_tuple(selfCell->position, *selfCell->liquid, adjacentPos)); } else if (targetCell->liquid && *targetCell->liquid != *selfCell->liquid) { if (targetCell->level <= m_engineParameters.interactTransformationLevel || selfCell->level <= m_engineParameters.interactTransformationLevel) { if (selfCell->level > targetCell->level) targetCell->liquid = selfCell->liquid; else selfCell->liquid = targetCell->liquid; } else { // Make sure to add the point pair in a predictable order so that any // combination of Vec2I points will be unique in m_liquidInteractions if (selfCell->position < targetCell->position) m_liquidInteractions.add(make_tuple(selfCell->position, *selfCell->liquid, targetCell->position, *targetCell->liquid)); else m_liquidInteractions.add(make_tuple(targetCell->position, *targetCell->liquid, selfCell->position, *selfCell->liquid)); } } } } } template void LiquidCellEngine::finish() { m_currentActiveCells.clear(); for (auto& workingCellPair : take(m_workingCells)) { if (workingCellPair.second && !workingCellPair.second->sourceCell) { if (workingCellPair.second->liquid) { if (workingCellPair.second->level < m_engineParameters.minimumLiquidLevel) workingCellPair.second->level = 0.0f; } else { workingCellPair.second->level = 0.0f; } if (workingCellPair.second->level == 0.0f) { workingCellPair.second->liquid = {}; workingCellPair.second->pressure = 0.0f; } m_cellWorld->setFlow(workingCellPair.second->position, CellularLiquidFlowCell{ workingCellPair.second->liquid, workingCellPair.second->level, workingCellPair.second->pressure}); } } for (auto const& interaction : take(m_liquidInteractions)) m_cellWorld->liquidInteraction(get<0>(interaction), get<1>(interaction), get<2>(interaction), get<3>(interaction)); for (auto const& interaction : take(m_liquidCollisions)) m_cellWorld->liquidCollision(get<0>(interaction), get<1>(interaction), get<2>(interaction)); for (auto const& c : take(m_nextActiveCells)) { auto visit = [this](Vec2I p) { p = m_cellWorld->uniqueLocation(p); auto cell = workingCell(p); if (cell && cell->liquid) m_activeCells[*cell->liquid].add(p); }; visit(c); visit(c + Vec2I(-1, 0)); visit(c + Vec2I(1, 0)); visit(c + Vec2I(0, -1)); visit(c + Vec2I(0, 1)); } eraseWhere(m_activeCells, [](auto const& p) { return p.second.empty(); }); } template typename LiquidCellEngine::WorkingCell* LiquidCellEngine::workingCell(Vec2I p) { p = m_cellWorld->uniqueLocation(p); auto res = m_workingCells.insert(make_pair(p, Maybe())); if (res.second) { auto cellData = m_cellWorld->cell(p); if (auto flowCell = cellData.template ptr>()) res.first->second = WorkingCell{p, flowCell->liquid, false, flowCell->level, flowCell->pressure, nullptr, nullptr, nullptr, nullptr}; else if (auto sourceCell = cellData.template ptr>()) res.first->second = WorkingCell{p, sourceCell->liquid, true, 1.0f, sourceCell->pressure, nullptr, nullptr, nullptr, nullptr}; } return res.first->second.ptr(); } template typename LiquidCellEngine::WorkingCell* LiquidCellEngine::adjacentCell( WorkingCell* cell, Adjacency adjacency) { auto getCell = [this](WorkingCell*& cellptr, Vec2I cellPos) { if (cellptr) return cellptr; cellptr = workingCell(cellPos); return cellptr; }; if (adjacency == Adjacency::Left) return getCell(cell->leftCell, cell->position + Vec2I(-1, 0)); else if (adjacency == Adjacency::Right) return getCell(cell->rightCell, cell->position + Vec2I(1, 0)); else if (adjacency == Adjacency::Bottom) return getCell(cell->bottomCell, cell->position + Vec2I(0, -1)); else if (adjacency == Adjacency::Top) return getCell(cell->topCell, cell->position + Vec2I(0, 1)); return nullptr; } template void LiquidCellEngine::setPressure(float pressure, WorkingCell& cell) { if (!cell.liquid || cell.sourceCell) return; if (fabs(cell.pressure - pressure) > m_engineParameters.minimumLivenPressureChange) m_nextActiveCells.add(cell.position); cell.pressure = pressure; } template void LiquidCellEngine::transferPressure(float amount, WorkingCell& source, WorkingCell& dest, bool allowReverse) { if (amount < 0.0f && allowReverse) { return transferPressure(-amount, dest, source, false); } else if (amount > 0.0f) { if (!source.liquid) return; if (source.sourceCell && dest.sourceCell) return; if (dest.liquid && dest.liquid != source.liquid) return; amount = min(amount, source.pressure); if (!source.sourceCell) source.pressure -= amount; if (dest.liquid && !dest.sourceCell) dest.pressure += amount; if (amount > m_engineParameters.minimumLivenPressureChange) { m_nextActiveCells.add(source.position); m_nextActiveCells.add(dest.position); } } } template void LiquidCellEngine::setLevel(float level, WorkingCell& cell) { if (!cell.liquid || cell.sourceCell) return; if (fabs(cell.level - level) > m_engineParameters.minimumLivenLevelChange) m_nextActiveCells.add(cell.position); cell.level = level; if (cell.level <= 0.0f) { cell.liquid = {}; cell.level = 0.0f; } } template void LiquidCellEngine::transferLevel( float amount, WorkingCell& source, WorkingCell& dest, bool allowReverse) { if (amount < 0.0f && allowReverse) { transferLevel(-amount, dest, source, false); } else if (amount > 0.0f) { if (!source.liquid) return; if (source.sourceCell && dest.sourceCell) return; if (dest.liquid && dest.liquid != source.liquid) return; amount = min(amount, source.level); if (!source.sourceCell) source.level -= amount; if (!dest.sourceCell) { dest.level += amount; dest.liquid = source.liquid; } if (!source.sourceCell && source.level == 0.0f) source.liquid = {}; if (amount > m_engineParameters.minimumLivenLevelChange) { m_nextActiveCells.add(source.position); m_nextActiveCells.add(dest.position); } } } }