Issue with my blood module/blood script

You can write your topic however you want, but you need to answer these questions:

  1. What do you want to achieve? Keep it simple and clear!
    Flinging blood parts from a character with a module without progressively getting faster

  2. What is the issue? Include screenshots / videos if possible!
    My blood system has blood parts that are flung out of a character based off of damage.
    But, the fling seems to get faster every time i hit an enemy, even though i’m dealing the same amount of damage.

  1. What solutions have you tried so far? Did you look for solutions on the Creator Hub?
    I’ve tried multiple AI models, including Claude, but they all didn’t seem to be able to fix it.

After that, you should include more details if you have any. Try to make your topic as descriptive as possible, so that it’s easier for people to help you!

Blood Module
local module = {}
local TweenService = game:GetService("TweenService")
local Debris = game:GetService("Debris")
local blood = game.ReplicatedStorage.Blood

local MAX_POOL_SIZE = 5
local MAX_BLOOD_PARTS = 200

-- Random position offset for splats
local function getRandomOffset()
	return Vector3.new(math.random(-5, 5) / 10, 0, math.random(-5, 5) / 10)
end

-- Make one blood splat merge into another (only if new is smaller or equal)
local function mergeBlood(existing, new)
	-- Only merge if new blood is smaller or equal in size
	if new.Size.X > existing.Size.X then
		return false
	end

	-- Don't let puddles get too big
	if existing.Size.X >= MAX_POOL_SIZE then
		TweenService:Create(new, TweenInfo.new(0.1), {Transparency = 1}):Play()
		Debris:AddItem(new, 0.1)
		return true
	end

	local sizeIncrease = math.random(8, 15) / 100 -- +8–15%
	local opacityBoost = 0.15 -- become more visible
	local newTransparency = math.max(0.5, math.min(1, existing.Transparency - opacityBoost))

	-- Just update size and transparency, no animations
	existing.Size = existing.Size * (1 + sizeIncrease)
	existing.Transparency = newTransparency

	-- Fast fade out the merged one
	TweenService:Create(new, TweenInfo.new(0.1), {Transparency = 1}):Play()
	Debris:AddItem(new, 0.1)
	return true
end

-- Shared function for when blood lands
local function setupBlood(blood_, character, sizeScale)
	local hasLanded = false

	blood_.Touched:Connect(function(hit)
		-- Ignore the character that spawned the blood
		if hit:IsDescendantOf(character) then return end
		if hit.Parent:FindFirstChild("Humanoid") then return end
		if hit.Parent:IsA("Accessory") then return end
		if hit.CanCollide == false then return end

		-- ALWAYS ignore other blood parts (both in air and on ground)
		if hit.Name == "Blood" then return end

		-- Landing behavior - only on actual ground/parts
		if (hit:IsA("BasePart") or hit:IsA("UnionOperation")) and not hasLanded then
			hasLanded = true
			blood_.Anchored = true
			blood_.CanCollide = false -- Disable collision after landing

			local offset = getRandomOffset()
			-- random rotation
			local randomSize = math.random(10, 15) / 10 * sizeScale

			-- Snap to final position immediately
			blood_.CFrame = CFrame.new(
				blood_.Position.X + offset.X,
				hit.Position.Y + hit.Size.Y / 2 + 0.01,
				blood_.Position.Z + offset.Z
			) * CFrame.Angles(0, math.rad(math.random(0, 360)), 0)

			-- Only tween the size
			local goal = {
				Size = Vector3.new(randomSize, 0.1, randomSize)
			}
			local expandTween = TweenService:Create(blood_, TweenInfo.new(1), goal)
			expandTween:Play()

			-- After expansion is complete, check for nearby blood to merge with
			expandTween.Completed:Connect(function()
				if not blood_ or not blood_.Parent then return end

				-- Look for nearby blood puddles
				for _, part in pairs(workspace:GetPartBoundsInRadius(blood_.Position, 2)) do
					if part:IsA("BasePart") and part.Name == "Blood" and part ~= blood_ and part.Anchored then
						if mergeBlood(part, blood_) then
							break -- Successfully merged, stop looking
						end
					end
				end
			end)
		end
	end)

	-- Cleanup fade
	task.delay(10, function()
		if blood_ and blood_.Parent then
			local goal = {Transparency = 1}
			local tween = TweenService:Create(blood_, TweenInfo.new(1), goal)
			tween:Play()
			Debris:AddItem(blood_, 1)
		end
	end)
end

