Jolt Physics Block Destruction Plugin
Integrate a C++ physics engine into a Kotlin Paper plugin with display entities.
Introduction
30 Second Summary
Every time a building crumbles in a video game and debris scatters realistically, a physics engine is doing the heavy lifting behind the scenes. That same technology can bring Minecraft's static block world to life.
In this project, you will build a Paper plugin that loads the Jolt Physics engine via JNI to simulate rigid body dynamics server-side. Blocks shatter into physics-simulated pieces and lanterns swing like pendulums, all visualized through display entities.
What You'll Build
A Kotlin Paper plugin called "JoltPhysics" that integrates jolt-jni into a Minecraft server, converting blocks into physics-simulated rigid bodies rendered with ItemDisplay entities and tick-synchronized transformation matrices.
By the end of this project, you'll have:
- A working physics world with collision layers separating moving and non-moving objects, powered by Jolt's native C++ engine running inside your JVM server.
- A block destruction system that shatters blocks in a radius into rigid bodies with realistic gravity, bounce, and collision.
- A lantern swing system using PointConstraint for pendulum physics, with smooth 60fps-like rendering via client-side interpolation.
- Secret Mission: Add player punch interaction so targeting a block sends it flying with physics in the direction you're facing.
Are there any prerequisites?
This project assumes you are proficient in Kotlin and have experience developing Paper plugins. You should already have IntelliJ IDEA, JDK 25, a Paper 26.1.2 server, and a Gradle project set up and ready to go.
Add Jolt-JNI Dependencies and Bootstrap the Plugin
Jolt Physics is a high-performance C++ physics engine. To use it from Kotlin on the JVM, you need jolt-jni (Java/Kotlin bindings that call the native C++ library through JNI).
The challenge is that native libraries are platform-specific binaries embedded inside JAR files. They need extracting and loading before your plugin can call any physics functions. In this step, you will wire up the Gradle dependencies, create the plugin skeleton, and implement cross-platform native loading with snaploader.
In this step, get ready to:
- Add jolt-jni and snaploader dependencies to your Gradle build file.
- Create the plugin main class and plugin.yml descriptor.
- Implement native library loading using snaploader's platform-aware extraction.
Add jolt-jni and snaploader dependencies to build.gradle.kts
Your plugin needs four external libraries to get physics working. The jolt-jni artifact provides the JVM API classes. Its DebugSp classifier bundles the actual native binary for Windows x64. snaploader handles extracting that native binary from the JAR at runtime. oshi-core is a runtime dependency snaploader uses for platform detection.
- Open build.gradle.kts in IntelliJ IDEA.
- Replace the entire contents of the file with the following:
plugins {
kotlin("jvm") version "2.1.0"
id("io.papermc.paperweight.userdev") version "2.0.0-beta.21"
}
group = "com.example"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
// Paper development bundle - provides the full Paper API
paperweight.paperDevBundle("26.1.2.build.+")
// Jolt-JNI: JVM bindings for the Jolt Physics C++ engine
implementation("com.github.stephengold:jolt-jni-Windows64:5.1.0")
// Native binary (single-precision debug build)
runtimeOnly("com.github.stephengold:jolt-jni-Windows64:5.1.0:DebugSp")
// Snaploader: cross-platform native library extractor and loader
implementation("io.github.electrostat-lab:snaploader:1.1.1-stable")
// Oshi: platform detection dependency for snaploader
runtimeOnly("com.github.oshi:oshi-core:7.3.2")
}
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(25))
}
tasks.assemble {
dependsOn(tasks.reobfJar)
}
What does this build file do?
- paperweight.paperDevBundle("26.1.2.build.+") pulls in the Paper API and internals matching Minecraft 26.1.2.
- The implementation dependency on jolt-jni gives you compile-time access to Jolt's Java classes (PhysicsSystem, Body, BoxShape, etc.).
- The runtimeOnly DebugSp classifier embeds the compiled C++ physics library inside the JAR so snaploader can extract it at server startup.
- tasks.reobfJar re-obfuscates the jar to match Paper's runtime mappings.
- Save build.gradle.kts.
- IntelliJ IDEA should prompt you to sync Gradle changes. Click the sync icon or press Ctrl+Shift+O (Windows) or Cmd+Shift+O (macOS) to trigger a Gradle sync.
- Wait for the sync to complete. You should see no errors in the Build tool window.
Gradle sync failing?
- Check that you have internet access. Gradle needs to download the new dependencies from Maven Central.
- Make sure your Gradle JVM is set to JDK 25. Go to Settings then Build, Execution, Deployment then Build Tools then Gradle and confirm the Gradle JVM matches your project JDK.
- If you see an offline-mode error, disable offline mode in the Gradle tool window and re-sync.
Ask for help: Why is my Gradle sync failing after adding jolt-jni dependencies?
✔️ Awesome, I've got everything!
Great. Make sure build.gradle.kts is saved before moving on.
ⓧ I'd like to double check the full code
plugins {
kotlin("jvm") version "2.1.0"
id("io.papermc.paperweight.userdev") version "2.0.0-beta.21"
}
group = "com.example"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
paperweight.paperDevBundle("26.1.2.build.+")
implementation("com.github.stephengold:jolt-jni-Windows64:5.1.0")
runtimeOnly("com.github.stephengold:jolt-jni-Windows64:5.1.0:DebugSp")
implementation("io.github.electrostat-lab:snaploader:1.1.1-stable")
runtimeOnly("com.github.oshi:oshi-core:7.3.2")
}
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(25))
}
tasks.assemble {
dependsOn(tasks.reobfJar)
}
Create the plugin main class and plugin.yml
Every Paper plugin needs two things: a plugin.yml descriptor that tells the server about your plugin, and a main class that extends JavaPlugin.
- Create the directory src/main/resources if it does not already exist.
- Create a new file called plugin.yml inside src/main/resources and paste the following:
name: JoltPhysics
version: 1.0.0
main: com.example.joltphysics.JoltPhysicsPlugin
api-version: "26.1.2"
commands:
shatter:
description: Shatter blocks in a radius into physics bodies
usage: /shatter <radius>
swing:
description: Make a targeted lantern swing with physics
usage: /swing
What does plugin.yml do?
This file is the plugin's identity card. The server reads it to know the plugin's name, entry point class, and what commands it registers. The api-version field tells Paper which API version your plugin targets.
- Save plugin.yml.
- Now create the plugin's main class. Create the directory path src/main/kotlin/com/example/joltphysics if it does not exist.
- Create a new file called JoltPhysicsPlugin.kt inside that directory and paste the following:
package com.example.joltphysics
import org.bukkit.plugin.java.JavaPlugin
class JoltPhysicsPlugin : JavaPlugin() {
lateinit var physicsWorld: PhysicsWorld
lateinit var renderLoop: RenderLoop
override fun onEnable() {
// Load the native Jolt library for the current platform
NativeLoader.load(this)
// Initialize the physics simulation
physicsWorld = PhysicsWorld()
physicsWorld.initialize()
// Start the render loop that syncs physics to display entities
renderLoop = RenderLoop(this, physicsWorld)
renderLoop.start()
// Register slash commands
getCommand("shatter")?.setExecutor(ShatterCommand(this))
getCommand("swing")?.setExecutor(SwingCommand(this))
logger.info("JoltPhysics plugin enabled - physics world initialized")
}
override fun onDisable() {
renderLoop.stop()
physicsWorld.destroy()
logger.info("JoltPhysics plugin disabled - physics world destroyed")
}
}
What does this class do?
- onEnable() runs when the server loads the plugin. It loads the native library, boots the physics world, starts the render loop, and registers both commands.
- onDisable() runs on server shutdown. It stops the render loop and frees physics memory to prevent resource leaks.
- The lateinit properties let other classes (commands, listeners) access the shared physics world and render loop.
The project will show compile errors for PhysicsWorld, RenderLoop, ShatterCommand, and SwingCommand. That is expected. You will create those classes in later steps.
- Save JoltPhysicsPlugin.kt.
✔️ Awesome, I've got everything!
Great. Both files are saved and you can move on to the native loader.
ⓧ I'd like to double check the full code
name: JoltPhysics
version: 1.0.0
main: com.example.joltphysics.JoltPhysicsPlugin
api-version: "26.1.2"
commands:
shatter:
description: Shatter blocks in a radius into physics bodies
usage: /shatter <radius>
swing:
description: Make a targeted lantern swing with physics
usage: /swing
package com.example.joltphysics
import org.bukkit.plugin.java.JavaPlugin
class JoltPhysicsPlugin : JavaPlugin() {
lateinit var physicsWorld: PhysicsWorld
lateinit var renderLoop: RenderLoop
override fun onEnable() {
NativeLoader.load(this)
physicsWorld = PhysicsWorld()
physicsWorld.initialize()
renderLoop = RenderLoop(this, physicsWorld)
renderLoop.start()
getCommand("shatter")?.setExecutor(ShatterCommand(this))
getCommand("swing")?.setExecutor(SwingCommand(this))
logger.info("JoltPhysics plugin enabled - physics world initialized")
}
override fun onDisable() {
renderLoop.stop()
physicsWorld.destroy()
logger.info("JoltPhysics plugin disabled - physics world destroyed")
}
}
Implement native library loading with snaploader
The jolt-jni JAR contains pre-compiled native binaries for each platform (Linux, macOS, Windows). At runtime, snaploader detects the server's operating system and CPU architecture, extracts the matching binary to disk, and loads it into the JVM.
- Create a new file called NativeLoader.kt inside src/main/kotlin/com/example/joltphysics and paste the following:
package com.example.joltphysics
import electrostatic4j.snaploader.LibraryInfo
import electrostatic4j.snaploader.LoadingCriterion
import electrostatic4j.snaploader.NativeBinaryLoader
import electrostatic4j.snaploader.filesystem.DirectoryPath
import electrostatic4j.snaploader.platform.NativeDynamicLibrary
import electrostatic4j.snaploader.platform.util.PlatformPredicate
import org.bukkit.plugin.java.JavaPlugin
object NativeLoader {
fun load(plugin: JavaPlugin) {
// Define library metadata: no jar path (classpath), basename "joltjni", extract to user.dir
val info = LibraryInfo(null, "joltjni", DirectoryPath.USER_DIR)
val loader = NativeBinaryLoader(info)
// Register platform-specific native binaries with their OS/arch predicates
val libraries = arrayOf(
NativeDynamicLibrary("linux/aarch64/com/github/stephengold", PlatformPredicate.LINUX_ARM_64),
NativeDynamicLibrary("linux/armhf/com/github/stephengold", PlatformPredicate.LINUX_ARM_32),
NativeDynamicLibrary("linux/x86-64/com/github/stephengold", PlatformPredicate.LINUX_X86_64),
NativeDynamicLibrary("osx/aarch64/com/github/stephengold", PlatformPredicate.MACOS_ARM_64),
NativeDynamicLibrary("osx/x86-64/com/github/stephengold", PlatformPredicate.MACOS_X86_64),
NativeDynamicLibrary("windows/x86-64/com/github/stephengold", PlatformPredicate.WIN_X86_64)
)
// Initialize platform detection and extract + load the native binary
loader.registerNativeLibraries(libraries).initPlatformLibrary()
loader.loadLibrary(LoadingCriterion.CLEAN_EXTRACTION)
plugin.logger.info("Jolt-JNI native library loaded successfully")
}
}
What does this code do?
- LibraryInfo(null, "joltjni", DirectoryPath.USER_DIR) tells snaploader to search the classpath (no explicit jar path), look for a library with basename joltjni, and extract it to the server's working directory.
- Each NativeDynamicLibrary maps a path inside the JAR to a platform predicate. At runtime, snaploader evaluates each predicate and picks the one matching the current OS and architecture.
- initPlatformLibrary() performs the platform detection. loadLibrary(LoadingCriterion.CLEAN_EXTRACTION) extracts a fresh copy of the native binary and loads it via System.load().
This pattern means you never call System.loadLibrary() manually. Snaploader handles the messy work of finding the right binary for whatever OS the server runs on.
- Save NativeLoader.kt.
- Trigger another Gradle sync to confirm no import errors exist on the snaploader and jolt-jni imports.
Unresolved imports in NativeLoader.kt?
- If electrostatic4j.snaploader imports show red, your Gradle sync has not completed. Open the Gradle tool window and click the sync button.
- Confirm that implementation("io.github.electrostat-lab:snaploader:1.1.1-stable") appears in your build.gradle.kts dependencies block exactly as shown.
Ask for help: Why are my snaploader imports unresolved after Gradle sync?
✔️ Awesome, I've got everything!
Great. Your native loader is ready. The compile errors for PhysicsWorld and other classes are expected and will resolve in the next steps.
ⓧ I'd like to double check the full code
package com.example.joltphysics
import electrostatic4j.snaploader.LibraryInfo
import electrostatic4j.snaploader.LoadingCriterion
import electrostatic4j.snaploader.NativeBinaryLoader
import electrostatic4j.snaploader.filesystem.DirectoryPath
import electrostatic4j.snaploader.platform.NativeDynamicLibrary
import electrostatic4j.snaploader.platform.util.PlatformPredicate
import org.bukkit.plugin.java.JavaPlugin
object NativeLoader {
fun load(plugin: JavaPlugin) {
val info = LibraryInfo(null, "joltjni", DirectoryPath.USER_DIR)
val loader = NativeBinaryLoader(info)
val libraries = arrayOf(
NativeDynamicLibrary("linux/aarch64/com/github/stephengold", PlatformPredicate.LINUX_ARM_64),
NativeDynamicLibrary("linux/armhf/com/github/stephengold", PlatformPredicate.LINUX_ARM_32),
NativeDynamicLibrary("linux/x86-64/com/github/stephengold", PlatformPredicate.LINUX_X86_64),
NativeDynamicLibrary("osx/aarch64/com/github/stephengold", PlatformPredicate.MACOS_ARM_64),
NativeDynamicLibrary("osx/x86-64/com/github/stephengold", PlatformPredicate.MACOS_X86_64),
NativeDynamicLibrary("windows/x86-64/com/github/stephengold", PlatformPredicate.WIN_X86_64)
)
loader.registerNativeLibraries(libraries).initPlatformLibrary()
loader.loadLibrary(LoadingCriterion.CLEAN_EXTRACTION)
plugin.logger.info("Jolt-JNI native library loaded successfully")
}
}
Your build file, plugin descriptor, main class, and native loader are all in place. Next, you will initialize the Jolt Physics world with collision layers and a ground plane.
Initialize the Physics World
Your plugin can now load Jolt's native C++ library into the JVM at startup. The next challenge is standing up an actual physics simulation that bodies can live in.
A physics world needs several components: memory allocation, a thread pool for parallel collision detection, rules defining which objects can collide, and at least one static surface for things to land on. In this step, you'll build all of that inside a new PhysicsWorld class.
In this step, get ready to:
- Initialize the Jolt engine with its required startup sequence and thread pool.
- Define collision layers that separate moving objects from static geometry.
- Create a static ground plane so physics bodies have something to land on.
Create the Jolt initialization sequence
Jolt Physics requires a specific boot sequence before you can simulate anything. Each call registers a subsystem that later calls depend on.
- Create a new file PhysicsWorld.kt in src/main/kotlin/com/example/joltphysics/ and paste the following code:
package com.example.joltphysics
import com.github.stephengold.joltjni.*
import com.github.stephengold.joltjni.enumerate.EActivation
import com.github.stephengold.joltjni.enumerate.EMotionType
class PhysicsWorld {
companion object {
const val OBJ_LAYER_MOVING = 0
const val OBJ_LAYER_NON_MOVING = 1
const val NUM_OBJ_LAYERS = 2
const val NUM_BP_LAYERS = 1
}
lateinit var physicsSystem: PhysicsSystem
lateinit var tempAllocator: TempAllocator
lateinit var jobSystem: JobSystem
lateinit var bodyInterface: BodyInterface
fun initialize() {
// Start the garbage collector for Jolt native objects
JoltPhysicsObject.startCleaner()
Jolt.registerDefaultAllocator()
Jolt.installDefaultAssertCallback()
Jolt.installDefaultTraceCallback()
Jolt.newFactory()
Jolt.registerTypes()
// Create memory allocator and parallel job system
tempAllocator = TempAllocatorMalloc()
val numWorkerThreads = Runtime.getRuntime().availableProcessors()
jobSystem = JobSystemThreadPool(Jolt.cMaxPhysicsJobs, Jolt.cMaxPhysicsBarriers, numWorkerThreads)
}
}
What does this code do?
The companion object defines collision layer constants. Every physics body belongs to either the MOVING or NON_MOVING layer, which determines what it can collide with.
The initialize() function runs Jolt's mandatory boot sequence in order. Each call depends on the previous one completing. TempAllocatorMalloc provides scratch memory for each physics step, and JobSystemThreadPool parallelizes collision detection across all available CPU cores.
- Save PhysicsWorld.kt.
- Check that IntelliJ shows no red underlines in the file. The lateinit properties and Jolt imports should all resolve correctly.
Seeing unresolved references?
If IntelliJ can't find com.github.stephengold.joltjni classes, click the Gradle sync icon (elephant) in the toolbar to refresh dependencies. The jolt-jni JAR must be downloaded before the IDE can resolve its types.
help me fix unresolved Jolt-JNI imports in IntelliJ
Define collision layers and configure the physics system
Collision layers tell Jolt which pairs of objects should be checked for contact. Moving objects need to collide with everything, but two static objects never need to check against each other.
- In PhysicsWorld.kt, find this line inside initialize():
jobSystem = JobSystemThreadPool(Jolt.cMaxPhysicsJobs, Jolt.cMaxPhysicsBarriers, numWorkerThreads)
- Add the following code directly below that line, still inside the initialize() function:
// Define which object layers can collide with each other
val ovoFilter = ObjectLayerPairFilterTable(NUM_OBJ_LAYERS)
ovoFilter.enableCollision(OBJ_LAYER_MOVING, OBJ_LAYER_MOVING)
ovoFilter.enableCollision(OBJ_LAYER_MOVING, OBJ_LAYER_NON_MOVING)
ovoFilter.disableCollision(OBJ_LAYER_NON_MOVING, OBJ_LAYER_NON_MOVING)
// Map object layers to broad phase layers for fast AABB culling
val layerMap = BroadPhaseLayerInterfaceTable(NUM_OBJ_LAYERS, NUM_BP_LAYERS)
layerMap.mapObjectToBroadPhaseLayer(OBJ_LAYER_MOVING, 0)
layerMap.mapObjectToBroadPhaseLayer(OBJ_LAYER_NON_MOVING, 0)
val ovbFilter = ObjectVsBroadPhaseLayerFilterTable(layerMap, NUM_BP_LAYERS, ovoFilter, NUM_OBJ_LAYERS)
// Initialize the physics system with capacity limits
physicsSystem = PhysicsSystem()
physicsSystem.init(5000, 0, 65536, 20480, layerMap, ovbFilter, ovoFilter)
bodyInterface = physicsSystem.getBodyInterface()
createGroundPlane()
What does this code do?
The ObjectLayerPairFilterTable is a matrix of yes/no collision rules. Moving-to-moving collisions are enabled (blocks hitting blocks). Moving-to-non-moving is enabled (blocks hitting the ground). Non-moving-to-non-moving is disabled because two static objects can never contact each other.
The BroadPhaseLayerInterfaceTable maps object layers into broad phase layers. The broad phase is a fast spatial index (AABB tree) that eliminates obviously non-colliding pairs before running expensive narrow-phase checks. Both object layers share one broad phase layer here because all objects live in the same spatial region.
The physicsSystem.init() call reserves capacity for 5000 bodies, 65536 body pairs, and 20480 contact constraints. The bodyInterface is the primary API for creating, removing, and querying bodies.
Why two layers of collision filtering?
Jolt separates collision detection into a cheap broad phase (bounding boxes) and an expensive narrow phase (exact geometry). The ObjectLayerPairFilter prevents pairs from ever reaching narrow phase. The BroadPhaseLayerInterface controls how objects are grouped in the spatial tree. Together they let you eliminate unnecessary work at two different stages of the pipeline.
- Save PhysicsWorld.kt.
- IntelliJ will show a red underline on createGroundPlane() because the function doesn't exist yet. You'll add it next.
Create a static ground plane body
Every physics simulation needs a floor. Without one, dynamic bodies fall forever into the void. You'll create a large static box positioned so its top surface sits at y=0.
- In PhysicsWorld.kt, add the following function after the closing brace of initialize():
private fun createGroundPlane() {
// Create a large flat box to act as the ground surface
val groundShape = BoxShape(Vec3(100f, 0.5f, 100f))
val groundSettings = BodyCreationSettings()
groundSettings.setShape(groundShape)
groundSettings.setPosition(RVec3(0.0, -0.5, 0.0))
groundSettings.setMotionType(EMotionType.Static)
groundSettings.setObjectLayer(OBJ_LAYER_NON_MOVING)
val ground = bodyInterface.createBody(groundSettings)
bodyInterface.addBody(ground, EActivation.DontActivate)
}
What does this code do?
BoxShape(Vec3(100f, 0.5f, 100f)) creates a box with half-extents of 100 meters on X/Z and 0.5 meters on Y. The full box is 200x1x200 meters.
Positioning it at RVec3(0.0, -0.5, 0.0) means its center is 0.5 meters below the origin. Since the box is 1 meter tall, its top surface lands exactly at y=0. Dynamic bodies dropped from above will collide with this surface.
EActivation.DontActivate tells Jolt not to wake this body for simulation. Static bodies never move, so they don't need activation.
Now add the remaining body management methods that the rest of the plugin will call. These handle creating dynamic bodies, stepping the simulation, querying transforms, and cleanup.
- Add the following methods below createGroundPlane(), still inside the PhysicsWorld class:
fun createDynamicBox(x: Double, y: Double, z: Double): Body {
val boxShape = BoxShape(Vec3(0.5f, 0.5f, 0.5f))
val settings = BodyCreationSettings()
settings.setShape(boxShape)
settings.setPosition(RVec3(x, y, z))
settings.setMotionType(EMotionType.Dynamic)
settings.setObjectLayer(OBJ_LAYER_MOVING)
val body = bodyInterface.createBody(settings)
bodyInterface.addBody(body, EActivation.Activate)
return body
}
fun createConstrainedLantern(anchorX: Double, anchorY: Double, anchorZ: Double,
lanternX: Double, lanternY: Double, lanternZ: Double): Body {
// Create a static anchor point at the ceiling
val anchorShape = BoxShape(Vec3(0.1f, 0.1f, 0.1f))
val anchorSettings = BodyCreationSettings()
anchorSettings.setShape(anchorShape)
anchorSettings.setPosition(RVec3(anchorX, anchorY, anchorZ))
anchorSettings.setMotionType(EMotionType.Static)
anchorSettings.setObjectLayer(OBJ_LAYER_NON_MOVING)
val anchorBody = bodyInterface.createBody(anchorSettings)
bodyInterface.addBody(anchorBody, EActivation.DontActivate)
// Create the dynamic lantern body
val lanternShape = BoxShape(Vec3(0.2f, 0.3f, 0.2f))
val lanternSettings = BodyCreationSettings()
lanternSettings.setShape(lanternShape)
lanternSettings.setPosition(RVec3(lanternX, lanternY, lanternZ))
lanternSettings.setMotionType(EMotionType.Dynamic)
lanternSettings.setObjectLayer(OBJ_LAYER_MOVING)
val lanternBody = bodyInterface.createBody(lanternSettings)
bodyInterface.addBody(lanternBody, EActivation.Activate)
// Constrain the lantern to the anchor with a point constraint (pendulum)
val constraintSettings = PointConstraintSettings()
constraintSettings.setPoint1(RVec3(0.0, 0.0, 0.0))
constraintSettings.setPoint2(RVec3(0.0, (anchorY - lanternY), 0.0))
val constraint = constraintSettings.create(anchorBody, lanternBody)
physicsSystem.addConstraint(constraint)
return lanternBody
}
fun step() {
physicsSystem.update(1f / 20f, 1, tempAllocator, jobSystem)
}
fun getBodyPosition(body: Body): RVec3 {
return bodyInterface.getPosition(body)
}
fun getBodyRotation(body: Body): Quat {
return bodyInterface.getRotation(body)
}
fun setLinearVelocity(body: Body, vx: Float, vy: Float, vz: Float) {
bodyInterface.setLinearVelocity(body, Vec3(vx, vy, vz))
}
fun removeBody(body: Body) {
bodyInterface.removeBody(body)
}
fun destroy() {
physicsSystem.close()
}
What do these methods do?
createDynamicBox() spawns a 1-meter cube rigid body at world coordinates. It uses EActivation.Activate so the body immediately participates in simulation (gravity pulls it down).
createConstrainedLantern() builds a pendulum: a static anchor body at the ceiling and a dynamic lantern body below it, connected by a PointConstraint. The constraint removes translational freedom, allowing only rotation around the anchor point.
step() advances the simulation by 1/20th of a second (matching Minecraft's 20 TPS tick rate). The remaining methods query body transforms, apply velocities, and clean up.
- Save PhysicsWorld.kt.
- Verify that IntelliJ shows no red underlines in PhysicsWorld.kt. All Jolt types, enums, and method calls should resolve without errors.
Still seeing unresolved symbols?
Make sure you have both import lines at the top: com.github.stephengold.joltjni.* for the core types and com.github.stephengold.joltjni.enumerate.* for EActivation and EMotionType. If IntelliJ suggests auto-importing individual classes, accept those suggestions.
help me resolve Jolt-JNI type errors in PhysicsWorld.kt
✔️ Awesome, I've got everything!
Great. Double check you've saved PhysicsWorld.kt and that the file has no red underlines in IntelliJ.
ⓧ I'd like to double check the full code
package com.example.joltphysics
import com.github.stephengold.joltjni.*
import com.github.stephengold.joltjni.enumerate.EActivation
import com.github.stephengold.joltjni.enumerate.EMotionType
class PhysicsWorld {
companion object {
const val OBJ_LAYER_MOVING = 0
const val OBJ_LAYER_NON_MOVING = 1
const val NUM_OBJ_LAYERS = 2
const val NUM_BP_LAYERS = 1
}
lateinit var physicsSystem: PhysicsSystem
lateinit var tempAllocator: TempAllocator
lateinit var jobSystem: JobSystem
lateinit var bodyInterface: BodyInterface
fun initialize() {
JoltPhysicsObject.startCleaner()
Jolt.registerDefaultAllocator()
Jolt.installDefaultAssertCallback()
Jolt.installDefaultTraceCallback()
Jolt.newFactory()
Jolt.registerTypes()
tempAllocator = TempAllocatorMalloc()
val numWorkerThreads = Runtime.getRuntime().availableProcessors()
jobSystem = JobSystemThreadPool(Jolt.cMaxPhysicsJobs, Jolt.cMaxPhysicsBarriers, numWorkerThreads)
val ovoFilter = ObjectLayerPairFilterTable(NUM_OBJ_LAYERS)
ovoFilter.enableCollision(OBJ_LAYER_MOVING, OBJ_LAYER_MOVING)
ovoFilter.enableCollision(OBJ_LAYER_MOVING, OBJ_LAYER_NON_MOVING)
ovoFilter.disableCollision(OBJ_LAYER_NON_MOVING, OBJ_LAYER_NON_MOVING)
val layerMap = BroadPhaseLayerInterfaceTable(NUM_OBJ_LAYERS, NUM_BP_LAYERS)
layerMap.mapObjectToBroadPhaseLayer(OBJ_LAYER_MOVING, 0)
layerMap.mapObjectToBroadPhaseLayer(OBJ_LAYER_NON_MOVING, 0)
val ovbFilter = ObjectVsBroadPhaseLayerFilterTable(layerMap, NUM_BP_LAYERS, ovoFilter, NUM_OBJ_LAYERS)
physicsSystem = PhysicsSystem()
physicsSystem.init(5000, 0, 65536, 20480, layerMap, ovbFilter, ovoFilter)
bodyInterface = physicsSystem.getBodyInterface()
createGroundPlane()
}
private fun createGroundPlane() {
val groundShape = BoxShape(Vec3(100f, 0.5f, 100f))
val groundSettings = BodyCreationSettings()
groundSettings.setShape(groundShape)
groundSettings.setPosition(RVec3(0.0, -0.5, 0.0))
groundSettings.setMotionType(EMotionType.Static)
groundSettings.setObjectLayer(OBJ_LAYER_NON_MOVING)
val ground = bodyInterface.createBody(groundSettings)
bodyInterface.addBody(ground, EActivation.DontActivate)
}
fun createDynamicBox(x: Double, y: Double, z: Double): Body {
val boxShape = BoxShape(Vec3(0.5f, 0.5f, 0.5f))
val settings = BodyCreationSettings()
settings.setShape(boxShape)
settings.setPosition(RVec3(x, y, z))
settings.setMotionType(EMotionType.Dynamic)
settings.setObjectLayer(OBJ_LAYER_MOVING)
val body = bodyInterface.createBody(settings)
bodyInterface.addBody(body, EActivation.Activate)
return body
}
fun createConstrainedLantern(anchorX: Double, anchorY: Double, anchorZ: Double,
lanternX: Double, lanternY: Double, lanternZ: Double): Body {
val anchorShape = BoxShape(Vec3(0.1f, 0.1f, 0.1f))
val anchorSettings = BodyCreationSettings()
anchorSettings.setShape(anchorShape)
anchorSettings.setPosition(RVec3(anchorX, anchorY, anchorZ))
anchorSettings.setMotionType(EMotionType.Static)
anchorSettings.setObjectLayer(OBJ_LAYER_NON_MOVING)
val anchorBody = bodyInterface.createBody(anchorSettings)
bodyInterface.addBody(anchorBody, EActivation.DontActivate)
val lanternShape = BoxShape(Vec3(0.2f, 0.3f, 0.2f))
val lanternSettings = BodyCreationSettings()
lanternSettings.setShape(lanternShape)
lanternSettings.setPosition(RVec3(lanternX, lanternY, lanternZ))
lanternSettings.setMotionType(EMotionType.Dynamic)
lanternSettings.setObjectLayer(OBJ_LAYER_MOVING)
val lanternBody = bodyInterface.createBody(lanternSettings)
bodyInterface.addBody(lanternBody, EActivation.Activate)
val constraintSettings = PointConstraintSettings()
constraintSettings.setPoint1(RVec3(0.0, 0.0, 0.0))
constraintSettings.setPoint2(RVec3(0.0, (anchorY - lanternY), 0.0))
val constraint = constraintSettings.create(anchorBody, lanternBody)
physicsSystem.addConstraint(constraint)
return lanternBody
}
fun step() {
physicsSystem.update(1f / 20f, 1, tempAllocator, jobSystem)
}
fun getBodyPosition(body: Body): RVec3 {
return bodyInterface.getPosition(body)
}
fun getBodyRotation(body: Body): Quat {
return bodyInterface.getRotation(body)
}
fun setLinearVelocity(body: Body, vx: Float, vy: Float, vz: Float) {
bodyInterface.setLinearVelocity(body, Vec3(vx, vy, vz))
}
fun removeBody(body: Body) {
bodyInterface.removeBody(body)
}
fun destroy() {
physicsSystem.close()
}
}
Your physics world is ready to simulate. Next up, you'll write the command that shatters Minecraft blocks into dynamic rigid bodies that fall and bounce inside this world.
Shatter Blocks into Physics Rigid Bodies
Your physics world is running, but everything it simulates is invisible. Rigid bodies exist purely as mathematical constructs inside Jolt's memory. To see physics in action, each body needs a visual counterpart that tracks its position and rotation every tick.
In this step, you'll build two pieces that work together: a render loop that syncs physics transforms to ItemDisplay entities, and a command that shatters nearby blocks into physics-simulated pieces with random scatter velocities.
In this step, get ready to:
- Create a render loop that maps physics body transforms to ItemDisplay entities every tick.
- Build a shatter command that converts blocks into dynamic rigid bodies with matching visuals.
- Apply random explosion velocities so blocks scatter realistically.
Create the tick-synchronized render loop
Minecraft's server ticks 20 times per second. The render loop hooks into this tick cycle, steps the physics simulation forward, reads each body's position and rotation, and writes a transformation matrix to the corresponding display entity.
- Create a new Kotlin file at src/main/kotlin/com/example/joltphysics/RenderLoop.kt and paste the following code:
package com.example.joltphysics
import org.bukkit.Bukkit
import org.bukkit.entity.ItemDisplay
import org.bukkit.scheduler.BukkitTask
import org.joml.Matrix4f
import org.joml.Quaternionf
class RenderLoop(
private val plugin: JoltPhysicsPlugin,
private val physicsWorld: PhysicsWorld
) {
// Pairs a Jolt physics body with its visual ItemDisplay entity
data class PhysicsDisplayPair(
val body: com.github.stephengold.joltjni.Body,
val display: ItemDisplay
)
val trackedBodies = mutableListOf<PhysicsDisplayPair>()
private var task: BukkitTask? = null
fun start() {
// Run every 1 tick (50ms) to match the server's tick rate
task = Bukkit.getScheduler().runTaskTimer(plugin, Runnable {
tick()
}, 1L, 1L)
}
fun stop() {
task?.cancel()
trackedBodies.forEach { pair ->
pair.display.remove()
physicsWorld.removeBody(pair.body)
}
trackedBodies.clear()
}
private fun tick() {
// Step the physics simulation forward by 1/20th of a second
physicsWorld.step()
val iterator = trackedBodies.iterator()
while (iterator.hasNext()) {
val pair = iterator.next()
val pos = physicsWorld.getBodyPosition(pair.body)
val rot = physicsWorld.getBodyRotation(pair.body)
// Remove bodies that have fallen out of the world
if (pos.getY() < -10.0) {
pair.display.remove()
physicsWorld.removeBody(pair.body)
iterator.remove()
continue
}
// Build a transformation matrix from the physics body's state
val matrix = Matrix4f()
.translation(
(pos.getX() - pair.display.location.x).toFloat(),
(pos.getY() - pair.display.location.y).toFloat(),
(pos.getZ() - pair.display.location.z).toFloat()
)
.rotate(Quaternionf(rot.getX(), rot.getY(), rot.getZ(), rot.getW()))
// Apply the transform and enable client-side interpolation
pair.display.setTransformationMatrix(matrix)
pair.display.interpolationDelay = 0
pair.display.interpolationDuration = 1
}
}
}
What does this code do?
The render loop is the bridge between Jolt's physics simulation and what players actually see in-game.
- PhysicsDisplayPair ties a Jolt Body to its visual ItemDisplay counterpart.
- Every tick, physicsWorld.step() advances the simulation by 1/20th of a second (matching Minecraft's 20 TPS).
- The transformation matrix uses the offset between the body's current world position and the display's spawn location, because display entity transforms are relative to the entity's position.
- interpolationDuration = 1 tells the client to smoothly animate between tick updates over 50ms, giving visually smooth 60fps-like movement.
Why are transforms relative to the display's spawn location?
The matrix passed to setTransformationMatrix() is an offset from the entity's world position, not an absolute coordinate. By subtracting the display's spawn location from the body's current position, you get the correct relative offset that moves the visual to the right spot.
- Save RenderLoop.kt.
You won't see a change yet. The render loop needs physics bodies to track, which the ShatterCommand will provide next.
Build the shatter command
The shatter command is where physics meets gameplay. When a player runs /shatter, every solid block within a radius gets removed from the world, replaced by a dynamic rigid body in the physics simulation with a matching ItemDisplay entity as its visual representation.
- Create a new Kotlin file at src/main/kotlin/com/example/joltphysics/ShatterCommand.kt and paste the following code:
package com.example.joltphysics
import org.bukkit.Material
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
import org.bukkit.entity.ItemDisplay
import org.bukkit.entity.Player
import org.bukkit.inventory.ItemStack
import kotlin.random.Random
class ShatterCommand(private val plugin: JoltPhysicsPlugin) : CommandExecutor {
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>): Boolean {
if (sender !is Player) return false
// Parse radius from args, default to 3
val radius = args.firstOrNull()?.toIntOrNull() ?: 3
val center = sender.location
for (x in -radius..radius) {
for (y in -radius..radius) {
for (z in -radius..radius) {
val block = center.clone().add(x.toDouble(), y.toDouble(), z.toDouble()).block
// Skip air and non-solid blocks
if (block.type.isAir || !block.type.isSolid) continue
val material = block.type
// Use block center (add 0.5 to get center of the 1m cube)
val blockX = block.x + 0.5
val blockY = block.y + 0.5
val blockZ = block.z + 0.5
// Remove the block from the world
block.type = Material.AIR
// Create a dynamic physics body at the block's position
val body = plugin.physicsWorld.createDynamicBox(blockX, blockY, blockZ)
// Apply random explosion velocity for scatter effect
plugin.physicsWorld.setLinearVelocity(
body,
Random.nextFloat() * 4f - 2f,
Random.nextFloat() * 5f + 2f,
Random.nextFloat() * 4f - 2f
)
// Spawn an ItemDisplay to visually represent the physics body
val display = center.world.spawn(
org.bukkit.Location(center.world, blockX, blockY, blockZ),
ItemDisplay::class.java
) { entity ->
entity.setItemStack(ItemStack(material))
entity.itemDisplayTransform = ItemDisplay.ItemDisplayTransform.FIXED
entity.isPersistent = false
}
// Register the pair with the render loop
plugin.renderLoop.trackedBodies.add(
RenderLoop.PhysicsDisplayPair(body, display)
)
}
}
}
sender.sendMessage("Shattered blocks in radius $radius!")
return true
}
}
What does this command do?
- The triple-nested loop iterates a cube of blocks around the player with the given radius. Air and non-solid blocks are skipped.
- For each solid block, the command records its material, sets it to Material.AIR (removing it from the world), then creates a dynamic physics body at the block's center coordinates.
- Random velocity gives each piece a unique scatter trajectory. The X and Z range is -2 to +2, and Y is 2 to 7 (always upward) to simulate an upward explosion.
- Each ItemDisplay spawns with the original block's material as its item stack and ItemDisplayTransform.FIXED to render at full block scale.
- isPersistent = false ensures display entities are not saved to disk. They only exist during the active simulation.
- Save ShatterCommand.kt.
- In IntelliJ's Project panel, verify that both RenderLoop.kt and ShatterCommand.kt appear under src/main/kotlin/com/example/joltphysics/.
Why does IntelliJ show an error on SwingCommand?
JoltPhysicsPlugin.kt references SwingCommand which does not exist yet. This is expected. You will create it in the next step, and the project will compile cleanly then.
Seeing unresolved reference errors beyond SwingCommand?
- If ItemDisplay or Matrix4f are unresolved, make sure your build.gradle.kts has paperweight.paperDevBundle("26.1.2.build.+") and that you have synced the Gradle project.
- If RenderLoop.PhysicsDisplayPair is unresolved in ShatterCommand.kt, confirm that RenderLoop.kt is saved and has the correct package declaration: package com.example.joltphysics.
- Resync Gradle by clicking the elephant icon in IntelliJ's toolbar, or press Cmd+Shift+I (macOS) or Ctrl+Shift+O (Windows).
Help me debug unresolved references in my JoltPhysics plugin
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before moving on.
ⓧ I'd like to double check the full code
Here is the full RenderLoop.kt:
package com.example.joltphysics
import org.bukkit.Bukkit
import org.bukkit.entity.ItemDisplay
import org.bukkit.scheduler.BukkitTask
import org.joml.Matrix4f
import org.joml.Quaternionf
class RenderLoop(
private val plugin: JoltPhysicsPlugin,
private val physicsWorld: PhysicsWorld
) {
data class PhysicsDisplayPair(
val body: com.github.stephengold.joltjni.Body,
val display: ItemDisplay
)
val trackedBodies = mutableListOf<PhysicsDisplayPair>()
private var task: BukkitTask? = null
fun start() {
task = Bukkit.getScheduler().runTaskTimer(plugin, Runnable {
tick()
}, 1L, 1L)
}
fun stop() {
task?.cancel()
trackedBodies.forEach { pair ->
pair.display.remove()
physicsWorld.removeBody(pair.body)
}
trackedBodies.clear()
}
private fun tick() {
physicsWorld.step()
val iterator = trackedBodies.iterator()
while (iterator.hasNext()) {
val pair = iterator.next()
val pos = physicsWorld.getBodyPosition(pair.body)
val rot = physicsWorld.getBodyRotation(pair.body)
if (pos.getY() < -10.0) {
pair.display.remove()
physicsWorld.removeBody(pair.body)
iterator.remove()
continue
}
val matrix = Matrix4f()
.translation(
(pos.getX() - pair.display.location.x).toFloat(),
(pos.getY() - pair.display.location.y).toFloat(),
(pos.getZ() - pair.display.location.z).toFloat()
)
.rotate(Quaternionf(rot.getX(), rot.getY(), rot.getZ(), rot.getW()))
pair.display.setTransformationMatrix(matrix)
pair.display.interpolationDelay = 0
pair.display.interpolationDuration = 1
}
}
}
And here is the full ShatterCommand.kt:
package com.example.joltphysics
import org.bukkit.Material
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
import org.bukkit.entity.ItemDisplay
import org.bukkit.entity.Player
import org.bukkit.inventory.ItemStack
import kotlin.random.Random
class ShatterCommand(private val plugin: JoltPhysicsPlugin) : CommandExecutor {
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>): Boolean {
if (sender !is Player) return false
val radius = args.firstOrNull()?.toIntOrNull() ?: 3
val center = sender.location
for (x in -radius..radius) {
for (y in -radius..radius) {
for (z in -radius..radius) {
val block = center.clone().add(x.toDouble(), y.toDouble(), z.toDouble()).block
if (block.type.isAir || !block.type.isSolid) continue
val material = block.type
val blockX = block.x + 0.5
val blockY = block.y + 0.5
val blockZ = block.z + 0.5
block.type = Material.AIR
val body = plugin.physicsWorld.createDynamicBox(blockX, blockY, blockZ)
plugin.physicsWorld.setLinearVelocity(
body,
Random.nextFloat() * 4f - 2f,
Random.nextFloat() * 5f + 2f,
Random.nextFloat() * 4f - 2f
)
val display = center.world.spawn(
org.bukkit.Location(center.world, blockX, blockY, blockZ),
ItemDisplay::class.java
) { entity ->
entity.setItemStack(ItemStack(material))
entity.itemDisplayTransform = ItemDisplay.ItemDisplayTransform.FIXED
entity.isPersistent = false
}
plugin.renderLoop.trackedBodies.add(
RenderLoop.PhysicsDisplayPair(body, display)
)
}
}
}
sender.sendMessage("Shattered blocks in radius $radius!")
return true
}
}
Your block destruction system is ready to go. Once the project compiles fully, running /shatter 3 will rip blocks out of the world and send them tumbling through physics space. Next up, you'll add pendulum-swinging lanterns using constraints.
Implement Lantern Pendulum Physics with PointConstraint
Your shatter command proves that Jolt can handle dozens of free-falling rigid bodies rendered in real time. Now you need a different kind of motion: constrained rotation.
A swinging lantern does not fly freely through space. It stays tethered to a fixed point and arcs back and forth like a pendulum. In this step, you will build a /swing command that uses Jolt's PointConstraint to pin a dynamic body to a static anchor, creating realistic pendulum physics.
In this step, get ready to:
- Create a swing command that targets lantern blocks via raycasting.
- Understand how PointConstraint removes translational freedom to create pendulum motion.
- Spawn a constrained ItemDisplay entity with initial velocity to start swinging.
Create the swing command handler
The swing command needs to detect which block the player is looking at, verify it is a lantern, then replace it with a physics-driven display entity. Your PhysicsWorld class already has the createConstrainedLantern method that handles the rigid body and constraint setup. Now you need the command class that wires player interaction to that physics method.
- Create a new file SwingCommand.kt in src/main/kotlin/com/example/joltphysics/ and paste the following code:
package com.example.joltphysics
import org.bukkit.Material
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
import org.bukkit.entity.ItemDisplay
import org.bukkit.entity.Player
import org.bukkit.inventory.ItemStack
class SwingCommand(private val plugin: JoltPhysicsPlugin) : CommandExecutor {
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>): Boolean {
if (sender !is Player) return false
// Raycast 5 blocks from the player's eye position
val targetBlock = sender.getTargetBlockExact(5)
if (targetBlock == null || !targetBlock.type.name.contains("LANTERN")) {
sender.sendMessage("Look at a lantern block!")
return true
}
// Record lantern material and calculate center coordinates
val lanternMaterial = targetBlock.type
val lanternX = targetBlock.x + 0.5
val lanternY = targetBlock.y + 0.5
val lanternZ = targetBlock.z + 0.5
// Anchor point is one block above the lantern (ceiling attachment)
val anchorY = lanternY + 1.0
// Remove the original block from the world
targetBlock.type = Material.AIR
// Create constrained physics bodies (static anchor + dynamic lantern)
val lanternBody = plugin.physicsWorld.createConstrainedLantern(
lanternX, anchorY, lanternZ,
lanternX, lanternY, lanternZ
)
// Apply initial horizontal velocity to start the pendulum swing
plugin.physicsWorld.setLinearVelocity(lanternBody, 2f, 0f, 0f)
// Spawn ItemDisplay entity to visualize the physics body
val display = sender.world.spawn(
org.bukkit.Location(sender.world, lanternX, lanternY, lanternZ),
ItemDisplay::class.java
) { entity ->
entity.setItemStack(ItemStack(lanternMaterial))
entity.itemDisplayTransform = ItemDisplay.ItemDisplayTransform.FIXED
entity.isPersistent = false
}
// Register with the render loop so transforms update each tick
plugin.renderLoop.trackedBodies.add(
RenderLoop.PhysicsDisplayPair(lanternBody, display)
)
sender.sendMessage("Lantern is now swinging!")
return true
}
}
What does this code do?
sender.getTargetBlockExact(5) performs a raycast from the player's eye position up to 5 blocks away. This is how the command knows which block the player is looking at.
The targetBlock.type.name.contains("LANTERN") check matches both LANTERN and SOUL_LANTERN material types without needing separate checks for each variant.
The anchor is positioned one block above the lantern (anchorY = lanternY + 1.0) to simulate the ceiling attachment point. The createConstrainedLantern method you built earlier creates a static body at that anchor and a dynamic body at the lantern's position, then connects them with a PointConstraint.
- Save SwingCommand.kt.
- Verify the project compiles without errors by checking the build output in IntelliJ. The SwingCommand reference in JoltPhysicsPlugin.kt should no longer show as an unresolved symbol.
Still seeing unresolved reference errors?
Make sure SwingCommand.kt is in the same package directory as your other files: src/main/kotlin/com/example/joltphysics/. If the file is in the wrong directory, the package declaration will not match and IntelliJ cannot resolve the class.
Check that the package com.example.joltphysics line at the top matches exactly. Help me fix unresolved references in my SwingCommand class.
Understand how PointConstraint creates pendulum physics
The pendulum behavior comes from the createConstrainedLantern method you wrote in the previous step. It uses PointConstraintSettings to lock two bodies together at a shared point in space.
What is a PointConstraint?
A PointConstraint removes 3 degrees of freedom (all translation) from the relationship between two bodies. The dynamic body can still rotate freely around the constraint point, but it cannot move away from it. This creates a ball-and-socket or pendulum effect.
In your code, point1 is set to RVec3(0.0, 0.0, 0.0) (the center of the static anchor body) and point2 is set to RVec3(0.0, (anchorY - lanternY), 0.0) (one block above the lantern body's center, in its local space). This offset creates the rope length of the pendulum.
Why PointConstraint instead of HingeConstraint?
A HingeConstraint restricts rotation to a single axis (removing 5 degrees of freedom). That would work for a door or gate. A lantern can swing in any direction, so the 3-DOF removal from PointConstraint gives a more natural pendulum that arcs freely in 3D space.
Test the swing command in-game
The initial velocity of 2f, 0f, 0f pushes the lantern body sideways along the X axis. Gravity then pulls it back, creating oscillation. The existing render loop automatically picks up the new PhysicsDisplayPair and updates the display entity's transform every tick with client interpolation.
- Build the plugin by running the Gradle assemble task.
- Copy the output JAR to your Paper server's plugins folder and restart the server.
- Place a lantern block in the world (either LANTERN or SOUL_LANTERN).
- Look directly at the lantern from within 5 blocks and run /swing.
- You should see the lantern block disappear and be replaced by a swinging display entity that oscillates back and forth like a pendulum.
Lantern not swinging?
If you see the message Look at a lantern block! even when looking at a lantern, make sure your crosshair is directly on the lantern block. The raycast is precise and will miss if you are looking slightly above or below it.
If the lantern disappears but the display entity does not swing, confirm the render loop is running. Check the server console for the JoltPhysics plugin enabled message on startup. Help me debug why my lantern display entity is not moving after running /swing.
✔️ Awesome, I've got everything!
Great. Double check you have saved SwingCommand.kt and that your project compiles cleanly.
ⓧ I'd like to double check the full code
package com.example.joltphysics
import org.bukkit.Material
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
import org.bukkit.entity.ItemDisplay
import org.bukkit.entity.Player
import org.bukkit.inventory.ItemStack
class SwingCommand(private val plugin: JoltPhysicsPlugin) : CommandExecutor {
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>): Boolean {
if (sender !is Player) return false
val targetBlock = sender.getTargetBlockExact(5)
if (targetBlock == null || !targetBlock.type.name.contains("LANTERN")) {
sender.sendMessage("Look at a lantern block!")
return true
}
val lanternMaterial = targetBlock.type
val lanternX = targetBlock.x + 0.5
val lanternY = targetBlock.y + 0.5
val lanternZ = targetBlock.z + 0.5
val anchorY = lanternY + 1.0
targetBlock.type = Material.AIR
val lanternBody = plugin.physicsWorld.createConstrainedLantern(
lanternX, anchorY, lanternZ,
lanternX, lanternY, lanternZ
)
plugin.physicsWorld.setLinearVelocity(lanternBody, 2f, 0f, 0f)
val display = sender.world.spawn(
org.bukkit.Location(sender.world, lanternX, lanternY, lanternZ),
ItemDisplay::class.java
) { entity ->
entity.setItemStack(ItemStack(lanternMaterial))
entity.itemDisplayTransform = ItemDisplay.ItemDisplayTransform.FIXED
entity.isPersistent = false
}
plugin.renderLoop.trackedBodies.add(
RenderLoop.PhysicsDisplayPair(lanternBody, display)
)
sender.sendMessage("Lantern is now swinging!")
return true
}
}
Your plugin now handles two distinct physics scenarios: free-flying rigid bodies and constrained pendulum motion. Next up, you will build the tick-synchronized render loop that ties everything together with smooth client-side interpolation.
Secret mission
Player Punch Destruction
Your /shatter command works great for demolishing entire areas, but what about targeted destruction? In this secret mission, you'll wire up player left-clicks so that punching any solid block sends it flying with physics. The block launches in the direction you're facing, turning every surface into a breakable physics toy.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with no cloud resources or paid services, so there are no ongoing costs.
Resources you used:
- Your JoltPhysics Gradle project folder (all Kotlin source files and build configuration).
- The compiled plugin JAR deployed to your Paper server's plugins/ folder.
- Native library files extracted by snaploader to your user directory.
Keep everything running
No action needed. Choose this if you're still actively experimenting with your physics plugin or plan to add more features.
- Your Paper server continues running with the JoltPhysics plugin loaded.
- Your project files remain in IntelliJ IDEA ready for further development.
- The extracted native libraries remain on disk for fast plugin reloads.
Pause - I'll come back to this later
Shut down running processes to free up memory. Your project files stay on disk for when you return.
- Stop your Paper server by typing stop in the server console or pressing Ctrl+C in the terminal running it.
- Close IntelliJ IDEA if you want to free up additional memory.
- Your project files and compiled JAR remain intact for next time.
Delete - I don't want to use this again
Remove all project resources and start fresh. This deletes your plugin source code and compiled output permanently.
- Stop your Paper server if it is still running.
- Delete the compiled plugin JAR from your Paper server's plugins/ folder. The file is named JoltPhysics-1.0-SNAPSHOT.jar (or similar depending on your build output).
- Delete your JoltPhysics project folder. In your terminal, navigate to the directory containing the project and run:
macOS
rm -rf JoltPhysics
Windows
rmdir /s /q JoltPhysics
- Remove the extracted native libraries. Snaploader extracts them to your user directory. Delete the joltjni folder or .dll/.so/.dylib files from your user home directory.
Where are the extracted native files?
Snaploader extracts native libraries to your user directory (the path specified by DirectoryPath.USER_DIR in the code). Look for files containing joltjni in your home folder.
That's a Wrap!
That's a Wrap!
Nice work! You've just built a Kotlin Paper plugin that integrates a native C++ physics engine into a Minecraft server, bridging the gap between real-time rigid body simulation and client-side rendering.
You've learned how to:
- Integrated jolt-jni into a Kotlin Paper plugin using snaploader for cross-platform native library loading via JNI.
- Built a physics world with collision layers, dynamic rigid bodies, and PointConstraint-based pendulum physics for lanterns.
- Created a tick-synchronized render loop that maps physics state to ItemDisplay transformation matrices with client-side interpolation for smooth visuals.
- Secret Mission: Added punch-to-shatter interaction using the player's eye direction as an impulse vector for directional physics destruction.
Ready to quiz yourself?