From e6c3d11bfb49541ea0442249afcc7d154e605efb Mon Sep 17 00:00:00 2001 From: Jackie Date: Thu, 1 Jul 2021 19:52:07 -0400 Subject: [PATCH] Version 4.0 --- README.md | 2 +- RaycastHitbox.lua | 170 --------- RaycastHitboxV4/HitboxCaster.lua | 325 ++++++++++++++++++ .../Tools => RaycastHitboxV4}/Signal.lua | 6 +- RaycastHitboxV4/Solvers/Attachment.lua | 29 ++ RaycastHitboxV4/Solvers/Bone.lua | 38 ++ RaycastHitboxV4/Solvers/LinkAttachments.lua | 24 ++ RaycastHitboxV4/Solvers/Vector3.lua | 38 ++ RaycastHitboxV4/VisualizerCache.lua | 84 +++++ RaycastHitboxV4/init.lua | 221 ++++++++++++ raycast-src/CastLogics/CastAttachment.lua | 19 - raycast-src/CastLogics/CastLinkAttachment.lua | 16 - raycast-src/CastLogics/CastVectorPoint.lua | 21 -- raycast-src/CastLogics/Debug/Debugger.lua | 17 - raycast-src/HitboxObject.lua | 177 ---------- raycast-src/MainHandler.lua | 86 ----- 16 files changed, 763 insertions(+), 510 deletions(-) delete mode 100644 RaycastHitbox.lua create mode 100644 RaycastHitboxV4/HitboxCaster.lua rename {raycast-src/Tools => RaycastHitboxV4}/Signal.lua (85%) create mode 100644 RaycastHitboxV4/Solvers/Attachment.lua create mode 100644 RaycastHitboxV4/Solvers/Bone.lua create mode 100644 RaycastHitboxV4/Solvers/LinkAttachments.lua create mode 100644 RaycastHitboxV4/Solvers/Vector3.lua create mode 100644 RaycastHitboxV4/VisualizerCache.lua create mode 100644 RaycastHitboxV4/init.lua delete mode 100644 raycast-src/CastLogics/CastAttachment.lua delete mode 100644 raycast-src/CastLogics/CastLinkAttachment.lua delete mode 100644 raycast-src/CastLogics/CastVectorPoint.lua delete mode 100644 raycast-src/CastLogics/Debug/Debugger.lua delete mode 100644 raycast-src/HitboxObject.lua delete mode 100644 raycast-src/MainHandler.lua diff --git a/README.md b/README.md index d4eace0..ab5cecd 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # raycastHitboxRbxl Requires Roblox Studio and knowledge of its API - Free to use for everybody -The contents of raycast-src should be a descendant of RaycastHitbox.lua for it to work. +Rojo-compliant. ____ Documentation and examples can be found at diff --git a/RaycastHitbox.lua b/RaycastHitbox.lua deleted file mode 100644 index 6c5aa1d..0000000 --- a/RaycastHitbox.lua +++ /dev/null @@ -1,170 +0,0 @@ ---[[ -____________________________________________________________________________________________________________________________________________________________________________ - - Created by Swordphin123 - 2020. If you have any questions, feel free to message me on DevForum. Credits not neccessary but is appreciated. - - [ How To Use - Quick Start Guide ] - - 1. Insert Attachments to places where you want your "hitbox" to be. For swords, I like to have attachments 1 stud apart and strung along the blade. - 2. Name those Attachments "DmgPoint" (so the script knows). You can configure what name the script will look for in the variables below. - 3. Open up a script. As an example, maybe we have a sword welded to the character or as a tool. Require this, and initialize: - - * Example Code - - local Damage = 10 - local Hitbox = RaycastHitbox:Initialize(Character, {Character}) - - Hitbox.OnHit:Connect(function(hit, humanoid) - print(hit.Name) - humanoid:TakeDamage(Damage) - end) - - Hitbox:HitStart() - wait(2) - Hitbox:HitStop() - - 4. Profit. Refer to the API below for more information. - - -____________________________________________________________________________________________________________________________________________________________________________ - - [ RaycastHitBox API ] - - * local RaycastHitbox = require(RaycastHitbox) ---Duh - --- To use, insert this at the top of your scripts or wherever. - - - - * RaycastHitbox:Initialize(Instance model, table ignoreList) - Description - --- Preps the model and recursively finds attachments in it so it knows where to shoot rays out of later. - Arguments - --- Instance model: Model instance (Like your character, a sword model, etc). May support Parts later. - --- table ignoreList: Raycast takes in ignorelists. Heavily recommended to add in a character so it doesn't hurt itself in its confusion. - Returns - Instance HitboxObject - - * RaycastHitbox:Deinitialize(Instance model) - Description - --- Removes references to the attachments and garbage collects values from the original init instance. Great if you are deleting the hitbox soon. - --- The script will attempt to run this function automatically if the model ancestry was changed. - Arguments - --- Instance model: Same model that you initialized with earlier. Will do nothing if model was not initialized. - - * RaycastHitModule:GetHitbox(Instance model) - Description - --- Gets the HitboxObject if it exists. - Returns - --- HitboxObject if found, else nil - - - - - - - - * HitboxObject:DebugMode(boolean true/false) - Description - --- Turn the Hitbox DebugRays on or off during runtime. - Arguments - --- boolean: true for on, false for off. - - * HitboxObject:PartMode(boolean true/false) - Description - --- If true, OnHit will return every hit part (in respect to the hitbox's ignore list), regardless if it's ascendant has a humanoid or not. Defaults false. - --- OnHit will no longer return a humanoid so you will have to check it. Performance may suffer if there are a lot of parts, use only if necessary. - Arguments - --- boolean: true for parts return, false for off. - - * HitboxObject:SetPoints(Instance part, table vectorPoints) - Description - --- Merges existing Hitbox points with new Vector3 values relative to a part position. This part can be a descendent of your original Hitbox model or can be - an entirely different instance that is not related to the hitbox (example: Have a weapon with attachments and you can then add in more vector3 - points without instancing new attachments, great for dynamic hitboxes) - Arguments - --- Instance part: Sets the part that these vectorPoints will move in relation to the part's origin using Vector3ToWorldSpace - --- table vectorPoints: Table of vector3 values. - - * HitboxObject:RemovePoints(Instance part, table vectorPoints) - Description - --- Remove given Vector3 values provided the part was the same as the ones you set in SetPoints - Arguments - --- Instance part: Sets the part that these vectorPoints will be removed from in relation to the part's origin using Vector3ToWorldSpace - --- table vectorPoints: Table of vector3 values. - - * HitboxObject:LinkAttachments(Instance attachment1, Instance attachment2) - Description - --- Set two attachments to be in a link. The Raycast module will raycast between these two points. - Arguments - --- Instance attachment1/attachment2: Attachment objects - - * HitboxObject:UnlinkAttachments(Instance attachment1) - Description - --- Removes the link of an attachment. Only needs the primary attachment (argument 1 of LinkAttachments) to work. Will automatically sever the connection - to the second attachment. - Arguments - --- Instance attachment1: Attachment object - - * HitboxObject:HitStart(seconds) - Description - --- Starts drawing the rays. Will only damage the target once. Call HitStop to reset the target pool so you can damage the same targets again. - If HitStart hits a target(s), OnHit event will be called. - Arguments - --- number seconds: Optional numerical value, the hitbox will automatically turn off after this amount of time has elapsed - - * HitboxObject:HitStop() - Description - --- Stops drawing the rays and resets the target pool. Will do nothing if no rays are being drawn from the initialized model. - - * HitboxObject.OnHit:Connect(returns: Instance part, returns: Instance humanoid, returns: RaycastResults) - Description - --- If HitStart hits a fresh new target, OnHit returns information about the hit target - Arguments - --- Instance part: Returns the part that the rays hit first - --- Instance humanoid: Returns the Humanoid object - --- Instance RaycastResults: Returns information about the last raycast results - - * HitboxObject.OnUpdate:Connect(returns: Vector3 position) - Description - --- This fires every frame, for every point, returning a Vector3 value of its last position in space. Do not use expensive operations in this function. - - -____________________________________________________________________________________________________________________________________________________________________________ - ---]] - -local RaycastHitbox = { - Version = "3.3", - AttachmentName = "DmgPoint", - DebugMode = false, - WarningMessage = false -} - --------- - -local Handler = require(script.MainHandler) -local HitboxClass = require(script.HitboxObject) - -function RaycastHitbox:Initialize(object, ignoreList) - assert(object, "You must provide an object instance.") - - local newHitbox = Handler:check(object) - if not newHitbox then - newHitbox = HitboxClass:new() - newHitbox:config(object, ignoreList) - newHitbox:seekAttachments(RaycastHitbox.AttachmentName, RaycastHitbox.WarningMessage) - newHitbox.debugMode = RaycastHitbox.DebugMode - Handler:add(newHitbox) - end - return newHitbox -end - -function RaycastHitbox:Deinitialize(object) --- Deprecated - Handler:remove(object) -end - -function RaycastHitbox:GetHitbox(object) - return Handler:check(object) -end - -return RaycastHitbox diff --git a/RaycastHitboxV4/HitboxCaster.lua b/RaycastHitboxV4/HitboxCaster.lua new file mode 100644 index 0000000..1be69b1 --- /dev/null +++ b/RaycastHitboxV4/HitboxCaster.lua @@ -0,0 +1,325 @@ +--!nocheck +--- Creates and manages the hitbox class +-- @author Swordphin123 + +-- Instance options +local DEFAULT_ATTACHMENT_INSTANCE: string = "DmgPoint" +local DEFAULT_GROUP_NAME_INSTANCE: string = "Group" + +-- Debug / Test ray visual options +local DEFAULT_DEBUGGER_RAY_DURATION: number = 0.25 + +-- Debug Message options +local DEFAULT_DEBUG_LOGGER_PREFIX: string = "[ Raycast Hitbox V4 ]\n" +local DEFAULT_MISSING_ATTACHMENTS: string = "No attachments found in object: %s. Can be safely ignored if using SetPoints." +local DEFAULT_ATTACH_COUNT_NOTICE: string = "%s attachments found in object: %s." + +-- Hitbox values +local MINIMUM_SECONDS_SCHEDULER: number = 1 / 60 +local DEFAULT_SIMULATION_TYPE: RBXScriptSignal = game:GetService("RunService").Heartbeat + +--- Variable definitions +local CollectionService: CollectionService = game:GetService("CollectionService") +local VisualizerCache = require(script.Parent.VisualizerCache) + +local ActiveHitboxes: {[number]: any} = {} +local Solvers: Instance = script.Parent:WaitForChild("Solvers") + +local Hitbox = {} +Hitbox.__index = Hitbox +Hitbox.__type = "RaycastHitbox" + +Hitbox.CastModes = { + LinkAttachments = 1, + Attachment = 2, + Vector3 = 3, + Bone = 4, +} + +--- Point type +type Point = { + Group: string?, + CastMode: number, + LastPosition: Vector3?, + WorldSpace: Vector3?, + Instances: {[number]: Instance | Vector3} +} + +-- AdornmentData type +type AdornmentData = VisualizerCache.AdornmentData + +--- Activates the raycasts for the hitbox object. +--- The hitbox will automatically stop and restart if the hitbox was already casting. +-- @param optional number parameter to automatically turn off the hitbox after 'n' seconds +function Hitbox:HitStart(seconds: number?) + if self.HitboxActive then + self:HitStop() + end + + if seconds then + self.HitboxStopTime = os.clock() + math.max(MINIMUM_SECONDS_SCHEDULER, seconds) + end + + self.HitboxActive = true +end + +--- Disables the raycasts for the hitbox object, and clears all current hit targets. +--- Also automatically cancels any current time scheduling for the current hitbox. +function Hitbox:HitStop() + self.HitboxActive = false + self.HitboxStopTime = 0 + table.clear(self.HitboxHitList) +end + +--- Queues the hitbox to be destroyed in the next frame +function Hitbox:Destroy() + self.HitboxPendingRemoval = true + self:HitStop() + self.OnHit:Destroy() + self.OnUpdate:Destroy() +end + +--- Searches for attachments for the given instance (if applicable) +function Hitbox:Recalibrate() + local descendants: {[number]: Instance} = self.HitboxObject:GetDescendants() + local attachmentCount: number = 0 + + for _, attachment: any in ipairs(descendants) do + if not attachment:IsA("Attachment") or attachment.Name ~= DEFAULT_ATTACHMENT_INSTANCE then + continue + end + + local group: string? = attachment:GetAttribute(DEFAULT_GROUP_NAME_INSTANCE) + local point: Point = self:_CreatePoint(group, Hitbox.CastModes.Attachment, attachment.WorldPosition) + + table.insert(point.Instances, attachment) + table.insert(self.HitboxRaycastPoints, point) + + attachmentCount += 1 + end + + if self.DebugLog then + print(string.format("%s%s", DEFAULT_DEBUG_LOGGER_PREFIX, + attachmentCount > 0 and string.format(DEFAULT_ATTACH_COUNT_NOTICE, attachmentCount, self.HitboxObject.Name) or + string.format(DEFAULT_MISSING_ATTACHMENTS, self.HitboxObject.Name)) + ) + end +end + +--- Creates a link between two attachments. The module will constantly raycast between these two attachments. +-- @param attachment1 Attachment object (can have a group attribute) +-- @param attachment2 Attachment object +function Hitbox:LinkAttachments(attachment1: Attachment, attachment2: Attachment) + local group: string? = attachment1:GetAttribute(DEFAULT_GROUP_NAME_INSTANCE) + local point: Point = self:_CreatePoint(group, Hitbox.CastModes.LinkAttachments) + + point.Instances[1] = attachment1 + point.Instances[2] = attachment2 + table.insert(self.HitboxRaycastPoints, point) +end + +--- Removes the link of an attachment. Putting one of any of the two original attachments you used in LinkAttachment will automatically sever the other +-- @param attachment +function Hitbox:UnlinkAttachments(attachment: Attachment) + for i = #self.HitboxRaycastPoints, 1, -1 do + if #self.HitboxRaycastPoints[i].Instances >= 2 then + if self.HitboxRaycastPoints[i].Instances[1] == attachment or self.HitboxRaycastPoints[i].Instances[2] == attachment then + table.remove(self.HitboxRaycastPoints, i) + end + end + end +end + +--- Creates raycast points using only vector3 values. +-- @param object BasePart or Bone, the part you want the points to be locally offset from +-- @param table of vector3 values that are in local space relative to the basePart or bone +-- @param optional group string parameter that names the group these points belong to +function Hitbox:SetPoints(object: BasePart | Bone, vectorPoints: {[number]: Vector3}, group: string?) + for _: number, vector: Vector3 in ipairs(vectorPoints) do + local point: Point = self:_CreatePoint(group, Hitbox.CastModes[object:IsA("Bone") and "Bone" or "Vector3"]) + + point.Instances[1] = object + point.Instances[2] = vector + table.insert(self.HitboxRaycastPoints, point) + end +end + +--- Removes raycast points using only vector3 values. Use the same vector3 table from SetPoints +-- @param object BasePart or Bone, the original instance you used for SetPoints +-- @param table of vector values that are in local space relative to the basePart +function Hitbox:RemovePoints(object: BasePart | Bone, vectorPoints: {[number]: Vector3}) + for i = #self.HitboxRaycastPoints, 1, -1 do + local part = (self.HitboxRaycastPoints[i] :: Point).Instances[1] + + if part == object then + local originalVector = (self.HitboxRaycastPoints[i] :: Point).Instances[2] + + for _: number, vector: Vector3 in ipairs(vectorPoints) do + if vector == originalVector :: Vector3 then + table.remove(self.HitboxRaycastPoints, i) + break + end + end + end + end +end + +--- Internal function that returns a point type +-- @param group string name +-- @param castMode numeric enum value +-- @param lastPosition Vector3 value +function Hitbox:_CreatePoint(group: string?, castMode: number, lastPosition: Vector3?): Point + return { + Group = group, + CastMode = castMode, + LastPosition = lastPosition, + WorldSpace = nil, + Instances = {}, + } +end + +--- Internal function that finds an existing hitbox from a given instance +-- @param instance object +function Hitbox:_FindHitbox(object: any) + for _: number, hitbox: any in ipairs(ActiveHitboxes) do + if hitbox.HitboxObject == object then + return hitbox + end + end +end + +--- Runs for the very first time whenever a hitbox is created +--- Do not run this more than once, you may introduce memory leaks if you do so +function Hitbox:_Init() + if not self.HitboxObject then return end + + local tagConnection: RBXScriptConnection + + local function onTagRemoved(instance: Instance) + if instance == self.HitboxObject then + tagConnection:Disconnect() + self:Destroy() + end + end + + self:Recalibrate() + table.insert(ActiveHitboxes, self) + CollectionService:AddTag(self.HitboxObject, self.Tag) + + tagConnection = CollectionService:GetInstanceRemovedSignal(self.Tag):Connect(onTagRemoved) +end + +local function Init() + --- Reserve table sizing for solver tables + local solversCache: {[number]: any} = table.create(#Solvers:GetChildren()) + + DEFAULT_SIMULATION_TYPE:Connect(function(step: number) + --- Iterate through all the hitboxes + for i = #ActiveHitboxes, 1, -1 do + --- Skip this hitbox if the hitbox will be garbage collected this frame + if ActiveHitboxes[i].HitboxPendingRemoval then + setmetatable(ActiveHitboxes[i], nil) + table.remove(ActiveHitboxes, i) + continue + end + + for _: number, point: Point in ipairs(ActiveHitboxes[i].HitboxRaycastPoints) do + --- Reset this point if the hitbox is inactive + if not ActiveHitboxes[i].HitboxActive then + point.LastPosition = nil + continue + end + + --- Calculate rays + local castMode: any = solversCache[point.CastMode] + local origin: Vector3, direction: Vector3 = castMode:Solve(point) + local raycastResult: RaycastResult = workspace:Raycast(origin, direction, ActiveHitboxes[i].RaycastParams) + + --- Draw debug rays + if ActiveHitboxes[i].Visualizer then + local adornmentData: AdornmentData? = VisualizerCache:GetAdornment() + + if adornmentData then + local debugStartPosition: CFrame = castMode:Visualize(point) + adornmentData.Adornment.Length = direction.Magnitude + adornmentData.Adornment.CFrame = debugStartPosition + end + end + + --- Update the current point's position + point.LastPosition = castMode:UpdateToNextPosition(point) + + --- If a ray detected a hit + if raycastResult then + local part: BasePart = raycastResult.Instance + local model: Instance? + local humanoid: Instance? + local target: Instance? + + if ActiveHitboxes[i].DetectionMode == 1 then + model = part:FindFirstAncestorOfClass("Model") + if model then + humanoid = model:FindFirstChildOfClass("Humanoid") + end + target = humanoid + else + target = part + end + + --- Found a target. Fire the OnHit event + if target then + if ActiveHitboxes[i].DetectionMode <= 2 then + if ActiveHitboxes[i].HitboxHitList[target] then + continue + else + ActiveHitboxes[i].HitboxHitList[target] = true + end + end + + ActiveHitboxes[i].OnHit:Fire(part, humanoid, raycastResult, point.Group) + end + end + + --- Hitbox Time scheduler + if ActiveHitboxes[i].HitboxStopTime > 0 then + if ActiveHitboxes[i].HitboxStopTime <= os.clock() then + ActiveHitboxes[i].HitboxStopTime = 0 + ActiveHitboxes[i]:HitStop() + end + end + + --- OnUpdate event that fires every frame for every point + ActiveHitboxes[i].OnUpdate:Fire(point.LastPosition) + end + end + + local adornmentsInUse: number = #VisualizerCache._AdornmentInUse + + --- Iterates through all the debug rays to see if they need to be cached or cleaned up + if adornmentsInUse > 0 then + for i = adornmentsInUse, 1, -1 do + if (os.clock() - VisualizerCache._AdornmentInUse[i].LastUse) >= DEFAULT_DEBUGGER_RAY_DURATION then + local adornment: AdornmentData? = table.remove(VisualizerCache._AdornmentInUse, i) + + if adornment then + VisualizerCache:ReturnAdornment(adornment) + end + end + end + end + end) + + --- Require all solvers + for castMode: string, enum: number in pairs(Hitbox.CastModes) do + local moduleScript: Instance? = Solvers:FindFirstChild(castMode) + + if moduleScript then + local load = require(moduleScript) + solversCache[enum] = load + end + end +end + +Init() + +return Hitbox \ No newline at end of file diff --git a/raycast-src/Tools/Signal.lua b/RaycastHitboxV4/Signal.lua similarity index 85% rename from raycast-src/Tools/Signal.lua rename to RaycastHitboxV4/Signal.lua index 77af723..50e5e68 100644 --- a/raycast-src/Tools/Signal.lua +++ b/RaycastHitboxV4/Signal.lua @@ -12,13 +12,13 @@ function connection:Connect(Listener) end function connection:Fire(...) - if not self[1] then return end - + if not self[1] then return end + local newThread = coroutine.create(self[1]) coroutine.resume(newThread, ...) end -function connection:Delete() +function connection:Destroy() self[1] = nil end diff --git a/RaycastHitboxV4/Solvers/Attachment.lua b/RaycastHitboxV4/Solvers/Attachment.lua new file mode 100644 index 0000000..4d22044 --- /dev/null +++ b/RaycastHitboxV4/Solvers/Attachment.lua @@ -0,0 +1,29 @@ +--!strict +--- Calculates ray origin and directions for attachment-based raycast points +-- @author Swordphin123 + +local solver = {} + +--- Solve direction and length of the ray by comparing current and last frame's positions +-- @param point type +function solver:Solve(point: {[string]: any}): (Vector3, Vector3) + --- If LastPosition is nil (caused by if the hitbox was stopped previously), rewrite its value to the current point position + if not point.LastPosition then + point.LastPosition = point.Instances[1].WorldPosition + end + + local origin: Vector3 = point.LastPosition + local direction: Vector3 = point.Instances[1].WorldPosition - point.LastPosition + + return origin, direction +end + +function solver:UpdateToNextPosition(point: {[string]: any}): Vector3 + return point.Instances[1].WorldPosition +end + +function solver:Visualize(point: {[string]: any}): CFrame + return CFrame.lookAt(point.Instances[1].WorldPosition, point.LastPosition) +end + +return solver \ No newline at end of file diff --git a/RaycastHitboxV4/Solvers/Bone.lua b/RaycastHitboxV4/Solvers/Bone.lua new file mode 100644 index 0000000..87e42fe --- /dev/null +++ b/RaycastHitboxV4/Solvers/Bone.lua @@ -0,0 +1,38 @@ +--!strict +--- Calculates ray origin and directions for vector-based raycast points +-- @author Swordphin123 + +local solver = {} + +local EMPTY_VECTOR: Vector3 = Vector3.new() + +--- Solve direction and length of the ray by comparing current and last frame's positions +-- @param point type +function solver:Solve(point: {[string]: any}): (Vector3, Vector3) + --- Translate localized bone positions to world space values + local originBone: Bone = point.Instances[1] + local vector: Vector3 = point.Instances[2] + local pointToWorldSpace: Vector3 = originBone.TransformedWorldCFrame.Position + vector + + --- If LastPosition is nil (caused by if the hitbox was stopped previously), rewrite its value to the current point position + if not point.LastPosition then + point.LastPosition = pointToWorldSpace + end + + local origin: Vector3 = point.LastPosition + local direction: Vector3 = pointToWorldSpace - (point.LastPosition or EMPTY_VECTOR) + + point.WorldSpace = pointToWorldSpace + + return origin, direction +end + +function solver:UpdateToNextPosition(point: {[string]: any}): Vector3 + return point.WorldSpace +end + +function solver:Visualize(point: {[string]: any}): CFrame + return CFrame.lookAt(point.WorldSpace, point.LastPosition) +end + +return solver \ No newline at end of file diff --git a/RaycastHitboxV4/Solvers/LinkAttachments.lua b/RaycastHitboxV4/Solvers/LinkAttachments.lua new file mode 100644 index 0000000..408acb3 --- /dev/null +++ b/RaycastHitboxV4/Solvers/LinkAttachments.lua @@ -0,0 +1,24 @@ +--!strict +--- Calculates ray origin and directions for attachment-based raycast points +-- @author Swordphin123 + +local solver = {} + +--- Solve direction and length of the ray by comparing both attachment1 and attachment2's positions +-- @param point type +function solver:Solve(point: {[string]: any}): (Vector3, Vector3) + local origin: Vector3 = point.Instances[1].WorldPosition + local direction: Vector3 = point.Instances[2].WorldPosition - point.Instances[1].WorldPosition + + return origin, direction +end + +function solver:UpdateToNextPosition(point: {[string]: any}): Vector3 + return point.Instances[1].WorldPosition +end + +function solver:Visualize(point: {[string]: any}): CFrame + return CFrame.lookAt(point.Instances[1].WorldPosition, point.Instances[2].WorldPosition) +end + +return solver \ No newline at end of file diff --git a/RaycastHitboxV4/Solvers/Vector3.lua b/RaycastHitboxV4/Solvers/Vector3.lua new file mode 100644 index 0000000..e7af38c --- /dev/null +++ b/RaycastHitboxV4/Solvers/Vector3.lua @@ -0,0 +1,38 @@ +--!strict +--- Calculates ray origin and directions for vector-based raycast points +-- @author Swordphin123 + +local solver = {} + +local EMPTY_VECTOR: Vector3 = Vector3.new() + +--- Solve direction and length of the ray by comparing current and last frame's positions +-- @param point type +function solver:Solve(point: {[string]: any}): (Vector3, Vector3) + --- Translate localized Vector3 positions to world space values + local originPart: BasePart = point.Instances[1] + local vector: Vector3 = point.Instances[2] + local pointToWorldSpace: Vector3 = originPart.Position + originPart.CFrame:VectorToWorldSpace(vector) + + --- If LastPosition is nil (caused by if the hitbox was stopped previously), rewrite its value to the current point position + if not point.LastPosition then + point.LastPosition = pointToWorldSpace + end + + local origin: Vector3 = point.LastPosition + local direction: Vector3 = pointToWorldSpace - (point.LastPosition or EMPTY_VECTOR) + + point.WorldSpace = pointToWorldSpace + + return origin, direction +end + +function solver:UpdateToNextPosition(point: {[string]: any}): Vector3 + return point.WorldSpace +end + +function solver:Visualize(point: {[string]: any}): CFrame + return CFrame.lookAt(point.WorldSpace, point.LastPosition) +end + +return solver \ No newline at end of file diff --git a/RaycastHitboxV4/VisualizerCache.lua b/RaycastHitboxV4/VisualizerCache.lua new file mode 100644 index 0000000..d47b96c --- /dev/null +++ b/RaycastHitboxV4/VisualizerCache.lua @@ -0,0 +1,84 @@ +--!strict +--- Cache LineHandleAdornments or create new ones if not in the cache +-- @author Swordphin123 + +-- Debug / Test ray visual options +local DEFAULT_DEBUGGER_RAY_COLOUR: Color3 = Color3.fromRGB(255, 0, 0) +local DEFAULT_DEBUGGER_RAY_WIDTH: number = 4 +local DEFAULT_DEBUGGER_RAY_NAME: string = "_RaycastHitboxDebugLine" +local DEFAULT_FAR_AWAY_CFRAME: CFrame = CFrame.new(0, math.huge, 0) + +local cache = {} +cache.__index = cache +cache.__type = "RaycastHitboxVisualizerCache" +cache._AdornmentInUse = {} +cache._AdornmentInReserve = {} + +--- AdornmentData type +export type AdornmentData = { + Adornment: LineHandleAdornment, + LastUse: number +} + +--- Internal function to create an AdornmentData type +--- Creates a LineHandleAdornment and a timer value +function cache:_CreateAdornment(): AdornmentData + local line: LineHandleAdornment = Instance.new("LineHandleAdornment") + line.Name = DEFAULT_DEBUGGER_RAY_NAME + line.Color3 = DEFAULT_DEBUGGER_RAY_COLOUR + line.Thickness = DEFAULT_DEBUGGER_RAY_WIDTH + + line.Length = 0 + line.CFrame = DEFAULT_FAR_AWAY_CFRAME + + line.Adornee = workspace.Terrain + line.Parent = workspace.Terrain + + return { + Adornment = line, + LastUse = 0 + } +end + +--- Gets an AdornmentData type. Creates one if there isn't one currently available. +function cache:GetAdornment() + if #cache._AdornmentInReserve <= 0 then + --- Create a new LineAdornmentHandle if none are in reserve + local adornment: AdornmentData = cache:_CreateAdornment() + table.insert(cache._AdornmentInReserve, adornment) + end + + local adornment: AdornmentData? = table.remove(cache._AdornmentInReserve, 1) + + if adornment then + adornment.Adornment.Visible = true + adornment.LastUse = os.clock() + table.insert(cache._AdornmentInUse, adornment) + end + + return adornment +end + +--- Returns an AdornmentData back into the cache. +-- @param AdornmentData +function cache:ReturnAdornment(adornment: AdornmentData) + adornment.Adornment.Length = 0 + adornment.Adornment.Visible = false + adornment.Adornment.CFrame = DEFAULT_FAR_AWAY_CFRAME + table.insert(cache._AdornmentInReserve, adornment) +end + +--- Clears the cache in reserve. Should only be used if you want to free up some memory. +--- If you end up turning on the visualizer again for this session, the cache will fill up again. +--- Does not clear adornments that are currently in use. +function cache:Clear() + for i = #cache._AdornmentInReserve, 1, -1 do + if cache._AdornmentInReserve[i].Adornment then + cache._AdornmentInReserve[i].Adornment:Destroy() + end + + table.remove(cache._AdornmentInReserve, i) + end +end + +return cache \ No newline at end of file diff --git a/RaycastHitboxV4/init.lua b/RaycastHitboxV4/init.lua new file mode 100644 index 0000000..05e9a56 --- /dev/null +++ b/RaycastHitboxV4/init.lua @@ -0,0 +1,221 @@ +--!strict +--- Main RaycastModuleV4 2021 +-- @author Swordphin123 + +--[[ +____________________________________________________________________________________________________________________________________________________________________________ + + If you have any questions, feel free to message me on DevForum. Credits not neccessary but is appreciated. + + [ How To Use - Quick Start Guide ] + + 1. Insert Attachments to places where you want your "hitbox" to be. For swords, I like to have attachments 1 stud apart and strung along the blade. + 2. Name those Attachments "DmgPoint" (so the script knows). You can configure what name the script will look for in the variables below. + 3. Open up a script. As an example, maybe we have a sword welded to the character or as a tool. Require this, and initialize: + + * Example Code + + local Damage = 10 + local Hitbox = RaycastHitbox.new(Character) + + Hitbox.OnHit:Connect(function(hit, humanoid) + print(hit.Name) + humanoid:TakeDamage(Damage) + end) + + Hitbox:HitStart() --- Turns on the hitbox + wait(10) --- Waits 10 seconds + Hitbox:HitStop() --- Turns off the hitbox + + 4. Profit. Refer to the API below for more information. + + +____________________________________________________________________________________________________________________________________________________________________________ + + [ RaycastHitBox API ] + + * local RaycastHitbox = require(RaycastHitboxV4) ---Duh + --- To use, insert this at the top of your scripts or wherever. + + + [ FUNCTIONS ] + + * RaycastHitbox.new(Instance model | BasePart | nil) + Description + --- Preps the model and recursively finds attachments in it so it knows where to shoot rays out of later. If a hitbox exists for this + --- object already, it simply returns the same hitbox. + Arguments + --- Instance: (Like your character, a sword model, etc). Can be left nil in case you want an empty Hitbox or use SetPoints later + Returns + Instance HitboxObject + + * RaycastHitModule:GetHitbox(Instance model) + Description + --- Gets the HitboxObject if it exists. + Returns + --- HitboxObject if found, else nil + + + + * HitboxObject:SetPoints(Instance BasePart | Bone, table vectorPoints, string group) + Description + --- Merges existing Hitbox points with new Vector3 values relative to a part/bone position. This part can be a descendent of your original Hitbox model or + can be an entirely different instance that is not related to the hitbox (example: Have a weapon with attachments and you can then add in more vector3 + points without instancing new attachments, great for dynamic hitboxes) + Arguments + --- Instance BasePart | Bone: Sets the part/bone that these vectorPoints will move in relation to the part's origin using Vector3ToWorldSpace + --- table vectorPoints: Table of vector3 values. + --- string group: optional group parameter + + * HitboxObject:RemovePoints(Instance BasePart | Bone, table vectorPoints) + Description + --- Remove given Vector3 values provided the part was the same as the ones you set in SetPoints + Arguments + --- Instance BasePart | Bone: Sets the part that these vectorPoints will be removed from in relation to the part's origin using Vector3ToWorldSpace + --- table vectorPoints: Table of vector3 values. + + * HitboxObject:LinkAttachments(Instance attachment1, Instance attachment2) + Description + --- Set two attachments to be in a link. The Raycast module will raycast between these two points. + Arguments + --- Instance attachment1/attachment2: Attachment objects + + * HitboxObject:UnlinkAttachments(Instance attachment1) + Description + --- Removes the link of an attachment. Only needs the primary attachment (argument 1 of LinkAttachments) to work. Will automatically sever the connection + to the second attachment. + Arguments + --- Instance attachment1: Attachment object + + * HitboxObject:HitStart(seconds) + Description + --- Starts drawing the rays. Will only damage the target once. Call HitStop to reset the target pool so you can damage the same targets again. + If HitStart hits a target(s), OnHit event will be called. + Arguments + --- number seconds: Optional numerical value, the hitbox will automatically turn off after this amount of time has elapsed + + * HitboxObject:HitStop() + Description + --- Stops drawing the rays and resets the target pool. Will do nothing if no rays are being drawn from the initialized model. + + * HitboxObject.OnHit:Connect(returns: Instance part, returns: Instance humanoid, returns: RaycastResults, returns: String group) + Description + --- If HitStart hits a fresh new target, OnHit returns information about the hit target + Arguments + --- Instance part: Returns the part that the rays hit first + --- Instance humanoid: Returns the Humanoid object + --- RaycastResults RaycastResults: Returns information about the last raycast results + --- String group: Returns information on the hitbox's group + + * HitboxObject.OnUpdate:Connect(returns: Vector3 position) + Description + --- This fires every frame, for every point, returning a Vector3 value of its last position in space. Do not use expensive operations in this function. + + + [ PROPERTIES ] + + * HitboxObject.RaycastParams: RaycastParams + Description + --- Takes in a RaycastParams object + + * HitboxObject.Visualizer: boolean + Description + --- Turns on or off the debug rays for this hitbox + + * HitboxObject.DebugLog: boolean + Description + --- Turns on or off output writing for this hitbox + + * HitboxObject.DetectionMode: number [1 - 3] + Description + --- Defaults to 1. Refer to DetectionMode subsection below for more information + + + [ DETECTION MODES ] + + * RaycastHitbox.DetectionMode.Default + Description + --- Checks if a humanoid exists when this hitbox touches a part. The hitbox will not return humanoids it has already hit for the duration + --- the hitbox has been active. + + * RaycastHitbox.DetectionMode.PartMode + Description + --- OnHit will return every hit part (in respect to the hitbox's RaycastParams), regardless if it's ascendant has a humanoid or not. + --- OnHit will no longer return a humanoid so you will have to check it. The hitbox will not return parts it has already hit for the + --- duration the hitbox has been active. + + * RaycastHitbox.DetectionMode.Bypass + Description + --- PERFORMANCE MAY SUFFER IF THERE ARE A LOT OF PARTS. Use only if necessary. + --- Similar to PartMode, the hitbox will return every hit part. Except, it will keep returning parts even if it has already hit them. + --- Warning: If you have multiple raycast or attachment points, each raycast will also call OnHit. Allows you to create your own + --- filter system. + +____________________________________________________________________________________________________________________________________________________________________________ + +--]] + +-- Show where the red lines are going. You can change their colour and width in VisualizerCache +local SHOW_DEBUG_RAY_LINES: boolean = false + +-- Allow RaycastModule to write to the output +local SHOW_OUTPUT_MESSAGES: boolean = true + +-- The tag name. Used for cleanup. +local DEFAULT_COLLECTION_TAG_NAME: string = "_RaycastHitboxV4Managed" + +--- Initialize required modules +local CollectionService: CollectionService = game:GetService("CollectionService") +local HitboxData = require(script.HitboxCaster) +local Signal = require(script.Signal) + +local RaycastHitbox = {} +RaycastHitbox.__index = RaycastHitbox +RaycastHitbox.__type = "RaycastHitboxModule" + +-- Detection mode enums +RaycastHitbox.DetectionMode = { + Default = 1, + PartMode = 2, + Bypass = 3, +} + +--- Creates or finds a hitbox object. Returns an hitbox object +-- @param required object parameter that takes in either a part or a model +function RaycastHitbox.new(object: any?) + local hitbox: any + + if object and CollectionService:HasTag(object, DEFAULT_COLLECTION_TAG_NAME) then + hitbox = HitboxData:_FindHitbox(object) + else + hitbox = setmetatable({ + RaycastParams = nil, + DetectionMode = RaycastHitbox.DetectionMode.Default, + HitboxRaycastPoints = {}, + HitboxPendingRemoval = false, + HitboxStopTime = 0, + HitboxObject = object, + HitboxHitList = {}, + HitboxActive = false, + Visualizer = SHOW_DEBUG_RAY_LINES, + DebugLog = SHOW_OUTPUT_MESSAGES, + OnUpdate = Signal:Create(), + OnHit = Signal:Create(), + Tag = DEFAULT_COLLECTION_TAG_NAME, + }, HitboxData) + + hitbox:_Init() + end + + return hitbox +end + +--- Finds a hitbox object if valid, else return nil +-- @param Object instance +function RaycastHitbox:GetHitbox(object: any?) + if object then + return HitboxData:_FindHitbox(object) + end +end + +return RaycastHitbox \ No newline at end of file diff --git a/raycast-src/CastLogics/CastAttachment.lua b/raycast-src/CastLogics/CastAttachment.lua deleted file mode 100644 index 6be8b25..0000000 --- a/raycast-src/CastLogics/CastAttachment.lua +++ /dev/null @@ -1,19 +0,0 @@ -local Cast = {} -local Debugger = require(script.Parent.Debug.Debugger) - -function Cast:solve(Point, bool) - if not Point.LastPosition then - Point.LastPosition = Point.Attachment.WorldPosition - end - - if bool then - Debugger(Point.Attachment.WorldPosition - Point.LastPosition, CFrame.new(Point.Attachment.WorldPosition, Point.LastPosition)) - end - return Point.LastPosition, Point.Attachment.WorldPosition - Point.LastPosition -end - -function Cast:lastPosition(Point) - Point.LastPosition = Point.Attachment.WorldPosition -end - -return Cast diff --git a/raycast-src/CastLogics/CastLinkAttachment.lua b/raycast-src/CastLogics/CastLinkAttachment.lua deleted file mode 100644 index 344a7c2..0000000 --- a/raycast-src/CastLogics/CastLinkAttachment.lua +++ /dev/null @@ -1,16 +0,0 @@ -local Cast = {} -local Debugger = require(script.Parent.Debug.Debugger) - -function Cast:solve(Point, bool) - if bool then - Debugger(Point.Attachment.WorldPosition - Point.Attachment0.WorldPosition, CFrame.new(Point.Attachment.WorldPosition, Point.Attachment0.WorldPosition)) - end - - return Point.Attachment.WorldPosition, Point.Attachment0.WorldPosition - Point.Attachment.WorldPosition -end - -function Cast:lastPosition(Point) - Point.LastPosition = Point.Attachment.WorldPosition -end - -return Cast diff --git a/raycast-src/CastLogics/CastVectorPoint.lua b/raycast-src/CastLogics/CastVectorPoint.lua deleted file mode 100644 index 0958d09..0000000 --- a/raycast-src/CastLogics/CastVectorPoint.lua +++ /dev/null @@ -1,21 +0,0 @@ -local Cast = {} -local Debugger = require(script.Parent.Debug.Debugger) - -function Cast:solve(Point, bool) - local RelativePartToWorldSpace = Point.RelativePart.Position + Point.RelativePart.CFrame:VectorToWorldSpace(Point.Attachment) - if not Point.LastPosition then - Point.LastPosition = RelativePartToWorldSpace - end - - if bool then - Debugger(RelativePartToWorldSpace - Point.LastPosition, CFrame.new(RelativePartToWorldSpace, Point.LastPosition)) - end - - return Point.LastPosition, RelativePartToWorldSpace - (Point.LastPosition and Point.LastPosition or Vector3.new()), RelativePartToWorldSpace -end - -function Cast:lastPosition(Point, RelativePartToWorldSpace) - Point.LastPosition = RelativePartToWorldSpace -end - -return Cast diff --git a/raycast-src/CastLogics/Debug/Debugger.lua b/raycast-src/CastLogics/Debug/Debugger.lua deleted file mode 100644 index 470659f..0000000 --- a/raycast-src/CastLogics/Debug/Debugger.lua +++ /dev/null @@ -1,17 +0,0 @@ -local Debris = game:GetService("Debris") - -return function(distance, newCFrame) - local beam = Instance.new("Part") - beam.BrickColor = BrickColor.new("Bright red") - beam.Material = Enum.Material.Neon - beam.Anchored = true - beam.CanCollide = false - beam.Name = "RaycastHitboxDebugPart" - - local Dist = (distance).Magnitude - beam.Size = Vector3.new(0.1, 0.1, Dist) - beam.CFrame = newCFrame * CFrame.new(0, 0, -Dist / 2) - - beam.Parent = workspace.Terrain - Debris:AddItem(beam, 1) -end diff --git a/raycast-src/HitboxObject.lua b/raycast-src/HitboxObject.lua deleted file mode 100644 index b7bf199..0000000 --- a/raycast-src/HitboxObject.lua +++ /dev/null @@ -1,177 +0,0 @@ --- [[ Services ]] -local Players = game:GetService("Players") -local CollectionService = game:GetService("CollectionService") - --- [[ Variables ]] -local MAIN = script.Parent - -local CastAttachment = require(MAIN.CastLogics.CastAttachment) -local CastVectorPoint = require(MAIN.CastLogics.CastVectorPoint) -local CastLinkAttach = require(MAIN.CastLogics.CastLinkAttachment) - -local Signal = require(MAIN.Tools.Signal) -local clock = os.clock - - --------- -local HitboxObject = {} -local Hitbox = {} -Hitbox.__index = Hitbox - -function Hitbox:__tostring() - return string.format("Hitbox for instance %s [%s]", self.object.Name, self.object.ClassName) -end - -function HitboxObject:new() - return setmetatable({}, Hitbox) -end - -function Hitbox:config(object, ignoreList) - self.active = false - self.deleted = false - self.partMode = false - self.debugMode = false - self.points = {} - self.targetsHit = {} - self.endTime = 0 - self.OnHit = Signal:Create() - self.OnUpdate = Signal:Create() - self.raycastParams = RaycastParams.new() - self.raycastParams.FilterType = Enum.RaycastFilterType.Blacklist - self.raycastParams.FilterDescendantsInstances = ignoreList or {} - - self.object = object - CollectionService:AddTag(self.object, "RaycastModuleManaged") -end - -function Hitbox:SetPoints(object, vectorPoints, groupName) - if object and (object:IsA("BasePart") or object:IsA("MeshPart") or object:IsA("Attachment")) then - for _, vectors in ipairs(vectorPoints) do - if typeof(vectors) == "Vector3" then - local Point = { - IsAttachment = object:IsA("Attachment"), - RelativePart = object, - Attachment = vectors, - LastPosition = nil, - group = groupName, - solver = CastVectorPoint - } - table.insert(self.points, Point) - end - end - end -end - -function Hitbox:RemovePoints(object, vectorPoints) - if object then - if object:IsA("BasePart") or object:IsA("MeshPart") then --- for some reason it doesn't recognize meshparts unless I add it in - for i = 1, #self.points do - local Point = self.points[i] - for _, vectors in ipairs(vectorPoints) do - if typeof(Point.Attachment) == "Vector3" and Point.Attachment == vectors and Point.RelativePart == object then - self.points[i] = nil - end - end - end - end - end -end - -function Hitbox:LinkAttachments(primaryAttachment, secondaryAttachment) - if primaryAttachment:IsA("Attachment") and secondaryAttachment:IsA("Attachment") then - local group = primaryAttachment:FindFirstChild("Group") - local Point = { - RelativePart = nil, - Attachment = primaryAttachment, - Attachment0 = secondaryAttachment, - LastPosition = nil, - group = group and group.Value, - solver = CastLinkAttach - } - table.insert(self.points, Point) - end -end - -function Hitbox:UnlinkAttachments(primaryAttachment) - for i, Point in ipairs(self.points) do - if Point.Attachment and Point.Attachment == primaryAttachment then - table.remove(self.points, i) - break - end - end -end - -function Hitbox:seekAttachments(attachmentName, canWarn) - if #self.points <= 0 then - table.insert(self.raycastParams.FilterDescendantsInstances, workspace.Terrain) - end - for _, attachment in ipairs(self.object:GetDescendants()) do - if attachment:IsA("Attachment") and attachment.Name == attachmentName then - local group = attachment:FindFirstChild("Group") - local Point = { - Attachment = attachment, - RelativePart = nil, - LastPosition = nil, - group = group and group.Value, - solver = CastAttachment - } - table.insert(self.points, Point) - end - end - - if canWarn then - if #self.points <= 0 then - warn(string.format("\n[[RAYCAST WARNING]]\nNo attachments with the name '%s' were found in %s. No raycasts will be drawn. Can be ignored if you are using SetPoints.", - attachmentName, self.object.Name) - ) - else - print(string.format("\n[[RAYCAST MESSAGE]]\n\nCreated Hitbox for %s - Attachments found: %s", - self.object.Name, #self.points) - ) - end - end -end - -function Hitbox:Destroy() - if self.deleted then return end - if self.OnHit then self.OnHit:Delete() end - if self.OnUpdate then self.OnUpdate:Delete() end - - self.points = nil - self.active = false - self.deleted = true -end - -function Hitbox:HitStart(seconds) - self.active = true - - if seconds then - assert(type(seconds) == "number", "Argument #1 must be a number!") - - local minSeconds = 1 / 60 --- Seconds cannot be under 1/60th - - if seconds <= minSeconds or seconds == math.huge then - seconds = minSeconds - end - - self.endTime = clock() + seconds - end -end - -function Hitbox:HitStop() - if self.deleted then return end - - self.active = false - self.endTime = 0 - table.clear(self.targetsHit) -end - -function Hitbox:PartMode(bool) - self.partMode = bool -end - -function Hitbox:DebugMode(bool) - self.debugMode = bool -end - -return HitboxObject diff --git a/raycast-src/MainHandler.lua b/raycast-src/MainHandler.lua deleted file mode 100644 index 340aa9e..0000000 --- a/raycast-src/MainHandler.lua +++ /dev/null @@ -1,86 +0,0 @@ --- [[ Services ]] -local RunService = game:GetService("RunService") -local CollectionService = game:GetService("CollectionService") - --- [[ Constants ]] -local SYNC_RATE = RunService.Heartbeat -local MAIN = script.Parent - --- [[ Variables ] -local ActiveHitboxes = {} -local Handler = {} -local clock = os.clock - - --------- -function Handler:add(hitboxObject) - assert(typeof(hitboxObject) ~= "Instance", "Make sure you are initializing from the Raycast module, not from this handler.") - table.insert(ActiveHitboxes, hitboxObject) -end - -function Handler:remove(object) - for i in ipairs(ActiveHitboxes) do - if ActiveHitboxes[i].object == object then - ActiveHitboxes[i]:Destroy() - setmetatable(ActiveHitboxes[i], nil) - table.remove(ActiveHitboxes, i) - end - end -end - -function Handler:check(object) - for _, hitbox in ipairs(ActiveHitboxes) do - if hitbox.object == object then - return hitbox - end - end -end - -function OnTagRemoved(object) - Handler:remove(object) -end - -CollectionService:GetInstanceRemovedSignal("RaycastModuleManaged"):Connect(OnTagRemoved) - - --------- -SYNC_RATE:Connect(function() - for Index, Object in ipairs(ActiveHitboxes) do - if Object.deleted then - Handler:remove(Object.object) - else - for _, Point in ipairs(Object.points) do - if not Object.active then - Point.LastPosition = nil - else - local rayStart, rayDir, RelativePointToWorld = Point.solver:solve(Point, Object.debugMode) - local raycastResult = workspace:Raycast(rayStart, rayDir, Object.raycastParams) - Point.solver:lastPosition(Point, RelativePointToWorld) - - if raycastResult then - local hitPart = raycastResult.Instance - local findModel = not Object.partMode and hitPart:FindFirstAncestorOfClass("Model") - local humanoid = findModel and findModel:FindFirstChildOfClass("Humanoid") - local target = humanoid or (Object.partMode and hitPart) - - if target and not Object.targetsHit[target] then - Object.targetsHit[target] = true - Object.OnHit:Fire(hitPart, humanoid, raycastResult, Point.group) - end - end - - if Object.endTime > 0 then - if Object.endTime <= clock() then - Object.endTime = 0 - Object:HitStop() - end - end - - Object.OnUpdate:Fire(Point.LastPosition) - end - end - end - end -end) - -return Handler