-- Spawn blood with batching to reduce lag
local function spawnBlood(character, count, sizeScale, intensity)
	local root = character:FindFirstChild("HumanoidRootPart")
	if not root then return end

	local spawned = 0
	while spawned < count do
		for i = 1, math.min(10, count - spawned) do
			spawned += 1
			local blood_ = blood:Clone()
			if not root then continue end
			blood_.CFrame = root.CFrame * CFrame.new(math.random(-1,1), math.random(-1,1), math.random(-1,1))
			blood_.Parent = workspace
			blood_.Transparency = 0.6 + math.random()*0.2

			-- Use BodyVelocity instead of direct velocity
			local bodyVel = Instance.new("BodyVelocity")
			bodyVel.MaxForce = Vector3.new(math.huge, math.huge, math.huge)
			bodyVel.Velocity = Vector3.new(
				math.random(-6,6), 
				math.random(2,8), 
				math.random(-6,6)
			) * (intensity/1.5)
			bodyVel.Parent = blood_
			Debris:AddItem(bodyVel, 0.2) -- Remove BodyVelocity after 0.2 seconds

			setupBlood(blood_, character, sizeScale)
		end
		task.wait(0.05)
	end
end

-- spawn death explosion
local function spawnDeathExplosion(character)
	local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
	if not humanoidRootPart then return end

	spawnBlood(character, character.Humanoid.MaxHealth / 2, 1.2, 7)
end

-- Damage-based blood spawning
function module.damage(character, damage)
	local humanoid = character:FindFirstChild("Humanoid")
	if not humanoid then return end
	humanoid.Health -= damage
	local isDead = humanoid.Health <= 0

	local baseCount = isDead and math.max(15, math.min(MAX_BLOOD_PARTS, math.log(damage+5)*10))
		or math.max(5, math.min(MAX_BLOOD_PARTS, math.log(damage+5)*5))

	local sizeScale = isDead and 1.2 or 1
	local intensity = damage/math.random(5,7) -- Moderate intensity

	spawnBlood(character, baseCount, sizeScale, intensity)

	-- DEATH EXPLOSION
	if isDead then
		task.wait(0.1)
		spawnDeathExplosion(character)
	end
end

-- Just spawn blood, don't apply damage
function module.OnlySpawnBlood(character, damage)
	local humanoid = character:FindFirstChild("Humanoid")
	if not humanoid then return end
	local isDead = humanoid.Health <= 0

	local baseCount = isDead and math.max(15, math.min(MAX_BLOOD_PARTS, math.log(damage+5)*10))
		or math.max(5, math.min(MAX_BLOOD_PARTS, math.log(damage+5)*5))

	local sizeScale = isDead and 1.2 or 1
	local intensity = damage/math.random(5,7) -- Moderate intensity

	spawnBlood(character, baseCount, sizeScale, intensity)

	-- DEATH EXPLOSION
	if isDead then
		task.wait(0.1)
		spawnDeathExplosion(character)
	end
end

return module

Although it may be coming from the module, i think it might have something to do with the script that calls the blood module’s functions:

Blood Caller
-- spawn blood on damage
local humanoid = script.Parent:WaitForChild("Humanoid")
local bloodModule = require(game.ServerScriptService.Blood)

local lastHP = humanoid.Health

local connection
connection = humanoid.HealthChanged:Connect(function(health)
	-- Skip if humanoid is dead or dying
	if health <= 0 then
		return
	end

	-- Calculate damage
	local damage = lastHP - health

	-- Only spawn blood if actually taking damage (not healing)
	if damage > 0 then
		bloodModule.OnlySpawnBlood(script.Parent, damage)
	end

	-- Always update lastHP to current health
	lastHP = health
end)

-- Cleanup on death
humanoid.Died:Connect(function()
	if connection then
		connection:Disconnect()
	end
end)

Anything helps! (make it optimized please, if you dont use touch events)
(beginner scripter, sorry stuff isn’t clean/bugfree or i have less knowledge)

The issue was a little tricky, but it was caused by the bloodModule.OnlySpawnBlood(script.Parent, damage) being a yielding function. The spawnBlood function in your Blood module has a while loop, with a task.wait() that runs for a good amount of time, meaning it takes a while for bloodModule.OnlySpawnBlood(script.Parent, damage) to return, causing lastHP = health to not really update (or update very late). Then, the next time you take damage, it still thinks lastHP is 100, so damage gets bigger and bigger as health gets smaller, causing the intensity to get bigger and bigger

To fix it, either use the task library in the Blood module itself, so that calls to OnlySpawnBlood() don’t yield (which is what I would do probably), like this


(Note that task.defer() is interchangeable with task.spawn() here. I wont go into the difference between them)
Since the OnlySpawnBlood() doesn’t return anything, it doesn’t need to yield, and it being a yielding function is unexpected, leading to these kinds of bugs

Alternatively, you can also use task.defer() or task.spawn() when calling the function, which will also fix the issue

1 Like

Wow! Thanks so much!

1 Like