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
You lowkey traumatized me with the amount of “bloat” this script has.
Currently crying in my ze new luau order king throne
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
Also bro 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
Congrats, you turned a single hitbox into a - bloat dependency apocalypse.
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.
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:
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.
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.
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:
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.