From 05a8f470067ab8f9542200836b12f2ea65b270ea Mon Sep 17 00:00:00 2001 From: Minjae Song Date: Thu, 13 Dec 2018 04:45:09 +0900 Subject: [PATCH] implementing water sim but not actually working --- src/net/torvald/terrarum/Terrarum.kt | 3 +- .../torvald/terrarum/blockproperties/Fluid.kt | 8 +- .../torvald/terrarum/gameworld/GameWorld.kt | 64 +++--- .../torvald/terrarum/modulebasegame/Ingame.kt | 2 +- .../gameworld/WorldSimulator.kt | 199 ++++++++++++++++-- .../terrarum/serialise/ReadLayerDataLzma.kt | 5 +- 6 files changed, 230 insertions(+), 51 deletions(-) diff --git a/src/net/torvald/terrarum/Terrarum.kt b/src/net/torvald/terrarum/Terrarum.kt index b35f506e9..5398680b7 100644 --- a/src/net/torvald/terrarum/Terrarum.kt +++ b/src/net/torvald/terrarum/Terrarum.kt @@ -33,6 +33,7 @@ import java.io.File import java.io.IOException import net.torvald.getcpuname.GetCpuName import net.torvald.terrarum.modulebasegame.Ingame +import kotlin.math.absoluteValue @@ -908,7 +909,7 @@ inline fun Double.abs() = Math.abs(this) inline fun Double.sqr() = this * this inline fun Double.sqrt() = Math.sqrt(this) inline fun Float.sqrt() = FastMath.sqrt(this) -inline fun Int.abs() = if (this < 0) -this else this +inline fun Int.abs() = this.absoluteValue fun Double.bipolarClamp(limit: Double) = this.coerceIn(-limit, limit) diff --git a/src/net/torvald/terrarum/blockproperties/Fluid.kt b/src/net/torvald/terrarum/blockproperties/Fluid.kt index c3207edf4..aa918cf51 100644 --- a/src/net/torvald/terrarum/blockproperties/Fluid.kt +++ b/src/net/torvald/terrarum/blockproperties/Fluid.kt @@ -1,13 +1,15 @@ package net.torvald.terrarum.blockproperties +import net.torvald.terrarum.gameworld.FluidType + /** * Created by minjaesong on 2016-08-06. */ object Fluid { - val NULL = 0 + val NULL = FluidType(0) - val WATER = 1 - val STATIC_WATER = -1 + val WATER = FluidType(1) + val STATIC_WATER = FluidType(-1) } \ No newline at end of file diff --git a/src/net/torvald/terrarum/gameworld/GameWorld.kt b/src/net/torvald/terrarum/gameworld/GameWorld.kt index 865567a88..ba51ae32e 100644 --- a/src/net/torvald/terrarum/gameworld/GameWorld.kt +++ b/src/net/torvald/terrarum/gameworld/GameWorld.kt @@ -5,9 +5,11 @@ import com.badlogic.gdx.graphics.Color import net.torvald.terrarum.blockproperties.Block import net.torvald.terrarum.realestate.LandUtil import net.torvald.terrarum.blockproperties.BlockCodex +import net.torvald.terrarum.blockproperties.Fluid import net.torvald.terrarum.modulebasegame.gameworld.WorldSimulator import net.torvald.terrarum.serialise.ReadLayerDataLzma import org.dyn4j.geometry.Vector2 +import kotlin.math.absoluteValue typealias BlockAddress = Long @@ -45,7 +47,7 @@ open class GameWorld { val wallDamages: HashMap val terrainDamages: HashMap - val fluidTypes: HashMap + val fluidTypes: HashMap val fluidFills: HashMap //public World physWorld = new World( new Vec2(0, -Terrarum.game.gravitationalAccel) ); @@ -79,7 +81,7 @@ open class GameWorld { wallDamages = HashMap() terrainDamages = HashMap() - fluidTypes = HashMap() + fluidTypes = HashMap() fluidFills = HashMap() // temperature layer: 2x2 is one cell @@ -154,8 +156,8 @@ open class GameWorld { get() = layerTerrainLowBits.data fun getTileFromWall(x: Int, y: Int): Int? { - val wall: Int? = layerWall.getTile(x fmod width, y) - val wallDamage: Int? = getWallLowBits(x fmod width, y) + val wall: Int? = layerWall.getTile(x fmod width, y.coerceWorld().coerceWorld()) + val wallDamage: Int? = getWallLowBits(x fmod width, y.coerceWorld()) return if (wall == null || wallDamage == null) null else @@ -163,8 +165,8 @@ open class GameWorld { } fun getTileFromTerrain(x: Int, y: Int): Int? { - val terrain: Int? = layerTerrain.getTile(x fmod width, y) - val terrainDamage: Int? = getTerrainLowBits(x fmod width, y) + val terrain: Int? = layerTerrain.getTile(x fmod width, y.coerceWorld()) + val terrainDamage: Int? = getTerrainLowBits(x fmod width, y.coerceWorld()) return if (terrain == null || terrainDamage == null) null else @@ -172,15 +174,15 @@ open class GameWorld { } fun getTileFromWire(x: Int, y: Int): Int? { - return layerWire.getTile(x fmod width, y) + return layerWire.getTile(x fmod width, y.coerceWorld()) } fun getWallLowBits(x: Int, y: Int): Int? { - return layerWallLowBits.getData(x fmod width, y) + return layerWallLowBits.getData(x fmod width, y.coerceWorld()) } fun getTerrainLowBits(x: Int, y: Int): Int? { - return layerTerrainLowBits.getData(x fmod width, y) + return layerTerrainLowBits.getData(x fmod width, y.coerceWorld()) } /** @@ -192,7 +194,7 @@ open class GameWorld { * @param combinedTilenum (tilenum * 16) + damage */ fun setTileWall(x: Int, y: Int, combinedTilenum: Int) { - setTileWall(x fmod width, y, (combinedTilenum / PairedMapLayer.RANGE).toByte(), combinedTilenum % PairedMapLayer.RANGE) + setTileWall(x fmod width, y.coerceWorld(), (combinedTilenum / PairedMapLayer.RANGE).toByte(), combinedTilenum % PairedMapLayer.RANGE) } /** @@ -204,23 +206,23 @@ open class GameWorld { * @param combinedTilenum (tilenum * 16) + damage */ fun setTileTerrain(x: Int, y: Int, combinedTilenum: Int) { - setTileTerrain(x fmod width, y, (combinedTilenum / PairedMapLayer.RANGE).toByte(), combinedTilenum % PairedMapLayer.RANGE) + setTileTerrain(x fmod width, y.coerceWorld(), (combinedTilenum / PairedMapLayer.RANGE).toByte(), combinedTilenum % PairedMapLayer.RANGE) } fun setTileWall(x: Int, y: Int, tile: Byte, damage: Int) { - layerWall.setTile(x fmod width, y, tile) - layerWallLowBits.setData(x fmod width, y, damage) + layerWall.setTile(x fmod width, y.coerceWorld(), tile) + layerWallLowBits.setData(x fmod width, y.coerceWorld(), damage) wallDamages.remove(LandUtil.getBlockAddr(this, x, y)) } fun setTileTerrain(x: Int, y: Int, tile: Byte, damage: Int) { - layerTerrain.setTile(x fmod width, y, tile) - layerTerrainLowBits.setData(x fmod width, y, damage) + layerTerrain.setTile(x fmod width, y.coerceWorld(), tile) + layerTerrainLowBits.setData(x fmod width, y.coerceWorld(), damage) terrainDamages.remove(LandUtil.getBlockAddr(this, x, y)) } fun setTileWire(x: Int, y: Int, tile: Byte) { - layerWire.setTile(x fmod width, y, tile) + layerWire.setTile(x fmod width, y.coerceWorld(), tile) } fun getTileFrom(mode: Int, x: Int, y: Int): Int? { @@ -340,13 +342,20 @@ open class GameWorld { fun getWallDamage(x: Int, y: Int): Float = wallDamages[LandUtil.getBlockAddr(this, x, y)] ?: 0f - fun setFluid(x: Int, y: Int, fluidType: Int, fill: Float) { + fun setFluid(x: Int, y: Int, fluidType: FluidType, fill: Float) { val addr = LandUtil.getBlockAddr(this, x, y) + // fluid completely drained if (fill <= WorldSimulator.FLUID_MIN_MASS) { - fluidTypes.remove(addr) - fluidFills.remove(addr) - setTileTerrain(x, y, 0) + /**********/ fluidTypes.remove(addr) + val oldMap = fluidFills.remove(addr) + + // oldMap not being null means there actually was a fluid there, so we can put AIR onto it + // otherwise, it means it was some solid and therefore we DON'T want to put AIR onto it + if (oldMap != null) { + setTileTerrain(x, y, 0) + } } + // update the fluid amount else { fluidTypes[addr] = fluidType fluidFills[addr] = fill @@ -354,27 +363,28 @@ open class GameWorld { } } - fun getFluid(x: Int, y: Int): Pair? { + fun getFluid(x: Int, y: Int): FluidInfo { val addr = LandUtil.getBlockAddr(this, x, y) val fill = fluidFills[addr] val type = fluidTypes[addr] - return if (type == null) null else Pair(type!!, fill!!) + return if (type == null) FluidInfo(Fluid.NULL, 0f) else FluidInfo(type, fill!!) } + data class FluidInfo(val type: FluidType, val amount: Float) + fun getTemperature(worldTileX: Int, worldTileY: Int): Float? { return null - //return layerThermal.getValue((worldTileX fmod width) / 2, worldTileY / 2) } fun getAirPressure(worldTileX: Int, worldTileY: Int): Float? { return null - //return layerFluidPressure.getValue((worldTileX fmod width) / 4, worldTileY / 8) } - + private fun Int.coerceWorld() = this.coerceIn(0, height - 1) + companion object { @Transient val WALL = 0 @Transient val TERRAIN = 1 @@ -390,3 +400,7 @@ open class GameWorld { infix fun Int.fmod(other: Int) = Math.floorMod(this, other) infix fun Float.fmod(other: Float) = if (this >= 0f) this % other else (this % other) + other + +inline class FluidType(val value: Int) { + infix fun sameAs(other: FluidType) = this.value.absoluteValue == other.value.absoluteValue +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/modulebasegame/Ingame.kt b/src/net/torvald/terrarum/modulebasegame/Ingame.kt index 9278a93c5..987d58a1f 100644 --- a/src/net/torvald/terrarum/modulebasegame/Ingame.kt +++ b/src/net/torvald/terrarum/modulebasegame/Ingame.kt @@ -598,7 +598,7 @@ open class Ingame(batch: SpriteBatch) : IngameInstance(batch) { } actorNowPlaying = newActor - WorldSimulator(actorNowPlaying, Terrarum.deltaTime) + //WorldSimulator(actorNowPlaying, Terrarum.deltaTime) } private fun changePossession(refid: Int) { diff --git a/src/net/torvald/terrarum/modulebasegame/gameworld/WorldSimulator.kt b/src/net/torvald/terrarum/modulebasegame/gameworld/WorldSimulator.kt index e25bbe4bc..7bbc71c17 100644 --- a/src/net/torvald/terrarum/modulebasegame/gameworld/WorldSimulator.kt +++ b/src/net/torvald/terrarum/modulebasegame/gameworld/WorldSimulator.kt @@ -1,17 +1,13 @@ package net.torvald.terrarum.modulebasegame.gameworld import com.badlogic.gdx.graphics.Color -import com.badlogic.gdx.graphics.g2d.SpriteBatch import net.torvald.terrarum.Terrarum -import net.torvald.terrarum.roundInt -import net.torvald.terrarum.worlddrawer.BlocksDrawer -import net.torvald.terrarum.worlddrawer.FeaturesDrawer import net.torvald.terrarum.blockproperties.Block +import net.torvald.terrarum.roundInt +import net.torvald.terrarum.worlddrawer.FeaturesDrawer import net.torvald.terrarum.blockproperties.BlockCodex import net.torvald.terrarum.blockproperties.Fluid -import net.torvald.terrarum.gameworld.FluidCodex -import net.torvald.terrarum.gameworld.GameWorld -import net.torvald.terrarum.modulebasegame.Ingame +import net.torvald.terrarum.gameworld.FluidType import net.torvald.terrarum.modulebasegame.gameactors.ActorHumanoid /** @@ -25,15 +21,20 @@ object WorldSimulator { * In tiles; * square width/height = field * 2 */ - const val FLUID_UPDATING_SQUARE_RADIUS = 64 // larger value will have dramatic impact on performance + const val FLUID_UPDATING_SQUARE_RADIUS = 80 // larger value will have dramatic impact on performance const private val DOUBLE_RADIUS = FLUID_UPDATING_SQUARE_RADIUS * 2 + // maps are separated as old-new for obvious reason, also it'll allow concurrent modification private val fluidMap = Array(DOUBLE_RADIUS, { FloatArray(DOUBLE_RADIUS) }) - private val fluidTypeMap = Array(DOUBLE_RADIUS, { IntArray(DOUBLE_RADIUS) }) + private val fluidTypeMap = Array(DOUBLE_RADIUS, { Array(DOUBLE_RADIUS) { Fluid.NULL } }) + private val fluidNewMap = Array(DOUBLE_RADIUS, { FloatArray(DOUBLE_RADIUS) }) + private val fluidNewTypeMap = Array(DOUBLE_RADIUS, { Array(DOUBLE_RADIUS) { Fluid.NULL } }) const val FLUID_MAX_MASS = 1f // The normal, un-pressurized mass of a full water cell - const val FLUID_MAX_COMP = 0.02f // How much excess water a cell can store, compared to the cell above it + const val FLUID_MAX_COMP = 0.02f // How much excess water a cell can store, compared to the cell above it. A tile of fluid can contain more than MaxMass water. const val FLUID_MIN_MASS = 0.0001f //Ignore cells that are almost dry + const val minFlow = 0.01f + const val maxSpeed = 1f // max units of water moved out of one block to another, per timestamp // END OF FLUID-RELATED STUFFS @@ -68,11 +69,160 @@ object WorldSimulator { * TODO multithread */ fun moveFluids(delta: Float) { - //////////////////// - // build fluidmap // - //////////////////// - purgeFluidMap() + makeFluidMapFromWorld() + + // before data: fluidMap/fluidTypeMap + // after data: fluidNewMap/fluidNewTypeMap + var flow = 0f + var remainingMass = 0f + + for (y in 1 until fluidMap.size - 1) { + for (x in 1 until fluidMap[0].size - 1) { + val worldX = x + updateXFrom + val worldY = y + updateYFrom + + // check solidity + if (isSolid(worldX, worldY)) continue + // check if the fluid is a same kind + //if (!isFlowable(type, worldX, worldY))) continue + + + // Custom push-only flow + flow = 0f + remainingMass = fluidMap[y][x] + if (remainingMass <= 0) continue + + // The block below this one + if (!isSolid(worldX, worldY + 1)) { // TODO use isFlowable + flow = getStableStateB(remainingMass + fluidMap[y + 1][x]) - fluidMap[y + 1][x] + if (flow > minFlow) { + flow *= 0.5f // leads to smoother flow + } + flow.coerceIn(0f, minOf(maxSpeed, remainingMass)) + + fluidNewMap[y][x] -= flow + fluidNewMap[y + 1][x] += flow + remainingMass -= flow + } + + if (remainingMass <= 0) continue + + // Left + if (!isSolid(worldX - 1, worldY)) { // TODO use isFlowable + // Equalise the amount fo water in this block and its neighbour + flow = (fluidMap[y][x] - fluidMap[y][x - 1]) / 4f + if (flow > minFlow) { + flow *= 0.5f + } + flow.coerceIn(0f, remainingMass) + + fluidNewMap[y][x] -= flow + fluidNewMap[y][x - 1] += flow + remainingMass -= flow + } + + if (remainingMass <= 0) continue + + // Right + if (!isSolid(worldX + 1, worldY)) { // TODO use isFlowable + // Equalise the amount fo water in this block and its neighbour + flow = (fluidMap[y][x] - fluidMap[y][x + 1]) / 4f + if (flow > minFlow) { + flow *= 0.5f + } + flow.coerceIn(0f, remainingMass) + + fluidNewMap[y][x] -= flow + fluidNewMap[y][x + 1] += flow + remainingMass -= flow + } + + if (remainingMass <= 0) continue + + // Up; only compressed water flows upwards + if (!isSolid(worldX, worldY - 1)) { // TODO use isFlowable + flow = remainingMass - getStableStateB(remainingMass + fluidMap[y - 1][x]) + if (flow > minFlow) { + flow *= 0.5f + } + flow.coerceIn(0f, minOf(maxSpeed, remainingMass)) + + fluidNewMap[y][x] -= flow + fluidNewMap[y - 1][x] += flow + remainingMass -= flow + } + + + } + } + + + fluidmapToWorld() + } + + fun isFlowable(type: FluidType, worldX: Int, worldY: Int): Boolean { + val targetFluid = world.getFluid(worldX, worldY) + + // true if target's type is the same as mine, or it's NULL (air) + return (targetFluid.type sameAs type || targetFluid.type sameAs Fluid.NULL) + } + + fun isSolid(worldX: Int, worldY: Int): Boolean { + val tile = world.getTileFromTerrain(worldX, worldY) + if (tile != Block.FLUID_MARKER) { + // check for block properties isSolid + return BlockCodex[tile].isSolid + } + else { + // check for fluid + + // no STATIC is implement yet, just return true + return true + } + } + + /* + Explanation of get_stable_state_b (well, kind-of) : + + if x <= 1, all water goes to the lower cell + * a = 0 + * b = 1 + + if x > 1 & x < 2*MaxMass + MaxCompress, the lower cell should have MaxMass + (upper_cell/MaxMass) * MaxCompress + b = MaxMass + (a/MaxMass)*MaxCompress + a = x - b + + -> + + b = MaxMass + ((x - b)/MaxMass)*MaxCompress -> + b = MaxMass + (x*MaxCompress - b*MaxCompress)/MaxMass + b*MaxMass = MaxMass^2 + (x*MaxCompress - b*MaxCompress) + b*(MaxMass + MaxCompress) = MaxMass*MaxMass + x*MaxCompress + + * b = (MaxMass*MaxMass + x*MaxCompress)/(MaxMass + MaxCompress) + * a = x - b; + + if x >= 2 * MaxMass + MaxCompress, the lower cell should have upper+MaxCompress + + b = a + MaxCompress + a = x - b + + -> + + b = x - b + MaxCompress -> + 2b = x + MaxCompress -> + + * b = (x + MaxCompress)/2 + * a = x - b + */ + private fun getStableStateB(totalMass: Float): Float { + if (totalMass <= 1) + return 1f + else if (totalMass < 2f * FLUID_MAX_MASS + FLUID_MAX_COMP) + return (FLUID_MAX_MASS * FLUID_MAX_MASS + totalMass * FLUID_MAX_COMP) / (FLUID_MAX_MASS + FLUID_MAX_COMP) + else + return (totalMass + FLUID_MAX_COMP) / 2f } /** @@ -109,11 +259,22 @@ object WorldSimulator { } - private fun purgeFluidMap() { - for (y in 1..DOUBLE_RADIUS) { - for (x in 1..DOUBLE_RADIUS) { - fluidMap[y - 1][x - 1] = 0f - fluidTypeMap[y - 1][x - 1] = Fluid.NULL + private fun makeFluidMapFromWorld() { + for (y in 0 until fluidMap.size) { + for (x in 0 until fluidMap[0].size) { + val fluidData = world.getFluid(x + updateXFrom, y + updateYFrom) + fluidMap[y][x] = fluidData.amount + fluidTypeMap[y][x] = fluidData.type + fluidNewMap[y][x] = fluidData.amount + fluidNewTypeMap[y][x] = fluidData.type + } + } + } + + private fun fluidmapToWorld() { + for (y in 0 until fluidMap.size) { + for (x in 0 until fluidMap[0].size) { + world.setFluid(x + updateXFrom, y + updateYFrom, fluidNewTypeMap[y][x], fluidNewMap[y][x]) } } } diff --git a/src/net/torvald/terrarum/serialise/ReadLayerDataLzma.kt b/src/net/torvald/terrarum/serialise/ReadLayerDataLzma.kt index d1cace49e..38a36fd16 100644 --- a/src/net/torvald/terrarum/serialise/ReadLayerDataLzma.kt +++ b/src/net/torvald/terrarum/serialise/ReadLayerDataLzma.kt @@ -3,6 +3,7 @@ package net.torvald.terrarum.serialise import com.badlogic.gdx.utils.compression.Lzma import net.torvald.terrarum.AppLoader.printdbg import net.torvald.terrarum.gameworld.BlockAddress +import net.torvald.terrarum.gameworld.FluidType import net.torvald.terrarum.gameworld.MapLayer import net.torvald.terrarum.gameworld.PairedMapLayer import net.torvald.terrarum.realestate.LandUtil @@ -156,7 +157,7 @@ internal object ReadLayerDataLzma { val terrainDamages = HashMap() val wallDamages = HashMap() - val fluidTypes = HashMap() + val fluidTypes = HashMap() val fluidFills = HashMap() // parse terrain damages @@ -215,7 +216,7 @@ internal object ReadLayerDataLzma { val spawnY: Int, val wallDamages: HashMap, val terrainDamages: HashMap, - val fluidTypes: HashMap, + val fluidTypes: HashMap, val fluidFills: HashMap )