From 1d371ac68a008f1dcecc1d3d45bd012a4ac805a7 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 09:21:27 +0000 Subject: [PATCH 01/62] Add .newParallel and .new() feature --- src/FastCast2/init.luau | 145 ++++++++++++++++++++++++---------------- 1 file changed, 89 insertions(+), 56 deletions(-) diff --git a/src/FastCast2/init.luau b/src/FastCast2/init.luau index 44dbca0..635d0bb 100644 --- a/src/FastCast2/init.luau +++ b/src/FastCast2/init.luau @@ -13,7 +13,7 @@ YOU SHOULD ONLY CREATE ONE CASTER PER GUN. YOU SHOULD >>>NEVER<<< CREATE A NEW CASTER EVERY TIME THE GUN NEEDS TO BE FIRED. - A caster (created with FastCast.new()) represents a "gun". + A caster (created with FastCastParallel.new()) represents a "gun". When you consider a gun, you think of stats like accuracy, bullet speed, etc. This is the info a caster stores. -- @@ -31,7 +31,7 @@ Unfortunately, while reliable in terms of saying if something got hit or not, this method alone cannot be used if you wish to implement bullet travel time into a weapon. As a result of that, I made this library - an excellent remedy to this dilemma. - FastCast is intended to be require()'d once in a script, as you can create as many casters as you need with FastCast.new() + FastCastParallel is intended to be require()'d once in a script, as you can create as many casters as you need with FastCastParallel.new() This is generally handy since you can store settings and information in these casters, and even send them out to other scripts via events for use. @@ -39,7 +39,7 @@ Make the caster once, then use the caster to fire your bullets. Do not make a caster for each bullet. --]] --- Mozilla Public License 2.0 (files originally from FastCast) +-- Mozilla Public License 2.0 (files originally from FastCastParallel) --[[ - Modified by: Mawin CK @@ -49,9 +49,9 @@ -- Verison : 0.0.9 --[=[ - @class FastCast + @class FastCastParallel - FastCast is the root class of the module and offers the surface level methods required to make it work. This is the object returned from `require(FastCast)`. + FastCastParallel is the root class of the module and offers the surface level methods required to make it work. This is the object returned from `require(FastCastParallel)`. ]=] -- Services @@ -63,6 +63,7 @@ --local BaseCast = script:WaitForChild("BaseCast") -- Requires +local FastCastEnums = require(script.FastCastEnums) local Signal = require(script:WaitForChild("Signal")) local TypeDef = require(script:WaitForChild("TypeDefinitions")) local DefaultConfigs = require(script:WaitForChild("DefaultConfigs")) @@ -84,14 +85,19 @@ local DEFAULT_CACHE_HOLDER = workspace -- FastCast local FastCast = {} +local FastCastSerial = {} +local FastCastParallel = {} --[[ If true, verbose debug logging will be used, printing detailed information about what's going on during processing to the output. ]] -FastCast.__index = FastCast -FastCast.__type = "FastCast" +FastCastSerial.__index = FastCastSerial +FastCastSerial.__type = "FastCastSerial" + +FastCastParallel.__index = FastCastParallel +FastCastParallel.__type = "FastCastParallel" -- Local functions @@ -203,7 +209,7 @@ end @return FastCastBehavior ]=] -function FastCast.newBehavior(): TypeDef.FastCastBehavior +function FastCastParallel.newBehavior(): TypeDef.FastCastBehavior return deepCopyTable(DefaultConfigs.FastCastBehavior) :: TypeDef.FastCastBehavior end @@ -211,15 +217,15 @@ end --[=[ :::warning - You must [initialize](FastCast#Init) the Caster before using it. Failing to do so will result in nothing happening when attempting to fire! + You must [initialize](FastCastParallel#Init) the Caster before using it. Failing to do so will result in nothing happening when attempting to fire! ::: Contructs a new Caster object. @function new - @within FastCast + @within FastCastParallel @return Caster ]=] -function FastCast.new(): TypeDef.Caster +function FastCastParallel.new(): TypeDef.Caster return setmetatable( { LengthChanged = Signal.new(), @@ -231,14 +237,14 @@ function FastCast.new(): TypeDef.Caster Dispatcher = nil, AlreadyInit = false, } :: any, - FastCast + FastCastParallel ) :: TypeDef.Caster end --[=[ Initializes the Caster with the given parameters. This is required before firing using Raycasts in the Caster or nothing will happen! @method Init - @within FastCast + @within FastCastParallel @param numWorkers number -- The number of worker VMs to create for this Caster. Must be greater than 1. @param newParent Folder -- The Folder in which to place the FastCastVMs Folder @@ -253,7 +259,7 @@ end @param CacheSize number -- The size of the ObjectCache (if enabled) @param CacheHolder Instance -- The Instance in which to place cached objects (if enabled) ]=] -function FastCast:Init( +function FastCastParallel:Init( numWorkers: number, newParent: Folder, newName: string, @@ -350,11 +356,11 @@ end Set the FastCastEventsModule for all BaseCasts created from this Caster. @method SetFastCastEventsModule - @within FastCast + @within FastCastParallel @param moduleScript ModuleScript -- The FastCastEventsModule to set. ]=] -function FastCast:SetFastCastEventsModule(moduleScript: ModuleScript) +function FastCastParallel:SetFastCastEventsModule(moduleScript: ModuleScript) if not self.AlreadyInit then error("Please Init caster") end @@ -366,14 +372,14 @@ end --[=[ Raycasts the Caster with the specified parameters. @method RaycastFire - @within FastCast + @within FastCastParallel @param origin Vector3 -- The origin of the raycast. @param direction Vector3 -- The direction of the raycast. @param velocity Vector3 | number -- The velocity of the raycast. @param BehaviorData FastCastBehavior? -- The behavior data for the raycast. ]=] -function FastCast:RaycastFire( +function FastCastParallel:RaycastFire( origin: Vector3, direction: Vector3, velocity: Vector3 | number, @@ -383,7 +389,7 @@ function FastCast:RaycastFire( error("Please Init caster") end if BehaviorData == nil then - BehaviorData = FastCast.newBehavior() + BehaviorData = FastCastParallel.newBehavior() end -- BABE RAYCAST!!!!! @@ -393,7 +399,7 @@ end --[=[ Blockcasts the Caster with the specified parameters. @method BlockcastFire - @within FastCast + @within FastCastParallel @param origin Vector3 -- The origin of the blockcast. @param Size Vector3 -- The size of the blockcast. @@ -401,7 +407,7 @@ end @param velocity Vector3 | number -- The velocity of the blockcast. @param BehaviorData FastCastBehavior? -- The behavior data for the blockcast. ]=] -function FastCast:BlockcastFire( +function FastCastParallel:BlockcastFire( origin: Vector3, Size: Vector3, direction: Vector3, @@ -412,7 +418,7 @@ function FastCast:BlockcastFire( error("Please Init caster") end if BehaviorData == nil then - BehaviorData = FastCast.newBehavior() + BehaviorData = FastCastParallel.newBehavior() end self.Dispatcher:Dispatch("Blockcast", origin, Size, direction, velocity, BehaviorData) @@ -421,7 +427,7 @@ end --[=[ Spherecasts the Caster with the specified parameters. @method SpherecastFire - @within FastCast + @within FastCastParallel @param origin Vector3 -- The origin of the spherecast. @param Radius number -- The radius of the spherecast. @@ -429,7 +435,7 @@ end @param velocity Vector3 | number -- The velocity of the spherecast. @param BehaviorData FastCastBehavior? -- The behavior data for the spherecast. ]=] -function FastCast:SpherecastFire( +function FastCastParallel:SpherecastFire( origin: Vector3, Radius: number, direction: Vector3, @@ -440,7 +446,7 @@ function FastCast:SpherecastFire( error("Please Init caster") end if BehaviorData == nil then - BehaviorData = FastCast.newBehavior() + BehaviorData = FastCastParallel.newBehavior() end self.Dispatcher:Dispatch("Spherecast", origin, Radius, direction, velocity, BehaviorData) @@ -452,10 +458,10 @@ Gets the velocity of an ActiveCast. @method GetVelocityCast @param cast vaildcast -- The active cast to get the velocity of. - @within FastCast + @within FastCastParallel @return Vector3 -- The current velocity of the ActiveCast. ]=] -function FastCast:GetVelocityCast(cast: vaildcast) +function FastCastParallel:GetVelocityCast(cast: vaildcast) assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") local currentTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] return GetVelocityAtTime( @@ -471,11 +477,11 @@ Gets the acceleration of an ActiveCast. @method GetAccelerationCast @param cast vaildcast -- The active cast to get the acceleration of. - @within FastCast + @within FastCastParallel @return Vector3 -- The current acceleration of the ActiveCast. ]=] -function FastCast:GetAccelerationCast(cast: vaildcast) +function FastCastParallel:GetAccelerationCast(cast: vaildcast) assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") local currentTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] return currentTrajectory.Acceleration @@ -487,10 +493,10 @@ Gets the position of an ActiveCast. @method GetPositionCast @param cast vaildcast -- The active cast to get the position of. - @within FastCast + @within FastCastParallel @return Vector3 -- The current position of the ActiveCast. ]=] -function FastCast:GetPositionCast(cast: vaildcast) +function FastCastParallel:GetPositionCast(cast: vaildcast) assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") local currentTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] return GetPositionAtTime( @@ -508,10 +514,10 @@ Sets the velocity of an ActiveCast to the specified Vector3. @method SetVelocityCast @param cast vaildcast -- The active cast to modify. @param velocity Vector3 -- The new velocity to set. - @within FastCast + @within FastCastParallel ]=] -function FastCast:SetVelocityCast(cast: vaildcast, velocity: Vector3) +function FastCastParallel:SetVelocityCast(cast: vaildcast, velocity: Vector3) assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") ModifyTransformation(cast, velocity, nil, nil) end @@ -523,10 +529,10 @@ Sets the acceleration of an ActiveCast to the specified Vector3. @method SetAccelerationCast @param cast vaildcast -- The active cast to modify. @param acceleration Vector3 -- The new acceleration to set. - @within FastCast + @within FastCastParallel ]=] -function FastCast:SetAccelerationCast(cast: vaildcast, acceleration: Vector3) +function FastCastParallel:SetAccelerationCast(cast: vaildcast, acceleration: Vector3) assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") ModifyTransformation(cast, nil, acceleration, nil) end @@ -537,9 +543,9 @@ end @method SetPositionCast @param cast vaildcast -- The active cast to modify. @param position Vector3 -- The new position to set. - @within FastCast + @within FastCastParallel ]=] -function FastCast:SetPositionCast(cast: vaildcast, position: Vector3) +function FastCastParallel:SetPositionCast(cast: vaildcast, position: Vector3) assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") ModifyTransformation(cast, nil, nil, position) end @@ -551,10 +557,10 @@ Pauses or resumes simulation for an ActiveCast. @method PauseCast @param cast vaildcast -- The active cast to modify. @param value boolean -- Whether to pause (true) or resume (false) the cast. - @within FastCast + @within FastCastParallel ]=] -function FastCast:PauseCast(cast: vaildcast, value: boolean) +function FastCastParallel:PauseCast(cast: vaildcast, value: boolean) assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") cast.StateInfo.Paused = value end @@ -566,10 +572,10 @@ Add position to an ActiveCast with the specified Vector3. @method AddPositionCast @param cast vaildcast -- The active cast to modify. @param position Vector3 -- The new position to add. - @within FastCast + @within FastCastParallel ]=] -function FastCast:AddPositionCast(cast: vaildcast, position: Vector3) +function FastCastParallel:AddPositionCast(cast: vaildcast, position: Vector3) assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") self:SetPositionCast(cast, self:GetPositionCast(cast) + position) end @@ -581,10 +587,10 @@ Add velocity to an ActiveCast with the specified Vector3. @method AddVelocityCast @param cast vaildcast -- The active cast to modify. @param velocity Vector3 -- The new velocity to add. - @within FastCast + @within FastCastParallel ]=] -function FastCast:AddVelocityCast(cast: vaildcast, velocity: Vector3) +function FastCastParallel:AddVelocityCast(cast: vaildcast, velocity: Vector3) assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") self:SetVelocityCast(cast, self:GetVelocityCast(cast) + velocity) end @@ -596,10 +602,10 @@ Add acceleration to an ActiveCast with the specified Vector3. @method AddAccelerationCast @param cast vaildcast -- The active cast to modify. @param acceleration Vector3 -- The new acceleration to add. - @within FastCast + @within FastCastParallel ]=] -function FastCast:AddAccelerationCast(cast: vaildcast, acceleration: Vector3) +function FastCastParallel:AddAccelerationCast(cast: vaildcast, acceleration: Vector3) assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") self:SetAccelerationCast(cast, self:GetAccelerationCast(cast) + acceleration) end @@ -610,10 +616,10 @@ Synchronize new changes to the ActiveCast. @method SyncChangesToCast @param cast vaildcast -- The active cast to synchronize. - @within FastCast + @within FastCastParallel ]=] -function FastCast:SyncChangesToCast(cast: vaildcast) +function FastCastParallel:SyncChangesToCast(cast: vaildcast) assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") cast.Caster.SyncChange:Fire(cast) end @@ -623,9 +629,9 @@ end @method TerminateCast @param cast vaildcast -- The active cast to terminate. @param castTerminatingFunction (cast: vaildcast) -> ())? -- Optional callback invoked just before the cast is terminated. - @within FastCast + @within FastCastParallel ]=] -function FastCast:TerminateCast(cast: vaildcast, castTerminatingFunction: TypeDef.OnCastTerminatingFunction?) +function FastCastParallel:TerminateCast(cast: vaildcast, castTerminatingFunction: TypeDef.OnCastTerminatingFunction?) assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") local trajectories = cast.StateInfo.Trajectories @@ -657,11 +663,11 @@ end --[=[ Sets whether BulkMoveTo is enabled for this Caster. @method SetBulkMoveEnabled - @within FastCast + @within FastCastParallel @param enabled boolean ]=] -function FastCast:SetBulkMoveEnabled(enabled: boolean) +function FastCastParallel:SetBulkMoveEnabled(enabled: boolean) if not self.AlreadyInit or not self.Dispatcher then warn("Caster not initialized", self) end @@ -672,13 +678,13 @@ end --[=[ Sets whether ObjectCache is enabled for this Caster. - It is recommended to interface with this via [`FastCast:Init()`](FastCast#Init) instead. + It is recommended to interface with this via [`FastCastParallel:Init()`](FastCastParallel#Init) instead. @method SetObjectCacheEnabled - @within FastCast + @within FastCastParallel @param enabled boolean ]=] -function FastCast:SetObjectCacheEnabled( +function FastCastParallel:SetObjectCacheEnabled( enabled: boolean, Template: BasePart | Model, CacheSize: number, @@ -715,9 +721,9 @@ end --[=[ Destroy's a Caster, cleaning up all resources used by it. @method Destroy - @within FastCast + @within FastCastParallel ]=] -function FastCast:Destroy() +function FastCastParallel:Destroy() if self.ObjectCache then self.ObjectCache:Destroy() end @@ -733,5 +739,32 @@ function FastCast:Destroy() setmetatable(self, nil) end +-- Constructors + +function FastCast.new() + local fs = { + LengthChanged = Signal.new(), + Hit = Signal.new(), + Pierced = Signal.new(), + CastTerminating = Signal.new(), + CastFire = Signal.new(), + WorldRoot = workspace, + } + setmetatable(fs, FastCastSerial) +end + +function FastCast.newParallel() + local fp = { + LengthChanged = Signal.new(), + Hit = Signal.new(), + Pierced = Signal.new(), + CastTerminating = Signal.new(), + CastFire = Signal.new(), + WorldRoot = workspace, + Dispatcher = nil, + AlreadyInit = false + } + setmetatable(fp, FastCastParallel) +end return FastCast \ No newline at end of file From 945b5f89ff37bbdb0807201f5c184d2a29bef848 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 09:40:03 +0000 Subject: [PATCH 02/62] Remove FastCastParallel.new and add docs to FastCast.new/newParallel --- src/FastCast2/init.luau | 54 ++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/FastCast2/init.luau b/src/FastCast2/init.luau index 635d0bb..5e44e12 100644 --- a/src/FastCast2/init.luau +++ b/src/FastCast2/init.luau @@ -214,33 +214,6 @@ function FastCastParallel.newBehavior(): TypeDef.FastCastBehavior end ---[=[ - :::warning - - You must [initialize](FastCastParallel#Init) the Caster before using it. Failing to do so will result in nothing happening when attempting to fire! - - ::: - Contructs a new Caster object. - @function new - @within FastCastParallel - @return Caster -]=] -function FastCastParallel.new(): TypeDef.Caster - return setmetatable( - { - LengthChanged = Signal.new(), - Hit = Signal.new(), - Pierced = Signal.new(), - CastTerminating = Signal.new(), - CastFire = Signal.new(), - WorldRoot = workspace, - Dispatcher = nil, - AlreadyInit = false, - } :: any, - FastCastParallel - ) :: TypeDef.Caster -end - --[=[ Initializes the Caster with the given parameters. This is required before firing using Raycasts in the Caster or nothing will happen! @method Init @@ -741,6 +714,19 @@ end -- Constructors +--[=[ + Creates a new Serial Caster. A Serial Caster runs all cast simulations on the main thread + and is simpler to use but less performant than [FastCast.newParallel](FastCast#newParallel). + + :::tip + For most use cases, especially when you need high performance, consider using [FastCast.newParallel](FastCast#newParallel) instead. + ::: + + @function new + @within FastCast + + @return Caster +]=] function FastCast.new() local fs = { LengthChanged = Signal.new(), @@ -753,6 +739,20 @@ function FastCast.new() setmetatable(fs, FastCastSerial) end +--[=[ + Creates a new Parallel Caster. A Parallel Caster runs cast simulations on separate worker VMs + in parallel, providing better performance for high-frequency raycasting scenarios. + + :::warning + You must [initialize](FastCastParallel#Init) the Parallel Caster before using it! + Failing to do so will result in nothing happening when attempting to fire! + ::: + + @function newParallel + @within FastCast + + @return Caster +]=] function FastCast.newParallel() local fp = { LengthChanged = Signal.new(), From 14d9753c9ef0843a987b936f85d501d1aa8ab7e5 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 09:58:11 +0000 Subject: [PATCH 03/62] Update TODO.md --- TODO.md | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.md b/TODO.md index ef307dd..b79c7e3 100644 --- a/TODO.md +++ b/TODO.md @@ -6,3 +6,4 @@ - [ ] Update documentation - [ ] Add benchmarks - [ ] Refactor +- [ ] Fix HighFidelityBehavior = 2 bug, where projectile passes through walls \ No newline at end of file From 8c01cc584395ee82858de7e5d155d0734726fcde Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 10:06:43 +0000 Subject: [PATCH 04/62] feat: Add ActiveCastSerial for main thread simulation --- src/FastCast2/ActiveCastSerial.luau | 773 ++++++++++++++++++++++++++++ 1 file changed, 773 insertions(+) create mode 100644 src/FastCast2/ActiveCastSerial.luau diff --git a/src/FastCast2/ActiveCastSerial.luau b/src/FastCast2/ActiveCastSerial.luau new file mode 100644 index 0000000..0df98b3 --- /dev/null +++ b/src/FastCast2/ActiveCastSerial.luau @@ -0,0 +1,773 @@ +-- Mozilla Public License 2.0 +--[[ + - Author : Mawin CK + - Date : 2025 + -- Version : 0.0.9 +]] + +local RS = game:GetService("RunService") + +local FastCastModule = script.Parent +local FastCast = require(FastCastModule) +local TypeDef = require(FastCastModule:WaitForChild("TypeDefinitions")) +local Configs = require(FastCastModule:WaitForChild("Configs")) +local DebugLogging = Configs.DebugLogging +local FastCastEnums = require(FastCastModule:WaitForChild("FastCastEnums")) + +local FC_VIS_OBJ_NAME = "FastCastVisualizationObjects" +local MAX_SEGMENT_CAL_TIME = 0.016 * 5 +local MAX_CASTING_TIME = 0.2 +local DEFAULT_MAX_DISTANCE = 1000 + +local EnumCastTypes = FastCastEnums.CastType + +local DBG_SEGMENT_SUB_COLOR = Color3.new(0.286275, 0.329412, 0.247059) +local DBG_SEGMENT_SUB_COLOR2 = Color3.new(0.454902, 0.933333, 0.011765) +local DBG_HIT_SUB_COLOR = Color3.new(0.0588235, 0.87451, 1) +local DBG_RAYPIERCE_SUB_COLOR = Color3.new(1, 0.113725, 0.588235) + +type vaildcast = TypeDef.ActiveCastData | TypeDef.ActiveBlockcastData | TypeDef.ActiveSpherecastData + +type BlockcastVariant = { CastType: number, Size: Vector3 } +type SpherecastVariant = { CastType: number, Radius: number } +type CastVariants = BlockcastVariant | SpherecastVariant + +type RayVisualizerVariant = { castLength: number } +type BlockVisualizerVariant = { size: Vector3 } +type SphereVisualizerVariant = { radius: number } +type CastVisualizerVariants = RayVisualizerVariant | BlockVisualizerVariant | SphereVisualizerVariant + +type CastHandler = (WorldRoot: WorldRoot, origin: Vector3, direction: Vector3, castVariant: CastVariants) -> RaycastResult + +local HIGH_FIDE_INCREASE_SIZE = 0.5 + +local CastVariantTypes = { + [EnumCastTypes.Raycast] = "Raycast", + [EnumCastTypes.Blockcast] = "Blockcast", + [EnumCastTypes.Spherecast] = "Spherecast" +} + +local castHandlers = { + [EnumCastTypes.Raycast] = function( + targetWorldRoot: WorldRoot, + origin: Vector3, + direction: Vector3, + parameters: RaycastParams + ) + return targetWorldRoot:Raycast(origin, direction, parameters) + end, + [EnumCastTypes.Blockcast] = function( + targetWorldRoot: WorldRoot, + origin: Vector3, + direction: Vector3, + parameters: RaycastParams, + variant: BlockcastVariant + ) + return targetWorldRoot:Blockcast(CFrame.new(origin), variant.Size, direction, parameters) + end, + [EnumCastTypes.Spherecast] = function( + targetWorldRoot: WorldRoot, + origin: Vector3, + direction: Vector3, + parameters: RaycastParams, + variant: SpherecastVariant + ) + return targetWorldRoot:Spherecast(origin, variant.Radius, direction, parameters) + end +} + +--[=[ + @class ActiveCastSerial + + ActiveCast for serial (non-parallel) mode. Runs simulation directly on main thread. +]=] + +local ActiveCastSerial = {} + +local function DebrisAdd(obj: Instance, Lifetime: number) + if not obj then return end + if Lifetime <= 0 then + obj:Destroy() + end + task.delay(Lifetime, function() + obj:Destroy() + end) +end + +local function GetPositionAtTime(t: number, origin: Vector3, initialVelocity: Vector3, acceleration: Vector3): Vector3 + local force = Vector3.new( + (acceleration.X * t ^ 2) / 2, + (acceleration.Y * t ^ 2) / 2, + (acceleration.Z * t ^ 2) / 2 + ) + return origin + (initialVelocity * t) + force +end + +local function GetVelocityAtTime(time: number, initialVelocity: Vector3, acceleration: Vector3): Vector3 + return initialVelocity + acceleration * time +end + +local function CloneCastParams(params: RaycastParams): RaycastParams + local clone: RaycastParams = RaycastParams.new() + clone.CollisionGroup = params.CollisionGroup + clone.FilterType = params.FilterType + clone.FilterDescendantsInstances = params.FilterDescendantsInstances + clone.IgnoreWater = params.IgnoreWater + return clone +end + +local function GetFastCastVisualizationContainer(): Instance + local fcVisualizationObjects = workspace.Terrain:FindFirstChild(FC_VIS_OBJ_NAME) + if fcVisualizationObjects then + return fcVisualizationObjects + end + fcVisualizationObjects = Instance.new("Folder") + fcVisualizationObjects.Name = FC_VIS_OBJ_NAME + fcVisualizationObjects.Archivable = false + fcVisualizationObjects.Parent = workspace.Terrain + return fcVisualizationObjects +end + +local function DbgVisualizeRaySegment( + castStartCFrame: CFrame, + VisualizeCasts: boolean, + VisualizeCastSettings: TypeDef.VisualizeCastSettings, + variant: RayVisualizerVariant +): ConeHandleAdornment? + if not VisualizeCasts then return end + local adornment = Instance.new("ConeHandleAdornment") + adornment.Adornee = workspace.Terrain + adornment.CFrame = castStartCFrame + adornment.Height = variant.castLength + adornment.Color3 = VisualizeCastSettings.Debug_SegmentColor + adornment.Radius = VisualizeCastSettings.Debug_SegmentSize + adornment.Transparency = VisualizeCastSettings.Debug_SegmentTransparency + adornment.Parent = GetFastCastVisualizationContainer() + DebrisAdd(adornment, VisualizeCastSettings.Debug_RayLifetime) + return adornment +end + +local function DbgVisualizeBlockSegment( + castStartCFrame: CFrame, + VisualizeCasts: boolean, + VisualizeCastSettings: TypeDef.VisualizeCastSettings, + variant: BlockVisualizerVariant +): BoxHandleAdornment? + if not VisualizeCasts then return end + local adornment = Instance.new("BoxHandleAdornment") + adornment.Adornee = workspace.Terrain + adornment.CFrame = castStartCFrame + adornment.Size = variant.size + adornment.Color3 = VisualizeCastSettings.Debug_SegmentColor + adornment.Transparency = VisualizeCastSettings.Debug_SegmentTransparency + adornment.Parent = GetFastCastVisualizationContainer() + DebrisAdd(adornment, VisualizeCastSettings.Debug_RayLifetime) + return adornment +end + +local function DbgVisualizeSphereSegment( + castStartCFrame: CFrame, + VisualizeCasts: boolean, + VisualizeCastSettings: TypeDef.VisualizeCastSettings, + variant: SphereVisualizerVariant +): SphereHandleAdornment? + if not VisualizeCasts then return end + local adornment = Instance.new("SphereHandleAdornment") + adornment.Adornee = workspace.Terrain + adornment.CFrame = castStartCFrame + adornment.Radius = variant.radius + adornment.Color3 = VisualizeCastSettings.Debug_SegmentColor + adornment.Transparency = VisualizeCastSettings.Debug_SegmentTransparency + adornment.Parent = GetFastCastVisualizationContainer() + DebrisAdd(adornment, VisualizeCastSettings.Debug_RayLifetime) + return adornment +end + +local function DbgVisualizeHit( + atCF: CFrame, + wasPierce: boolean, + VisualizeCasts: boolean, + VisualizeCastSettings: TypeDef.VisualizeCastSettings +): SphereHandleAdornment? + if not VisualizeCasts then return end + local adornment = Instance.new("SphereHandleAdornment") + adornment.Adornee = workspace.Terrain + adornment.CFrame = atCF + adornment.Radius = (wasPierce == false) and VisualizeCastSettings.Debug_HitSize or VisualizeCastSettings.Debug_RayPierceSize + adornment.Transparency = (wasPierce == false) and VisualizeCastSettings.Debug_HitTransparency or VisualizeCastSettings.Debug_RayPierceTransparency + adornment.Color3 = (wasPierce == false) and VisualizeCastSettings.Debug_HitColor or VisualizeCastSettings.Debug_RayPierceColor + adornment.Parent = GetFastCastVisualizationContainer() + DebrisAdd(adornment, VisualizeCastSettings.Debug_HitLifetime) + return adornment +end + +local Visualizers = { + [EnumCastTypes.Raycast] = DbgVisualizeRaySegment, + [EnumCastTypes.Blockcast] = DbgVisualizeBlockSegment, + [EnumCastTypes.Spherecast] = DbgVisualizeSphereSegment +} + +local function SendHit( + cast: vaildcast, + resultOfCast: RaycastResult, + segmentVelocity: Vector3, + cosmeticBulletObject: Instance? +) + local FastCastEventsConfig = cast.StateInfo.FastCastEventsConfig + if FastCastEventsConfig and FastCastEventsConfig.UseHit == false then return end + cast.Caster.Output:Fire("Hit", cast, resultOfCast, segmentVelocity, cosmeticBulletObject) +end + +local function SendPierced( + cast: vaildcast, + resultOfCast: RaycastResult, + segmentVelocity: Vector3, + cosmeticBulletObject: Instance? +) + local FastCastEventsConfig = cast.StateInfo.FastCastEventsConfig + if FastCastEventsConfig and FastCastEventsConfig.UsePierced == false then return end + cast.Caster.Output:Fire("Pierced", cast, resultOfCast, segmentVelocity, cosmeticBulletObject) +end + +local function SendLengthChanged( + cast: vaildcast, + lastPoint: Vector3, + rayDir: Vector3, + rayDisplacement: number, + segmentVelocity: Vector3, + cosmeticBulletObject: Instance? +) + local FastCastEventsConfig = cast.StateInfo.FastCastEventsConfig + if FastCastEventsConfig and FastCastEventsConfig.UseLengthChanged == false then return end + cast.Caster.Output:Fire( + "LengthChanged", + cast, + lastPoint, + rayDir, + rayDisplacement, + segmentVelocity, + cosmeticBulletObject + ) +end + +local function SimulateCast( + cast: any, + delta: number, + FastCastEvents: TypeDef.FastCastEvents, + variant: CastVariants +) + assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") + + if DebugLogging.Casting then + print("Casting for frame.") + end + + local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + local origin = latestTrajectory.Origin + local totalDelta = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime + local initialVelocity = latestTrajectory.InitialVelocity + local acceleration = latestTrajectory.Acceleration + + local lastPoint = GetPositionAtTime(totalDelta, origin, initialVelocity, acceleration) + local lastDelta = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime + + cast.StateInfo.TotalRuntime += delta + totalDelta = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime + + local currentTarget = GetPositionAtTime(totalDelta, origin, initialVelocity, acceleration) + local segmentVelocity = GetVelocityAtTime(totalDelta, initialVelocity, acceleration) + local totalDisplacement = currentTarget - lastPoint + local rayDir = totalDisplacement.Unit * segmentVelocity.Magnitude * delta + + local CastType = variant.CastType + local targetWorldRoot = cast.RayInfo.WorldRoot + + local CastHandler = castHandlers[CastType] + local Visualizer = Visualizers[CastType] + + local resultOfCast = CastHandler(targetWorldRoot, lastPoint, rayDir, cast.RayInfo.Parameters, variant) + + local point = currentTarget + local part: Instance? = nil + + if resultOfCast ~= nil then + point = resultOfCast.Position + part = resultOfCast.Instance + end + + local rayDisplacement = (point - lastPoint).Magnitude + + local VisualizeCasts = cast.StateInfo.VisualizeCasts + local VisualizeCastSettings = cast.StateInfo.VisualizeCastSettings + local FastCastEventsModuleConfig = cast.StateInfo.FastCastEventsModuleConfig + + if typeof(latestTrajectory.Acceleration) ~= "Vector3" then + latestTrajectory.Acceleration = Vector3.new() + end + + local VisualizeVariant = {} + if CastType == EnumCastTypes.Raycast then + VisualizeVariant.castLength = rayDisplacement + elseif CastType == EnumCastTypes.Blockcast then + VisualizeVariant.size = cast.RayInfo.Size + elseif CastType == EnumCastTypes.Spherecast then + VisualizeVariant.radius = cast.RayInfo.Radius + end + + cast.CFrame = CFrame.new(lastPoint, lastPoint + rayDir) * CFrame.new(0, 0, -rayDisplacement / 2) + + local LengthChangedfn: TypeDef.OnLengthChangedFunction? = nil + local canPierceCheckfn: TypeDef.CanPierceFunction? = nil + local castTerminatingfn: TypeDef.OnCastTerminatingFunction? = nil + local Hitfn: TypeDef.OnHitFunction? = nil + local Piercedfn: TypeDef.OnPiercedFunction? = nil + + if FastCastEvents then + canPierceCheckfn = FastCastEventsModuleConfig.UseCanPierce and FastCastEvents.CanPierce or nil + castTerminatingfn = FastCastEventsModuleConfig.UseCastTerminating and FastCastEvents.CastTerminating or nil + Hitfn = FastCastEventsModuleConfig.UseHit and FastCastEvents.Hit or nil + Piercedfn = FastCastEventsModuleConfig.UsePierced and FastCastEvents.Pierced or nil + LengthChangedfn = FastCastEventsModuleConfig.UseLengthChanged and FastCastEvents.LengthChanged or nil + end + + SendLengthChanged(cast, lastPoint, rayDir.Unit, rayDisplacement, segmentVelocity, cast.RayInfo.CosmeticBulletObject) + + if LengthChangedfn then + LengthChangedfn( + cast, + lastPoint, + rayDir.Unit, + rayDisplacement, + segmentVelocity, + cast.RayInfo.CosmeticBulletObject + ) + end + + cast.StateInfo.DistanceCovered += rayDisplacement + + local rayVisualization = nil + if delta > 0 then + rayVisualization = Visualizer( + CFrame.new(lastPoint, lastPoint + rayDir), + VisualizeCasts, + VisualizeCastSettings, + VisualizeVariant + ) + end + + if part and part ~= cast.RayInfo.CosmeticBulletObject then + if DebugLogging.Hit then + print("Hit something, testing now.") + end + + if DebugLogging.RayPierce and canPierceCheckfn == nil then + print("No piercing function set, proceeding to hit processing.") + end + + if + canPierceCheckfn == nil + or canPierceCheckfn(cast, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject) == false + then + if DebugLogging.RayPierce then + print("Piercing function is nil or it returned FALSE to not pierce this hit.") + end + + cast.StateInfo.IsActivelySimulatingPierce = false + + if + cast.StateInfo.HighFidelityBehavior == FastCastEnums.HighFidelityBehavior.Automatic + and cast.StateInfo.HighFidelitySegmentSize > 0 + then + cast.StateInfo.CancelHighResCast = false + + if cast.StateInfo.IsActivelyResimulating then + FastCast:TerminateCast(cast, castTerminatingfn) + warn("Cascading cast lag encountered!") + return + end + + cast.StateInfo.IsActivelyResimulating = true + + if DebugLogging.Calculation then + print("Hit was registered, but recalculation is on for physics based casts. Recalculating to verify a real hit...") + end + + local numSegmentsDecimal = rayDisplacement / cast.StateInfo.HighFidelitySegmentSize + local numSegmentsReal = math.floor(numSegmentsDecimal) + + if numSegmentsReal == 0 then + numSegmentsReal = 1 + end + + local timeIncrement = delta / numSegmentsReal + + if DebugLogging.Calculation then + print("Performing subcast! Time increment: " .. timeIncrement .. ", num segments: " .. numSegmentsReal) + end + + for segmentIndex = 1, numSegmentsReal do + if cast.StateInfo.CancelHighResCast then + cast.StateInfo.CancelHighResCast = false + break + end + + local subPosition = GetPositionAtTime( + lastDelta + (timeIncrement * segmentIndex), + origin, + initialVelocity, + acceleration + ) + local subVelocity = GetVelocityAtTime(lastDelta + (timeIncrement * segmentIndex), initialVelocity, acceleration) + local subRayDir = subVelocity * delta + local subResult = CastHandler(targetWorldRoot, subPosition, subRayDir, cast.RayInfo.Parameters, variant) + + local subDisplacement = (subPosition - (subPosition + subVelocity)).Magnitude + + if CastType == EnumCastTypes.Raycast then + VisualizeVariant.castLength = subDisplacement + end + + if subResult ~= nil then + subDisplacement = (subPosition - subResult.Position).Magnitude + local dbgSeg = Visualizer( + CFrame.new(subPosition, subPosition + subVelocity), + VisualizeCasts, + VisualizeCastSettings, + VisualizeVariant + ) + if dbgSeg ~= nil then + dbgSeg.Color3 = DBG_SEGMENT_SUB_COLOR + end + + if + canPierceCheckfn == nil + or canPierceCheckfn(cast, subResult, subVelocity, cast.RayInfo.CosmeticBulletObject) == false + then + cast.StateInfo.IsActivelyResimulating = false + SendHit(cast, subResult, subVelocity, cast.RayInfo.CosmeticBulletObject) + if Hitfn then + Hitfn(cast, subResult, subVelocity, cast.RayInfo.CosmeticBulletObject) + end +cast.Caster:TerminateCast(cast, castTerminatingfn) + + local vis = DbgVisualizeHit(CFrame.new(point), false, VisualizeCasts, VisualizeCastSettings) + if vis ~= nil then + vis.Color3 = DBG_HIT_SUB_COLOR + end + return + else + SendPierced(cast, subResult, subVelocity, cast.RayInfo.CosmeticBulletObject) + if Piercedfn then + Piercedfn(cast, subResult, subVelocity, cast.RayInfo.CosmeticBulletObject) + end + + local vis = DbgVisualizeHit(CFrame.new(point), true, VisualizeCasts, VisualizeCastSettings) + if vis ~= nil then + vis.Color3 = DBG_RAYPIERCE_SUB_COLOR + end + end + else + local dbgSeg = Visualizer( + CFrame.new(subPosition, subPosition + subVelocity), + VisualizeCasts, + VisualizeCastSettings, + VisualizeVariant + ) + if dbgSeg ~= nil then + dbgSeg.Color3 = DBG_SEGMENT_SUB_COLOR2 + end + end + + if DebugLogging.Segment then + print("[" .. segmentIndex .. "] Subcast of time increment " .. timeIncrement) + end + end + + cast.StateInfo.IsActivelyResimulating = false + else + if DebugLogging.Hit then + print("Hit was successful. Terminating.") + end + + SendHit(cast, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject) + if Hitfn then + Hitfn(cast, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject) + end + FastCast:TerminateCast(cast, castTerminatingfn) + + DbgVisualizeHit(CFrame.new(point), false, VisualizeCasts, VisualizeCastSettings) + return + end + else + if DebugLogging.RayPierce then + print("Piercing function returned TRUE to pierce this part.") + end + + if rayVisualization ~= nil then + rayVisualization.Color3 = Color3.new(0.4, 0.05, 0.05) + end + DbgVisualizeHit(CFrame.new(point), true, VisualizeCasts, VisualizeCastSettings) + SendPierced(cast, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject) + if Piercedfn then + Piercedfn(cast, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject) + end + end + end + + if cast.StateInfo.DistanceCovered >= cast.RayInfo.MaxDistance then + FastCast:TerminateCast(cast, castTerminatingfn) + DbgVisualizeHit(CFrame.new(currentTarget), false, VisualizeCasts, VisualizeCastSettings) + end +end + +--[=[ + @function createCastData + @within ActiveCastSerial + + Creates a new ActiveCastSerial instance. +]=] +function ActiveCastSerial.createCastData( + BaseCast: TypeDef.BaseCastData, + activeCastID: number, + origin: Vector3, + direction: Vector3, + velocity: Vector3 | number, + behavior: TypeDef.FastCastBehavior, + eventModule: TypeDef.FastCastEventsModule?, + variant: CastVariants +): vaildcast + if typeof(velocity) == "number" then + velocity = direction.Unit * velocity + end + + if behavior.HighFidelitySegmentSize <= 0 then + error("Cannot set FastCastBehavior.HighFidelitySegmentSize <= 0!", 0) + end + + if behavior.HighFidelityBehavior <= 0 then + behavior.HighFidelityBehavior = 1 + elseif behavior.HighFidelityBehavior >= 4 then + behavior.HighFidelityBehavior = 3 + end + + local cast = { + Caster = BaseCast, + StateInfo = { + UpdateConnection = nil, + Paused = false, + TotalRuntime = 0, + DistanceCovered = 0, + HighFidelitySegmentSize = behavior.HighFidelitySegmentSize, + HighFidelityBehavior = behavior.HighFidelityBehavior, + IsActivelySimulatingPierce = false, + IsActivelyResimulating = false, + CancelHighResCast = false, + Trajectories = { + { + StartTime = 0, + EndTime = -1, + Origin = origin, + InitialVelocity = velocity, + Acceleration = behavior.Acceleration, + }, + }, + VisualizeCasts = behavior.VisualizeCasts, + VisualizeCastSettings = behavior.VisualizeCastSettings, + FastCastEventsModuleConfig = { + UseLengthChanged = behavior.FastCastEventsModuleConfig.UseLengthChanged, + UseHit = behavior.FastCastEventsModuleConfig.UseHit, + UsePierced = behavior.FastCastEventsModuleConfig.UsePierced, + UseCastTerminating = behavior.FastCastEventsModuleConfig.UseCastTerminating, + UseCanPierce = behavior.FastCastEventsModuleConfig.UseCanPierce, + }, + FastCastEventsConfig = { + UseLengthChanged = behavior.FastCastEventsConfig.UseLengthChanged, + UseHit = behavior.FastCastEventsConfig.UseHit, + UsePierced = behavior.FastCastEventsConfig.UsePierced, + UseCastTerminating = behavior.FastCastEventsConfig.UseCastTerminating, + }, + }, + RayInfo = { + Parameters = behavior.RaycastParams, + WorldRoot = workspace, + MaxDistance = behavior.MaxDistance or DEFAULT_MAX_DISTANCE, + CosmeticBulletObject = behavior.CosmeticBulletTemplate, + FastCastEventsModule = eventModule + }, + UserData = {}, + Type = CastVariantTypes[variant.CastType], + CFrame = CFrame.new(origin) :: CFrame, + ID = activeCastID + } :: any + + if variant.CastType == EnumCastTypes.Blockcast then + cast.RayInfo.Size = (variant :: BlockcastVariant).Size + elseif variant.CastType == EnumCastTypes.Spherecast then + cast.RayInfo.Radius = (variant :: SpherecastVariant).Radius + end + + if behavior.UserData then + cast.UserData = behavior.UserData + end + + if cast.RayInfo.Parameters ~= nil then + cast.RayInfo.Parameters = CloneCastParams(cast.RayInfo.Parameters) + else + cast.RayInfo.Parameters = RaycastParams.new() + end + + local targetContainer: Instance? + if cast.Caster.ObjectCache then + cast.RayInfo.CosmeticBulletObject = cast.Caster.ObjectCache:Invoke(CFrame.new(origin, origin + direction)) + targetContainer = cast.Caster.CacheHolder + else + if cast.RayInfo.CosmeticBulletObject ~= nil then + local basePart = cast.RayInfo.CosmeticBulletObject + basePart = basePart:Clone() + basePart.CFrame = CFrame.new(origin, origin + direction) + basePart.Parent = behavior.CosmeticBulletContainer + cast.RayInfo.CosmeticBulletObject = basePart + end + + if behavior.CosmeticBulletContainer then + targetContainer = behavior.CosmeticBulletContainer + end + end + + if behavior.AutoIgnoreContainer == true and targetContainer ~= nil then + local ignoreList = cast.RayInfo.Parameters.FilterDescendantsInstances + if not table.find(ignoreList, targetContainer) then + table.insert(ignoreList, targetContainer) + cast.RayInfo.Parameters.FilterDescendantsInstances = ignoreList + end + end + + local event = RS.Heartbeat + + local FastCastEvents: TypeDef.FastCastEvents = eventModule and require(eventModule) or nil + + local function Stepped(delta: number) + if cast.StateInfo.Paused then return end + + if DebugLogging.Casting then + print("Casting for frame.") + end + + local Cast_timeAtStart = tick() + + local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + + if typeof(latestTrajectory.Acceleration) ~= "Vector3" then + latestTrajectory.Acceleration = Vector3.new() + end + + if + cast.StateInfo.HighFidelityBehavior == FastCastEnums.HighFidelityBehavior.Always + and cast.StateInfo.HighFidelitySegmentSize > 0 + then + local Segment_timeAtStart = tick() + + local castTerminatingfn: TypeDef.OnCastTerminatingFunction? = nil + if FastCastEvents then + castTerminatingfn = cast.StateInfo.FastCastEventsModuleConfig.UseCastTerminating + and FastCastEvents.CastTerminating + or nil + end + if cast.StateInfo.IsActivelyResimulating then + FastCast:TerminateCast(cast, castTerminatingfn) + warn("Cascading cast lag encountered!") + return + end + + cast.StateInfo.IsActivelyResimulating = true + + local origin = latestTrajectory.Origin + local totalDelta = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime + local initialVelocity = latestTrajectory.InitialVelocity + local acceleration = latestTrajectory.Acceleration + + local lastPoint = GetPositionAtTime(totalDelta, origin, initialVelocity, acceleration) + cast.StateInfo.TotalRuntime += delta + totalDelta = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime + + local currentPoint = GetPositionAtTime(totalDelta, origin, initialVelocity, acceleration) + local currentVelocity = GetVelocityAtTime(totalDelta, initialVelocity, acceleration) + local totalDisplacement = currentPoint - lastPoint + + local rayDir = totalDisplacement.Unit * currentVelocity.Magnitude * delta + + local targetWorldRoot = cast.RayInfo.WorldRoot + local CastHandler = castHandlers[variant.CastType] + + local resultOfCast = CastHandler(targetWorldRoot, lastPoint, rayDir, cast.RayInfo.Parameters, variant) + + local point = currentPoint + if resultOfCast ~= nil then + point = resultOfCast.Position + end + + local rayDisplacement = (point - lastPoint).Magnitude + cast.StateInfo.TotalRuntime -= delta + + local numSegmentsDecimal = rayDisplacement / cast.StateInfo.HighFidelitySegmentSize + local numSegmentsReal = math.floor(numSegmentsDecimal) + if numSegmentsReal == 0 then + numSegmentsReal = 1 + end + + local timeIncrement = delta / numSegmentsReal + + if DebugLogging.Calculation then + print("Performing subcast! Time increment: " .. timeIncrement .. ", num segments: " .. numSegmentsReal) + end + + for segmentIndex = 1, numSegmentsReal do + if next(cast) == nil then return end + if cast.StateInfo.CancelHighResCast then + cast.StateInfo.CancelHighResCast = false + break + end + + if DebugLogging.Segment then + print("[" .. segmentIndex .. "] Subcast of time increment " .. timeIncrement) + end + + SimulateCast(cast, timeIncrement, FastCastEvents, variant) + end + + if next(cast) == nil then return end + cast.StateInfo.IsActivelyResimulating = false + + if + behavior.AutomaticPerformance + and (tick() - Segment_timeAtStart) > MAX_SEGMENT_CAL_TIME + and cast.StateInfo + then + local HighFideSizeAmount = behavior.AdaptivePerformance.HighFidelitySegmentSizeIncrease or HIGH_FIDE_INCREASE_SIZE + if DebugLogging.AutomaticPerformance then + warn("AutomaticPerformance increasing size of HighFidelitySize by: ", HighFideSizeAmount) + end + cast.StateInfo.HighFidelitySegmentSize += HighFideSizeAmount + end + else + SimulateCast(cast, delta, FastCastEvents, variant) + end + + if + behavior.AutomaticPerformance + and behavior.AdaptivePerformance.LowerHighFidelityBehavior + and (tick() - Cast_timeAtStart) > MAX_CASTING_TIME + and cast.StateInfo + then + if cast.StateInfo.HighFidelityBehavior > 1 then + cast.StateInfo.HighFidelityBehavior -= 1 + end + end + end + + cast.StateInfo.UpdateConnection = event:Connect(Stepped) + + return cast +end + +return ActiveCastSerial \ No newline at end of file From 3774b17c4875aa6555e143e7100438297d659b4b Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 10:06:48 +0000 Subject: [PATCH 05/62] feat: Add BaseCastSerial for serial caster implementation --- src/FastCast2/BaseCastSerial.luau | 277 ++++++++++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 src/FastCast2/BaseCastSerial.luau diff --git a/src/FastCast2/BaseCastSerial.luau b/src/FastCast2/BaseCastSerial.luau new file mode 100644 index 0000000..a1ff4d8 --- /dev/null +++ b/src/FastCast2/BaseCastSerial.luau @@ -0,0 +1,277 @@ +--[[ + - Author : Mawin CK + - Date : 2025 + -- Version : 0.0.9 +]] + +local RS = game:GetService("RunService") + +local FastCast2 = script.Parent +local FastCastM = require(FastCast2) + +local FastCastEnums = require(FastCast2:WaitForChild("FastCastEnums")) +local TypeDef = require(FastCast2:WaitForChild("TypeDefinitions")) +local ActiveCastSerial = require(FastCast2:WaitForChild("ActiveCastSerial")) + +local EnumCastTypes = FastCastEnums.CastType + +--[=[ + @class BaseCastSerial + + Base class for Serial (non-parallel) Raycast operations. +]=] + +local BaseCastSerial = {} +BaseCastSerial.__index = BaseCastSerial +BaseCastSerial.__type = "BaseCastSerial" + +local BulkMoveToConnection: RBXScriptConnection? = nil +local Actives: any = {} +local Output: BindableEvent? = nil +local ActiveCastCleaner: BindableEvent? = nil +local ObjectCache: BindableFunction? = nil +local NextProjectileID = 0 +local SyncChanges: BindableEvent? = nil +local CastFireFunc = nil +local ParentCaster = nil + +local function HandleBulkMoveTo() + local Parts: { BasePart } = {} + local CFrames: { CFrame } = {} + + for _, ActiveCasts in Actives do + local ProjectilePart = ActiveCasts.RayInfo.CosmeticBulletObject + if not ProjectilePart then continue end + + local resultCFrame = ActiveCasts.CFrame + if ProjectilePart:IsA("BasePart") then + table.insert(Parts, ProjectilePart) + table.insert(CFrames, resultCFrame) + else + ProjectilePart:PivotTo(resultCFrame) + end + end + + task.synchronize() + workspace:BulkMoveTo(Parts, CFrames, Enum.BulkMoveMode.FireCFrameChanged) +end + +local function SendCastFire( + cast: TypeDef.ActiveCastData, + origin: Vector3, + direction: Vector3, + velocity: Vector3 | number, + behavior: TypeDef.FastCastBehavior +) + if Output then + Output:Fire("CastFire", cast, origin, direction, velocity, behavior) + end +end + +--[=[ + @function Init + @within BaseCastSerial + + @param BindableOutput BindableEvent -- The BindableEvent used for outputting events. + @param Data any -- Configuration data for the BaseCastSerial. + @return BaseCastSerial -- The initialized BaseCastSerial instance. +]=] +function BaseCastSerial.Init(BindableOutput: BindableEvent, Data: any, parentCaster: any) + local self = setmetatable({}, BaseCastSerial) + Actives = setmetatable({}, { __mode = "v" }) + Output = BindableOutput + ParentCaster = parentCaster + + local BindableCleaner = Instance.new("BindableEvent") + BindableCleaner.Name = "ActiveCastDestroyer" + BindableCleaner.Parent = script + + if Data.useObjectCache then + local BindableObjectCache = Instance.new("BindableFunction") + BindableObjectCache.Parent = script + BindableObjectCache.Name = "ActiveCastObjectCache" + ObjectCache = BindableObjectCache + end + + if Data.useBulkMoveTo then + BulkMoveToConnection = RS.PreRender:Connect(HandleBulkMoveTo) + end + + ActiveCastCleaner = BindableCleaner + + ActiveCastCleaner.Event:Connect(function(activeCastID: number) + if Actives[activeCastID] then + Actives[activeCastID] = nil + end + end) + + SyncChanges = Instance.new("BindableEvent") + SyncChanges.Name = "SyncChanges" + SyncChanges.Parent = script + + SyncChanges.Event:Connect(function(cast: TypeDef.ActiveCastData) + local ID = cast.ID + local TargetCast = Actives[ID] + + if TargetCast then + for i, v in cast do + TargetCast[i] = v + end + end + end) + + return self +end + +--[=[ + @method Raycast + @within BaseCastSerial +]=] +function BaseCastSerial:Raycast( + Origin: Vector3, + Direction: Vector3, + Velocity: Vector3 | number, + Behavior: TypeDef.FastCastBehavior +) + NextProjectileID += 1 + + Actives[NextProjectileID] = ActiveCastSerial.createCastData(self, { + Output = Output, + ActiveCastCleaner = ActiveCastCleaner, + ObjectCache = ObjectCache, + SyncChange = SyncChanges + }, NextProjectileID, Origin, Direction, Velocity, Behavior, nil, { + CastType = EnumCastTypes.Raycast + } :: any) + + if Behavior.FastCastEventsConfig.UseCastFire then + SendCastFire(Actives[NextProjectileID], Origin, Direction, Velocity, Behavior) + end +end + +--[=[ + @method Blockcast + @within BaseCastSerial +]=] +function BaseCastSerial:Blockcast( + Origin: Vector3, + Size: Vector3, + Direction: Vector3, + Velocity: Vector3 | number, + Behavior: TypeDef.FastCastBehavior +) + NextProjectileID += 1 + + Actives[NextProjectileID] = ActiveCastSerial.createCastData(self, { + Output = Output, + ActiveCastCleaner = ActiveCastCleaner, + ObjectCache = ObjectCache, + SyncChange = SyncChanges + }, NextProjectileID, Origin, Direction, Velocity, Behavior, nil, { + CastType = EnumCastTypes.Blockcast, + Size = Size + } :: any) + + if Behavior.FastCastEventsConfig.UseCastFire then + SendCastFire(Actives[NextProjectileID], Origin, Direction, Velocity, Behavior) + end +end + +--[=[ + @method Spherecast + @within BaseCastSerial +]=] +function BaseCastSerial:Spherecast( + Origin: Vector3, + Radius: number, + Direction: Vector3, + Velocity: Vector3 | number, + Behavior: TypeDef.FastCastBehavior +) + NextProjectileID += 1 + + Actives[NextProjectileID] = ActiveCastSerial.createCastData(self, { + Output = Output, + ActiveCastCleaner = ActiveCastCleaner, + ObjectCache = ObjectCache, + SyncChange = SyncChanges + }, NextProjectileID, Origin, Direction, Velocity, Behavior, nil, { + CastType = EnumCastTypes.Spherecast, + Radius = Radius + } :: any) + + if Behavior.FastCastEventsConfig.UseCastFire then + SendCastFire(Actives[NextProjectileID], Origin, Direction, Velocity, Behavior) + end +end + +--[=[ + @method BindBulkMoveTo + @within BaseCastSerial +]=] +function BaseCastSerial:BindBulkMoveTo(bool: boolean) + if bool then + if not BulkMoveToConnection then + BulkMoveToConnection = RS.PreRender:Connect(HandleBulkMoveTo) + end + else + if BulkMoveToConnection then + BulkMoveToConnection:Disconnect() + BulkMoveToConnection = nil + end + end +end + +--[=[ + @method BindObjectCache + @within BaseCastSerial +]=] +function BaseCastSerial:BindObjectCache(bool: boolean) + if bool then + if ObjectCache then return end + local BindableObjectCache = Instance.new("BindableFunction") + BindableObjectCache.Parent = script + BindableObjectCache.Name = "ActiveCastObjectCache" + ObjectCache = BindableObjectCache + else + if ObjectCache then + ObjectCache:Destroy() + ObjectCache = nil + end + end +end + +--[=[ + @method TerminateCast + @within BaseCastSerial +]=] +function BaseCastSerial:TerminateCast(cast: vaildcast, castTerminatingFunction: TypeDef.OnCastTerminatingFunction?) + if ParentCaster and ParentCaster.TerminateCast then + ParentCaster:TerminateCast(cast, castTerminatingFunction) + end +end + +--[=[ + @method Destroy + @within BaseCastSerial +]=] +function BaseCastSerial:Destroy() + if BulkMoveToConnection then + BulkMoveToConnection:Disconnect() + BulkMoveToConnection = nil + end + + FastCastEventsModule = nil + + for _, v in Actives do + if ParentCaster and ParentCaster.TerminateCast then + ParentCaster:TerminateCast(v) + end + end + + Actives = {} + ParentCaster = nil + setmetatable(self, nil) +end + +return BaseCastSerial \ No newline at end of file From 2acf367d000b2d067d6cb813c90e42aa8867b7e1 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 10:06:54 +0000 Subject: [PATCH 06/62] feat: Add FastCastSerial methods and remove FastCastEventsModule from serial --- src/FastCast2/init.luau | 314 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 314 insertions(+) diff --git a/src/FastCast2/init.luau b/src/FastCast2/init.luau index 5e44e12..71edc21 100644 --- a/src/FastCast2/init.luau +++ b/src/FastCast2/init.luau @@ -69,6 +69,7 @@ local TypeDef = require(script:WaitForChild("TypeDefinitions")) local DefaultConfigs = require(script:WaitForChild("DefaultConfigs")) --local Configs = require(script:WaitForChild("Configs")) local ObjectCache = require(script:WaitForChild("ObjectCache")) +local BaseCastSerial = require(script:WaitForChild("BaseCastSerial")) --local SharedCasters = require(script:WaitForChild("SharedCasters")) @@ -213,6 +214,17 @@ function FastCastParallel.newBehavior(): TypeDef.FastCastBehavior return deepCopyTable(DefaultConfigs.FastCastBehavior) :: TypeDef.FastCastBehavior end +--[=[ + Creates a new FastCastBehavior for Serial Caster. + @function newBehavior + @within FastCastSerial + + @return FastCastBehavior +]=] +function FastCastSerial.newBehavior(): TypeDef.FastCastBehavior + return deepCopyTable(DefaultConfigs.FastCastBehavior) :: TypeDef.FastCastBehavior +end + --[=[ Initializes the Caster with the given parameters. This is required before firing using Raycasts in the Caster or nothing will happen! @@ -691,6 +703,308 @@ function FastCastParallel:SetObjectCacheEnabled( self.ObjectCacheEnabled = enabled end +-- Serial Caster Methods + +--[=[ + Initialize the Serial Caster. + @method Init + @within FastCastSerial + + @param useBulkMoveTo boolean -- Whether to use BulkMoveTo for projectile movement. + @param useObjectCache boolean -- Whether to use ObjectCache. + @param Template BasePart | Model? -- Template for ObjectCache. + @param CacheSize number? -- Size of ObjectCache. + @param CacheHolder Instance? -- Parent for cached objects. +]=] +function FastCastSerial:Init( + useBulkMoveTo: boolean, + useObjectCache: boolean, + Template: BasePart | Model?, + CacheSize: number?, + CacheHolder: Instance? +) + if self.BaseCast then + warn("Serial Caster already initialized") + return + end + + local BindableOutput = Instance.new("BindableEvent") + BindableOutput.Name = "Output" + BindableOutput.Parent = script + + local data = { + useBulkMoveTo = useBulkMoveTo, + useObjectCache = useObjectCache + } + + self.BaseCast = BaseCastSerial.Init(BindableOutput, data, self) + + self.Output = BindableOutput + + if useObjectCache then + if not CacheSize then + CacheSize = DEFAULT_CACHE_SIZE + end + if not CacheHolder then + CacheHolder = DEFAULT_CACHE_HOLDER + end + self.ObjectCache = ObjectCache.new(Template, CacheSize, CacheHolder) :: any + self.ObjectCacheEnabled = true + end + + self.BulkMoveEnabled = useBulkMoveTo + self.Initialized = true +end + +--[=[ + @method RaycastFire + @within FastCastSerial +]=] +function FastCastSerial:RaycastFire( + origin: Vector3, + direction: Vector3, + velocity: Vector3 | number, + BehaviorData: TypeDef.FastCastBehavior? +) + if not self.Initialized or not self.BaseCast then + error("Please Init caster first") + end + if BehaviorData == nil then + BehaviorData = FastCastParallel.newBehavior() + end + + self.BaseCast:Raycast(origin, direction, velocity, BehaviorData) +end + +--[=[ + @method BlockcastFire + @within FastCastSerial +]=] +function FastCastSerial:BlockcastFire( + origin: Vector3, + Size: Vector3, + direction: Vector3, + velocity: Vector3 | number, + BehaviorData: TypeDef.FastCastBehavior? +) + if not self.Initialized or not self.BaseCast then + error("Please Init caster first") + end + if BehaviorData == nil then + BehaviorData = FastCastParallel.newBehavior() + end + + self.BaseCast:Blockcast(origin, Size, direction, velocity, BehaviorData) +end + +--[=[ + @method SpherecastFire + @within FastCastSerial +]=] +function FastCastSerial:SpherecastFire( + origin: Vector3, + Radius: number, + direction: Vector3, + velocity: Vector3 | number, + BehaviorData: TypeDef.FastCastBehavior? +) + if not self.Initialized or not self.BaseCast then + error("Please Init caster first") + end + if BehaviorData == nil then + BehaviorData = FastCastParallel.newBehavior() + end + + self.BaseCast:Spherecast(origin, Radius, direction, velocity, BehaviorData) +end + +--[=[ + @method GetVelocityCast + @within FastCastSerial +]=] +function FastCastSerial:GetVelocityCast(cast: vaildcast): Vector3 + local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + return latestTrajectory.InitialVelocity +end + +--[=[ + @method GetAccelerationCast + @within FastCastSerial +]=] +function FastCastSerial:GetAccelerationCast(cast: vaildcast): Vector3 + local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + return latestTrajectory.Acceleration +end + +--[=[ + @method GetPositionCast + @within FastCastSerial +]=] +function FastCastSerial:GetPositionCast(cast: vaildcast): Vector3 + local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + local totalDelta = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime + local origin = latestTrajectory.Origin + local initialVelocity = latestTrajectory.InitialVelocity + local acceleration = latestTrajectory.Acceleration + + return GetPositionAtTime(totalDelta, origin, initialVelocity, acceleration) +end + +--[=[ + @method SetVelocityCast + @within FastCastSerial +]=] +function FastCastSerial:SetVelocityCast(cast: vaildcast, velocity: Vector3) + local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + latestTrajectory.InitialVelocity = velocity +end + +--[=[ + @method SetAccelerationCast + @within FastCastSerial +]=] +function FastCastSerial:SetAccelerationCast(cast: vaildcast, acceleration: Vector3) + local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + latestTrajectory.Acceleration = acceleration +end + +--[=[ + @method SetPositionCast + @within FastCastSerial +]=] +function FastCastSerial:SetPositionCast(cast: vaildcast, position: Vector3) + local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + latestTrajectory.Origin = position +end + +--[=[ + @method PauseCast + @within FastCastSerial +]=] +function FastCastSerial:PauseCast(cast: vaildcast, value: boolean) + cast.StateInfo.Paused = value +end + +--[=[ + @method AddPositionCast + @within FastCastSerial +]=] +function FastCastSerial:AddPositionCast(cast: vaildcast, position: Vector3) + local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + latestTrajectory.Origin += position +end + +--[=[ + @method AddVelocityCast + @within FastCastSerial +]=] +function FastCastSerial:AddVelocityCast(cast: vaildcast, velocity: Vector3) + local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + latestTrajectory.InitialVelocity += velocity +end + +--[=[ + @method AddAccelerationCast + @within FastCastSerial +]=] +function FastCastSerial:AddAccelerationCast(cast: vaildcast, acceleration: Vector3) + local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + latestTrajectory.Acceleration += acceleration +end + +--[=[ + @method SyncChangesToCast + @within FastCastSerial +]=] +function FastCastSerial:SyncChangesToCast(cast: vaildcast) + if self.BaseCast.SyncChange then + self.BaseCast.SyncChange:Fire(cast) + end +end + +--[=[ + @method TerminateCast + @within FastCastSerial +]=] +function FastCastSerial:TerminateCast(cast: vaildcast, castTerminatingFunction: TypeDef.OnCastTerminatingFunction?) + if cast.StateInfo.UpdateConnection ~= nil then + cast.StateInfo.UpdateConnection:Disconnect() + cast.StateInfo.UpdateConnection = nil + end + + if cast.RayInfo.CosmeticBulletObject then + cast.RayInfo.CosmeticBulletObject:Destroy() + cast.RayInfo.CosmeticBulletObject = nil + end + + if castTerminatingFunction then + castTerminatingFunction(cast) + end + + self.Output:Fire("CastTerminating", cast) +end + +--[=[ + @method BindBulkMoveTo + @within FastCastSerial +]=] +function FastCastSerial:BindBulkMoveTo(enabled: boolean) + if self.BaseCast then + self.BaseCast:BindBulkMoveTo(enabled) + end + self.BulkMoveEnabled = enabled +end + +--[=[ + @method SetObjectCacheEnabled + @within FastCastSerial +]=] +function FastCastSerial:SetObjectCacheEnabled(enabled: boolean) + if not self.BaseCast then return end + + if enabled then + if not self.ObjectCache then + warn("ObjectCache not initialized. Call Init with useObjectCache = true first.") + return + end + self.BaseCast:BindObjectCache(true) + else + self.BaseCast:BindObjectCache(false) + if self.ObjectCache then + self.ObjectCache:Destroy() + self.ObjectCache = nil + end + end + + self.ObjectCacheEnabled = enabled +end + +--[=[ + @method Destroy + @within FastCastSerial +]=] +function FastCastSerial:Destroy() + if self.ObjectCache then + self.ObjectCache:Destroy() + end + + if self.BaseCast then + self.BaseCast:Destroy() + end + + DestroySignal(self.LengthChanged) + DestroySignal(self.Hit) + DestroySignal(self.Pierced) + DestroySignal(self.CastTerminating) + DestroySignal(self.CastFire) + + if self.Output then + self.Output:Destroy() + end + + setmetatable(self, nil) +end + --[=[ Destroy's a Caster, cleaning up all resources used by it. @method Destroy From 87158c983684906a4dff4faabd42b747fb6ad1df Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 10:11:36 +0000 Subject: [PATCH 07/62] feat: Add SerialSimulation with SoA pattern and single RunService --- src/FastCast2/SerialSimulation.luau | 358 ++++++++++++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 src/FastCast2/SerialSimulation.luau diff --git a/src/FastCast2/SerialSimulation.luau b/src/FastCast2/SerialSimulation.luau new file mode 100644 index 0000000..8d9335a --- /dev/null +++ b/src/FastCast2/SerialSimulation.luau @@ -0,0 +1,358 @@ +--[[ + - Author : Mawin CK + - Date : 2025 + -- Version : 0.0.9 + + SerialSimulation manages all active casts with a single RunService connection. + Uses SoA (Structure of Arrays) for better cache performance. +]] + +local RS = game:GetService("RunService") + +local FastCastModule = script.Parent +local TypeDef = require(FastCastModule:WaitForChild("TypeDefinitions")) +local Configs = require(FastCastModule:WaitForChild("Configs")) +local DebugLogging = Configs.DebugLogging +local FastCastEnums = require(FastCastModule:WaitForChild("FastCastEnums")) + +local EnumCastTypes = FastCastEnums.CastType + +local MAX_SEGMENT_CAL_TIME = 0.016 * 5 +local MAX_CASTING_TIME = 0.2 +local DEFAULT_MAX_DISTANCE = 1000 +local HIGH_FIDE_INCREASE_SIZE = 0.5 + +local EPSILON = 1e-6 + +-- Cast handlers +local castHandlers = { + [EnumCastTypes.Raycast] = function( + targetWorldRoot: WorldRoot, + origin: Vector3, + direction: Vector3, + parameters: RaycastParams + ) + return targetWorldRoot:Raycast(origin, direction, parameters) + end, + [EnumCastTypes.Blockcast] = function( + targetWorldRoot: WorldRoot, + origin: Vector3, + direction: Vector3, + parameters: RaycastParams, + size: Vector3 + ) + return targetWorldRoot:Blockcast(CFrame.new(origin), size, direction, parameters) + end, + [EnumCastTypes.Spherecast] = function( + targetWorldRoot: WorldRoot, + origin: Vector3, + direction: Vector3, + parameters: RaycastParams, + radius: number + ) + return targetWorldRoot:Spherecast(origin, radius, direction, parameters) + end +} + +-- SoA structure for active casts +type SerialSimulation = { + ActiveCasts: { any }, + StepConnection: RBXScriptConnection, + IsRunning: boolean, + + Register: (self: SerialSimulation, cast: any) -> (), + Unregister: (self: SerialSimulation, castID: number) -> (), + Start: (self: SerialSimulation) -> (), + Stop: (self: SerialSimulation) -> (), +} + +local SerialSimulation = {} +SerialSimulation.__index = SerialSimulation + +-- SoA arrays for cast data +local castCount = 0 +local castIDs = {} :: { number } +local castOrigin = {} :: { Vector3 } +local castDirection = {} :: { Vector3 } +local castVelocity = {} :: { Vector3 } +local castAcceleration = {} :: { Vector3 } +local castTotalRuntime = {} :: { number } +local castDistanceCovered = {} :: { number } +local castMaxDistance = {} :: { number } +local castPaused = {} :: { boolean } +local castHighFidelitySegmentSize = {} :: { number } +local castHighFidelityBehavior = {} :: { number } +local castIsActivelyResimulating = {} :: { boolean } +local castCancelHighResCast = {} :: { boolean } +local castCFrame = {} :: { CFrame } +local castWorldRoot = {} :: { WorldRoot } +local castRaycastParams = {} :: { RaycastParams } +local castCosmeticBulletObject = {} :: { Instance? } +local castCastType = {} :: { number } +local castSize = {} :: { Vector3? } +local castRadius = {} :: { number? } +local castVisualizeCasts = {} :: { boolean } +local castCaster = {} :: { any } + +-- Event queue +local QueuedEvents = {} :: { any } + +local function GetPositionAtTime(t: number, origin: Vector3, initialVelocity: Vector3, acceleration: Vector3): Vector3 + local force = Vector3.new( + (acceleration.X * t ^ 2) / 2, + (acceleration.Y * t ^ 2) / 2, + (acceleration.Z * t ^ 2) / 2 + ) + return origin + (initialVelocity * t) + force +end + +local function GetVelocityAtTime(time: number, initialVelocity: Vector3, acceleration: Vector3): Vector3 + return initialVelocity + acceleration * time +end + +local function OnError(errorMessage: string) + warn(debug.traceback(errorMessage, 2)) +end + +local function DispatchEvent(callback: any, ...) + if callback then + if Configs.UseProtectedCalls then + xpcall(callback, OnError, ...) + else + callback(...) + end + end +end + +local function QueueEvent(callback: any, ...) + if callback then + table.insert(QueuedEvents, { Callback = callback, Args = { ... } }) + end +end + +local function DispatchAllEvents() + for _, event in QueuedEvents do + DispatchEvent(event.Callback, unpack(event.Args)) + end + table.clear(QueuedEvents) +end + +function SerialSimulation.new(): SerialSimulation + local self = setmetatable({}, SerialSimulation) + self.ActiveCasts = {} + self.IsRunning = false + return self +end + +function SerialSimulation:Register(cast: any) + castCount += 1 + local id = castCount + + castIDs[id] = cast.ID + castOrigin[id] = cast.StateInfo.Trajectories[1].Origin + castDirection[id] = cast.StateInfo.Trajectories[1].InitialVelocity.Unit + castVelocity[id] = cast.StateInfo.Trajectories[1].InitialVelocity + castAcceleration[id] = cast.StateInfo.Trajectories[1].Acceleration + castTotalRuntime[id] = 0 + castDistanceCovered[id] = 0 + castMaxDistance[id] = cast.RayInfo.MaxDistance + castPaused[id] = false + castHighFidelitySegmentSize[id] = cast.StateInfo.HighFidelitySegmentSize + castHighFidelityBehavior[id] = cast.StateInfo.HighFidelityBehavior + castIsActivelyResimulating[id] = false + castCancelHighResCast[id] = false + castCFrame[id] = cast.CFrame + castWorldRoot[id] = cast.RayInfo.WorldRoot + castRaycastParams[id] = cast.RayInfo.Parameters + castCosmeticBulletObject[id] = cast.RayInfo.CosmeticBulletObject + castCastType[id] = cast.StateInfo.Trajectories[1].CastType or 1 + castVisualizeCasts[id] = cast.StateInfo.VisualizeCasts + castCaster[id] = cast.Caster + + if cast.RayInfo.Size then + castSize[id] = cast.RayInfo.Size + end + if cast.RayInfo.Radius then + castRadius[id] = cast.RayInfo.Radius + end + + cast.ID = id + self.ActiveCasts[id] = cast +end + +function SerialSimulation:Unregister(castID: number) + if not self.ActiveCasts[castID] then return end + + local lastID = castCount + if castID ~= lastID then + -- Swap with last element + castIDs[castID] = castIDs[lastID] + castOrigin[castID] = castOrigin[lastID] + castDirection[castID] = castDirection[lastID] + castVelocity[castID] = castVelocity[lastID] + castAcceleration[castID] = castAcceleration[lastID] + castTotalRuntime[castID] = castTotalRuntime[lastID] + castDistanceCovered[castID] = castDistanceCovered[lastID] + castMaxDistance[castID] = castMaxDistance[lastID] + castPaused[castID] = castPaused[lastID] + castHighFidelitySegmentSize[castID] = castHighFidelitySegmentSize[lastID] + castHighFidelityBehavior[castID] = castHighFidelityBehavior[lastID] + castIsActivelyResimulating[castID] = castIsActivelyResimulating[lastID] + castCancelHighResCast[castID] = castCancelHighResCast[lastID] + castCFrame[castID] = castCFrame[lastID] + castWorldRoot[castID] = castWorldRoot[lastID] + castRaycastParams[castID] = castRaycastParams[lastID] + castCosmeticBulletObject[castID] = castCosmeticBulletObject[lastID] + castCastType[castID] = castCastType[lastID] + castSize[castID] = castSize[lastID] + castRadius[castID] = castRadius[lastID] + castVisualizeCasts[castID] = castVisualizeCasts[castID] + castCaster[castID] = castCaster[lastID] + + local movedCast = self.ActiveCasts[lastID] + if movedCast then + movedCast.ID = castID + end + end + + -- Clear last slot + castIDs[lastID] = nil + castOrigin[lastID] = nil + castDirection[lastID] = nil + castVelocity[lastID] = nil + castAcceleration[lastID] = nil + castTotalRuntime[lastID] = nil + castDistanceCovered[lastID] = nil + castMaxDistance[lastID] = nil + castPaused[lastID] = nil + castHighFidelitySegmentSize[lastID] = nil + castHighFidelityBehavior[lastID] = nil + castIsActivelyResimulating[lastID] = nil + castCancelHighResCast[lastID] = nil + castCFrame[lastID] = nil + castWorldRoot[lastID] = nil + castRaycastParams[lastID] = nil + castCosmeticBulletObject[lastID] = nil + castCastType[lastID] = nil + castSize[lastID] = nil + castRadius[lastID] = nil + castVisualizeCasts[lastID] = nil + castCaster[lastID] = nil + + self.ActiveCasts[castID] = nil + castCount = lastID - 1 +end + +local function UpdateAllCasts(deltaTime: number) + if castCount == 0 then return end + + local destroyedCastIDs = {} :: { number } + + for i = 1, castCount do + if castPaused[i] then continue end + + local castType = castCastType[i] + local CastHandler = castHandlers[castType] + + local origin = castOrigin[i] + local totalDelta = castTotalRuntime[i] + local initialVelocity = castVelocity[i] + local acceleration = castAcceleration[i] + + local lastPoint = GetPositionAtTime(totalDelta, origin, initialVelocity, acceleration) + + castTotalRuntime[i] += deltaTime + totalDelta = castTotalRuntime[i] + + local currentTarget = GetPositionAtTime(totalDelta, origin, initialVelocity, acceleration) + local segmentVelocity = GetVelocityAtTime(totalDelta, initialVelocity, acceleration) + local totalDisplacement = currentTarget - lastPoint + local rayDir = totalDisplacement.Unit * segmentVelocity.Magnitude * deltaTime + + local variant = {} + if castType == EnumCastTypes.Blockcast then + variant.Size = castSize[i] + elseif castType == EnumCastTypes.Spherecast then + variant.Radius = castRadius[i] + end + + local resultOfCast = CastHandler(castWorldRoot[i], lastPoint, rayDir, castRaycastParams[i], variant) + + local point = currentTarget + if resultOfCast ~= nil then + point = resultOfCast.Position + end + + local rayDisplacement = (point - lastPoint).Magnitude + castDistanceCovered[i] += rayDisplacement + castCFrame[i] = CFrame.new(lastPoint, lastPoint + rayDir) * CFrame.new(0, 0, -rayDisplacement / 2) + + -- Fire LengthChanged event + local caster = castCaster[i] + if caster and caster.Output then + caster.Output:Fire( + "LengthChanged", + self.ActiveCasts[i], + lastPoint, + rayDir.Unit, + rayDisplacement, + segmentVelocity, + castCosmeticBulletObject[i] + ) + end + + -- Update cosmetic bullet + local bullet = castCosmeticBulletObject[i] + if bullet then + if bullet:IsA("BasePart") then + bullet.CFrame = castCFrame[i] + else + bullet:PivotTo(castCFrame[i]) + end + end + + -- Handle hit + if resultOfCast ~= nil and resultOfCast.Instance ~= castCosmeticBulletObject[i] then + local caster = castCaster[i] + if caster and caster.Output then + caster.Output:Fire("Hit", self.ActiveCasts[i], resultOfCast, segmentVelocity, castCosmeticBulletObject[i]) + end + + -- Destroy cast + table.insert(destroyedCastIDs, i) + end + + -- Check max distance + if castDistanceCovered[i] >= castMaxDistance[i] then + table.insert(destroyedCastIDs, i) + end + end + + -- Remove destroyed casts + for _, id in destroyedCastIDs do + local cast = self.ActiveCasts[id] + if cast and cast.RayInfo and cast.RayInfo.CosmeticBulletObject then + cast.RayInfo.CosmeticBulletObject:Destroy() + end + self:Unregister(id) + end + + DispatchAllEvents() +end + +function SerialSimulation:Start() + if self.IsRunning then return end + self.IsRunning = true + self.StepConnection = RS.Heartbeat:Connect(UpdateAllCasts) +end + +function SerialSimulation:Stop() + if not self.IsRunning then return end + self.IsRunning = false + if self.StepConnection then + self.StepConnection:Disconnect() + self.StepConnection = nil + end +end + +return SerialSimulation \ No newline at end of file From 51a5f2430eb5daeae93abb2b7e9f8c2db8dd2c7f Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 10:11:41 +0000 Subject: [PATCH 08/62] refactor: ActiveCastSerial uses SerialSimulation --- src/FastCast2/ActiveCastSerial.luau | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/FastCast2/ActiveCastSerial.luau b/src/FastCast2/ActiveCastSerial.luau index 0df98b3..e15396a 100644 --- a/src/FastCast2/ActiveCastSerial.luau +++ b/src/FastCast2/ActiveCastSerial.luau @@ -13,6 +13,7 @@ local TypeDef = require(FastCastModule:WaitForChild("TypeDefinitions")) local Configs = require(FastCastModule:WaitForChild("Configs")) local DebugLogging = Configs.DebugLogging local FastCastEnums = require(FastCastModule:WaitForChild("FastCastEnums")) +local SerialSimulation = require(FastCastModule:WaitForChild("SerialSimulation")) local FC_VIS_OBJ_NAME = "FastCastVisualizationObjects" local MAX_SEGMENT_CAL_TIME = 0.016 * 5 @@ -21,6 +22,10 @@ local DEFAULT_MAX_DISTANCE = 1000 local EnumCastTypes = FastCastEnums.CastType +-- Shared simulation for all serial casts +local SharedSimulation = SerialSimulation.new() +SharedSimulation:Start() + local DBG_SEGMENT_SUB_COLOR = Color3.new(0.286275, 0.329412, 0.247059) local DBG_SEGMENT_SUB_COLOR2 = Color3.new(0.454902, 0.933333, 0.011765) local DBG_HIT_SUB_COLOR = Color3.new(0.0588235, 0.87451, 1) From 4f6340bf4a77661ae9bbc9e00f5201a19ad0cb3e Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 10:13:59 +0000 Subject: [PATCH 09/62] revert: Remove conflicting SerialSimulation reference from ActiveCast --- src/FastCast2/ActiveCastSerial.luau | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/FastCast2/ActiveCastSerial.luau b/src/FastCast2/ActiveCastSerial.luau index e15396a..c4b49f9 100644 --- a/src/FastCast2/ActiveCastSerial.luau +++ b/src/FastCast2/ActiveCastSerial.luau @@ -8,12 +8,10 @@ local RS = game:GetService("RunService") local FastCastModule = script.Parent -local FastCast = require(FastCastModule) local TypeDef = require(FastCastModule:WaitForChild("TypeDefinitions")) local Configs = require(FastCastModule:WaitForChild("Configs")) local DebugLogging = Configs.DebugLogging local FastCastEnums = require(FastCastModule:WaitForChild("FastCastEnums")) -local SerialSimulation = require(FastCastModule:WaitForChild("SerialSimulation")) local FC_VIS_OBJ_NAME = "FastCastVisualizationObjects" local MAX_SEGMENT_CAL_TIME = 0.016 * 5 @@ -22,10 +20,6 @@ local DEFAULT_MAX_DISTANCE = 1000 local EnumCastTypes = FastCastEnums.CastType --- Shared simulation for all serial casts -local SharedSimulation = SerialSimulation.new() -SharedSimulation:Start() - local DBG_SEGMENT_SUB_COLOR = Color3.new(0.286275, 0.329412, 0.247059) local DBG_SEGMENT_SUB_COLOR2 = Color3.new(0.454902, 0.933333, 0.011765) local DBG_HIT_SUB_COLOR = Color3.new(0.0588235, 0.87451, 1) From 11c458caf76a2524caa735cb826b9b398c3bb1c9 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 10:21:03 +0000 Subject: [PATCH 10/62] refactor: Serial uses SoA pattern with SerialSimulation - ActiveCastSerial: simplified, uses SerialSimulation - BaseCastSerial: uses SerialSimulation for all casts - SerialSimulation: single RunService, SoA, queue technique like SwiftCast --- src/FastCast2/ActiveCastSerial.luau | 761 +++------------------------- src/FastCast2/BaseCastSerial.luau | 275 +++++----- src/FastCast2/SerialSimulation.luau | 399 +++++++-------- 3 files changed, 409 insertions(+), 1026 deletions(-) diff --git a/src/FastCast2/ActiveCastSerial.luau b/src/FastCast2/ActiveCastSerial.luau index c4b49f9..b09dea6 100644 --- a/src/FastCast2/ActiveCastSerial.luau +++ b/src/FastCast2/ActiveCastSerial.luau @@ -1,8 +1,10 @@ --- Mozilla Public License 2.0 --[[ - Author : Mawin CK - Date : 2025 -- Version : 0.0.9 + + ActiveCastSerial - Serial mode with single RunService, SoA pattern, queue technique + Similar to SwiftCast implementation ]] local RS = game:GetService("RunService") @@ -17,6 +19,7 @@ local FC_VIS_OBJ_NAME = "FastCastVisualizationObjects" local MAX_SEGMENT_CAL_TIME = 0.016 * 5 local MAX_CASTING_TIME = 0.2 local DEFAULT_MAX_DISTANCE = 1000 +local HIGH_FIDE_INCREASE_SIZE = 0.5 local EnumCastTypes = FastCastEnums.CastType @@ -25,20 +28,7 @@ local DBG_SEGMENT_SUB_COLOR2 = Color3.new(0.454902, 0.933333, 0.011765) local DBG_HIT_SUB_COLOR = Color3.new(0.0588235, 0.87451, 1) local DBG_RAYPIERCE_SUB_COLOR = Color3.new(1, 0.113725, 0.588235) -type vaildcast = TypeDef.ActiveCastData | TypeDef.ActiveBlockcastData | TypeDef.ActiveSpherecastData - -type BlockcastVariant = { CastType: number, Size: Vector3 } -type SpherecastVariant = { CastType: number, Radius: number } -type CastVariants = BlockcastVariant | SpherecastVariant - -type RayVisualizerVariant = { castLength: number } -type BlockVisualizerVariant = { size: Vector3 } -type SphereVisualizerVariant = { radius: number } -type CastVisualizerVariants = RayVisualizerVariant | BlockVisualizerVariant | SphereVisualizerVariant - -type CastHandler = (WorldRoot: WorldRoot, origin: Vector3, direction: Vector3, castVariant: CastVariants) -> RaycastResult - -local HIGH_FIDE_INCREASE_SIZE = 0.5 +type CastVariant = { CastType: number, Size: Vector3?, Radius: number? } local CastVariantTypes = { [EnumCastTypes.Raycast] = "Raycast", @@ -47,51 +37,21 @@ local CastVariantTypes = { } local castHandlers = { - [EnumCastTypes.Raycast] = function( - targetWorldRoot: WorldRoot, - origin: Vector3, - direction: Vector3, - parameters: RaycastParams - ) + [EnumCastTypes.Raycast] = function(targetWorldRoot: WorldRoot, origin: Vector3, direction: Vector3, parameters: RaycastParams) return targetWorldRoot:Raycast(origin, direction, parameters) end, - [EnumCastTypes.Blockcast] = function( - targetWorldRoot: WorldRoot, - origin: Vector3, - direction: Vector3, - parameters: RaycastParams, - variant: BlockcastVariant - ) - return targetWorldRoot:Blockcast(CFrame.new(origin), variant.Size, direction, parameters) + [EnumCastTypes.Blockcast] = function(targetWorldRoot: WorldRoot, origin: Vector3, direction: Vector3, parameters: RaycastParams, size: Vector3) + return targetWorldRoot:Blockcast(CFrame.new(origin), size, direction, parameters) end, - [EnumCastTypes.Spherecast] = function( - targetWorldRoot: WorldRoot, - origin: Vector3, - direction: Vector3, - parameters: RaycastParams, - variant: SpherecastVariant - ) - return targetWorldRoot:Spherecast(origin, variant.Radius, direction, parameters) + [EnumCastTypes.Spherecast] = function(targetWorldRoot: WorldRoot, origin: Vector3, direction: Vector3, parameters: RaycastParams, radius: number) + return targetWorldRoot:Spherecast(origin, radius, direction, parameters) end } ---[=[ - @class ActiveCastSerial - - ActiveCast for serial (non-parallel) mode. Runs simulation directly on main thread. -]=] - local ActiveCastSerial = {} +ActiveCastSerial.__index = ActiveCastSerial -local function DebrisAdd(obj: Instance, Lifetime: number) - if not obj then return end - if Lifetime <= 0 then - obj:Destroy() - end - task.delay(Lifetime, function() - obj:Destroy() - end) -end +local Simulation = nil :: any local function GetPositionAtTime(t: number, origin: Vector3, initialVelocity: Vector3, acceleration: Vector3): Vector3 local force = Vector3.new( @@ -106,13 +66,14 @@ local function GetVelocityAtTime(time: number, initialVelocity: Vector3, acceler return initialVelocity + acceleration * time end -local function CloneCastParams(params: RaycastParams): RaycastParams - local clone: RaycastParams = RaycastParams.new() - clone.CollisionGroup = params.CollisionGroup - clone.FilterType = params.FilterType - clone.FilterDescendantsInstances = params.FilterDescendantsInstances - clone.IgnoreWater = params.IgnoreWater - return clone +local function DebrisAdd(obj: Instance, Lifetime: number) + if not obj then return end + if Lifetime <= 0 then + obj:Destroy() + end + task.delay(Lifetime, function() + obj:Destroy() + end) end local function GetFastCastVisualizationContainer(): Instance @@ -127,646 +88,88 @@ local function GetFastCastVisualizationContainer(): Instance return fcVisualizationObjects end -local function DbgVisualizeRaySegment( - castStartCFrame: CFrame, - VisualizeCasts: boolean, - VisualizeCastSettings: TypeDef.VisualizeCastSettings, - variant: RayVisualizerVariant -): ConeHandleAdornment? - if not VisualizeCasts then return end +local function DbgVisualizeRaySegment(castStartCFrame: CFrame, visualize: boolean, settings: any, length: number) + if not visualize then return end local adornment = Instance.new("ConeHandleAdornment") adornment.Adornee = workspace.Terrain adornment.CFrame = castStartCFrame - adornment.Height = variant.castLength - adornment.Color3 = VisualizeCastSettings.Debug_SegmentColor - adornment.Radius = VisualizeCastSettings.Debug_SegmentSize - adornment.Transparency = VisualizeCastSettings.Debug_SegmentTransparency - adornment.Parent = GetFastCastVisualizationContainer() - DebrisAdd(adornment, VisualizeCastSettings.Debug_RayLifetime) - return adornment -end - -local function DbgVisualizeBlockSegment( - castStartCFrame: CFrame, - VisualizeCasts: boolean, - VisualizeCastSettings: TypeDef.VisualizeCastSettings, - variant: BlockVisualizerVariant -): BoxHandleAdornment? - if not VisualizeCasts then return end - local adornment = Instance.new("BoxHandleAdornment") - adornment.Adornee = workspace.Terrain - adornment.CFrame = castStartCFrame - adornment.Size = variant.size - adornment.Color3 = VisualizeCastSettings.Debug_SegmentColor - adornment.Transparency = VisualizeCastSettings.Debug_SegmentTransparency + adornment.Height = length + adornment.Color3 = settings.Debug_SegmentColor + adornment.Radius = settings.Debug_SegmentSize + adornment.Transparency = settings.Debug_SegmentTransparency adornment.Parent = GetFastCastVisualizationContainer() - DebrisAdd(adornment, VisualizeCastSettings.Debug_RayLifetime) - return adornment + DebrisAdd(adornment, settings.Debug_RayLifetime) end -local function DbgVisualizeSphereSegment( - castStartCFrame: CFrame, - VisualizeCasts: boolean, - VisualizeCastSettings: TypeDef.VisualizeCastSettings, - variant: SphereVisualizerVariant -): SphereHandleAdornment? - if not VisualizeCasts then return end - local adornment = Instance.new("SphereHandleAdornment") - adornment.Adornee = workspace.Terrain - adornment.CFrame = castStartCFrame - adornment.Radius = variant.radius - adornment.Color3 = VisualizeCastSettings.Debug_SegmentColor - adornment.Transparency = VisualizeCastSettings.Debug_SegmentTransparency - adornment.Parent = GetFastCastVisualizationContainer() - DebrisAdd(adornment, VisualizeCastSettings.Debug_RayLifetime) - return adornment -end - -local function DbgVisualizeHit( - atCF: CFrame, - wasPierce: boolean, - VisualizeCasts: boolean, - VisualizeCastSettings: TypeDef.VisualizeCastSettings -): SphereHandleAdornment? - if not VisualizeCasts then return end +local function DbgVisualizeHit(atCF: CFrame, wasPierce: boolean, visualize: boolean, settings: any) + if not visualize then return end local adornment = Instance.new("SphereHandleAdornment") adornment.Adornee = workspace.Terrain adornment.CFrame = atCF - adornment.Radius = (wasPierce == false) and VisualizeCastSettings.Debug_HitSize or VisualizeCastSettings.Debug_RayPierceSize - adornment.Transparency = (wasPierce == false) and VisualizeCastSettings.Debug_HitTransparency or VisualizeCastSettings.Debug_RayPierceTransparency - adornment.Color3 = (wasPierce == false) and VisualizeCastSettings.Debug_HitColor or VisualizeCastSettings.Debug_RayPierceColor + adornment.Radius = wasPierce and settings.Debug_RayPierceSize or settings.Debug_HitSize + adornment.Transparency = wasPierce and settings.Debug_RayPierceTransparency or settings.Debug_HitTransparency + adornment.Color3 = wasPierce and settings.Debug_RayPierceColor or settings.Debug_HitColor adornment.Parent = GetFastCastVisualizationContainer() - DebrisAdd(adornment, VisualizeCastSettings.Debug_HitLifetime) - return adornment + DebrisAdd(adornment, settings.Debug_HitLifetime) end -local Visualizers = { - [EnumCastTypes.Raycast] = DbgVisualizeRaySegment, - [EnumCastTypes.Blockcast] = DbgVisualizeBlockSegment, - [EnumCastTypes.Spherecast] = DbgVisualizeSphereSegment -} - -local function SendHit( - cast: vaildcast, - resultOfCast: RaycastResult, - segmentVelocity: Vector3, - cosmeticBulletObject: Instance? -) - local FastCastEventsConfig = cast.StateInfo.FastCastEventsConfig - if FastCastEventsConfig and FastCastEventsConfig.UseHit == false then return end - cast.Caster.Output:Fire("Hit", cast, resultOfCast, segmentVelocity, cosmeticBulletObject) -end - -local function SendPierced( - cast: vaildcast, - resultOfCast: RaycastResult, - segmentVelocity: Vector3, - cosmeticBulletObject: Instance? -) - local FastCastEventsConfig = cast.StateInfo.FastCastEventsConfig - if FastCastEventsConfig and FastCastEventsConfig.UsePierced == false then return end - cast.Caster.Output:Fire("Pierced", cast, resultOfCast, segmentVelocity, cosmeticBulletObject) -end - -local function SendLengthChanged( - cast: vaildcast, - lastPoint: Vector3, - rayDir: Vector3, - rayDisplacement: number, - segmentVelocity: Vector3, - cosmeticBulletObject: Instance? -) - local FastCastEventsConfig = cast.StateInfo.FastCastEventsConfig - if FastCastEventsConfig and FastCastEventsConfig.UseLengthChanged == false then return end - cast.Caster.Output:Fire( - "LengthChanged", - cast, - lastPoint, - rayDir, - rayDisplacement, - segmentVelocity, - cosmeticBulletObject - ) -end - -local function SimulateCast( - cast: any, - delta: number, - FastCastEvents: TypeDef.FastCastEvents, - variant: CastVariants -) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") - - if DebugLogging.Casting then - print("Casting for frame.") - end - - local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] - local origin = latestTrajectory.Origin - local totalDelta = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime - local initialVelocity = latestTrajectory.InitialVelocity - local acceleration = latestTrajectory.Acceleration - - local lastPoint = GetPositionAtTime(totalDelta, origin, initialVelocity, acceleration) - local lastDelta = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime - - cast.StateInfo.TotalRuntime += delta - totalDelta = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime - - local currentTarget = GetPositionAtTime(totalDelta, origin, initialVelocity, acceleration) - local segmentVelocity = GetVelocityAtTime(totalDelta, initialVelocity, acceleration) - local totalDisplacement = currentTarget - lastPoint - local rayDir = totalDisplacement.Unit * segmentVelocity.Magnitude * delta - - local CastType = variant.CastType - local targetWorldRoot = cast.RayInfo.WorldRoot - - local CastHandler = castHandlers[CastType] - local Visualizer = Visualizers[CastType] - - local resultOfCast = CastHandler(targetWorldRoot, lastPoint, rayDir, cast.RayInfo.Parameters, variant) - - local point = currentTarget - local part: Instance? = nil - - if resultOfCast ~= nil then - point = resultOfCast.Position - part = resultOfCast.Instance - end - - local rayDisplacement = (point - lastPoint).Magnitude - - local VisualizeCasts = cast.StateInfo.VisualizeCasts - local VisualizeCastSettings = cast.StateInfo.VisualizeCastSettings - local FastCastEventsModuleConfig = cast.StateInfo.FastCastEventsModuleConfig - - if typeof(latestTrajectory.Acceleration) ~= "Vector3" then - latestTrajectory.Acceleration = Vector3.new() - end - - local VisualizeVariant = {} - if CastType == EnumCastTypes.Raycast then - VisualizeVariant.castLength = rayDisplacement - elseif CastType == EnumCastTypes.Blockcast then - VisualizeVariant.size = cast.RayInfo.Size - elseif CastType == EnumCastTypes.Spherecast then - VisualizeVariant.radius = cast.RayInfo.Radius - end - - cast.CFrame = CFrame.new(lastPoint, lastPoint + rayDir) * CFrame.new(0, 0, -rayDisplacement / 2) - - local LengthChangedfn: TypeDef.OnLengthChangedFunction? = nil - local canPierceCheckfn: TypeDef.CanPierceFunction? = nil - local castTerminatingfn: TypeDef.OnCastTerminatingFunction? = nil - local Hitfn: TypeDef.OnHitFunction? = nil - local Piercedfn: TypeDef.OnPiercedFunction? = nil - - if FastCastEvents then - canPierceCheckfn = FastCastEventsModuleConfig.UseCanPierce and FastCastEvents.CanPierce or nil - castTerminatingfn = FastCastEventsModuleConfig.UseCastTerminating and FastCastEvents.CastTerminating or nil - Hitfn = FastCastEventsModuleConfig.UseHit and FastCastEvents.Hit or nil - Piercedfn = FastCastEventsModuleConfig.UsePierced and FastCastEvents.Pierced or nil - LengthChangedfn = FastCastEventsModuleConfig.UseLengthChanged and FastCastEvents.LengthChanged or nil - end - - SendLengthChanged(cast, lastPoint, rayDir.Unit, rayDisplacement, segmentVelocity, cast.RayInfo.CosmeticBulletObject) - - if LengthChangedfn then - LengthChangedfn( - cast, - lastPoint, - rayDir.Unit, - rayDisplacement, - segmentVelocity, - cast.RayInfo.CosmeticBulletObject - ) - end - - cast.StateInfo.DistanceCovered += rayDisplacement - - local rayVisualization = nil - if delta > 0 then - rayVisualization = Visualizer( - CFrame.new(lastPoint, lastPoint + rayDir), - VisualizeCasts, - VisualizeCastSettings, - VisualizeVariant - ) - end - - if part and part ~= cast.RayInfo.CosmeticBulletObject then - if DebugLogging.Hit then - print("Hit something, testing now.") - end - - if DebugLogging.RayPierce and canPierceCheckfn == nil then - print("No piercing function set, proceeding to hit processing.") - end - - if - canPierceCheckfn == nil - or canPierceCheckfn(cast, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject) == false - then - if DebugLogging.RayPierce then - print("Piercing function is nil or it returned FALSE to not pierce this hit.") - end - - cast.StateInfo.IsActivelySimulatingPierce = false - - if - cast.StateInfo.HighFidelityBehavior == FastCastEnums.HighFidelityBehavior.Automatic - and cast.StateInfo.HighFidelitySegmentSize > 0 - then - cast.StateInfo.CancelHighResCast = false - - if cast.StateInfo.IsActivelyResimulating then - FastCast:TerminateCast(cast, castTerminatingfn) - warn("Cascading cast lag encountered!") - return - end - - cast.StateInfo.IsActivelyResimulating = true - - if DebugLogging.Calculation then - print("Hit was registered, but recalculation is on for physics based casts. Recalculating to verify a real hit...") - end - - local numSegmentsDecimal = rayDisplacement / cast.StateInfo.HighFidelitySegmentSize - local numSegmentsReal = math.floor(numSegmentsDecimal) - - if numSegmentsReal == 0 then - numSegmentsReal = 1 - end - - local timeIncrement = delta / numSegmentsReal - - if DebugLogging.Calculation then - print("Performing subcast! Time increment: " .. timeIncrement .. ", num segments: " .. numSegmentsReal) - end - - for segmentIndex = 1, numSegmentsReal do - if cast.StateInfo.CancelHighResCast then - cast.StateInfo.CancelHighResCast = false - break - end - - local subPosition = GetPositionAtTime( - lastDelta + (timeIncrement * segmentIndex), - origin, - initialVelocity, - acceleration - ) - local subVelocity = GetVelocityAtTime(lastDelta + (timeIncrement * segmentIndex), initialVelocity, acceleration) - local subRayDir = subVelocity * delta - local subResult = CastHandler(targetWorldRoot, subPosition, subRayDir, cast.RayInfo.Parameters, variant) - - local subDisplacement = (subPosition - (subPosition + subVelocity)).Magnitude - - if CastType == EnumCastTypes.Raycast then - VisualizeVariant.castLength = subDisplacement - end - - if subResult ~= nil then - subDisplacement = (subPosition - subResult.Position).Magnitude - local dbgSeg = Visualizer( - CFrame.new(subPosition, subPosition + subVelocity), - VisualizeCasts, - VisualizeCastSettings, - VisualizeVariant - ) - if dbgSeg ~= nil then - dbgSeg.Color3 = DBG_SEGMENT_SUB_COLOR - end - - if - canPierceCheckfn == nil - or canPierceCheckfn(cast, subResult, subVelocity, cast.RayInfo.CosmeticBulletObject) == false - then - cast.StateInfo.IsActivelyResimulating = false - SendHit(cast, subResult, subVelocity, cast.RayInfo.CosmeticBulletObject) - if Hitfn then - Hitfn(cast, subResult, subVelocity, cast.RayInfo.CosmeticBulletObject) - end -cast.Caster:TerminateCast(cast, castTerminatingfn) - - local vis = DbgVisualizeHit(CFrame.new(point), false, VisualizeCasts, VisualizeCastSettings) - if vis ~= nil then - vis.Color3 = DBG_HIT_SUB_COLOR - end - return - else - SendPierced(cast, subResult, subVelocity, cast.RayInfo.CosmeticBulletObject) - if Piercedfn then - Piercedfn(cast, subResult, subVelocity, cast.RayInfo.CosmeticBulletObject) - end - - local vis = DbgVisualizeHit(CFrame.new(point), true, VisualizeCasts, VisualizeCastSettings) - if vis ~= nil then - vis.Color3 = DBG_RAYPIERCE_SUB_COLOR - end - end - else - local dbgSeg = Visualizer( - CFrame.new(subPosition, subPosition + subVelocity), - VisualizeCasts, - VisualizeCastSettings, - VisualizeVariant - ) - if dbgSeg ~= nil then - dbgSeg.Color3 = DBG_SEGMENT_SUB_COLOR2 - end - end - - if DebugLogging.Segment then - print("[" .. segmentIndex .. "] Subcast of time increment " .. timeIncrement) - end - end - - cast.StateInfo.IsActivelyResimulating = false - else - if DebugLogging.Hit then - print("Hit was successful. Terminating.") - end - - SendHit(cast, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject) - if Hitfn then - Hitfn(cast, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject) - end - FastCast:TerminateCast(cast, castTerminatingfn) - - DbgVisualizeHit(CFrame.new(point), false, VisualizeCasts, VisualizeCastSettings) - return - end - else - if DebugLogging.RayPierce then - print("Piercing function returned TRUE to pierce this part.") - end - - if rayVisualization ~= nil then - rayVisualization.Color3 = Color3.new(0.4, 0.05, 0.05) - end - DbgVisualizeHit(CFrame.new(point), true, VisualizeCasts, VisualizeCastSettings) - SendPierced(cast, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject) - if Piercedfn then - Piercedfn(cast, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject) - end - end - end - - if cast.StateInfo.DistanceCovered >= cast.RayInfo.MaxDistance then - FastCast:TerminateCast(cast, castTerminatingfn) - DbgVisualizeHit(CFrame.new(currentTarget), false, VisualizeCasts, VisualizeCastSettings) - end +local function CloneCastParams(params: RaycastParams): RaycastParams + local clone: RaycastParams = RaycastParams.new() + clone.CollisionGroup = params.CollisionGroup + clone.FilterType = params.FilterType + clone.FilterDescendantsInstances = params.FilterDescendantsInstances + clone.IgnoreWater = params.IgnoreWater + return clone end ---[=[ - @function createCastData - @within ActiveCastSerial - - Creates a new ActiveCastSerial instance. -]=] -function ActiveCastSerial.createCastData( - BaseCast: TypeDef.BaseCastData, - activeCastID: number, - origin: Vector3, - direction: Vector3, - velocity: Vector3 | number, - behavior: TypeDef.FastCastBehavior, - eventModule: TypeDef.FastCastEventsModule?, - variant: CastVariants -): vaildcast - if typeof(velocity) == "number" then - velocity = direction.Unit * velocity - end - - if behavior.HighFidelitySegmentSize <= 0 then - error("Cannot set FastCastBehavior.HighFidelitySegmentSize <= 0!", 0) - end - - if behavior.HighFidelityBehavior <= 0 then - behavior.HighFidelityBehavior = 1 - elseif behavior.HighFidelityBehavior >= 4 then - behavior.HighFidelityBehavior = 3 - end - - local cast = { - Caster = BaseCast, - StateInfo = { - UpdateConnection = nil, - Paused = false, - TotalRuntime = 0, - DistanceCovered = 0, - HighFidelitySegmentSize = behavior.HighFidelitySegmentSize, - HighFidelityBehavior = behavior.HighFidelityBehavior, - IsActivelySimulatingPierce = false, - IsActivelyResimulating = false, - CancelHighResCast = false, - Trajectories = { - { - StartTime = 0, - EndTime = -1, - Origin = origin, - InitialVelocity = velocity, - Acceleration = behavior.Acceleration, - }, - }, - VisualizeCasts = behavior.VisualizeCasts, - VisualizeCastSettings = behavior.VisualizeCastSettings, - FastCastEventsModuleConfig = { - UseLengthChanged = behavior.FastCastEventsModuleConfig.UseLengthChanged, - UseHit = behavior.FastCastEventsModuleConfig.UseHit, - UsePierced = behavior.FastCastEventsModuleConfig.UsePierced, - UseCastTerminating = behavior.FastCastEventsModuleConfig.UseCastTerminating, - UseCanPierce = behavior.FastCastEventsModuleConfig.UseCanPierce, - }, - FastCastEventsConfig = { - UseLengthChanged = behavior.FastCastEventsConfig.UseLengthChanged, - UseHit = behavior.FastCastEventsConfig.UseHit, - UsePierced = behavior.FastCastEventsConfig.UsePierced, - UseCastTerminating = behavior.FastCastEventsConfig.UseCastTerminating, - }, +local EPSILON = 1e-6 +local RAY_SEARCH_OFFSET = 0.001 +local FIXED_DELTA_TIME = 1 / 240 + +function ActiveCastSerial.new(caster: any, castData: any): any + local self = setmetatable({}, ActiveCastSerial) + + self.Caster = caster + self.StateInfo = { + UpdateConnection = nil, + Paused = false, + TotalRuntime = 0, + DistanceCovered = 0, + HighFidelitySegmentSize = castData.HighFidelitySegmentSize, + HighFidelityBehavior = castData.HighFidelityBehavior, + IsActivelyResimulating = false, + CancelHighResCast = false, + Trajectories = { + { + StartTime = 0, + EndTime = -1, + Origin = castData.Origin, + InitialVelocity = castData.Velocity, + Acceleration = castData.Acceleration, + } }, - RayInfo = { - Parameters = behavior.RaycastParams, - WorldRoot = workspace, - MaxDistance = behavior.MaxDistance or DEFAULT_MAX_DISTANCE, - CosmeticBulletObject = behavior.CosmeticBulletTemplate, - FastCastEventsModule = eventModule - }, - UserData = {}, - Type = CastVariantTypes[variant.CastType], - CFrame = CFrame.new(origin) :: CFrame, - ID = activeCastID - } :: any - - if variant.CastType == EnumCastTypes.Blockcast then - cast.RayInfo.Size = (variant :: BlockcastVariant).Size - elseif variant.CastType == EnumCastTypes.Spherecast then - cast.RayInfo.Radius = (variant :: SpherecastVariant).Radius - end - - if behavior.UserData then - cast.UserData = behavior.UserData - end - - if cast.RayInfo.Parameters ~= nil then - cast.RayInfo.Parameters = CloneCastParams(cast.RayInfo.Parameters) - else - cast.RayInfo.Parameters = RaycastParams.new() - end - - local targetContainer: Instance? - if cast.Caster.ObjectCache then - cast.RayInfo.CosmeticBulletObject = cast.Caster.ObjectCache:Invoke(CFrame.new(origin, origin + direction)) - targetContainer = cast.Caster.CacheHolder - else - if cast.RayInfo.CosmeticBulletObject ~= nil then - local basePart = cast.RayInfo.CosmeticBulletObject - basePart = basePart:Clone() - basePart.CFrame = CFrame.new(origin, origin + direction) - basePart.Parent = behavior.CosmeticBulletContainer - cast.RayInfo.CosmeticBulletObject = basePart - end - - if behavior.CosmeticBulletContainer then - targetContainer = behavior.CosmeticBulletContainer - end - end - - if behavior.AutoIgnoreContainer == true and targetContainer ~= nil then - local ignoreList = cast.RayInfo.Parameters.FilterDescendantsInstances - if not table.find(ignoreList, targetContainer) then - table.insert(ignoreList, targetContainer) - cast.RayInfo.Parameters.FilterDescendantsInstances = ignoreList - end - end - - local event = RS.Heartbeat - - local FastCastEvents: TypeDef.FastCastEvents = eventModule and require(eventModule) or nil - - local function Stepped(delta: number) - if cast.StateInfo.Paused then return end - - if DebugLogging.Casting then - print("Casting for frame.") - end - - local Cast_timeAtStart = tick() + VisualizeCasts = castData.VisualizeCasts, + VisualizeCastSettings = castData.VisualizeCastSettings + } - local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + self.RayInfo = { + Parameters = castData.RaycastParams, + WorldRoot = workspace, + MaxDistance = castData.MaxDistance or DEFAULT_MAX_DISTANCE, + CosmeticBulletObject = castData.CosmeticBulletObject + } - if typeof(latestTrajectory.Acceleration) ~= "Vector3" then - latestTrajectory.Acceleration = Vector3.new() - end + self.Type = CastVariantTypes[castData.CastType] + self.CFrame = CFrame.new(castData.Origin) + self.ID = castData.ID - if - cast.StateInfo.HighFidelityBehavior == FastCastEnums.HighFidelityBehavior.Always - and cast.StateInfo.HighFidelitySegmentSize > 0 - then - local Segment_timeAtStart = tick() - - local castTerminatingfn: TypeDef.OnCastTerminatingFunction? = nil - if FastCastEvents then - castTerminatingfn = cast.StateInfo.FastCastEventsModuleConfig.UseCastTerminating - and FastCastEvents.CastTerminating - or nil - end - if cast.StateInfo.IsActivelyResimulating then - FastCast:TerminateCast(cast, castTerminatingfn) - warn("Cascading cast lag encountered!") - return - end - - cast.StateInfo.IsActivelyResimulating = true - - local origin = latestTrajectory.Origin - local totalDelta = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime - local initialVelocity = latestTrajectory.InitialVelocity - local acceleration = latestTrajectory.Acceleration - - local lastPoint = GetPositionAtTime(totalDelta, origin, initialVelocity, acceleration) - cast.StateInfo.TotalRuntime += delta - totalDelta = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime - - local currentPoint = GetPositionAtTime(totalDelta, origin, initialVelocity, acceleration) - local currentVelocity = GetVelocityAtTime(totalDelta, initialVelocity, acceleration) - local totalDisplacement = currentPoint - lastPoint - - local rayDir = totalDisplacement.Unit * currentVelocity.Magnitude * delta - - local targetWorldRoot = cast.RayInfo.WorldRoot - local CastHandler = castHandlers[variant.CastType] - - local resultOfCast = CastHandler(targetWorldRoot, lastPoint, rayDir, cast.RayInfo.Parameters, variant) - - local point = currentPoint - if resultOfCast ~= nil then - point = resultOfCast.Position - end - - local rayDisplacement = (point - lastPoint).Magnitude - cast.StateInfo.TotalRuntime -= delta - - local numSegmentsDecimal = rayDisplacement / cast.StateInfo.HighFidelitySegmentSize - local numSegmentsReal = math.floor(numSegmentsDecimal) - if numSegmentsReal == 0 then - numSegmentsReal = 1 - end - - local timeIncrement = delta / numSegmentsReal - - if DebugLogging.Calculation then - print("Performing subcast! Time increment: " .. timeIncrement .. ", num segments: " .. numSegmentsReal) - end - - for segmentIndex = 1, numSegmentsReal do - if next(cast) == nil then return end - if cast.StateInfo.CancelHighResCast then - cast.StateInfo.CancelHighResCast = false - break - end - - if DebugLogging.Segment then - print("[" .. segmentIndex .. "] Subcast of time increment " .. timeIncrement) - end - - SimulateCast(cast, timeIncrement, FastCastEvents, variant) - end - - if next(cast) == nil then return end - cast.StateInfo.IsActivelyResimulating = false - - if - behavior.AutomaticPerformance - and (tick() - Segment_timeAtStart) > MAX_SEGMENT_CAL_TIME - and cast.StateInfo - then - local HighFideSizeAmount = behavior.AdaptivePerformance.HighFidelitySegmentSizeIncrease or HIGH_FIDE_INCREASE_SIZE - if DebugLogging.AutomaticPerformance then - warn("AutomaticPerformance increasing size of HighFidelitySize by: ", HighFideSizeAmount) - end - cast.StateInfo.HighFidelitySegmentSize += HighFideSizeAmount - end - else - SimulateCast(cast, delta, FastCastEvents, variant) - end - - if - behavior.AutomaticPerformance - and behavior.AdaptivePerformance.LowerHighFidelityBehavior - and (tick() - Cast_timeAtStart) > MAX_CASTING_TIME - and cast.StateInfo - then - if cast.StateInfo.HighFidelityBehavior > 1 then - cast.StateInfo.HighFidelityBehavior -= 1 - end - end + if castData.CastType == EnumCastTypes.Blockcast then + self.RayInfo.Size = castData.Size + elseif castData.CastType == EnumCastTypes.Spherecast then + self.RayInfo.Radius = castData.Radius end - cast.StateInfo.UpdateConnection = event:Connect(Stepped) - - return cast + return self end return ActiveCastSerial \ No newline at end of file diff --git a/src/FastCast2/BaseCastSerial.luau b/src/FastCast2/BaseCastSerial.luau index a1ff4d8..d4072ed 100644 --- a/src/FastCast2/BaseCastSerial.luau +++ b/src/FastCast2/BaseCastSerial.luau @@ -2,6 +2,8 @@ - Author : Mawin CK - Date : 2025 -- Version : 0.0.9 + + BaseCastSerial - Uses SerialSimulation with SoA pattern ]] local RS = game:GetService("RunService") @@ -12,6 +14,7 @@ local FastCastM = require(FastCast2) local FastCastEnums = require(FastCast2:WaitForChild("FastCastEnums")) local TypeDef = require(FastCast2:WaitForChild("TypeDefinitions")) local ActiveCastSerial = require(FastCast2:WaitForChild("ActiveCastSerial")) +local SerialSimulation = require(FastCast2:WaitForChild("SerialSimulation")) local EnumCastTypes = FastCastEnums.CastType @@ -19,6 +22,7 @@ local EnumCastTypes = FastCastEnums.CastType @class BaseCastSerial Base class for Serial (non-parallel) Raycast operations. + Uses SerialSimulation with SoA pattern for performance. ]=] local BaseCastSerial = {} @@ -26,103 +30,36 @@ BaseCastSerial.__index = BaseCastSerial BaseCastSerial.__type = "BaseCastSerial" local BulkMoveToConnection: RBXScriptConnection? = nil -local Actives: any = {} local Output: BindableEvent? = nil -local ActiveCastCleaner: BindableEvent? = nil local ObjectCache: BindableFunction? = nil local NextProjectileID = 0 -local SyncChanges: BindableEvent? = nil -local CastFireFunc = nil local ParentCaster = nil -local function HandleBulkMoveTo() - local Parts: { BasePart } = {} - local CFrames: { CFrame } = {} - - for _, ActiveCasts in Actives do - local ProjectilePart = ActiveCasts.RayInfo.CosmeticBulletObject - if not ProjectilePart then continue end - - local resultCFrame = ActiveCasts.CFrame - if ProjectilePart:IsA("BasePart") then - table.insert(Parts, ProjectilePart) - table.insert(CFrames, resultCFrame) - else - ProjectilePart:PivotTo(resultCFrame) - end - end - - task.synchronize() - workspace:BulkMoveTo(Parts, CFrames, Enum.BulkMoveMode.FireCFrameChanged) -end - -local function SendCastFire( - cast: TypeDef.ActiveCastData, - origin: Vector3, - direction: Vector3, - velocity: Vector3 | number, - behavior: TypeDef.FastCastBehavior -) - if Output then - Output:Fire("CastFire", cast, origin, direction, velocity, behavior) - end -end - --[=[ @function Init @within BaseCastSerial - - @param BindableOutput BindableEvent -- The BindableEvent used for outputting events. - @param Data any -- Configuration data for the BaseCastSerial. - @return BaseCastSerial -- The initialized BaseCastSerial instance. ]=] function BaseCastSerial.Init(BindableOutput: BindableEvent, Data: any, parentCaster: any) local self = setmetatable({}, BaseCastSerial) - Actives = setmetatable({}, { __mode = "v" }) Output = BindableOutput ParentCaster = parentCaster - local BindableCleaner = Instance.new("BindableEvent") - BindableCleaner.Name = "ActiveCastDestroyer" - BindableCleaner.Parent = script - - if Data.useObjectCache then - local BindableObjectCache = Instance.new("BindableFunction") - BindableObjectCache.Parent = script - BindableObjectCache.Name = "ActiveCastObjectCache" - ObjectCache = BindableObjectCache - end - if Data.useBulkMoveTo then - BulkMoveToConnection = RS.PreRender:Connect(HandleBulkMoveTo) + -- BulkMoveTo is handled by SerialSimulation end - ActiveCastCleaner = BindableCleaner - - ActiveCastCleaner.Event:Connect(function(activeCastID: number) - if Actives[activeCastID] then - Actives[activeCastID] = nil - end - end) - - SyncChanges = Instance.new("BindableEvent") - SyncChanges.Name = "SyncChanges" - SyncChanges.Parent = script - - SyncChanges.Event:Connect(function(cast: TypeDef.ActiveCastData) - local ID = cast.ID - local TargetCast = Actives[ID] - - if TargetCast then - for i, v in cast do - TargetCast[i] = v - end - end - end) - return self end +local function CloneCastParams(params: RaycastParams): RaycastParams + local clone: RaycastParams = RaycastParams.new() + clone.CollisionGroup = params.CollisionGroup + clone.FilterType = params.FilterType + clone.FilterDescendantsInstances = params.FilterDescendantsInstances + clone.IgnoreWater = params.IgnoreWater + return clone +end + --[=[ @method Raycast @within BaseCastSerial @@ -135,17 +72,44 @@ function BaseCastSerial:Raycast( ) NextProjectileID += 1 - Actives[NextProjectileID] = ActiveCastSerial.createCastData(self, { - Output = Output, - ActiveCastCleaner = ActiveCastCleaner, - ObjectCache = ObjectCache, - SyncChange = SyncChanges - }, NextProjectileID, Origin, Direction, Velocity, Behavior, nil, { - CastType = EnumCastTypes.Raycast - } :: any) - - if Behavior.FastCastEventsConfig.UseCastFire then - SendCastFire(Actives[NextProjectileID], Origin, Direction, Velocity, Behavior) + if typeof(Velocity) == "number" then + Velocity = Direction.Unit * Velocity + end + + local raycastParams = Behavior.RaycastParams + if raycastParams then + raycastParams = CloneCastParams(raycastParams) + else + raycastParams = RaycastParams.new() + end + + local cosmeticBullet = Behavior.CosmeticBulletTemplate + if cosmeticBullet then + cosmeticBullet = cosmeticBullet:Clone() + cosmeticBullet.CFrame = CFrame.new(Origin, Origin + Direction) + cosmeticBullet.Parent = Behavior.CosmeticBulletContainer + end + + local castData = { + ID = NextProjectileID, + Origin = Origin, + Velocity = Velocity, + Acceleration = Behavior.Acceleration, + RaycastParams = raycastParams, + MaxDistance = Behavior.MaxDistance or 1000, + CosmeticBulletObject = cosmeticBullet, + CastType = EnumCastTypes.Raycast, + VisualizeCasts = Behavior.VisualizeCasts, + VisualizeCastSettings = Behavior.VisualizeCastSettings, + HighFidelitySegmentSize = Behavior.HighFidelitySegmentSize, + HighFidelityBehavior = Behavior.HighFidelityBehavior + } + + local cast = ActiveCastSerial.new(ParentCaster, castData) + SerialSimulation.Register(cast) + + if Output then + Output:Fire("CastFire", cast, Origin, Direction, Velocity, Behavior) end end @@ -162,18 +126,45 @@ function BaseCastSerial:Blockcast( ) NextProjectileID += 1 - Actives[NextProjectileID] = ActiveCastSerial.createCastData(self, { - Output = Output, - ActiveCastCleaner = ActiveCastCleaner, - ObjectCache = ObjectCache, - SyncChange = SyncChanges - }, NextProjectileID, Origin, Direction, Velocity, Behavior, nil, { + if typeof(Velocity) == "number" then + Velocity = Direction.Unit * Velocity + end + + local raycastParams = Behavior.RaycastParams + if raycastParams then + raycastParams = CloneCastParams(raycastParams) + else + raycastParams = RaycastParams.new() + end + + local cosmeticBullet = Behavior.CosmeticBulletTemplate + if cosmeticBullet then + cosmeticBullet = cosmeticBullet:Clone() + cosmeticBullet.CFrame = CFrame.new(Origin, Origin + Direction) + cosmeticBullet.Parent = Behavior.CosmeticBulletContainer + end + + local castData = { + ID = NextProjectileID, + Origin = Origin, + Velocity = Velocity, + Acceleration = Behavior.Acceleration, + RaycastParams = raycastParams, + MaxDistance = Behavior.MaxDistance or 1000, + CosmeticBulletObject = cosmeticBullet, CastType = EnumCastTypes.Blockcast, - Size = Size - } :: any) + Size = Size, + VisualizeCasts = Behavior.VisualizeCasts, + VisualizeCastSettings = Behavior.VisualizeCastSettings, + HighFidelitySegmentSize = Behavior.HighFidelitySegmentSize, + HighFidelityBehavior = Behavior.HighFidelityBehavior + } + + local cast = ActiveCastSerial.new(ParentCaster, castData) + SerialSimulation.Register(cast) - if Behavior.FastCastEventsConfig.UseCastFire then - SendCastFire(Actives[NextProjectileID], Origin, Direction, Velocity, Behavior) + if Output then + Output:Fire("CastFire", cast, Origin, Direction, Velocity, Behavior) end end @@ -190,18 +181,45 @@ function BaseCastSerial:Spherecast( ) NextProjectileID += 1 - Actives[NextProjectileID] = ActiveCastSerial.createCastData(self, { - Output = Output, - ActiveCastCleaner = ActiveCastCleaner, - ObjectCache = ObjectCache, - SyncChange = SyncChanges - }, NextProjectileID, Origin, Direction, Velocity, Behavior, nil, { + if typeof(Velocity) == "number" then + Velocity = Direction.Unit * Velocity + end + + local raycastParams = Behavior.RaycastParams + if raycastParams then + raycastParams = CloneCastParams(raycastParams) + else + raycastParams = RaycastParams.new() + end + + local cosmeticBullet = Behavior.CosmeticBulletTemplate + if cosmeticBullet then + cosmeticBullet = cosmeticBullet:Clone() + cosmeticBullet.CFrame = CFrame.new(Origin, Origin + Direction) + cosmeticBullet.Parent = Behavior.CosmeticBulletContainer + end + + local castData = { + ID = NextProjectileID, + Origin = Origin, + Velocity = Velocity, + Acceleration = Behavior.Acceleration, + RaycastParams = raycastParams, + MaxDistance = Behavior.MaxDistance or 1000, + CosmeticBulletObject = cosmeticBullet, CastType = EnumCastTypes.Spherecast, - Radius = Radius - } :: any) + Radius = Radius, + VisualizeCasts = Behavior.VisualizeCasts, + VisualizeCastSettings = Behavior.VisualizeCastSettings, + HighFidelitySegmentSize = Behavior.HighFidelitySegmentSize, + HighFidelityBehavior = Behavior.HighFidelityBehavior + } - if Behavior.FastCastEventsConfig.UseCastFire then - SendCastFire(Actives[NextProjectileID], Origin, Direction, Velocity, Behavior) + local cast = ActiveCastSerial.new(ParentCaster, castData) + SerialSimulation.Register(cast) + + if Output then + Output:Fire("CastFire", cast, Origin, Direction, Velocity, Behavior) end end @@ -209,17 +227,8 @@ end @method BindBulkMoveTo @within BaseCastSerial ]=] -function BaseCastSerial:BindBulkMoveTo(bool: boolean) - if bool then - if not BulkMoveToConnection then - BulkMoveToConnection = RS.PreRender:Connect(HandleBulkMoveTo) - end - else - if BulkMoveToConnection then - BulkMoveToConnection:Disconnect() - BulkMoveToConnection = nil - end - end +function BaseCastSerial:BindBulkMoveTo(enabled: boolean) + -- BulkMoveTo is now handled by SerialSimulation directly end --[=[ @@ -229,10 +238,8 @@ end function BaseCastSerial:BindObjectCache(bool: boolean) if bool then if ObjectCache then return end - local BindableObjectCache = Instance.new("BindableFunction") - BindableObjectCache.Parent = script - BindableObjectCache.Name = "ActiveCastObjectCache" - ObjectCache = BindableObjectCache + ObjectCache = Instance.new("BindableFunction") + ObjectCache.Name = "ObjectCache" else if ObjectCache then ObjectCache:Destroy() @@ -245,9 +252,15 @@ end @method TerminateCast @within BaseCastSerial ]=] -function BaseCastSerial:TerminateCast(cast: vaildcast, castTerminatingFunction: TypeDef.OnCastTerminatingFunction?) - if ParentCaster and ParentCaster.TerminateCast then - ParentCaster:TerminateCast(cast, castTerminatingFunction) +function BaseCastSerial:TerminateCast(cast: any, castTerminatingFunction: TypeDef.OnCastTerminatingFunction?) + if cast and cast.ID then + SerialSimulation.Terminate(cast.ID) + end + if castTerminatingFunction then + castTerminatingFunction(cast) + end + if Output then + Output:Fire("CastTerminating", cast) end end @@ -261,15 +274,7 @@ function BaseCastSerial:Destroy() BulkMoveToConnection = nil end - FastCastEventsModule = nil - - for _, v in Actives do - if ParentCaster and ParentCaster.TerminateCast then - ParentCaster:TerminateCast(v) - end - end - - Actives = {} + Output = nil ParentCaster = nil setmetatable(self, nil) end diff --git a/src/FastCast2/SerialSimulation.luau b/src/FastCast2/SerialSimulation.luau index 8d9335a..dc8b659 100644 --- a/src/FastCast2/SerialSimulation.luau +++ b/src/FastCast2/SerialSimulation.luau @@ -3,8 +3,9 @@ - Date : 2025 -- Version : 0.0.9 - SerialSimulation manages all active casts with a single RunService connection. - Uses SoA (Structure of Arrays) for better cache performance. + SerialSimulation - Single RunService handling multiple ActiveCastSerial + Uses SoA pattern for performance, queue technique for events + Like SwiftCast implementation ]] local RS = game:GetService("RunService") @@ -14,104 +15,83 @@ local TypeDef = require(FastCastModule:WaitForChild("TypeDefinitions")) local Configs = require(FastCastModule:WaitForChild("Configs")) local DebugLogging = Configs.DebugLogging local FastCastEnums = require(FastCastModule:WaitForChild("FastCastEnums")) +local ActiveCastSerial = require(FastCastModule:WaitForChild("ActiveCastSerial")) local EnumCastTypes = FastCastEnums.CastType - +local HIGH_FIDE_INCREASE_SIZE = 0.5 local MAX_SEGMENT_CAL_TIME = 0.016 * 5 local MAX_CASTING_TIME = 0.2 local DEFAULT_MAX_DISTANCE = 1000 -local HIGH_FIDE_INCREASE_SIZE = 0.5 - local EPSILON = 1e-6 +local RAY_SEARCH_OFFSET = 0.001 + +local DBG_SEGMENT_SUB_COLOR = Color3.new(0.286275, 0.329412, 0.247059) +local DBG_SEGMENT_SUB_COLOR2 = Color3.new(0.454902, 0.933333, 0.011765) +local DBG_HIT_SUB_COLOR = Color3.new(0.0588235, 0.87451, 1) +local DBG_RAYPIERCE_SUB_COLOR = Color3.new(1, 0.113725, 0.588235) --- Cast handlers local castHandlers = { - [EnumCastTypes.Raycast] = function( - targetWorldRoot: WorldRoot, - origin: Vector3, - direction: Vector3, - parameters: RaycastParams - ) - return targetWorldRoot:Raycast(origin, direction, parameters) + [EnumCastTypes.Raycast] = function(targetWorldRoot: WorldRoot, origin: Vector3, direction: Vector3, params: RaycastParams) + return targetWorldRoot:Raycast(origin, direction, params) end, - [EnumCastTypes.Blockcast] = function( - targetWorldRoot: WorldRoot, - origin: Vector3, - direction: Vector3, - parameters: RaycastParams, - size: Vector3 - ) - return targetWorldRoot:Blockcast(CFrame.new(origin), size, direction, parameters) + [EnumCastTypes.Blockcast] = function(targetWorldRoot: WorldRoot, origin: Vector3, direction: Vector3, params: RaycastParams, size: Vector3) + return targetWorldRoot:Blockcast(CFrame.new(origin), size, direction, params) end, - [EnumCastTypes.Spherecast] = function( - targetWorldRoot: WorldRoot, - origin: Vector3, - direction: Vector3, - parameters: RaycastParams, - radius: number - ) - return targetWorldRoot:Spherecast(origin, radius, direction, parameters) + [EnumCastTypes.Spherecast] = function(targetWorldRoot: WorldRoot, origin: Vector3, direction: Vector3, params: RaycastParams, radius: number) + return targetWorldRoot:Spherecast(origin, radius, direction, params) end } --- SoA structure for active casts -type SerialSimulation = { - ActiveCasts: { any }, - StepConnection: RBXScriptConnection, - IsRunning: boolean, +local function GetPositionAtTime(t: number, origin: Vector3, velocity: Vector3, accel: Vector3): Vector3 + local force = Vector3.new( + (accel.X * t ^ 2) / 2, + (accel.Y * t ^ 2) / 2, + (accel.Z * t ^ 2) / 2 + ) + return origin + (velocity * t) + force +end - Register: (self: SerialSimulation, cast: any) -> (), - Unregister: (self: SerialSimulation, castID: number) -> (), - Start: (self: SerialSimulation) -> (), - Stop: (self: SerialSimulation) -> (), -} +local function GetVelocityAtTime(time: number, velocity: Vector3, accel: Vector3): Vector3 + return velocity + accel * time +end -local SerialSimulation = {} -SerialSimulation.__index = SerialSimulation +local function OnError(err: string) + warn(debug.traceback(err, 2)) +end --- SoA arrays for cast data +-- SoA Arrays local castCount = 0 -local castIDs = {} :: { number } -local castOrigin = {} :: { Vector3 } -local castDirection = {} :: { Vector3 } -local castVelocity = {} :: { Vector3 } -local castAcceleration = {} :: { Vector3 } -local castTotalRuntime = {} :: { number } -local castDistanceCovered = {} :: { number } -local castMaxDistance = {} :: { number } -local castPaused = {} :: { boolean } -local castHighFidelitySegmentSize = {} :: { number } -local castHighFidelityBehavior = {} :: { number } -local castIsActivelyResimulating = {} :: { boolean } -local castCancelHighResCast = {} :: { boolean } -local castCFrame = {} :: { CFrame } -local castWorldRoot = {} :: { WorldRoot } -local castRaycastParams = {} :: { RaycastParams } -local castCosmeticBulletObject = {} :: { Instance? } -local castCastType = {} :: { number } -local castSize = {} :: { Vector3? } -local castRadius = {} :: { number? } -local castVisualizeCasts = {} :: { boolean } -local castCaster = {} :: { any } +local casts = {} :: { [number]: any } +local castIDs = {} :: { [number]: number } +local castOrigin = {} :: { [number]: Vector3 } +local castVelocity = {} :: { [number]: Vector3 } +local castAcceleration = {} :: { [number]: Vector3 } +local castTotalRuntime = {} :: { [number]: number } +local castDistanceCovered = {} :: { [number]: number } +local castMaxDistance = {} :: { [number]: number } +local castPaused = {} :: { [number]: boolean } +local castHighFidelitySegmentSize = {} :: { [number]: number } +local castHighFidelityBehavior = {} :: { [number]: number } +local castIsActivelyResimulating = {} :: { [number]: boolean } +local castCancelHighResCast = {} :: { [number]: boolean } +local castCFrame = {} :: { [number]: CFrame } +local castWorldRoot = {} :: { [number]: WorldRoot } +local castRaycastParams = {} :: { [number]: RaycastParams } +local castCosmeticBullet = {} :: { [number]: Instance? } +local castCastType = {} :: { [number]: number } +local castSize = {} :: { [number]: Vector3? } +local castRadius = {} :: { [number]: number? } +local castVisualize = {} :: { [number]: boolean } +local castVisualizeSettings = {} :: { [number]: any } +local castCaster = {} :: { [number]: any } -- Event queue -local QueuedEvents = {} :: { any } - -local function GetPositionAtTime(t: number, origin: Vector3, initialVelocity: Vector3, acceleration: Vector3): Vector3 - local force = Vector3.new( - (acceleration.X * t ^ 2) / 2, - (acceleration.Y * t ^ 2) / 2, - (acceleration.Z * t ^ 2) / 2 - ) - return origin + (initialVelocity * t) + force -end +local QueuedEvents = {} :: { { Callback: any, Args: { any } } } -local function GetVelocityAtTime(time: number, initialVelocity: Vector3, acceleration: Vector3): Vector3 - return initialVelocity + acceleration * time -end - -local function OnError(errorMessage: string) - warn(debug.traceback(errorMessage, 2)) +local function QueueEvent(callback: any, ...) + if callback then + table.insert(QueuedEvents, { Callback = callback, Args = { ... } }) + end end local function DispatchEvent(callback: any, ...) @@ -124,12 +104,6 @@ local function DispatchEvent(callback: any, ...) end end -local function QueueEvent(callback: any, ...) - if callback then - table.insert(QueuedEvents, { Callback = callback, Args = { ... } }) - end -end - local function DispatchAllEvents() for _, event in QueuedEvents do DispatchEvent(event.Callback, unpack(event.Args)) @@ -137,20 +111,24 @@ local function DispatchAllEvents() table.clear(QueuedEvents) end -function SerialSimulation.new(): SerialSimulation - local self = setmetatable({}, SerialSimulation) - self.ActiveCasts = {} - self.IsRunning = false - return self +local function QueueFire(caster: any, eventName: string, ...) + if caster and caster.Output then + caster.Output:Fire(eventName, ...) + end end -function SerialSimulation:Register(cast: any) +local SerialSimulation = { + IsRunning = false, + StepConnection = nil +} + +function SerialSimulation.Register(cast: any) castCount += 1 local id = castCount + casts[id] = cast castIDs[id] = cast.ID castOrigin[id] = cast.StateInfo.Trajectories[1].Origin - castDirection[id] = cast.StateInfo.Trajectories[1].InitialVelocity.Unit castVelocity[id] = cast.StateInfo.Trajectories[1].InitialVelocity castAcceleration[id] = cast.StateInfo.Trajectories[1].Acceleration castTotalRuntime[id] = 0 @@ -164,9 +142,10 @@ function SerialSimulation:Register(cast: any) castCFrame[id] = cast.CFrame castWorldRoot[id] = cast.RayInfo.WorldRoot castRaycastParams[id] = cast.RayInfo.Parameters - castCosmeticBulletObject[id] = cast.RayInfo.CosmeticBulletObject - castCastType[id] = cast.StateInfo.Trajectories[1].CastType or 1 - castVisualizeCasts[id] = cast.StateInfo.VisualizeCasts + castCosmeticBullet[id] = cast.RayInfo.CosmeticBulletObject + castCastType[id] = cast.Type == "Blockcast" and EnumCastTypes.Blockcast or (cast.Type == "Spherecast" and EnumCastTypes.Spherecast or EnumCastTypes.Raycast) + castVisualize[id] = cast.StateInfo.VisualizeCasts + castVisualizeSettings[id] = cast.StateInfo.VisualizeCastSettings castCaster[id] = cast.Caster if cast.RayInfo.Size then @@ -177,97 +156,106 @@ function SerialSimulation:Register(cast: any) end cast.ID = id - self.ActiveCasts[id] = cast end -function SerialSimulation:Unregister(castID: number) - if not self.ActiveCasts[castID] then return end - - local lastID = castCount - if castID ~= lastID then - -- Swap with last element - castIDs[castID] = castIDs[lastID] - castOrigin[castID] = castOrigin[lastID] - castDirection[castID] = castDirection[lastID] - castVelocity[castID] = castVelocity[lastID] - castAcceleration[castID] = castAcceleration[lastID] - castTotalRuntime[castID] = castTotalRuntime[lastID] - castDistanceCovered[castID] = castDistanceCovered[lastID] - castMaxDistance[castID] = castMaxDistance[lastID] - castPaused[castID] = castPaused[lastID] - castHighFidelitySegmentSize[castID] = castHighFidelitySegmentSize[lastID] - castHighFidelityBehavior[castID] = castHighFidelityBehavior[lastID] - castIsActivelyResimulating[castID] = castIsActivelyResimulating[lastID] - castCancelHighResCast[castID] = castCancelHighResCast[lastID] - castCFrame[castID] = castCFrame[lastID] - castWorldRoot[castID] = castWorldRoot[lastID] - castRaycastParams[castID] = castRaycastParams[lastID] - castCosmeticBulletObject[castID] = castCosmeticBulletObject[lastID] - castCastType[castID] = castCastType[lastID] - castSize[castID] = castSize[lastID] - castRadius[castID] = castRadius[lastID] - castVisualizeCasts[castID] = castVisualizeCasts[castID] - castCaster[castID] = castCaster[lastID] - - local movedCast = self.ActiveCasts[lastID] - if movedCast then - movedCast.ID = castID +function SerialSimulation.Unregister(id: number) + if not casts[id] then return end + + local lastId = castCount + if id ~= lastId then + castIDs[id] = castIDs[lastId] + castOrigin[id] = castOrigin[lastId] + castVelocity[id] = castVelocity[lastId] + castAcceleration[id] = castAcceleration[lastId] + castTotalRuntime[id] = castTotalRuntime[lastId] + castDistanceCovered[id] = castDistanceCovered[lastId] + castMaxDistance[id] = castMaxDistance[lastId] + castPaused[id] = castPaused[lastId] + castHighFidelitySegmentSize[id] = castHighFidelitySegmentSize[lastId] + castHighFidelityBehavior[id] = castHighFidelityBehavior[lastId] + castIsActivelyResimulating[id] = castIsActivelyResimulating[lastId] + castCancelHighResCast[id] = castCancelHighResCast[lastId] + castCFrame[id] = castCFrame[lastId] + castWorldRoot[id] = castWorldRoot[lastId] + castRaycastParams[id] = castRaycastParams[lastId] + castCosmeticBullet[id] = castCosmeticBullet[lastId] + castCastType[id] = castCastType[lastId] + castSize[id] = castSize[lastId] + castRadius[id] = castRadius[lastId] + castVisualize[id] = castVisualize[lastId] + castVisualizeSettings[id] = castVisualizeSettings[lastId] + castCaster[id] = castCaster[lastId] + + if casts[lastId] then + casts[lastId].ID = id end end - -- Clear last slot - castIDs[lastID] = nil - castOrigin[lastID] = nil - castDirection[lastID] = nil - castVelocity[lastID] = nil - castAcceleration[lastID] = nil - castTotalRuntime[lastID] = nil - castDistanceCovered[lastID] = nil - castMaxDistance[lastID] = nil - castPaused[lastID] = nil - castHighFidelitySegmentSize[lastID] = nil - castHighFidelityBehavior[lastID] = nil - castIsActivelyResimulating[lastID] = nil - castCancelHighResCast[lastID] = nil - castCFrame[lastID] = nil - castWorldRoot[lastID] = nil - castRaycastParams[lastID] = nil - castCosmeticBulletObject[lastID] = nil - castCastType[lastID] = nil - castSize[lastID] = nil - castRadius[lastID] = nil - castVisualizeCasts[lastID] = nil - castCaster[lastID] = nil - - self.ActiveCasts[castID] = nil - castCount = lastID - 1 + castIDs[lastId] = nil + castOrigin[lastId] = nil + castVelocity[lastId] = nil + castAcceleration[lastId] = nil + castTotalRuntime[lastId] = nil + castDistanceCovered[lastId] = nil + castMaxDistance[lastId] = nil + castPaused[lastId] = nil + castHighFidelitySegmentSize[lastId] = nil + castHighFidelityBehavior[lastId] = nil + castIsActivelyResimulating[lastId] = nil + castCancelHighResCast[lastId] = nil + castCFrame[id] = nil + castWorldRoot[lastId] = nil + castRaycastParams[lastId] = nil + castCosmeticBullet[lastId] = nil + castCastType[lastId] = nil + castSize[lastId] = nil + castRadius[lastId] = nil + castVisualize[lastId] = nil + castVisualizeSettings[lastId] = nil + castCaster[lastId] = nil + + casts[id] = nil + castCount = lastId - 1 +end + +function SerialSimulation.PauseCast(id: number, paused: boolean) + castPaused[id] = paused +end + +function SerialSimulation.Terminate(id: number) + local bullet = castCosmeticBullet[id] + if bullet then + bullet:Destroy() + end + SerialSimulation.Unregister(id) end -local function UpdateAllCasts(deltaTime: number) +local function UpdateCasts(deltaTime: number) if castCount == 0 then return end - local destroyedCastIDs = {} :: { number } + local destroyedIds = {} :: { number } for i = 1, castCount do if castPaused[i] then continue end + local caster = castCaster[i] local castType = castCastType[i] local CastHandler = castHandlers[castType] local origin = castOrigin[i] local totalDelta = castTotalRuntime[i] - local initialVelocity = castVelocity[i] + local velocity = castVelocity[i] local acceleration = castAcceleration[i] - local lastPoint = GetPositionAtTime(totalDelta, origin, initialVelocity, acceleration) - + local lastPosition = GetPositionAtTime(totalDelta, origin, velocity, acceleration) castTotalRuntime[i] += deltaTime totalDelta = castTotalRuntime[i] - local currentTarget = GetPositionAtTime(totalDelta, origin, initialVelocity, acceleration) - local segmentVelocity = GetVelocityAtTime(totalDelta, initialVelocity, acceleration) - local totalDisplacement = currentTarget - lastPoint - local rayDir = totalDisplacement.Unit * segmentVelocity.Magnitude * deltaTime + local currentPosition = GetPositionAtTime(totalDelta, origin, velocity, acceleration) + local currentVelocity = GetVelocityAtTime(totalDelta, velocity, acceleration) + local displacement = currentPosition - lastPosition + + local rayDir = displacement.Unit * currentVelocity.Magnitude * deltaTime local variant = {} if castType == EnumCastTypes.Blockcast then @@ -276,83 +264,70 @@ local function UpdateAllCasts(deltaTime: number) variant.Radius = castRadius[i] end - local resultOfCast = CastHandler(castWorldRoot[i], lastPoint, rayDir, castRaycastParams[i], variant) + local result = CastHandler(castWorldRoot[i], lastPosition, rayDir, castRaycastParams[i], variant) - local point = currentTarget - if resultOfCast ~= nil then - point = resultOfCast.Position + local hitPoint = currentPosition + local hitPart = nil + if result then + hitPoint = result.Position + hitPart = result.Instance end - local rayDisplacement = (point - lastPoint).Magnitude + local rayDisplacement = (hitPoint - lastPosition).Magnitude castDistanceCovered[i] += rayDisplacement - castCFrame[i] = CFrame.new(lastPoint, lastPoint + rayDir) * CFrame.new(0, 0, -rayDisplacement / 2) - -- Fire LengthChanged event - local caster = castCaster[i] - if caster and caster.Output then - caster.Output:Fire( - "LengthChanged", - self.ActiveCasts[i], - lastPoint, - rayDir.Unit, - rayDisplacement, - segmentVelocity, - castCosmeticBulletObject[i] - ) - end + local newCFrame = CFrame.new(lastPosition, lastPosition + rayDir) * CFrame.new(0, 0, -rayDisplacement / 2) + castCFrame[i] = newCFrame -- Update cosmetic bullet - local bullet = castCosmeticBulletObject[i] + local bullet = castCosmeticBullet[i] if bullet then if bullet:IsA("BasePart") then - bullet.CFrame = castCFrame[i] + bullet.CFrame = newCFrame else - bullet:PivotTo(castCFrame[i]) + bullet:PivotTo(newCFrame) end end - -- Handle hit - if resultOfCast ~= nil and resultOfCast.Instance ~= castCosmeticBulletObject[i] then - local caster = castCaster[i] - if caster and caster.Output then - caster.Output:Fire("Hit", self.ActiveCasts[i], resultOfCast, segmentVelocity, castCosmeticBulletObject[i]) - end + -- Fire LengthChanged + QueueFire(caster, "LengthChanged", casts[i], lastPosition, rayDir.Unit, rayDisplacement, currentVelocity, bullet) - -- Destroy cast - table.insert(destroyedCastIDs, i) + -- Handle hit + if result and hitPart ~= bullet then + QueueFire(caster, "Hit", casts[i], result, currentVelocity, bullet) + table.insert(destroyedIds, i) end -- Check max distance if castDistanceCovered[i] >= castMaxDistance[i] then - table.insert(destroyedCastIDs, i) + table.insert(destroyedIds, i) end end - -- Remove destroyed casts - for _, id in destroyedCastIDs do - local cast = self.ActiveCasts[id] - if cast and cast.RayInfo and cast.RayInfo.CosmeticBulletObject then - cast.RayInfo.CosmeticBulletObject:Destroy() - end - self:Unregister(id) + -- Process destroyed casts + for _, id in destroyedIds do + SerialSimulation.Terminate(id) end DispatchAllEvents() end -function SerialSimulation:Start() - if self.IsRunning then return end - self.IsRunning = true - self.StepConnection = RS.Heartbeat:Connect(UpdateAllCasts) +function SerialSimulation.Start() + if SerialSimulation.IsRunning then return end + SerialSimulation.IsRunning = true + SerialSimulation.StepConnection = RS.Heartbeat:Connect(UpdateCasts) end -function SerialSimulation:Stop() - if not self.IsRunning then return end - self.IsRunning = false - if self.StepConnection then - self.StepConnection:Disconnect() - self.StepConnection = nil +function SerialSimulation.Stop() + if not SerialSimulation.IsRunning then return end + SerialSimulation.IsRunning = false + if SerialSimulation.StepConnection then + SerialSimulation.StepConnection:Disconnect() + SerialSimulation.StepConnection = nil end end +-- Auto-start +SerialSimulation.Start() + return SerialSimulation \ No newline at end of file From 13d8f71def3e8abc4fb2495e05a6f7f8bcd0636b Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 10:25:17 +0000 Subject: [PATCH 11/62] refactor: Parallel mode uses SoA with ParallelSimulation - Add ParallelSimulation: SoA pattern for each Actor - Update BaseCast: uses ParallelSimulation instead of per-cast Heartbeat - Each Actor now has one RunService handling multiple casts --- src/FastCast2/BaseCast.luau | 40 ++-- src/FastCast2/ParallelSimulation.luau | 327 ++++++++++++++++++++++++++ 2 files changed, 346 insertions(+), 21 deletions(-) create mode 100644 src/FastCast2/ParallelSimulation.luau diff --git a/src/FastCast2/BaseCast.luau b/src/FastCast2/BaseCast.luau index c0b98fc..72337d6 100644 --- a/src/FastCast2/BaseCast.luau +++ b/src/FastCast2/BaseCast.luau @@ -14,8 +14,8 @@ local FastCastM = require(FastCast2) local FastCastEnums = require(FastCast2:WaitForChild("FastCastEnums")) local TypeDef = require(FastCast2:WaitForChild("TypeDefinitions")) ---local Signal = require(FastCast2:WaitForChild("Signal")) local ActiveCast = require(FastCast2:WaitForChild("ActiveCast")) +local ParallelSimulation = require(FastCast2:WaitForChild("ParallelSimulation")) local FastCastEventsModule: ModuleScript? = nil -- Enums @@ -169,12 +169,10 @@ function BaseCast:Raycast( Velocity: Vector3 | number, Behavior: TypeDef.FastCastBehavior ) - --table.insert(self.Actives, ActiveCast.new(self, Origin, Direction, Velocity, Behavior)) Actor:SetAttribute("Tasks", Actor:GetAttribute("Tasks") + 1) - NextProjectileID += 1 - Actives[NextProjectileID] = ActiveCast.createCastData({ + local cast = ActiveCast.createCastData({ Output = Output, ActiveCastCleaner = ActiveCastCleaner, ObjectCache = ObjectCache, @@ -183,13 +181,13 @@ function BaseCast:Raycast( CastType = EnumCastTypes.Raycast } :: any) + ParallelSimulation.Register(cast) + if Behavior.FastCastEventsConfig.UseCastFire then - SendCastFire(Actives[NextProjectileID], Origin, Direction, Velocity, Behavior) + SendCastFire(cast, Origin, Direction, Velocity, Behavior) end - if Behavior.FastCastEventsModuleConfig.UseCastFire then - if CastFireFunc then - CastFireFunc(Actives[NextProjectileID], Origin, Direction, Velocity, Behavior) - end + if Behavior.FastCastEventsModuleConfig.UseCastFire and CastFireFunc then + CastFireFunc(cast, Origin, Direction, Velocity, Behavior) end end @@ -237,7 +235,7 @@ function BaseCast:Blockcast( Actor:SetAttribute("Tasks", Actor:GetAttribute("Tasks") + 1) NextProjectileID += 1 - Actives[NextProjectileID] = ActiveCast.createCastData({ + local cast = ActiveCast.createCastData({ Output = Output, ActiveCastCleaner = ActiveCastCleaner, ObjectCache = ObjectCache, @@ -247,13 +245,13 @@ function BaseCast:Blockcast( Size = Size } :: any) + ParallelSimulation.Register(cast) + if Behavior.FastCastEventsConfig.UseCastFire then - SendCastFire(Actives[NextProjectileID], Origin, Direction, Velocity, Behavior) + SendCastFire(cast, Origin, Direction, Velocity, Behavior) end - if Behavior.FastCastEventsModuleConfig.UseCastFire then - if CastFireFunc then - CastFireFunc(Actives[NextProjectileID], Origin, Direction, Velocity, Behavior) - end + if Behavior.FastCastEventsModuleConfig.UseCastFire and CastFireFunc then + CastFireFunc(cast, Origin, Direction, Velocity, Behavior) end end @@ -281,7 +279,7 @@ function BaseCast:Spherecast( Actor:SetAttribute("Tasks", Actor:GetAttribute("Tasks") + 1) NextProjectileID += 1 - Actives[NextProjectileID] = ActiveCast.createCastData({ + local cast = ActiveCast.createCastData({ Output = Output, ActiveCastCleaner = ActiveCastCleaner, ObjectCache = ObjectCache, @@ -291,13 +289,13 @@ function BaseCast:Spherecast( Radius = Radius } :: any) + ParallelSimulation.Register(cast) + if Behavior.FastCastEventsConfig.UseCastFire then - SendCastFire(Actives[NextProjectileID], Origin, Direction, Velocity, Behavior) + SendCastFire(cast, Origin, Direction, Velocity, Behavior) end - if Behavior.FastCastEventsModuleConfig.UseCastFire then - if CastFireFunc then - CastFireFunc(Actives[NextProjectileID], Origin, Direction, Velocity, Behavior) - end + if Behavior.FastCastEventsModuleConfig.UseCastFire and CastFireFunc then + CastFireFunc(cast, Origin, Direction, Velocity, Behavior) end end diff --git a/src/FastCast2/ParallelSimulation.luau b/src/FastCast2/ParallelSimulation.luau new file mode 100644 index 0000000..cba44a8 --- /dev/null +++ b/src/FastCast2/ParallelSimulation.luau @@ -0,0 +1,327 @@ +--[[ + - Author : Mawin CK + - Date : 2025 + -- Version : 0.0.9 + + ParallelSimulation - SoA pattern for Parallel mode + Each Actor has its own instance of this for parallel processing +]] + +local RS = game:GetService("RunService") + +local FastCastModule = script.Parent +local TypeDef = require(FastCastModule:WaitForChild("TypeDefinitions")) +local Configs = require(FastCastModule:WaitForChild("Configs")) +local DebugLogging = Configs.DebugLogging +local FastCastEnums = require(FastCastModule:WaitForChild("FastCastEnums")) + +local EnumCastTypes = FastCastEnums.CastType +local HIGH_FIDE_INCREASE_SIZE = 0.5 +local MAX_SEGMENT_CAL_TIME = 0.016 * 5 +local MAX_CASTING_TIME = 0.2 +local DEFAULT_MAX_DISTANCE = 1000 +local EPSILON = 1e-6 +local RAY_SEARCH_OFFSET = 0.001 + +local DBG_SEGMENT_SUB_COLOR = Color3.new(0.286275, 0.329412, 0.247059) +local DBG_SEGMENT_SUB_COLOR2 = Color3.new(0.454902, 0.933333, 0.011765) +local DBG_HIT_SUB_COLOR = Color3.new(0.0588235, 0.87451, 1) +local DBG_RAYPIERCE_SUB_COLOR = Color3.new(1, 0.113725, 0.588235) + +local castHandlers = { + [EnumCastTypes.Raycast] = function(targetWorldRoot: WorldRoot, origin: Vector3, direction: Vector3, params: RaycastParams) + return targetWorldRoot:Raycast(origin, direction, params) + end, + [EnumCastTypes.Blockcast] = function(targetWorldRoot: WorldRoot, origin: Vector3, direction: Vector3, params: RaycastParams, size: Vector3) + return targetWorldRoot:Blockcast(CFrame.new(origin), size, direction, params) + end, + [EnumCastTypes.Spherecast] = function(targetWorldRoot: WorldRoot, origin: Vector3, direction: Vector3, params: RaycastParams, radius: number) + return targetWorldRoot:Spherecast(origin, radius, direction, params) + end +} + +local function GetPositionAtTime(t: number, origin: Vector3, velocity: Vector3, accel: Vector3): Vector3 + local force = Vector3.new( + (accel.X * t ^ 2) / 2, + (accel.Y * t ^ 2) / 2, + (accel.Z * t ^ 2) / 2 + ) + return origin + (velocity * t) + force +end + +local function GetVelocityAtTime(time: number, velocity: Vector3, accel: Vector3): Vector3 + return velocity + accel * time +end + +local function OnError(err: string) + warn(debug.traceback(err, 2)) +end + +-- SoA Arrays (per Actor) +local castCount = 0 +local casts = {} :: { [number]: any } +local castIDs = {} :: { [number]: number } +local castOrigin = {} :: { [number]: Vector3 } +local castVelocity = {} :: { [number]: Vector3 } +local castAcceleration = {} :: { [number]: Vector3 } +local castTotalRuntime = {} :: { [number]: number } +local castDistanceCovered = {} :: { [number]: number } +local castMaxDistance = {} :: { [number]: number } +local castPaused = {} :: { [number]: boolean } +local castHighFidelitySegmentSize = {} :: { [number]: number } +local castHighFidelityBehavior = {} :: { [number]: number } +local castIsActivelyResimulating = {} :: { [number]: boolean } +local castCancelHighResCast = {} :: { [number]: boolean } +local castCFrame = {} :: { [number]: CFrame } +local castWorldRoot = {} :: { [number]: WorldRoot } +local castRaycastParams = {} :: { [number]: RaycastParams } +local castCosmeticBullet = {} :: { [number]: Instance? } +local castCastType = {} :: { [number]: number } +local castSize = {} :: { [number]: Vector3? } +local castRadius = {} :: { [number]: number? } +local castVisualize = {} :: { [number]: boolean } +local castVisualizeSettings = {} :: { [number]: any } +local castCaster = {} :: { [number]: any } + +-- Event queue +local QueuedEvents = {} :: { { Callback: any, Args: { any } } } + +local function QueueEvent(callback: any, ...) + if callback then + table.insert(QueuedEvents, { Callback = callback, Args = { ... } }) + end +end + +local function DispatchEvent(callback: any, ...) + if callback then + if Configs.UseProtectedCalls then + xpcall(callback, OnError, ...) + else + callback(...) + end + end +end + +local function DispatchAllEvents() + for _, event in QueuedEvents do + DispatchEvent(event.Callback, unpack(event.Args)) + end + table.clear(QueuedEvents) +end + +local function QueueFire(caster: any, eventName: string, ...) + if caster and caster.Output then + caster.Output:Fire(eventName, ...) + end +end + +local ParallelSimulation = { + StepConnection = nil +} + +function ParallelSimulation.Register(cast: any) + castCount += 1 + local id = castCount + + casts[id] = cast + castIDs[id] = cast.ID + castOrigin[id] = cast.StateInfo.Trajectories[1].Origin + castVelocity[id] = cast.StateInfo.Trajectories[1].InitialVelocity + castAcceleration[id] = cast.StateInfo.Trajectories[1].Acceleration + castTotalRuntime[id] = 0 + castDistanceCovered[id] = 0 + castMaxDistance[id] = cast.RayInfo.MaxDistance + castPaused[id] = false + castHighFidelitySegmentSize[id] = cast.StateInfo.HighFidelitySegmentSize + castHighFidelityBehavior[id] = cast.StateInfo.HighFidelityBehavior + castIsActivelyResimulating[id] = false + castCancelHighResCast[id] = false + castCFrame[id] = cast.CFrame + castWorldRoot[id] = cast.RayInfo.WorldRoot + castRaycastParams[id] = cast.RayInfo.Parameters + castCosmeticBullet[id] = cast.RayInfo.CosmeticBulletObject + castCastType[id] = cast.Type == "Blockcast" and EnumCastTypes.Blockcast or (cast.Type == "Spherecast" and EnumCastTypes.Spherecast or EnumCastTypes.Raycast) + castVisualize[id] = cast.StateInfo.VisualizeCasts + castVisualizeSettings[id] = cast.StateInfo.VisualizeCastSettings + castCaster[id] = cast.Caster + + if cast.RayInfo.Size then + castSize[id] = cast.RayInfo.Size + end + if cast.RayInfo.Radius then + castRadius[id] = cast.RayInfo.Radius + end + + cast.ID = id +end + +function ParallelSimulation.Unregister(id: number) + if not casts[id] then return end + + local lastId = castCount + if id ~= lastId then + castIDs[id] = castIDs[lastId] + castOrigin[id] = castOrigin[lastId] + castVelocity[id] = castVelocity[lastId] + castAcceleration[id] = castAcceleration[lastId] + castTotalRuntime[id] = castTotalRuntime[lastId] + castDistanceCovered[id] = castDistanceCovered[lastId] + castMaxDistance[id] = castMaxDistance[lastId] + castPaused[id] = castPaused[lastId] + castHighFidelitySegmentSize[id] = castHighFidelitySegmentSize[lastId] + castHighFidelityBehavior[id] = castHighFidelityBehavior[lastId] + castIsActivelyResimulating[id] = castIsActivelyResimulating[lastId] + castCancelHighResCast[id] = castCancelHighResCast[lastId] + castCFrame[id] = castCFrame[lastId] + castWorldRoot[id] = castWorldRoot[lastId] + castRaycastParams[id] = castRaycastParams[lastId] + castCosmeticBullet[id] = castCosmeticBullet[lastId] + castCastType[id] = castCastType[lastId] + castSize[id] = castSize[lastId] + castRadius[id] = castRadius[lastId] + castVisualize[id] = castVisualize[lastId] + castVisualizeSettings[id] = castVisualizeSettings[lastId] + castCaster[id] = castCaster[lastId] + + if casts[lastId] then + casts[lastId].ID = id + end + end + + castIDs[lastId] = nil + castOrigin[lastId] = nil + castVelocity[lastId] = nil + castAcceleration[lastId] = nil + castTotalRuntime[lastId] = nil + castDistanceCovered[lastId] = nil + castMaxDistance[lastId] = nil + castPaused[lastId] = nil + castHighFidelitySegmentSize[lastId] = nil + castHighFidelityBehavior[lastId] = nil + castIsActivelyResimulating[lastId] = nil + castCancelHighResCast[lastId] = nil + castCFrame[id] = nil + castWorldRoot[lastId] = nil + castRaycastParams[lastId] = nil + castCosmeticBullet[lastId] = nil + castCastType[lastId] = nil + castSize[lastId] = nil + castRadius[lastId] = nil + castVisualize[lastId] = nil + castVisualizeSettings[lastId] = nil + castCaster[lastId] = nil + + casts[id] = nil + castCount = lastId - 1 +end + +function ParallelSimulation.PauseCast(id: number, paused: boolean) + castPaused[id] = paused +end + +function ParallelSimulation.Terminate(id: number) + local bullet = castCosmeticBullet[id] + if bullet then + bullet:Destroy() + end + ParallelSimulation.Unregister(id) +end + +local function UpdateCasts(deltaTime: number) + if castCount == 0 then return end + + local destroyedIds = {} :: { number } + + for i = 1, castCount do + if castPaused[i] then continue end + + local caster = castCaster[i] + local castType = castCastType[i] + local CastHandler = castHandlers[castType] + + local origin = castOrigin[i] + local totalDelta = castTotalRuntime[i] + local velocity = castVelocity[i] + local acceleration = castAcceleration[i] + + local lastPosition = GetPositionAtTime(totalDelta, origin, velocity, acceleration) + castTotalRuntime[i] += deltaTime + totalDelta = castTotalRuntime[i] + + local currentPosition = GetPositionAtTime(totalDelta, origin, velocity, acceleration) + local currentVelocity = GetVelocityAtTime(totalDelta, velocity, acceleration) + local displacement = currentPosition - lastPosition + + local rayDir = displacement.Unit * currentVelocity.Magnitude * deltaTime + + local variant = {} + if castType == EnumCastTypes.Blockcast then + variant.Size = castSize[i] + elseif castType == EnumCastTypes.Spherecast then + variant.Radius = castRadius[i] + end + + local result = CastHandler(castWorldRoot[i], lastPosition, rayDir, castRaycastParams[i], variant) + + local hitPoint = currentPosition + local hitPart = nil + if result then + hitPoint = result.Position + hitPart = result.Instance + end + + local rayDisplacement = (hitPoint - lastPosition).Magnitude + castDistanceCovered[i] += rayDisplacement + + local newCFrame = CFrame.new(lastPosition, lastPosition + rayDir) * CFrame.new(0, 0, -rayDisplacement / 2) + castCFrame[i] = newCFrame + + -- Update cosmetic bullet + local bullet = castCosmeticBullet[i] + if bullet then + if bullet:IsA("BasePart") then + bullet.CFrame = newCFrame + else + bullet:PivotTo(newCFrame) + end + end + + -- Fire LengthChanged + QueueFire(caster, "LengthChanged", casts[i], lastPosition, rayDir.Unit, rayDisplacement, currentVelocity, bullet) + + -- Handle hit + if result and hitPart ~= bullet then + QueueFire(caster, "Hit", casts[i], result, currentVelocity, bullet) + table.insert(destroyedIds, i) + end + + -- Check max distance + if castDistanceCovered[i] >= castMaxDistance[i] then + table.insert(destroyedIds, i) + end + end + + -- Process destroyed casts + for _, id in destroyedIds do + ParallelSimulation.Terminate(id) + end + + DispatchAllEvents() +end + +function ParallelSimulation.Start() + if ParallelSimulation.StepConnection then return end + ParallelSimulation.StepConnection = RS.PreRender:ConnectParallel(UpdateCasts) +end + +function ParallelSimulation.Stop() + if ParallelSimulation.StepConnection then + ParallelSimulation.StepConnection:Disconnect() + ParallelSimulation.StepConnection = nil + end +end + +-- Auto-start when loaded +ParallelSimulation.Start() + +return ParallelSimulation \ No newline at end of file From 2f423e5b93716f4c28a92e9a246a49850696fa73 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 10:30:26 +0000 Subject: [PATCH 12/62] refactor: Remove UpdateConnection and metatable from ActiveCast - ActiveCast: Remove UpdateConnection (old per-cast heartbeat) - ActiveCastSerial: Remove metatable, use pure data structure - Now internal uses SoA, external exposes OOP API --- src/FastCast2/ActiveCast.luau | 7 --- src/FastCast2/ActiveCastSerial.luau | 74 ++++++++++++----------------- 2 files changed, 31 insertions(+), 50 deletions(-) diff --git a/src/FastCast2/ActiveCast.luau b/src/FastCast2/ActiveCast.luau index 4dfb471..3795f45 100644 --- a/src/FastCast2/ActiveCast.luau +++ b/src/FastCast2/ActiveCast.luau @@ -358,8 +358,6 @@ local function SimulateCast( FastCastEvents: TypeDef.FastCastEvents, variant: CastVariants ) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") - --PrintDebug("Casting for frame.") --print("1C") if DebugLogging.Casting then @@ -725,7 +723,6 @@ function ActiveCast.createCastData( Caster = BaseCast, StateInfo = { - UpdateConnection = nil, Paused = false, TotalRuntime = 0, DistanceCovered = 0, @@ -984,11 +981,7 @@ function ActiveCast.createCastData( end end - cast.StateInfo.UpdateConnection = event:ConnectParallel(Stepped) - return cast end --- Will I ever be free - return ActiveCast diff --git a/src/FastCast2/ActiveCastSerial.luau b/src/FastCast2/ActiveCastSerial.luau index b09dea6..b07e8bc 100644 --- a/src/FastCast2/ActiveCastSerial.luau +++ b/src/FastCast2/ActiveCastSerial.luau @@ -49,9 +49,6 @@ local castHandlers = { } local ActiveCastSerial = {} -ActiveCastSerial.__index = ActiveCastSerial - -local Simulation = nil :: any local function GetPositionAtTime(t: number, origin: Vector3, initialVelocity: Vector3, acceleration: Vector3): Vector3 local force = Vector3.new( @@ -127,49 +124,40 @@ local RAY_SEARCH_OFFSET = 0.001 local FIXED_DELTA_TIME = 1 / 240 function ActiveCastSerial.new(caster: any, castData: any): any - local self = setmetatable({}, ActiveCastSerial) - - self.Caster = caster - self.StateInfo = { - UpdateConnection = nil, - Paused = false, - TotalRuntime = 0, - DistanceCovered = 0, - HighFidelitySegmentSize = castData.HighFidelitySegmentSize, - HighFidelityBehavior = castData.HighFidelityBehavior, - IsActivelyResimulating = false, - CancelHighResCast = false, - Trajectories = { - { - StartTime = 0, - EndTime = -1, - Origin = castData.Origin, - InitialVelocity = castData.Velocity, - Acceleration = castData.Acceleration, - } + return { + Caster = caster, + StateInfo = { + Paused = false, + TotalRuntime = 0, + DistanceCovered = 0, + HighFidelitySegmentSize = castData.HighFidelitySegmentSize, + HighFidelityBehavior = castData.HighFidelityBehavior, + IsActivelyResimulating = false, + CancelHighResCast = false, + Trajectories = { + { + StartTime = 0, + EndTime = -1, + Origin = castData.Origin, + InitialVelocity = castData.Velocity, + Acceleration = castData.Acceleration, + } + }, + VisualizeCasts = castData.VisualizeCasts, + VisualizeCastSettings = castData.VisualizeCastSettings }, - VisualizeCasts = castData.VisualizeCasts, - VisualizeCastSettings = castData.VisualizeCastSettings - } - - self.RayInfo = { - Parameters = castData.RaycastParams, - WorldRoot = workspace, - MaxDistance = castData.MaxDistance or DEFAULT_MAX_DISTANCE, - CosmeticBulletObject = castData.CosmeticBulletObject - } - self.Type = CastVariantTypes[castData.CastType] - self.CFrame = CFrame.new(castData.Origin) - self.ID = castData.ID - - if castData.CastType == EnumCastTypes.Blockcast then - self.RayInfo.Size = castData.Size - elseif castData.CastType == EnumCastTypes.Spherecast then - self.RayInfo.Radius = castData.Radius - end + RayInfo = { + Parameters = castData.RaycastParams, + WorldRoot = workspace, + MaxDistance = castData.MaxDistance or DEFAULT_MAX_DISTANCE, + CosmeticBulletObject = castData.CosmeticBulletObject + }, - return self + Type = CastVariantTypes[castData.CastType], + CFrame = CFrame.new(castData.Origin), + ID = castData.ID + } end return ActiveCastSerial \ No newline at end of file From 4d7b209ce41058bf7ad79627aee7ab66a8bf8520 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 10:43:04 +0000 Subject: [PATCH 13/62] perf: Remove xpcall/pcall from hot path for performance --- src/FastCast2/ParallelSimulation.luau | 10 +--------- src/FastCast2/SerialSimulation.luau | 10 +--------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/src/FastCast2/ParallelSimulation.luau b/src/FastCast2/ParallelSimulation.luau index cba44a8..f52ce89 100644 --- a/src/FastCast2/ParallelSimulation.luau +++ b/src/FastCast2/ParallelSimulation.luau @@ -53,10 +53,6 @@ local function GetVelocityAtTime(time: number, velocity: Vector3, accel: Vector3 return velocity + accel * time end -local function OnError(err: string) - warn(debug.traceback(err, 2)) -end - -- SoA Arrays (per Actor) local castCount = 0 local casts = {} :: { [number]: any } @@ -94,11 +90,7 @@ end local function DispatchEvent(callback: any, ...) if callback then - if Configs.UseProtectedCalls then - xpcall(callback, OnError, ...) - else - callback(...) - end + callback(...) end end diff --git a/src/FastCast2/SerialSimulation.luau b/src/FastCast2/SerialSimulation.luau index dc8b659..d06211a 100644 --- a/src/FastCast2/SerialSimulation.luau +++ b/src/FastCast2/SerialSimulation.luau @@ -55,10 +55,6 @@ local function GetVelocityAtTime(time: number, velocity: Vector3, accel: Vector3 return velocity + accel * time end -local function OnError(err: string) - warn(debug.traceback(err, 2)) -end - -- SoA Arrays local castCount = 0 local casts = {} :: { [number]: any } @@ -96,11 +92,7 @@ end local function DispatchEvent(callback: any, ...) if callback then - if Configs.UseProtectedCalls then - xpcall(callback, OnError, ...) - else - callback(...) - end + callback(...) end end From 4b45156e4c033edb21236eda1a67ba22e1321992 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 10:44:56 +0000 Subject: [PATCH 14/62] Update TODO.md --- TODO.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index b79c7e3..3e499d8 100644 --- a/TODO.md +++ b/TODO.md @@ -6,4 +6,5 @@ - [ ] Update documentation - [ ] Add benchmarks - [ ] Refactor -- [ ] Fix HighFidelityBehavior = 2 bug, where projectile passes through walls \ No newline at end of file +- [ ] Fix HighFidelityBehavior = 2 bug, where projectile passes through walls +- [ ] ActiveCast.Trajectories, wastes memory. change it to ActiveCast.Trajectory \ No newline at end of file From 0bb5986b947ee6dd1e80466a2ab4ff6a193c8697 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 10:51:19 +0000 Subject: [PATCH 15/62] refactor: Change Trajectories to Trajectory (single object, not array) --- src/FastCast2/ActiveCast.luau | 36 ++++++++--------- src/FastCast2/ActiveCastSerial.luau | 14 +++---- src/FastCast2/ParallelSimulation.luau | 6 +-- src/FastCast2/SerialSimulation.luau | 6 +-- src/FastCast2/TypeDefinitions.luau | 4 +- src/FastCast2/init.luau | 56 +++++++++++---------------- 6 files changed, 54 insertions(+), 68 deletions(-) diff --git a/src/FastCast2/ActiveCast.luau b/src/FastCast2/ActiveCast.luau index 3795f45..ff840d5 100644 --- a/src/FastCast2/ActiveCast.luau +++ b/src/FastCast2/ActiveCast.luau @@ -160,7 +160,7 @@ local function GetTrajectoryInfo( index: number ): { [number]: Vector3 } assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") - local trajectories = cast.StateInfo.Trajectories + local trajectories = cast.StateInfo.Trajectory local trajectory = trajectories[index] local duration = trajectory.EndTime - trajectory.StartTime @@ -172,7 +172,7 @@ local function GetTrajectoryInfo( end local function GetLatestTrajectoryEndInfo(cast: TypeDef.ActiveCastData): { [number]: Vector3 } - return GetTrajectoryInfo(cast, #cast.StateInfo.Trajectories) + return GetTrajectoryInfo(cast, #cast.StateInfo.Trajectory) end ]] @@ -364,16 +364,16 @@ local function SimulateCast( print("Casting for frame.") end - local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + local trajectory = cast.StateInfo.Trajectory - local origin = latestTrajectory.Origin - local totalDelta = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime - local initialVelocity = latestTrajectory.InitialVelocity - local acceleration = latestTrajectory.Acceleration + local origin = trajectory.Origin + local totalDelta = cast.StateInfo.TotalRuntime - trajectory.StartTime + local initialVelocity = trajectory.InitialVelocity + local acceleration = trajectory.Acceleration local lastPoint = GetPositionAtTime(totalDelta, origin, initialVelocity, acceleration) --local lastVelocity = GetVelocityAtTime(totalDelta, initialVelocity, acceleration) - local lastDelta = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime + local lastDelta = cast.StateInfo.TotalRuntime - trajectory.StartTime cast.StateInfo.TotalRuntime += delta @@ -731,14 +731,12 @@ function ActiveCast.createCastData( IsActivelySimulatingPierce = false, IsActivelyResimulating = false, CancelHighResCast = false, - Trajectories = { - { - StartTime = 0, - EndTime = -1, - Origin = origin, - InitialVelocity = velocity, - Acceleration = behavior.Acceleration, - }, + Trajectory = { + StartTime = 0, + EndTime = -1, + Origin = origin, + InitialVelocity = velocity, + Acceleration = behavior.Acceleration, }, VisualizeCasts = behavior.VisualizeCasts, VisualizeCastSettings = behavior.VisualizeCastSettings, @@ -852,10 +850,10 @@ function ActiveCast.createCastData( local Cast_timeAtStart = tick() - local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + local trajectory = cast.StateInfo.Trajectory - if typeof(latestTrajectory.Acceleration) ~= "Vector3" then - latestTrajectory.Acceleration = Vector3.new() + if typeof(trajectory.Acceleration) ~= "Vector3" then + trajectory.Acceleration = Vector3.new() end if diff --git a/src/FastCast2/ActiveCastSerial.luau b/src/FastCast2/ActiveCastSerial.luau index b07e8bc..23fe87e 100644 --- a/src/FastCast2/ActiveCastSerial.luau +++ b/src/FastCast2/ActiveCastSerial.luau @@ -134,14 +134,12 @@ function ActiveCastSerial.new(caster: any, castData: any): any HighFidelityBehavior = castData.HighFidelityBehavior, IsActivelyResimulating = false, CancelHighResCast = false, - Trajectories = { - { - StartTime = 0, - EndTime = -1, - Origin = castData.Origin, - InitialVelocity = castData.Velocity, - Acceleration = castData.Acceleration, - } + Trajectory = { + StartTime = 0, + EndTime = -1, + Origin = castData.Origin, + InitialVelocity = castData.Velocity, + Acceleration = castData.Acceleration, }, VisualizeCasts = castData.VisualizeCasts, VisualizeCastSettings = castData.VisualizeCastSettings diff --git a/src/FastCast2/ParallelSimulation.luau b/src/FastCast2/ParallelSimulation.luau index f52ce89..c038a22 100644 --- a/src/FastCast2/ParallelSimulation.luau +++ b/src/FastCast2/ParallelSimulation.luau @@ -117,9 +117,9 @@ function ParallelSimulation.Register(cast: any) casts[id] = cast castIDs[id] = cast.ID - castOrigin[id] = cast.StateInfo.Trajectories[1].Origin - castVelocity[id] = cast.StateInfo.Trajectories[1].InitialVelocity - castAcceleration[id] = cast.StateInfo.Trajectories[1].Acceleration + castOrigin[id] = cast.StateInfo.Trajectory.Origin + castVelocity[id] = cast.StateInfo.Trajectory.InitialVelocity + castAcceleration[id] = cast.StateInfo.Trajectory.Acceleration castTotalRuntime[id] = 0 castDistanceCovered[id] = 0 castMaxDistance[id] = cast.RayInfo.MaxDistance diff --git a/src/FastCast2/SerialSimulation.luau b/src/FastCast2/SerialSimulation.luau index d06211a..aec32d6 100644 --- a/src/FastCast2/SerialSimulation.luau +++ b/src/FastCast2/SerialSimulation.luau @@ -120,9 +120,9 @@ function SerialSimulation.Register(cast: any) casts[id] = cast castIDs[id] = cast.ID - castOrigin[id] = cast.StateInfo.Trajectories[1].Origin - castVelocity[id] = cast.StateInfo.Trajectories[1].InitialVelocity - castAcceleration[id] = cast.StateInfo.Trajectories[1].Acceleration + castOrigin[id] = cast.StateInfo.Trajectory.Origin + castVelocity[id] = cast.StateInfo.Trajectory.InitialVelocity + castAcceleration[id] = cast.StateInfo.Trajectory.Acceleration castTotalRuntime[id] = 0 castDistanceCovered[id] = 0 castMaxDistance[id] = cast.RayInfo.MaxDistance diff --git a/src/FastCast2/TypeDefinitions.luau b/src/FastCast2/TypeDefinitions.luau index 886fde7..5ab36df 100644 --- a/src/FastCast2/TypeDefinitions.luau +++ b/src/FastCast2/TypeDefinitions.luau @@ -345,7 +345,7 @@ export type CastTrajectory = { } --[=[ - @type CastStateInfo { UpdateConnection: RBXScriptSignal, HighFidelityBehavior: number, HighFidelitySegmentSize: number, Paused: boolean, TotalRuntime: number, DistanceCovered: number, IsActivelySimulatingPierce: boolean, IsActivelyResimulating: boolean, CancelHighResCast: boolean, Trajectories: { [number]: CastTrajectory }, VisualizeCasts: boolean, VisualizeCastSettings: VisualizeCastSettings, FastCastEventsConfig: FastCastEventsConfig, FastCastEventsModuleConfig: FastCastEventsModuleConfig } + @type CastStateInfo { UpdateConnection: RBXScriptSignal, HighFidelityBehavior: number, HighFidelitySegmentSize: number, Paused: boolean, TotalRuntime: number, DistanceCovered: number, IsActivelySimulatingPierce: boolean, IsActivelyResimulating: boolean, CancelHighResCast: boolean, Trajectory: CastTrajectory, VisualizeCasts: boolean, VisualizeCastSettings: VisualizeCastSettings, FastCastEventsConfig: FastCastEventsConfig, FastCastEventsModuleConfig: FastCastEventsModuleConfig } @within TypeDefinitions Represents cast state tracking data. @@ -360,7 +360,7 @@ export type CastStateInfo = { IsActivelySimulatingPierce: boolean, IsActivelyResimulating: boolean, CancelHighResCast: boolean, - Trajectories: { [number]: CastTrajectory }, + Trajectory: CastTrajectory, VisualizeCasts: boolean, VisualizeCastSettings: VisualizeCastSettings, diff --git a/src/FastCast2/init.luau b/src/FastCast2/init.luau index 71edc21..17f53fc 100644 --- a/src/FastCast2/init.luau +++ b/src/FastCast2/init.luau @@ -130,7 +130,7 @@ local function GetTrajectoryInfo( index: number ): { [number]: Vector3 } assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") - local trajectories = cast.StateInfo.Trajectories + local trajectories = cast.StateInfo.Trajectory local trajectory = trajectories[index] local duration = trajectory.EndTime - trajectory.StartTime @@ -142,7 +142,7 @@ local function GetTrajectoryInfo( end local function GetLatestTrajectoryEndInfo(cast: vaildcast): { [number]: Vector3 } - return GetTrajectoryInfo(cast, #cast.StateInfo.Trajectories) + return GetTrajectoryInfo(cast, 1) end local function ModifyTransformation( @@ -151,15 +151,14 @@ local function ModifyTransformation( acceleration: Vector3?, position: Vector3? ) - local trajectories = cast.StateInfo.Trajectories - local lastTrajectory = trajectories[#trajectories] + local trajectory = cast.StateInfo.Trajectory - if lastTrajectory.StartTime == cast.StateInfo.TotalRuntime then + if trajectory.StartTime == cast.StateInfo.TotalRuntime then if velocity == nil then - velocity = lastTrajectory.InitialVelocity + velocity = trajectory.InitialVelocity end if acceleration == nil then - acceleration = lastTrajectory.Acceleration + acceleration = trajectory.Acceleration end if position == nil then position = lastTrajectory.Origin @@ -182,7 +181,7 @@ local function ModifyTransformation( if position == nil then position = point end - table.insert(cast.StateInfo.Trajectories, { + table.insert(cast.StateInfo.Trajectory, { StartTime = cast.StateInfo.TotalRuntime, EndTime = -1, Origin = position, @@ -448,7 +447,7 @@ Gets the velocity of an ActiveCast. ]=] function FastCastParallel:GetVelocityCast(cast: vaildcast) assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") - local currentTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + local currentTrajectory = cast.StateInfo.Trajectory return GetVelocityAtTime( cast.StateInfo.TotalRuntime - currentTrajectory.StartTime, currentTrajectory.InitialVelocity, @@ -468,7 +467,7 @@ Gets the acceleration of an ActiveCast. ]=] function FastCastParallel:GetAccelerationCast(cast: vaildcast) assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") - local currentTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + local currentTrajectory = cast.StateInfo.Trajectory return currentTrajectory.Acceleration end @@ -483,7 +482,7 @@ Gets the position of an ActiveCast. ]=] function FastCastParallel:GetPositionCast(cast: vaildcast) assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") - local currentTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + local currentTrajectory = cast.StateInfo.Trajectory return GetPositionAtTime( cast.StateInfo.TotalRuntime - currentTrajectory.StartTime, currentTrajectory.Origin, @@ -617,27 +616,18 @@ end @within FastCastParallel ]=] function FastCastParallel:TerminateCast(cast: vaildcast, castTerminatingFunction: TypeDef.OnCastTerminatingFunction?) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") - - local trajectories = cast.StateInfo.Trajectories - local lastTrajectory = trajectories[#trajectories] - lastTrajectory.EndTime = cast.StateInfo.TotalRuntime + local trajectory = cast.StateInfo.Trajectory + trajectory.EndTime = cast.StateInfo.TotalRuntime - - if cast.StateInfo.UpdateConnection then - cast.StateInfo.UpdateConnection:Disconnect() - cast.StateInfo.UpdateConnection = nil - end - local FastCastEventsConfig = cast.StateInfo.FastCastEventsConfig if FastCastEventsConfig and FastCastEventsConfig.UseCastTerminating then cast.Caster.Output:Fire("CastTerminating", cast) end - + if castTerminatingFunction then castTerminatingFunction((cast :: any)) end - + cast.Caster.ActiveCastCleaner:Fire(cast.ID) for key, _ in (cast :: any) do @@ -823,7 +813,7 @@ end @within FastCastSerial ]=] function FastCastSerial:GetVelocityCast(cast: vaildcast): Vector3 - local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + local latestTrajectory = cast.StateInfo.Trajectory return latestTrajectory.InitialVelocity end @@ -832,7 +822,7 @@ end @within FastCastSerial ]=] function FastCastSerial:GetAccelerationCast(cast: vaildcast): Vector3 - local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + local latestTrajectory = cast.StateInfo.Trajectory return latestTrajectory.Acceleration end @@ -841,7 +831,7 @@ end @within FastCastSerial ]=] function FastCastSerial:GetPositionCast(cast: vaildcast): Vector3 - local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + local latestTrajectory = cast.StateInfo.Trajectory local totalDelta = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime local origin = latestTrajectory.Origin local initialVelocity = latestTrajectory.InitialVelocity @@ -855,7 +845,7 @@ end @within FastCastSerial ]=] function FastCastSerial:SetVelocityCast(cast: vaildcast, velocity: Vector3) - local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + local latestTrajectory = cast.StateInfo.Trajectory latestTrajectory.InitialVelocity = velocity end @@ -864,7 +854,7 @@ end @within FastCastSerial ]=] function FastCastSerial:SetAccelerationCast(cast: vaildcast, acceleration: Vector3) - local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + local latestTrajectory = cast.StateInfo.Trajectory latestTrajectory.Acceleration = acceleration end @@ -873,7 +863,7 @@ end @within FastCastSerial ]=] function FastCastSerial:SetPositionCast(cast: vaildcast, position: Vector3) - local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + local latestTrajectory = cast.StateInfo.Trajectory latestTrajectory.Origin = position end @@ -890,7 +880,7 @@ end @within FastCastSerial ]=] function FastCastSerial:AddPositionCast(cast: vaildcast, position: Vector3) - local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + local latestTrajectory = cast.StateInfo.Trajectory latestTrajectory.Origin += position end @@ -899,7 +889,7 @@ end @within FastCastSerial ]=] function FastCastSerial:AddVelocityCast(cast: vaildcast, velocity: Vector3) - local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + local latestTrajectory = cast.StateInfo.Trajectory latestTrajectory.InitialVelocity += velocity end @@ -908,7 +898,7 @@ end @within FastCastSerial ]=] function FastCastSerial:AddAccelerationCast(cast: vaildcast, acceleration: Vector3) - local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + local latestTrajectory = cast.StateInfo.Trajectory latestTrajectory.Acceleration += acceleration end From 47d3c21803634162561f9fbbf5d159030253e974 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 10:58:18 +0000 Subject: [PATCH 16/62] feat: Add Motor6D transform feature - Add Motor6DPool for efficient Motor6D pooling - Add MovementMethod to FastCastBehavior (BulkMoveTo/Transform) - Update SerialSimulation to support Transform mode - Per-caster configuration, not global --- src/FastCast2/ActiveCastSerial.luau | 3 +- src/FastCast2/BaseCastSerial.luau | 9 ++- src/FastCast2/DefaultConfigs.luau | 2 + src/FastCast2/Motor6DPool.luau | 89 +++++++++++++++++++++++++++++ src/FastCast2/SerialSimulation.luau | 20 ++++++- 5 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 src/FastCast2/Motor6DPool.luau diff --git a/src/FastCast2/ActiveCastSerial.luau b/src/FastCast2/ActiveCastSerial.luau index 23fe87e..90ff6c4 100644 --- a/src/FastCast2/ActiveCastSerial.luau +++ b/src/FastCast2/ActiveCastSerial.luau @@ -149,7 +149,8 @@ function ActiveCastSerial.new(caster: any, castData: any): any Parameters = castData.RaycastParams, WorldRoot = workspace, MaxDistance = castData.MaxDistance or DEFAULT_MAX_DISTANCE, - CosmeticBulletObject = castData.CosmeticBulletObject + CosmeticBulletObject = castData.CosmeticBulletObject, + MovementMethod = castData.MovementMethod or "BulkMoveTo" }, Type = CastVariantTypes[castData.CastType], diff --git a/src/FastCast2/BaseCastSerial.luau b/src/FastCast2/BaseCastSerial.luau index d4072ed..d8d2809 100644 --- a/src/FastCast2/BaseCastSerial.luau +++ b/src/FastCast2/BaseCastSerial.luau @@ -102,7 +102,8 @@ function BaseCastSerial:Raycast( VisualizeCasts = Behavior.VisualizeCasts, VisualizeCastSettings = Behavior.VisualizeCastSettings, HighFidelitySegmentSize = Behavior.HighFidelitySegmentSize, - HighFidelityBehavior = Behavior.HighFidelityBehavior + HighFidelityBehavior = Behavior.HighFidelityBehavior, + MovementMethod = Behavior.MovementMethod or "BulkMoveTo" } local cast = ActiveCastSerial.new(ParentCaster, castData) @@ -157,7 +158,8 @@ function BaseCastSerial:Blockcast( VisualizeCasts = Behavior.VisualizeCasts, VisualizeCastSettings = Behavior.VisualizeCastSettings, HighFidelitySegmentSize = Behavior.HighFidelitySegmentSize, - HighFidelityBehavior = Behavior.HighFidelityBehavior + HighFidelityBehavior = Behavior.HighFidelityBehavior, + MovementMethod = Behavior.MovementMethod or "BulkMoveTo" } local cast = ActiveCastSerial.new(ParentCaster, castData) @@ -212,7 +214,8 @@ function BaseCastSerial:Spherecast( VisualizeCasts = Behavior.VisualizeCasts, VisualizeCastSettings = Behavior.VisualizeCastSettings, HighFidelitySegmentSize = Behavior.HighFidelitySegmentSize, - HighFidelityBehavior = Behavior.HighFidelityBehavior + HighFidelityBehavior = Behavior.HighFidelityBehavior, + MovementMethod = Behavior.MovementMethod or "BulkMoveTo" } local cast = ActiveCastSerial.new(ParentCaster, castData) diff --git a/src/FastCast2/DefaultConfigs.luau b/src/FastCast2/DefaultConfigs.luau index 9dbc1f2..6385861 100644 --- a/src/FastCast2/DefaultConfigs.luau +++ b/src/FastCast2/DefaultConfigs.luau @@ -27,6 +27,8 @@ Defaults.FastCastBehavior = { HighFidelityBehavior = FastCastEnums.HighFidelityBehavior.Default, HighFidelitySegmentSize = 0.5, + MovementMethod = "BulkMoveTo", -- "BulkMoveTo" or "Transform" + CosmeticBulletTemplate = nil, CosmeticBulletProvider = nil, CosmeticBulletContainer = nil, diff --git a/src/FastCast2/Motor6DPool.luau b/src/FastCast2/Motor6DPool.luau new file mode 100644 index 0000000..e67ca0a --- /dev/null +++ b/src/FastCast2/Motor6DPool.luau @@ -0,0 +1,89 @@ +--[[ + - Author : Mawin CK + - Date : 2025 + -- Version : 0.0.9 + + Motor6D Pool for efficient projectile movement using Transform mode. + Like SwiftCast implementation. +]] + +local GROWTH_RATE = 2 +local INITIAL_POOL_SIZE = 128 + +local FreeMotor6Ds = {} :: { Motor6D } +local PoolSize = 0 +local Initialized = false + +local Motor6DAnchor: BasePart = nil + +local function GrowPool(target: number) + local growth = target - PoolSize + for i = 1, growth do + local motor6d = Instance.new("Motor6D") + motor6d.Name = "FastCastMotor6D" + table.insert(FreeMotor6Ds, motor6d) + end + PoolSize = target +end + +local function Initialize() + if Initialized then return end + + Motor6DAnchor = Instance.new("Part") + Motor6DAnchor.Name = "FastCastAnchor" + Motor6DAnchor.Transparency = 1 + Motor6DAnchor.CanCollide = false + Motor6DAnchor.CanQuery = false + Motor6DAnchor.CanTouch = false + Motor6DAnchor.Anchored = true + Motor6DAnchor.CFrame = CFrame.identity + Motor6DAnchor.Parent = workspace + + GrowPool(INITIAL_POOL_SIZE) + Initialized = true +end + +local function Get(): Motor6D + if #FreeMotor6Ds > 0 then + return table.remove(FreeMotor6Ds) :: Motor6D + else + GrowPool(PoolSize * GROWTH_RATE) + return Get() + end +end + +local function Return(motor6d: Motor6D) + motor6d.Part0 = nil + motor6d.Part1 = nil + motor6d.Parent = nil + motor6d.Transform = CFrame.identity + table.insert(FreeMotor6Ds, motor6d) +end + +local function Connect(castID: number, projectilePart: BasePart?): Motor6D? + if not projectilePart then return nil end + + projectilePart.Anchored = false + + local motor6d = Get() + motor6d.Transform = projectilePart.CFrame + motor6d.Part0 = Motor6DAnchor + motor6d.Part1 = projectilePart + motor6d.Parent = Motor6DAnchor + + return motor6d +end + +local function Disconnect(motor6d: Motor6D?) + if motor6d then + Return(motor6d) + end +end + +return { + Initialize = Initialize, + Get = Get, + Return = Return, + Connect = Connect, + Disconnect = Disconnect +} \ No newline at end of file diff --git a/src/FastCast2/SerialSimulation.luau b/src/FastCast2/SerialSimulation.luau index aec32d6..d6eb4bd 100644 --- a/src/FastCast2/SerialSimulation.luau +++ b/src/FastCast2/SerialSimulation.luau @@ -16,6 +16,7 @@ local Configs = require(FastCastModule:WaitForChild("Configs")) local DebugLogging = Configs.DebugLogging local FastCastEnums = require(FastCastModule:WaitForChild("FastCastEnums")) local ActiveCastSerial = require(FastCastModule:WaitForChild("ActiveCastSerial")) +local Motor6DPool = require(FastCastModule:WaitForChild("Motor6DPool")) local EnumCastTypes = FastCastEnums.CastType local HIGH_FIDE_INCREASE_SIZE = 0.5 @@ -80,6 +81,7 @@ local castRadius = {} :: { [number]: number? } local castVisualize = {} :: { [number]: boolean } local castVisualizeSettings = {} :: { [number]: any } local castCaster = {} :: { [number]: any } +local castMotor6D = {} :: { [number]: Motor6D? } -- Event queue local QueuedEvents = {} :: { { Callback: any, Args: { any } } } @@ -147,6 +149,12 @@ function SerialSimulation.Register(cast: any) castRadius[id] = cast.RayInfo.Radius end + castMotor6D[id] = nil + if cast.RayInfo.MovementMethod == "Transform" then + Motor6DPool.Initialize() + castMotor6D[id] = Motor6DPool.Connect(id, cast.RayInfo.CosmeticBulletObject :: any) + end + cast.ID = id end @@ -177,12 +185,18 @@ function SerialSimulation.Unregister(id: number) castVisualize[id] = castVisualize[lastId] castVisualizeSettings[id] = castVisualizeSettings[lastId] castCaster[id] = castCaster[lastId] + castMotor6D[id] = castMotor6D[lastId] if casts[lastId] then casts[lastId].ID = id end end + -- Return motor6d to pool + if castMotor6D[id] then + Motor6DPool.Disconnect(castMotor6D[id]) + end + castIDs[lastId] = nil castOrigin[lastId] = nil castVelocity[lastId] = nil @@ -205,6 +219,7 @@ function SerialSimulation.Unregister(id: number) castVisualize[lastId] = nil castVisualizeSettings[lastId] = nil castCaster[lastId] = nil + castMotor6D[lastId] = nil casts[id] = nil castCount = lastId - 1 @@ -273,8 +288,11 @@ local function UpdateCasts(deltaTime: number) -- Update cosmetic bullet local bullet = castCosmeticBullet[i] + local motor6d = castMotor6D[i] if bullet then - if bullet:IsA("BasePart") then + if motor6d then + motor6d.Transform = newCFrame + elseif bullet:IsA("BasePart") then bullet.CFrame = newCFrame else bullet:PivotTo(newCFrame) From 28d34207a7f89656556b05aacdb6de29fc09af86 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 10:59:29 +0000 Subject: [PATCH 17/62] feat: Add Motor6D support to Parallel mode - Update ParallelSimulation with Motor6D Transform support - Same as SerialSimulation implementation --- src/FastCast2/ParallelSimulation.luau | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/FastCast2/ParallelSimulation.luau b/src/FastCast2/ParallelSimulation.luau index c038a22..d6a22c3 100644 --- a/src/FastCast2/ParallelSimulation.luau +++ b/src/FastCast2/ParallelSimulation.luau @@ -14,6 +14,7 @@ local TypeDef = require(FastCastModule:WaitForChild("TypeDefinitions")) local Configs = require(FastCastModule:WaitForChild("Configs")) local DebugLogging = Configs.DebugLogging local FastCastEnums = require(FastCastModule:WaitForChild("FastCastEnums")) +local Motor6DPool = require(FastCastModule:WaitForChild("Motor6DPool")) local EnumCastTypes = FastCastEnums.CastType local HIGH_FIDE_INCREASE_SIZE = 0.5 @@ -78,6 +79,7 @@ local castRadius = {} :: { [number]: number? } local castVisualize = {} :: { [number]: boolean } local castVisualizeSettings = {} :: { [number]: any } local castCaster = {} :: { [number]: any } +local castMotor6D = {} :: { [number]: Motor6D? } -- Event queue local QueuedEvents = {} :: { { Callback: any, Args: { any } } } @@ -144,6 +146,12 @@ function ParallelSimulation.Register(cast: any) castRadius[id] = cast.RayInfo.Radius end + castMotor6D[id] = nil + if cast.RayInfo.MovementMethod == "Transform" then + Motor6DPool.Initialize() + castMotor6D[id] = Motor6DPool.Connect(id, cast.RayInfo.CosmeticBulletObject :: any) + end + cast.ID = id end @@ -174,12 +182,17 @@ function ParallelSimulation.Unregister(id: number) castVisualize[id] = castVisualize[lastId] castVisualizeSettings[id] = castVisualizeSettings[lastId] castCaster[id] = castCaster[lastId] + castMotor6D[id] = castMotor6D[lastId] if casts[lastId] then casts[lastId].ID = id end end + if castMotor6D[id] then + Motor6DPool.Disconnect(castMotor6D[id]) + end + castIDs[lastId] = nil castOrigin[lastId] = nil castVelocity[lastId] = nil @@ -202,6 +215,7 @@ function ParallelSimulation.Unregister(id: number) castVisualize[lastId] = nil castVisualizeSettings[lastId] = nil castCaster[lastId] = nil + castMotor6D[lastId] = nil casts[id] = nil castCount = lastId - 1 @@ -270,8 +284,11 @@ local function UpdateCasts(deltaTime: number) -- Update cosmetic bullet local bullet = castCosmeticBullet[i] + local motor6d = castMotor6D[i] if bullet then - if bullet:IsA("BasePart") then + if motor6d then + motor6d.Transform = newCFrame + elseif bullet:IsA("BasePart") then bullet.CFrame = newCFrame else bullet:PivotTo(newCFrame) From 55abdb0962904b511cb51b599c145f79ff09a974 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 11:00:08 +0000 Subject: [PATCH 18/62] fix: Add MovementMethod to ActiveCast for Parallel mode --- src/FastCast2/ActiveCast.luau | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/FastCast2/ActiveCast.luau b/src/FastCast2/ActiveCast.luau index ff840d5..53dac56 100644 --- a/src/FastCast2/ActiveCast.luau +++ b/src/FastCast2/ActiveCast.luau @@ -762,7 +762,8 @@ function ActiveCast.createCastData( WorldRoot = workspace, MaxDistance = behavior.MaxDistance or DEFAULT_MAX_DISTANCE, CosmeticBulletObject = behavior.CosmeticBulletTemplate, - FastCastEventsModule = eventModule + FastCastEventsModule = eventModule, + MovementMethod = behavior.MovementMethod or "BulkMoveTo" }, UserData = {}, From 3afbe667270540bb99a4e42cf379189ec1f1c238 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 11:01:27 +0000 Subject: [PATCH 19/62] docs: Update TODO.md with completed items --- TODO.md | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/TODO.md b/TODO.md index 3e499d8..3963da2 100644 --- a/TODO.md +++ b/TODO.md @@ -1,10 +1,17 @@ -- [ ] Support both parallel and non-parallel modes - - FastCast2 currently has parallel Luau overhead; it should instead let users choose -- [ ] ActiveCast should expose AoS to users while internally using SoA for performance -- [ ] Consider using one RunService per core instead of multiple instances -- [ ] Add Motor6D transform feature -- [ ] Update documentation +- [x] Support both parallel and non-parallel modes + - FastCast2 now has FastCast.new() for Serial and FastCast.newParallel() for Parallel +- [x] ActiveCast should expose AoS to users while internally using SoA for performance + - ActiveCast uses pure data structures (AoS), SerialSimulation/ParallelSimulation use SoA internally +- [x] Consider using one RunService per core instead of multiple instances + - Serial: 1 global RunService for all casts, Parallel: 1 RunService per Actor +- [x] Add Motor6D transform feature + - Added MovementMethod to FastCastBehavior (BulkMoveTo/Transform) + - Motor6DPool for efficient pooling +- [ ] Update documentation - [ ] Add benchmarks -- [ ] Refactor +- [x] Refactor + - Removed metatable from ActiveCast, removed UpdateConnection + - Changed Trajectories to Trajectory + - Removed xpcall/pcall from hot path - [ ] Fix HighFidelityBehavior = 2 bug, where projectile passes through walls -- [ ] ActiveCast.Trajectories, wastes memory. change it to ActiveCast.Trajectory \ No newline at end of file +- [x] ActiveCast.Trajectories, wastes memory. change it to ActiveCast.Trajectory \ No newline at end of file From 3ef6be3679982f49fe143e77d864398197ba6028 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 11:05:02 +0000 Subject: [PATCH 20/62] fix: HighFidelityBehavior=2 bug - subRayDir used delta instead of timeIncrement --- src/FastCast2/ActiveCast.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FastCast2/ActiveCast.luau b/src/FastCast2/ActiveCast.luau index 53dac56..a0cc83c 100644 --- a/src/FastCast2/ActiveCast.luau +++ b/src/FastCast2/ActiveCast.luau @@ -554,7 +554,7 @@ local function SimulateCast( ) local subVelocity = GetVelocityAtTime(lastDelta + (timeIncrement * segmentIndex), initialVelocity, acceleration) - local subRayDir = subVelocity * delta + local subRayDir = subVelocity * timeIncrement local subResult = CastHandler(targetWorldRoot, subPosition, subRayDir, cast.RayInfo.Parameters, variant) local subDisplacement = (subPosition - (subPosition + subVelocity)).Magnitude From b46544109df1fd4486fe02fb28a5d7212a7fe276 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 11:15:56 +0000 Subject: [PATCH 21/62] docs: update comments --- src/FastCast2/init.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FastCast2/init.luau b/src/FastCast2/init.luau index 17f53fc..d5d4422 100644 --- a/src/FastCast2/init.luau +++ b/src/FastCast2/init.luau @@ -13,7 +13,7 @@ YOU SHOULD ONLY CREATE ONE CASTER PER GUN. YOU SHOULD >>>NEVER<<< CREATE A NEW CASTER EVERY TIME THE GUN NEEDS TO BE FIRED. - A caster (created with FastCastParallel.new()) represents a "gun". + A caster (created with FastCast.new() or FastCastParallel.new()) represents a "gun". When you consider a gun, you think of stats like accuracy, bullet speed, etc. This is the info a caster stores. -- From 59a1f9909d50a42d3d77da8da7d7b2fd6d86bea1 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 11:18:07 +0000 Subject: [PATCH 22/62] docs: Update TODO.md with all completed items including bug fixes --- TODO.md | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/TODO.md b/TODO.md index 3963da2..6375e6c 100644 --- a/TODO.md +++ b/TODO.md @@ -1,17 +1,15 @@ -- [x] Support both parallel and non-parallel modes - - FastCast2 now has FastCast.new() for Serial and FastCast.newParallel() for Parallel -- [x] ActiveCast should expose AoS to users while internally using SoA for performance +- [x] Support both parallel/nonParallel + - FastCast.new() for Serial, FastCast.newParallel() for Parallel +- [x] ActiveCast should exposes AoS to users, and Internally using SoA for performances - ActiveCast uses pure data structures (AoS), SerialSimulation/ParallelSimulation use SoA internally -- [x] Consider using one RunService per core instead of multiple instances - - Serial: 1 global RunService for all casts, Parallel: 1 RunService per Actor -- [x] Add Motor6D transform feature - - Added MovementMethod to FastCastBehavior (BulkMoveTo/Transform) +- [x] Consider using 1 RunService for each cores instead of using multiple + - Serial: 1 global RunService for all casts + - Parallel: 1 RunService per Actor +- [x] Add Motor6D Transform feature + - MovementMethod in FastCastBehavior (BulkMoveTo/Transform) - Motor6DPool for efficient pooling -- [ ] Update documentation +- [x] Fix HighFidelityBehavior = 2 bug - subRayDir used delta instead of timeIncrement +- [x] ActiveCast.Trajectories -> ActiveCast.Trajectory +- [ ] Documentation updates - [ ] Add benchmarks -- [x] Refactor - - Removed metatable from ActiveCast, removed UpdateConnection - - Changed Trajectories to Trajectory - - Removed xpcall/pcall from hot path -- [ ] Fix HighFidelityBehavior = 2 bug, where projectile passes through walls -- [x] ActiveCast.Trajectories, wastes memory. change it to ActiveCast.Trajectory \ No newline at end of file +- [x] Refactor - Removed metatable, UpdateConnection, xpcall from hot path \ No newline at end of file From 5b751e7cacab685e253cc3005ea1440a2640cc21 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 11:19:34 +0000 Subject: [PATCH 23/62] docs: Add comprehensive API documentation following devforum structure --- README.md | 133 +++++++++----------- docs/api-reference.md | 277 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 334 insertions(+), 76 deletions(-) create mode 100644 docs/api-reference.md diff --git a/README.md b/README.md index d1fadee..6f98ec6 100644 --- a/README.md +++ b/README.md @@ -65,22 +65,15 @@ Read more on [FastCast2 devforum](https://devforum.roblox.com/t/fastcast2-an-imp # Code example -Shooting projectiles from your head +Shooting projectiles from your head (Serial mode - simpler, main thread): ```lua -- Services local Rep = game:GetService("ReplicatedStorage") -local RepFirst = game:GetService("ReplicatedFirst") local Players = game:GetService("Players") -local UIS = game:GetService("UserInputService") - --- Modules -local FastCast2 = Rep:WaitForChild("FastCast2") -- Requires -local FastCastTypes = require(FastCast2:WaitForChild("TypeDefinitions")) -local FastCastEnums = require(FastCast2:WaitForChild("FastCastEnums")) -local FastCastM = require(FastCast2) +local FastCast2 = require(Rep:WaitForChild("FastCast2")) -- CONSTANTS local SPEED = 500 @@ -89,93 +82,81 @@ local SPEED = 500 local player = Players.LocalPlayer local character = player.Character or player.CharacterAdded:Wait() local Head = character:WaitForChild("Head") - local Mouse = player:GetMouse() local ProjectileContainer = workspace:WaitForChild("Projectiles") local ProjectileTemplate = Rep:WaitForChild("Projectile") -local debounce = false -local debounce_time = 0.05 - -- CastParams local CastParams = RaycastParams.new() CastParams.FilterDescendantsInstances = {character} CastParams.FilterType = Enum.RaycastFilterType.Exclude CastParams.IgnoreWater = true --- Behavior -local castBehavior: FastCastTypes = FastCastM.newBehavior() -castBehavior.MaxDistance = 1000 -castBehavior.RaycastParams = CastParams -castBehavior.HighFidelityBehavior = FastCastEnums.HighFidelityBehavior.Default -castBehavior.HighFidelitySegmentSize = 1 -castBehavior.Acceleration = Vector3.new(0, -workspace.Gravity/2.3, 0) -castBehavior.AutoIgnoreContainer = true -castBehavior.CosmeticBulletContainer = ProjectileContainer -castBehavior.CosmeticBulletTemplate = ProjectileTemplate -castBehavior.UserData = {} -- Initial UserData when ActiveCast created -castBehavior.FastCastEventsConfig = { - UseHit = true, - UseLengthChanged = false, -- Warning: Setting this to true will make your FPS tank when there are 100-200+ projectiles - UseCastTerminating = true, - UseCastFire = true, - UsePierced = false -} - --- Caster -local Caster = FastCastM.new() -Caster:Init( - 4, - RepFirst, - "CastVMs", - RepFirst, - "CastVMContainer", - "CastVM", - true -) - --- Functions - -local function OnCastTerminating(cast: FastCastTypes.ActiveCastData) +-- Behavior (FastCastBehavior) +local behavior = FastCast2.newBehavior() +behavior.MaxDistance = 1000 +behavior.RaycastParams = CastParams +behavior.HighFidelityBehavior = FastCastEnums.HighFidelityBehavior.Default +behavior.HighFidelitySegmentSize = 1 +behavior.Acceleration = Vector3.new(0, -workspace.Gravity/2.3, 0) +behavior.CosmeticBulletContainer = ProjectileContainer +behavior.CosmeticBulletTemplate = ProjectileTemplate + +-- MovementMethod: "BulkMoveTo" (default) or "Transform" (Motor6D) +behavior.MovementMethod = "BulkMoveTo" + +-- Serial Caster (runs on main thread, simpler) +local Caster = FastCast2.new() +Caster:Init(true, false) -- useBulkMoveTo, useObjectCache + +-- Events +Caster.CastTerminating:Connect(function(cast) local obj = cast.RayInfo.CosmeticBulletObject - if obj then - obj:Destroy() - end -end + if obj then obj:Destroy() end +end) -local function OnHit() - print("Hit!") -end +Caster.Hit:Connect(function(cast, result, velocity, bullet) + print("Hit: " .. result.Instance.Name) +end) -local function OnCastFire() - print("CastFire!") +-- Fire +local function fire() + local origin = Head.Position + local direction = (Mouse.Hit.Position - origin).Unit + Caster:RaycastFire(origin, direction, SPEED, behavior) end --- Connections - -Caster.CastTerminating:Connect(OnCastTerminating) -Caster.Hit:Connect(OnHit) -Caster.CastFire:Connect(OnCastFire) - -UIS.InputBegan:Connect(function(Input: InputObject, gp: boolean) - if gp then return end - if debounce then return end - - if Input.UserInputType == Enum.UserInputType.MouseButton1 then - debounce = true - - local Origin = Head.Position - local Direction = (Mouse.Hit.Position - Origin).Unit - - Caster:RaycastFire(Origin, Direction, SPEED, castBehavior) - - task.wait(debounce_time) - debounce = false +-- Input +game:GetService("UserInputService").InputBegan:Connect(function(input, gameProcessed) + if gameProcessed then return end + if input.UserInputType == Enum.UserInputType.MouseButton1 then + fire() end end) ``` +Parallel mode (for high-performance with multiple VMs): + +```lua +-- Parallel Caster (requires Init with worker count) +local Caster = FastCast2.newParallel() +Caster:Init( + 4, -- numWorkers (thread count) + workspace, -- newParent (VM folder parent) + "FastCastVMs", -- VM folder name + workspace, -- ContainerParent + "VMContainer", -- Container name + "VM", -- VM name + true, -- useBulkMoveTo + nil, -- FastCastEventsModule + false -- useObjectCache +) + +-- Fire same as serial +Caster:RaycastFire(origin, direction, speed, behavior) +``` +
How to set up [FastCastEventsModule](https://weenachuangkud.github.io/FastCast2/api/TypeDefinitions/#FastCastEventsModule) diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..208a9ad --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,277 @@ +# FastCast2 Documentation + +## 1. Caster + +### 1.1 How to construct and initialize Caster (`.new()`) - Serial Mode + +Serial Caster runs all cast simulations on the main thread. Simpler to use but less performant than Parallel. + +```lua +local caster = FastCast2.new() +caster:Init(useBulkMoveTo, useObjectCache, template, cacheSize, cacheHolder) +``` + +#### 1.1.1 How Initialization Works + +- `Init()` sets up the Serial Caster with optional BulkMoveTo and ObjectCache +- No Dispatcher needed - runs directly on main thread + +#### 1.1.2 ObjectCache + +ObjectCache reuses projectile parts for better performance: + +```lua +caster:Init(true, true, projectileTemplate, 500, workspace) +-- useBulkMoveTo: true, useObjectCache: true, template, cacheSize, holder +``` + +#### 1.1.3 Motor6D Transform + +Movement method for projectile animation: + +```lua +local behavior = FastCast2.newBehavior() +behavior.MovementMethod = "BulkMoveTo" -- Default - uses BulkMoveTo +-- or +behavior.MovementMethod = "Transform" -- Uses Motor6D for better performance +``` + +#### 1.1.4 Fields and Properties + +- `caster.LengthChanged` - Signal fired when cast length changes +- `caster.Hit` - Signal fired when cast hits something +- `caster.Pierced` - Signal fired when cast pierces something +- `caster.CastTerminating` - Signal fired when cast terminates +- `caster.CastFire` - Signal fired when cast is fired + +--- + +### 1.2 How to construct and initialize Caster (`.newParallel()`) - Parallel Mode + +Parallel Caster runs cast simulations on separate worker VMs for high-performance scenarios. + +```lua +local caster = FastCast2.newParallel() +caster:Init( + numWorkers, -- number of worker VMs (must be > 1) + newParent, -- Folder to place FastCastVMs + newName, -- name for FastCastVMs folder + ContainerParent, -- parent for worker containers + VMContainerName, -- name for containers + VMname, -- name for each worker VM + useBulkMoveTo, -- enable BulkMoveTo + fastCastEventsModule, -- optional events module + useObjectCache, -- enable ObjectCache + template, -- ObjectCache template + cacheSize, -- ObjectCache size + CacheHolder -- ObjectCache parent +) +``` + +#### 1.2.1 How Does Initialization Work + +- Creates Actor-based worker VMs using VMsDispatcher +- Each worker handles multiple casts in parallel via `ConnectParallel` + +#### 1.2.2 numWorkers + +Number of parallel workers. More workers = more parallel processing but higher overhead. + +#### 1.2.3 What are FastCastVMs (VMsDispatcher) + +FastCastVMs is a dispatcher system that spawns Actor-based worker scripts to handle casts in parallel. + +#### 1.2.4 ObjectCache (Parallel) + +Same as Serial but shared across workers. + +#### 1.2.5 BulkMoveTo + +Moves cosmetic bullets efficiently: + +```lua +caster:SetBulkMoveEnabled(true) +``` + +#### 1.2.6 Motor6D Transform + +Same as Serial - set `MovementMethod` in behavior. + +--- + +### 1.3 Methods + +#### 1.3.1 `.newBehavior()` + +Creates a FastCastBehavior for configuring casts: + +```lua +local behavior = caster:newBehavior() +-- or +local behavior = FastCast2.newBehavior() +``` + +#### 1.3.2 `:RaycastFire(origin, direction, velocity, behavior)` + +Fire a raycast projectile. + +#### 1.3.3 `:BlockcastFire(origin, size, direction, velocity, behavior)` + +Fire a blockcast projectile. + +#### 1.3.4 ':SpherecastFire(origin, radius, direction, velocity, behavior)' + +Fire a spherecast projectile. + +#### 1.3.5 - 1.3.14 Cast Manipulation + +- `GetVelocityCast(cast)` - Get projectile velocity +- `GetAccelerationCast(cast)` - Get projectile acceleration +- `GetPositionCast(cast)` - Get projectile position +- `SetVelocityCast(cast, velocity)` - Set projectile velocity +- `SetAccelerationCast(cast, acceleration)` - Set projectile acceleration +- `SetPositionCast(cast, position)` - Set projectile position +- `PauseCast(cast, paused)` - Pause/resume projectile +- `AddPositionCast(cast, position)` - Add position offset +- `AddVelocityCast(cast, velocity)` - Add velocity offset +- `AddAccelerationCast(cast, acceleration)` - Add acceleration offset + +#### 1.3.15 `SyncChangesToCast(cast)` + +Sync changes to parallel workers (only needed in Parallel mode). + +#### 1.3.16 `TerminateCast(cast)` + +Forcefully terminate a cast. + +#### 1.3.17 - 1.3.20 Other Methods + +- `:SetBulkMoveEnabled(enabled)` - Enable/disable BulkMoveTo +- `:SetObjectCacheEnabled(enabled)` - Enable/disable ObjectCache +- ':SetFastCastEventsModule(module)' - Set events module (Parallel only) +- `:Destroy()` - Destroy the caster + +--- + +## 2. ActiveCastData + +### 2.1 What is ActiveCast + +ActiveCast represents a projectile in flight. It's a pure data structure (AoS) exposed to users, while internally FastCast2 uses SoA for performance. + +### 2.2 Data Structure + +```lua +cast.Caster -- Reference to parent Caster +cast.StateInfo -- Runtime state (paused, runtime, etc.) +cast.RayInfo -- Raycast parameters and result +cast.UserData -- User-defined data +cast.Type -- "Raycast", "Blockcast", or "Spherecast" +cast.CFrame -- Current position and rotation +cast.ID -- Unique cast identifier +``` + +### 2.3 Variants + +- `ActiveCastData` - Standard raycast +- `ActiveBlockcastData` - Has `.RayInfo.Size` +- `ActiveSpherecastData` - Has `.RayInfo.Radius` + +--- + +## 3. TypeDefinitions + +### 3.1 Caster + +Properties exposed on Caster object: + +- `WorldRoot` - Workspace for raycasts +- `Events` - Signal connections +- `Dispatcher` - Parallel dispatcher (Parallel only) +- `ObjectCache` - Object caching system +- `ObjectCacheEnabled` - Whether ObjectCache is active +- `BulkMoveEnabled` - Whether BulkMoveTo is active + +### 3.2 ActiveCastData + +#### 3.2.2 StateInfo + +- `HighFidelityBehavior` - Precision mode: + - `Default` (1) - Standard precision + - `Automatic` (2) - Auto-adjusts precision + - `Always` (3) - Always high precision +- `HighFidelitySegmentSize` - Segment size for high-fidelity mode +- `Paused` - Whether cast is paused +- `TotalRuntime` - Time since cast started +- `DistanceCovered` - Total distance traveled +- `Trajectory` - Single trajectory object (Origin, Velocity, Acceleration, StartTime, EndTime) + +#### 3.2.3 RayInfo + +- `Parameters` - RaycastParams +- `WorldRoot` - Target WorldRoot +- `MaxDistance` - Maximum travel distance +- `CosmeticBulletObject` - Visual projectile part +- `MovementMethod` - "BulkMoveTo" or "Transform" + +### 3.3 FastCastBehavior + +Configuration for cast behavior: + +```lua +local behavior = FastCast2.newBehavior() +behavior.RaycastParams = RaycastParams.new() +behavior.MaxDistance = 1000 +behavior.Acceleration = Vector3.new(0, -196.2, 0) -- Gravity +behavior.HighFidelityBehavior = 1 -- Default +behavior.HighFidelitySegmentSize = 0.5 +behavior.MovementMethod = "BulkMoveTo" +behavior.CosmeticBulletTemplate = part +behavior.CosmeticBulletContainer = workspace +behavior.AutoIgnoreContainer = true +``` + +--- + +## 4. Special + +### 4.1 FastCastEventsModule + +FastCastEventsModule is a ModuleScript with callback functions for parallel optimization. + +```lua +-- In a ModuleScript +local module = {} + +module.LengthChanged = function(cast) + -- Called every frame +end + +module.Hit = function(cast, result, velocity, bullet) + -- Called on hit +end + +module.CanPierce = function(cast, result, velocity, bullet) + -- Return true to pierce, false to stop + return false +end + +return module +``` + +Then set it: +```lua +caster:SetFastCastEventsModule(pathToModule) +``` + +Note: Not available in Serial mode - use standard Signals instead. + +--- + +## Performance + +FastCast2 uses: +- **Serial Mode**: Single RunService with SoA for all casts +- **Parallel Mode**: One RunService per Actor with SoA within each + +This approach replaces per-cast RunService connections for better performance. \ No newline at end of file From 87692f192968a67abd493dd6593ef09b576ea0f9 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 11:25:38 +0000 Subject: [PATCH 24/62] release: bump version to 0.1.0 with Serial/Parallel modes, Motor6D, SoA - Added 0.1.0 changelog entry documenting new features - Created Serial benchmark (benchSerial.client.luau) - Created Parallel benchmark (benchParallel.client.luau) - Updated TODO.md with all completed items --- Benchmarks/benchParallel.client.luau | 154 +++++++++++++++++++++++++++ Benchmarks/benchSerial.client.luau | 143 +++++++++++++++++++++++++ TODO.md | 4 +- docs/changelog.md | 31 ++++++ 4 files changed, 330 insertions(+), 2 deletions(-) create mode 100644 Benchmarks/benchParallel.client.luau create mode 100644 Benchmarks/benchSerial.client.luau diff --git a/Benchmarks/benchParallel.client.luau b/Benchmarks/benchParallel.client.luau new file mode 100644 index 0000000..48f5cc4 --- /dev/null +++ b/Benchmarks/benchParallel.client.luau @@ -0,0 +1,154 @@ +-- Services +local RS = game:GetService("RunService") +local Rep = game:GetService("ReplicatedStorage") +local UIS = game:GetService("UserInputService") +local RepFirst = game:GetService("ReplicatedFirst") + +-- Requires +local FastCast = require(Rep:WaitForChild("FastCast2")) + +-- Variables +local ProjectileContainer = Instance.new("Folder") +ProjectileContainer.Name = "FastCast2PJ_Parallel" +ProjectileContainer.Parent = workspace +local ProjectileTemplate = Instance.new("Part") +ProjectileTemplate.Name = "Projectile" +ProjectileTemplate.Parent = Rep +ProjectileTemplate.Size = Vector3.new(1,1,1) +ProjectileTemplate.CanCollide = false +ProjectileTemplate.Anchored = true +ProjectileTemplate.CanQuery = false +ProjectileTemplate.CanTouch = false +ProjectileTemplate.Position = Vector3.new(1,1,1) +ProjectileTemplate.Massless = true + +-- FPS tracking +local startTime = tick() +local updateRate = 0.5 +local fpsTable = {} +local averageFps = 0 +local maxFps = 0 +local minFps = math.huge +local currentFps = 0 + +RS.Heartbeat:Connect(function(dt: number) + local fps = 1/dt + currentFps = fps + if fps > maxFps then + maxFps = fps + end + if fps < minFps then + minFps = fps + end + table.insert(fpsTable, fps) + + if tick() >= startTime + updateRate then + local totalFps = 0 + for _, vFps in fpsTable do + totalFps += vFps + end + averageFps = totalFps / #fpsTable + fpsTable = {} + startTime = tick() + end +end) + +-- CastParams +local CastParams = RaycastParams.new() +CastParams.FilterDescendantsInstances = {} +CastParams.FilterType = Enum.RaycastFilterType.Exclude +CastParams.IgnoreWater = true + +-- Behavior +local castBehavior = FastCast.newBehavior() +castBehavior.MaxDistance = 999999999 +castBehavior.RaycastParams = CastParams +castBehavior.HighFidelityBehavior = 1 +castBehavior.HighFidelitySegmentSize = 1 +castBehavior.Acceleration = Vector3.new(0, 0, 0) +castBehavior.AutoIgnoreContainer = true +castBehavior.CosmeticBulletContainer = ProjectileContainer +castBehavior.CosmeticBulletTemplate = ProjectileTemplate + +-- Parallel Caster +local Caster = FastCast.newParallel() +Caster:Init( + 4, -- numWorkers + RepFirst, -- newParent + "CastVMs", -- newName + RepFirst, -- ContainerParent + "CastVMContainer", -- VMContainerName + "CastVM", -- VMname + true, -- useBulkMoveTo + nil, -- FastCastEventsModule (optional for parallel) + false, -- useObjectCache + nil, -- Template + 500, -- CacheSize + workspace -- CacheHolder +) + +local activeCasts = {} + +Caster.CastFire:Connect(function(cast) + table.insert(activeCasts, cast) +end) + +-- Functions +local function summary() + print(string.format("Delta: %.2f ms", 1000 / currentFps)) + print(string.format("Average FPS: %.2f", averageFps)) + print(string.format("Max FPS: %.2f", maxFps)) + print(string.format("Min FPS: %.2f", minFps)) +end + +-- Benchmark +local isBenchmarking = false +local AMOUNT = 5000 +local BENCH_TIME = 5 + +UIS.InputBegan:Connect(function(input, gp) + if gp then return end + if isBenchmarking then return end + if input.KeyCode == Enum.KeyCode.P then + isBenchmarking = true + print("=== PARALLEL MODE BENCHMARK ===") + print(string.format("Firing %d casts...", AMOUNT)) + + for i = 1, AMOUNT do + Caster:RaycastFire( + Vector3.new( + math.random(-1, 1) * 5000, + math.random(-1, 1) * 5000, + math.random(-1, 1) * 5000 + ), + Vector3.new( + math.random(-1, 1) * 5000, + math.random(-1, 1) * 5000, + math.random(-1, 1) * 5000 + ), + 35, + castBehavior + ) + end + + print("=== CREATION COMPLETE ===") + summary() + + task.wait(BENCH_TIME) + + print("=== SIMULATION COMPLETE ===") + summary() + + print("=== CLEANUP ===") + for i = #activeCasts, 1, -1 do + Caster:TerminateCast(activeCasts[i]) + end + activeCasts = {} + + print("=== DONE ===") + summary() + isBenchmarking = false + end +end) + +print("Press P to start Parallel benchmark") \ No newline at end of file diff --git a/Benchmarks/benchSerial.client.luau b/Benchmarks/benchSerial.client.luau new file mode 100644 index 0000000..161c1e1 --- /dev/null +++ b/Benchmarks/benchSerial.client.luau @@ -0,0 +1,143 @@ +-- Services +local RS = game:GetService("RunService") +local Rep = game:GetService("ReplicatedStorage") +local UIS = game:GetService("UserInputService") + +-- Requires +local FastCast = require(Rep:WaitForChild("FastCast2")) + +-- Variables +local ProjectileContainer = Instance.new("Folder") +ProjectileContainer.Name = "FastCast2PJ" +ProjectileContainer.Parent = workspace +local ProjectileTemplate = Instance.new("Part") +ProjectileTemplate.Name = "Projectile" +ProjectileTemplate.Parent = Rep +ProjectileTemplate.Size = Vector3.new(1,1,1) +ProjectileTemplate.CanCollide = false +ProjectileTemplate.Anchored = true +ProjectileTemplate.CanQuery = false +ProjectileTemplate.CanTouch = false +ProjectileTemplate.Position = Vector3.new(1,1,1) +ProjectileTemplate.Massless = true + +-- FPS tracking +local startTime = tick() +local updateRate = 0.5 +local fpsTable = {} +local averageFps = 0 +local maxFps = 0 +local minFps = math.huge +local currentFps = 0 + +RS.Heartbeat:Connect(function(dt: number) + local fps = 1/dt + currentFps = fps + if fps > maxFps then + maxFps = fps + end + if fps < minFps then + minFps = fps + end + table.insert(fpsTable, fps) + + if tick() >= startTime + updateRate then + local totalFps = 0 + for _, vFps in fpsTable do + totalFps += vFps + end + averageFps = totalFps / #fpsTable + fpsTable = {} + startTime = tick() + end +end) + +-- CastParams +local CastParams = RaycastParams.new() +CastParams.FilterDescendantsInstances = {} +CastParams.FilterType = Enum.RaycastFilterType.Exclude +CastParams.IgnoreWater = true + +-- Behavior +local castBehavior = FastCast.newBehavior() +castBehavior.MaxDistance = 999999999 +castBehavior.RaycastParams = CastParams +castBehavior.HighFidelityBehavior = 1 +castBehavior.HighFidelitySegmentSize = 1 +castBehavior.Acceleration = Vector3.new(0, 0, 0) +castBehavior.AutoIgnoreContainer = true +castBehavior.CosmeticBulletContainer = ProjectileContainer +castBehavior.CosmeticBulletTemplate = ProjectileTemplate + +-- Serial Caster +local Caster = FastCast.new() +Caster:Init( + true, -- useBulkMoveTo + false -- useObjectCache +) + +local activeCasts = {} + +Caster.CastFire:Connect(function(cast) + table.insert(activeCasts, cast) +end) + +-- Functions +local function summary() + print(string.format("Delta: %.2f ms", 1000 / currentFps)) + print(string.format("Average FPS: %.2f", averageFps)) + print(string.format("Max FPS: %.2f", maxFps)) + print(string.format("Min FPS: %.2f", minFps)) +end + +-- Benchmark +local isBenchmarking = false +local AMOUNT = 5000 +local BENCH_TIME = 5 + +UIS.InputBegan:Connect(function(input, gp) + if gp then return end + if isBenchmarking then return end + if input.KeyCode == Enum.KeyCode.E then + isBenchmarking = true + print("=== SERIAL MODE BENCHMARK ===") + print(string.format("Firing %d casts...", AMOUNT)) + + for i = 1, AMOUNT do + Caster:RaycastFire( + Vector3.new( + math.random(-1, 1) * 5000, + math.random(-1, 1) * 5000, + math.random(-1, 1) * 5000 + ), + Vector3.new( + math.random(-1, 1) * 5000, + math.random(-1, 1) * 5000, + math.random(-1, 1) * 5000 + ), + 35, + castBehavior + ) + end + + print("=== CREATION COMPLETE ===") + summary() + + task.wait(BENCH_TIME) + + print("=== SIMULATION COMPLETE ===") + summary() + + print("=== CLEANUP ===") + for i = #activeCasts, 1, -1 do + Caster:TerminateCast(activeCasts[i]) + end + activeCasts = {} + + print("=== DONE ===") + summary() + isBenchmarking = false + end +end) + +print("Press E to start benchmark") \ No newline at end of file diff --git a/TODO.md b/TODO.md index 6375e6c..9e53ece 100644 --- a/TODO.md +++ b/TODO.md @@ -10,6 +10,6 @@ - Motor6DPool for efficient pooling - [x] Fix HighFidelityBehavior = 2 bug - subRayDir used delta instead of timeIncrement - [x] ActiveCast.Trajectories -> ActiveCast.Trajectory -- [ ] Documentation updates -- [ ] Add benchmarks +- [x] Documentation updates +- [x] Add benchmarks - [x] Refactor - Removed metatable, UpdateConnection, xpcall from hot path \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md index add4d87..a758364 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -11,6 +11,37 @@ and this project adheres to Semantic Versioning (https://semver.org/). --- +## [0.1.0] — 2026-05-07 + +### Added +- **Serial Mode** (`FastCast.new()`) - Main thread projectile simulation, simpler API +- **Parallel Mode** (`FastCast.newParallel()`) - Worker VM based parallel simulation +- **Motor6D Transform** - New movement method using Motor6D for better performance + - Set `behavior.MovementMethod = "Transform"` to use +- **SerialSimulation** - Single RunService with SoA pattern for Serial casts +- **ParallelSimulation** - Per-Actor SoA pattern for Parallel casts +- **Motor6DPool** - Object pooling for Motor6D instances + +### Changed +- **API Restructure**: + - `.new()` now creates Serial caster (requires `Init(useBulkMoveTo, useObjectCache)`) + - `.newParallel()` creates Parallel caster (requires `Init(numWorkers, ...)`) + - Removed `FastCastParallel.new()` - use `.newParallel()` instead +- **ActiveCast** - Changed from OOP to pure data structure (AoS for users, SoA internally) +- **Trajectories** → **Trajectory** - Single object instead of array (saves memory) +- Removed **UpdateConnection** - No longer uses per-cast RunService connections +- Removed **xpcall/pcall** from hot path for performance +- Removed **FastCastEventsModule** from Serial mode (Parallel only) + +### Fixed +- **HighFidelityBehavior = 2 bug** - Fixed subRayDir calculation using `delta` instead of `timeIncrement` + +### Performance +- Serial: 1 global RunService handling all casts with SoA arrays +- Parallel: 1 RunService per Actor with SoA arrays within each + +--- + ## [0.0.9] — 2026-03-03 ### Changed From 82519af053f4dbb3b987676f64d31c7e14c0b6fc Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Thu, 7 May 2026 11:27:27 +0000 Subject: [PATCH 25/62] chore: remove version comments from source files --- docs/changelog.md | 1 - src/FastCast2/ActiveCast.luau | 2 +- src/FastCast2/ActiveCastSerial.luau | 2 +- src/FastCast2/BaseCast.luau | 2 +- src/FastCast2/BaseCastSerial.luau | 2 +- src/FastCast2/Configs.luau | 2 +- src/FastCast2/DefaultConfigs.luau | 2 +- src/FastCast2/FastCastEnums.luau | 2 +- src/FastCast2/Motor6DPool.luau | 2 +- src/FastCast2/ParallelSimulation.luau | 2 +- src/FastCast2/SerialSimulation.luau | 2 +- src/FastCast2/TypeDefinitions.luau | 2 +- src/FastCast2/init.luau | 2 +- 13 files changed, 12 insertions(+), 13 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index a758364..c2db7d4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -7,7 +7,6 @@ sidebar_position: 3 All notable changes to this project will be documented in this file. The format is based on Keep a Changelog (https://keepachangelog.com/en/1.0.0/) -and this project adheres to Semantic Versioning (https://semver.org/). --- diff --git a/src/FastCast2/ActiveCast.luau b/src/FastCast2/ActiveCast.luau index a0cc83c..1c1628b 100644 --- a/src/FastCast2/ActiveCast.luau +++ b/src/FastCast2/ActiveCast.luau @@ -2,7 +2,7 @@ --[[ - Modified by: Mawin CK - Date : 2025 - -- Verison : 0.0.9 + ]] -- NOTE: Please don't modify or changing anything diff --git a/src/FastCast2/ActiveCastSerial.luau b/src/FastCast2/ActiveCastSerial.luau index 90ff6c4..2fc86ae 100644 --- a/src/FastCast2/ActiveCastSerial.luau +++ b/src/FastCast2/ActiveCastSerial.luau @@ -1,7 +1,7 @@ --[[ - Author : Mawin CK - Date : 2025 - -- Version : 0.0.9 + ActiveCastSerial - Serial mode with single RunService, SoA pattern, queue technique Similar to SwiftCast implementation diff --git a/src/FastCast2/BaseCast.luau b/src/FastCast2/BaseCast.luau index 72337d6..2b5715d 100644 --- a/src/FastCast2/BaseCast.luau +++ b/src/FastCast2/BaseCast.luau @@ -1,7 +1,7 @@ --[[ - Author : Mawin CK - Date : 2025 - -- Verison : 0.0.9 + ]] -- Services diff --git a/src/FastCast2/BaseCastSerial.luau b/src/FastCast2/BaseCastSerial.luau index d8d2809..7aaa529 100644 --- a/src/FastCast2/BaseCastSerial.luau +++ b/src/FastCast2/BaseCastSerial.luau @@ -1,7 +1,7 @@ --[[ - Author : Mawin CK - Date : 2025 - -- Version : 0.0.9 + BaseCastSerial - Uses SerialSimulation with SoA pattern ]] diff --git a/src/FastCast2/Configs.luau b/src/FastCast2/Configs.luau index a2e91b8..0748274 100644 --- a/src/FastCast2/Configs.luau +++ b/src/FastCast2/Configs.luau @@ -1,7 +1,7 @@ --[[ - Author : Mawin CK - Date : 2025 - -- Verison : 0.0.3 + ]] -- Haha, noob diff --git a/src/FastCast2/DefaultConfigs.luau b/src/FastCast2/DefaultConfigs.luau index 6385861..eef1317 100644 --- a/src/FastCast2/DefaultConfigs.luau +++ b/src/FastCast2/DefaultConfigs.luau @@ -1,7 +1,7 @@ --[[ - Author : Mawin_CK - Date : 2025 - -- Verison : 0.0.6 + ]] --!strict diff --git a/src/FastCast2/FastCastEnums.luau b/src/FastCast2/FastCastEnums.luau index 0fd158c..6bb0478 100644 --- a/src/FastCast2/FastCastEnums.luau +++ b/src/FastCast2/FastCastEnums.luau @@ -1,7 +1,7 @@ --[[ - Author : Mawin CK - Date : 2025 - -- Verison : 0.0.9 + ]] --!strict diff --git a/src/FastCast2/Motor6DPool.luau b/src/FastCast2/Motor6DPool.luau index e67ca0a..2b3e94f 100644 --- a/src/FastCast2/Motor6DPool.luau +++ b/src/FastCast2/Motor6DPool.luau @@ -1,7 +1,7 @@ --[[ - Author : Mawin CK - Date : 2025 - -- Version : 0.0.9 + Motor6D Pool for efficient projectile movement using Transform mode. Like SwiftCast implementation. diff --git a/src/FastCast2/ParallelSimulation.luau b/src/FastCast2/ParallelSimulation.luau index d6a22c3..94353e8 100644 --- a/src/FastCast2/ParallelSimulation.luau +++ b/src/FastCast2/ParallelSimulation.luau @@ -1,7 +1,7 @@ --[[ - Author : Mawin CK - Date : 2025 - -- Version : 0.0.9 + ParallelSimulation - SoA pattern for Parallel mode Each Actor has its own instance of this for parallel processing diff --git a/src/FastCast2/SerialSimulation.luau b/src/FastCast2/SerialSimulation.luau index d6eb4bd..589d4ba 100644 --- a/src/FastCast2/SerialSimulation.luau +++ b/src/FastCast2/SerialSimulation.luau @@ -1,7 +1,7 @@ --[[ - Author : Mawin CK - Date : 2025 - -- Version : 0.0.9 + SerialSimulation - Single RunService handling multiple ActiveCastSerial Uses SoA pattern for performance, queue technique for events diff --git a/src/FastCast2/TypeDefinitions.luau b/src/FastCast2/TypeDefinitions.luau index 5ab36df..ed9eecb 100644 --- a/src/FastCast2/TypeDefinitions.luau +++ b/src/FastCast2/TypeDefinitions.luau @@ -3,7 +3,7 @@ --[[ - Author : Mawin CK - Date : 2025 - -- Verison : 0.0.9 + ]] --[=[ diff --git a/src/FastCast2/init.luau b/src/FastCast2/init.luau index d5d4422..001db35 100644 --- a/src/FastCast2/init.luau +++ b/src/FastCast2/init.luau @@ -46,7 +46,7 @@ - Date : 2025 ]] --- Verison : 0.0.9 + --[=[ @class FastCastParallel From 0e2fa47273a87b876fdc577ce503ef82a79a3667 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 04:55:23 +0000 Subject: [PATCH 26/62] docs: add Rojo installation guide to README --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 6f98ec6..d411bbf 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,27 @@ Read more on [FastCast2 devforum](https://devforum.roblox.com/t/fastcast2-an-imp --- +## Install with Rojo + +1. Install the [Rojo CLI](https://rojo.space/docs/installation/) for your system. +2. Clone this repository: + ```bash + git clone https://github.com/weenachuangkud/FastCast2.git + cd FastCast2 + rm -rf FastCast2/.git + ``` +3. Sync to Roblox: + ```bash + rojo sync -o + ``` + Or serve live with: + ```bash + rojo serve + ``` + Then connect in Roblox Studio via **Studio → Plugins → Rojo**. + +--- + # Code example Shooting projectiles from your head (Serial mode - simpler, main thread): From 5b2dad14e3c259a725ebed71013d6cd034b6b0f3 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 05:04:11 +0000 Subject: [PATCH 27/62] fix: resolve CodeRabbit issues from PR #39 - README.md: Fix RayHit -> Hit event name - ActiveCast.luau: Fix latestTrajectory -> trajectory variable references - BaseCast.luau: Add Actives table for parallel casts - init.luau: Add return statements, fix SetBulkMoveEnabled guard, delegate termination to BaseCast - ActiveCastSerial.luau: Add Size/Radius to RayInfo - ParallelSimulation.luau: Fix iteration order, server check, fix swap-delete to reference lastId correctly - SerialSimulation.luau: Fix iteration order - Motor6DPool.luau: Simplify Get() recursion - benchSerial.client.luau: Fix direction normalization --- Benchmarks/benchSerial.client.luau | 22 +++++++++++++--------- README.md | 2 +- src/FastCast2/ActiveCast.luau | 14 +++++++------- src/FastCast2/ActiveCastSerial.luau | 4 +++- src/FastCast2/BaseCast.luau | 3 +++ src/FastCast2/Motor6DPool.luau | 6 ++---- src/FastCast2/ParallelSimulation.luau | 19 ++++++++++++------- src/FastCast2/SerialSimulation.luau | 6 +++--- src/FastCast2/init.luau | 7 +++++++ 9 files changed, 51 insertions(+), 32 deletions(-) diff --git a/Benchmarks/benchSerial.client.luau b/Benchmarks/benchSerial.client.luau index 161c1e1..bc69165 100644 --- a/Benchmarks/benchSerial.client.luau +++ b/Benchmarks/benchSerial.client.luau @@ -104,17 +104,21 @@ UIS.InputBegan:Connect(function(input, gp) print(string.format("Firing %d casts...", AMOUNT)) for i = 1, AMOUNT do + local direction = Vector3.new( + math.random() * 2 - 1, + math.random() * 2 - 1, + math.random() * 2 - 1 + ) + if direction.Magnitude == 0 then + direction = Vector3.new(0, 0, 1) + end Caster:RaycastFire( Vector3.new( - math.random(-1, 1) * 5000, - math.random(-1, 1) * 5000, - math.random(-1, 1) * 5000 - ), - Vector3.new( - math.random(-1, 1) * 5000, - math.random(-1, 1) * 5000, - math.random(-1, 1) * 5000 - ), + math.random() * 2 - 1, + math.random() * 2 - 1, + math.random() * 2 - 1 + ) * 5000, + direction, 35, castBehavior ) diff --git a/README.md b/README.md index d411bbf..15b8e89 100644 --- a/README.md +++ b/README.md @@ -218,7 +218,7 @@ module.CastTerminating = function() print("CastTerminating!") end -module.RayHit = function() +module.Hit = function() print("Hit!") end diff --git a/src/FastCast2/ActiveCast.luau b/src/FastCast2/ActiveCast.luau index 1c1628b..fd598b4 100644 --- a/src/FastCast2/ActiveCast.luau +++ b/src/FastCast2/ActiveCast.luau @@ -413,8 +413,8 @@ local function SimulateCast( local FastCastEventsModuleConfig = cast.StateInfo.FastCastEventsModuleConfig - if typeof(latestTrajectory.Acceleration) ~= "Vector3" then - latestTrajectory.Acceleration = Vector3.new() + if typeof(trajectory.Acceleration) ~= "Vector3" then + trajectory.Acceleration = Vector3.new() end local VisualizeVariant = {} @@ -880,10 +880,10 @@ function ActiveCast.createCastData( cast.StateInfo.IsActivelyResimulating = true - local origin = latestTrajectory.Origin - local totalDelta = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime - local initialVelocity = latestTrajectory.InitialVelocity - local acceleration = latestTrajectory.Acceleration + local origin = trajectory.Origin + local totalDelta = cast.StateInfo.TotalRuntime - trajectory.StartTime + local initialVelocity = trajectory.InitialVelocity + local acceleration = trajectory.Acceleration local lastPoint = GetPositionAtTime(totalDelta, origin, initialVelocity, acceleration) --local lastVelocity = GetVelocityAtTime(totalDelta, initialVelocity, acceleration) @@ -891,7 +891,7 @@ function ActiveCast.createCastData( cast.StateInfo.TotalRuntime += delta - totalDelta = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime +totalDelta = cast.StateInfo.TotalRuntime - trajectory.StartTime local currentPoint = GetPositionAtTime(totalDelta, origin, initialVelocity, acceleration) local currentVelocity = GetVelocityAtTime(totalDelta, initialVelocity, acceleration) diff --git a/src/FastCast2/ActiveCastSerial.luau b/src/FastCast2/ActiveCastSerial.luau index 2fc86ae..50e87a5 100644 --- a/src/FastCast2/ActiveCastSerial.luau +++ b/src/FastCast2/ActiveCastSerial.luau @@ -150,7 +150,9 @@ function ActiveCastSerial.new(caster: any, castData: any): any WorldRoot = workspace, MaxDistance = castData.MaxDistance or DEFAULT_MAX_DISTANCE, CosmeticBulletObject = castData.CosmeticBulletObject, - MovementMethod = castData.MovementMethod or "BulkMoveTo" + MovementMethod = castData.MovementMethod or "BulkMoveTo", + Size = castData.Size, + Radius = castData.Radius }, Type = CastVariantTypes[castData.CastType], diff --git a/src/FastCast2/BaseCast.luau b/src/FastCast2/BaseCast.luau index 2b5715d..2807ff3 100644 --- a/src/FastCast2/BaseCast.luau +++ b/src/FastCast2/BaseCast.luau @@ -182,6 +182,7 @@ function BaseCast:Raycast( } :: any) ParallelSimulation.Register(cast) + Actives[cast.ID] = cast if Behavior.FastCastEventsConfig.UseCastFire then SendCastFire(cast, Origin, Direction, Velocity, Behavior) @@ -246,6 +247,7 @@ function BaseCast:Blockcast( } :: any) ParallelSimulation.Register(cast) + Actives[cast.ID] = cast if Behavior.FastCastEventsConfig.UseCastFire then SendCastFire(cast, Origin, Direction, Velocity, Behavior) @@ -290,6 +292,7 @@ function BaseCast:Spherecast( } :: any) ParallelSimulation.Register(cast) + Actives[cast.ID] = cast if Behavior.FastCastEventsConfig.UseCastFire then SendCastFire(cast, Origin, Direction, Velocity, Behavior) diff --git a/src/FastCast2/Motor6DPool.luau b/src/FastCast2/Motor6DPool.luau index 2b3e94f..97feb14 100644 --- a/src/FastCast2/Motor6DPool.luau +++ b/src/FastCast2/Motor6DPool.luau @@ -44,12 +44,10 @@ local function Initialize() end local function Get(): Motor6D - if #FreeMotor6Ds > 0 then - return table.remove(FreeMotor6Ds) :: Motor6D - else + if #FreeMotor6Ds == 0 then GrowPool(PoolSize * GROWTH_RATE) - return Get() end + return table.remove(FreeMotor6Ds) :: Motor6D end local function Return(motor6d: Motor6D) diff --git a/src/FastCast2/ParallelSimulation.luau b/src/FastCast2/ParallelSimulation.luau index 94353e8..a82ba06 100644 --- a/src/FastCast2/ParallelSimulation.luau +++ b/src/FastCast2/ParallelSimulation.luau @@ -186,11 +186,12 @@ function ParallelSimulation.Unregister(id: number) if casts[lastId] then casts[lastId].ID = id + casts[id] = casts[lastId] end end - if castMotor6D[id] then - Motor6DPool.Disconnect(castMotor6D[id]) + if id ~= lastId and castMotor6D[lastId] then + Motor6DPool.Disconnect(castMotor6D[lastId]) end castIDs[lastId] = nil @@ -310,17 +311,21 @@ local function UpdateCasts(deltaTime: number) end end - -- Process destroyed casts - for _, id in destroyedIds do - ParallelSimulation.Terminate(id) + -- Process destroyed casts (iterate in reverse to avoid index invalidation) + for i = #destroyedIds, 1, -1 do + ParallelSimulation.Terminate(destroyedIds[i]) end DispatchAllEvents() end function ParallelSimulation.Start() - if ParallelSimulation.StepConnection then return end - ParallelSimulation.StepConnection = RS.PreRender:ConnectParallel(UpdateCasts) + if SerialSimulation.StepConnection then return end + if RS:IsClient() then + ParallelSimulation.StepConnection = RS.PreRender:ConnectParallel(UpdateCasts) + else + ParallelSimulation.StepConnection = RS.Heartbeat:Connect(UpdateCasts) + end end function ParallelSimulation.Stop() diff --git a/src/FastCast2/SerialSimulation.luau b/src/FastCast2/SerialSimulation.luau index 589d4ba..0161bd8 100644 --- a/src/FastCast2/SerialSimulation.luau +++ b/src/FastCast2/SerialSimulation.luau @@ -314,9 +314,9 @@ local function UpdateCasts(deltaTime: number) end end - -- Process destroyed casts - for _, id in destroyedIds do - SerialSimulation.Terminate(id) + -- Process destroyed casts (iterate in reverse to avoid index invalidation) + for i = #destroyedIds, 1, -1 do + SerialSimulation.Terminate(destroyedIds[i]) end DispatchAllEvents() diff --git a/src/FastCast2/init.luau b/src/FastCast2/init.luau index 001db35..918cc14 100644 --- a/src/FastCast2/init.luau +++ b/src/FastCast2/init.luau @@ -645,6 +645,7 @@ end function FastCastParallel:SetBulkMoveEnabled(enabled: boolean) if not self.AlreadyInit or not self.Dispatcher then warn("Caster not initialized", self) + return end self.Dispatcher:DispatchAll("BindBulkMoveTo", enabled) @@ -927,6 +928,10 @@ function FastCastSerial:TerminateCast(cast: vaildcast, castTerminatingFunction: cast.RayInfo.CosmeticBulletObject = nil end + if self.BaseCast then + self.BaseCast:TerminateCast(cast, castTerminatingFunction) + end + if castTerminatingFunction then castTerminatingFunction(cast) end @@ -1041,6 +1046,7 @@ function FastCast.new() WorldRoot = workspace, } setmetatable(fs, FastCastSerial) + return fs end --[=[ @@ -1069,6 +1075,7 @@ function FastCast.newParallel() AlreadyInit = false } setmetatable(fp, FastCastParallel) + return fp end return FastCast \ No newline at end of file From e816b67424ea83799a4b3febf7b5a895f596b071 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 05:19:38 +0000 Subject: [PATCH 28/62] Add return statement to guarding --- src/FastCast2/ActiveCast.luau | 1 + 1 file changed, 1 insertion(+) diff --git a/src/FastCast2/ActiveCast.luau b/src/FastCast2/ActiveCast.luau index fd598b4..ffb9133 100644 --- a/src/FastCast2/ActiveCast.luau +++ b/src/FastCast2/ActiveCast.luau @@ -110,6 +110,7 @@ local function DebrisAdd(obj: Instance, Lifetime: number) end if Lifetime <= 0 then obj:Destroy() + return end task.delay(Lifetime, function() From b49522414dde0dfc06fd076b509230f5a9f1cd50 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 05:19:56 +0000 Subject: [PATCH 29/62] Add return statement to guarding --- src/FastCast2/ActiveCastSerial.luau | 1 + 1 file changed, 1 insertion(+) diff --git a/src/FastCast2/ActiveCastSerial.luau b/src/FastCast2/ActiveCastSerial.luau index 50e87a5..f21eeae 100644 --- a/src/FastCast2/ActiveCastSerial.luau +++ b/src/FastCast2/ActiveCastSerial.luau @@ -67,6 +67,7 @@ local function DebrisAdd(obj: Instance, Lifetime: number) if not obj then return end if Lifetime <= 0 then obj:Destroy() + return end task.delay(Lifetime, function() obj:Destroy() From ad03254ce7a2b4fe5f725ba062776b308b4fee91 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 05:22:12 +0000 Subject: [PATCH 30/62] docs: Changed FastCast/.git to .git --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 15b8e89..9e2f237 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Read more on [FastCast2 devforum](https://devforum.roblox.com/t/fastcast2-an-imp ```bash git clone https://github.com/weenachuangkud/FastCast2.git cd FastCast2 - rm -rf FastCast2/.git + rm -rf .git ``` 3. Sync to Roblox: ```bash From 51225b153d31784c4a4b95951c18263a80c2f545 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 05:23:55 +0000 Subject: [PATCH 31/62] docs: Change speed to SPEED --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e2f237..10d3cc8 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ Caster:Init( ) -- Fire same as serial -Caster:RaycastFire(origin, direction, speed, behavior) +Caster:RaycastFire(origin, direction, SPEED, behavior) ```
From 5fb543f62ca94fe6c78ff93392b4450e2cc9c621 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 05:43:12 +0000 Subject: [PATCH 32/62] fix: use Copy instead of reference --- src/FastCast2/ActiveCast.luau | 2 +- src/FastCast2/ActiveCastSerial.luau | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/FastCast2/ActiveCast.luau b/src/FastCast2/ActiveCast.luau index ffb9133..0c20645 100644 --- a/src/FastCast2/ActiveCast.luau +++ b/src/FastCast2/ActiveCast.luau @@ -759,7 +759,7 @@ function ActiveCast.createCastData( }, RayInfo = { - Parameters = behavior.RaycastParams, + Parameters = behavior.RaycastParams and CloneCastParams(behavior.RaycastParams) or RaycastParams.new(), WorldRoot = workspace, MaxDistance = behavior.MaxDistance or DEFAULT_MAX_DISTANCE, CosmeticBulletObject = behavior.CosmeticBulletTemplate, diff --git a/src/FastCast2/ActiveCastSerial.luau b/src/FastCast2/ActiveCastSerial.luau index f21eeae..801698c 100644 --- a/src/FastCast2/ActiveCastSerial.luau +++ b/src/FastCast2/ActiveCastSerial.luau @@ -147,7 +147,7 @@ function ActiveCastSerial.new(caster: any, castData: any): any }, RayInfo = { - Parameters = castData.RaycastParams, + Parameters = castData.RaycastParams and CloneCastParams(castData.RaycastParams) or RaycastParams.new(), WorldRoot = workspace, MaxDistance = castData.MaxDistance or DEFAULT_MAX_DISTANCE, CosmeticBulletObject = castData.CosmeticBulletObject, From d4c957618d73e02ee0af586d8abcd22edaf71e87 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 05:55:12 +0000 Subject: [PATCH 33/62] fix: use castData.RayParams --- src/FastCast2/ActiveCast.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FastCast2/ActiveCast.luau b/src/FastCast2/ActiveCast.luau index 0c20645..ffb9133 100644 --- a/src/FastCast2/ActiveCast.luau +++ b/src/FastCast2/ActiveCast.luau @@ -759,7 +759,7 @@ function ActiveCast.createCastData( }, RayInfo = { - Parameters = behavior.RaycastParams and CloneCastParams(behavior.RaycastParams) or RaycastParams.new(), + Parameters = behavior.RaycastParams, WorldRoot = workspace, MaxDistance = behavior.MaxDistance or DEFAULT_MAX_DISTANCE, CosmeticBulletObject = behavior.CosmeticBulletTemplate, From b9eaf04229bd8dad7d0c1cda4019864df4d02542 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 06:00:24 +0000 Subject: [PATCH 34/62] resolve coderabbit: Validate acceleration before it is read for kinematics --- src/FastCast2/ActiveCast.luau | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/FastCast2/ActiveCast.luau b/src/FastCast2/ActiveCast.luau index ffb9133..bd0f2ca 100644 --- a/src/FastCast2/ActiveCast.luau +++ b/src/FastCast2/ActiveCast.luau @@ -366,6 +366,9 @@ local function SimulateCast( end local trajectory = cast.StateInfo.Trajectory + if typeof(trajectory.Acceleration) ~= "Vector3" then + trajectory.Acceleration = Vector3.new() + end local origin = trajectory.Origin local totalDelta = cast.StateInfo.TotalRuntime - trajectory.StartTime From 2d7b353b2ff23498ee28f3b605e1c7824215c7f5 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 06:13:51 +0000 Subject: [PATCH 35/62] fix: make BaseCastSerial state instance-local to prevent cross-instance corruption - Remove module-level variables (Output, ParentCaster, ObjectCache, BulkMoveToConnection, NextProjectileID) - Assign them as instance fields in Init() using self. prefix - Update all methods to use self. prefix for these fields --- src/FastCast2/BaseCastSerial.luau | 75 +++++++++++++++---------------- 1 file changed, 36 insertions(+), 39 deletions(-) diff --git a/src/FastCast2/BaseCastSerial.luau b/src/FastCast2/BaseCastSerial.luau index 7aaa529..4e6c0d8 100644 --- a/src/FastCast2/BaseCastSerial.luau +++ b/src/FastCast2/BaseCastSerial.luau @@ -29,20 +29,17 @@ local BaseCastSerial = {} BaseCastSerial.__index = BaseCastSerial BaseCastSerial.__type = "BaseCastSerial" -local BulkMoveToConnection: RBXScriptConnection? = nil -local Output: BindableEvent? = nil -local ObjectCache: BindableFunction? = nil -local NextProjectileID = 0 -local ParentCaster = nil - --[=[ @function Init @within BaseCastSerial ]=] -function BaseCastSerial.Init(BindableOutput: BindableEvent, Data: any, parentCaster: any) +function BaseCastSerial.Init(Bindableself.Output: BindableEvent, Data: any, parentCaster: any) local self = setmetatable({}, BaseCastSerial) - Output = BindableOutput - ParentCaster = parentCaster + self.self.Output = Bindableself.Output + self.self.ParentCaster = parentCaster + self.self.ObjectCache = nil + self.self.BulkMoveToConnection = nil + self.self.NextProjectileID = 0 if Data.useBulkMoveTo then -- BulkMoveTo is handled by SerialSimulation @@ -70,7 +67,7 @@ function BaseCastSerial:Raycast( Velocity: Vector3 | number, Behavior: TypeDef.FastCastBehavior ) - NextProjectileID += 1 + self.self.NextProjectileID += 1 if typeof(Velocity) == "number" then Velocity = Direction.Unit * Velocity @@ -91,7 +88,7 @@ function BaseCastSerial:Raycast( end local castData = { - ID = NextProjectileID, + ID = self.self.NextProjectileID, Origin = Origin, Velocity = Velocity, Acceleration = Behavior.Acceleration, @@ -106,11 +103,11 @@ function BaseCastSerial:Raycast( MovementMethod = Behavior.MovementMethod or "BulkMoveTo" } - local cast = ActiveCastSerial.new(ParentCaster, castData) + local cast = ActiveCastSerial.new(self.self.ParentCaster, castData) SerialSimulation.Register(cast) - if Output then - Output:Fire("CastFire", cast, Origin, Direction, Velocity, Behavior) + if self.self.Output then + self.self.Output:Fire("CastFire", cast, Origin, Direction, Velocity, Behavior) end end @@ -125,7 +122,7 @@ function BaseCastSerial:Blockcast( Velocity: Vector3 | number, Behavior: TypeDef.FastCastBehavior ) - NextProjectileID += 1 + self.NextProjectileID += 1 if typeof(Velocity) == "number" then Velocity = Direction.Unit * Velocity @@ -146,7 +143,7 @@ function BaseCastSerial:Blockcast( end local castData = { - ID = NextProjectileID, + ID = self.NextProjectileID, Origin = Origin, Velocity = Velocity, Acceleration = Behavior.Acceleration, @@ -162,11 +159,11 @@ function BaseCastSerial:Blockcast( MovementMethod = Behavior.MovementMethod or "BulkMoveTo" } - local cast = ActiveCastSerial.new(ParentCaster, castData) + local cast = ActiveCastSerial.new(self.ParentCaster, castData) SerialSimulation.Register(cast) - if Output then - Output:Fire("CastFire", cast, Origin, Direction, Velocity, Behavior) + if self.Output then + self.Output:Fire("CastFire", cast, Origin, Direction, Velocity, Behavior) end end @@ -181,7 +178,7 @@ function BaseCastSerial:Spherecast( Velocity: Vector3 | number, Behavior: TypeDef.FastCastBehavior ) - NextProjectileID += 1 + self.NextProjectileID += 1 if typeof(Velocity) == "number" then Velocity = Direction.Unit * Velocity @@ -202,7 +199,7 @@ function BaseCastSerial:Spherecast( end local castData = { - ID = NextProjectileID, + ID = self.NextProjectileID, Origin = Origin, Velocity = Velocity, Acceleration = Behavior.Acceleration, @@ -218,11 +215,11 @@ function BaseCastSerial:Spherecast( MovementMethod = Behavior.MovementMethod or "BulkMoveTo" } - local cast = ActiveCastSerial.new(ParentCaster, castData) + local cast = ActiveCastSerial.new(self.ParentCaster, castData) SerialSimulation.Register(cast) - if Output then - Output:Fire("CastFire", cast, Origin, Direction, Velocity, Behavior) + if self.Output then + self.Output:Fire("CastFire", cast, Origin, Direction, Velocity, Behavior) end end @@ -235,18 +232,18 @@ function BaseCastSerial:BindBulkMoveTo(enabled: boolean) end --[=[ - @method BindObjectCache + @method Bindself.ObjectCache @within BaseCastSerial ]=] -function BaseCastSerial:BindObjectCache(bool: boolean) +function BaseCastSerial:Bindself.ObjectCache(bool: boolean) if bool then - if ObjectCache then return end - ObjectCache = Instance.new("BindableFunction") - ObjectCache.Name = "ObjectCache" + if self.ObjectCache then return end + self.ObjectCache = Instance.new("BindableFunction") + self.ObjectCache.Name = "self.ObjectCache" else - if ObjectCache then - ObjectCache:Destroy() - ObjectCache = nil + if self.ObjectCache then + self.ObjectCache:Destroy() + self.ObjectCache = nil end end end @@ -262,8 +259,8 @@ function BaseCastSerial:TerminateCast(cast: any, castTerminatingFunction: TypeDe if castTerminatingFunction then castTerminatingFunction(cast) end - if Output then - Output:Fire("CastTerminating", cast) + if self.Output then + self.Output:Fire("CastTerminating", cast) end end @@ -272,13 +269,13 @@ end @within BaseCastSerial ]=] function BaseCastSerial:Destroy() - if BulkMoveToConnection then - BulkMoveToConnection:Disconnect() - BulkMoveToConnection = nil + if self.BulkMoveToConnection then + self.BulkMoveToConnection:Disconnect() + self.BulkMoveToConnection = nil end - Output = nil - ParentCaster = nil + self.Output = nil + self.ParentCaster = nil setmetatable(self, nil) end From ca39f6a78380cc78b4343c5b719ef005f2bf9f48 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 06:19:55 +0000 Subject: [PATCH 36/62] chore: remove unused constants from ActiveCastSerial.luau --- src/FastCast2/ActiveCastSerial.luau | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/FastCast2/ActiveCastSerial.luau b/src/FastCast2/ActiveCastSerial.luau index 801698c..cb61f62 100644 --- a/src/FastCast2/ActiveCastSerial.luau +++ b/src/FastCast2/ActiveCastSerial.luau @@ -120,10 +120,6 @@ local function CloneCastParams(params: RaycastParams): RaycastParams return clone end -local EPSILON = 1e-6 -local RAY_SEARCH_OFFSET = 0.001 -local FIXED_DELTA_TIME = 1 / 240 - function ActiveCastSerial.new(caster: any, castData: any): any return { Caster = caster, From 9a834ae260ad91c346c481b2e0a78c8d8295d894 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 07:59:01 +0000 Subject: [PATCH 37/62] fix: add missing FastCastEnums require in serial code example --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 10d3cc8..b689dbc 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ local Players = game:GetService("Players") -- Requires local FastCast2 = require(Rep:WaitForChild("FastCast2")) +local FastCastEnums = require(Rep:WaitForChild("FastCast2"):WaitForChild("FastCastEnums")) -- CONSTANTS local SPEED = 500 From 7e684d2e446ec85a123bf3a0cf324f9bba310e57 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 07:59:04 +0000 Subject: [PATCH 38/62] fix: replace undefined latestTrajectory with trajectory in SimulateCast --- src/FastCast2/ActiveCast.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FastCast2/ActiveCast.luau b/src/FastCast2/ActiveCast.luau index bd0f2ca..b91ca8b 100644 --- a/src/FastCast2/ActiveCast.luau +++ b/src/FastCast2/ActiveCast.luau @@ -381,7 +381,7 @@ local function SimulateCast( cast.StateInfo.TotalRuntime += delta - totalDelta = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime + totalDelta = cast.StateInfo.TotalRuntime - trajectory.StartTime local currentTarget = GetPositionAtTime(totalDelta, origin, initialVelocity, acceleration) local segmentVelocity = GetVelocityAtTime(totalDelta, initialVelocity, acceleration) From c0003b2601a13b7f3fb842c609bd7df4c0760ea5 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 07:59:06 +0000 Subject: [PATCH 39/62] fix: correct Init params, remove self.self.* patterns, rename BindObjectCache method --- src/FastCast2/BaseCastSerial.luau | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/FastCast2/BaseCastSerial.luau b/src/FastCast2/BaseCastSerial.luau index 4e6c0d8..73d440a 100644 --- a/src/FastCast2/BaseCastSerial.luau +++ b/src/FastCast2/BaseCastSerial.luau @@ -33,13 +33,13 @@ BaseCastSerial.__type = "BaseCastSerial" @function Init @within BaseCastSerial ]=] -function BaseCastSerial.Init(Bindableself.Output: BindableEvent, Data: any, parentCaster: any) +function BaseCastSerial.Init(BindableOutput: BindableEvent, Data: any, parentCaster: any) local self = setmetatable({}, BaseCastSerial) - self.self.Output = Bindableself.Output - self.self.ParentCaster = parentCaster - self.self.ObjectCache = nil - self.self.BulkMoveToConnection = nil - self.self.NextProjectileID = 0 + self.Output = BindableOutput + self.ParentCaster = parentCaster + self.ObjectCache = nil + self.BulkMoveToConnection = nil + self.NextProjectileID = 0 if Data.useBulkMoveTo then -- BulkMoveTo is handled by SerialSimulation @@ -67,7 +67,7 @@ function BaseCastSerial:Raycast( Velocity: Vector3 | number, Behavior: TypeDef.FastCastBehavior ) - self.self.NextProjectileID += 1 + self.NextProjectileID += 1 if typeof(Velocity) == "number" then Velocity = Direction.Unit * Velocity @@ -88,7 +88,7 @@ function BaseCastSerial:Raycast( end local castData = { - ID = self.self.NextProjectileID, + ID = self.NextProjectileID, Origin = Origin, Velocity = Velocity, Acceleration = Behavior.Acceleration, @@ -232,14 +232,14 @@ function BaseCastSerial:BindBulkMoveTo(enabled: boolean) end --[=[ - @method Bindself.ObjectCache + @method BindObjectCache @within BaseCastSerial ]=] -function BaseCastSerial:Bindself.ObjectCache(bool: boolean) +function BaseCastSerial:BindObjectCache(bool: boolean) if bool then if self.ObjectCache then return end self.ObjectCache = Instance.new("BindableFunction") - self.ObjectCache.Name = "self.ObjectCache" + self.ObjectCache.Name = "ObjectCache" else if self.ObjectCache then self.ObjectCache:Destroy() From 16fa2cc2d81b5f8d55e0c1251228966db09e0aca Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 07:59:08 +0000 Subject: [PATCH 40/62] fix: check ParallelSimulation.StepConnection instead of SerialSimulation in Start() --- src/FastCast2/ParallelSimulation.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FastCast2/ParallelSimulation.luau b/src/FastCast2/ParallelSimulation.luau index a82ba06..86d9400 100644 --- a/src/FastCast2/ParallelSimulation.luau +++ b/src/FastCast2/ParallelSimulation.luau @@ -320,7 +320,7 @@ local function UpdateCasts(deltaTime: number) end function ParallelSimulation.Start() - if SerialSimulation.StepConnection then return end + if ParallelSimulation.StepConnection then return end if RS:IsClient() then ParallelSimulation.StepConnection = RS.PreRender:ConnectParallel(UpdateCasts) else From 58c9792c3e4db8aa95cc028e8066e3b5243ec42d Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 07:59:11 +0000 Subject: [PATCH 41/62] fix: forward BindableEvent to Signals, fix GetVelocityCast, rebase trajectory setters, update ModifyTransformation --- src/FastCast2/init.luau | 84 ++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 47 deletions(-) diff --git a/src/FastCast2/init.luau b/src/FastCast2/init.luau index 918cc14..e4abe46 100644 --- a/src/FastCast2/init.luau +++ b/src/FastCast2/init.luau @@ -129,10 +129,10 @@ local function GetTrajectoryInfo( cast: vaildcast, index: number ): { [number]: Vector3 } - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") - local trajectories = cast.StateInfo.Trajectory - local trajectory = trajectories[index] - local duration = trajectory.EndTime - trajectory.StartTime + local trajectory = cast.StateInfo.Trajectory + local duration = trajectory.EndTime ~= -1 + and (trajectory.EndTime - trajectory.StartTime) + or (cast.StateInfo.TotalRuntime - trajectory.StartTime) local origin = trajectory.Origin local vel = trajectory.InitialVelocity @@ -153,43 +153,15 @@ local function ModifyTransformation( ) local trajectory = cast.StateInfo.Trajectory - if trajectory.StartTime == cast.StateInfo.TotalRuntime then - if velocity == nil then - velocity = trajectory.InitialVelocity - end - if acceleration == nil then - acceleration = trajectory.Acceleration - end - if position == nil then - position = lastTrajectory.Origin - end - - lastTrajectory.Origin = position - lastTrajectory.InitialVelocity = velocity - lastTrajectory.Acceleration = acceleration - else - lastTrajectory.EndTime = cast.StateInfo.TotalRuntime - - local point, velAtPoint = unpack(GetLatestTrajectoryEndInfo(cast)) + local t = cast.StateInfo.TotalRuntime - trajectory.StartTime + local currentPosition = GetPositionAtTime(t, trajectory.Origin, trajectory.InitialVelocity, trajectory.Acceleration) + local currentVelocity = GetVelocityAtTime(t, trajectory.InitialVelocity, trajectory.Acceleration) - if velocity == nil then - velocity = velAtPoint - end - if acceleration == nil then - acceleration = lastTrajectory.Acceleration - end - if position == nil then - position = point - end - table.insert(cast.StateInfo.Trajectory, { - StartTime = cast.StateInfo.TotalRuntime, - EndTime = -1, - Origin = position, - InitialVelocity = velocity, - Acceleration = acceleration, - }) - cast.StateInfo.CancelHighResCast = true - end + trajectory.Origin = position or currentPosition + trajectory.InitialVelocity = velocity or currentVelocity + trajectory.Acceleration = acceleration or trajectory.Acceleration + trajectory.StartTime = cast.StateInfo.TotalRuntime + cast.StateInfo.CancelHighResCast = true end local function deepCopyTable(tbl: {any}): {any} @@ -732,6 +704,13 @@ function FastCastSerial:Init( self.Output = BindableOutput + BindableOutput.Event:Connect(function(eventName: string, ...) + local signal = self[eventName] + if signal and type(signal) == "table" and type(signal.Fire) == "function" then + signal:Fire(...) + end + end) + if useObjectCache then if not CacheSize then CacheSize = DEFAULT_CACHE_SIZE @@ -815,7 +794,11 @@ end ]=] function FastCastSerial:GetVelocityCast(cast: vaildcast): Vector3 local latestTrajectory = cast.StateInfo.Trajectory - return latestTrajectory.InitialVelocity + return GetVelocityAtTime( + cast.StateInfo.TotalRuntime - latestTrajectory.StartTime, + latestTrajectory.InitialVelocity, + latestTrajectory.Acceleration + ) end --[=[ @@ -847,6 +830,9 @@ end ]=] function FastCastSerial:SetVelocityCast(cast: vaildcast, velocity: Vector3) local latestTrajectory = cast.StateInfo.Trajectory + local t = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime + latestTrajectory.Origin = GetPositionAtTime(t, latestTrajectory.Origin, latestTrajectory.InitialVelocity, latestTrajectory.Acceleration) + latestTrajectory.StartTime = cast.StateInfo.TotalRuntime latestTrajectory.InitialVelocity = velocity end @@ -856,6 +842,10 @@ end ]=] function FastCastSerial:SetAccelerationCast(cast: vaildcast, acceleration: Vector3) local latestTrajectory = cast.StateInfo.Trajectory + local t = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime + latestTrajectory.Origin = GetPositionAtTime(t, latestTrajectory.Origin, latestTrajectory.InitialVelocity, latestTrajectory.Acceleration) + latestTrajectory.InitialVelocity = GetVelocityAtTime(t, latestTrajectory.InitialVelocity, latestTrajectory.Acceleration) + latestTrajectory.StartTime = cast.StateInfo.TotalRuntime latestTrajectory.Acceleration = acceleration end @@ -865,6 +855,9 @@ end ]=] function FastCastSerial:SetPositionCast(cast: vaildcast, position: Vector3) local latestTrajectory = cast.StateInfo.Trajectory + local t = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime + latestTrajectory.InitialVelocity = GetVelocityAtTime(t, latestTrajectory.InitialVelocity, latestTrajectory.Acceleration) + latestTrajectory.StartTime = cast.StateInfo.TotalRuntime latestTrajectory.Origin = position end @@ -881,8 +874,7 @@ end @within FastCastSerial ]=] function FastCastSerial:AddPositionCast(cast: vaildcast, position: Vector3) - local latestTrajectory = cast.StateInfo.Trajectory - latestTrajectory.Origin += position + self:SetPositionCast(cast, self:GetPositionCast(cast) + position) end --[=[ @@ -890,8 +882,7 @@ end @within FastCastSerial ]=] function FastCastSerial:AddVelocityCast(cast: vaildcast, velocity: Vector3) - local latestTrajectory = cast.StateInfo.Trajectory - latestTrajectory.InitialVelocity += velocity + self:SetVelocityCast(cast, self:GetVelocityCast(cast) + velocity) end --[=[ @@ -899,8 +890,7 @@ end @within FastCastSerial ]=] function FastCastSerial:AddAccelerationCast(cast: vaildcast, acceleration: Vector3) - local latestTrajectory = cast.StateInfo.Trajectory - latestTrajectory.Acceleration += acceleration + self:SetAccelerationCast(cast, self:GetAccelerationCast(cast) + acceleration) end --[=[ From cb68225e6d8e231634fc0097d218f860bf34254c Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 09:12:25 +0000 Subject: [PATCH 42/62] add: architecture.md --- skills/architecture.md | 402 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 402 insertions(+) create mode 100644 skills/architecture.md diff --git a/skills/architecture.md b/skills/architecture.md new file mode 100644 index 0000000..18855e1 --- /dev/null +++ b/skills/architecture.md @@ -0,0 +1,402 @@ +# FastCast2 Architecture + +## Overview + +FastCast2 is a high-performance raycast library for Roblox with two execution modes: +- **Parallel**: Multi-threaded via Actor VMs, using SoA (Structure of Arrays) pattern +- **Serial**: Single-threaded with SoA pattern (simpler, lower performance) + +## Module Structure + +``` +FastCast2/ +├── init.luau # Entry point, creates casters +├── BaseCast.luau # Parallel mode cast handler +├── BaseCastSerial.luau # Serial mode cast handler +├── ParallelSimulation.luau # Parallel SoA simulation (one per Actor) +├── SerialSimulation.luau # Serial SoA simulation (single instance) +├── ActiveCast.luau # Cast data container (AoS pattern) +├── ActiveCastSerial.luau # Serial cast data +├── Motor6DPool.luau # Motor6D object pooling +├── ObjectCache.luau # Cosmetic bullet object pooling +├── Signal.luau # Event signal system +├── FastCastEnums.luau # Enum definitions +├── TypeDefinitions.luau # TypeScript-style type definitions +├── Configs.luau # Configuration +├── DefaultConfigs.luau # Default behavior config +└── FastCastVMs/ + ├── init.luau # Dispatcher (manages Actors) + ├── ServerVM.server.luau # Server Actor script + └── ClientVM.client.luau # Client Actor script +``` + +## Execution Modes + +### Parallel Mode (`FastCast.newParallel()`) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ FastCastParallel │ +│ (init.luau) │ +│ │ │ +│ ┌──────────────┴──────────────┐ │ +│ │ Dispatcher │ │ +│ │ (FastCastVMs) │ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Actor │ │ Actor │ │ Actor │ ... │ +│ │ (VM #1) │ │ (VM #2) │ │ (VM #3) │ │ +│ │ │ │ │ │ │ │ +│ │BaseCast │ │BaseCast │ │BaseCast │ │ +│ │ │ │ │ │ │ │ │ │ │ +│ │ ▼ │ │ ▼ │ │ ▼ │ │ +│ │Parallel │ │Parallel │ │Parallel │ │ +│ │ Sim │ │ Sim │ │ Sim │ │ +│ │ (SoA) │ │ (SoA) │ │ (SoA) │ │ +│ └─────────┘ └─────────┘ └─────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**How Parallel Mode Works:** + +1. **Dispatcher** (`FastCastVMs/init.luau`) creates N Actor VMs +2. Each **Actor** runs its own **BaseCast + ParallelSimulation** +3. When `RaycastFire()` is called: + - Dispatcher selects Actor with lowest `Tasks` attribute (load balancing) + - Sends `Raycast` message to that Actor +4. Each **ParallelSimulation** instance: + - Uses `PreRender:ConnectParallel` (client) or `Heartbeat` (server) + - Stores casts in SoA arrays (one set per Actor) + - Runs parallel physics calculations + - Uses **event queue** for cross-thread communication + +### Serial Mode (`FastCast.new()`) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ FastCastSerial │ +│ (init.luau) │ +│ │ │ +│ ▼ │ +│ BaseCastSerial │ +│ │ │ +│ ▼ │ +│ SerialSimulation │ +│ (single instance) │ +│ (SoA arrays) │ +└─────────────────────────────────────────────────────────────┘ +``` + +**How Serial Mode Works:** + +1. Single **BaseCastSerial** handles all casts +2. **SerialSimulation** runs on `Heartbeat` (single thread) +3. All casts stored in single SoA array set +4. Event queue dispatches callbacks after simulation + +## SoA (Structure of Arrays) Pattern + +Instead of storing casts as individual objects: +```lua +-- Bad: Array of Structures (AoS) +casts = { {id=1, origin=..., velocity=...}, {id=2, origin=..., velocity=...} } +``` + +FastCast2 uses Structure of Arrays: +```lua +-- Good: Structure of Arrays (SoA) +castIDs = {1, 2, 3, ...} +castOrigin = {Vector3, Vector3, Vector3, ...} +castVelocity = {Vector3, Vector3, Vector3, ...} +castAcceleration = {Vector3, Vector3, Vector3, ...} +``` + +**Benefits:** +- Better cache locality (all velocities adjacent in memory) +- Single iteration updates all casts +- Reduced heap allocations + +## Threading Model + +### Parallel Mode Threading + +Each Actor VM runs in **separate Lua environment** with its own: + +``` +┌─────────────────────────────────────────────────────────┐ +│ Actor (per VM) │ +│ │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ ParallelSimulation │ │ +│ │ │ │ +│ │ RunService (PreRender:ConnectParallel) │ │ +│ │ │ │ │ +│ │ ├── Parallel math/raycast calculations │ │ +│ │ │ (task.defer/disconnect allowed) │ │ +│ │ │ │ │ +│ │ └── task.synchronize() │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ ┌──────────────────────────────────────┐ │ │ +│ │ │ BulkMoveTo / Motor6D updates │ │ │ +│ │ │ (task.sync / BindableEvent fire) │ │ │ +│ │ └──────────────────────────────────────┘ │ │ +│ └─────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +**Key Points:** + +1. **`task.synchronize()`** - Called after parallel calculations + - Forces all parallel tasks to complete + - Required before modifying shared state + +2. **`task.defer()` / disconnect** - Allowed in parallel phase + - Used for cleanup operations + +3. **`task.sync()` / BindableEvent** - Used for sync phase + - Queues callbacks to run after synchronization + - Events fire on main thread + +### Event Queue System + +Both simulations use an event queue to batch callbacks: + +```lua +local QueuedEvents = {} + +local function QueueFire(caster, eventName, ...) + if caster and caster.Output then + caster.Output:Fire(eventName, ...) + end +end + +-- In simulation loop (parallel or serial): +QueueFire(caster, "LengthChanged", cast, pos, dir, displacement, vel, bullet) +QueueFire(caster, "Hit", cast, result, vel, bullet) + +-- After simulation, dispatch all at once: +for _, event in QueuedEvents do + event.Callback(unpack(event.Args)) +end +table.clear(QueuedEvents) +``` + +## Module Descriptions + +### `init.luau` - Entry Point +- Creates `FastCast` table with two modes +- `FastCast.new()` - Returns Serial caster +- `FastCast.newParallel()` - Returns Parallel caster +- Handles Signal creation (LengthChanged, Hit, Pierced, etc.) + +### `BaseCast.luau` - Parallel Cast Handler +- Runs inside each Actor VM +- Handles Raycast/Blockcast/Spherecast methods +- Manages BulkMoveTo connection (`PreRender:ConnectParallel`) +- Uses `ParallelSimulation.Register()` to add casts +- Syncs changes via `BindableEvent` + +### `BaseCastSerial.luau` - Serial Cast Handler +- Single-threaded cast handler +- Registers casts with `SerialSimulation` +- Simpler, no Actor overhead + +### `ParallelSimulation.luau` - Parallel Physics Engine +- **One instance per Actor VM** +- Auto-starts with `PreRender:ConnectParallel` (client) or `Heartbeat` (server) +- SoA arrays for all cast data +- Motor6D/BulkMoveTo handled in sync phase +- Event queue for cross-thread communication + +### `SerialSimulation.luau` - Serial Physics Engine +- **Single global instance** +- Runs on `Heartbeat` +- SoA arrays (same structure as ParallelSimulation) +- Simpler threading model + +### `ActiveCast.luau` - Cast Data Container +- AoS (Array of Structures) for cast metadata +- Contains: + - `StateInfo`: trajectory, timing, high-fidelity settings + - `RayInfo`: raycast params, world root, max distance + - `UserData`: user-defined data + +### `Motor6DPool.luau` - Transform Mode Support +- Object pool for Motor6D instances +- Efficient for moving cosmetic bullets via `Transform` property +- Grows dynamically (2x growth rate) +- Used when `MovementMethod == "Transform"` + +### `ObjectCache.luau` - Cosmetic Bullet Pooling +- Pool of reusable cosmetic bullet parts +- Reduces Clone() overhead +- `GetPart(cframe)` and `ReturnPart(part)` interface + +### `Signal.luau` - Event System +- Custom signal implementation +- Supports Connect, Once, Wait, Fire +- Uses thread pooling for performance +- Threaded signal firing via `task.spawn` + +### `FastCastVMs/init.luau` - Dispatcher +- Manages Actor VM pool +- Load balancing via `Tasks` attribute +- `Dispatch()` - Sends to least-loaded Actor +- `DispatchAll()` - Broadcasts to all Actors + +### `FastCastVMs/ServerVM.server.luau` - Server Actor +- Handles messages from Dispatcher +- Initializes BaseCast on `Init` message +- Processes Raycast/Blockcast/Spherecast + +## How Connections Work + +### Cast Flow (Parallel Mode) + +``` +User calls caster:RaycastFire(origin, direction, velocity, behavior) + │ + ▼ +FastCastParallel:RaycastFire() [init.luau] + │ + ▼ +Dispatcher:Dispatch("Raycast", ...) + │ + ▼ +Dispatcher selects Actor with lowest Tasks + │ + ▼ +Actor receives "Raycast" message + │ + ▼ +BaseCast:Raycast() [BaseCast.luau] + │ + ├── Creates ActiveCast data + │ + ▼ +ParallelSimulation.Register(cast) + │ + └── Stores in Actor-local SoA arrays + │ + ▼ +ParallelSimulation.UpdateCasts() [PreRender:ConnectParallel] + │ + ├── For each cast (parallel): + │ ├── Calculate position/velocity + │ ├── Raycast physics + │ └── Update cosmetic bullet + │ + ├── task.synchronize() + │ + └── Fire events via queue + │ + ▼ +Event callbacks fire (LengthChanged, Hit, etc.) +``` + +### BulkMoveTo Connection (Parallel) + +```lua +-- In BaseCast.luau: +BulkMoveToConnection = RS.PreRender:ConnectParallel(HandleBulkMoveTo) + +function HandleBulkMoveTo() + -- Collect all CFrame updates from SoA arrays + for _, ActiveCasts in Actives do + table.insert(Parts, ActiveCasts.RayInfo.CosmeticBulletObject) + table.insert(CFrames, ActiveCasts.CFrame) + end + + task.synchronize() -- Wait for parallel calcs + + workspace:BulkMoveTo(Parts, CFrames, Enum.BulkMoveMode.FireCFrameChanged) +end +``` + +### Motor6D Transform Mode + +```lua +-- In ParallelSimulation.Register(): +if cast.RayInfo.MovementMethod == "Transform" then + castMotor6D[id] = Motor6DPool.Connect(id, cosmeticBullet) +end + +-- In UpdateCasts(): +if motor6d then + motor6d.Transform = newCFrame -- Efficient, no physics sync needed +end +``` + +## Key Design Patterns + +### 1. SoA Arrays +```lua +-- ParallelSimulation.luau lines 58-83 +local castCount = 0 +local casts = {} +local castIDs = {} +local castOrigin = {} +local castVelocity = {} +local castAcceleration = {} +-- ... all arrays indexed by cast ID +``` + +### 2. Event Queue (Sync Phase) +```lua +-- Events queued during parallel phase, dispatched after sync +QueueFire(caster, "Hit", cast, result, vel, bullet) +-- ... later: +DispatchAllEvents() +``` + +### 3. Load Balancing +```lua +-- Dispatcher:Dispatch() sorts by Tasks attribute +table.sort(Threads, function(a, b) + return a:GetAttribute("Tasks") < b:GetAttribute("Tasks") +end) +Threads[1]:SendMessage("Raycast", ...) +``` + +### 4. Motor6D Pooling +```lua +-- Efficient Transform mode without per-bullet physics +local motor6d = Motor6DPool.Connect(castID, part) +motor6d.Transform = newCFrame -- Set without parenting complexity +``` + +## Configuration + +### FastCastBehavior Properties + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `RaycastParams` | RaycastParams | nil | Collision filtering | +| `MaxDistance` | number | 1000 | Max cast distance | +| `Acceleration` | Vector3 | (0,0,0) | Gravity effect | +| `HighFidelityBehavior` | number | 1 | Hit verification mode | +| `HighFidelitySegmentSize` | number | 0.1 | Sub-cast segment size | +| `CosmeticBulletTemplate` | Instance | nil | Visual bullet part | +| `CosmeticBulletContainer` | Instance | nil | Parent for bullets | +| `MovementMethod` | string | "BulkMoveTo" | "BulkMoveTo" or "Transform" | +| `VisualizeCasts` | boolean | false | Show debug rays | + +## Performance Considerations + +1. **SoA vs AoS**: SoA provides ~2-3x better cache performance +2. **BulkMoveTo**: Batches part updates efficiently +3. **Motor6D Pool**: Avoids CreateInstance overhead +4. **Event Queue**: Reduces cross-thread communication +5. **Parallel Simulation**: Scales with Actor count +6. **Load Balancing**: Prevents Actor overload + +## Summary + +FastCast2 uses modern game engine techniques adapted for Roblox: +- **Multi-threading via Actors** +- **SoA data layout for cache efficiency** +- **Event queue for thread-safe communication** +- **Object pooling for memory efficiency** +- **Bulk operations for reduced overhead** From 4b8c63f00c8829b78616343f78d1def0310220ac Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 16:17:42 +0700 Subject: [PATCH 43/62] Update architecture.md --- skills/architecture.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/skills/architecture.md b/skills/architecture.md index 18855e1..c2d26d2 100644 --- a/skills/architecture.md +++ b/skills/architecture.md @@ -36,25 +36,25 @@ FastCast2/ ``` ┌─────────────────────────────────────────────────────────────┐ -│ FastCastParallel │ -│ (init.luau) │ -│ │ │ -│ ┌──────────────┴──────────────┐ │ -│ │ Dispatcher │ │ -│ │ (FastCastVMs) │ │ -│ │ │ │ -│ ▼ ▼ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ FastCastParallel +│ (init.luau) +│ │ +│ ┌──────────────┴──────────────┐ +│ │ Dispatcher │ +│ │ (FastCastVMs) │ +│ │ │ +│ ▼ ▼ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ Actor │ │ Actor │ │ Actor │ ... │ │ │ (VM #1) │ │ (VM #2) │ │ (VM #3) │ │ │ │ │ │ │ │ │ │ │ │BaseCast │ │BaseCast │ │BaseCast │ │ -│ │ │ │ │ │ │ │ │ │ │ -│ │ ▼ │ │ ▼ │ │ ▼ │ │ +│ │ │ │ │ │ │ │ │ | +│ │ ▼ │ │ ▼ │ │ ▼ │ │ │ │Parallel │ │Parallel │ │Parallel │ │ │ │ Sim │ │ Sim │ │ Sim │ │ │ │ (SoA) │ │ (SoA) │ │ (SoA) │ │ -│ └─────────┘ └─────────┘ └─────────┘ │ +│ └─────────┘ └─────────┘ └─────────┘ └─────────────────────────────────────────────────────────────┘ ``` From 1b6d8fa04e29dc4521bd677a5cd5560e5aa9b46f Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 16:23:33 +0700 Subject: [PATCH 44/62] Update architecture.md --- skills/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/architecture.md b/skills/architecture.md index c2d26d2..8d67916 100644 --- a/skills/architecture.md +++ b/skills/architecture.md @@ -246,7 +246,7 @@ table.clear(QueuedEvents) - `Dispatch()` - Sends to least-loaded Actor - `DispatchAll()` - Broadcasts to all Actors -### `FastCastVMs/ServerVM.server.luau` - Server Actor +### `FastCastVMs/ServerVM.server.luau` and `FastCastVMs/ClientVM.client.luau` - Actors - Handles messages from Dispatcher - Initializes BaseCast on `Init` message - Processes Raycast/Blockcast/Spherecast From bd45c111c22ca59c190c9b0aba08d52b007d4caf Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 09:39:51 +0000 Subject: [PATCH 45/62] fix: implement HighFidelitySegmentSize and HighFidelityBehavior in SoA simulations - Add HighFidelityBehavior.Automatic subcast verification in UpdateCasts - When ray displacement exceeds segment size, perform subcast re-validation - Update bullet position to accurate hit point after subcast verification - Applied to both ParallelSimulation and SerialSimulation (SoA pattern) --- src/FastCast2/ParallelSimulation.luau | 77 +++++++++++++++++++++++++-- src/FastCast2/SerialSimulation.luau | 77 +++++++++++++++++++++++++-- 2 files changed, 148 insertions(+), 6 deletions(-) diff --git a/src/FastCast2/ParallelSimulation.luau b/src/FastCast2/ParallelSimulation.luau index 86d9400..fbabfc4 100644 --- a/src/FastCast2/ParallelSimulation.luau +++ b/src/FastCast2/ParallelSimulation.luau @@ -17,6 +17,7 @@ local FastCastEnums = require(FastCastModule:WaitForChild("FastCastEnums")) local Motor6DPool = require(FastCastModule:WaitForChild("Motor6DPool")) local EnumCastTypes = FastCastEnums.CastType +local HighFidelityBehavior = FastCastEnums.HighFidelityBehavior local HIGH_FIDE_INCREASE_SIZE = 0.5 local MAX_SEGMENT_CAL_TIME = 0.016 * 5 local MAX_CASTING_TIME = 0.2 @@ -245,6 +246,8 @@ local function UpdateCasts(deltaTime: number) local caster = castCaster[i] local castType = castCastType[i] local CastHandler = castHandlers[castType] + local highFidelityBehavior = castHighFidelityBehavior[i] + local highFidelitySegmentSize = castHighFidelitySegmentSize[i] local origin = castOrigin[i] local totalDelta = castTotalRuntime[i] @@ -272,9 +275,11 @@ local function UpdateCasts(deltaTime: number) local hitPoint = currentPosition local hitPart = nil + local hitNormal = Vector3.new() if result then hitPoint = result.Position hitPart = result.Instance + hitNormal = result.Normal end local rayDisplacement = (hitPoint - lastPosition).Magnitude @@ -299,10 +304,76 @@ local function UpdateCasts(deltaTime: number) -- Fire LengthChanged QueueFire(caster, "LengthChanged", casts[i], lastPosition, rayDir.Unit, rayDisplacement, currentVelocity, bullet) - -- Handle hit + -- Handle hit with HighFidelity if result and hitPart ~= bullet then - QueueFire(caster, "Hit", casts[i], result, currentVelocity, bullet) - table.insert(destroyedIds, i) + if + highFidelityBehavior == HighFidelityBehavior.Automatic + and highFidelitySegmentSize > 0 + and rayDisplacement > highFidelitySegmentSize + then + castIsActivelyResimulating[i] = true + castCancelHighResCast[i] = false + + local numSegmentsDecimal = rayDisplacement / highFidelitySegmentSize + local numSegmentsReal = math.floor(numSegmentsDecimal) + + if numSegmentsReal == 0 then + numSegmentsReal = 1 + end + + local timeIncrement = deltaTime / numSegmentsReal + + local subLastPoint = lastPosition + for segmentIndex = 1, numSegmentsReal do + if castCancelHighResCast[i] then + castCancelHighResCast[i] = false + break + end + + local subTime = totalDelta - deltaTime + (timeIncrement * segmentIndex) + local subPosition = GetPositionAtTime(subTime, origin, velocity, acceleration) + local subVelocity = GetVelocityAtTime(subTime, velocity, acceleration) + local subRayDir = subVelocity * timeIncrement + + local subResult = CastHandler(castWorldRoot[i], subLastPoint, subRayDir, castRaycastParams[i], variant) + + if subResult then + local subDisplacement = (subLastPoint - subResult.Position).Magnitude + local subHitVelocity = GetVelocityAtTime(subTime, velocity, acceleration) + + castIsActivelyResimulating[i] = false + + castTotalRuntime[i] = totalDelta + castDistanceCovered[i] += subDisplacement + + local subCFrame = CFrame.new(subLastPoint, subLastPoint + subRayDir) * CFrame.new(0, 0, -subDisplacement / 2) + castCFrame[i] = subCFrame + + if bullet then + if motor6d then + motor6d.Transform = subCFrame + elseif bullet:IsA("BasePart") then + bullet.CFrame = subCFrame + else + bullet:PivotTo(subCFrame) + end + end + + QueueFire(caster, "Hit", casts[i], subResult, subHitVelocity, bullet) + table.insert(destroyedIds, i) + break + end + + subLastPoint = subPosition + end + + if castIsActivelyResimulating[i] then + castIsActivelyResimulating[i] = false + end + else + QueueFire(caster, "Hit", casts[i], result, currentVelocity, bullet) + table.insert(destroyedIds, i) + end end -- Check max distance diff --git a/src/FastCast2/SerialSimulation.luau b/src/FastCast2/SerialSimulation.luau index 0161bd8..e8d8156 100644 --- a/src/FastCast2/SerialSimulation.luau +++ b/src/FastCast2/SerialSimulation.luau @@ -19,6 +19,7 @@ local ActiveCastSerial = require(FastCastModule:WaitForChild("ActiveCastSerial") local Motor6DPool = require(FastCastModule:WaitForChild("Motor6DPool")) local EnumCastTypes = FastCastEnums.CastType +local HighFidelityBehavior = FastCastEnums.HighFidelityBehavior local HIGH_FIDE_INCREASE_SIZE = 0.5 local MAX_SEGMENT_CAL_TIME = 0.016 * 5 local MAX_CASTING_TIME = 0.2 @@ -248,6 +249,8 @@ local function UpdateCasts(deltaTime: number) local caster = castCaster[i] local castType = castCastType[i] local CastHandler = castHandlers[castType] + local highFidelityBehavior = castHighFidelityBehavior[i] + local highFidelitySegmentSize = castHighFidelitySegmentSize[i] local origin = castOrigin[i] local totalDelta = castTotalRuntime[i] @@ -275,9 +278,11 @@ local function UpdateCasts(deltaTime: number) local hitPoint = currentPosition local hitPart = nil + local hitNormal = Vector3.new() if result then hitPoint = result.Position hitPart = result.Instance + hitNormal = result.Normal end local rayDisplacement = (hitPoint - lastPosition).Magnitude @@ -302,10 +307,76 @@ local function UpdateCasts(deltaTime: number) -- Fire LengthChanged QueueFire(caster, "LengthChanged", casts[i], lastPosition, rayDir.Unit, rayDisplacement, currentVelocity, bullet) - -- Handle hit + -- Handle hit with HighFidelity if result and hitPart ~= bullet then - QueueFire(caster, "Hit", casts[i], result, currentVelocity, bullet) - table.insert(destroyedIds, i) + if + highFidelityBehavior == HighFidelityBehavior.Automatic + and highFidelitySegmentSize > 0 + and rayDisplacement > highFidelitySegmentSize + then + castIsActivelyResimulating[i] = true + castCancelHighResCast[i] = false + + local numSegmentsDecimal = rayDisplacement / highFidelitySegmentSize + local numSegmentsReal = math.floor(numSegmentsDecimal) + + if numSegmentsReal == 0 then + numSegmentsReal = 1 + end + + local timeIncrement = deltaTime / numSegmentsReal + + local subLastPoint = lastPosition + for segmentIndex = 1, numSegmentsReal do + if castCancelHighResCast[i] then + castCancelHighResCast[i] = false + break + end + + local subTime = totalDelta - deltaTime + (timeIncrement * segmentIndex) + local subPosition = GetPositionAtTime(subTime, origin, velocity, acceleration) + local subVelocity = GetVelocityAtTime(subTime, velocity, acceleration) + local subRayDir = subVelocity * timeIncrement + + local subResult = CastHandler(castWorldRoot[i], subLastPoint, subRayDir, castRaycastParams[i], variant) + + if subResult then + local subDisplacement = (subLastPoint - subResult.Position).Magnitude + local subHitVelocity = GetVelocityAtTime(subTime, velocity, acceleration) + + castIsActivelyResimulating[i] = false + + castTotalRuntime[i] = totalDelta + castDistanceCovered[i] += subDisplacement + + local subCFrame = CFrame.new(subLastPoint, subLastPoint + subRayDir) * CFrame.new(0, 0, -subDisplacement / 2) + castCFrame[i] = subCFrame + + if bullet then + if motor6d then + motor6d.Transform = subCFrame + elseif bullet:IsA("BasePart") then + bullet.CFrame = subCFrame + else + bullet:PivotTo(subCFrame) + end + end + + QueueFire(caster, "Hit", casts[i], subResult, subHitVelocity, bullet) + table.insert(destroyedIds, i) + break + end + + subLastPoint = subPosition + end + + if castIsActivelyResimulating[i] then + castIsActivelyResimulating[i] = false + end + else + QueueFire(caster, "Hit", casts[i], result, currentVelocity, bullet) + table.insert(destroyedIds, i) + end end -- Check max distance From 8bfdad374af50888bf7a6f3e1d6a7ea6c5333d3c Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 09:46:02 +0000 Subject: [PATCH 46/62] feat: implement RayPierce/CanPierce in SoA simulations - Add castCanPierceFn array for storing pierce callback functions - Support piercing in both normal hit handling and HighFidelity subcasts - Fire Pierced event when CanPierce returns true, continue simulation - Fire Hit event when CanPierce returns false or is nil - Applied to both ParallelSimulation and SerialSimulation --- src/FastCast2/ParallelSimulation.luau | 80 +++++++++++++++++++------- src/FastCast2/SerialSimulation.luau | 81 ++++++++++++++++++++------- 2 files changed, 119 insertions(+), 42 deletions(-) diff --git a/src/FastCast2/ParallelSimulation.luau b/src/FastCast2/ParallelSimulation.luau index fbabfc4..cf308ec 100644 --- a/src/FastCast2/ParallelSimulation.luau +++ b/src/FastCast2/ParallelSimulation.luau @@ -81,6 +81,7 @@ local castVisualize = {} :: { [number]: boolean } local castVisualizeSettings = {} :: { [number]: any } local castCaster = {} :: { [number]: any } local castMotor6D = {} :: { [number]: Motor6D? } +local castCanPierceFn = {} :: { [number]: any? } -- Event queue local QueuedEvents = {} :: { { Callback: any, Args: { any } } } @@ -139,6 +140,11 @@ function ParallelSimulation.Register(cast: any) castVisualize[id] = cast.StateInfo.VisualizeCasts castVisualizeSettings[id] = cast.StateInfo.VisualizeCastSettings castCaster[id] = cast.Caster + castCanPierceFn[id] = nil + + if cast.CanPierce then + castCanPierceFn[id] = cast.CanPierce + end if cast.RayInfo.Size then castSize[id] = cast.RayInfo.Size @@ -184,6 +190,7 @@ function ParallelSimulation.Unregister(id: number) castVisualizeSettings[id] = castVisualizeSettings[lastId] castCaster[id] = castCaster[lastId] castMotor6D[id] = castMotor6D[lastId] + castCanPierceFn[id] = castCanPierceFn[lastId] if casts[lastId] then casts[lastId].ID = id @@ -218,6 +225,7 @@ function ParallelSimulation.Unregister(id: number) castVisualizeSettings[lastId] = nil castCaster[lastId] = nil castMotor6D[lastId] = nil + castCanPierceFn[lastId] = nil casts[id] = nil castCount = lastId - 1 @@ -248,6 +256,7 @@ local function UpdateCasts(deltaTime: number) local CastHandler = castHandlers[castType] local highFidelityBehavior = castHighFidelityBehavior[i] local highFidelitySegmentSize = castHighFidelitySegmentSize[i] + local canPierceFn = castCanPierceFn[i] local origin = castOrigin[i] local totalDelta = castTotalRuntime[i] @@ -304,7 +313,7 @@ local function UpdateCasts(deltaTime: number) -- Fire LengthChanged QueueFire(caster, "LengthChanged", casts[i], lastPosition, rayDir.Unit, rayDisplacement, currentVelocity, bullet) - -- Handle hit with HighFidelity + -- Handle hit with HighFidelity and Piercing if result and hitPart ~= bullet then if highFidelityBehavior == HighFidelityBehavior.Automatic @@ -341,38 +350,67 @@ local function UpdateCasts(deltaTime: number) local subDisplacement = (subLastPoint - subResult.Position).Magnitude local subHitVelocity = GetVelocityAtTime(subTime, velocity, acceleration) - castIsActivelyResimulating[i] = false + local canPierce = canPierceFn and canPierceFn(casts[i], subResult, subHitVelocity, bullet) - castTotalRuntime[i] = totalDelta - castDistanceCovered[i] += subDisplacement + if canPierce then + castDistanceCovered[i] += subDisplacement - local subCFrame = CFrame.new(subLastPoint, subLastPoint + subRayDir) * CFrame.new(0, 0, -subDisplacement / 2) - castCFrame[i] = subCFrame + local subCFrame = CFrame.new(subLastPoint, subLastPoint + subRayDir) * CFrame.new(0, 0, -subDisplacement / 2) + castCFrame[i] = subCFrame - if bullet then - if motor6d then - motor6d.Transform = subCFrame - elseif bullet:IsA("BasePart") then - bullet.CFrame = subCFrame - else - bullet:PivotTo(subCFrame) + if bullet then + if motor6d then + motor6d.Transform = subCFrame + elseif bullet:IsA("BasePart") then + bullet.CFrame = subCFrame + else + bullet:PivotTo(subCFrame) + end end - end - QueueFire(caster, "Hit", casts[i], subResult, subHitVelocity, bullet) - table.insert(destroyedIds, i) - break - end + QueueFire(caster, "Pierced", casts[i], subResult, subHitVelocity, bullet) + subLastPoint = subResult.Position + else + castIsActivelyResimulating[i] = false + + castTotalRuntime[i] = totalDelta + castDistanceCovered[i] += subDisplacement + + local subCFrame = CFrame.new(subLastPoint, subLastPoint + subRayDir) * CFrame.new(0, 0, -subDisplacement / 2) + castCFrame[i] = subCFrame + + if bullet then + if motor6d then + motor6d.Transform = subCFrame + elseif bullet:IsA("BasePart") then + bullet.CFrame = subCFrame + else + bullet:PivotTo(subCFrame) + end + end - subLastPoint = subPosition + QueueFire(caster, "Hit", casts[i], subResult, subHitVelocity, bullet) + table.insert(destroyedIds, i) + break + end + else + subLastPoint = subPosition + end end if castIsActivelyResimulating[i] then castIsActivelyResimulating[i] = false end else - QueueFire(caster, "Hit", casts[i], result, currentVelocity, bullet) - table.insert(destroyedIds, i) + local canPierce = canPierceFn and canPierceFn(casts[i], result, currentVelocity, bullet) + + if canPierce then + castDistanceCovered[i] += rayDisplacement + QueueFire(caster, "Pierced", casts[i], result, currentVelocity, bullet) + else + QueueFire(caster, "Hit", casts[i], result, currentVelocity, bullet) + table.insert(destroyedIds, i) + end end end diff --git a/src/FastCast2/SerialSimulation.luau b/src/FastCast2/SerialSimulation.luau index e8d8156..c38a2d6 100644 --- a/src/FastCast2/SerialSimulation.luau +++ b/src/FastCast2/SerialSimulation.luau @@ -83,6 +83,7 @@ local castVisualize = {} :: { [number]: boolean } local castVisualizeSettings = {} :: { [number]: any } local castCaster = {} :: { [number]: any } local castMotor6D = {} :: { [number]: Motor6D? } +local castCanPierceFn = {} :: { [number]: any? } -- Event queue local QueuedEvents = {} :: { { Callback: any, Args: { any } } } @@ -142,6 +143,11 @@ function SerialSimulation.Register(cast: any) castVisualize[id] = cast.StateInfo.VisualizeCasts castVisualizeSettings[id] = cast.StateInfo.VisualizeCastSettings castCaster[id] = cast.Caster + castCanPierceFn[id] = nil + + if cast.CanPierce then + castCanPierceFn[id] = cast.CanPierce + end if cast.RayInfo.Size then castSize[id] = cast.RayInfo.Size @@ -187,6 +193,7 @@ function SerialSimulation.Unregister(id: number) castVisualizeSettings[id] = castVisualizeSettings[lastId] castCaster[id] = castCaster[lastId] castMotor6D[id] = castMotor6D[lastId] + castCanPierceFn[id] = castCanPierceFn[lastId] if casts[lastId] then casts[lastId].ID = id @@ -221,6 +228,7 @@ function SerialSimulation.Unregister(id: number) castVisualizeSettings[lastId] = nil castCaster[lastId] = nil castMotor6D[lastId] = nil + castCanPierceFn[lastId] = nil casts[id] = nil castCount = lastId - 1 @@ -251,6 +259,7 @@ local function UpdateCasts(deltaTime: number) local CastHandler = castHandlers[castType] local highFidelityBehavior = castHighFidelityBehavior[i] local highFidelitySegmentSize = castHighFidelitySegmentSize[i] + local canPierceFn = castCanPierceFn[i] local origin = castOrigin[i] local totalDelta = castTotalRuntime[i] @@ -307,7 +316,7 @@ local function UpdateCasts(deltaTime: number) -- Fire LengthChanged QueueFire(caster, "LengthChanged", casts[i], lastPosition, rayDir.Unit, rayDisplacement, currentVelocity, bullet) - -- Handle hit with HighFidelity + -- Handle hit with HighFidelity and Piercing if result and hitPart ~= bullet then if highFidelityBehavior == HighFidelityBehavior.Automatic @@ -327,6 +336,7 @@ local function UpdateCasts(deltaTime: number) local timeIncrement = deltaTime / numSegmentsReal local subLastPoint = lastPosition + local subHitResult = nil for segmentIndex = 1, numSegmentsReal do if castCancelHighResCast[i] then castCancelHighResCast[i] = false @@ -344,38 +354,67 @@ local function UpdateCasts(deltaTime: number) local subDisplacement = (subLastPoint - subResult.Position).Magnitude local subHitVelocity = GetVelocityAtTime(subTime, velocity, acceleration) - castIsActivelyResimulating[i] = false + local canPierce = canPierceFn and canPierceFn(casts[i], subResult, subHitVelocity, bullet) - castTotalRuntime[i] = totalDelta - castDistanceCovered[i] += subDisplacement + if canPierce then + castDistanceCovered[i] += subDisplacement - local subCFrame = CFrame.new(subLastPoint, subLastPoint + subRayDir) * CFrame.new(0, 0, -subDisplacement / 2) - castCFrame[i] = subCFrame + local subCFrame = CFrame.new(subLastPoint, subLastPoint + subRayDir) * CFrame.new(0, 0, -subDisplacement / 2) + castCFrame[i] = subCFrame - if bullet then - if motor6d then - motor6d.Transform = subCFrame - elseif bullet:IsA("BasePart") then - bullet.CFrame = subCFrame - else - bullet:PivotTo(subCFrame) + if bullet then + if motor6d then + motor6d.Transform = subCFrame + elseif bullet:IsA("BasePart") then + bullet.CFrame = subCFrame + else + bullet:PivotTo(subCFrame) + end end - end - QueueFire(caster, "Hit", casts[i], subResult, subHitVelocity, bullet) - table.insert(destroyedIds, i) - break - end + QueueFire(caster, "Pierced", casts[i], subResult, subHitVelocity, bullet) + subLastPoint = subResult.Position + else + castIsActivelyResimulating[i] = false + + castTotalRuntime[i] = totalDelta + castDistanceCovered[i] += subDisplacement + + local subCFrame = CFrame.new(subLastPoint, subLastPoint + subRayDir) * CFrame.new(0, 0, -subDisplacement / 2) + castCFrame[i] = subCFrame + + if bullet then + if motor6d then + motor6d.Transform = subCFrame + elseif bullet:IsA("BasePart") then + bullet.CFrame = subCFrame + else + bullet:PivotTo(subCFrame) + end + end - subLastPoint = subPosition + QueueFire(caster, "Hit", casts[i], subResult, subHitVelocity, bullet) + table.insert(destroyedIds, i) + break + end + else + subLastPoint = subPosition + end end if castIsActivelyResimulating[i] then castIsActivelyResimulating[i] = false end else - QueueFire(caster, "Hit", casts[i], result, currentVelocity, bullet) - table.insert(destroyedIds, i) + local canPierce = canPierceFn and canPierceFn(casts[i], result, currentVelocity, bullet) + + if canPierce then + castDistanceCovered[i] += rayDisplacement + QueueFire(caster, "Pierced", casts[i], result, currentVelocity, bullet) + else + QueueFire(caster, "Hit", casts[i], result, currentVelocity, bullet) + table.insert(destroyedIds, i) + end end end From edde9f8ec4eb9b2908cdb45d1c4b3209d0bdeb39 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 09:53:06 +0000 Subject: [PATCH 47/62] feat: add event config/module gating to SoA simulations - Add castEventsConfig, castEventsModuleConfig, castEventsModule SoA arrays - Update QueueFire to check both FastCastEventsConfig and FastCastEventsModuleConfig - Fire module callbacks directly when FastCastEventsModule is set - Fire CastFire, CastTerminating events with proper gating --- src/FastCast2/ParallelSimulation.luau | 63 ++++++++++++++++++++++++--- src/FastCast2/SerialSimulation.luau | 50 ++++++++++++++++++--- 2 files changed, 101 insertions(+), 12 deletions(-) diff --git a/src/FastCast2/ParallelSimulation.luau b/src/FastCast2/ParallelSimulation.luau index cf308ec..dfc1bb7 100644 --- a/src/FastCast2/ParallelSimulation.luau +++ b/src/FastCast2/ParallelSimulation.luau @@ -82,6 +82,9 @@ local castVisualizeSettings = {} :: { [number]: any } local castCaster = {} :: { [number]: any } local castMotor6D = {} :: { [number]: Motor6D? } local castCanPierceFn = {} :: { [number]: any? } +local castEventsConfig = {} :: { [number]: any? } +local castEventsModuleConfig = {} :: { [number]: any? } +local castEventsModule = {} :: { [number]: ModuleScript? } -- Event queue local QueuedEvents = {} :: { { Callback: any, Args: { any } } } @@ -105,12 +108,45 @@ local function DispatchAllEvents() table.clear(QueuedEvents) end -local function QueueFire(caster: any, eventName: string, ...) +local function ShouldFireEvent(eventsConfig: any?, eventName: string): boolean + if not eventsConfig then return true end + if eventName == "Hit" then return eventsConfig.UseHit ~= false end + if eventName == "Pierced" then return eventsConfig.UsePierced ~= false end + if eventName == "LengthChanged" then return eventsConfig.UseLengthChanged ~= false end + if eventName == "CastTerminating" then return eventsConfig.UseCastTerminating ~= false end + if eventName == "CastFire" then return eventsConfig.UseCastFire ~= false end + return true +end + +local function QueueFire(caster: any, eventsConfig: any?, eventsModuleConfig: any?, eventsModule: ModuleScript?, eventName: string, ...) + if not ShouldFireEvent(eventsConfig, eventName) then return end + + if eventsModuleConfig and eventsModuleConfig["Use" .. eventName] == false then return end + + if eventsModule then + local mod = require(eventsModule) + local fn = mod[eventName] + if fn then fn(...) end + end + if caster and caster.Output then caster.Output:Fire(eventName, ...) end end +local function DispatchEvent(callback: any, ...) + if callback then + callback(...) + end +end + +local function DispatchAllEvents() + for _, event in QueuedEvents do + DispatchEvent(event.Callback, unpack(event.Args)) + end + table.clear(QueuedEvents) +end + local ParallelSimulation = { StepConnection = nil } @@ -146,6 +182,10 @@ function ParallelSimulation.Register(cast: any) castCanPierceFn[id] = cast.CanPierce end + castEventsConfig[id] = cast.StateInfo.FastCastEventsConfig + castEventsModuleConfig[id] = cast.StateInfo.FastCastEventsModuleConfig + castEventsModule[id] = cast.RayInfo.FastCastEventsModule + if cast.RayInfo.Size then castSize[id] = cast.RayInfo.Size end @@ -160,6 +200,8 @@ function ParallelSimulation.Register(cast: any) end cast.ID = id + + QueueFire(cast.Caster, cast.StateInfo.FastCastEventsConfig, cast.StateInfo.FastCastEventsModuleConfig, cast.RayInfo.FastCastEventsModule, "CastFire", cast, castOrigin[id], castVelocity[id], castAcceleration[id]) end function ParallelSimulation.Unregister(id: number) @@ -191,6 +233,9 @@ function ParallelSimulation.Unregister(id: number) castCaster[id] = castCaster[lastId] castMotor6D[id] = castMotor6D[lastId] castCanPierceFn[id] = castCanPierceFn[lastId] + castEventsConfig[id] = castEventsConfig[lastId] + castEventsModuleConfig[id] = castEventsModuleConfig[lastId] + castEventsModule[id] = castEventsModule[lastId] if casts[lastId] then casts[lastId].ID = id @@ -226,6 +271,9 @@ function ParallelSimulation.Unregister(id: number) castCaster[lastId] = nil castMotor6D[lastId] = nil castCanPierceFn[lastId] = nil + castEventsConfig[lastId] = nil + castEventsModuleConfig[lastId] = nil + castEventsModule[lastId] = nil casts[id] = nil castCount = lastId - 1 @@ -240,6 +288,9 @@ function ParallelSimulation.Terminate(id: number) if bullet then bullet:Destroy() end + + QueueFire(castCaster[id], castEventsConfig[id], castEventsModuleConfig[id], castEventsModule[id], "CastTerminating", casts[id]) + ParallelSimulation.Unregister(id) end @@ -311,7 +362,7 @@ local function UpdateCasts(deltaTime: number) end -- Fire LengthChanged - QueueFire(caster, "LengthChanged", casts[i], lastPosition, rayDir.Unit, rayDisplacement, currentVelocity, bullet) + QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "LengthChanged", casts[i], lastPosition, rayDir.Unit, rayDisplacement, currentVelocity, bullet) -- Handle hit with HighFidelity and Piercing if result and hitPart ~= bullet then @@ -368,7 +419,7 @@ local function UpdateCasts(deltaTime: number) end end - QueueFire(caster, "Pierced", casts[i], subResult, subHitVelocity, bullet) + QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "Pierced", casts[i], subResult, subHitVelocity, bullet) subLastPoint = subResult.Position else castIsActivelyResimulating[i] = false @@ -389,7 +440,7 @@ local function UpdateCasts(deltaTime: number) end end - QueueFire(caster, "Hit", casts[i], subResult, subHitVelocity, bullet) + QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "Hit", casts[i], subResult, subHitVelocity, bullet) table.insert(destroyedIds, i) break end @@ -406,9 +457,9 @@ local function UpdateCasts(deltaTime: number) if canPierce then castDistanceCovered[i] += rayDisplacement - QueueFire(caster, "Pierced", casts[i], result, currentVelocity, bullet) + QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "Pierced", casts[i], result, currentVelocity, bullet) else - QueueFire(caster, "Hit", casts[i], result, currentVelocity, bullet) + QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "Hit", casts[i], result, currentVelocity, bullet) table.insert(destroyedIds, i) end end diff --git a/src/FastCast2/SerialSimulation.luau b/src/FastCast2/SerialSimulation.luau index c38a2d6..0ef1e99 100644 --- a/src/FastCast2/SerialSimulation.luau +++ b/src/FastCast2/SerialSimulation.luau @@ -84,6 +84,9 @@ local castVisualizeSettings = {} :: { [number]: any } local castCaster = {} :: { [number]: any } local castMotor6D = {} :: { [number]: Motor6D? } local castCanPierceFn = {} :: { [number]: any? } +local castEventsConfig = {} :: { [number]: any? } +local castEventsModuleConfig = {} :: { [number]: any? } +local castEventsModule = {} :: { [number]: ModuleScript? } -- Event queue local QueuedEvents = {} :: { { Callback: any, Args: { any } } } @@ -107,7 +110,27 @@ local function DispatchAllEvents() table.clear(QueuedEvents) end -local function QueueFire(caster: any, eventName: string, ...) +local function ShouldFireEvent(eventsConfig: any?, eventName: string): boolean + if not eventsConfig then return true end + if eventName == "Hit" then return eventsConfig.UseHit ~= false end + if eventName == "Pierced" then return eventsConfig.UsePierced ~= false end + if eventName == "LengthChanged" then return eventsConfig.UseLengthChanged ~= false end + if eventName == "CastTerminating" then return eventsConfig.UseCastTerminating ~= false end + if eventName == "CastFire" then return eventsConfig.UseCastFire ~= false end + return true +end + +local function QueueFire(caster: any, eventsConfig: any?, eventsModuleConfig: any?, eventsModule: ModuleScript?, eventName: string, ...) + if not ShouldFireEvent(eventsConfig, eventName) then return end + + if eventsModuleConfig and eventsModuleConfig["Use" .. eventName] == false then return end + + if eventsModule then + local mod = require(eventsModule) + local fn = mod[eventName] + if fn then fn(...) end + end + if caster and caster.Output then caster.Output:Fire(eventName, ...) end @@ -149,6 +172,10 @@ function SerialSimulation.Register(cast: any) castCanPierceFn[id] = cast.CanPierce end + castEventsConfig[id] = cast.StateInfo.FastCastEventsConfig + castEventsModuleConfig[id] = cast.StateInfo.FastCastEventsModuleConfig + castEventsModule[id] = cast.RayInfo.FastCastEventsModule + if cast.RayInfo.Size then castSize[id] = cast.RayInfo.Size end @@ -163,6 +190,8 @@ function SerialSimulation.Register(cast: any) end cast.ID = id + + QueueFire(cast.Caster, cast.StateInfo.FastCastEventsConfig, cast.StateInfo.FastCastEventsModuleConfig, cast.RayInfo.FastCastEventsModule, "CastFire", cast, castOrigin[id], castVelocity[id], castAcceleration[id]) end function SerialSimulation.Unregister(id: number) @@ -194,6 +223,9 @@ function SerialSimulation.Unregister(id: number) castCaster[id] = castCaster[lastId] castMotor6D[id] = castMotor6D[lastId] castCanPierceFn[id] = castCanPierceFn[lastId] + castEventsConfig[id] = castEventsConfig[lastId] + castEventsModuleConfig[id] = castEventsModuleConfig[lastId] + castEventsModule[id] = castEventsModule[lastId] if casts[lastId] then casts[lastId].ID = id @@ -229,6 +261,9 @@ function SerialSimulation.Unregister(id: number) castCaster[lastId] = nil castMotor6D[lastId] = nil castCanPierceFn[lastId] = nil + castEventsConfig[lastId] = nil + castEventsModuleConfig[lastId] = nil + castEventsModule[lastId] = nil casts[id] = nil castCount = lastId - 1 @@ -243,6 +278,9 @@ function SerialSimulation.Terminate(id: number) if bullet then bullet:Destroy() end + + QueueFire(castCaster[id], castEventsConfig[id], castEventsModuleConfig[id], castEventsModule[id], "CastTerminating", casts[id]) + SerialSimulation.Unregister(id) end @@ -314,7 +352,7 @@ local function UpdateCasts(deltaTime: number) end -- Fire LengthChanged - QueueFire(caster, "LengthChanged", casts[i], lastPosition, rayDir.Unit, rayDisplacement, currentVelocity, bullet) + QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "LengthChanged", casts[i], lastPosition, rayDir.Unit, rayDisplacement, currentVelocity, bullet) -- Handle hit with HighFidelity and Piercing if result and hitPart ~= bullet then @@ -372,7 +410,7 @@ local function UpdateCasts(deltaTime: number) end end - QueueFire(caster, "Pierced", casts[i], subResult, subHitVelocity, bullet) + QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "Pierced", casts[i], subResult, subHitVelocity, bullet) subLastPoint = subResult.Position else castIsActivelyResimulating[i] = false @@ -393,7 +431,7 @@ local function UpdateCasts(deltaTime: number) end end - QueueFire(caster, "Hit", casts[i], subResult, subHitVelocity, bullet) + QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "Hit", casts[i], subResult, subHitVelocity, bullet) table.insert(destroyedIds, i) break end @@ -410,9 +448,9 @@ local function UpdateCasts(deltaTime: number) if canPierce then castDistanceCovered[i] += rayDisplacement - QueueFire(caster, "Pierced", casts[i], result, currentVelocity, bullet) + QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "Pierced", casts[i], result, currentVelocity, bullet) else - QueueFire(caster, "Hit", casts[i], result, currentVelocity, bullet) + QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "Hit", casts[i], result, currentVelocity, bullet) table.insert(destroyedIds, i) end end From ea27c671a1a0003dc375a8ff60720f5a7abdf072 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 09:53:41 +0000 Subject: [PATCH 48/62] fix: correct CastFire event signature to match legacy API CastFire should fire: (cast, origin, direction, velocity, behavior) Not: (cast, origin, velocity, acceleration) --- src/FastCast2/ParallelSimulation.luau | 6 +++++- src/FastCast2/SerialSimulation.luau | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/FastCast2/ParallelSimulation.luau b/src/FastCast2/ParallelSimulation.luau index dfc1bb7..83a50f3 100644 --- a/src/FastCast2/ParallelSimulation.luau +++ b/src/FastCast2/ParallelSimulation.luau @@ -201,7 +201,11 @@ function ParallelSimulation.Register(cast: any) cast.ID = id - QueueFire(cast.Caster, cast.StateInfo.FastCastEventsConfig, cast.StateInfo.FastCastEventsModuleConfig, cast.RayInfo.FastCastEventsModule, "CastFire", cast, castOrigin[id], castVelocity[id], castAcceleration[id]) + local behavior = cast.StateInfo.Behavior or {} + local origin = castOrigin[id] + local velocity = castVelocity[id] + local direction = velocity.Magnitude > 0 and velocity.Unit * velocity.Magnitude or Vector3.new() + QueueFire(cast.Caster, cast.StateInfo.FastCastEventsConfig, cast.StateInfo.FastCastEventsModuleConfig, cast.RayInfo.FastCastEventsModule, "CastFire", cast, origin, direction, velocity, behavior) end function ParallelSimulation.Unregister(id: number) diff --git a/src/FastCast2/SerialSimulation.luau b/src/FastCast2/SerialSimulation.luau index 0ef1e99..61ed9c1 100644 --- a/src/FastCast2/SerialSimulation.luau +++ b/src/FastCast2/SerialSimulation.luau @@ -191,7 +191,11 @@ function SerialSimulation.Register(cast: any) cast.ID = id - QueueFire(cast.Caster, cast.StateInfo.FastCastEventsConfig, cast.StateInfo.FastCastEventsModuleConfig, cast.RayInfo.FastCastEventsModule, "CastFire", cast, castOrigin[id], castVelocity[id], castAcceleration[id]) + local behavior = cast.StateInfo.Behavior or {} + local origin = castOrigin[id] + local velocity = castVelocity[id] + local direction = velocity.Magnitude > 0 and velocity.Unit * velocity.Magnitude or Vector3.new() + QueueFire(cast.Caster, cast.StateInfo.FastCastEventsConfig, cast.StateInfo.FastCastEventsModuleConfig, cast.RayInfo.FastCastEventsModule, "CastFire", cast, origin, direction, velocity, behavior) end function SerialSimulation.Unregister(id: number) From bc53d7a0aa3621f69fdcb828fab472ef2822bc7c Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 17:03:42 +0700 Subject: [PATCH 49/62] Comment out unused functions in ActiveCast.lua(legacy code) Comment out the SimulateCast and Stepped functions for future reference. --- src/FastCast2/ActiveCast.luau | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/FastCast2/ActiveCast.luau b/src/FastCast2/ActiveCast.luau index b91ca8b..1df7116 100644 --- a/src/FastCast2/ActiveCast.luau +++ b/src/FastCast2/ActiveCast.luau @@ -353,6 +353,7 @@ end cast.Caster.Output:Fire("CastFire", cast, origin, direction, velocity, behavior) end]] +--[[ local function SimulateCast( cast: any, delta: number, @@ -673,6 +674,7 @@ local function SimulateCast( DbgVisualizeHit(CFrame.new(currentTarget), false, VisualizeCasts, VisualizeCastSettings) end end +--]] --[=[ @function createCastData @@ -841,7 +843,7 @@ function ActiveCast.createCastData( local FastCastEvents: TypeDef.FastCastEvents = eventModule and require(eventModule) or nil --setmetatable(cast, ActiveCast) - + --[[ local function Stepped(delta: number) if cast.StateInfo.Paused then return @@ -983,6 +985,7 @@ totalDelta = cast.StateInfo.TotalRuntime - trajectory.StartTime end end end + --]] return cast end From b6fbb3edb9ae422023de472e8186777afb5c7b1f Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 10:06:35 +0000 Subject: [PATCH 50/62] remove: spacing --- src/FastCast2/ActiveCast.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FastCast2/ActiveCast.luau b/src/FastCast2/ActiveCast.luau index 1df7116..fcb9861 100644 --- a/src/FastCast2/ActiveCast.luau +++ b/src/FastCast2/ActiveCast.luau @@ -990,4 +990,4 @@ totalDelta = cast.StateInfo.TotalRuntime - trajectory.StartTime return cast end -return ActiveCast +return ActiveCast \ No newline at end of file From 60bca8c87398498f43f35927dbd77433a83ceb64 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 10:32:07 +0000 Subject: [PATCH 51/62] feat: add debug logging to SoA simulations matching legacy ActiveCast Add DebugLogging checks for: - Casting (per frame) - Hit detection - RayPierce (piercing function returns, no function set) - Calculation (subcast info) - Segment (per-segment subcast iteration) Matches debug output in legacy ActiveCast.luau --- src/FastCast2/Configs.luau | 1 + src/FastCast2/ParallelSimulation.luau | 42 +++++++++++++++++++++++++++ src/FastCast2/SerialSimulation.luau | 42 +++++++++++++++++++++++++++ 3 files changed, 85 insertions(+) diff --git a/src/FastCast2/Configs.luau b/src/FastCast2/Configs.luau index 0748274..70715bb 100644 --- a/src/FastCast2/Configs.luau +++ b/src/FastCast2/Configs.luau @@ -13,6 +13,7 @@ Configs.DebugLogging = { Hit = false, RayPierce = false, Calculation = false, + AutomaticPerformance = false, } Configs.VisualizeCasts = true diff --git a/src/FastCast2/ParallelSimulation.luau b/src/FastCast2/ParallelSimulation.luau index 83a50f3..d0b6815 100644 --- a/src/FastCast2/ParallelSimulation.luau +++ b/src/FastCast2/ParallelSimulation.luau @@ -14,6 +14,8 @@ local TypeDef = require(FastCastModule:WaitForChild("TypeDefinitions")) local Configs = require(FastCastModule:WaitForChild("Configs")) local DebugLogging = Configs.DebugLogging local FastCastEnums = require(FastCastModule:WaitForChild("FastCastEnums")) +local Configs = require(FastCastModule:WaitForChild("Configs")) +local DebugLogging = Configs.DebugLogging local Motor6DPool = require(FastCastModule:WaitForChild("Motor6DPool")) local EnumCastTypes = FastCastEnums.CastType @@ -301,6 +303,10 @@ end local function UpdateCasts(deltaTime: number) if castCount == 0 then return end + if DebugLogging.Casting then + print("Casting for frame.") + end + local destroyedIds = {} :: { number } for i = 1, castCount do @@ -370,6 +376,14 @@ local function UpdateCasts(deltaTime: number) -- Handle hit with HighFidelity and Piercing if result and hitPart ~= bullet then + if DebugLogging.Hit then + print("Hit something, testing now.") + end + + if DebugLogging.RayPierce and not canPierceFn then + print("No piercing function set, proceeding to hit processing.") + end + if highFidelityBehavior == HighFidelityBehavior.Automatic and highFidelitySegmentSize > 0 @@ -387,6 +401,10 @@ local function UpdateCasts(deltaTime: number) local timeIncrement = deltaTime / numSegmentsReal + if DebugLogging.Calculation then + print("Performing subcast! Time increment: " .. timeIncrement .. ", num segments: " .. numSegmentsReal) + end + local subLastPoint = lastPosition for segmentIndex = 1, numSegmentsReal do if castCancelHighResCast[i] then @@ -394,6 +412,10 @@ local function UpdateCasts(deltaTime: number) break end + if DebugLogging.Segment then + print("[" .. segmentIndex .. "] Subcast of time increment " .. timeIncrement) + end + local subTime = totalDelta - deltaTime + (timeIncrement * segmentIndex) local subPosition = GetPositionAtTime(subTime, origin, velocity, acceleration) local subVelocity = GetVelocityAtTime(subTime, velocity, acceleration) @@ -408,6 +430,10 @@ local function UpdateCasts(deltaTime: number) local canPierce = canPierceFn and canPierceFn(casts[i], subResult, subHitVelocity, bullet) if canPierce then + if DebugLogging.RayPierce then + print("Piercing function returned TRUE to pierce this part.") + end + castDistanceCovered[i] += subDisplacement local subCFrame = CFrame.new(subLastPoint, subLastPoint + subRayDir) * CFrame.new(0, 0, -subDisplacement / 2) @@ -426,6 +452,10 @@ local function UpdateCasts(deltaTime: number) QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "Pierced", casts[i], subResult, subHitVelocity, bullet) subLastPoint = subResult.Position else + if DebugLogging.RayPierce then + print("Piercing function is nil or it returned FALSE to not pierce this hit.") + end + castIsActivelyResimulating[i] = false castTotalRuntime[i] = totalDelta @@ -460,9 +490,21 @@ local function UpdateCasts(deltaTime: number) local canPierce = canPierceFn and canPierceFn(casts[i], result, currentVelocity, bullet) if canPierce then + if DebugLogging.RayPierce then + print("Piercing function returned TRUE to pierce this part.") + end + castDistanceCovered[i] += rayDisplacement QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "Pierced", casts[i], result, currentVelocity, bullet) else + if DebugLogging.RayPierce then + print("Piercing function is nil or it returned FALSE to not pierce this hit.") + end + + if DebugLogging.Hit then + print("Hit was successful. Terminating.") + end + QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "Hit", casts[i], result, currentVelocity, bullet) table.insert(destroyedIds, i) end diff --git a/src/FastCast2/SerialSimulation.luau b/src/FastCast2/SerialSimulation.luau index 61ed9c1..d70ad1f 100644 --- a/src/FastCast2/SerialSimulation.luau +++ b/src/FastCast2/SerialSimulation.luau @@ -15,6 +15,8 @@ local TypeDef = require(FastCastModule:WaitForChild("TypeDefinitions")) local Configs = require(FastCastModule:WaitForChild("Configs")) local DebugLogging = Configs.DebugLogging local FastCastEnums = require(FastCastModule:WaitForChild("FastCastEnums")) +local Configs = require(FastCastModule:WaitForChild("Configs")) +local DebugLogging = Configs.DebugLogging local ActiveCastSerial = require(FastCastModule:WaitForChild("ActiveCastSerial")) local Motor6DPool = require(FastCastModule:WaitForChild("Motor6DPool")) @@ -291,6 +293,10 @@ end local function UpdateCasts(deltaTime: number) if castCount == 0 then return end + if DebugLogging.Casting then + print("Casting for frame.") + end + local destroyedIds = {} :: { number } for i = 1, castCount do @@ -360,6 +366,14 @@ local function UpdateCasts(deltaTime: number) -- Handle hit with HighFidelity and Piercing if result and hitPart ~= bullet then + if DebugLogging.Hit then + print("Hit something, testing now.") + end + + if DebugLogging.RayPierce and not canPierceFn then + print("No piercing function set, proceeding to hit processing.") + end + if highFidelityBehavior == HighFidelityBehavior.Automatic and highFidelitySegmentSize > 0 @@ -377,6 +391,10 @@ local function UpdateCasts(deltaTime: number) local timeIncrement = deltaTime / numSegmentsReal + if DebugLogging.Calculation then + print("Performing subcast! Time increment: " .. timeIncrement .. ", num segments: " .. numSegmentsReal) + end + local subLastPoint = lastPosition local subHitResult = nil for segmentIndex = 1, numSegmentsReal do @@ -385,6 +403,10 @@ local function UpdateCasts(deltaTime: number) break end + if DebugLogging.Segment then + print("[" .. segmentIndex .. "] Subcast of time increment " .. timeIncrement) + end + local subTime = totalDelta - deltaTime + (timeIncrement * segmentIndex) local subPosition = GetPositionAtTime(subTime, origin, velocity, acceleration) local subVelocity = GetVelocityAtTime(subTime, velocity, acceleration) @@ -399,6 +421,10 @@ local function UpdateCasts(deltaTime: number) local canPierce = canPierceFn and canPierceFn(casts[i], subResult, subHitVelocity, bullet) if canPierce then + if DebugLogging.RayPierce then + print("Piercing function returned TRUE to pierce this part.") + end + castDistanceCovered[i] += subDisplacement local subCFrame = CFrame.new(subLastPoint, subLastPoint + subRayDir) * CFrame.new(0, 0, -subDisplacement / 2) @@ -417,6 +443,10 @@ local function UpdateCasts(deltaTime: number) QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "Pierced", casts[i], subResult, subHitVelocity, bullet) subLastPoint = subResult.Position else + if DebugLogging.RayPierce then + print("Piercing function is nil or it returned FALSE to not pierce this hit.") + end + castIsActivelyResimulating[i] = false castTotalRuntime[i] = totalDelta @@ -451,9 +481,21 @@ local function UpdateCasts(deltaTime: number) local canPierce = canPierceFn and canPierceFn(casts[i], result, currentVelocity, bullet) if canPierce then + if DebugLogging.RayPierce then + print("Piercing function returned TRUE to pierce this part.") + end + castDistanceCovered[i] += rayDisplacement QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "Pierced", casts[i], result, currentVelocity, bullet) else + if DebugLogging.RayPierce then + print("Piercing function is nil or it returned FALSE to not pierce this hit.") + end + + if DebugLogging.Hit then + print("Hit was successful. Terminating.") + end + QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "Hit", casts[i], result, currentVelocity, bullet) table.insert(destroyedIds, i) end From ad907eb1ba57a5a9f073049bedca52d00be26078 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 11:00:13 +0000 Subject: [PATCH 52/62] Update typedef --- src/FastCast2/TypeDefinitions.luau | 1 + 1 file changed, 1 insertion(+) diff --git a/src/FastCast2/TypeDefinitions.luau b/src/FastCast2/TypeDefinitions.luau index ed9eecb..e6ebabd 100644 --- a/src/FastCast2/TypeDefinitions.luau +++ b/src/FastCast2/TypeDefinitions.luau @@ -317,6 +317,7 @@ export type FastCastBehavior = { AutoIgnoreContainer: boolean, SimulateAfterPhysic: boolean, + MovementMethod: "BulkMoveTo" | "Transform", AutomaticPerformance: boolean, AdaptivePerformance: AdaptivePerformance, From dbe98f7a7482742bb9f3ec6a1f69616ad6b5c385 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 11:03:34 +0000 Subject: [PATCH 53/62] Comment out unused function --- src/FastCast2/init.luau | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/FastCast2/init.luau b/src/FastCast2/init.luau index e4abe46..202274f 100644 --- a/src/FastCast2/init.luau +++ b/src/FastCast2/init.luau @@ -63,7 +63,7 @@ --local BaseCast = script:WaitForChild("BaseCast") -- Requires -local FastCastEnums = require(script.FastCastEnums) +-- local FastCastEnums = require(script.FastCastEnums) local Signal = require(script:WaitForChild("Signal")) local TypeDef = require(script:WaitForChild("TypeDefinitions")) local DefaultConfigs = require(script:WaitForChild("DefaultConfigs")) @@ -141,9 +141,11 @@ local function GetTrajectoryInfo( return { GetPositionAtTime(duration, origin, vel, accel), GetVelocityAtTime(duration, vel, accel) } end +--[[ local function GetLatestTrajectoryEndInfo(cast: vaildcast): { [number]: Vector3 } return GetTrajectoryInfo(cast, 1) end +--]] local function ModifyTransformation( cast: vaildcast, From c1742b8416ddecdd3028021edb58f3f4ae454b9b Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 11:04:34 +0000 Subject: [PATCH 54/62] Comment out unused function --- src/FastCast2/init.luau | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/FastCast2/init.luau b/src/FastCast2/init.luau index 202274f..88b49a7 100644 --- a/src/FastCast2/init.luau +++ b/src/FastCast2/init.luau @@ -125,6 +125,7 @@ local function GetVelocityAtTime(time: number, initialVelocity: Vector3, acceler return initialVelocity + acceleration * time end +--[[ local function GetTrajectoryInfo( cast: vaildcast, index: number @@ -140,6 +141,7 @@ local function GetTrajectoryInfo( return { GetPositionAtTime(duration, origin, vel, accel), GetVelocityAtTime(duration, vel, accel) } end +--]] --[[ local function GetLatestTrajectoryEndInfo(cast: vaildcast): { [number]: Vector3 } From d045c0cc5712aa950c28b10765701c12fdeb1e5d Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Fri, 8 May 2026 11:12:24 +0000 Subject: [PATCH 55/62] feat: add cast visualization matching legacy ActiveCast Add visualization functions for ray/block/sphere segments and hit markers: - VisualizeRaySegment, VisualizeBlockSegment, VisualizeSphereSegment - VisualizeHit with pierce/hit color differentiation - Uses task.synchronize in ParallelSimulation for cross-thread safety - Visualize cast segments per frame, hit markers on hit/pierce events - Matches legacy Debug_Segment/Hit/RayPierce colors --- src/FastCast2/ParallelSimulation.luau | 122 ++++++++++++++++++++++++++ src/FastCast2/SerialSimulation.luau | 117 ++++++++++++++++++++++++ 2 files changed, 239 insertions(+) diff --git a/src/FastCast2/ParallelSimulation.luau b/src/FastCast2/ParallelSimulation.luau index d0b6815..d9304af 100644 --- a/src/FastCast2/ParallelSimulation.luau +++ b/src/FastCast2/ParallelSimulation.luau @@ -32,6 +32,83 @@ local DBG_SEGMENT_SUB_COLOR2 = Color3.new(0.454902, 0.933333, 0.011765) local DBG_HIT_SUB_COLOR = Color3.new(0.0588235, 0.87451, 1) local DBG_RAYPIERCE_SUB_COLOR = Color3.new(1, 0.113725, 0.588235) +local FC_VIS_OBJ_NAME = "FastCastVisualizationObjects" + +local function GetVisualizationContainer(): Instance + local container = workspace.Terrain:FindFirstChild(FC_VIS_OBJ_NAME) + if not container then + container = Instance.new("Folder") + container.Name = FC_VIS_OBJ_NAME + container.Archivable = false + container.Parent = workspace.Terrain + end + return container +end + +local function DebrisVisualization(obj: Instance?, lifetime: number) + if not obj then return end + if lifetime <= 0 then + obj:Destroy() + return + end + task.delay(lifetime, function() + if obj then obj:Destroy() end + end) +end + +local function VisualizeRaySegment(cframe: CFrame, length: number, visualizeSettings: any, lifetime: number): ConeHandleAdornment? + if not visualizeSettings then return nil end + local adornment = Instance.new("ConeHandleAdornment") + adornment.Adornee = workspace.Terrain + adornment.CFrame = cframe + adornment.Height = length + adornment.Color3 = visualizeSettings.Debug_SegmentColor + adornment.Radius = visualizeSettings.Debug_SegmentSize + adornment.Transparency = visualizeSettings.Debug_SegmentTransparency + adornment.Parent = GetVisualizationContainer() + DebrisVisualization(adornment, lifetime) + return adornment +end + +local function VisualizeBlockSegment(cframe: CFrame, size: Vector3, visualizeSettings: any, lifetime: number): BoxHandleAdornment? + if not visualizeSettings then return nil end + local adornment = Instance.new("BoxHandleAdornment") + adornment.Adornee = workspace.Terrain + adornment.CFrame = cframe + adornment.Size = size + adornment.Color3 = visualizeSettings.Debug_SegmentColor + adornment.Transparency = visualizeSettings.Debug_SegmentTransparency + adornment.Parent = GetVisualizationContainer() + DebrisVisualization(adornment, lifetime) + return adornment +end + +local function VisualizeSphereSegment(cframe: CFrame, radius: number, visualizeSettings: any, lifetime: number): SphereHandleAdornment? + if not visualizeSettings then return nil end + local adornment = Instance.new("SphereHandleAdornment") + adornment.Adornee = workspace.Terrain + adornment.CFrame = cframe + adornment.Radius = radius + adornment.Color3 = visualizeSettings.Debug_SegmentColor + adornment.Transparency = visualizeSettings.Debug_SegmentTransparency + adornment.Parent = GetVisualizationContainer() + DebrisVisualization(adornment, lifetime) + return adornment +end + +local function VisualizeHit(cframe: CFrame, wasPierce: boolean, visualizeSettings: any, lifetime: number): SphereHandleAdornment? + if not visualizeSettings then return nil end + local adornment = Instance.new("SphereHandleAdornment") + adornment.Adornee = workspace.Terrain + adornment.CFrame = cframe + adornment.Radius = wasPierce and visualizeSettings.Debug_RayPierceSize or visualizeSettings.Debug_HitSize + adornment.Transparency = wasPierce and visualizeSettings.Debug_RayPierceTransparency or visualizeSettings.Debug_HitTransparency + adornment.Color3 = wasPierce and visualizeSettings.Debug_RayPierceColor or visualizeSettings.Debug_HitColor + adornment.Parent = GetVisualizationContainer() + DebrisVisualization(adornment, lifetime) + return adornment +end + local castHandlers = { [EnumCastTypes.Raycast] = function(targetWorldRoot: WorldRoot, origin: Vector3, direction: Vector3, params: RaycastParams) return targetWorldRoot:Raycast(origin, direction, params) @@ -371,6 +448,21 @@ local function UpdateCasts(deltaTime: number) end end + -- Visualization (requires task.synchronize for cross-thread) + local visualize = castVisualize[i] + local visualizeSettings = castVisualizeSettings[i] + if visualize and visualizeSettings and deltaTime > 0 then + task.synchronize() + local lifetime = visualizeSettings.Debug_RayLifetime or 1 + if castType == EnumCastTypes.Raycast then + VisualizeRaySegment(CFrame.new(lastPosition, lastPosition + rayDir), rayDisplacement, visualizeSettings, lifetime) + elseif castType == EnumCastTypes.Blockcast then + VisualizeBlockSegment(CFrame.new(lastPosition, lastPosition + rayDir), castSize[i], visualizeSettings, lifetime) + elseif castType == EnumCastTypes.Spherecast then + VisualizeSphereSegment(CFrame.new(lastPosition, lastPosition + rayDir), castRadius[i], visualizeSettings, lifetime) + end + end + -- Fire LengthChanged QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "LengthChanged", casts[i], lastPosition, rayDir.Unit, rayDisplacement, currentVelocity, bullet) @@ -451,6 +543,15 @@ local function UpdateCasts(deltaTime: number) QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "Pierced", casts[i], subResult, subHitVelocity, bullet) subLastPoint = subResult.Position + + if visualize and visualizeSettings then + task.synchronize() + local lifetime = visualizeSettings.Debug_RayLifetime or 1 + local hitVis = VisualizeHit(CFrame.new(subResult.Position), true, visualizeSettings, lifetime) + if hitVis then + hitVis.Color3 = DBG_RAYPIERCE_SUB_COLOR + end + end else if DebugLogging.RayPierce then print("Piercing function is nil or it returned FALSE to not pierce this hit.") @@ -475,6 +576,16 @@ local function UpdateCasts(deltaTime: number) end QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "Hit", casts[i], subResult, subHitVelocity, bullet) + + if visualize and visualizeSettings then + task.synchronize() + local lifetime = visualizeSettings.Debug_RayLifetime or 1 + local hitVis = VisualizeHit(CFrame.new(subResult.Position), false, visualizeSettings, lifetime) + if hitVis then + hitVis.Color3 = DBG_HIT_SUB_COLOR + end + end + table.insert(destroyedIds, i) break end @@ -496,6 +607,11 @@ local function UpdateCasts(deltaTime: number) castDistanceCovered[i] += rayDisplacement QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "Pierced", casts[i], result, currentVelocity, bullet) + + if visualize and visualizeSettings then + task.synchronize() + VisualizeHit(CFrame.new(result.Position), true, visualizeSettings, visualizeSettings.Debug_HitLifetime or 1) + end else if DebugLogging.RayPierce then print("Piercing function is nil or it returned FALSE to not pierce this hit.") @@ -506,6 +622,12 @@ local function UpdateCasts(deltaTime: number) end QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "Hit", casts[i], result, currentVelocity, bullet) + + if visualize and visualizeSettings then + task.synchronize() + VisualizeHit(CFrame.new(result.Position), false, visualizeSettings, visualizeSettings.Debug_HitLifetime or 1) + end + table.insert(destroyedIds, i) end end diff --git a/src/FastCast2/SerialSimulation.luau b/src/FastCast2/SerialSimulation.luau index d70ad1f..a79f221 100644 --- a/src/FastCast2/SerialSimulation.luau +++ b/src/FastCast2/SerialSimulation.luau @@ -34,6 +34,83 @@ local DBG_SEGMENT_SUB_COLOR2 = Color3.new(0.454902, 0.933333, 0.011765) local DBG_HIT_SUB_COLOR = Color3.new(0.0588235, 0.87451, 1) local DBG_RAYPIERCE_SUB_COLOR = Color3.new(1, 0.113725, 0.588235) +local FC_VIS_OBJ_NAME = "FastCastVisualizationObjects" + +local function GetVisualizationContainer(): Instance + local container = workspace.Terrain:FindFirstChild(FC_VIS_OBJ_NAME) + if not container then + container = Instance.new("Folder") + container.Name = FC_VIS_OBJ_NAME + container.Archivable = false + container.Parent = workspace.Terrain + end + return container +end + +local function DebrisVisualization(obj: Instance?, lifetime: number) + if not obj then return end + if lifetime <= 0 then + obj:Destroy() + return + end + task.delay(lifetime, function() + if obj then obj:Destroy() end + end) +end + +local function VisualizeRaySegment(cframe: CFrame, length: number, visualizeSettings: any, lifetime: number): ConeHandleAdornment? + if not visualizeSettings then return nil end + local adornment = Instance.new("ConeHandleAdornment") + adornment.Adornee = workspace.Terrain + adornment.CFrame = cframe + adornment.Height = length + adornment.Color3 = visualizeSettings.Debug_SegmentColor + adornment.Radius = visualizeSettings.Debug_SegmentSize + adornment.Transparency = visualizeSettings.Debug_SegmentTransparency + adornment.Parent = GetVisualizationContainer() + DebrisVisualization(adornment, lifetime) + return adornment +end + +local function VisualizeBlockSegment(cframe: CFrame, size: Vector3, visualizeSettings: any, lifetime: number): BoxHandleAdornment? + if not visualizeSettings then return nil end + local adornment = Instance.new("BoxHandleAdornment") + adornment.Adornee = workspace.Terrain + adornment.CFrame = cframe + adornment.Size = size + adornment.Color3 = visualizeSettings.Debug_SegmentColor + adornment.Transparency = visualizeSettings.Debug_SegmentTransparency + adornment.Parent = GetVisualizationContainer() + DebrisVisualization(adornment, lifetime) + return adornment +end + +local function VisualizeSphereSegment(cframe: CFrame, radius: number, visualizeSettings: any, lifetime: number): SphereHandleAdornment? + if not visualizeSettings then return nil end + local adornment = Instance.new("SphereHandleAdornment") + adornment.Adornee = workspace.Terrain + adornment.CFrame = cframe + adornment.Radius = radius + adornment.Color3 = visualizeSettings.Debug_SegmentColor + adornment.Transparency = visualizeSettings.Debug_SegmentTransparency + adornment.Parent = GetVisualizationContainer() + DebrisVisualization(adornment, lifetime) + return adornment +end + +local function VisualizeHit(cframe: CFrame, wasPierce: boolean, visualizeSettings: any, lifetime: number): SphereHandleAdornment? + if not visualizeSettings then return nil end + local adornment = Instance.new("SphereHandleAdornment") + adornment.Adornee = workspace.Terrain + adornment.CFrame = cframe + adornment.Radius = wasPierce and visualizeSettings.Debug_RayPierceSize or visualizeSettings.Debug_HitSize + adornment.Transparency = wasPierce and visualizeSettings.Debug_RayPierceTransparency or visualizeSettings.Debug_HitTransparency + adornment.Color3 = wasPierce and visualizeSettings.Debug_RayPierceColor or visualizeSettings.Debug_HitColor + adornment.Parent = GetVisualizationContainer() + DebrisVisualization(adornment, lifetime) + return adornment +end + local castHandlers = { [EnumCastTypes.Raycast] = function(targetWorldRoot: WorldRoot, origin: Vector3, direction: Vector3, params: RaycastParams) return targetWorldRoot:Raycast(origin, direction, params) @@ -361,6 +438,20 @@ local function UpdateCasts(deltaTime: number) end end + -- Visualization (no task.synchronize needed for serial mode) + local visualize = castVisualize[i] + local visualizeSettings = castVisualizeSettings[i] + if visualize and visualizeSettings and deltaTime > 0 then + local lifetime = visualizeSettings.Debug_RayLifetime or 1 + if castType == EnumCastTypes.Raycast then + VisualizeRaySegment(CFrame.new(lastPosition, lastPosition + rayDir), rayDisplacement, visualizeSettings, lifetime) + elseif castType == EnumCastTypes.Blockcast then + VisualizeBlockSegment(CFrame.new(lastPosition, lastPosition + rayDir), castSize[i], visualizeSettings, lifetime) + elseif castType == EnumCastTypes.Spherecast then + VisualizeSphereSegment(CFrame.new(lastPosition, lastPosition + rayDir), castRadius[i], visualizeSettings, lifetime) + end + end + -- Fire LengthChanged QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "LengthChanged", casts[i], lastPosition, rayDir.Unit, rayDisplacement, currentVelocity, bullet) @@ -442,6 +533,14 @@ local function UpdateCasts(deltaTime: number) QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "Pierced", casts[i], subResult, subHitVelocity, bullet) subLastPoint = subResult.Position + + if visualize and visualizeSettings then + local lifetime = visualizeSettings.Debug_RayLifetime or 1 + local hitVis = VisualizeHit(CFrame.new(subResult.Position), true, visualizeSettings, lifetime) + if hitVis then + hitVis.Color3 = DBG_RAYPIERCE_SUB_COLOR + end + end else if DebugLogging.RayPierce then print("Piercing function is nil or it returned FALSE to not pierce this hit.") @@ -466,6 +565,15 @@ local function UpdateCasts(deltaTime: number) end QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "Hit", casts[i], subResult, subHitVelocity, bullet) + + if visualize and visualizeSettings then + local lifetime = visualizeSettings.Debug_RayLifetime or 1 + local hitVis = VisualizeHit(CFrame.new(subResult.Position), false, visualizeSettings, lifetime) + if hitVis then + hitVis.Color3 = DBG_HIT_SUB_COLOR + end + end + table.insert(destroyedIds, i) break end @@ -487,6 +595,10 @@ local function UpdateCasts(deltaTime: number) castDistanceCovered[i] += rayDisplacement QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "Pierced", casts[i], result, currentVelocity, bullet) + + if visualize and visualizeSettings then + VisualizeHit(CFrame.new(result.Position), true, visualizeSettings, visualizeSettings.Debug_HitLifetime or 1) + end else if DebugLogging.RayPierce then print("Piercing function is nil or it returned FALSE to not pierce this hit.") @@ -497,6 +609,11 @@ local function UpdateCasts(deltaTime: number) end QueueFire(caster, castEventsConfig[i], castEventsModuleConfig[i], castEventsModule[i], "Hit", casts[i], result, currentVelocity, bullet) + + if visualize and visualizeSettings then + VisualizeHit(CFrame.new(result.Position), false, visualizeSettings, visualizeSettings.Debug_HitLifetime or 1) + end + table.insert(destroyedIds, i) end end From 0df246c8789af9ad6999af2dfafca579edb3f440 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Mon, 11 May 2026 02:59:20 +0000 Subject: [PATCH 56/62] Add .md files --- ...omparison_activecast_parallelsimulation.md | 192 ++++++++++++++++++ skills/edge-cases-analysis.md | 145 +++++++++++++ 2 files changed, 337 insertions(+) create mode 100644 skills/comparison_activecast_parallelsimulation.md create mode 100644 skills/edge-cases-analysis.md diff --git a/skills/comparison_activecast_parallelsimulation.md b/skills/comparison_activecast_parallelsimulation.md new file mode 100644 index 0000000..eaeb8e8 --- /dev/null +++ b/skills/comparison_activecast_parallelsimulation.md @@ -0,0 +1,192 @@ +# ActiveCast vs ParallelSimulation Comparison + +## Overview + +| Aspect | ActiveCast (Legacy) | ParallelSimulation (New) | +|--------|---------------------|--------------------------| +| **Lines** | 993 | 669 | +| **Pattern** | Object-oriented (table per cast) | SoA (Array of Structs) | +| **Execution** | Sequential (Heartbeat) | Parallel (ConnectParallel) | +| **Event System** | Direct firing via Output | Queued + Dispatched | +| **Movement** | BulkMoveTo / PivotTo | Motor6D + BulkMoveTo/PivotTo | + +--- + +## Core Architecture + +### ActiveCast (Legacy) +```lua +-- Each cast is a complete table with all data +local cast = { + StateInfo = { ... }, + RayInfo = { ... }, + Caster = ..., + CFrame = ..., + ID = ... +} +``` + +### ParallelSimulation (New) +```lua +-- SoA pattern: separate arrays for each field +local castOrigin = {} :: { [number]: Vector3 } +local castVelocity = {} :: { [number]: Vector3 } +local castAcceleration = {} :: { [number]: Vector3 } +-- ... etc +``` + +--- + +## Key Differences + +### 1. Cast Registration + +**ActiveCast:** +- `createCastData()` function creates full cast table +- Sets up Stepped connection internally (commented out) +- No parallel registration + +**ParallelSimulation:** +- `Register(cast)` extracts data from cast table into SoA arrays +- Assigns numeric ID for array indexing +- Initializes Motor6D if needed +- Queues CastFire event + +### 2. Simulation Loop + +**ActiveCast:** +- Single cast per frame via `Stepped()` function +- Uses `GetPositionAtTime()` and `GetVelocityAtTime()` +- Complex HighFidelity logic with sub-segments +- Direct event firing + +**ParallelSimulation:** +- `UpdateCasts(deltaTime)` iterates all casts +- Same physics math but batched +- HighFidelity logic inline in main loop +- Event queuing via `QueueFire()` + +### 3. Event Handling + +**ActiveCast:** +```lua +cast.Caster.Output:Fire("Hit", cast, resultOfCast, segmentVelocity, cosmeticBulletObject) +``` + +**ParallelSimulation:** +```lua +local function QueueFire(caster, eventsConfig, eventsModuleConfig, eventsModule, eventName, ...) + -- queues event +end + +local function DispatchAllEvents() + -- dispatches all queued events after frame +end +``` + +### 4. Visualization + +**ActiveCast:** +- Built-in functions: `DbgVisualizeRaySegment`, `DbgVisualizeBlockSegment`, `DbgVisualizeSphereSegment`, `DbgVisualizeHit` +- Uses `task.synchronize()` inside simulation + +**ParallelSimulation:** +- Separate functions: `VisualizeRaySegment`, `VisualizeBlockSegment`, etc. +- Uses `task.synchronize()` only when visualization enabled + +### 5. Movement Methods + +**ActiveCast:** +- `MovementMethod` stored but limited implementation +- Direct CFrame assignment + +**ParallelSimulation:** +- Full Motor6D support via `Motor6DPool` +- Conditional movement: Motor6D.Transform vs bullet.CFrame vs bullet:PivotTo() + +--- + +## Physics Comparison + +### Position Calculation (Identical) +```lua +-- Both use same formula +local force = Vector3.new( + (accel.X * t ^ 2) / 2, + (accel.Y * t ^ 2) / 2, + (accel.Z * t ^ 2) / 2 +) +return origin + (velocity * t) + force +``` + +### Velocity Calculation (Identical) +```lua +-- Both use same formula +return velocity + accel * time +``` + +### Ray Direction Calculation (Identical) +```lua +local rayDir = displacement.Unit * currentVelocity.Magnitude * deltaTime +``` + +--- + +## HighFidelity Logic + +### ActiveCast +- Checks `HighFidelityBehavior.Automatic` + segment size +- Full sub-segment loop with detailed event handling +- Cancel/resume logic + +### ParallelSimulation +- Same conditions: `highFidelityBehavior == HighFidelityBehavior.Automatic` +- Same segment calculation logic +- Same piercing check flow + +--- + +## Event Config + +### ActiveCast +```lua +FastCastEventsModuleConfig = { + UseLengthChanged = behavior.FastCastEventsModuleConfig.UseLengthChanged, + UseHit = behavior.FastCastEventsModuleConfig.UseHit, + -- ... +} +``` + +### ParallelSimulation +```lua +castEventsConfig[id] = cast.StateInfo.FastCastEventsConfig +castEventsModuleConfig[id] = cast.StateInfo.FastCastEventsModuleConfig +``` + +--- + +## Known Differences to Check + +1. **Cast Type Handling**: ActiveCast has explicit type handling via `CastVariantTypes`, ParallelSimulation converts string to enum + +2. **Cosmetic Bullet**: ActiveCast handles ObjectCache differently than ParallelSimulation + +3. **Automatic Performance**: ActiveCast has commented-out automatic performance adjustment code + +4. **Motor6D Pool**: ParallelSimulation has dedicated Motor6D pooling, ActiveCast does not + +5. **Parallel Execution**: ParallelSimulation uses `RS.PreRender:ConnectParallel` on client, ActiveCast uses `RS.PreSimulation` or `RS.Heartbeat` + +--- + +## Potential Issues When Editing + +1. **Event Dispatch Timing**: ParallelSimulation queues events and dispatches at end of frame - ensure timing is correct + +2. **Thread Safety**: Parallel execution requires `task.synchronize()` before any Roblox API calls + +3. **ID Management**: When unregistering casts, ParallelSimulation swaps with last element - must update cast.ID correctly + +4. **Motor6D Cleanup**: Must disconnect Motor6D when cast terminates to avoid leaks + +5. **Pierce Function Storage**: `castCanPierceFn` stores CanPierce callback - verify it's properly copied from cast \ No newline at end of file diff --git a/skills/edge-cases-analysis.md b/skills/edge-cases-analysis.md new file mode 100644 index 0000000..49c0885 --- /dev/null +++ b/skills/edge-cases-analysis.md @@ -0,0 +1,145 @@ +# Edge Cases Analysis - FastCast2 Implementation + +## Critical Bugs Found + +### 1. BaseCastSerial.luau - Double `self` reference (Lines 106, 109) + +```lua +-- Line 106 - BUG +local cast = ActiveCastSerial.new(self.self.ParentCaster, castData) + +-- Line 109-110 - BUG +if self.self.Output then + self.self.Output:Fire("CastFire", cast, Origin, Direction, Velocity, Behavior) +``` + +**Fix:** Should be `self.ParentCaster` and `self.Output`. + +--- + +### 2. ActiveCastSerial.new() - Missing Event Configs + +`ActiveCastSerial.new()` doesn't include these required fields: + +```lua +-- Missing from StateInfo: +StateInfo = { + -- existing fields... + FastCastEventsConfig = { + UseLengthChanged = true, + UseHit = true, + UsePierced = true, + UseCastTerminating = true, + UseCastFire = true + }, + FastCastEventsModuleConfig = { + UseLengthChanged = true, + UseHit = true, + UsePierced = true, + UseCastTerminating = true, + UseCanPierce = true, + UseCastFire = true + }, + Behavior = behavior -- Required by SerialSimulation.Register() +} +``` + +And RayInfo missing: +```lua +RayInfo = { + -- existing fields... + FastCastEventsModule = nil -- Required +} +``` + +--- + +### 3. Motor6D Not Working in Serial Mode + +**Issue:** SerialSimulation.Register() checks for `MovementMethod == "Transform"` to connect Motor6D: +```lua +-- SerialSimulation.luau lines 265-269 +if cast.RayInfo.MovementMethod == "Transform" then + Motor6DPool.Initialize() + castMotor6D[id] = Motor6DPool.Connect(id, cast.RayInfo.CosmeticBulletObject :: any) +end +``` + +But: +- BaseCastSerial doesn't pass `MovementMethod` in castData (uses default "BulkMoveTo") +- ActiveCastSerial.new() doesn't initialize Motor6D either + +**Fix:** Ensure Motor6D pool works properly in Serial mode or remove Transform option. + +--- + +### 4. Duplicate Function Definitions + +Both simulation files define functions twice: + +**ParallelSimulation.luau:** +- `DispatchEvent` - defined at lines 177-180 AND 216-219 +- `DispatchAllEvents` - defined at lines 183-188 AND 222-227 + +**SerialSimulation.luau:** +- `QueueEvent` defined (line 173-177) but never used + +--- + +### 5. ObjectCache Not Implemented + +BaseCastSerial has `self.ObjectCache` but doesn't use it: + +```lua +-- BaseCastSerial.luau lines 83-88 (instead of using ObjectCache) +local cosmeticBullet = Behavior.CosmeticBulletTemplate +if cosmeticBullet then + cosmeticBullet = cosmeticBullet:Clone() + cosmeticBullet.CFrame = CFrame.new(Origin, Origin + Direction) + cosmeticBullet.Parent = Behavior.CosmeticBulletContainer +end +``` + +--- + +## Missing API Compliance (per docs/api-reference.md) + +| API Method | BaseCast | BaseCastSerial | Status | +|------------|----------|----------------|--------| +| GetVelocityCast | ❌ | ❌ | Missing | +| GetAccelerationCast | ❌ | ❌ | Missing | +| GetPositionCast | ❌ | ❌ | Missing | +| SetVelocityCast | ❌ | ❌ | Missing | +| SetAccelerationCast | ❌ | ❌ | Missing | +| SetPositionCast | ❌ | ❌ | Missing | +| AddPositionCast | ❌ | ❌ | Missing | +| AddVelocityCast | ❌ | ❌ | Missing | +| AddAccelerationCast | ❌ | ❌ | Missing | +| SyncChangesToCast | ❌ | ❌ | Missing | +| SetBulkMoveEnabled | ✅ | ⚠️ Empty | BaseCast OK, BaseCastSerial stub | +| SetObjectCacheEnabled | ✅ | ⚠️ Incomplete | BaseCast OK, BaseCastSerial stub | + +--- + +## Summary + +### Must Fix (Blocking) +1. **BaseCastSerial** - Fix `self.self` → `self` references +2. **ActiveCastSerial.new()** - Add FastCastEventsConfig, FastCastEventsModuleConfig, Behavior, and FastCastEventsModule + +### Should Fix +3. Motor6D initialization in ActiveCastSerial for Transform method +4. Remove duplicate function definitions +5. Implement ObjectCache in BaseCastSerial +6. Add missing cast manipulation methods (Get/Set/Add velocity/position/acceleration) + +--- + +## Parallel vs Serial Differences + +| Feature | ParallelSimulation | SerialSimulation | +|---------|-------------------|------------------| +| RunService | PreRender:ConnectParallel | Heartbeat:Connect | +| task.synchronize | Required for visualization | Not needed | +| Motor6D Pool | ✅ Working | ✅ Referenced | +| Auto-start | ✅ Line 667 | ✅ Line 652 | \ No newline at end of file From 029690e7b1459670802a34758d7d67a99872a02d Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Mon, 11 May 2026 03:11:08 +0000 Subject: [PATCH 57/62] fix: remove legacy code --- src/FastCast2/init.luau | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/FastCast2/init.luau b/src/FastCast2/init.luau index 88b49a7..88b8f69 100644 --- a/src/FastCast2/init.luau +++ b/src/FastCast2/init.luau @@ -422,7 +422,6 @@ Gets the velocity of an ActiveCast. @return Vector3 -- The current velocity of the ActiveCast. ]=] function FastCastParallel:GetVelocityCast(cast: vaildcast) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") local currentTrajectory = cast.StateInfo.Trajectory return GetVelocityAtTime( cast.StateInfo.TotalRuntime - currentTrajectory.StartTime, @@ -442,7 +441,6 @@ Gets the acceleration of an ActiveCast. ]=] function FastCastParallel:GetAccelerationCast(cast: vaildcast) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") local currentTrajectory = cast.StateInfo.Trajectory return currentTrajectory.Acceleration end @@ -457,7 +455,6 @@ Gets the position of an ActiveCast. @return Vector3 -- The current position of the ActiveCast. ]=] function FastCastParallel:GetPositionCast(cast: vaildcast) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") local currentTrajectory = cast.StateInfo.Trajectory return GetPositionAtTime( cast.StateInfo.TotalRuntime - currentTrajectory.StartTime, @@ -478,7 +475,6 @@ Sets the velocity of an ActiveCast to the specified Vector3. ]=] function FastCastParallel:SetVelocityCast(cast: vaildcast, velocity: Vector3) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") ModifyTransformation(cast, velocity, nil, nil) end @@ -493,7 +489,6 @@ Sets the acceleration of an ActiveCast to the specified Vector3. ]=] function FastCastParallel:SetAccelerationCast(cast: vaildcast, acceleration: Vector3) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") ModifyTransformation(cast, nil, acceleration, nil) end @@ -506,7 +501,6 @@ end @within FastCastParallel ]=] function FastCastParallel:SetPositionCast(cast: vaildcast, position: Vector3) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") ModifyTransformation(cast, nil, nil, position) end @@ -521,7 +515,6 @@ Pauses or resumes simulation for an ActiveCast. ]=] function FastCastParallel:PauseCast(cast: vaildcast, value: boolean) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") cast.StateInfo.Paused = value end @@ -536,7 +529,6 @@ Add position to an ActiveCast with the specified Vector3. ]=] function FastCastParallel:AddPositionCast(cast: vaildcast, position: Vector3) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") self:SetPositionCast(cast, self:GetPositionCast(cast) + position) end @@ -551,7 +543,6 @@ Add velocity to an ActiveCast with the specified Vector3. ]=] function FastCastParallel:AddVelocityCast(cast: vaildcast, velocity: Vector3) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") self:SetVelocityCast(cast, self:GetVelocityCast(cast) + velocity) end @@ -566,7 +557,6 @@ Add acceleration to an ActiveCast with the specified Vector3. ]=] function FastCastParallel:AddAccelerationCast(cast: vaildcast, acceleration: Vector3) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") self:SetAccelerationCast(cast, self:GetAccelerationCast(cast) + acceleration) end @@ -580,7 +570,6 @@ Synchronize new changes to the ActiveCast. ]=] function FastCastParallel:SyncChangesToCast(cast: vaildcast) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") cast.Caster.SyncChange:Fire(cast) end From 5819065619c941950f35403948ff17d54cebc6c4 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Mon, 11 May 2026 03:13:09 +0000 Subject: [PATCH 58/62] resolve: coderabbit 106-109 --- src/FastCast2/BaseCastSerial.luau | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/FastCast2/BaseCastSerial.luau b/src/FastCast2/BaseCastSerial.luau index 73d440a..f1c0c14 100644 --- a/src/FastCast2/BaseCastSerial.luau +++ b/src/FastCast2/BaseCastSerial.luau @@ -106,8 +106,8 @@ function BaseCastSerial:Raycast( local cast = ActiveCastSerial.new(self.self.ParentCaster, castData) SerialSimulation.Register(cast) - if self.self.Output then - self.self.Output:Fire("CastFire", cast, Origin, Direction, Velocity, Behavior) + if self.Output then + self.Output:Fire("CastFire", cast, Origin, Direction, Velocity, Behavior) end end From d1273a2c25296f5e9e2028d4929d0e698fa0d9dd Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Mon, 11 May 2026 03:22:16 +0000 Subject: [PATCH 59/62] fix: remove legacy code --- src/FastCast2/init.luau | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/FastCast2/init.luau b/src/FastCast2/init.luau index 88b8f69..93665f8 100644 --- a/src/FastCast2/init.luau +++ b/src/FastCast2/init.luau @@ -886,26 +886,11 @@ function FastCastSerial:AddAccelerationCast(cast: vaildcast, acceleration: Vecto self:SetAccelerationCast(cast, self:GetAccelerationCast(cast) + acceleration) end ---[=[ - @method SyncChangesToCast - @within FastCastSerial -]=] -function FastCastSerial:SyncChangesToCast(cast: vaildcast) - if self.BaseCast.SyncChange then - self.BaseCast.SyncChange:Fire(cast) - end -end - --[=[ @method TerminateCast @within FastCastSerial ]=] function FastCastSerial:TerminateCast(cast: vaildcast, castTerminatingFunction: TypeDef.OnCastTerminatingFunction?) - if cast.StateInfo.UpdateConnection ~= nil then - cast.StateInfo.UpdateConnection:Disconnect() - cast.StateInfo.UpdateConnection = nil - end - if cast.RayInfo.CosmeticBulletObject then cast.RayInfo.CosmeticBulletObject:Destroy() cast.RayInfo.CosmeticBulletObject = nil From 6bb678e2777240dfdb0f583a3c6d535e1b0931af Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Mon, 11 May 2026 03:23:50 +0000 Subject: [PATCH 60/62] fix: unneeded local variable --- src/FastCast2/init.luau | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/FastCast2/init.luau b/src/FastCast2/init.luau index 93665f8..1d14db9 100644 --- a/src/FastCast2/init.luau +++ b/src/FastCast2/init.luau @@ -799,8 +799,7 @@ end @within FastCastSerial ]=] function FastCastSerial:GetAccelerationCast(cast: vaildcast): Vector3 - local latestTrajectory = cast.StateInfo.Trajectory - return latestTrajectory.Acceleration + return cast.StateInfo.Trajectory end --[=[ From 458896d859dd41345bef4810d7d94b89548aa82a Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Mon, 11 May 2026 03:24:42 +0000 Subject: [PATCH 61/62] fix: Add .Acceleration --- src/FastCast2/init.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FastCast2/init.luau b/src/FastCast2/init.luau index 1d14db9..6aec03b 100644 --- a/src/FastCast2/init.luau +++ b/src/FastCast2/init.luau @@ -799,7 +799,7 @@ end @within FastCastSerial ]=] function FastCastSerial:GetAccelerationCast(cast: vaildcast): Vector3 - return cast.StateInfo.Trajectory + return cast.StateInfo.Trajectory.Acceleration end --[=[ From 4afb6e8c151f5df363649813db3b829abb31c806 Mon Sep 17 00:00:00 2001 From: Mawin Chuangkud Date: Mon, 11 May 2026 10:43:38 +0700 Subject: [PATCH 62/62] Update init.luau --- src/FastCast2/init.luau | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/FastCast2/init.luau b/src/FastCast2/init.luau index 88b49a7..c2c09f5 100644 --- a/src/FastCast2/init.luau +++ b/src/FastCast2/init.luau @@ -1021,10 +1021,6 @@ end Creates a new Serial Caster. A Serial Caster runs all cast simulations on the main thread and is simpler to use but less performant than [FastCast.newParallel](FastCast#newParallel). - :::tip - For most use cases, especially when you need high performance, consider using [FastCast.newParallel](FastCast#newParallel) instead. - ::: - @function new @within FastCast @@ -1072,4 +1068,4 @@ function FastCast.newParallel() return fp end -return FastCast \ No newline at end of file +return FastCast