ShapecastHitbox: For all your melee needs! [V.0.2.5]

Note: This module is still relatively new, and has yet to be tested for bugs. If you are uncertain about the stability of this module, I recommend using RaycastHitbox or ClientCast, both of which have a proven track record in thousands of games.

This module is the successor to my previous team’s RaycastHitbox: For All Your Melee Needs. Each library has unique features, so I recommend reading both to determine which one best suits your needs. In summary, ShapecastHitbox offers the following advantages over RaycastHitbox:

  • Shapecasting (Blockcast, Spherecast, Raycast)
  • Performance improvements when running many hitboxes
  • Simplified API
  • Decoupled hitboxes from instances/humanoids, allowing you to reuse hitboxes or use the same instances for different hitboxes
  • Typechecking (still new to this so might get some wrong)
  • Support for Mesh Deformation and Bones
  • Resolution (adjust accuracy for better performance)
  • Chaining operations

Limitations:

  • Casts do not detect hits when the rays start intersecting a part
  • Stationary hitboxes (like traps) will not work
  • Wide objects requires a sizable amount of attachments to be working correctly
  • Like RaycastHitbox, still requires a few redundant attachments, even if using Sphere/Blockcast as Roblox’s casting methods are still not entirely accurate

New to Raycasting Hitboxes? Original Post:

A few years ago, I became intrigued by the design of MORDHAU and Chivalry. I was particularly interested in how they achieved accurate hitboxing across a variety of weapon shapes and sizes while keeping the system simple. Here’s a timestamp showing how MORDHAU handles hitboxes for swords—it might be helpful.

Warning: Does include realistic depiction of blood so please be aware before opening

Examples

Similar to Mordhau, we will be using raycast to simulate our hitboxes.

Unlike RaycastHitbox, ShapecastHitbox does not limit you to just raycasting; you can also use Blockcasts and Spherecasts to create your hitboxes.

New Helper Plugin Now Available:

This optional plugin allows you to freely edit your hitboxes created with ShapecastHitbox, RaycastHitbox, and ClientCast. You can move groups of attachments, position or rotate Blockcast regions, or upscale Spherecast hitboxes. It is still experimental and lacks many features, but it will be periodically updated. Please refer to the main plugin post for updates.

How do I try?

If you want to experience what it’s like to use Shape/Raycast hitboxes without the effort in setting it up, try the new playground! It’s also open-sourced.

Documentation

As this is still a relatively new module, there are no official documentation available yet. To start off, you can look at ShapecastHitbox/Types.luau to see the full API and more examples. Here are some simple scripts to get your toes wet.

Basic Hitbox Example

For any of these scripts to work, you must have added an Attachment into the part you want to become the hitbox. Rename or add the tag “DmgPoint” to this attachment.

local hitbox = ShapecastHitbox.new(swordHandle, raycastParams)

-- The hitbox will automatically disable itself after 3 seconds
-- The duration field is optional and will last indefinitely otherwise
local duration = 3 

-- HitStart is chainable into OnUpdate. You can alternatively use    
-- `Hitbox:OnUpdate()` on a new line if you prefer multi-lined usage.
hitbox:HitStart(duration):OnUpdate(function()

    -- We hit something!
    if hitbox.RaycastResult then 

        -- HitStop is chainable into OnUpdate or HitStart.
        -- For this example, we'll destroy the hitbox right
        -- after hitting something.
        hitbox:HitStop():Destroy()
    end
end)

-- Alternative, we can manually destroy the hitbox at some other time.
task.delay(10, hitbox.Destroy, hitbox)
local damage = 0
local duration = 3

hitbox:BeforeStart(function()
    damage = 5
end):HitStart(duration):OnHit(function(raycastResult, segmentHit)
    raycastResult.Instance.Parent.Humanoid:TakeDamage(damage)
end):OnUpdate(function(deltaTime)
    damage += (0.1 * deltaTime)
end):OnStopped(function(cleanCallbacks)
    cleanCallbacks() --- This will clean up this chain
    hitbox:Destroy()
end)

