Feedback on my hitbox module?

This is the first hitbox module i’ve made and my goal was for maximum optimisation hence why i used getpartsboundinbox, etc.

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Trove = require(ReplicatedStorage.Shared.Dependencies.Trove)
local Signal = require(ReplicatedStorage.Shared.Dependencies.Signal)
local Promise = require(ReplicatedStorage.Shared.Dependencies.Promise)

local HitboxService = {}
HitboxService.__index = HitboxService

function HitboxService.new(PrimaryPart : Part, Size: Vector3,Offset: CFrame, Duration: number)
	assert(PrimaryPart and PrimaryPart:IsA("BasePart"), "[HitboxService] requires a PrimaryPart")
	assert(typeof(Size) == "Vector3", "[HitboxService] Size must be a Vector3")
	assert(Duration > 0, "[HitboxService] Duration must be a positive float/integer")
	
	local self =  setmetatable({}, HitboxService)
	self._maid = Trove.new()
	self.Hit = Signal.new()
	self._maid:Connect(self.Hit)
	
	local Parameters = OverlapParams.new()
	Parameters.FilterType = Enum.RaycastFilterType.Exclude
	Parameters.FilterDescendantsInstances = {PrimaryPart.Parent}
	
	self.PrimaryPart = PrimaryPart
	self.Offset = Offset
	self.Parameters = Parameters
	self.Size = Size
	self.Duration = Duration or 0.1
	self._hitModels = {}
	
	self._maid:Add(RunService.Heartbeat:Connect(function(deltaTime: number) 
		self:_update()
	end))
	
	
	Promise.delay(Duration):andThen(function()
		self:Destroy()
	end)
	
	return self
end

function HitboxService:_update()
	if not self.PrimaryPart then
		return
	end
	
	if not self.PrimaryPart.Parent then
		return
	end
	
	local CFrame = self.PrimaryPart.CFrame * self.Offset
	local Table = workspace:GetPartBoundsInBox(CFrame, self.Size, self.Parameters)
	
	for _,Part in pairs(Table) do
		local Character = Part:FindFirstAncestorWhichIsA("Model")
		if Character and not self._hitModels[Character] then
			local Humanoid = Character:FindFirstChildOfClass("Humanoid")
			if Humanoid then
				local lastHit = self._hitModels[Character]
				local now = tick()
				if lastHit and now - lastHit < 0.1 then
					continue
				end
				
				self._hitModels[Character] = now
				self.Hit:Fire(Character,Humanoid,Part)
			end
		end
	end
end

function HitboxService:Destroy()
	self._maid:Destroy()
	self.PrimaryPart = nil;
	self.Size = nil;
	self.Offset = nil;
	self._hitModels = nil;
	self.Hit = nil;
end
1 Like

You lowkey traumatized me with the amount of “bloat” this script has.
Currently crying in my ze new luau order king throne :smiling_face_with_tear:

Not only did you FORCE OOP in a place where it makes the least sense to be, you added bloatware (malware literally) named Maid and some unknown signal library.
That just comically evil atp :folded_hands: :skull: :wilted_flower: :headstone:

Also bro :wilted_flower: its not JS. What the hell do you do with promises here?
That is, in fact, such an overkill that I’m not going to complain about the use of deprecated stuff and metatables because THE AMOUNT of other bloat just owerpowers them :broken_heart:

Congrats, you turned a single hitbox into a - bloat dependency apocalypse. :handshake: :smiling_face_with_tear:

Fun fact: that called a hurtbox actually.

3 Likes

It sure looks nice, consistent variable naming and I don’t see issue in this. You can easily expand with modules and idk where the mentioned “bloatware” is.

You could add some comments, but it is quite readable for me already.

What gives me shivers, is hard coded :Destroy() method. Try replacing it with for loop instead, cuts on lines and you don’t have to hard code more “nilling” when you expand the module.

Thanks for the feedback, even if it was a little cruel :sob:. I’ll take this into account.

2 Likes

That the opposite as to what you should do LOL

You need to get code to be lightweight and super optimized.
This? This is a waste of CPU cycles in a comically evil way

I attempted to make this as detailed and easy to read as possible. Let me know if you have questions:

Architecture Concerns

Current Issue: Object-Oriented Overhead

The current implementation creates a new HitboxService instance for each hitbox, which has several performance and maintainability issues:

  1. Multiple RunService Connections: Each hitbox creates its own RunService.Heartbeat:Connect() listener. If you have 10 active hitboxes, you’re running 10 separate update functions every frame. This fragments the update logic and creates unnecessary overhead.
  2. Excessive Object Creation: Every hitbox instantiates a full service object with its own Trove, Signal, and state management. This increases memory pressure and GC load, especially during combat scenarios with multiple simultaneous attacks.
  3. Bottom-Up Updates: Currently, each service updates itself independently (self:_update()). This makes it harder to:
  • Implement global hitbox prioritization
  • Batch spatial queries efficiently
  • Debug multiple hitboxes simultaneously
  • Control update order when it matters

Recommended Refactor: Data-Oriented Design

Create a Single Manager with Data Arrays

Instead of creating service instances, use a centralized HitboxManager that maintains an array of hitbox data:

-- Data structure (plain table/dict)
local hitboxData = {
    primaryPart = Part,
    size = Vector3.new(),
    offset = CFrame.new(),
    endTime = tick() + duration,
    hitModels = {},
    onHitCallback = function(character, humanoid, part) end,
    parameters = OverlapParams.new()
}

Benefits:

  • Single Update Loop: One Heartbeat connection iterates through all active hitboxes
  • Better Cache Locality: Data is stored contiguously, improving CPU cache hits
  • Easier Optimization: You can sort hitboxes by priority, implement spatial partitioning, or skip inactive ones
  • Simpler Debugging: Single point to inspect all active hitboxes
  • Cleanup: Remove completed hitboxes from array instead of destroying individual objects

Implementation Approach

-- Pseudo-code structure
HitboxManager = {
    _activeHitboxes = {},
    _connection = nil
}

function HitboxManager:CreateHitbox(data)
    table.insert(self._activeHitboxes, data)
    -- Start connection if first hitbox
end

function HitboxManager:_updateAll(deltaTime)
    local now = tick()
    for i = #self._activeHitboxes, 1, -1 do
        local hitbox = self._activeHitboxes[i]
        
        -- Check expiration
        if now >= hitbox.endTime then
            table.remove(self._activeHitboxes, i)
            continue
        end
        
        -- Perform hitbox check (same logic as before)
        self:_checkHitbox(hitbox)
    end
end

This refactor moves from “many objects managing themselves” to “one manager processing data”, which scales much better and aligns with modern ECS (Entity Component System) principles.

7 Likes