implementing water sim but not actually working

This commit is contained in:
Minjae Song
2018-12-13 04:45:09 +09:00
parent 1f1d6f1eda
commit 05a8f47006
6 changed files with 230 additions and 51 deletions

View File

@@ -33,6 +33,7 @@ import java.io.File
import java.io.IOException import java.io.IOException
import net.torvald.getcpuname.GetCpuName import net.torvald.getcpuname.GetCpuName
import net.torvald.terrarum.modulebasegame.Ingame 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.sqr() = this * this
inline fun Double.sqrt() = Math.sqrt(this) inline fun Double.sqrt() = Math.sqrt(this)
inline fun Float.sqrt() = FastMath.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) = fun Double.bipolarClamp(limit: Double) =
this.coerceIn(-limit, limit) this.coerceIn(-limit, limit)

View File

@@ -1,13 +1,15 @@
package net.torvald.terrarum.blockproperties package net.torvald.terrarum.blockproperties
import net.torvald.terrarum.gameworld.FluidType
/** /**
* Created by minjaesong on 2016-08-06. * Created by minjaesong on 2016-08-06.
*/ */
object Fluid { object Fluid {
val NULL = 0 val NULL = FluidType(0)
val WATER = 1 val WATER = FluidType(1)
val STATIC_WATER = -1 val STATIC_WATER = FluidType(-1)
} }

View File