To turn off the red trails (these are used for debugging and test), you can directly disable it in the settings table:

ShapecastHitbox.Settings.Debug_Visible = false

If you are more familiar with the RaycastHitbox’s API, you can do similar things here:

local hitbox = ShapecastHitbox.new(tool, raycastParams)

local function onToolHit(raycastResult)
     local partHit = raycastResult.Instance
     partHit.Parent.Humanoid:TakeDamage(20)
end

hitbox:OnHit(onToolHit)

tool.Activated:Connect(function()
     hitbox:HitStart()
     task.wait(3)
     hitbox:HitStop()
end)

tool.Destroying:Connect(function()
     hitbox:Destroy()
end)
Main API

ShapecastHitbox constructor @returns Hitbox

@class Hitbox
Create easy hitboxes using Raycasts and Shapecasts.

To start using ShapecastHitbox, first initialize your hitbox via
ShapecastHitbox.new(instance). After you have configured it, you
can activate the hitbox by calling Hitbox:HitStart().

To stop the Hitbox, call Hitbox:HitStop(). If you are done using
the hitbox, it is recommended to call Hitbox:Destroy() to clean
up any connections left behind.

@function new
@param instance Instance
@param raycastParams? RaycastParams
@within Hitbox
Creates a new hitbox.

Hitbox methods

@function GetAllSegments
@return { [Instance]: Types.Segment }
@within Hitbox
Gets all segments of a hitbox. Segments refers to each individual
point that are being raycasted out of.

@function GetSegment
@param instance Instance
@return Types.Segment
@within Hitbox
Gets one segment from the hitbox using the original instance
as the reference.

@function SetResolution
@param resolution number
@return Types.Hitbox
@within Hitbox
Sets the resolution of the hitbox’s raycasting. This resolution is capped to
the user’s current frame rate (lower frames = less accurate). Lower accuracy
can result in better ShapecastHitbox performance so adjust according to the
context of your project.

@function SetCastData
@param castData Types.CastData
@return Types.Hitbox
@within Hitbox
Sets the current CastData of this hitbox. This is where you can set if the Hitbox should use
Raycast, Blockcast, or Spherecast. There is also additional funtionality (read the devforum post)
on only adjusting specific segments to use those raycast types.

@function BeforeStart
@param startCallback Types.StartCallback
@return Types.Hitbox
@within Hitbox
This callback runs before HitStart activates the hitbox.
Use OnStopped to clean up these callbacks or alternatively use
Hitbox:Destroy().

@function OnUpdate
@param updateCallback Types.UpdateCallback
@return Types.Hitbox
@within Hitbox
This callback runs per frame while the hitbox exists. Do note that
it will still run even if the hitbox is stopped. You can use
Hitbox.Active to determine if a hitbox is active or not. Use OnStopped
to clean up these callbacks or alternatively Hitbox:Destroy().

@function OnHit
@param hitCallback Types.HitCallback
@return Types.Hitbox
@within Hitbox
This callback is activated upon the hitbox returning a RaycastResult.
Use OnStopped to clean up these callbacks or alternatively Hitbox:Destroy().

@function OnStopped
@param stopCallback Types.StopCallback
@return Types.Hitbox
@within Hitbox
This callback runs after HitStop has activated. Do note that
Hitbox:Destroy() does not automatically run this function. This callback
has a parameter which helps you auto-cleanup every callback used so far.
If you don’t clean up, you may end up having duplicate callback calls
when reusing the hitbox.

@function Reconcile
@return Types.Hitbox
@within Hitbox
Runs automatically the first time a hitbox is initialized. Can be re-ran
again to make the hitbox re-search the instance for any new changes in the
hitbox. Do not run frequently as it is not performant.

