Last active
February 21, 2026 17:53
-
-
Save imlogvn/80b3c7272dcb782456e0a427464141a5 to your computer and use it in GitHub Desktop.
ADVANCED KINETIC & VISUAL FRAMEWORK DEMO
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --[[ | |
| ADVANCED KINEMATIC PROJECTILE SYSTEM | |
| Developed as a application for HiddenDevs. | |
| This system prioritizes performance and mathematical accuracy over standard Roblox physics. | |
| By using a purely kinematic approach, we avoid the overhead of the 'PhysicsService' and | |
| ensure frame-rate independent behavior across all client hardware. | |
| --]] | |
| local RunService = game:GetService("RunService") | |
| local UserInputService = game:GetService("UserInputService") | |
| local TweenService = game:GetService("TweenService") | |
| local Debris = game:GetService("Debris") | |
| local Players = game:GetService("Players") | |
| local Player = Players.LocalPlayer | |
| local Mouse = Player:GetMouse() | |
| -- // CONFIGURATION & HEURISTICS | |
| local SETTINGS = { | |
| GRAVITY = Vector3.new(0, -32, 0), -- Constant acceleration vector for projectile arc. | |
| POOL_SIZE = 75, -- Pre-allocated part count to prevent memory spikes during combat. | |
| MAX_BOUNCES = 3, -- Limit to prevent infinite recursion/performance leaks on ricochet. | |
| PIERCE_COUNT = 2, -- Logic gate for multi-hit registration. | |
| PROJECTILE_SPEED = 180, | |
| HIT_MARKER_TIME = 0.12, | |
| VFX_COLOR = Color3.fromRGB(0, 255, 150) | |
| } | |
| -- // SESSION STATE TRACKING | |
| -- Used for internal telemetry. Decoupling stats from the logic allows for easier data export later. | |
| local SessionStats = { | |
| ShotsFired = 0, | |
| HitsRegistered = 0, | |
| BouncesOccurred = 0 | |
| } | |
| -- // UI MANAGEMENT | |
| -- Note: UI is procedural to demonstrate mastery of the 'Instance' hierarchy without relying on Studio assets. | |
| local function InitializeUI() | |
| local Gui = Instance.new("ScreenGui") | |
| Gui.Name = "ScripterShowcaseUI" | |
| Gui.IgnoreGuiInset = true | |
| Gui.Parent = Player:WaitForChild("PlayerGui") | |
| local Marker = Instance.new("Frame") | |
| Marker.Name = "Hitmarker" | |
| Marker.Size = UDim2.fromOffset(2, 2) | |
| Marker.BackgroundColor3 = Color3.fromRGB(255, 255, 255) | |
| Marker.AnchorPoint = Vector2.new(0.5, 0.5) | |
| Marker.Position = UDim2.fromScale(0.5, 0.5) | |
| Marker.BackgroundTransparency = 1 | |
| Marker.Visible = false | |
| Marker.Parent = Gui | |
| local StatsLabel = Instance.new("TextLabel") | |
| StatsLabel.Size = UDim2.new(0, 200, 0, 50) | |
| StatsLabel.Position = UDim2.new(0, 20, 1, -70) | |
| StatsLabel.BackgroundTransparency = 1 | |
| StatsLabel.TextColor3 = Color3.new(1, 1, 1) | |
| StatsLabel.TextXAlignment = Enum.TextXAlignment.Left | |
| StatsLabel.Font = Enum.Font.Code | |
| StatsLabel.TextSize = 14 | |
| StatsLabel.Text = "SYSTEM ACTIVE" | |
| StatsLabel.Parent = Gui | |
| return Marker, StatsLabel | |
| end | |
| local Hitmarker, StatsDisplay = InitializeUI() | |
| -- HUD Update logic: Separated from the fire loop to maintain a clean MVC-style architecture. | |
| local function UpdateHUD() | |
| StatsDisplay.Text = string.format( | |
| "SHOTS: %d\nHITS: %d\nBOUNCES: %d", | |
| SessionStats.ShotsFired, | |
| SessionStats.HitsRegistered, | |
| SessionStats.BouncesOccurred | |
| ) | |
| end | |
| local function PlayHitEffect() | |
| Hitmarker.Visible = true | |
| Hitmarker.BackgroundTransparency = 0 | |
| Hitmarker.Size = UDim2.fromOffset(24, 24) | |
| -- Sine Easing is chosen here for a more "snappy" visual response common in modern shooters. | |
| local info = TweenInfo.new(SETTINGS.HIT_MARKER_TIME, Enum.EasingStyle.Sine, Enum.EasingDirection.Out) | |
| TweenService:Create(Hitmarker, info, { | |
| Size = UDim2.fromOffset(4, 4), | |
| BackgroundTransparency = 1 | |
| }):Play() | |
| end | |
| -- // OBJECT POOLING ENGINE | |
| -- CRITICAL PERFORMANCE: We use pooling to avoid the 'Instantiate/Destroy' lag spike. | |
| -- By moving parts to a far-off CFrame rather than destroying them, we keep the Luau heap stable. | |
| local ProjectilePool = { Available = {}, Container = nil } | |
| function ProjectilePool:Setup() | |
| local folder = Instance.new("Folder") | |
| folder.Name = "ProjectileStorage" | |
| folder.Parent = workspace | |
| self.Container = folder | |
| for i = 1, SETTINGS.POOL_SIZE do | |
| local part = Instance.new("Part") | |
| part.Size = Vector3.new(0.2, 0.2, 4) | |
| part.Material = Enum.Material.Neon | |
| part.Color = SETTINGS.VFX_COLOR | |
| part.CanCollide = false | |
| part.Anchored = true | |
| part.Transparency = 1 | |
| part.CFrame = CFrame.new(0, 1000, 0) | |
| part.Parent = self.Container | |
| table.insert(self.Available, part) | |
| end | |
| end | |
| function ProjectilePool:Borrow() | |
| -- Pulls a pre-existing part from the stack. If the stack is empty, it falls back to creation. | |
| local part = table.remove(self.Available) | |
| if not part then | |
| part = Instance.new("Part") | |
| part.Anchored = true | |
| part.Parent = self.Container | |
| end | |
| part.Transparency = 0 | |
| return part | |
| end | |
| function ProjectilePool:Recycle(part) | |
| -- Resets the part state and pushes it back into the pool for future reuse. | |
| part.Transparency = 1 | |
| part.CFrame = CFrame.new(0, 1000, 0) | |
| table.insert(self.Available, part) | |
| end | |
| -- // METATABLE-BASED SIGNAL CLASS | |
| -- This mimics the 'RBXScriptSignal' behavior but runs internally via Luau tables. | |
| -- This reduces overhead and avoids the latency associated with BindableEvents. | |
| local Signal = {} | |
| Signal.__index = Signal | |
| function Signal.new() | |
| return setmetatable({ _listeners = {} }, Signal) | |
| end | |
| function Signal:Connect(fn) | |
| table.insert(self._listeners, fn) | |
| return { Disconnect = function() | |
| local idx = table.find(self._listeners, fn) | |
| if idx then table.remove(self._listeners, idx) end | |
| end } | |
| end | |
| function Signal:Fire(...) | |
| -- Uses task.spawn to ensure that one failing listener doesn't crash the entire signal chain. | |
| for _, fn in ipairs(self._listeners) do | |
| task.spawn(fn, ...) | |
| end | |
| end | |
| -- // CORE PROJECTILE CLASS (OOP) | |
| local Projectile = {} | |
| Projectile.__index = Projectile | |
| function Projectile.new(origin, velocity, owner) | |
| local self = setmetatable({}, Projectile) | |
| self.Origin = origin | |
| self.Velocity = velocity | |
| self.Owner = owner | |
| self.Instance = ProjectilePool:Borrow() | |
| self.CurrentPos = origin | |
| self.TimeAlive = 0 | |
| self.Active = true | |
| self.BounceCount = 0 | |
| self.PierceCount = 0 | |
| self.Collided = Signal.new() | |
| return self | |
| end | |
| -- THE PHYSICS LOGIC: | |
| -- Standard Kinematic Equation: s = ut + 0.5at^2 | |
| -- This allows us to calculate the EXACT position of the bullet at any time 't' without | |
| -- needing the Roblox physics engine to update. This is much more reliable for high-speed projectiles. | |
| function Projectile:GetTrajectory(t) | |
| return self.Origin + (self.Velocity * t) + (0.5 * SETTINGS.GRAVITY * (t^2)) | |
| end | |
| function Projectile:Step(dt) | |
| if not self.Active then return end | |
| self.TimeAlive += dt | |
| -- Self-cleanup logic: Prevents "ghost" projectiles from consuming memory if they fly into the sky. | |
| if self.TimeAlive > 5 then | |
| self:Destroy() | |
| return | |
| end | |
| -- Raycast segmenting: We calculate the next frame's position and cast a ray between | |
| -- the current position and the next to check for intersections. | |
| local nextPoint = self:GetTrajectory(self.TimeAlive) | |
| local segment = nextPoint - self.CurrentPos | |
| local rayParams = RaycastParams.new() | |
| rayParams.FilterDescendantsInstances = {self.Instance, self.Owner, self.Instance.Parent} | |
| rayParams.FilterType = Enum.RaycastFilterType.Exclude | |
| local hitResult = workspace:Raycast(self.CurrentPos, segment, rayParams) | |
| if hitResult then | |
| self:HandleHit(hitResult) | |
| else | |
| -- Visual Synchronicity: We update the CFrame to face the direction of travel | |
| -- so the projectile "tilts" with the gravity arc. | |
| if segment.Magnitude > 0.001 then | |
| self.Instance.CFrame = CFrame.new(nextPoint, nextPoint + segment) | |
| end | |
| self.CurrentPos = nextPoint | |
| end | |
| end | |
| function Projectile:HandleHit(result) | |
| local hitPart = result.Instance | |
| local hitPos = result.Position | |
| local hitNormal = result.Normal | |
| -- IMPACT VISUALS: Simple explosion to provide immediate visual confirmation of the hit. | |
| local flash = Instance.new("Explosion") | |
| flash.Position = hitPos | |
| flash.BlastRadius = 0 | |
| flash.Parent = workspace | |
| -- RICOCHET MATHEMATICS: | |
| -- We use the Dot Product to calculate the reflection vector across the surface normal. | |
| -- Formula: v_reflect = v - 2 * (v · n) * n | |
| if hitPart.Material == Enum.Material.Glass and self.BounceCount < SETTINGS.MAX_BOUNCES then | |
| self.BounceCount += 1 | |
| SessionStats.BouncesOccurred += 1 | |
| self.Origin = hitPos | |
| self.Velocity = (self.Velocity - 2 * self.Velocity:Dot(hitNormal) * hitNormal) * 0.6 | |
| self.TimeAlive = 0 -- Resetting time is required because we are establishing a new 'origin' point. | |
| UpdateHUD() | |
| return | |
| end | |
| -- COMBAT INTERACTION: | |
| -- Checks for a Humanoid. This is abstracted so it could easily be swapped for a generic 'DamageModule'. | |
| local model = hitPart:FindFirstAncestorOfClass("Model") | |
| local hum = model and model:FindFirstChildOfClass("Humanoid") | |
| if hum and hum.Health > 0 then | |
| hum:TakeDamage(10) | |
| SessionStats.HitsRegistered += 1 | |
| PlayHitEffect() | |
| UpdateHUD() | |
| -- PIERCING LOGIC: If pierce is available, we slightly offset the 'CurrentPos' | |
| -- to prevent the raycast from getting stuck inside the hit part on the next frame. | |
| if self.PierceCount < SETTINGS.PIERCE_COUNT then | |
| self.PierceCount += 1 | |
| self.CurrentPos = hitPos + (self.Velocity.Unit * 1.5) | |
| return | |
| end | |
| end | |
| self:Destroy() | |
| end | |
| function Projectile:Destroy() | |
| if not self.Active then return end | |
| self.Active = false | |
| ProjectilePool:Recycle(self.Instance) -- Return to pool instead of calling :Destroy() | |
| end | |
| -- // MAIN GAME LOOP | |
| local ActiveProjectiles = {} | |
| local function Fire() | |
| local char = Player.Character | |
| if not char or not char:FindFirstChild("HumanoidRootPart") then return end | |
| SessionStats.ShotsFired += 1 | |
| UpdateHUD() | |
| local startPos = char.HumanoidRootPart.Position + Vector3.new(0, 2, 0) | |
| local targetDir = (Mouse.Hit.Position - startPos).Unit | |
| -- SPREAD LOGIC: We iterate through a loop to create a multi-shot effect. | |
| -- CFrame.Angles is used to rotate the velocity vector locally. | |
| for i = -2, 2 do | |
| local spread = CFrame.Angles(0, math.rad(i * 4), 0) | |
| local vel = (spread * targetDir) * SETTINGS.PROJECTILE_SPEED | |
| local p = Projectile.new(startPos, vel, char) | |
| table.insert(ActiveProjectiles, p) | |
| end | |
| end | |
| -- // SYSTEM BOOT | |
| ProjectilePool:Setup() | |
| UpdateHUD() | |
| -- Heartbeat is chosen over RenderStepped because projectile physics do not | |
| -- need to block the rendering pipeline, but must remain consistent with the physics clock. | |
| RunService.Heartbeat:Connect(function(dt) | |
| for i = #ActiveProjectiles, 1, -1 do | |
| local bullet = ActiveProjectiles[i] | |
| bullet:Step(dt) | |
| if not bullet.Active then | |
| table.remove(ActiveProjectiles, i) | |
| end | |
| end | |
| end) | |
| UserInputService.InputBegan:Connect(function(input, processed) | |
| if processed then return end -- Standard check to ignore inputs while typing in chat. | |
| if input.UserInputType == Enum.UserInputType.MouseButton1 then | |
| Fire() | |
| end | |
| end) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment