added ticking logic to particles

This commit is contained in:
2024-11-16 15:07:04 +01:00
parent 15200edda9
commit 112ed55b2a
8 changed files with 151 additions and 59 deletions
@@ -11,17 +11,15 @@ import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents
import net.fabricmc.fabric.api.event.player.UseItemCallback import net.fabricmc.fabric.api.event.player.UseItemCallback
import net.minecraft.entity.player.PlayerEntity import net.minecraft.entity.player.PlayerEntity
import net.minecraft.item.ItemStack import net.minecraft.item.ItemStack
import net.minecraft.server.MinecraftServer
import net.minecraft.server.network.ServerPlayerEntity import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.server.world.ServerWorld import net.minecraft.server.world.ServerWorld
import net.minecraft.text.Text
import net.minecraft.util.ActionResult import net.minecraft.util.ActionResult
import net.minecraft.util.Hand import net.minecraft.util.Hand
import net.minecraft.util.TypedActionResult import net.minecraft.util.TypedActionResult
import net.minecraft.world.World import net.minecraft.world.World
// const val ALL_PARTICLE_EMITTERS: Array<> // ParticleEmitter var ALL_PARTICLE_EMITTERS: Array<ParticleEmitter> = emptyArray()
data class Config( data class Config(
val cooldown: Int = 1 val cooldown: Int = 1
@@ -56,12 +54,17 @@ class Bde_particles : ModInitializer {
fun init() { fun init() {
addToParticleRegister("DEBUG", ParticleEmitterParams()) addToParticleRegister("DEBUG", ParticleEmitterParams(loopDur = 20))
} }
fun tick() { fun tick() {
for (emitter in ALL_PARTICLE_EMITTERS) {
if (emitter.isDead) {
ALL_PARTICLE_EMITTERS = ALL_PARTICLE_EMITTERS.toMutableList().apply { remove(emitter) }.toTypedArray()
}
else { emitter.tick() }
}
} }
@@ -80,7 +83,7 @@ fun onRightClick(player: PlayerEntity, world: ServerWorld, hand: Hand) : TypedAc
} }
val emitter = ParticleEmitter(hitResult.pos, world, emitterParams) val emitter = ParticleEmitter(hitResult.pos, world, emitterParams)
emitter.tick() ALL_PARTICLE_EMITTERS += emitter
return TypedActionResult(ActionResult.PASS, handItem) return TypedActionResult(ActionResult.PASS, handItem)
} }
@@ -5,15 +5,17 @@ import net.minecraft.entity.decoration.DisplayEntity.BlockDisplayEntity
import net.minecraft.nbt.NbtCompound import net.minecraft.nbt.NbtCompound
import net.minecraft.nbt.NbtFloat import net.minecraft.nbt.NbtFloat
import net.minecraft.nbt.NbtList import net.minecraft.nbt.NbtList
import net.minecraft.nbt.NbtString
import net.minecraft.server.world.ServerWorld import net.minecraft.server.world.ServerWorld
class DisplayEntity(val properties: DisplayEntityProperties?) { class DisplayEntity(val properties: DisplayEntityProperties?) {
private var hasSpawned = false private var hasSpawned = false
private var entity: BlockDisplayEntity? = null
fun spawn(world: ServerWorld) : BlockDisplayEntity? { fun spawn(world: ServerWorld) : BlockDisplayEntity? {
if (this.hasSpawned || properties == null) { return null } if (hasSpawned || properties == null) { return null }
val e = updateProperties(world, this.properties) val e = updateProperties(world, properties)
world.spawnEntity(e) world.spawnEntity(e)
this.hasSpawned = true this.hasSpawned = true
@@ -23,8 +25,18 @@ class DisplayEntity(val properties: DisplayEntityProperties?) {
fun updateProperties(world: ServerWorld, properties: DisplayEntityProperties) : BlockDisplayEntity { fun updateProperties(world: ServerWorld, properties: DisplayEntityProperties) : BlockDisplayEntity {
val e = BlockDisplayEntity(EntityType.BLOCK_DISPLAY, world) val e = BlockDisplayEntity(EntityType.BLOCK_DISPLAY, world)
// TODO: add tag nbt and kill all particles when server starts
val nbt = NbtCompound().apply { val nbt = NbtCompound().apply {
val tagList = NbtList()
properties.tags.forEach { tag ->
tagList.add(NbtString.of(tag))
}
put("Tags", tagList)
put("block_state", NbtCompound().apply { putString("Name", properties.blockType) }) put("block_state", NbtCompound().apply { putString("Name", properties.blockType) })
put("transformation", NbtCompound().apply { put("transformation", NbtCompound().apply {
val offsetList = NbtList().apply { val offsetList = NbtList().apply {
add(NbtFloat.of(properties.translation.x)) add(NbtFloat.of(properties.translation.x))
@@ -60,6 +72,11 @@ class DisplayEntity(val properties: DisplayEntityProperties?) {
e.refreshPositionAndAngles(properties.pos.x, properties.pos.y, properties.pos.z, properties.rot.x, properties.rot.y) e.refreshPositionAndAngles(properties.pos.x, properties.pos.y, properties.pos.z, properties.rot.x, properties.rot.y)
entity = e
return e return e
} }
fun kill() {
entity?.kill()
}
} }
@@ -9,7 +9,8 @@ data class DisplayEntityProperties (
val pos: Vec3d = Vec3d(0.0, 0.0, 0.0), val pos: Vec3d = Vec3d(0.0, 0.0, 0.0),
val rot: Vector2f = Vector2f(0.0f, 0.0f), val rot: Vector2f = Vector2f(0.0f, 0.0f),
val blockType: String = "minecraft:purple_concrete", val blockType: String = "minecraft:purple_concrete",
val tags: Array<String> = emptyArray(),
val translation: Vector3f = Vector3f(-0.5f, -0.5f, -0.5f), val translation: Vector3f = Vector3f(-0.5f, -0.5f, -0.5f),
val leftRotation: Vector4f = Vector4f(0.0f, 0.0f, 0.0f, 1.0f), val leftRotation: Vector4f = Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
@@ -67,24 +67,20 @@ fun quatToEuler(quat: Vector4f): Vector3f {
val z = quat.z val z = quat.z
val w = quat.w val w = quat.w
// Roll (X-axis rotation) val sinRCosP = 2 * (w * x + y * z)
val sinr_cosp = 2 * (w * x + y * z) val cosRCosP = 1 - 2 * (x * x + y * y)
val cosr_cosp = 1 - 2 * (x * x + y * y) val roll = atan2(sinRCosP, cosRCosP)
val roll = atan2(sinr_cosp, cosr_cosp)
// Pitch (Y-axis rotation) val sinP = 2 * (w * y - z * x)
val sinp = 2 * (w * y - z * x) val pitch = if (abs(sinP) >= 1) {
val pitch = if (kotlin.math.abs(sinp) >= 1) { sign(sinP) * (Math.PI.toFloat() / 2)
// Use 90 degrees if out of range
kotlin.math.sign(sinp) * (Math.PI.toFloat() / 2)
} else { } else {
asin(sinp) asin(sinP)
} }
// Yaw (Z-axis rotation) val sinYCosP = 2 * (w * z + x * y)
val siny_cosp = 2 * (w * z + x * y) val cosYCosP = 1 - 2 * (y * y + z * z)
val cosy_cosp = 1 - 2 * (y * y + z * z) val yaw = atan2(sinYCosP, cosYCosP)
val yaw = atan2(siny_cosp, cosy_cosp)
return Vector3f(roll, pitch, yaw) return Vector3f(roll, pitch, yaw)
} }
@@ -145,4 +141,9 @@ fun transformOffsetByQuat(offset: Vector3f, rotation: Vector4f): Vector3f {
fun transformOffsetByScale(offset: Vector3f, scale: Vector3f): Vector3f { fun transformOffsetByScale(offset: Vector3f, scale: Vector3f): Vector3f {
return offset.mul(scale) return offset.mul(scale)
}
fun lerp(x: Float, y: Float, t: Float) : Float {
return x + (y - x) * t
} }
@@ -0,0 +1,10 @@
package com.bytedice.bde_particles.particle
data class ForceField (
val x: Float = 0.0f,
val y: Float = 0.0f,
val z: Float = 0.0f,
val minForce: Float = 0.0f,
val maxForce: Float = 0.0f,
val radius: Float = -1.0f
)
@@ -6,57 +6,89 @@ import com.bytedice.bde_particles.math.*
import net.minecraft.server.world.ServerWorld import net.minecraft.server.world.ServerWorld
import net.minecraft.util.math.Vec3d import net.minecraft.util.math.Vec3d
import org.joml.Vector3f import org.joml.Vector3f
import kotlin.math.round
class Particle(particleParams: ParticleParams) { class Particle(private val particleParams: ParticleParams) {
val rot = Vector3f( private val rot = Vector3f(
randomFloatBetween(particleParams.rotRandom.first.x, particleParams.rotRandom.second.x), randomFloatBetween(particleParams.rotRandom.first.x, particleParams.rotRandom.second.x),
randomFloatBetween(particleParams.rotRandom.first.y, particleParams.rotRandom.second.x), randomFloatBetween(particleParams.rotRandom.first.y, particleParams.rotRandom.second.x),
randomFloatBetween(particleParams.rotRandom.first.z, particleParams.rotRandom.second.z) randomFloatBetween(particleParams.rotRandom.first.z, particleParams.rotRandom.second.z)
) )
val rotVel = Vector3f( private val rotVel = Vector3f(
randomFloatBetween(particleParams.rotVelRandom.first.x, particleParams.rotVelRandom.second.x), randomFloatBetween(particleParams.rotVelRandom.first.x, particleParams.rotVelRandom.second.x),
randomFloatBetween(particleParams.rotVelRandom.first.y, particleParams.rotVelRandom.second.x), randomFloatBetween(particleParams.rotVelRandom.first.y, particleParams.rotVelRandom.second.x),
randomFloatBetween(particleParams.rotVelRandom.first.z, particleParams.rotVelRandom.second.z) randomFloatBetween(particleParams.rotVelRandom.first.z, particleParams.rotVelRandom.second.z)
) )
val scale = Vector3f( private val scale = Vector3f(
randomFloatBetween(particleParams.sizeRandom.first.x, particleParams.sizeRandom.second.x), randomFloatBetween(particleParams.sizeRandom.first.x, particleParams.sizeRandom.second.x),
randomFloatBetween(particleParams.sizeRandom.first.y, particleParams.sizeRandom.second.x), randomFloatBetween(particleParams.sizeRandom.first.y, particleParams.sizeRandom.second.x),
randomFloatBetween(particleParams.sizeRandom.first.z, particleParams.sizeRandom.second.z) randomFloatBetween(particleParams.sizeRandom.first.z, particleParams.sizeRandom.second.z)
) )
var blockDisplay: DisplayEntity? = null private var blockDisplay: DisplayEntity? = null
val params = particleParams
var isInit = false var isDead = false
private var timeAlive = 0
private var isInit = false
fun init(pos: Vec3d) { fun init(pos: Vec3d) {
val offset = Vector3f(-0.5f, -0.5f, -0.5f) val offset = Vector3f(-0.5f, -0.5f, -0.5f)
val quatRot = eulerToQuat(this.rot) val quatRot = eulerToQuat(rot)
val scaleOffset = transformOffsetByScale(offset, this.scale) val scaleOffset = transformOffsetByScale(offset, scale)
val rotOffset = transformOffsetByQuat(scaleOffset, quatRot) val rotOffset = transformOffsetByQuat(scaleOffset, quatRot)
val properties = DisplayEntityProperties( val properties = DisplayEntityProperties(
pos = pos, pos = pos,
blockType = this.params.blockCurve[0], blockType = particleParams.blockCurve[0],
translation = rotOffset, translation = rotOffset,
leftRotation = quatRot, leftRotation = quatRot,
scale = this.scale scale = scale,
tags = arrayOf("BPS_Particle")
) )
this.blockDisplay = DisplayEntity(properties) blockDisplay = DisplayEntity(properties)
this.isInit = true isInit = true
} }
fun spawn(world: ServerWorld) { fun spawn(world: ServerWorld) {
if (isInit) { blockDisplay?.spawn(world) } if (isInit) { blockDisplay?.spawn(world) }
else { error("Particle is not initialized before being spawned. Please use Particle.init() to initialize it") } else { error("Particle is not initialized before being spawned. Please use Particle.init() to initialize it.") }
} }
fun tick() { fun tick(world: ServerWorld) {
if (!isInit) { error("Particle is not initialized before being ticked. Please use Particle.init() to initialize it.") }
val properties = blockDisplay?.properties ?: return
val newProperties = calculateNewProperties(properties, particleParams)
blockDisplay?.updateProperties(world, newProperties)
if (timeAlive > particleParams.lifeTime) { isDead = true; blockDisplay?.kill() }
timeAlive += 1
}
private fun calculateNewProperties(propetries: DisplayEntityProperties, params: ParticleParams) : DisplayEntityProperties {
val newBlockIdx = round(lerp(
0.0f,
params.blockCurve.lastIndex.toFloat(),
(timeAlive.toFloat() / params.lifeTime.toFloat()).coerceIn(0.0f, 1.0f)
)).toInt()
val newBlock = params.blockCurve[newBlockIdx]
val newProperties = DisplayEntityProperties(
blockType = newBlock
)
return newProperties
} }
} }
@@ -4,18 +4,54 @@ import net.minecraft.server.world.ServerWorld
import net.minecraft.util.math.Vec3d import net.minecraft.util.math.Vec3d
class ParticleEmitter(emitterPos: Vec3d, emitterWorld: ServerWorld, emitterParams: ParticleEmitterParams) { class ParticleEmitter(emitterPos: Vec3d, emitterWorld: ServerWorld, emitterParams: ParticleEmitterParams) {
val world = emitterWorld private val world = emitterWorld
val pos = emitterPos private val pos = emitterPos
val params = emitterParams private val params = emitterParams
val allParticles: Array<Particle> = emptyArray()
val timeAlive = 0 private var allParticles: Array<Particle> = emptyArray()
private var timeAlive = 0
private var loopCount = 0
private var loopDelay = 0
private var loopDur = 0
private var loopDelayActive = false
var isDead = false
fun tick() { fun tick() {
repeat(this.params.spawnsPerTick) { count()
val particle = Particle(ParticleParams())
particle.init(this.pos) if (!loopDelayActive) {
particle.spawn(world) repeat(this.params.spawnsPerTick) {
val particle = Particle(ParticleParams())
particle.init(this.pos)
particle.spawn(world)
allParticles += particle
}
}
for (particle in allParticles) {
if (particle.isDead) { allParticles = allParticles.toMutableList().apply { remove(particle) }.toTypedArray() }
else { particle.tick(world) }
} }
} }
private fun count() {
if (loopDur >= params.loopDur) {
loopCount += 1
loopDur = 0
loopDelayActive = true
if (!params.loop) { isDead = true }
}
else if (loopDelay >= params.loopDelay) {
loopDelay = 0
loopDelayActive = false
}
if (!loopDelayActive) { loopDur += 1 }
else { loopDelay += 1 }
timeAlive += 1
}
} }
@@ -5,24 +5,16 @@ import org.joml.Vector3f
data class ParticleParams ( data class ParticleParams (
//val shape: Shape, // add later, start with single point //val shape: Shape, // add later, start with single point
val blockCurve: Array<String> = arrayOf("minecraft:purple_concrete"), val blockCurve: Array<String> = arrayOf("minecraft:purple_concrete", "minecraft:purple_stained_glass"),
val rotRandom: Pair<Vector3f, Vector3f> = Pair(Vector3f(0.0f, 0.0f, 0.0f), Vector3f(360.0f, 360.0f, 360.0f)), val rotRandom: Pair<Vector3f, Vector3f> = Pair(Vector3f(0.0f, 0.0f, 0.0f), Vector3f(360.0f, 360.0f, 360.0f)),
val rotVelRandom: Pair<Vector3f, Vector3f> = Pair(Vector3f(1.0f, 1.0f, 1.0f), Vector3f(5.0f, 5.0f, 5.0f)), val rotVelRandom: Pair<Vector3f, Vector3f> = Pair(Vector3f(1.0f, 1.0f, 1.0f), Vector3f(5.0f, 5.0f, 5.0f)),
val rotVelCurve: Array<Vector3f> = arrayOf(Vector3f(1.0f, 1.0f, 1.0f), Vector3f(0.0f, 0.0f, 0.0f)), val rotVelCurve: Array<Vector3f> = arrayOf(Vector3f(1.0f, 1.0f, 1.0f), Vector3f(0.0f, 0.0f, 0.0f)),
val sizeRandom: Pair<Vector3f, Vector3f> = Pair(Vector3f(0.5f, 0.5f, 0.5f), Vector3f(1.0f, 1.0f, 1.0f)), val sizeRandom: Pair<Vector3f, Vector3f> = Pair(Vector3f(0.5f, 0.5f, 0.5f), Vector3f(1.0f, 1.0f, 1.0f)),
val sizeCurve: Array<Vector3f> = arrayOf(Vector3f(1.0f, 1.0f, 1.0f), Vector3f(0.0f, 0.0f, 0.0f)), val sizeCurve: Array<Vector3f> = arrayOf(Vector3f(1.0f, 1.0f, 1.0f), Vector3f(0.0f, 0.0f, 0.0f)),
val velRandom: Pair<Vector3f, Vector3f> = Pair(Vector3f(1.0f, 1.0f, 1.0f), Vector3f(5.0f, 5.0f, 5.0f)),
val forceFields: ForceField = ForceField(), val forceFields: ForceField = ForceField(),
val gravity: Vec3d = Vec3d(0.0, -9.0, 0.0), val gravity: Vec3d = Vec3d(0.0, -9.0, 0.0),
val drag: Float = 0.2f, val drag: Float = 0.2f,
val minVel: Float = 0.003f val minVel: Float = 0.003f,
) val lifeTime: Int = 20
data class ForceField (
val x: Float = 0.0f,
val y: Float = 0.0f,
val z: Float = 0.0f,
val minForce: Float = 0.0f,
val maxForce: Float = 0.0f,
val radius: Float = -1.0f,
) )