@function HitStart
@param timer? number
@param overrideParams? RaycastParams
@return Types.Hitbox
@within Hitbox
Activates the hitbox. Can be given an optional timer parameter to make
the hitbox automatically stop after a certain amount of seconds. OverrideParams
can be used to switch RaycastParams on the fly (which the hitbox will default to).
Will activate all BeforeStart callbacks before activating the hitbox.

@function HitStop
@return Types.Hitbox
@within Hitbox
Deactivates the hitbox. Will call all OnStopped callbacks after deactivation.

@function Destroy
@within Hitbox
Disconnects the scheduler. When no references to the hitbox remain, it will be
automatically garbage collected.

How do I convert my attachments to Shapecasts?

Method 1:
The first method is to use Attributes. They allow you to fine-tune which attachments should be shapecasted. These are the important attributes to add:
CastType: “Blockcast” | “Raycast” | “Spherecast”
CFrame: Usually the orientation cframe is what matters here
CastSize: Only needed if you are using Blockcast; the size of the cast
CastRadius: Only needed if you are using Spherecast; the radius of the cast

Method 2:
The second method is directly using the ShapecastHitbox API. Useful if you want to make the entire hitbox a shapecast. See below for an example.

local hitbox = ShapecastHitbox.new(part)

-- Making the hitbox a blockcast. This will change every point to be a blockcast.
hitbox:SetCastData({ CastType = "Blockcast", CFrame = CFrame.new(0, 5, 0) * CFrame.Angles(math.rad(90), 0, 0), Size = Vector3.new(20, 10, 20) })

-- Making the hitbox a spherecast. This will change every point to be a spherecast.
hitbox:SetCastData({ CastType = "Spherecast", Radius = 10 })

Method 3:
You can use the companion plugin to help automate this process for you.

Tips and Tricks and FAQ

Q1. Why does my hitbox instant-kill even though I set it’s damage to like, one??
A1. Unlike RaycastHitbox, ShapecastHitbox will fire OnHit for every raycast that hits a part. This can have an unfortunate side effect of triggering certain functions repeatedly. This is by design and for performance reasons. You must take this into account when designing your hitboxes. Here is an example code you can use that will filter out targets already hit by former raycasts:

local hitbox = ShapecastHitbox.new(part)
local targetsHit = {}

hitbox:OnHit(function(raycastResult)
     local instance = raycastResult.Instance

     if not targetsHit[instance.Parent] then
          targetsHit[instance.Parent] = true
     else
          return
     end

     local humanoid = instance.Parent:FindFirstChild("Humanoid")

     if humanoid then
          humanoid:TakeDamage(25)
     end
end):HitStart():OnStopped(function(cleanup)
     cleanup()
     table.clear(targetsHit)
end)

Q2. I made one big blockcast cover my entire weapon, but sometimes it’s still not hitting!
A2. Unfortunately, this is where we need to put redundancies. There are many reasons why your hitbox may still not be hitting, even though your sword or whatever is literally inside the enemy. This is due to how fundamentally Raycast, Blockcast, and Spherecasting work. They only detect things upon intersection and at their edge point. If a part has already passed a cast’s intersection part (like its inside a blockcast), then the cast will not detect any hits. I recommend splitting up your singular blockcast into multiple, smaller blockcasts to increase the chances of a hit.

