Pls someone review this explosion module

Hello, i want to optimise this explosion script and correct some logic issues. I want the explosion script to be polished and have the capabillity to perform tons of explosions in quick sucsession without exploding my computer

The explosion script works fine but it has alot more room for improvement such as
-performance
-readablilty
-cleaned up logic
-reliability
And as a bonus you can use this script for free!!

The explosion script was made to replace the already existing roblox engine explosion written in (C). The purpose of the script is to give developers way more controll over an explosion which has many good use cases (fighter games, game details, realism, action, just pure fun)

What i am also aiming for is to add explosion FX that is contructed with SpecialMesh and Normal Parts that runs on one client or multiple. Smoke effects would interact with the enviroment using raycasting and Tweening. Some smoke effects could also shape into a mushroom cloud.

--!nocheck
-- ExplosionModule.lua

local ExplosionModule = {}

--// Services
local RunService = game:GetService("RunService")
local Debris = game:GetService("Debris")
local ContentProvider = game:GetService("ContentProvider")
local TweenService = game:GetService("TweenService")
local Lighting = game:GetService("Lighting")

--// Cosmetic folders
local CosmeticFolder = workspace:FindFirstChild("CosmeticWorkspaceFolder") or Instance.new("Folder")
CosmeticFolder.Name = "CosmeticWorkspaceFolder"
CosmeticFolder.Parent = workspace

local ExplosionFXFolder = CosmeticFolder:FindFirstChild("ExplosionFX") or Instance.new("Folder")
ExplosionFXFolder.Name = "ExplosionFX"
ExplosionFXFolder.Parent = CosmeticFolder

--// Shared FX attachment
local FXAttachment = Instance.new("Attachment")
FXAttachment.Name = "XplosionFXAttachment"
FXAttachment.Parent = workspace.Terrain

--// Fast locals
local clamp, max, min, random = math.clamp, math.max, math.min, math.random
local GetPartBoundsInRadius = workspace.GetPartBoundsInRadius
local clone, t_type = table.clone, type
local Stepped = RunService.Stepped

--// Default config
function ExplosionModule.GetDefaultExplosionConfigs()
	return {
		explosionSound = {
			{ id = "3802270141", volume = 1.5, pitch = 1 },
		},
		explosionRadius = 75,
		explosionMaxDamage = 75,
		shockwaveSpeed = 210,--96.04,
		explosionVelocity = 38,
		explosionDestroyVelocity = 30,
		explosionCanDestroy = false,
		explosionMomentumThreshold = 0.2,
		ignoreParts = {},
		DEBUG = false,
	}
end

--// Merge configs
local function mergeConfigs(defaults, overrides)
	if not overrides then return clone(defaults) end
	local out = clone(defaults)
	for k, v in overrides do
		local dv = out[k]
		out[k] = (t_type(v) == "table" and t_type(dv) == "table") and mergeConfigs(dv, v) or v
	end
	return out
end

--// Sound cache
local SoundCache = {}
local function createSound(data)
	local s = Instance.new("Sound")
	s.SoundId = "rbxassetid://" .. data.id
	s.PlaybackSpeed = data.pitch or 1
	s.Volume = data.volume or 0.5
	s.RollOffMode = Enum.RollOffMode.Inverse
	s.Name = tostring(data.id)
	return s
end

