[MOVED] VoxelDestruct | Voxelated destruction physics with greedy meshing, hitboxes, and more!

This topic has been moved to this thread.

Ever wanted customizable, optimal voxel destruction? Try VoxelDestruct!

| Download Model |

VoxelDestruct is a voxelating destruction tool created for developers to add destruction to their games in the most performance friendly way using greedy meshing, part caching, a splitting algorithm, and queueing.

Features

  • Single destruction & OOP hitbox destruction

  • Greedy meshing

  • Function queueing

  • Voxelization

  • Destruction states with preset animations and materials support, (Cracked, shatter, protrude)

| Brief Showcase |

Credit is not required from me but you should always consider giving credit to these contributors out of respect.

This tool wouldn’t have been possible without the Splitting Algorithm from @EgoMoose , Non-Voxel Greedy Mesher from @RoyalTHEUSERNAME , PartCache from @Xan_TheDragon , and inspiration from VoxBreaker made by @Bartokens

A huge thank you to these developers for their contributions!

This is my first community resource so be patient with me about missing information. I consider all suggestions and questions about usage, please don’t hesitate to @ me with any suggestions, questions, or concerns you may have!

Suggestions & Future Updates?

  • Support for wedge parts

  • Chunk generator

  • Welding touching parts, converting disconnected fragments into debris

  • Debris animation presets

  • Destruction states with preset animations and materials support, (Cracked, shatter, protrude)

  • Part durability and collision damage

Methodology & Concepts

(This section is optional but I highly recommend you read it anyways to get a better understanding of the controls)

Before introducing the methods and settings we need to understand what concepts are applied to the parameters and settings at your disposal.

Here is the approach this module uses:

  1. Get a bounding box oriented with your wall but surrounding your hitbox region/part, it should engulf the part while having a rotation of your wall.

  2. Slice the wall using all sides of the bounding box that are within your wall, each slice will give you the part that you will then use the next slice on as well as an extra part that will remain as a part of your wall.

  3. You will be left with a part representing the intersection of your bounding box with the wall and at most six extra pieces(one for every face of a cuboid) that belong to your wall. Take your intersection piece and voxelize it.

  4. Resize your region to get the voxels you would like to turn into debris, get the voxels touching this resized region and make them debris.

  5. Greedy mesh everything except for your debris.

  6. For another hole repeat the first five steps on the wall pieces gotten from step 2. This entire process should be wrapped in a queue so that destruction does not occur simultaneously on a single part or issue would occur.

Region/Hitbox part (green)
Bounding box (black)
Wall parts (orange / blue / pink / yellow)
Intersection part (white)
Voxels (red)
Debris region (purple)
image

Now to go over concepts-

Our first concept is a simple one, Relativity. Relativity is optional and can be applied to voxel size and debris size to avoid having extreme detail for large hitboxes (as too much detail would cause lag due to high part count). Imagine having a large hitbox with very small voxels, this would lead to a high part count even with greedy meshing. Relativity prevents this issue by resizing your voxels based off of your region’s size.

Relativity is simple to understand but things get complicated when we consider that your hitbox has a distance parameter which specifies how far the hitbox must travel before it destroys again, and that is to prevent your hitbox from firing constantly.

So you may be asking, why shouldn’t a hitbox destroy everything at every position? And the reason for that is you will have insignificant detail which is caught by greedy meshing but slows down the meshing process causing flickering in the visibility of your wall.

image

image
(travel distance 0)

Specifying a distance works to combat this but we still wind up with an unnecessary amount of parts.

image
(travel distance 1)

We can take it even further and apply relativity to it by having the distance parameter be nil, this is meant to optimize large hitboxes. If your distance parameter is nil then your voxelSize parameter will be used instead. In the case that you pass voxelSize as nil your hitbox will not return debris but will return the entire intersection of your region and the wall, in this special scenario your voxel size cannot be used so relativity is applied using the hitboxRelative setting.

image
(Distance of nil, relative voxel size)

By passing distance as nil and voxelSize as 0 we have applied relativity to our hitbox’s distance as well as our voxelSize.

That’s not confusing right? >_> In case you didn’t understand…
Here is the order for what is used as your hitbox’s distance limitation IF the previous value was nil:
distance parameter > voxelSize parameter > hitboxRelative setting