Best practices
  1. You should, ideally, always use ShapecastHitbox on the client for best performance, accuracy, and responsiveness. My examples uses server-sided ShapecastHitbox, but I actually don’t recommend doing it this way as the server’s update rate is much slower than a client. This can lead to inaccuracies. Sometimes it can’t be helped (such as a server-sided enemy) so take care to increase the hitbox size on the server to compensate for lag (or use lag compensation methods).
  1. When using callback methods like :OnHit, :BeforeStart, :OnStopped, or :OnUpdate (anything with a colon prefix), avoid calling them repeatedly in a loop or within a single block of code. These methods act like event listeners or connections—each time you call them, you’re adding a new listener.

    For example, if you call :OnHit twice in a loop, you’re stacking multiple OnHit callbacks. Every time that event triggers, all stacked callbacks will run, which can cause memory leaks or unexpected behavior.

    If you must use something like :OnHit multiple times inside a loop, make sure to clean up your previous callbacks. Use the cleanCallback method from :OnStopped to reset the listeners before the next loop iteration. This prevents stacking and keeps the behavior predictable.

  1. Always destroy the hitbox when no longer in use. Roblox’s garbage collection system should handle this automatically, but it’s good practice to do this yourself. For example, if your character dies with a sword in hand, you should call hitbox:Destroy() to ensure it’s done gracefully.
Update Log

0.2.5: Added Bone and Mesh Deformation support
0.2.4: Bug fixes for hitbox points skipping towards the next update points when stopped and started. Added some checks for when the hitbox is stationary.

You may find the library and their accessories with these links:

ShapecastHitbox
Playground
Plugin
Wally
Github

75 Likes

Very nice to see some high quality resources (unlike recently). I wished this module existed 3 years ago for my game. I still have PTSD from having to edit hundreds of DmgPoint attachments with RaycastHitbox :sob: . Will definitely check this out!

5 Likes

Bless you :pray:

10 Likes

Awesome!
I’ve been waiting for this since the first time you mentioned about it in a RaycastHitbox post

4 Likes

Hi all, I set up a playground place (open sourced too) for those who just wants a quick way to test how the module is.

Also updated the post with more code examples.

2 Likes

Oh! It has released!! I’ve been using a mounted version from the GitHub for a while, I’ll definitely check this out.

2 Likes

hey man, i was using the unfinished repo for this like a week ago. now its out. great stuff! love this module

3 Likes

i might even use this for my game
thank you Phin, you are a legend :heart:

2 Likes

Hi,

  1. On your demo video , why are you ( I think, making a hitbox area , on the back side of the character, instead of just the sword / tool ? )

  1. Also can this be used for projectile weapons?

  2. Is this client side detection or server , or both? How does it handle exploits?

Thanks

1 Like
  1. It is an example of using the plugin. No purpose really otherwise. You can use it on a sword too.
  2. Technically yes, but there are better modules specifically designed for projectiles, so I wouldn’t recommend it. You’d likely need to clone attachments along with parts, which would negate any performance benefits this module offers. In comparison, a dedicated projectile library would have much less overhead.
  3. It’s merely a hitbox detection library, so it can be both on server and client. Security against exploits is up to the skill level and discretion of the developer, and how your game is architecturally built. Treat it as trying to secure touched events.

Exploit protection is a broad topic and I think you can find a lot of resources on that, and you can use the same methods in securing this library. Just know the golden rule; never trust the client and you’ll be fine.

Edit: If you want an example:

You can use ShapecastHitbox on the client. Let the client handle the hit detection. Once they hit an enemy, it’ll send a request to the server saying they hit an enemy.

The server will respond but won’t immediately listen to the client. The server will have to perform sanity checks, to ensure the client isn’t lying. Now, this part varies based on the type of game you have. If you say, have a game where everybody has the same sized sword, then a basic sanity check could be just making sure the client isn’t hitting the enemy further than their sword can hit (so its just impossible!)

So develop your game’s sanity checks in response to how its built, and what makes sense in it. Then any module, including ShapecastHitbox; will be bulletproof from exploits.

4 Likes

Thanks for the above info and your continued opens source shizzle !
Enjoy the day!

2 Likes

u are gods gift to this planet

1 Like

Ooooo, really interesting stuff. Downloading it for raycasting purposes, but this looks like it could be really really useful down the line for all kinds of usecases.

Awwweeesome!

1 Like

Fixed an issue where chaining operations will not properly show the intellisensing; this is now fixed.

