CooParticles API Mod (1.21.1) – An Library to Spawn Particles
CooParticles API Mod (1.21.1) is a library that provides refactored data packages to allow clients to accept more Minecraft particles, enabling different players to synchronize their rendering and exceed the particle limit of 16384.
How to use:
Create a particle class that inherits from ControlableParticle
.
Example Particle
class TestEndRodParticle(
// Parameters required for Particle
world: ClientWorld,
pos: Vec3d,
velocity: Vec3d,
// Unique identifier to obtain ParticleControler
controlUUID: UUID,
val provider: SpriteProvider
) :
// Must inherit from ControlableParticle
ControlableParticle(world, pos, velocity, controlUUID) {
override fun getType(): ParticleTextureSheet {
return ParticleTextureSheet.PARTICLE_SHEET_OPAQUE
}
init {
setSprite(provider.getSprite(0, 120))
// Since ControlableParticle prohibits overriding the tick method
// Use this method instead
controler.addPreTickAction {
setSpriteForAge(provider)
}
}
// Basic particle registration
class Factory(val provider: SpriteProvider) : ParticleFactory<TestEndRodEffect> {
override fun createParticle(
parameters: TestEndRodEffect,
world: ClientWorld,
x: Double,
y: Double,
z: Double,
velocityX: Double,
velocityY: Double,
velocityZ: Double
): Particle {
return TestEndRodParticle(
world,
Vec3d(x, y, z),
Vec3d(velocityX, velocityY, velocityZ),
parameters.controlUUID,
provider
)
}
}
}
To obtain the corresponding UUID, your ParticleEffect
must also have a UUID.
// As a constructor parameter
class TestEndRodEffect(controlUUID: UUID) : ControlableParticleEffect(controlUUID) {
companion object {
@JvmStatic
val codec: MapCodec<TestEndRodEffect> = RecordCodecBuilder.mapCodec {
return@mapCodec it.group(
Codec.BYTE_BUFFER.fieldOf("uuid").forGetter { effect ->
val toString = effect.controlUUID.toString()
val buffer = Unpooled.buffer()
buffer.writeBytes(toString.toByteArray())
buffer.nioBuffer()
}
).apply(it) { buf ->
TestEndRodEffect(
UUID.fromString(
String(buf.array())
)
)
}
}
@JvmStatic
val packetCode: PacketCodec<RegistryByteBuf, TestEndRodEffect> = PacketCodec.of(
{ effect, buf ->
buf.writeUuid(effect.controlUUID)
}, {
TestEndRodEffect(it.readUuid())
}
)
}
override fun getType(): ParticleType<*> {
return ModParticles.testEndRod
}
}
After registering the particle using the Fabric API on the client side, proceed to build a ControlableParticleGroup
.
Building a ControlableParticleGroup
The role of ControlableParticleGroup
is to render particle combinations on the player’s client.
Example code for building a basic ControlableParticleGroup: A magic circle at the center of the player’s field of view, rotating 10 degrees per tick.
class TestGroupClient(uuid: UUID, val bindPlayer: UUID) : ControlableParticleGroup(uuid) {
// To allow the server to properly forward ParticleGroup data to each player
// The server sends a PacketParticleGroupS2C packet
// This is for decoding
class Provider : ControlableParticleGroupProvider {
override fun createGroup(
uuid: UUID,
// Args are parameters synchronized from the server to the client
// Refer to the cn.coostack.network.packet.PacketParticleGroupS2C class for fields that are not recommended to override or need handling (already handled)
args: Map<String, ParticleControlerDataBuffer<*>>
): ControlableParticleGroup {
// Bound player
val bindUUID = args["bindUUID"]!!.loadedValue as UUID
return TestGroupClient(uuid, bindUUID)
}
}
// Magic circle particle combination
override fun loadParticleLocations(): Map<ParticleRelativeData, RelativeLocation> {
// Magic circle on the XZ plane
val list = Math3DUtil.getCycloidGraphic(3.0, 5.0, 2, -3, 360, 0.2).onEach { it.y += 6 }
return list.associateBy {
withEffect({
// Provide ParticleEffect (used in world.addParticle in the display method)
// 'it' is of type UUID
// If you need to set a ParticleGroup at this position, use
// ParticleDisplayer.withGroup(yourParticleGroup)
ParticleDisplayer.withSingle(TestEndRodEffect(it))
}) {
// kt: this is ControlableParticle
// java: this instanceof ControlableParticle
// Used to initialize particle information
// If the parameter is withGroup, this method does not need to be implemented
color = Vector3f(230 / 255f, 130 / 255f, 60 / 255f)
this.maxAliveTick = this.maxAliveTick
}
}
}
/**
* When the particle is first rendered in the player’s view
* This will also be called when the player moves out of render range and returns
* Can be understood as particle group initialization
*/
override fun onGroupDisplay() {
MinecraftClient.getInstance().player?.sendMessage(Text.of("Sending particle: ${this::class.java.name} success"))
addPreTickAction {
// When the player can see the particle (this class will be instantiated)
val bindPlayerEntity = world!!.getPlayerByUuid(bindPlayer) ?: let {
return@addPreTickAction
}
teleportGroupTo(bindPlayerEntity.eyePos)
rotateToWithAngle(
RelativeLocation.of(bindPlayerEntity.rotationVector),
Math.toRadians(10.0)
)
}
}
}
After creating the ControlableParticleGroup
, register it on the client side.
ClientParticleGroupManager.register(
// If this particleGroup’s loadParticleLocations method includes a sub-ParticleGroup, the sub-Group does not need to be registered here
// Unless you need to use ClientParticleGroupManager.addVisibleGroup(subGroup)
TestGroupClient::class.java, TestGroupClient.Provider()
)
To allow other players to synchronize operations, set up a server-side ControlableParticleGroup
example.
/**
* No specific requirements for constructor parameters
*/
class TestParticleGroup(private val bindPlayer: ServerPlayerEntity) :
// First parameter is the unique identifier for the ParticleGroup
// This content will be synchronized to the client
// Second parameter is the particle’s visible range
// When the player exceeds this range, a packet to delete the particle group will be sent (invisible to that player)
ServerParticleGroup(UUID.randomUUID(), 16.0) {
override fun tick() {
withPlayerStats(bindPlayer)
setPosOnServer(bindPlayer.eyePos)
}
/**
* These are the parameters you want to send to the client to build the ControlableParticleGroup
* They will ultimately be passed to ControlableParticleGroupProvider.createGroup()
*/
override fun otherPacketArgs(): Map<String, ParticleControlerDataBuffer<out Any>> {
return mapOf(
"bindUUID" to ParticleControlerDataBuffers.uuid(bindPlayer.uuid)
)
}
override fun getClientType(): Class<out ControlableParticleGroup> {
return TestGroupClient::class.java
}
}
After completing the above setup, add the particle on the server.
val serverGroup = TestParticleGroup(user as ServerPlayerEntity)
ServerParticleGroupManager.addParticleGroup(
// world must be ServerWorld
serverGroup, user.pos, world as ServerWorld
)
For other advanced usage, refer to cn.coostack.particles.control.group.ControlableParticleGroup
and cn.coostack.network.particle.ServerParticleGroup
.
ParticleGroup Nesting Example
Main ParticleGroup:
class TestGroupClient(uuid: UUID, val bindPlayer: UUID) : ControlableParticleGroup(uuid) {
class Provider : ControlableParticleGroupProvider {
override fun createGroup(
uuid: UUID,
args: Map<String, ParticleControlerDataBuffer<*>>
): ControlableParticleGroup {
val bindUUID = args["bindUUID"]!!.loadedValue as UUID
return TestGroupClient(uuid, bindUUID)
}
/**
* When ServerParticleGroup’s change method is called, apply changes to the group here
* Parameters in PacketParticleGroupS2C.PacketArgsType as keys do not need to be handled here
* But they will also be passed as args
*/
override fun changeGroup(group: ControlableParticleGroup, args: Map<String, ParticleControlerDataBuffer<*>>) {
}
}
override fun loadParticleLocations(): Map<ParticleRelativeData, RelativeLocation> {
val r1 = 3.0
val r2 = 5.0
val w1 = -2
val w2 = 3
val scale = 1.0
val count = 360
val list = Math3DUtil.getCycloidGraphic(r1, r2, w1, w2, count, scale).onEach { it.y += 6 }
val map = list.associateBy {
withEffect({ ParticleDisplayer.withSingle(TestEndRodEffect(it)) }) {
color = Vector3f(230 / 255f, 130 / 255f, 60 / 255f)
this.maxAliveTick = this.maxAliveTick
}
}
val mutable = map.toMutableMap()
// Obtain vertices of the generated graphic under these parameters
for (rel in Math3DUtil.computeCycloidVertices(r1, r2, w1, w2, count, scale)) {
// Set a SubParticleGroup at these vertices
mutable[withEffect({ u -> ParticleDisplayer.withGroup(TestSubGroupClient(u, bindPlayer)) }) {}] =
rel.clone()
}
return mutable
}
override fun onGroupDisplay() {
MinecraftClient.getInstance().player?.sendMessage(Text.of("Sending particle: ${this::class.java.name} success"))
addPreTickAction {
// This method causes the particle to appear above other players’ heads instead of the bound player’s head...
val bindPlayerEntity = world!!.getPlayerByUuid(bindPlayer) ?: let {
return@addPreTickAction
}
teleportTo(bindPlayerEntity.eyePos)
rotateToWithAngle(
RelativeLocation.of(bindPlayerEntity.rotationVector),
Math.toRadians(10.0)
)
}
}
}
Sub-ParticleGroup Example
class TestSubGroupClient(uuid: UUID, val bindPlayer: UUID) : ControlableParticleGroup(uuid) {
override fun loadParticleLocations(): Map<ParticleRelativeData, RelativeLocation> {
val list = Math3DUtil.getCycloidGraphic(2.0, 2.0, -1, 2, 360, 1.0).onEach { it.y += 6 }
return list.associateBy {
withEffect({ ParticleDisplayer.withSingle(TestEndRodEffect(it)) }) {
color = Vector3f(100 / 255f, 100 / 255f, 255 / 255f)
this.maxAliveTick = this.maxAliveTick
}
}
}
override fun onGroupDisplay() {
addPreTickAction {
val bindPlayerEntity = world!!.getPlayerByUuid(bindPlayer) ?: let {
return@addPreTickAction
}
rotateToWithAngle(
RelativeLocation.of(bindPlayerEntity.rotationVector),
Math.toRadians(-10.0)
)
}
}
}
Other Usage
SequencedParticleGroup Usage
This class addresses the need to control the order and speed of particle generation. It modifies some basic methods of ControlableParticleGroup
. When using this class, use SequencedServerParticleGroup
on the server side.
class SequencedMagicCircleClient(uuid: UUID, val bindPlayer: UUID) : SequencedParticleGroup(uuid) {
// Test scaling
var maxScaleTick = 36
var current = 0
// Provider is the same as usual
class Provider : ControlableParticleGroupProvider {
override fun createGroup(
uuid: UUID,
args: Map<String, ParticleControlerDataBuffer<*>>
): ControlableParticleGroup {
val bindUUID = args["bind_player"]!!.loadedValue as UUID
return SequencedMagicCircleClient(uuid, bindUUID)
}
override fun changeGroup(
group: ControlableParticleGroup,
args: Map<String, ParticleControlerDataBuffer<*>>
) {
}
}
// To record the order of particles, use a sequence here
override fun loadParticleLocationsWithIndex(): SortedMap<SequencedParticleRelativeData, RelativeLocation> {
val res = TreeMap<SequencedParticleRelativeData, RelativeLocation>()
val points = Math3DUtil.getCycloidGraphic(3.0, 5.0, -2, 3, 360, .5)
points.forEachIndexed { index, it ->
res[withEffect(
{ id -> ParticleDisplayer.withSingle(TestEndRodEffect(id)) }, {
color = Vector3f(100 / 255f, 100 / 255f, 255 / 255f)
}, index // Particle order, ascending
)] = it.also { it.y += 15.0 }
}
return res
}
override fun beforeDisplay(locations: SortedMap<SequencedParticleRelativeData, RelativeLocation>) {
super.beforeDisplay(locations)
// Set scaling
scale = 1.0 / maxScaleTick
}
var toggle = false
override fun onGroupDisplay() {
addPreTickAction {
// Set scaling, size loop
if (current < maxScaleTick && !toggle) {
current++
scale(scale + 1.0 / maxScaleTick)
} else if (current < maxScaleTick) {
current++
scale(scale - 1.0 / maxScaleTick)
} else {
toggle = !toggle
current = 0
}
// Set rotation
rotateParticlesAsAxis(Math.toRadians(10.0))
val player = world!!.getPlayerByUuid(bindPlayer) ?: return@addPreTickAction
val dir = player.rotationVector
rotateParticlesToPoint(RelativeLocation.of(dir))
teleportTo(player.eyePos)
}
}
}
Differences between the above particle and ControlableParticleGroup
- By default, the number of particles generated is 0.
- Use
addSingle
,addMultiple
,addAll
,removeSingle
,removeAll
,removeMultiple
to control the particle generation order. - Use
setSingleStatus
to control the order of particles at a specific index. - It is recommended to use
SequencedServerParticleGroup
to control particle generation order.
Corresponding Server-Side
class SequencedMagicCircleServer(val bindPlayer: UUID) : SequencedServerParticleGroup(16.0) {
val maxCount = maxCount()
// Control particles to appear and disappear one by one
var add = false
// Control individual particle controller
var st = 0
val maxSt = 72
var stToggle = false
override fun tick() {
val player = world!!.getPlayerByUuid(bindPlayer) ?: return
setPosOnServer(player.pos)
if (st++ > maxSt) {
if (!stToggle) {
stToggle = true
// Set the display status of a particle on the server
for (i in 0 until maxCount()) {
if (i <= 30) {
setDisplayed(i, true)
} else {
setDisplayed(i, false)
}
}
// Sync to client: particle count and particle status
toggleCurrentCount()
}
return
}
if (add && serverSequencedParticleCount >= maxCount) {
add = false
serverSequencedParticleCount = maxCount
} else if (!add && serverSequencedParticleCount <= 0) {
add = true
serverSequencedParticleCount = 0
}
// Server controls sub-particle generation
if (add) {
addMultiple(10)
} else {
removeMultiple(10)
}
}
override fun otherPacketArgs(): Map<String, ParticleControlerDataBuffer<out Any>> {
return mapOf(
"bind_player" to ParticleControlerDataBuffers.uuid(bindPlayer),
toggleArgLeastIndex(), // Sync particle count, generates from the 1st particle to the serverSequencedParticleCount-th particle
toggleArgStatus() // After generating serverSequencedParticleCount particles, sync the status stored in clientIndexStatus
)
}
override fun getClientType(): Class<out ControlableParticleGroup>? {
return SequencedMagicCircleClient::class.java
}
/**
* Must match the size of SequencedParticleGroup.loadParticleLocationsWithIndex()
* If the particle count of your group is variable (e.g., flushed the particle style with a changed length)
* Ensure proper data synchronization on the server side (size synchronization)
* If maxCount > SequencedParticleGroup.loadParticleLocationsWithIndex().size, it will cause an array out-of-bounds exception
* If maxCount < SequencedParticleGroup.loadParticleLocationsWithIndex().size, it will lead to incomplete particle control (some particles cannot be generated from the server)
*/
override fun maxCount(): Int {
return 360
}
}
Using ParticleGroupStyle
Purpose of this class
When synchronizing rendering data between the client and server, it was found that each new operation required duplicating the same code on the server class and creating the same variables, which was quite cumbersome. Thus, this class was built based on ControlableParticleGroup
and ServerParticleGroup
.
Usage
class ExampleStyle(val bindPlayer: UUID, uuid: UUID = UUID.randomUUID()) :
/**
* First parameter represents the player’s visible range, default is 32.0
* Second parameter is the unique identifier for this particle style
* Use the default value (randomUUID) here
*/
ParticleGroupStyle(16.0, uuid) {
/**
* Like ControlableParticleGroup, to build this class on the server, you also need to create a builder
*/
class Provider : ParticleStyleProvider {
override fun createStyle(
uuid: UUID,
args: Map<String, ParticleControlerDataBuffer<*>>
): ParticleGroupStyle {
val player = args["bind_player"]!!.loadedValue as UUID
return ExampleStyle(player, uuid)
}
}
// Custom parameters
val maxScaleTick = 60
var scaleTick = 0
val maxTick = 240
var current = 0
var angleSpeed = PI / 72
init {
// If you want to modify the base class (ParticleGroupStyle)
// Do not modify in beforeDisplay, modify in the constructor
// Otherwise, it will cause synchronization issues in multiplayer clients (or use change?)
scale = 1.0 / maxScaleTick
}
/**
* Corresponds to ControlableParticleGroup’s loadParticleLocations method
*/
override fun getCurrentFrames(): Map<StyleData, RelativeLocation> {
// Uses a custom point graphic builder, refer to cn.coostack.cooparticlesapi.utils.builder.PointsBuilder
val res = mutableMapOf<StyleData, RelativeLocation>().apply {
putAll(
PointsBuilder()
.addDiscreteCircleXZ(8.0, 720, 10.0)
.createWithStyleData {
// Supports single particles
StyleData { ParticleDisplayer.withSingle(ControlableCloudEffect(it)) }
.withParticleHandler {
colorOfRGB(127, 139, 175)
this.scale(1.5f)
textureSheet = ParticleTextureSheet.PARTICLE_SHEET_LIT
}
})
putAll(
PointsBuilder()
.addCircle(6.0, 4)
.pointsOnEach { it.y -= 12.0 }
.addCircle(6.0, 4)
.pointsOnEach { it.y += 6.0 }
// Your Data builder here
.createWithStyleData {
// Equivalent to ControlableParticleGroup’s withEffect
StyleData {
// Also supports particle groups
// If there are other styles, you can change to ParticleDisplayer.withStyle(xxxStyle(it,...))
ParticleDisplayer.withGroup(
MagicSubGroup(it, bindPlayer)
)
}
}
)
}
return res
}
override fun onDisplay() {
// Enable automatic parameter synchronization
autoToggle = true
/**
* To distinguish between client and server environments
* This class provides the client property
* Alternatively, use world!!.isClient to check if it’s the client
*/
addPreTickAction {
if (scaleTick++ >= maxScaleTick) {
return@addPreTickAction
}
scale(scale + 1.0 / maxScaleTick)
}
addPreTickAction {
current++
if (current >= maxTick) {
remove()
}
val player = world!!.getPlayerByUuid(bindPlayer) ?: return@addPreTickAction
teleportTo(player.pos)
rotateParticlesAsAxis(angleSpeed)
}
}
// When automatic parameter synchronization is enabled, these server parameters will be automatically synced to each client
override fun writePacketArgs(): Map<String, ParticleControlerDataBuffer<*>> {
return mapOf(
"current" to ParticleControlerDataBuffers.int(current),
"angle_speed" to ParticleControlerDataBuffers.double(angleSpeed),
"bind_player" to ParticleControlerDataBuffers.uuid(bindPlayer),
"scaleTick" to ParticleControlerDataBuffers.int(scaleTick),
)
}
// When receiving synchronized data from the server, execute this method
override fun readPacketArgs(args: Map<String, ParticleControlerDataBuffer<*>>) {
if (args.containsKey("current")) {
current = args["current"]!!.loadedValue as Int
}
if (args.containsKey("angle_speed")) {
angleSpeed = args["angle_speed"]!!.loadedValue as Double
}
if (args.containsKey("scaleTick")) {
scaleTick = args["scaleTick"]!!.loadedValue as Int
}
}
}
When the class is built, register it in ClientModInitializer
.
ParticleStyleManager.register(ExampleStyle::class.java, ExampleStyle.Provider())
How to spawn this particle style on the server? Example with an Item
class TestStyleItem : Item(Settings()) {
override fun use(world: World, user: PlayerEntity, hand: Hand): TypedActionResult<ItemStack?>? {
val res = super.use(world, user, hand)
// If you spawn particles in a world.isClient == true environment
// The spawn will only affect this client
// Otherwise, it’s a server-side spawn – all eligible players can see it
if (world.isClient) {
return res
}
val style = ExampleStyle(user.uuid)
// server world
ParticleStyleManager.spawnStyle(world, user.pos, style)
// Test delay for automatic synchronization
CooParticleAPI.scheduler.runTask(30) {
style.angleSpeed += PI / 72
}
return res
}
}
Particle Style Helper Usage Guidelines
- All Helpers must call
loadControler
in the constructor, otherwise, application failure bugs may occur (reason unknown).
Particle Emitters
Introduction
Since all previous examples pointed to a style with a limited number of particles, to better implement effects like explosions, shockwaves, or flames, this class was abstracted.
This particle emitter mainly consists of three classes to implement functionality:
SimpleParticleEmitters
PhysicsParticleEmitters
ClassParticleEmitters
These three classes correspond to three different implementation methods.
First, let’s discuss the first two classes
SimpleParticleEmitters
/**
* Particle emitter position offset expression, providing t as a parameter (t is generation time, int type)
*/
var evalEmittersXWithT = "0"
var evalEmittersYWithT = "0"
var evalEmittersZWithT = "0"
/**
* Provides several presets
* box: box emitter
* point: point emitter
* math: mathematical trajectory emitter
*/
var shootType = EmittersShootTypes.point()
private var bufferX = Expression(evalEmittersXWithT)
private var bufferY = Expression(evalEmittersYWithT)
private var bufferZ = Expression(evalEmittersZWithT)
var offset = Vec3d(0.0, 0.0, 0.0)
/**
* Number of particles generated per tick
*/
var count = 1
/**
* The actual number of particles generated per tick is affected by this random range (0 .. countRandom)
*/
var countRandom = 0
Construction Method
val emitters = SimpleParticleEmitters(position, serverWorld, particleInfo)
val emitters = PhysicsParticleEmitters(position, serverWorld, particleInfo)
Particle Information Synchronization
To facilitate synchronizing particle properties between servers, the ControlableParticleData
class was built. If your ParticleEffect
has additional properties, you can inherit this class and override the PacketCodec
parser.
/**
* Modifiable properties of this class
*/
// UUID is not recommended to be modified
var uuid = UUID.randomUUID()
var velocity: Vec3d = Vec3d.ZERO
var size = 0.2f
var color = Vector3f(1f, 1f, 1f)
var alpha = 1f
var age = 0
var maxAge = 120
// Particle visible range (not tested)
var visibleRange = 128f
// When you want to change to another Effect (only supports ControlableParticleEffect, see TestEndRodEffect for implementation)
var effect: ControlableParticleEffect = TestEndRodEffect(uuid)
var textureSheet = ParticleTextureSheet.PARTICLE_SHEET_TRANSLUCENT
/**
* Particle movement speed, applied in SimpleParticleEmitter and PhysicsParticleEmitter
*/
var speed = 1.0
PhysicsParticleEmitters
This class provides some basic physical parameters: gravity, air density, mass, wind direction.
/**
* Gravitational acceleration, time unit is tick
*/
var gravity = 0.0
/**
* Air density
*/
var airDensity = 0.0
/**
* Wind direction
*/
var wind: WindDirection = GlobalWindDirection(Vec3d.ZERO)
/**
* Mass
* Unit: g
*/
var mass = 1.0
Enable Emitter in the World
ParticleEmittersManager.spawnEmitters(emitter)
Modify Emitter Properties
var pos: Vec3d // Emitter position
var world: World? // World where the emitter resides (cannot be null during construction, nullable because world info is not needed during serialization)
var tick: Int // Generation time, tick
/**
* When maxTick == -1
* Indicates this particle is not controlled by lifecycle
* Particle lifecycle
*/
var maxTick: Int
// Emission delay (delay after each emission)
var delay: Int
// Unique identifier for the emitter, not recommended to modify
var uuid: UUID
// If set to true, the emitter will become invalid
var cancelled: Boolean
// Whether the emitter has already been spawned in the world
var playing: Boolean
Note
- Modify emitter properties only in the server environment.
- Each tick automatically synchronizes emitter properties to all visible clients.
ClassParticleEmitters
This is an abstract class designed to allow developers (me) to avoid writing expressions by generating particles according to custom rules.
abstract class ClassParticleEmitters(
override var pos: Vec3d,
override var world: World?,
) : ParticleEmitters {
override var tick: Int = 0
override var maxTick: Int = 120
override var delay: Int = 0
override var uuid: UUID = UUID.randomUUID()
override var cancelled: Boolean = false
override var playing: Boolean = false
var airDensity = 0.0
var gravity: Double = 0.0
companion object {
fun encodeBase(data: ClassParticleEmitters, buf: RegistryByteBuf) {
buf.writeVec3d(data.pos)
buf.writeInt(data.tick)
buf.writeInt(data.maxTick)
buf.writeInt(data.delay)
buf.writeUuid(data.uuid)
buf.writeBoolean(data.cancelled)
buf.writeBoolean(data.playing)
buf.writeDouble(data.gravity)
buf.writeDouble(data.airDensity)
buf.writeDouble(data.mass)
buf.writeString(data.wind.getID())
data.wind.getCodec().encode(buf, data.wind)
}
/**
* Implementation
* First, create this object in the codec’s decode method
* Then pass buf and container to this method
* Then continue decoding your own parameters
*/
fun decodeBase(container: ClassParticleEmitters, buf: RegistryByteBuf) {
val pos = buf.readVec3d()
val tick = buf.readInt()
val maxTick = buf.readInt()
val delay = buf.readInt()
val uuid = buf.readUuid()
val canceled = buf.readBoolean()
val playing = buf.readBoolean()
val gravity = buf.readDouble()
val airDensity = buf.readDouble()
val mass = buf.readDouble()
val id = buf.readString()
val wind = WindDirections.getCodecFromID(id)
.decode(buf)
container.apply {
this.pos = pos
this.tick = tick
this.maxTick = maxTick
this.delay = delay
this.uuid = uuid
this.cancelled = canceled
this.airDensity = airDensity
this.gravity = gravity
this.mass = mass
this.playing = playing
this.airDensity = airDensity
this.wind = wind
}
}
}
/**
* Wind direction
*/
var wind: WindDirection = GlobalWindDirection(Vec3d.ZERO).also {
it.loadEmitters(this)
}
/**
* Mass
* Unit: g
*/
var mass: Double = 1.0
override fun start() {
if (playing) return
playing = true
if (world?.isClient == false) {
ParticleEmittersManager.updateEmitters(this)
}
}
override fun stop() {
cancelled = true
if (world?.isClient == false) {
ParticleEmittersManager.updateEmitters(this)
}
}
override fun tick() {
if (cancelled || !playing) {
return
}
if (tick++ >= maxTick && maxTick != -1) {
stop()
}
world ?: return
doTick()
if (!world!!.isClient) {
return
}
if (tick % max(1, delay) == 0) {
// Perform particle change operations
// Generate new particles
spawnParticle()
}
}
override fun spawnParticle() {
if (!world!!.isClient) {
return
}
val world = world as ClientWorld
// Generate particle styles
genParticles().forEach {
spawnParticle(world, pos.add(it.value.toVector()), it.key)
}
}
/**
* Both server and client execute this method
* To check server, use if(!world!!.isClient)
*/
abstract fun doTick()
/**
* Particle style generator
*/
abstract fun genParticles(): List<Pair<ControlableParticleData, RelativeLocation>>
/**
* To modify particle position, velocity, or properties
* Modify ControlableParticleData directly
* @param data Class for manipulating single particle properties
* To execute tick, use controler.addPreTickAction
*/
abstract fun singleParticleAction(
controler: ParticleControler,
data: ControlableParticleData,
spawnPos: RelativeLocation,
spawnWorld: World
)
private fun spawnParticle(world: ClientWorld, pos: Vec3d, data: ControlableParticleData) {
val effect = data.effect
effect.controlUUID = data.uuid
val displayer = ParticleDisplayer.withSingle(effect)
val control = ControlParticleManager.createControl(effect.controlUUID)
control.initInvoker = {
this.size = data.size
this.color = data.color
this.currentAge = data.age
this.maxAge = data.maxAge
this.textureSheet = data.textureSheet
this.particleAlpha = data.alpha
}
singleParticleAction(control, data, pos, world)
control.addPreTickAction {
// Simulate particle motion speed
teleportTo(
this.pos.add(data.velocity)
)
if (currentAge++ >= maxAge) {
markDead()
}
}
displayer.display(pos, world)
}
protected fun updatePhysics(pos: Vec3d, data: ControlableParticleData) {
val m = mass / 1000
val v = data.velocity
val speed = v.length()
val gravityForce = Vec3d(0.0, -m * gravity, 0.0)
val airResistanceForce = if (speed > 0.01) {
val dragMagnitude = 0.5 * airDensity * DRAG_COEFFICIENT *
CROSS_SECTIONAL_AREA * speed.pow(2) * 0.05
v.normalize().multiply(-dragMagnitude)
} else {
Vec3d.ZERO
}
val windForce = WindDirections.handleWindForce(
wind, pos,
airDensity, DRAG_COEFFICIENT, CROSS_SECTIONAL_AREA, v
)
val a = gravityForce
.add(airResistanceForce)
.add(windForce)
.multiply(1.0 / m)
data.velocity = v.add(a)
}
/**
* Optional override
* If overridden, ensure to call super.update() (this method)
* Or replicate the exact same code
* Otherwise, update failures may occur
*
* Implementation Notes
* If your ClassParticleEmitters has new parameters
* And these parameters may change externally during use
* You must assign them in update
* The input parameter emitters receives the updated emitters (used only for parameter transfer)
*/
override fun update(emitters: ParticleEmitters) {
if (emitters !is ClassParticleEmitters) return
this.pos = emitters.pos
this.world = emitters.world
this.tick = emitters.tick
this.maxTick = emitters.maxTick
this.delay = emitters.delay
this.uuid = emitters.uuid
this.cancelled = emitters.cancelled
this.playing = emitters.playing
}
}
Implementation Example
class ExampleClassParticleEmitters(pos: Vec3d, world: World?) : ClassParticleEmitters(pos, world) {
var moveDirection = Vec3d.ZERO
var templateData = ControlableParticleData()
companion object {
// Must provide emitter ID for serialization
const val ID = "example-class-particle-emitters"
@JvmStatic
// Build your own CODEC for synchronizing your data
val CODEC = PacketCodec.ofStatic<RegistryByteBuf, ParticleEmitters>(
{ buf, data ->
data as ExampleClassParticleEmitters
// Must call this method (from ClassParticleEmitters) to sync parent class parameters
encodeBase(data, buf)
buf.writeVec3d(data.moveDirection)
ControlableParticleData.PACKET_CODEC.encode(buf, data.templateData)
}, {
val instance = ExampleClassParticleEmitters(Vec3d.ZERO, null)
// Must call this method (from ClassParticleEmitters) to deserialize parent class parameters
decodeBase(instance, it)
instance.moveDirection = it.readVec3d()
instance.templateData = ControlableParticleData.PACKET_CODEC.decode(it)
instance
}
)
}
// Your emitter tick
override fun doTick() {
pos = pos.add(moveDirection)
}
/**
* Called after every delay tick
* delay is a parameter provided by ParticleEmitters, same meaning as above
* Get particle generation position
*/
override fun genParticles(): List<Pair<ControlableParticleData, RelativeLocation>> {
return PointsBuilder()
.addBall(2.0, 20)
.create().associateBy {
// Copy input particle data
templateData.clone()
.apply {
// Modify initial particle velocity (similar to ball contraction)
this.velocity = it.normalize().multiplyClone(-0.1).toVector()
}
}
}
override fun singleParticleAction(
controler: ParticleControler,
data: ControlableParticleData,
spawnPos: RelativeLocation,
spawnWorld: World
) {
// Executed for each generated particle
// To set motion, opacity, or color changes for individual particles
// Use controler.addPreTickAction to set per-tick particle change methods
// Changes to data will also be applied to the particle
// spawnPos is the initial generation position
// spawnWorld is the initial generation world (unchanging)
}
override fun update(emitters: ParticleEmitters) {
super.update(emitters)
if (emitters !is ExampleClassParticleEmitters) {
return
}
this.templateData = emitters.templateData
this.moveDirection = emitters.moveDirection
}
override fun getEmittersID(): String {
return ID
}
override fun getCodec(): PacketCodec<RegistryByteBuf, ParticleEmitters> {
return CODEC
}
}
After implementation, don’t forget to register.
ParticleEmittersManager.register(ExampleClassParticleEmitters.ID, ExampleClassParticleEmitters.CODEC)
Requires:
How to install:
How To Download & Install Mods with Minecraft Forge
How To Download & Install Fabric Mods
Don’t miss out today’s latest Minecraft Mods
CooParticles API Mod (1.21.1) Download Links
For Minecraft 1.21.1, 1.21
NeoForge version: Download from Server 1 – Download from Server 2
Fabric version: Download from Server 1 – Download from Server 2