Making Destructible Parts

To make a destructible part you need to add a boolean attribute to your part named after your attribute setting, by default the name is “Breakable”.

When your destructible part is first destroyed you will get 4 extra attributes that you can edit with your scripts or change through parameters. These extra attributes are named after your attribute setting and are necessary for cross-collision interaction.

image

The BreakableDestroy attribute decides whether or not the wall will be deleted after the timer runs out. This attribute can be updated with the destroy parameter.

The BreakableFreeze attribute set to true will yield the timer until it is set back to false. This attribute can be updated with the freeze parameter.

The BreakableLocked attribute set to true will prevent the wall from being broken again. This attribute can be updated with the lock parameter.

The BreakableTimer attribute is the countdown timer until the wall either repairs itself or destroyed if the BreakableDestroy attribute is true. This attribute can be updated with the reset parameter.

Methods

Require the module to begin using methods. You can get the stored part cache with vox.Cache

local vox = require(game:GetService("ReplicatedStorage"):WaitForChild("VoxelDestruction"))
local cache = vox.Cache -- If you ever need it for checking if parts belong to this module? 
.destroy()
function breaker.destroy(
cframe: CFrame, 
size: Vector3, 
shape: Enum.PartType?,  
parameters: OverlapParams?,
voxelSize: number?, 
debrisSize: number?, 
debrisCount: number?, 
cutoutSize: number?, 
gridLock: boolean?,
reset: number?,
freeze: boolean?,
destroy: boolean?,
lock: boolean?
)

Used for singular destruction.

  • Returns a table of debris and a table of affected walls.
  • The cframe and size parameters are required but everything else is optional with defaults, some defaults are applied through settings.
cframe: CFrame

The CFrame of your region.

size: Vector3

The Vector3 size of your region.

shape: Enum.PartType?

The shape of your region.

  • If passed as nill will default to shapeDefault setting
parameters: OverlapParams?

The parameters for whitelisting or blacklisting destructible parts.
Example:

local params = OverlapParams.new()
params.FilterType = Enum.RaycastFilterType.Include -- .Exclude for BlackList
params.FilterDescendantsInstances = {game.Workspace.Baseplate}
voxelSize: number?

The detail-providing voxel size of your destruction.

  • If passed as nil your intersections are not voxelized and are returned instead of voxels
  • If passed as 0 and useRelativity setting is true then relativity is applied
  • If passed as 0 and useRelativity setting is false then voxelDefault setting is applied
debrisSize: number?

The size of your debris.

  • If passed as nil your debris size is inherited from the voxelSize parameter
  • If passed as 0 and useRelativity setting is true then relativity is applied
  • If passed as 0 and useRelativity setting is false then debrisDefault setting is applied
debrisCount: number?

The quantity of debris produced.

  • If passed as nil all the debris will be returned
cutoutSize: number?

The size of the region getting debris relative to the original size of your region. This is to provide more detail for non-cuboid shaped regions, a promising value seems to be 3/4ths.

  • If passed as nil will default to cutoutDefault setting
  • Should be a float - a number between 0 and 1
  • *Check the diagram in *Methodology & Concepts
gridLock: boolean?

Determines whether or not parameters will be rounded to the nearest unit intended to produce a uniform unit detail to your destructions.

  • If passed as nil will default to gridLockDefault setting
  • Will round the cframe parameter, size parameter, voxelSize parameter, and debrisSize parameter to the nearest unit
reset: number?

The timer until a destructible part is cleaned up, reverted, or destroyed if your BreakableDestroy attribute is true.

  • If passed as nil the destroy parameter will not update the current BreakableTimer attribute
freeze: boolean?

Whether or not the current timer will yield and resume when the attribute BreakableFreeze becomes true.

  • If passed as nil the freeze parameter will not update the current BreakableFreeze attribute
destroy: boolean?

Whether or not the destructible part will be destroyed or cleaned up when the BreakableTimer attribute becomes 0.

  • If passed as nil the destroy parameter will not update the current BreakableDestroy attribute
lock: boolean?

Whether or not the destructible part is locked from further destruction until the BreakableLocked attribute becomes false.

  • If passed as nil the lock parameter will not update the current BreakableLocked attribute
  • Intended to be used alongside the destroy parameter to destructible parts cannot be updated before being removed