local function preloadExplosionSounds(cfg)
	local list = cfg.explosionSound
	if not list or #list == 0 then return end
	local preload = table.create(#list)
	for _, s in list do
		local id = tostring(s.id)
		if not SoundCache[id] then
			local tmpl = createSound(s)
			SoundCache[id] = tmpl
			preload[#preload + 1] = tmpl
		end
	end
	if #preload > 0 then ContentProvider:PreloadAsync(preload) end
end

local function pickCachedSound(list)
	if not list or #list == 0 then return nil end
	local s = list[random(1, #list)]
	return SoundCache[tostring(s.id)] or createSound(s)
end

local function playSoundAtPos(template, pos)
	local sound = template:Clone()
	sound.PlaybackSpeed += random(-200, 200) * 0.001
	sound.PlayOnRemove = true
	sound.Parent = FXAttachment
	local function play()
		FXAttachment.WorldPosition = pos
		sound:Destroy()
	end
	if sound.IsLoaded then play() else sound.Loaded:Once(play) end
end

--// FX cache
local FXCache = {}
local function setupPart(p)
	p.Anchored, p.CanCollide, p.CanQuery, p.CanTouch, p.Locked, p.CastShadow =
		true, false, false, false, true, false
end

if not FXCache.DebugSphere then
	local sphere = Instance.new("Part")
	sphere.Shape, sphere.Material, sphere.Color = Enum.PartType.Ball, Enum.Material.Neon, Color3.new(1, 0.2, 0.1)
	sphere.Transparency, sphere.Size, sphere.Name = 0.7, Vector3.one, "DebugShockwaveFX"
	local Mesh=Instance.new("SpecialMesh")
	Mesh.MeshType,Mesh.Scale=Enum.MeshType.Sphere,Vector3.one
	Mesh.Parent,Mesh.Name=sphere,"SpecialMesh"
	setupPart(sphere)
	FXCache.DebugSphere = sphere
end

if not FXCache.DebugHit then
	local p = Instance.new("ParticleEmitter")
	p.Speed, p.Lifetime, p.Enabled, p.LightEmission, p.LightInfluence, p.Brightness =
		NumberRange.new(0), NumberRange.new(1), false, 1, 0, 1
	p.Parent = FXAttachment
	FXCache.DebugHit = p
end

local function createDebugSphere(pos, radius, speed)
	local sphere = FXCache.DebugSphere:Clone()
	sphere.Parent, sphere.Position = ExplosionFXFolder, pos
	local total = radius / speed
	TweenService:Create(sphere.SpecialMesh, TweenInfo.new(total, Enum.EasingStyle.Linear),
		{ Scale = Vector3.one * radius * 2 }):Play()
	TweenService:Create(sphere,TweenInfo.new(total, Enum.EasingStyle.Cubic,Enum.EasingDirection.In),{Transparency = 1}):Play()
	Debris:AddItem(sphere, total)
end

--// Humanoid cache
local humanoidCache = setmetatable({}, { __mode = "k" })
local function getHumanoid(model)
	local cached = humanoidCache[model]
	if cached ~= nil then
		if cached == false then return nil end
		if cached:IsA("Humanoid") and cached.Parent == model then return cached end
	end
	local found = model:FindFirstChildOfClass("Humanoid")
	humanoidCache[model] = found or false
	return found
end

local function applyImpulseOnce(part:BasePart, center, dist, radius, blastPressure, threshold, maxVel, offset, destroyVel)
	local root = part.AssemblyRootPart or part
	if root.Anchored==true and part.Anchored==false then
		root=part
	end
	local falloff = 1 - (dist / radius)
	falloff = falloff * falloff
	local impulse = blastPressure * falloff / (min(max(root.AssemblyMass, 0.01), part.Mass))
	if impulse < threshold then return threshold end
	local newVel = root.AssemblyLinearVelocity + offset * (impulse / dist) 
	local maxSpeed = max(maxVel * 4, destroyVel)
	local speed = newVel.Magnitude
	if speed > maxSpeed then
		newVel *= maxSpeed / speed
		speed = maxSpeed
	end
	root.AssemblyLinearVelocity = newVel
	return speed
end

--// Damage handler
local DamagedTag = Instance.new("Folder")
DamagedTag.Name = "XplosionDamagedTag"
local function tryDamage(part, dist, radius, dmg, canDestroy)
	local model = part.Parent
	if model:FindFirstChild("XplosionDamagedTag") then return end
	local ratio = 1 - (dist / radius)
	ratio = ratio * ratio
	if ratio>.1 then
		local hum = getHumanoid(model)
		if hum then
			hum:TakeDamage(dmg * ratio)
			local tag = DamagedTag:Clone()
			tag.Parent = model
			Debris:AddItem(tag, 0.175)
		elseif canDestroy and part:IsA("BasePart") then
			part:BreakJoints()
		end
	end
end

local function applyEffectsOnHit(part, center, dist, profile, offset)
	local v = applyImpulseOnce(part, center, dist, profile.R, profile.BlastPressure,
		profile.Threshold, profile.ExplosionVel, offset, profile.ExplosionDestroyVelocity)
	if v >= profile.ExplosionDestroyVelocity then
		tryDamage(part, dist, profile.R, profile.MaxDmg, profile.CanDestroy)
	end
end

--// Shockwave runner
local XplodeParams = OverlapParams.new()
XplodeParams.FilterType=Enum.RaycastFilterType.Exclude
local function makeShockwaveRunner(pos, profile, Params)
	local elapsed, R, V, DEBUG = 0, profile.R, profile.V, profile.DEBUG
	XplodeParams.FilterDescendantsInstances = Params.FilterDescendantsInstances
	local function DoDebug() end
	if DEBUG then
		createDebugSphere(pos, R, V)
		function DoDebug(hit)
			FXAttachment.CFrame = hit.CFrame
			FXCache.DebugHit:Emit(1)
		end
	end
	while true do
		local _, dt = Stepped:Wait()
		elapsed += dt
		local currR = V * elapsed
		if currR >= R then currR = R end
		for _, part: BasePart in GetPartBoundsInRadius(workspace, pos, currR, XplodeParams) do
			if not part.Anchored then
				local offset = part.Position - pos
				applyEffectsOnHit(part, pos, offset.Magnitude, profile, offset)
				if DEBUG then DoDebug(part) end
				XplodeParams:AddToFilter(part)
			end
		end
		if currR >= R then break end
	end
end

--// Public API
local Params = OverlapParams.new()
Params.FilterType = Enum.RaycastFilterType.Exclude
function ExplosionModule.CreateExplosion(config, position)
	local merged = mergeConfigs(ExplosionModule.GetDefaultExplosionConfigs(), config)
	local BlastPressure = merged.blastPressure or (merged.explosionMaxDamage * 9000)
	local Threshold = merged.explosionMomentumThreshold or
		clamp((BlastPressure / (merged.explosionRadius * 9000)), 0.05, 1.5)
	local profile = {
		R = merged.explosionRadius,
		V = merged.shockwaveSpeed,
		MaxDmg = merged.explosionMaxDamage,
		BlastPressure = BlastPressure,
		CanDestroy = merged.explosionCanDestroy,
		DEBUG = merged.DEBUG,
		Threshold = Threshold,
		ExplosionDestroyVelocity = merged.explosionDestroyVelocity,
		ExplosionVel = merged.explosionVelocity,
	}
	Params.FilterDescendantsInstances = merged.ignoreParts
	preloadExplosionSounds(merged)
	local sound = pickCachedSound(merged.explosionSound)
	if sound then playSoundAtPos(sound, position) end
	makeShockwaveRunner(position, profile, Params)
end

return ExplosionModule
  • Caution -
    Please do not ask people to write entire scripts or design entire systems for you.
1 Like

Looks like a lot of work. Seen a few things..
for k, v in overrides do ..should be.. for k, v in pairs(overrides) do.
XplodeParams:AddToFilter(part) .. may be invalid; OverlapParams has no AddToFilter method. Think you’ll need to rebuild the filter table each iteration or use a custom exclusion list.