@@ -5,9 +5,11 @@ import com.badlogic.gdx.graphics.Color
import net.torvald.terrarum.blockproperties.Block import net.torvald.terrarum.blockproperties.Block
import net.torvald.terrarum.realestate.LandUtil import net.torvald.terrarum.realestate.LandUtil
import net.torvald.terrarum.blockproperties.BlockCodex import net.torvald.terrarum.blockproperties.BlockCodex
import net.torvald.terrarum.blockproperties.Fluid
import net.torvald.terrarum.modulebasegame.gameworld.WorldSimulator import net.torvald.terrarum.modulebasegame.gameworld.WorldSimulator
import net.torvald.terrarum.serialise.ReadLayerDataLzma import net.torvald.terrarum.serialise.ReadLayerDataLzma
import org.dyn4j.geometry.Vector2 import org.dyn4j.geometry.Vector2
import kotlin.math.absoluteValue
typealias BlockAddress = Long typealias BlockAddress = Long
@@ -45,7 +47,7 @@ open class GameWorld {
val wallDamages: HashMap<BlockAddress, Float> val wallDamages: HashMap<BlockAddress, Float>
val terrainDamages: HashMap<BlockAddress, Float> val terrainDamages: HashMap<BlockAddress, Float>
val fluidTypes: HashMap<BlockAddress, Int> val fluidTypes: HashMap<BlockAddress, FluidType>
val fluidFills: HashMap<BlockAddress, Float> val fluidFills: HashMap<BlockAddress, Float>
//public World physWorld = new World( new Vec2(0, -Terrarum.game.gravitationalAccel) ); //public World physWorld = new World( new Vec2(0, -Terrarum.game.gravitationalAccel) );
@@ -79,7 +81,7 @@ open class GameWorld {
wallDamages = HashMap<BlockAddress, Float>() wallDamages = HashMap<BlockAddress, Float>()
terrainDamages = HashMap<BlockAddress, Float>() terrainDamages = HashMap<BlockAddress, Float>()
fluidTypes = HashMap<BlockAddress, Int>() fluidTypes = HashMap<BlockAddress, FluidType>()
fluidFills = HashMap<BlockAddress, Float>() fluidFills = HashMap<BlockAddress, Float>()
// temperature layer: 2x2 is one cell // temperature layer: 2x2 is one cell
@@ -154,8 +156,8 @@ open class GameWorld {
get() = layerTerrainLowBits.data get() = layerTerrainLowBits.data
fun getTileFromWall(x: Int, y: Int): Int? { fun getTileFromWall(x: Int, y: Int): Int? {
val wall: Int? = layerWall.getTile(x fmod width, y) val wall: Int? = layerWall.getTile(x fmod width, y.coerceWorld().coerceWorld())
val wallDamage: Int? = getWallLowBits(x fmod width, y) val wallDamage: Int? = getWallLowBits(x fmod width, y.coerceWorld())
return if (wall == null || wallDamage == null) return if (wall == null || wallDamage == null)
null null
else else
@@ -163,8 +165,8 @@ open class GameWorld {
} }
fun getTileFromTerrain(x: Int, y: Int): Int? { fun getTileFromTerrain(x: Int, y: Int): Int? {
val terrain: Int? = layerTerrain.getTile(x fmod width, y) val terrain: Int? = layerTerrain.getTile(x fmod width, y.coerceWorld())
val terrainDamage: Int? = getTerrainLowBits(x fmod width, y) val terrainDamage: Int? = getTerrainLowBits(x fmod width, y.coerceWorld())
return if (terrain == null || terrainDamage == null) return if (terrain == null || terrainDamage == null)
null null
else else
@@ -172,15 +174,15 @@ open class GameWorld {
} }
fun getTileFromWire(x: Int, y: Int): Int? { 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? { 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? { 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 * @param combinedTilenum (tilenum * 16) + damage
*/ */
fun setTileWall(x: Int, y: Int, combinedTilenum: Int) { 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 * @param combinedTilenum (tilenum * 16) + damage
*/ */
fun setTileTerrain(x: Int, y: Int, combinedTilenum: Int) { 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) { fun setTileWall(x: Int, y: Int, tile: Byte, damage: Int) {
layerWall.setTile(x fmod width, y, tile) layerWall.setTile(x fmod width, y.coerceWorld(), tile)
layerWallLowBits.setData(x fmod width, y, damage) layerWallLowBits.setData(x fmod width, y.coerceWorld(), damage)
wallDamages.remove(LandUtil.getBlockAddr(this, x, y)) wallDamages.remove(LandUtil.getBlockAddr(this, x, y))
} }
fun setTileTerrain(x: Int, y: Int, tile: Byte, damage: Int) { fun setTileTerrain(x: Int, y: Int, tile: Byte, damage: Int) {
layerTerrain.setTile(x fmod width, y, tile) layerTerrain.setTile(x fmod width, y.coerceWorld(), tile)
layerTerrainLowBits.setData(x fmod width, y, damage) layerTerrainLowBits.setData(x fmod width, y.coerceWorld(), damage)
terrainDamages.remove(LandUtil.getBlockAddr(this, x, y)) terrainDamages.remove(LandUtil.getBlockAddr(this, x, y))
} }
fun setTileWire(x: Int, y: Int, tile: Byte) { 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? { fun getTileFrom(mode: Int, x: Int, y: Int): Int? {
@@ -340,13 +342,20 @@ open class GameWorld {
fun getWallDamage(x: Int, y: Int): Float = fun getWallDamage(x: Int, y: Int): Float =
wallDamages[LandUtil.getBlockAddr(this, x, y)] ?: 0f 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) val addr = LandUtil.getBlockAddr(this, x, y)
// fluid completely drained
if (fill <= WorldSimulator.FLUID_MIN_MASS) { if (fill <= WorldSimulator.FLUID_MIN_MASS) {
fluidTypes.remove(addr) /**********/ fluidTypes.remove(addr)
fluidFills.remove(addr) val oldMap = fluidFills.remove(addr)
setTileTerrain(x, y, 0)
// 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 { else {
fluidTypes[addr] = fluidType fluidTypes[addr] = fluidType
fluidFills[addr] = fill fluidFills[addr] = fill
@@ -354,27 +363,28 @@ open class GameWorld {
} }
} }
fun getFluid(x: Int, y: Int): Pair<Int, Float>? { fun getFluid(x: Int, y: Int): FluidInfo {
val addr = LandUtil.getBlockAddr(this, x, y) val addr = LandUtil.getBlockAddr(this, x, y)
val fill = fluidFills[addr] val fill = fluidFills[addr]
val type = fluidTypes[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? { fun getTemperature(worldTileX: Int, worldTileY: Int): Float? {
return null return null
//return layerThermal.getValue((worldTileX fmod width) / 2, worldTileY / 2)
} }
fun getAirPressure(worldTileX: Int, worldTileY: Int): Float? { fun getAirPressure(worldTileX: Int, worldTileY: Int): Float? {
return null return null
//return layerFluidPressure.getValue((worldTileX fmod width) / 4, worldTileY / 8)
} }
private fun Int.coerceWorld() = this.coerceIn(0, height - 1)
companion object { companion object {
@Transient val WALL = 0 @Transient val WALL = 0
@Transient val TERRAIN = 1 @Transient val TERRAIN = 1
@@ -390,3 +400,7 @@ open class GameWorld {
infix fun Int.fmod(other: Int) = Math.floorMod(this, other) 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 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
}

View File

@@ -598,7 +598,7 @@ open class Ingame(batch: SpriteBatch) : IngameInstance(batch) {
} }
actorNowPlaying = newActor actorNowPlaying = newActor
WorldSimulator(actorNowPlaying, Terrarum.deltaTime) //WorldSimulator(actorNowPlaying, Terrarum.deltaTime)
} }
private fun changePossession(refid: Int) { private fun changePossession(refid: Int) {

View File

@@ -1,17 +1,13 @@
package net.torvald.terrarum.modulebasegame.gameworld package net.torvald.terrarum.modulebasegame.gameworld
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import net.torvald.terrarum.Terrarum 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.blockproperties.Block
import net.torvald.terrarum.roundInt
import net.torvald.terrarum.worlddrawer.FeaturesDrawer
import net.torvald.terrarum.blockproperties.BlockCodex import net.torvald.terrarum.blockproperties.BlockCodex
import net.torvald.terrarum.blockproperties.Fluid import net.torvald.terrarum.blockproperties.Fluid
import net.torvald.terrarum.gameworld.FluidCodex import net.torvald.terrarum.gameworld.FluidType
import net.torvald.terrarum.gameworld.GameWorld
import net.torvald.terrarum.modulebasegame.Ingame
import net.torvald.terrarum.modulebasegame.gameactors.ActorHumanoid import net.torvald.terrarum.modulebasegame.gameactors.ActorHumanoid
/** /**
@@ -25,15 +21,20 @@ object WorldSimulator {
* In tiles; * In tiles;
* square width/height = field * 2 * 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 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 fluidMap = Array(DOUBLE_RADIUS, { FloatArray(DOUBLE_RADIUS) })
private val fluidTypeMap = Array(DOUBLE_RADIUS, { IntArray(DOUBLE_RADIUS) }) private val fluidTypeMap = Array(DOUBLE_RADIUS, { Array<FluidType>(DOUBLE_RADIUS) { Fluid.NULL } })
private val fluidNewMap = Array(DOUBLE_RADIUS, { FloatArray(DOUBLE_RADIUS) })
private val fluidNewTypeMap = Array(DOUBLE_RADIUS, { Array<FluidType>(DOUBLE_RADIUS) { Fluid.NULL } })
const val FLUID_MAX_MASS = 1f // The normal, un-pressurized mass of a full water cell 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 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 // END OF FLUID-RELATED STUFFS
@@ -68,11 +69,160 @@ object WorldSimulator {
* TODO multithread * TODO multithread
*/ */
fun moveFluids(delta: Float) { fun moveFluids(delta: Float) {
//////////////////// makeFluidMapFromWorld()
// build fluidmap //
////////////////////
purgeFluidMap()
// 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() { private fun makeFluidMapFromWorld() {
for (y in 1..DOUBLE_RADIUS) { for (y in 0 until fluidMap.size) {
for (x in 1..DOUBLE_RADIUS) { for (x in 0 until fluidMap[0].size) {
fluidMap[y - 1][x - 1] = 0f val fluidData = world.getFluid(x + updateXFrom, y + updateYFrom)
fluidTypeMap[y - 1][x - 1] = Fluid.NULL 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])
} }
} }
} }

View File

@@ -3,6 +3,7 @@ package net.torvald.terrarum.serialise
import com.badlogic.gdx.utils.compression.Lzma import com.badlogic.gdx.utils.compression.Lzma
import net.torvald.terrarum.AppLoader.printdbg import net.torvald.terrarum.AppLoader.printdbg
import net.torvald.terrarum.gameworld.BlockAddress import net.torvald.terrarum.gameworld.BlockAddress
import net.torvald.terrarum.gameworld.FluidType
import net.torvald.terrarum.gameworld.MapLayer import net.torvald.terrarum.gameworld.MapLayer
import net.torvald.terrarum.gameworld.PairedMapLayer import net.torvald.terrarum.gameworld.PairedMapLayer
import net.torvald.terrarum.realestate.LandUtil import net.torvald.terrarum.realestate.LandUtil
@@ -156,7 +157,7 @@ internal object ReadLayerDataLzma {
val terrainDamages = HashMap<BlockAddress, Float>() val terrainDamages = HashMap<BlockAddress, Float>()
val wallDamages = HashMap<BlockAddress, Float>() val wallDamages = HashMap<BlockAddress, Float>()
val fluidTypes = HashMap<BlockAddress, Int>() val fluidTypes = HashMap<BlockAddress, FluidType>()
val fluidFills = HashMap<BlockAddress, Float>() val fluidFills = HashMap<BlockAddress, Float>()
// parse terrain damages // parse terrain damages
@@ -215,7 +216,7 @@ internal object ReadLayerDataLzma {
val spawnY: Int, val spawnY: Int,
val wallDamages: HashMap<BlockAddress, Float>, val wallDamages: HashMap<BlockAddress, Float>,
val terrainDamages: HashMap<BlockAddress, Float>, val terrainDamages: HashMap<BlockAddress, Float>,
val fluidTypes: HashMap<BlockAddress, Int>, val fluidTypes: HashMap<BlockAddress, FluidType>,
val fluidFills: HashMap<BlockAddress, Float> val fluidFills: HashMap<BlockAddress, Float>
) )