.hitbox()

Used to create hitboxes from parts or models and allows runtime editting.

  • Models for hitboxes must have a primary part set or one will be randomly assigned, changes in any of the part’s cframe, size, or shape will fire the hitbox
  • Models for hitboxes are not limited to being flat models - models which only consist of parts and have no difference in hierarchy (no nested parts within parts). They can have nested parts and any changes to any descendant’s regions will fire the hitbox.
function breaker.hitbox(
focus: Instance, 
limit: number?, 
auto: boolean?,  
revert: boolean?,
distance: number?,
parameters: OverlapParams?,
voxelSize: number?, 
debrisSize: number?, 
debrisCount: number?, 
cutoutSize: number?, 
gridLock: boolean?,
reset: number?,
freeze: boolean?,
destroy: boolean?,
lock: boolean?
)
Controls
hitbox:Start()

Starts your hitbox.

  • Your hitbox can be restarted by calling :Start() while it is active without needing to call :Stop() first. It will stop itself then start again.
  • Will fire the hitbox.Started:Connect() event
hitbox:Stop()

Stops your hitbox.

  • Will disconnect connections associated with all parts in the assembly and reset the hitbox.collisions data to 0 if the hitbox.revert parameter is true
  • Will fire the hitbox.Stopped:Connect() event passing the present debris and affected destructible parts from the hitboxes runtime from start to stop
hitbox:Fire()

Will activate the hitbox once.

  • If destructible parts are detected it will fire the hitbox.Collision:Connect() event passing the collision’s debris and affected parts
hitbox:GetRuntimeParts()

Will return the all the created debris and affected destructible parts from the hitbox’s runtime between when it was started and stopped.

hitbox:GetLifetimeParts()

Will return the all the created debris and affected destructible parts from the hitbox’s runtime between when it was started and stopped.

Runtime Updating

Hitboxes store your original parameters, you are able to update these parameters without stopping and restarting the hitbox to see their effects. All parameters passed to the .hitbox() are retrievable and updatable like this. To update a hitbox’s stored data get the returned object and the parameter name inside of it like so:

local hitbox = vox.hitbox(focus, limit, auto, distance, parameters, voxelSize, debrisSize, debrisCount, cutoutSize, gridLock, reset) -- Whatever your parameters are?

hitbox.reset = 60 -- Update the reset timer
hitbox.voxelSize = 5 -- Update the voxel size
hitbox.gridLock = true -- Update grid lock
Unique Parameters

The following parameters are unique to the .hitbox() method.

focus: Instance

The part or model the hitbox is focused on.

  • If O is the entire operation of the destruction algorithm then your runtime based off of part count is linear, O(n). Keep your models within a realistic amount of detail to avoid slowing the queue.
limit: number?

The ceiling for the number of collisions a hitbox can handle.

  • If passed as 0 then there is no limit for the amount of collisions a hitbox can handle
  • Defaults to 0
auto: boolean?

Whether or not the hitbox will stop itself after reaching the limit ceiling.

  • Defaults to true
  • Intended to allow hitboxes to have penetration depth into wide parts when used in conjunction with the limit parameter and to make stopping the hitbox optional
revert: boolean?

Whether or not the hitbox’s collision counter will be reset to 0 every time the hitbox is stopped.

  • Defaults to false
distance: number?

The distance that the hitbox has to travel before it fires again.

  • If passed as nil will apply the voxelSize parameter, if the voxelSize parameter is nil then hitboxRelative setting is applied. The useRelativity setting does not have to be true for relativity to be applied to a hitbox’s distance
  • Check Methodology & Concepts for more information on how this works and why it is important
Inherited Parameters

All of these parameters are inherited from the .destroy() method, check the method description above for more information on their use.

parameters: OverlapParams?,
voxelSize: number?, 
debrisSize: number?, 
debrisCount: number?, 
cutoutSize: number?, 
gridLock: boolean?,
reset: number?,
freeze: boolean?,
destroy: boolean?,
lock: boolean?
Read Only Data

Hitboxes contain various read-only (intended use) properties but you can still use them if you understand them. The only one you will likely use frequently is hitbox.collisions = 0 to reset the collisions.

  • The hitbox.runDebris, hitbox.totalDebris, hitbox.runWalls, and hitbox.totalWalls data are intended for storage, to retrieve these outside of events use the :GetRuntimeParts() and :GetLifetimeParts() methods
