A client-side prediction framework for Roblox projectile-based weapons. Provides smooth, lag-free shooting mechanics with server-authoritative hit validation and anti-cheat protection.
- Client-Side Prediction β Instant visual feedback for projectiles, no waiting for server round-trip
- Server-Authoritative Validation β All hits are validated server-side to prevent cheating
- Lag Compensation β Automatic catch-up simulation for replicated projectiles
- Full Rewind System β Optional full-world rewind for accurate lag compensation
- 9 Trajectory Types β Linear, Exponential (gravity), Sine Wave, Spiral, Decelerate, Accelerate, Boomerang, Quadratic & Cubic Bezier Curves
- Signal Events β Subscribe to projectile events for custom logic
- Humanoid Position Tracking β Historical position interpolation for accurate hit validation
- Rate Limiting β Built-in protection against event spam
- Debug Utilities β Built-in debug visualization tools
- Type Definitions β Full Luau type exports for IDE support
Just go to Github and find Release Section, Download it, Drag it, Enjoy!
- Clone this repository
- Build the place:
rojo build -o "BitGunPrediction.rbxlx"- Serve with Rojo:
rojo serveCopy the src/shared/BitGunPredictionShared, src/server/BitGunPredictionServer, and src/client/BitGunPredictionClient folders into your project.
local GPredictionService = require(path.to.GPredictionService)
local Callbacks = {}
function Callbacks.Linear(shooter, result, speed)
local hitPart = result.Instance
local hitPosition = result.Position
local humanoid = hitPart.Parent:FindFirstChild("Humanoid")
if humanoid then
humanoid:TakeDamage(25)
end
end
function Callbacks.LinearCheck(player, origin, direction, speed, lifetime, clientFiredAt, simType)
return true
end
GPredictionService.GlobalInit(Callbacks)local GPredictionController = require(path.to.GPredictionController)
GPredictionController.GlobalInit(
function(startCF, endCF, shooter, bulletKey)
return false
end,
function(bulletKey)
end,
function(confirmed, hitPart, hitPosition, shotId)
if confirmed then
print("Hit confirmed!")
else
print("Hit rejected by server")
end
end
)
GPredictionController.FireProjectile({
Origin = head.Position,
Direction = Camera.CFrame.LookVector,
Speed = 100,
Lifetime = 5,
SimulationType = "Linear",
OnStep = function(startCF, endCF, player)
return nil
end,
OnFinish = function()
end,
})Here is a complete example showing Exponential trajectory, Client-Side Prediction with Rollback, and Server Validation.
--[[ ========= SERVICES ========= ]]
local Players = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
local Workspace = game:GetService("Workspace")
--[[ ========= MODULES ========= ]]
local BitGunPredictionClient = script:WaitForChild("BitGunPredictionClient")
local GPredictionController = require(BitGunPredictionClient.GPredictionController)
--[[ ========= CONSTANTS ========= ]]
local SIMULATION_TYPE = "Exponential"
local PROJECTILE_SPEED = 100
local PROJECTILE_LIFETIME = 2
local PREDICTED_DAMAGE = 25
--[[ ========= VARIABLES ========= ]]
local LocalPlayer = Players.LocalPlayer
local Camera = Workspace.CurrentCamera
local ReplicatedBullets = {}
local PendingHits = {}
--[[ ========= HELPER FUNCTIONS ========= ]]
local function CreateBullet()
local part = Instance.new("Part")
part.Size = Vector3.new(0.3, 0.3, 0.3)
part.Color = Color3.fromRGB(255, 200, 50)
part.Material = Enum.Material.Neon
part.Anchored = true
part.CanCollide = false
part.CastShadow = false
part.Parent = Workspace
return part
end
local function GetHumanoid(part)
local current = part
while current do
local humanoid = current:FindFirstChildOfClass("Humanoid")
if humanoid then return humanoid, current end
current = current.Parent
end
return nil, nil
end
local function FireProjectile()
local head = LocalPlayer.Character and LocalPlayer.Character:FindFirstChild("Head")
if not head then return end
local myBullet = CreateBullet()
GPredictionController.FireProjectile({
Origin = head.Position,
Direction = Camera.CFrame.LookVector,
Speed = PROJECTILE_SPEED,
Lifetime = PROJECTILE_LIFETIME,
SimulationType = SIMULATION_TYPE,
OnStep = function(startCF, endCF, player)
if myBullet and myBullet.Parent then
myBullet.CFrame = endCF
end
local rayParams = RaycastParams.new()
rayParams.FilterDescendantsInstances = {LocalPlayer.Character, myBullet}
rayParams.FilterType = Enum.RaycastFilterType.Exclude
local dir = endCF.Position - startCF.Position
local result = Workspace:Raycast(startCF.Position, dir, rayParams)
if result then
if myBullet then myBullet:Destroy() end
local humanoid = GetHumanoid(result.Instance)
if humanoid then
local previousHealth = humanoid.Health
humanoid.Health -= PREDICTED_DAMAGE
PendingHits[result.Instance] = {
Humanoid = humanoid,
PreviousHealth = previousHealth,
PredictedHealth = humanoid.Health,
}
end
end
return result
end,
OnFinish = function()
if myBullet then myBullet:Destroy() end
end,
})
end
--[[ ========= MAIN ========= ]]
GPredictionController.GlobalInit(
function(startCF, endCF, shooterPlayer, bulletKey)
if not ReplicatedBullets[bulletKey] then
ReplicatedBullets[bulletKey] = CreateBullet()
ReplicatedBullets[bulletKey].Color = Color3.fromRGB(255, 100, 100)
end
ReplicatedBullets[bulletKey].CFrame = endCF
local rayParams = RaycastParams.new()
rayParams.FilterDescendantsInstances = {shooterPlayer.Character, ReplicatedBullets[bulletKey]}
rayParams.FilterType = Enum.RaycastFilterType.Exclude
local dir = endCF.Position - startCF.Position
local result = Workspace:Raycast(startCF.Position, dir, rayParams)
return result ~= nil
end,
function(bulletKey)
if ReplicatedBullets[bulletKey] then
ReplicatedBullets[bulletKey]:Destroy()
ReplicatedBullets[bulletKey] = nil
end
end,
function(confirmed, hitPart, hitPosition, shotId)
if not hitPart then return end
local pendingHit = PendingHits[hitPart]
if not pendingHit then return end
if confirmed then
PendingHits[hitPart] = nil
else
if pendingHit.Humanoid and pendingHit.Humanoid.Parent then
pendingHit.Humanoid.Health = pendingHit.PreviousHealth
end
PendingHits[hitPart] = nil
end
end
)
UserInputService.InputBegan:Connect(function(input, gameProcessed)
if gameProcessed then return end
if input.UserInputType == Enum.UserInputType.MouseButton1 then
FireProjectile()
end
end)--[[ ========= SERVICES ========= ]]
local Players = game:GetService("Players")
--[[ ========= MODULES ========= ]]
local BitGunPredictionServer = script:WaitForChild("BitGunPredictionServer")
local GPredictionService = require(BitGunPredictionServer.GPredictionService)
--[[ ========= CONSTANTS ========= ]]
local DAMAGE = 25
--[[ ========= HELPER FUNCTIONS ========= ]]
local function GetHumanoid(part)
local current = part
while current do
local humanoid = current:FindFirstChildOfClass("Humanoid")
if humanoid then return humanoid, current end
current = current.Parent
end
return nil, nil
end
--[[ ========= CALLBACKS ========= ]]
local Callbacks = {}
function Callbacks.Exponential(shooter, result, speed)
local humanoid, character = GetHumanoid(result.Instance)
if not humanoid then return end
local hitPlayer = Players:GetPlayerFromCharacter(character)
if hitPlayer and hitPlayer ~= shooter then
humanoid:TakeDamage(DAMAGE)
end
end
function Callbacks.ExponentialCheck(player, origin, direction, speed, lifetime)
return speed <= 200 and lifetime <= 5
end
--[[ ========= MAIN ========= ]]
-- this can do aswell :>
GPredictionService.GlobalInit(Callbacks)| Type | Description | Args |
|---|---|---|
Linear |
Straight line projectile | - |
Exponential |
Affected by gravity (arcing trajectory) | - |
SineWave |
Oscillates side-to-side | Amplitude, Frequency |
Spiral |
Corkscrews through the air | Radius, RotationSpeed |
Decelerate |
Slows down over time | DecayRate |
Accelerate |
Speeds up over time | Acceleration |
Boomerang |
Returns to origin | PeakTime |
QuadraticBezier |
Follows a 3-point curve | ControlPoint1, ControlPoint2, ControlOffset |
CubicBezier |
Follows a 4-point curve | ControlPoint1, ControlPoint2, ControlOffset1, ControlOffset2 |
- Origin Validation β Shots must originate within configurable distance of player
- Latency Buffer β Rejects claims with excessive latency
- Historical Position Check β Validates hits against recorded positions
- Full Rewind Mode β Optional server-side world rewind for hit validation
- Shot Tracking β Prevents duplicate hit claims
- Rate Limiting β Configurable event rate limits
- Anti-NaN / nil β Rejects invalid data types
---
## π API Reference
### GPredictionController (Client)
| Method | Description |
|--------|-------------|
| `GlobalInit(visualOnStep, visualOnFinish, onHitAcknowledge)` | Initialize the client controller |
| `FireProjectile(config: FireConfig)` | Fire a new projectile using config table |
| `BulletLerping(bullet, targetCF, lerpPercentage?)` | Smoothly interpolate bullet position |
| `DrawDebugLine(startPos, endPos, color?, duration?)` | Draw debug visualization line |
#### FireConfig Type
```lua
{
Origin: Vector3,
Direction: Vector3,
Speed: number,
Lifetime: number,
SimulationType: string,
TrajectoryArgs: TrajectoryArgs?,
OnStep: ((startCF, endCF, player) -> RaycastResult?)?,
OnFinish: (() -> nil)?,
}
| Event | Parameters |
|---|---|
OnProjectileFired |
origin, direction, shotId, simType |
OnHitRegistered |
hitInstance, hitPosition, shotId |
OnHitAcknowledged |
confirmed, hitPart, hitPosition, shotId |
| Method | Description |
|---|---|
GlobalInit(callbacks) |
Initialize the server service with hit callbacks |
| Event | Parameters |
|---|---|
OnProjectileFired |
player, origin, direction, speed, simType |
OnHitValidated |
player, hitPart, hitPosition, shotId, simType |
OnHitRejected |
player, hitPart, hitPosition, shotId, reason |
This project is licensed under the MIT License - see the LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.
- New Config-Based API β
FireProjectilenow accepts a config table for cleaner syntax - Signal Events β Added subscribable events for projectile lifecycle
- OnStep - Can Return a Basepart/Model aswell.
- BulletLerping Helper β New utility method for smooth bullet visual interpolation
- Debug Utilities β Added
DrawDebugLineand configurable debug options - Separated ServerConfig β Server configuration now in dedicated
ServerConfig.luau - Full Rewind System β Optional full-world rewind mode for lag compensation
- Type Exports β Full Luau type definitions for IDE autocomplete support
- Improved Validation β Enhanced hit validation with rejection reasons
- Initial release
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Made with β€οΈ by RiseBit