Skip to content

Commit efee6d4

Browse files
committed
draw shocks as arcs off the shocker with a custom particle
1 parent bf75c5f commit efee6d4

6 files changed

Lines changed: 408 additions & 64 deletions

File tree

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package openshock.integrations.minecraft
2+
3+
//? if >=1.21.4 {
4+
import net.minecraft.client.Camera
5+
import net.minecraft.client.Minecraft
6+
import net.minecraft.client.multiplayer.ClientLevel
7+
import net.minecraft.client.particle.Particle
8+
import net.minecraft.client.particle.ParticleProvider
9+
import net.minecraft.client.particle.ParticleRenderType
10+
import net.minecraft.client.particle.SpriteSet
11+
import net.minecraft.core.particles.SimpleParticleType
12+
import kotlin.math.atan2
13+
import kotlin.math.sqrt
14+
15+
// The base class for a sprite particle was TextureSheetParticle until 1.21.11 folded it into
16+
// SingleQuadParticle, which now takes the sprite in its constructor and asks for a Layer instead
17+
// of a ParticleRenderType. Everything else about this file is the same on both.
18+
//? if >=1.21.11 {
19+
import net.minecraft.client.particle.SingleQuadParticle
20+
import net.minecraft.client.renderer.texture.TextureAtlasSprite
21+
import net.minecraft.util.RandomSource
22+
//?} else {
23+
/*import net.minecraft.client.particle.TextureSheetParticle
24+
*///?}
25+
26+
/**
27+
* One straight piece of a bolt: a streak that lies along the segment it was sent for.
28+
*
29+
* This is what stops the arcs reading as a row of dots. Vanilla has no particle shaped like a line
30+
* - its sparks are round blobs - so however carefully you space them, a bolt built out of them is
31+
* a dotted line and looks like scattered sparkle. Here each particle *is* a segment: the server
32+
* sends the midpoint and the vector from one end to the other, and this turns that into a streak
33+
* of the right length pointing the right way, so consecutive ones join into a continuous jagged
34+
* line.
35+
*
36+
* ### The velocity is not a velocity
37+
*
38+
* [ServerLevel.sendParticles] with a count of zero sends the three offsets as the particle's
39+
* speed, which is the only way to get three arbitrary numbers down to a client per particle
40+
* without a packet of our own. So [openshock.integrations.minecraft.ShockEffects] puts the
41+
* segment vector there instead: its length is the length of the streak and its direction is the
42+
* direction of the streak. The particle then stands perfectly still, which it has to - a streak
43+
* that drifts stops lining up with its neighbours and the bolt comes apart.
44+
*
45+
* ### Pointing it
46+
*
47+
* The quad always faces the camera, and [roll] spins it in screen space. So all that is needed is
48+
* the angle of the segment *as the camera sees it*: project the world direction onto the camera's
49+
* up and left vectors and take the arctangent. The texture streak runs along the quad's local Y,
50+
* which is screen-up, so rotating by that angle lays it along the segment.
51+
*
52+
* Worked out once per tick from the camera rather than per frame, which would need a render
53+
* override and three versions of one. A tick of lag on a rotation nobody is tracking is invisible,
54+
* and the particle only lives five of them.
55+
*/
56+
//? if >=1.21.11 {
57+
class ShockArcParticle private constructor(
58+
level: ClientLevel,
59+
x: Double,
60+
y: Double,
61+
z: Double,
62+
segmentX: Double,
63+
segmentY: Double,
64+
segmentZ: Double,
65+
sprite: TextureAtlasSprite,
66+
) : SingleQuadParticle(level, x, y, z, 0.0, 0.0, 0.0, sprite) {
67+
//?} else {
68+
/*class ShockArcParticle private constructor(
69+
level: ClientLevel,
70+
x: Double,
71+
y: Double,
72+
z: Double,
73+
segmentX: Double,
74+
segmentY: Double,
75+
segmentZ: Double,
76+
sprite: SpriteSet,
77+
) : TextureSheetParticle(level, x, y, z, 0.0, 0.0, 0.0) {
78+
*///?}
79+
80+
/** The segment as a unit vector, kept for [aim]. */
81+
private val dirX: Double
82+
private val dirY: Double
83+
private val dirZ: Double
84+
85+
/** Faded out over the last of these, so a strike decays rather than blinks off. */
86+
private val fadeFrom: Int
87+
88+
init {
89+
val length = sqrt(segmentX * segmentX + segmentY * segmentY + segmentZ * segmentZ)
90+
.coerceAtLeast(0.001)
91+
92+
dirX = segmentX / length
93+
dirY = segmentY / length
94+
dirZ = segmentZ / length
95+
96+
// quadSize is the half-extent, so this makes the streak exactly as long as the segment,
97+
// plus a little so neighbours overlap instead of leaving a seam at every joint.
98+
quadSize = (length * 0.5 * 1.2).toFloat()
99+
100+
// Just longer than the two ticks between bursts, so one set of arcs is still fading as
101+
// the next is struck and the flicker has no gaps in it. Any longer and they pile up into
102+
// a haze instead of reading as separate flashes.
103+
lifetime = LIFETIME
104+
fadeFrom = 1
105+
106+
// Nothing moves it: no gravity, no friction, no collision, no drift.
107+
hasPhysics = false
108+
gravity = 0f
109+
friction = 1f
110+
111+
aim()
112+
}
113+
114+
override fun tick() {
115+
super.tick()
116+
117+
if (age >= fadeFrom) {
118+
alpha = 1f - (age - fadeFrom).toFloat() / (lifetime - fadeFrom).toFloat()
119+
}
120+
121+
aim()
122+
}
123+
124+
/** Lays the streak along its segment, as seen from where the camera is now. */
125+
private fun aim() {
126+
// 26.2 dropped the get- prefix, and the private field it used to hide behind is still
127+
// there - so this has to name the method rather than let Kotlin pick a property.
128+
//? if >=26.2 {
129+
val camera: Camera = Minecraft.getInstance().gameRenderer.mainCamera()
130+
//?} else {
131+
/*val camera: Camera = Minecraft.getInstance().gameRenderer.getMainCamera()
132+
*///?}
133+
134+
//? if >=1.21.11 {
135+
val up = camera.upVector()
136+
val left = camera.leftVector()
137+
//?} else {
138+
/*val up = camera.getUpVector()
139+
val left = camera.getLeftVector()
140+
*///?}
141+
142+
val alongUp = dirX * up.x() + dirY * up.y() + dirZ * up.z()
143+
val alongLeft = dirX * left.x() + dirY * left.y() + dirZ * left.z()
144+
145+
// Rotating the quad's local +Y by roll counter-clockwise gives (-sin, cos) in screen
146+
// space, and that has to line up with (right, up) - hence left rather than right on top.
147+
// A streak is symmetric end to end, so being half a turn out would not show either way.
148+
roll = atan2(alongLeft, alongUp).toFloat()
149+
}
150+
151+
//? if >=1.21.11 {
152+
override fun getLayer(): SingleQuadParticle.Layer = SingleQuadParticle.Layer.TRANSLUCENT
153+
//?} else {
154+
/*override fun getRenderType(): ParticleRenderType = ParticleRenderType.PARTICLE_SHEET_TRANSLUCENT
155+
*///?}
156+
157+
companion object {
158+
/** A tick longer than the gap between bursts, so the flicker never leaves a blank frame. */
159+
private const val LIFETIME = 3
160+
}
161+
162+
/**
163+
* Handed the sprite sheet by whichever loader is registering us - both of them supply one, and
164+
* this particle only ever uses the single frame in it.
165+
*/
166+
class Provider(private val sprites: SpriteSet) : ParticleProvider<SimpleParticleType> {
167+
168+
//? if >=1.21.11 {
169+
override fun createParticle(
170+
options: SimpleParticleType,
171+
level: ClientLevel,
172+
x: Double,
173+
y: Double,
174+
z: Double,
175+
segmentX: Double,
176+
segmentY: Double,
177+
segmentZ: Double,
178+
random: RandomSource,
179+
): Particle = ShockArcParticle(
180+
level, x, y, z, segmentX, segmentY, segmentZ, sprites.get(random),
181+
)
182+
//?} else {
183+
/*override fun createParticle(
184+
options: SimpleParticleType,
185+
level: ClientLevel,
186+
x: Double,
187+
y: Double,
188+
z: Double,
189+
segmentX: Double,
190+
segmentY: Double,
191+
segmentZ: Double,
192+
): Particle = ShockArcParticle(
193+
level, x, y, z, segmentX, segmentY, segmentZ, sprites,
194+
).also { it.pickSprite(sprites) }
195+
*///?}
196+
}
197+
}
198+
//?}

0 commit comments

Comments
 (0)