1 Like

When I play an animation the hitbox lags behind. Someone please help.

1 Like

Refer to 1. of the Best Practises section in the main post.

If it’s a weapon your player will be using, use it on the client (this will effectively eliminate all lag).

If it’s an enemy on the server-side, implement lag compensation or increase the hitbox size to compensate for the lag. The latter will have a lot of additional work and even current, popular games struggle with this. Unfortunately, that is out of the scope of this module but regardless the methods will still apply.

2 Likes

Hey, I just started using ShapecastHitbox, but it isn’t starting? I made debug print statements in it and only “yes-3” prints, nor “yess-3” and “yesss-3”.

Here’s the code.

-- [[ < Services > ]] --

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

local ServerScriptService = game:GetService("ServerScriptService")

-- [[ < Variables > ]] --

local Main = ReplicatedStorage:FindFirstChild("Main")
local Animations = Main:FindFirstChild("Animations")
local Remote = Main:FindFirstChild("Remote")
local Events = Remote:FindFirstChild("Events")
local UseAbility = Events:FindFirstChild("Use")
local PlayAnimation = Events:FindFirstChild("Play")

local Modules = ServerScriptService:FindFirstChild("Modules")
local ShapecastHitbox = require(Modules:FindFirstChild("ShapecastHitbox"))

local RaycastParams_Coolkid_M1 = RaycastParams.new()
RaycastParams_Coolkid_M1.FilterType = Enum.RaycastFilterType.Blacklist

local Abilities = {
	coolkid = {
		[1] = function(Character)
			print("yes-1")
			RaycastParams_Coolkid_M1.FilterDescendantsInstances = {Character:FindFirstChild("Sword")}
			
			PlayAnimation:FireClient(Players:GetPlayerFromCharacter(Character),"CoolkidM1",Enum.AnimationPriority.Action4)
			
			local hitbox = ShapecastHitbox.new(Character:FindFirstChild("Sword"), RaycastParams_Coolkid_M1)
			local duration = 1 
			print("yes-3")
			hitbox:HitStart(duration):OnUpdate(function()
				print("yess-3")
				if hitbox.RaycastResult then 
					print("yesss-3")
					hitbox:HitStop():Destroy()
					print(hitbox.RaycastResult.Instance.Name)
				end
			end)
			task.delay(1, hitbox.Destroy, hitbox)
		end,
	}
}

UseAbility.OnServerEvent:Connect(function(Player,Number)
	print("yes-2")
	local Character = Player.Character
	local CurrentCharacter = Character:FindFirstChild("Character").Value or "None"
	if CurrentCharacter == "None" then
		Player:Kick()
		return
	end
	print("yess-2")
	Abilities[CurrentCharacter][Number](Character)
end)
1 Like

I’ve been using this for a hot minute now, and I’m not sure if it is a bug or not, but whenever I make a big cube and it doesn’t move, and the hitbox gets created inside a mob, it doesn’t register as a hit until I start moving around, I’m not sure if this is intended or not

1 Like

Yes, this will fall under stationary hitboxes, which is listed as a limitation in the main post.

The reason this happens is because the module casts between the difference of positioning between the current frame and the previous frame. When you are not moving, there is no difference. Therefore the cast length is zero so there would be nothing to detect.

I guess I can try experimenting with a minimum distance vector where itll continually cast towards the last frame position if it hasn’t moved a significant amount, which will probably fix that issue.

Edit: I will try to come up with a solution, stay tuned.

1 Like

First, make sure the sword actually exists, especially if this code is on the client. As of now, the module doesn’t say anything if the instance passed is nil.

Secondly, make sure you have attachments in the sword, either with the name or tag of “DmgPoint”.

The latter prints not printing suggests either sword doesn’t exist or there are no attachments with the specified names are found.

Also suggest turning on debug mode. If there are no red lines or shapes, then it’ll confirm the problems.

1 Like