Skip to content

Instantly share code, notes, and snippets.

@imlogvn
Last active February 21, 2026 17:53
Show Gist options
  • Select an option

  • Save imlogvn/80b3c7272dcb782456e0a427464141a5 to your computer and use it in GitHub Desktop.

Select an option

Save imlogvn/80b3c7272dcb782456e0a427464141a5 to your computer and use it in GitHub Desktop.
ADVANCED KINETIC & VISUAL FRAMEWORK DEMO
--[[
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