Current Version: Release 7
Shatterbox Client-Server Voxel Destruction
External Resources
is used for client-server communication.
VoxBreaker inspired many features as well as the current syntax of the module.
Hello, I am here presenting a library designed for use in games that require voxel destruction on static maps (similar to Jujutsu Shenanigans). It is made to be scalable with performance and maintenance. It is also designed to be very easy to use. If you're looking for a fast destruction library that won't make your buildings flash (with StreamingEnabled), you found it!
In terms of performance, I would say that my system rivals JJS. It might even out-perform it.
Here is the test place file if you just want to jump into Studio and see it in action:
ShatterboxTestPlace.rbxl (163.7 KB)
I have provided 2 “maps” to showcase building with the GridSize in mind for quicker destructions, as well as showcasing “Chunking” which is an optimization strategy I will mention in this thread. The city map you see in this thread is included there. A big thank you to the builder denferrt for giving me the detailed towers in the test place, they look great!
There are 5 different tools that showcase registering effects, waiting for them, and using existing effects.
Examples
Encapsulation checks are used to make large operations insanely fast. This optimization doesn’t work with MeshParts as the hitbox.
Any BasePart shape is supported when using Destroy()
. MeshParts will always be slower than normal parts because I cannot use encapsulation checks on them.
In a Part operation, the time to completion is only based on the number of divisions required along the edge of the intersection (Encapsulation checks are used even when SkipEncapsulatedVoxels = false
)
(notice the player is 1 pixel large in this gif):
The Reset function will reset all modified objects to the state they were in before subdividing. Use this before you change maps, to properly reset your map to the state it was in before Shatterbox touched it.
There is configurable smooth cleanup for destructions (you can turn it off with a setting)
There are 2 built-in destruction effects, one which is the first gif shown, and the second is a re-creation of JJS M1 destructions. It’s called “BumpyFloorBreakWalls”:
How to use
Require the “Shatterbox.Main” module, this will initialize the Heartbeat
connection that Shatterbox listens to (and also gives you access to the module functions).
The module is designed for server-side use, so make sure you require it on the server side on initialization (even if you only want to perform client-only effects with the Voxelize()
function)
Blink users
- Import
Shatterbox/Shatterbox.blink
at the top of your Blink config file:
import "PATH_TO_SHATTERBOX/Shatterbox.blink"
-- the rest of your Blink config
-
Rebuild your Blink files.
-
At the top of the
Shatterbox.Main
module, edit the following lines to contain the path to yourServer
andClient
files:
local PathToClient = PATH_TO_CLIENT:WaitForChild("Client")
local PathToServer = PATH_TO_SERVER:WaitForChild("Server")
These steps are important to ensure your Blink events and mine will batch together properly.
Getting started tutorial
Insert a Box Part into the workspace, as well as another part you wish to use that will “cut into” the box. Anchor them both.
Then run this code:
Shatterbox.Destroy(cuttingPart)
This will perform voxel destruction around the cuttingPart, for every Block Part that it touches (Which isn’t a descendant of a character).
You can further specify objects which you want destroyed by using OverlapParams. I would suggest sticking to tags if you are going to use OverlapParams for your game, because Shatterbox messes with the heirarchy of the map (which you can revert using Reset()
function)
local overlapParams = OverlapParams.new()
overlapParams.FilterType = Enum.RaycastFilterType.Include
overlapParams.FilterDescendantInstances = { workspace.Map }
Shatterbox.Destroy(cuttingPart, overlapParams)
If you want to simulate debris, the destruction functions also support a callback event for when voxels are destroyed:
local function OnVoxelDestruct(Part : Part)
local GridSize = Part:GetAttribute("GridSize") or 1
if math.random() > 0.001 * GridSize * GridSize then Part:Destroy() return end
-- On the server-side, voxels are transparent so this will be needed
Part.Transparency = Part:GetAttribute("OriginalTransparency") or 0
Part.Anchored = false
Part.CanCollide = false
Part.AssemblyLinearVelocity = (Vector3.new(math.random(), math.random(), math.random()) - Vector3.one * 0.5) * 80
Part.AssemblyAngularVelocity = (Vector3.new(math.random(), math.random(), math.random()) - Vector3.one * 0.5) * 20
end
Shatterbox.RegisterOnVoxelDestruct("SomeName", OnVoxelDestruct)
Shatterbox.Destroy({ -- alternate table syntax is more user-friendly
CuttingPart = p,
OnVoxelDestruct = "SomeName"
})
This registers a side-only callback for whatever side you call it from. There can only be one callback with a given name, so there is a provided function for you to wait for a callback to exist:
Shatterbox.WaitForRegistry("SomeName")
Registering effects can be more user-friendly by using the Shatterbox/effects/
modules, and it is where you should register client-side destruction effects. Otherwise, make sure you are properly using WaitForRegistry
dirtyParent
can be used to make destructions cleaned on smooth cleanup.
If you are using server-side debris (and NOT
Shatterbox.Puppeteer
), thedirtyParent
does not replicate. So you will have to usedirtyParent.Destroying:Once()
to make the voxel get deleted on smooth cleanup.
Any time after a division has happened, it is important that these models do not move anymore (for proper resetting with smooth cleanup and the Reset
function).
Hitbox tutorial
The CreateHitbox()
function returns an object very similar to a VoxBreaker hitbox. You can set its Shape, CFrame, Size, and all other properties relevant to the destruction functions (though the constructor is empty, this may change in the future to be more similar to VoxBreaker if people request it).
Hitboxes also have built-in functions to repeatedly destroy, which acts as a moving hitbox (see the first gif in the thread).
Here is an explanation of the functions provided by a Hitbox:
Hitbox:WeldTo(BasePart)
Will "weld" the hitbox to the given part, continously setting its CFrame to be equal to the parts CFrame.Hitbox:Start()
Will continuously cut around the hitbox according to the DestructDelay, which defaults to 0.1Hitbox:Destroy()
Will queue a cut around the hitboxHitbox:ImaginaryVoxels()
Will instantly cut around the hitbox, returning the uncreated voxels (for you to optionally create)Hitbox:Unweld()
Will "unweld" the Hitbox.Hitbox:Stop()
Will stop continuous destructionHitbox:DestroyHitbox()
Will disconnect all connections.DestructParameters tutorial
DestructParameters is a nice tool that lets you further customize your destructions. It lets you pass additional arguments to the destruction functions, past the 3 that are provided.
First register an OnVoxelDestruct callback with more parameters, like this:
Shatterbox.RegisterOnVoxelDestruct("SomeName",
function(voxel: Part, dirtyParent: Model, cuttingPart: BasePart, someString : string)
print(someString)
voxel:Destroy()
end)
Then, when you go to destroy, supply the DestructParameters starting at index 1 for the new parameter you just added:
local hitbox = Shatterbox.CreateHitbox()
hitbox.OnVoxelDestruct = "SomeName"
hitbox.DestructParameters[1] = "Hello World!"
hitbox:Destroy() -- prints "Hello World!" for every destroyed voxel
IMPORTANT If you are using client-side effects AND DestructParameters, make sure all of your parameters can be parsed using Blink
Functions Explained
The DestructParamsType is a table with autocomplete (so you don’t have to spam nil
for un-needed parameters). Parameter-by-Parameter syntax is also supported.
Destroy(DestructParamsType)
Performs voxel destruction according to the parameters you supply. OnVoxelDestruct should be a string naming the destruction effect you want to apply.ImaginaryVoxels(DestructParamsType)
Instantly performs "imaginary" voxel destruction according to the parameters you supply. Returns an array of imaginary voxels for you to optionally create. Currently incompatible with `Destroy()`, so if you want to use this, it's one or the other.CreateHitbox()
Creates a "Hitbox" object which can be used in place of the destruction functions and has additional functionality, similar to the VoxBreaker moveable hitbox. See the Hitbox tutorial for more information.Puppeteer(voxel)
A server-side only function which makes `voxel` no longer replicate to clients. The client receives an anchored clone which gets slower CFrame replications from the server. Clients can optionally tween between these replications. The puppeteering stops when the voxel is destroyed.Example usage of Shatterbox.Puppeteer
:
-- this is an example effect where debris is controlled by the server, anchored on the client side, and replicated at a slower rate while the client tweens it (like how JJS does it)
-- the puppet is destroyed when the voxel is destroyed, so setting the parent to dirtyParent makes it get cleaned on smooth cleanup
Shatterbox.RegisterOnVoxelDestruct("PuppetDebris", function(voxel, dirtyParent)
voxel.Anchored = false
Shatterbox.Puppeteer(voxel)
voxel.Parent = dirtyParent
end)
Reset()
Will revert all modified objects to the state they were in when they were first subdivided.ClearQueue()
Will stop all ongoing operations, use this if you deliberately clutter the queue for whatever reason.RegisterOnVoxelDestruct(name, func)
Will register the callback to the given name, only for the side RegisterOnVoxelDestruct was called from.WaitForRegistry(name)
Will wait for the give name to exist in the OnVoxelDestruct registry.Changing grid gize
If you give a Block Part a number attribute “GridSize”, that will be used instead of the default GridSize. If you pass GridSize to Destroy/ImaginaryVoxels it will override both of those.
Shatterbox Settings
The Shatterbox module contains 11 settings:
-
Whether or not to use smooth cleanup
-
The maximum number of divisions that can be processed per frame. This is no longer soft-capped, so
math.huge
would likely be bad for performance lol -
The default smooth cleanup delay
-
The default GridSize
-
Whether to use round-robin processing or priority processing
Queue Processing Order Comparison
Roundrobin | Priority |
---|---|
|
|
-
If priority queue is true, how many of the most recent operations should be prioritized.
-
How often “puppet” voxels are replicated to clients
-
Whether or not puppet voxels should be tweened (client-side setting)
-
How many puppets can be created and sent to every client per Heartbeat
-
How slow puppets should move before their CFrames are no longer sent to clients
-
A function for you to specify behavior for instances that are trying to be considered for destruction. Should return true if it was skipped and return false if it was not.
You can change these settings at runtime if you need to, for whatever reason.
Quality of Life
-
You should really use the Hitbox syntax, it’s very similar to VoxBreaker hitbox and will generally be more user-friendly than using the functions themselves.
-
The cutting part doesn’t actually have to be parented to anything, not even the workspace.
-
You can pass a model into Destroy and ImaginaryVoxels, this will queue an operation for all BasePart descendants of the model. Beware that using a Model can be slow and look weird as each part needs to be processed separately.
How does it work?
Part divisions use a modified form of octree subdividing, where parts can either divide into 8, 4, or just 2 pieces. Shatterbox decides when it is best to use KD subdivisioning instead (CutPartInHalf like VoxBreaker).
If an overlapping Block is fully inside of the intersecting part, it either gets deleted or voxelized using Voxelize()
(according to SkipEncapsulatedVoxels). Otherwise, it gets divided. If a division is attempted but it’s at the GridSize, it gets voxel destroyed. That’s the entire algorithm.
The client-server algorithm just tells clients to copy the map before the server breaks it. When the client is done copying, it instantly starts its own destruction. When all clients are done copying (or timed out), the server starts destroying the map. The server-owned parts are not replicated to clients (unless a new player joins, that player gets fresh parts).
Client and server destructions/puppets are synced using a GUID.
The puppeteering system was gracefully explained below by @Impermanently
Optimization tips
-
The key to efficient destructions is to use GridSize according to part sizes. Large parts should have a larger GridSize. This keeps the number of divisions required for the average operation consistent
-
Consider “chunking” up large parts like the ground. This saves operation steps (only 1 operation step happens per frame) and allows encapsulation checks to work their magic faster.
Changelog
Release 7
Release Date: [date=2025-07-07 timezone=“America/Detroit”]
-
Shatterbox is now a package, so you must require
Shatterbox.Main
instead ofShatterbox
-
There are like 4 new settings, you can see “Shatterbox Settings” under “How to use” if you want to see what they do
-
Shatterbox.Puppeteer
is a new function to make server-controlled debris, that is anchored on client sides. -
Part ghosting is fixed completely (that I am aware of)
-
Server-side hitbox handling is better and more performant. Server-side view of destructions is now possible.
-
Shatterbox.effects
is a path that containsClient
andServer
modules for you to have a place to define your destruction effects (rather than usingShatterbox.RegisterOnVoxelDestruct
). They also contain the default effects.
Release 6
Release Date: [date=2025-06-18 timezone=“America/Detroit”]
-
You must now register OnVoxelDestruct callbacks, and then use their name when calling the destruction functions (instead of passing in the callback).
-
Hitbox objects that are “welded” to a BasePart will now be dynamically disconnected when that BasePart is destroyed using “:Destroy()”
-
There is a setting to give the server a head-start on reloading the map. This prevents flashing on smooth cleanups.
-
There is now a setting to change the behavior of priority queue to prioritize the most recent N operations, instead of the most recent.
-
Priority queue is now the default queue type
-
Patched the bug where smooth cleanup can destroy the map and cause infinite destruction loops
Release 5
Release Date: [date=2025-06-14 timezone=“America/Detroit”]
- Smooth cleanup is added as a 5th parameter to the destruction functions (and a property of Hitbox objects)
There is also a new setting to change the default smooth cleanup time if one is not provided.
-
There is now a CreateHitbox() function, very similar to VoxBreaker but is more user-friendly. All functions have commented descriptions you should be able to see with an IDE
-
I have made it so that parts no longer have to adhere to the grid, and will just be destroyed when they can’t possibly divide.
No more Part readjusting! That was a weird feature to begin with.
-
Patched a bug which made it insanely easy to clutter the queue
-
Patched being able to destroy debris
Release 4
Release Date: [date=2025-06-13 timezone=“America/Detroit”]
-
Shatterbox is now modular, so you just require the module and use its functions now. They are descriptive
-
There is an optional callback parameter when using either FastDestroy or BruteDestroy, so you can make debris.
-
BruteDestroy is a new function that will divide the intersection all the way without encapsulation checks.
-
Shatterbox no longer uses tagging to dictate destructables. You have to use OverlapParams now.
-
There is a setting to cap divisions per frame, so insanely large division counts don’t lag the game.
-
There is a setting to change between KD and Octree divisioning
-
There is a setting to change the default voxel destruction behavior
Release 3
Release Date: [date=2025-06-09 timezone=“America/Detroit”]
Work in progress changelog, It’s really long for this release.
Release 2
Release Date: [date=2025-06-03 timezone=“America/Detroit”]
- Queue ordering for part processing has been reversed, so now most recent additions are processed first.
Queue Order Comparison
Before | After |
---|---|
|
|
- There is now a timeout for processing parts before a new shatter operation takes precedence. This timeout is configurable.
This makes it so that operations that take a long ass time don’t take priority over newly added operations
- Axis of division is now determined by the longest size axis of the part, a huge improvement compared to the BS I was doing before.
This change should reduce the number of parts required for the average shatter operation.
- I have modified the subdivision math to no longer require a 2-parameter call to VectorToWorldSpace every division
Additionally, the math now uses much less memory, resulting in fewer lag spikes.
Known issues
-
ImaginaryVoxels
andDestroy
are mutually exclusive. Currently, you can only use one or the other for proper behavior. If you useDestroy
at all, you cannot rely onImaginaryVoxels
being correct until you useShatterbox.Reset
-
Large quantities of puppet debris cause lag even when they are stationary. I already have checks for non-moving debris, so I don’t know why.
Planned features
-
Optionally, a more smooth “Smooth cleanup” algorithm which will dynamically remove operations, while preserving present ones.
-
Optional usage of the “ObjectCache” module.
-
Optional dynamic welding, for anyone who wants falling buildings instead of levitating ones.
-
Removal of the “static map” restriction. In the future, I plan to make Shatterbox a system akin to the “Clone Drone in the Danger Zone” so that resetting of moved divisions is possible.
-
Union “slice” style destructions, which are not voxel destruction, but I think are cool.
-
Some sort of EditableMesh destructions, perhaps with Marching Cubes?
-
A way to disable the client-server functionality (as well as making ImaginaryVoxels compatible with client-server communication)
Current development sneak peaks
There are none sorry! I will update this when I have something cool lol
I know it’s not perfect, and I am all ears for any possible optimizations. I am actively developing this and will update the thread if any information changes accordingly. I hope you guys like it!
Join the discord server if you need assistance: Shatterbox Hub