hitbox.queue = nil
hitbox.collisions = 0
hitbox.connections = {}
hitbox.runDebris = {}
hitbox.totalDebris = {}
hitbox.runWalls = {}
hitbox.totalWalls = {}
hitbox.queue = nil

The queue which handles hitbox position collisions to prevent skipping material in walls for high velocity hitboxes.

hitbox.collisions = 0

The current collision amount detected by the hitbox. Whenever the hitbox breaks a part. Used to create depth.

hitbox.connections = {}

The connections applied to all parts in the assembly which check for updated cframe, size, and part shape.

hitbox.runDebris = {}

All of the debris created from the hitbox from when the hitbox was started to when it was stopped.

  • Handling debris in separate events may show nil values in this table, it is best to loop through and check for non-nil parts
hitbox.totalDebris = {}

All of the debris created from the hit from when the hitbox was created.

  • Handling debris in separate events may show nil values in this table, it is best to loop through and check for non-nil parts
hitbox.runWalls = {}

All of the walls interacted with from the hitbox from when the hitbox was started to when it was stopped.

  • Handling walls in separate events may show nil values in this table, it is best to loop through and check for non-nil parts
hitbox.totalWalls = {}

All of the walls interacted with from when the hitbox was created.

  • Handling walls in separate events may show nil values in this table, it is best to loop through and check for non-nil parts
Events
hitbox.Started:Connect(function()
	print("Started")
end)

Fires when the hitbox is activated with the :Start() method.

hitbox.Stopped:Connect(function(debris, walls)
	print("Stopped")
end)

Fires when the hitbox is stopped for any case.

  • Returns all the debris created and breakable parts affected during the runtime of the hitbox, starting from when the hitbox was activated to when it was deactivated
hitbox.Ceiling:Connect(function()
	print("Ceiling hit")
end)

Fires when the collisions counter reaches the limit ceiling. The hitbox.collisions parameter must reach the hitbox.limit parameter.

hitbox.Collision:Connect(function(debris, walls)
	print("Collision detected")
end)

Fires when a hitbox interacts with a destructible part in any way.

  • Returns the debris created from the interaction as well as the walls affected

Do NOT destroy debris and destructible parts when finished using them!

Instead use the .cleanup() method of the module.

You can pass either a single part or a table of parts to the .cleanup() method.

Instead of using :Destroy() to destroy debris and affected destructible parts you need to use the .cleanup() method of the module since destroyed parts cannot be returned to cache.

  • If the useCache setting is true and the parts are stored in cache they will be returned, otherwise they will be destroyed if either of those conditions are false

It is optional to cleanup the hitbox object since ROBLOX’s garbage collection should clean up the object once it is no longer in scope, since it is good practice to free up any references immediately use hitbox = nil when you no longer need the hitbox.

Settings

Inside of the module’s hierarchy you will find a “Settings” module containing default settings that change the functionality of your methods. Defaults have been set to be what I deem to be the most appropriate settings for immediate use.

image

Settings.attribute = "Breakable"

The name of the attribute which you must apply to all parts that you want to use this module on.

Settings.debrisContainer = game:GetService("Workspace")

Where your debris are stored.

Settings.resetDefault = 60

The default time used for a timer if the passed reset parameter is 0.

Settings.minimumReset = 10

The minimum amount of time a reset timer can be set to, passing the reset parameter as less than this will update it to this minimum.

Settings.shapeDefault = Enum.PartType.Ball

The default region shape.

Settings.voxelDefault = 1

The default voxel size used if the passed voxelSize parameter is 0 and useRelativity setting is false.

Settings.debrisDefault = 1 

The default debrissize used if the passed debrisSize parameter is 0 and useRelativity setting is false.

Settings.cutoutDefault = 1 

The default cutout size relative to your region’s size used if the passed cutoutSize is nil or 0.

Settings.gridLockDefault = false

Determines whether grid locking is applied if the passed gridLock parameter is nil.

Settings.useRelativity = true

Determines whether relativity is applied instead of default values if passed voxelSize or debrisSize parameters are nil.

  • If true then relativity is applied if false then default values are applied
Settings.voxelRelative = 1/3

