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..bc69165 --- /dev/null +++ b/Benchmarks/benchSerial.client.luau @@ -0,0 +1,147 @@ +-- 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 + 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() * 2 - 1, + math.random() * 2 - 1, + math.random() * 2 - 1 + ) * 5000, + direction, + 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/README.md b/README.md index d1fadee..b689dbc 100644 --- a/README.md +++ b/README.md @@ -63,24 +63,39 @@ 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 .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 +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")) +local FastCastEnums = require(Rep:WaitForChild("FastCast2"):WaitForChild("FastCastEnums")) -- CONSTANTS local SPEED = 500 @@ -89,93 +104,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) @@ -216,7 +219,7 @@ module.CastTerminating = function() print("CastTerminating!") end -module.RayHit = function() +module.Hit = function() print("Hit!") end diff --git a/TODO.md b/TODO.md index ef307dd..9e53ece 100644 --- a/TODO.md +++ b/TODO.md @@ -1,8 +1,15 @@ -- [ ] 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 -- [ ] Add benchmarks -- [ ] Refactor +- [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 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 +- [x] Fix HighFidelityBehavior = 2 bug - subRayDir used delta instead of timeIncrement +- [x] ActiveCast.Trajectories -> ActiveCast.Trajectory +- [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/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 diff --git a/docs/changelog.md b/docs/changelog.md index add4d87..c2db7d4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -7,7 +7,37 @@ 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/). + +--- + +## [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 --- diff --git a/skills/architecture.md b/skills/architecture.md new file mode 100644 index 0000000..8d67916 --- /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` and `FastCastVMs/ClientVM.client.luau` - Actors +- 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** 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 diff --git a/src/FastCast2/ActiveCast.luau b/src/FastCast2/ActiveCast.luau index 4dfb471..fcb9861 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 @@ -110,6 +110,7 @@ local function DebrisAdd(obj: Instance, Lifetime: number) end if Lifetime <= 0 then obj:Destroy() + return end task.delay(Lifetime, function() @@ -160,7 +161,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 +173,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 ]] @@ -352,34 +353,36 @@ end cast.Caster.Output:Fire("CastFire", cast, origin, direction, velocity, behavior) end]] +--[[ local function SimulateCast( cast: any, delta: number, FastCastEvents: TypeDef.FastCastEvents, variant: CastVariants ) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") - --PrintDebug("Casting for frame.") --print("1C") if DebugLogging.Casting then print("Casting for frame.") end - local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] + local trajectory = cast.StateInfo.Trajectory + if typeof(trajectory.Acceleration) ~= "Vector3" then + trajectory.Acceleration = Vector3.new() + end - 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 - 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) @@ -415,8 +418,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 = {} @@ -556,7 +559,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 @@ -671,6 +674,7 @@ local function SimulateCast( DbgVisualizeHit(CFrame.new(currentTarget), false, VisualizeCasts, VisualizeCastSettings) end end +--]] --[=[ @function createCastData @@ -725,7 +729,6 @@ function ActiveCast.createCastData( Caster = BaseCast, StateInfo = { - UpdateConnection = nil, Paused = false, TotalRuntime = 0, DistanceCovered = 0, @@ -734,14 +737,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, @@ -767,7 +768,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 = {}, @@ -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 @@ -855,10 +857,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 @@ -884,10 +886,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) @@ -895,7 +897,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) @@ -983,12 +985,9 @@ function ActiveCast.createCastData( end end end - - cast.StateInfo.UpdateConnection = event:ConnectParallel(Stepped) + --]] return cast end --- Will I ever be free - -return ActiveCast +return ActiveCast \ No newline at end of file diff --git a/src/FastCast2/ActiveCastSerial.luau b/src/FastCast2/ActiveCastSerial.luau new file mode 100644 index 0000000..cb61f62 --- /dev/null +++ b/src/FastCast2/ActiveCastSerial.luau @@ -0,0 +1,161 @@ +--[[ + - Author : Mawin CK + - Date : 2025 + + + ActiveCastSerial - Serial mode with single RunService, SoA pattern, queue technique + Similar to SwiftCast implementation +]] + +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 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 + +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 CastVariant = { CastType: number, Size: Vector3?, Radius: number? } + +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, 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 +} + +local ActiveCastSerial = {} + +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 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() + end) +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, 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 = length + adornment.Color3 = settings.Debug_SegmentColor + adornment.Radius = settings.Debug_SegmentSize + adornment.Transparency = settings.Debug_SegmentTransparency + adornment.Parent = GetFastCastVisualizationContainer() + DebrisAdd(adornment, settings.Debug_RayLifetime) +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 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, settings.Debug_HitLifetime) +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 ActiveCastSerial.new(caster: any, castData: any): any + return { + Caster = caster, + StateInfo = { + Paused = false, + TotalRuntime = 0, + DistanceCovered = 0, + HighFidelitySegmentSize = castData.HighFidelitySegmentSize, + HighFidelityBehavior = castData.HighFidelityBehavior, + IsActivelyResimulating = false, + CancelHighResCast = false, + Trajectory = { + StartTime = 0, + EndTime = -1, + Origin = castData.Origin, + InitialVelocity = castData.Velocity, + Acceleration = castData.Acceleration, + }, + VisualizeCasts = castData.VisualizeCasts, + VisualizeCastSettings = castData.VisualizeCastSettings + }, + + RayInfo = { + Parameters = castData.RaycastParams and CloneCastParams(castData.RaycastParams) or RaycastParams.new(), + WorldRoot = workspace, + MaxDistance = castData.MaxDistance or DEFAULT_MAX_DISTANCE, + CosmeticBulletObject = castData.CosmeticBulletObject, + MovementMethod = castData.MovementMethod or "BulkMoveTo", + Size = castData.Size, + Radius = castData.Radius + }, + + Type = CastVariantTypes[castData.CastType], + CFrame = CFrame.new(castData.Origin), + ID = castData.ID + } +end + +return ActiveCastSerial \ No newline at end of file diff --git a/src/FastCast2/BaseCast.luau b/src/FastCast2/BaseCast.luau index c0b98fc..2807ff3 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 @@ -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,14 @@ function BaseCast:Raycast( CastType = EnumCastTypes.Raycast } :: any) + ParallelSimulation.Register(cast) + Actives[cast.ID] = 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 +236,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 +246,14 @@ function BaseCast:Blockcast( Size = Size } :: any) + ParallelSimulation.Register(cast) + Actives[cast.ID] = 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 +281,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 +291,14 @@ function BaseCast:Spherecast( Radius = Radius } :: any) + ParallelSimulation.Register(cast) + Actives[cast.ID] = 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/BaseCastSerial.luau b/src/FastCast2/BaseCastSerial.luau new file mode 100644 index 0000000..f1c0c14 --- /dev/null +++ b/src/FastCast2/BaseCastSerial.luau @@ -0,0 +1,282 @@ +--[[ + - Author : Mawin CK + - Date : 2025 + + + BaseCastSerial - Uses SerialSimulation with SoA pattern +]] + +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 SerialSimulation = require(FastCast2:WaitForChild("SerialSimulation")) + +local EnumCastTypes = FastCastEnums.CastType + +--[=[ + @class BaseCastSerial + + Base class for Serial (non-parallel) Raycast operations. + Uses SerialSimulation with SoA pattern for performance. +]=] + +local BaseCastSerial = {} +BaseCastSerial.__index = BaseCastSerial +BaseCastSerial.__type = "BaseCastSerial" + +--[=[ + @function Init + @within BaseCastSerial +]=] +function BaseCastSerial.Init(BindableOutput: BindableEvent, Data: any, parentCaster: any) + local self = setmetatable({}, BaseCastSerial) + self.Output = BindableOutput + self.ParentCaster = parentCaster + self.ObjectCache = nil + self.BulkMoveToConnection = nil + self.NextProjectileID = 0 + + if Data.useBulkMoveTo then + -- BulkMoveTo is handled by SerialSimulation + 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 +]=] +function BaseCastSerial:Raycast( + Origin: Vector3, + Direction: Vector3, + Velocity: Vector3 | number, + Behavior: TypeDef.FastCastBehavior +) + self.NextProjectileID += 1 + + 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 = self.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, + MovementMethod = Behavior.MovementMethod or "BulkMoveTo" + } + + local cast = ActiveCastSerial.new(self.self.ParentCaster, castData) + SerialSimulation.Register(cast) + + if self.Output then + self.Output:Fire("CastFire", cast, 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 +) + self.NextProjectileID += 1 + + 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 = self.NextProjectileID, + Origin = Origin, + Velocity = Velocity, + Acceleration = Behavior.Acceleration, + RaycastParams = raycastParams, + MaxDistance = Behavior.MaxDistance or 1000, + CosmeticBulletObject = cosmeticBullet, + CastType = EnumCastTypes.Blockcast, + Size = Size, + VisualizeCasts = Behavior.VisualizeCasts, + VisualizeCastSettings = Behavior.VisualizeCastSettings, + HighFidelitySegmentSize = Behavior.HighFidelitySegmentSize, + HighFidelityBehavior = Behavior.HighFidelityBehavior, + MovementMethod = Behavior.MovementMethod or "BulkMoveTo" + } + + local cast = ActiveCastSerial.new(self.ParentCaster, castData) + SerialSimulation.Register(cast) + + if self.Output then + self.Output:Fire("CastFire", cast, 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 +) + self.NextProjectileID += 1 + + 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 = self.NextProjectileID, + Origin = Origin, + Velocity = Velocity, + Acceleration = Behavior.Acceleration, + RaycastParams = raycastParams, + MaxDistance = Behavior.MaxDistance or 1000, + CosmeticBulletObject = cosmeticBullet, + CastType = EnumCastTypes.Spherecast, + Radius = Radius, + VisualizeCasts = Behavior.VisualizeCasts, + VisualizeCastSettings = Behavior.VisualizeCastSettings, + HighFidelitySegmentSize = Behavior.HighFidelitySegmentSize, + HighFidelityBehavior = Behavior.HighFidelityBehavior, + MovementMethod = Behavior.MovementMethod or "BulkMoveTo" + } + + local cast = ActiveCastSerial.new(self.ParentCaster, castData) + SerialSimulation.Register(cast) + + if self.Output then + self.Output:Fire("CastFire", cast, Origin, Direction, Velocity, Behavior) + end +end + +--[=[ + @method BindBulkMoveTo + @within BaseCastSerial +]=] +function BaseCastSerial:BindBulkMoveTo(enabled: boolean) + -- BulkMoveTo is now handled by SerialSimulation directly +end + +--[=[ + @method BindObjectCache + @within BaseCastSerial +]=] +function BaseCastSerial:BindObjectCache(bool: boolean) + if bool then + if self.ObjectCache then return end + self.ObjectCache = Instance.new("BindableFunction") + self.ObjectCache.Name = "ObjectCache" + else + if self.ObjectCache then + self.ObjectCache:Destroy() + self.ObjectCache = nil + end + end +end + +--[=[ + @method TerminateCast + @within BaseCastSerial +]=] +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 self.Output then + self.Output:Fire("CastTerminating", cast) + end +end + +--[=[ + @method Destroy + @within BaseCastSerial +]=] +function BaseCastSerial:Destroy() + if self.BulkMoveToConnection then + self.BulkMoveToConnection:Disconnect() + self.BulkMoveToConnection = nil + end + + self.Output = nil + self.ParentCaster = nil + setmetatable(self, nil) +end + +return BaseCastSerial \ No newline at end of file diff --git a/src/FastCast2/Configs.luau b/src/FastCast2/Configs.luau index a2e91b8..70715bb 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 @@ -13,6 +13,7 @@ Configs.DebugLogging = { Hit = false, RayPierce = false, Calculation = false, + AutomaticPerformance = false, } Configs.VisualizeCasts = true diff --git a/src/FastCast2/DefaultConfigs.luau b/src/FastCast2/DefaultConfigs.luau index 9dbc1f2..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 @@ -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/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 new file mode 100644 index 0000000..97feb14 --- /dev/null +++ b/src/FastCast2/Motor6DPool.luau @@ -0,0 +1,87 @@ +--[[ + - Author : Mawin CK + - Date : 2025 + + + 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 + GrowPool(PoolSize * GROWTH_RATE) + end + return table.remove(FreeMotor6Ds) :: Motor6D +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/ParallelSimulation.luau b/src/FastCast2/ParallelSimulation.luau new file mode 100644 index 0000000..d9304af --- /dev/null +++ b/src/FastCast2/ParallelSimulation.luau @@ -0,0 +1,669 @@ +--[[ + - Author : Mawin CK + - Date : 2025 + + + 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 Configs = require(FastCastModule:WaitForChild("Configs")) +local DebugLogging = Configs.DebugLogging +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 +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 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) + 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 + +-- 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 } +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 } } } + +local function QueueEvent(callback: any, ...) + if callback then + table.insert(QueuedEvents, { Callback = callback, Args = { ... } }) + 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 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 +} + +function ParallelSimulation.Register(cast: any) + castCount += 1 + local id = castCount + + casts[id] = cast + castIDs[id] = cast.ID + 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 + 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 + castCanPierceFn[id] = nil + + if cast.CanPierce then + 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 + if cast.RayInfo.Radius then + 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 + + 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) + 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] + 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 + casts[id] = casts[lastId] + end + end + + if id ~= lastId and castMotor6D[lastId] then + Motor6DPool.Disconnect(castMotor6D[lastId]) + 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 + castMotor6D[lastId] = nil + castCanPierceFn[lastId] = nil + castEventsConfig[lastId] = nil + castEventsModuleConfig[lastId] = nil + castEventsModule[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 + + QueueFire(castCaster[id], castEventsConfig[id], castEventsModuleConfig[id], castEventsModule[id], "CastTerminating", casts[id]) + + ParallelSimulation.Unregister(id) +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 + if castPaused[i] then continue end + + local caster = castCaster[i] + local castType = castCastType[i] + local CastHandler = castHandlers[castType] + local highFidelityBehavior = castHighFidelityBehavior[i] + local highFidelitySegmentSize = castHighFidelitySegmentSize[i] + local canPierceFn = castCanPierceFn[i] + + 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 + local hitNormal = Vector3.new() + if result then + hitPoint = result.Position + hitPart = result.Instance + hitNormal = result.Normal + 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] + local motor6d = castMotor6D[i] + if bullet then + if motor6d then + motor6d.Transform = newCFrame + elseif bullet:IsA("BasePart") then + bullet.CFrame = newCFrame + else + bullet:PivotTo(newCFrame) + 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) + + -- 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 + 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 + + 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 + castCancelHighResCast[i] = false + 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) + 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) + + 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) + 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, 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.") + end + + 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, 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 + else + subLastPoint = subPosition + end + end + + if castIsActivelyResimulating[i] then + castIsActivelyResimulating[i] = false + end + else + 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) + + 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.") + 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) + + 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 + end + + -- Check max distance + if castDistanceCovered[i] >= castMaxDistance[i] then + table.insert(destroyedIds, i) + end + end + + -- 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 + if RS:IsClient() then + ParallelSimulation.StepConnection = RS.PreRender:ConnectParallel(UpdateCasts) + else + ParallelSimulation.StepConnection = RS.Heartbeat:Connect(UpdateCasts) + end +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 diff --git a/src/FastCast2/SerialSimulation.luau b/src/FastCast2/SerialSimulation.luau new file mode 100644 index 0000000..a79f221 --- /dev/null +++ b/src/FastCast2/SerialSimulation.luau @@ -0,0 +1,654 @@ +--[[ + - Author : Mawin CK + - Date : 2025 + + + SerialSimulation - Single RunService handling multiple ActiveCastSerial + Uses SoA pattern for performance, queue technique for events + Like SwiftCast implementation +]] + +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 Configs = require(FastCastModule:WaitForChild("Configs")) +local DebugLogging = Configs.DebugLogging +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 +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 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) + 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 + +-- SoA Arrays +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 } +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 } } } + +local function QueueEvent(callback: any, ...) + if callback then + table.insert(QueuedEvents, { Callback = callback, Args = { ... } }) + 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 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 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.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 + 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 + castCanPierceFn[id] = nil + + if cast.CanPierce then + 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 + if cast.RayInfo.Radius then + 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 + + 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) + 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] + 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 + end + end + + -- Return motor6d to pool + if castMotor6D[id] then + Motor6DPool.Disconnect(castMotor6D[id]) + 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 + castMotor6D[lastId] = nil + castCanPierceFn[lastId] = nil + castEventsConfig[lastId] = nil + castEventsModuleConfig[lastId] = nil + castEventsModule[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 + + QueueFire(castCaster[id], castEventsConfig[id], castEventsModuleConfig[id], castEventsModule[id], "CastTerminating", casts[id]) + + SerialSimulation.Unregister(id) +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 + if castPaused[i] then continue end + + local caster = castCaster[i] + local castType = castCastType[i] + local CastHandler = castHandlers[castType] + local highFidelityBehavior = castHighFidelityBehavior[i] + local highFidelitySegmentSize = castHighFidelitySegmentSize[i] + local canPierceFn = castCanPierceFn[i] + + 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 + local hitNormal = Vector3.new() + if result then + hitPoint = result.Position + hitPart = result.Instance + hitNormal = result.Normal + 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] + local motor6d = castMotor6D[i] + if bullet then + if motor6d then + motor6d.Transform = newCFrame + elseif bullet:IsA("BasePart") then + bullet.CFrame = newCFrame + else + bullet:PivotTo(newCFrame) + 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) + + -- 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 + 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 + + 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 + if castCancelHighResCast[i] then + castCancelHighResCast[i] = false + 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) + 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) + + 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) + 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, 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.") + end + + 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, 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 + else + subLastPoint = subPosition + end + end + + if castIsActivelyResimulating[i] then + castIsActivelyResimulating[i] = false + end + else + 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) + + 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.") + 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) + + if visualize and visualizeSettings then + VisualizeHit(CFrame.new(result.Position), false, visualizeSettings, visualizeSettings.Debug_HitLifetime or 1) + end + + table.insert(destroyedIds, i) + end + end + end + + -- Check max distance + if castDistanceCovered[i] >= castMaxDistance[i] then + table.insert(destroyedIds, i) + end + end + + -- Process destroyed casts (iterate in reverse to avoid index invalidation) + for i = #destroyedIds, 1, -1 do + SerialSimulation.Terminate(destroyedIds[i]) + end + + DispatchAllEvents() +end + +function SerialSimulation.Start() + if SerialSimulation.IsRunning then return end + SerialSimulation.IsRunning = true + SerialSimulation.StepConnection = RS.Heartbeat:Connect(UpdateCasts) +end + +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 diff --git a/src/FastCast2/TypeDefinitions.luau b/src/FastCast2/TypeDefinitions.luau index 886fde7..e6ebabd 100644 --- a/src/FastCast2/TypeDefinitions.luau +++ b/src/FastCast2/TypeDefinitions.luau @@ -3,7 +3,7 @@ --[[ - Author : Mawin CK - Date : 2025 - -- Verison : 0.0.9 + ]] --[=[ @@ -317,6 +317,7 @@ export type FastCastBehavior = { AutoIgnoreContainer: boolean, SimulateAfterPhysic: boolean, + MovementMethod: "BulkMoveTo" | "Transform", AutomaticPerformance: boolean, AdaptivePerformance: AdaptivePerformance, @@ -345,7 +346,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 +361,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 44dbca0..a35c862 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 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. -- @@ -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,19 +39,19 @@ 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 - Date : 2025 ]] --- 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,11 +63,13 @@ --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")) --local Configs = require(script:WaitForChild("Configs")) local ObjectCache = require(script:WaitForChild("ObjectCache")) +local BaseCastSerial = require(script:WaitForChild("BaseCastSerial")) --local SharedCasters = require(script:WaitForChild("SharedCasters")) @@ -84,14 +86,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 @@ -118,14 +125,15 @@ local function GetVelocityAtTime(time: number, initialVelocity: Vector3, acceler return initialVelocity + acceleration * time end +--[[ local function GetTrajectoryInfo( cast: vaildcast, index: number ): { [number]: Vector3 } - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") - local trajectories = cast.StateInfo.Trajectories - 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 @@ -133,10 +141,13 @@ local function GetTrajectoryInfo( return { GetPositionAtTime(duration, origin, vel, accel), GetVelocityAtTime(duration, vel, accel) } end +--]] +--[[ local function GetLatestTrajectoryEndInfo(cast: vaildcast): { [number]: Vector3 } - return GetTrajectoryInfo(cast, #cast.StateInfo.Trajectories) + return GetTrajectoryInfo(cast, 1) end +--]] local function ModifyTransformation( cast: vaildcast, @@ -144,46 +155,17 @@ 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 velocity == nil then - velocity = lastTrajectory.InitialVelocity - end - if acceleration == nil then - acceleration = lastTrajectory.Acceleration - end - if position == nil then - position = lastTrajectory.Origin - end + 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) - lastTrajectory.Origin = position - lastTrajectory.InitialVelocity = velocity - lastTrajectory.Acceleration = acceleration - else - lastTrajectory.EndTime = cast.StateInfo.TotalRuntime - - local point, velAtPoint = unpack(GetLatestTrajectoryEndInfo(cast)) - - 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.Trajectories, { - 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} @@ -203,42 +185,26 @@ end @return FastCastBehavior ]=] -function FastCast.newBehavior(): TypeDef.FastCastBehavior +function FastCastParallel.newBehavior(): TypeDef.FastCastBehavior return deepCopyTable(DefaultConfigs.FastCastBehavior) :: TypeDef.FastCastBehavior 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! + Creates a new FastCastBehavior for Serial Caster. + @function newBehavior + @within FastCastSerial - ::: - Contructs a new Caster object. - @function new - @within FastCast - @return Caster + @return FastCastBehavior ]=] -function FastCast.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, - FastCast - ) :: TypeDef.Caster +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! @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 +219,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 +316,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 +332,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 +349,7 @@ function FastCast:RaycastFire( error("Please Init caster") end if BehaviorData == nil then - BehaviorData = FastCast.newBehavior() + BehaviorData = FastCastParallel.newBehavior() end -- BABE RAYCAST!!!!! @@ -393,7 +359,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 +367,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 +378,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 +387,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 +395,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 +406,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,12 +418,11 @@ 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) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") - local currentTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] +function FastCastParallel:GetVelocityCast(cast: vaildcast) + local currentTrajectory = cast.StateInfo.Trajectory return GetVelocityAtTime( cast.StateInfo.TotalRuntime - currentTrajectory.StartTime, currentTrajectory.InitialVelocity, @@ -471,13 +436,12 @@ 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) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") - local currentTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] +function FastCastParallel:GetAccelerationCast(cast: vaildcast) + local currentTrajectory = cast.StateInfo.Trajectory return currentTrajectory.Acceleration end @@ -487,12 +451,11 @@ 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) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") - local currentTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories] +function FastCastParallel:GetPositionCast(cast: vaildcast) + local currentTrajectory = cast.StateInfo.Trajectory return GetPositionAtTime( cast.StateInfo.TotalRuntime - currentTrajectory.StartTime, currentTrajectory.Origin, @@ -508,11 +471,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) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") +function FastCastParallel:SetVelocityCast(cast: vaildcast, velocity: Vector3) ModifyTransformation(cast, velocity, nil, nil) end @@ -523,11 +485,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) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") +function FastCastParallel:SetAccelerationCast(cast: vaildcast, acceleration: Vector3) ModifyTransformation(cast, nil, acceleration, nil) end @@ -537,10 +498,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) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") +function FastCastParallel:SetPositionCast(cast: vaildcast, position: Vector3) ModifyTransformation(cast, nil, nil, position) end @@ -551,11 +511,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) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") +function FastCastParallel:PauseCast(cast: vaildcast, value: boolean) cast.StateInfo.Paused = value end @@ -566,11 +525,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) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") +function FastCastParallel:AddPositionCast(cast: vaildcast, position: Vector3) self:SetPositionCast(cast, self:GetPositionCast(cast) + position) end @@ -581,11 +539,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) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") +function FastCastParallel:AddVelocityCast(cast: vaildcast, velocity: Vector3) self:SetVelocityCast(cast, self:GetVelocityCast(cast) + velocity) end @@ -596,11 +553,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) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") +function FastCastParallel:AddAccelerationCast(cast: vaildcast, acceleration: Vector3) self:SetAccelerationCast(cast, self:GetAccelerationCast(cast) + acceleration) end @@ -610,11 +566,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) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") +function FastCastParallel:SyncChangesToCast(cast: vaildcast) cast.Caster.SyncChange:Fire(cast) end @@ -623,30 +578,21 @@ 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?) - assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") +function FastCastParallel:TerminateCast(cast: vaildcast, castTerminatingFunction: TypeDef.OnCastTerminatingFunction?) + local trajectory = cast.StateInfo.Trajectory + trajectory.EndTime = cast.StateInfo.TotalRuntime - local trajectories = cast.StateInfo.Trajectories - local lastTrajectory = trajectories[#trajectories] - lastTrajectory.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 @@ -657,13 +603,14 @@ 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) + return end self.Dispatcher:DispatchAll("BindBulkMoveTo", enabled) @@ -672,13 +619,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, @@ -712,12 +659,320 @@ function FastCast: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 + + 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 + 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.Trajectory + return GetVelocityAtTime( + cast.StateInfo.TotalRuntime - latestTrajectory.StartTime, + latestTrajectory.InitialVelocity, + latestTrajectory.Acceleration + ) +end + +--[=[ + @method GetAccelerationCast + @within FastCastSerial +]=] +function FastCastSerial:GetAccelerationCast(cast: vaildcast): Vector3 + return cast.StateInfo.Trajectory.Acceleration +end + +--[=[ + @method GetPositionCast + @within FastCastSerial +]=] +function FastCastSerial:GetPositionCast(cast: vaildcast): Vector3 + local latestTrajectory = cast.StateInfo.Trajectory + 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.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 + +--[=[ + @method SetAccelerationCast + @within FastCastSerial +]=] +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 + +--[=[ + @method SetPositionCast + @within FastCastSerial +]=] +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 + +--[=[ + @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) + self:SetPositionCast(cast, self:GetPositionCast(cast) + position) +end + +--[=[ + @method AddVelocityCast + @within FastCastSerial +]=] +function FastCastSerial:AddVelocityCast(cast: vaildcast, velocity: Vector3) + self:SetVelocityCast(cast, self:GetVelocityCast(cast) + velocity) +end + +--[=[ + @method AddAccelerationCast + @within FastCastSerial +]=] +function FastCastSerial:AddAccelerationCast(cast: vaildcast, acceleration: Vector3) + self:SetAccelerationCast(cast, self:GetAccelerationCast(cast) + acceleration) +end + +--[=[ + @method TerminateCast + @within FastCastSerial +]=] +function FastCastSerial:TerminateCast(cast: vaildcast, castTerminatingFunction: TypeDef.OnCastTerminatingFunction?) + if cast.RayInfo.CosmeticBulletObject then + cast.RayInfo.CosmeticBulletObject:Destroy() + cast.RayInfo.CosmeticBulletObject = nil + end + + if self.BaseCast then + self.BaseCast:TerminateCast(cast, castTerminatingFunction) + 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 - @within FastCast + @within FastCastParallel ]=] -function FastCast:Destroy() +function FastCastParallel:Destroy() if self.ObjectCache then self.ObjectCache:Destroy() end @@ -733,5 +988,57 @@ function FastCast:Destroy() setmetatable(self, nil) 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). + + @function new + @within FastCast + + @return Caster +]=] +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) + return fs +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(), + Hit = Signal.new(), + Pierced = Signal.new(), + CastTerminating = Signal.new(), + CastFire = Signal.new(), + WorldRoot = workspace, + Dispatcher = nil, + AlreadyInit = false + } + setmetatable(fp, FastCastParallel) + return fp +end -return FastCast \ No newline at end of file +return FastCast