Shatterbox | Client-Server Voxel destruction, simple and optimized [Release 7]

Current Version: Release 7

Shatterbox Client-Server Voxel Destruction

Discord | Creator Store | GitHub | Test Experience


External Resources

BlinkLogo 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.

_Cover6


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.

_EncapsulationCheck


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):

_AllPartTypes


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.

_ResetDirties


There is configurable smooth cleanup for destructions (you can turn it off with a setting)

SmoothCleanup


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”:

JJSM1Replica


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
  1. Import Shatterbox/Shatterbox.blink at the top of your Blink config file:
import "PATH_TO_SHATTERBOX/Shatterbox.blink"

-- the rest of your Blink config
  1. Rebuild your Blink files.

  2. At the top of the Shatterbox.Main module, edit the following lines to contain the path to your Server and Client 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), the dirtyParent does not replicate. So you will have to use dirtyParent.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.1
Hitbox:Destroy()
Will queue a cut around the hitbox
Hitbox: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 destruction
Hitbox: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
RoundrobinPriority

_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 of Shatterbox

  • 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 contains Client and Server modules for you to have a place to define your destruction effects (rather than using Shatterbox.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
BeforeAfter

QueueShatter

ReverseQueueOrdering

  • 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 and Destroy are mutually exclusive. Currently, you can only use one or the other for proper behavior. If you use Destroy at all, you cannot rely on ImaginaryVoxels being correct until you use Shatterbox.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

_BigDiv

65 Likes

Does this work on wedges? Support for all part shapes would be amazing

3 Likes

For part intersections, any shape is supported as the part of intersection. This is because GetPartsInPart is used for collision checking. It should even work with custom meshes if they have the proper CollisionFidelity. In effect, you can make whatever destruction shape you desire.

For part divisions, the algorithm will work the same for any part type. The box type is the only one that is gonna look good as of now. I have been considering how I would implement divisions with wedge parts to smooth the edges, like this:

But this is not yet supported

3 Likes

Bump for Release 3. Thread contains new information and examples. Shatterbox has had its internals completely re-worked. This resulted in an optimization breakthrough (encapsulation checking):

_EncapsulationCheck

6 Likes

This is absolute Peak bro, Respect!!!

2 Likes

hi could u make a rbxl cuz like idk how to use it i tried,

1 Like

that cool ty
does it suffer with big parts?

1 Like

things if u did its will be so perfect :
make a cleanup method that clean destroyed part
make it 2 styles all part or just the part that just got destroyed by the module not all of them
and make a way for debris.

for me ill try to figure out the debris

1 Like

The Reset function cleans the map, but all operations have a 60 second smooth cleanup duration. This is configurable as the 5th parameter of the destruction functions.

To answer your question about performance, it’s a little complicated. When using encapsulation checking (fast destructions), I only have to worry about divisions along the boundary of the volume, instead of every single division contained within the volume. Consider the difference between the volume of a sphere and its surface area, that is the difference between MeshPart destructions and Part destructions.

TL:DR

Fast Destruction (only with Parts): Scales amazing with part size, you might see your performance increase as it gets bigger. See the first gif in examples where I delete a huge map using a sphere at max size

All other destruction: Scales horribly with part size, because each part must be subdivided all the way down into its constituent voxels

just a suggestion make a way so we take the part have been destroyed so
i can make a debris from it like :

local Debris = game:GetService("Debris")

local function getDestroyedPart(part, chanceForDebris)
	if not part or not part:IsA("BasePart") then return end

	
	if Random.new():NextNumber(0, 1) > (chanceForDebris / 100) then
		part:Destroy()
		return
	end

	
	part.Size -= Vector3.new(0.2, 0.2, 0.2)
	part.Anchored = false
	part.CanCollide = true
	part.CanTouch = false
	part.Position += Vector3.new(0, 0.5, 0)

	


	Debris:AddItem(part, 13.5)
 
end

thats just a example from what i mean u could make it better or a function

1 Like

Yes, that feature is coming in release 4! It’s almost done, sorry for the delay :sweat_smile:

Edit: I just posted release 4 in the Creator Store. Give me time to update the thread, although you should be able to use it without any issues.

This is what a setup looks like in Release 4:

local Shatterbox = require(game:GetService("ServerScriptService").Shatterbox)

local b = workspace.Ball


local function OnVoxelDestruct(Part : Part)

	local GridSize = Part:GetAttribute("GridSize") or 1

	if math.random() > 0.001 * GridSize * GridSize then Part:Destroy() return end

	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


local overlapParams = OverlapParams.new()
overlapParams.FilterType = Enum.RaycastFilterType.Include
overlapParams.FilterDescendantsInstances = { workspace.Map }



Shatterbox.FastDestroy(b, overlapParams, 1, OnVoxelDestruct)

1 Like

kk ill see and give u feedback!!

1 Like

This is the best voxel destruction system I’ve ever seen, It was even better after the update. I’m definitely i am adding this into my projects, so keep up the good work, dude.

1 Like


so when i tested before i didnt have time tbh and didnt test debris this is a bug i got
its keep making debris and its like this

1 Like

After the fourth release comes out, can you please post it along with a test place?

1 Like

4th release is out, I will work on a test place ASAP

1 Like

Hey, thank you for letting me know about this. I just pushed a fix for it. It was a rather silly bug; I wasn’t readjusting the parts correctly. Sorry about that.

1 Like

ty for fixing it ill try it now!!


this is so cool right now!!
i really appreciate ur work

2 Likes

How did you create moving hitboxes? Can you place those into the test place?

1 Like