The relative size of voxels to your region size if relativity is applied.

Settings.debrisRelative = 1/3

The relative size of debris to your region size if relativity is applied.

Settings.hitboxRelative = 1/3

The relative size of your hitboxes size determining the distance your hitbox needs to travel before the next collision.

  • Check the hitbox distance concept under Methodology & Concepts for more information
Settings.useGreedyMeshing = true

Whether or not greedy meshing will be utilized after destruction, it is highly recommend to leave this as true as meshing parts limits part count reducing lag considerably.

Settings.useRunService = false

Whether or not RunService will be utilized for hitbox distance checking. Requires more memory but may produce better results for high velocity hitboxes.

Settings.usePartCache = true

Whether or not part caching will be utilized. t is highly recommended to leave this as true as part caching prevents lag caused by instancing numerous parts.

Settings.cachePrecreated = 10000

The quantity of pre-created parts if part caching is active.

Settings.cacheExtra = 100

The amount of additional parts cached if the cache ceiling is reached.

Default Settings
Settings.attribute = "Breakable" -- Name of attribute to check for
Settings.debrisContainer = game:GetService("Workspace") -- Debris storage

Settings.resetDefault = 60 -- Time it takes for the wall to repair itself since last break
Settings.minimumReset = 10 -- Minimum reset timer, passing a value less than this will use this

Settings.shapeDefault = Enum.PartType.Ball -- Default region shape
Settings.voxelDefault = 1 -- Default voxel size
Settings.debrisDefault = 1 -- Default debris size
Settings.cutoutDefault = 1 -- Default cutout size
Settings.gridLockDefault = false -- Default grid lock, 

Settings.useRelativity = true -- If true relativity will be applied instead of default size when size is not specified
Settings.voxelRelative = 1/3 -- Voxel size relative to hitbox size
Settings.debrisRelative = 1/3 -- Debris size relative to hitbox size
Settings.hitboxRelative = 1/3 -- If voxel size is not specified hitboxes will use a relative size applied to the part size
-- *this only applies to movement, to have your intersection sizes match your part size keep this at 1

Settings.useGreedyMeshing = true -- Recommended to leave this on for reduced part count to combat lag
Settings.useRunService = false -- Use RunService instead of detecting changed position, may be more accurate
Settings.usePartCache = true -- Use PartCache so instancing parts does not create lag
Settings.cachePrecreated = 10000 -- The original cache ceiling
Settings.cacheExtra = 100 -- The amount of extra parts created everytime the cache ceiling is reached
Examples

Example 1#

local object = script:WaitForChild("Part")
object.Parent = game:GetService("Workspace")

local vox = require(game:GetService("ReplicatedStorage"):WaitForChild("VoxelDestruction"))

local focus = object
local limit = 0
local auto = true
local revert = true
local distance = nil
local parameters = nil

local voxelSize = 1
local debrisSize = nil
local debrisCount = 3
local cutoutSize = 1
local gridLock = false
local reset = 60

local hitbox = vox.hitbox( 
	focus,
	limit, 
	auto,
	revert, 
	distance, 
	parameters, 
	voxelSize, 
	debrisSize, 
	debrisCount, 
	cutoutSize, 
	gridLock, 
	reset)

hitbox.Started:Connect(function()
	print("Started")
end)
hitbox.Stopped:Connect(function(debris, walls)
	print("Stopped")
end)
hitbox.Ceiling:Connect(function()
	print("Ceiling hit")
end)
hitbox.Collision:Connect(function(debris, walls)
	print("Collision detected")
	
	for i,debri in ipairs(debris) do
		coroutine.resume(coroutine.create(function()
			debri.Anchored = false
			task.wait(task.wait(math.random(1,3)))
			vox.cleanup(debri)
		end))
	end
	
end)

task.wait(10)
hitbox:Start()

task.wait(3)
hitbox.gridLock = true -- Want to edit data during runtime?

task.wait(10)
hitbox:Stop()

hitbox = nil
3 Likes

Nice! This will be great, I love voxels and destruction

1 Like

Can you post the resource link and the methods

This work’s great but I’m not sure if you have it so if the object that’s colliding with the destructible object, must have a minimum velocity it needs to have to actually destroy it (that probably was hard to read, I couldn’t really think of a better way to word it.)