Getting started
This page takes you from an empty project to a working bullet that does lag-compensated hit detection. If you'd rather read the example source directly, it lives at example/.
1. Install
Hindsight is distributed through Wally.
# wally.toml
[dependencies]
Hindsight = "realencryptal/hindsight@^0.1"
wally install
Then sync the resulting Packages/Hindsight into ReplicatedStorage via your Rojo project file (or whatever sync tool you use).
2. Project layout
The minimum scaffolding Hindsight needs to do anything:
ReplicatedStorage/
Hindsight/ -- synced from Packages
Shared/
Definitions -- ModuleScript (you write this)
Events/
Simulate -- RemoteEvent
ServerScriptService/
Server -- Script that builds the server World
StarterPlayer/
StarterPlayerScripts/
Client -- LocalScript that builds the client World
workspace/
Characters -- Folder (players + NPCs go here)
Bullets -- Folder (visuals go here)
The names Characters and Bullets are conventions only — Hindsight reads instances, not names. Pass whatever instances you actually have.
3. Definitions
Create ReplicatedStorage/Shared/Definitions.lua:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Hindsight = require(ReplicatedStorage.Hindsight)
local Bullet: Hindsight.ProjectileDefinition = {
velocity = 100,
gravity = Vector3.new(0, -8, 0),
lifetime = 5,
power = 50,
angle = 20,
loss = 0,
filter = function(caster, victim, character)
return character == caster or victim == caster
end,
onIntersection = function(ctx)
local humanoid = ctx.character:FindFirstChildOfClass("Humanoid")
if humanoid and humanoid.Health > 0 then
humanoid:TakeDamage(10)
end
end,
}
return { Bullet = Bullet }
This is the smallest definitions module that does damage. See Defining projectiles for every field.
4. Server
Create ServerScriptService/Server.server.lua:
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local ServerScriptService = game:GetService("ServerScriptService")
local Hindsight = require(ReplicatedStorage.Hindsight)
local Characters = workspace:WaitForChild("Characters")
local Bullets = workspace:WaitForChild("Bullets")
local SimulateEvent = ReplicatedStorage.Shared.Events.Simulate
local definitionsModule = ReplicatedStorage.Shared.Definitions
local raycastFilter = RaycastParams.new()
raycastFilter.FilterType = Enum.RaycastFilterType.Exclude
raycastFilter.FilterDescendantsInstances = { Characters, Bullets }
local world = Hindsight.createWorld({
actorContainer = ServerScriptService,
definitionsModule = definitionsModule,
excludeContainers = { Bullets, Characters },
defaultRaycastFilter = raycastFilter,
})
world.rollback:autoCaptureCharacters(Characters)
Players.PlayerAdded:Connect(function(player)
player.CharacterAdded:Connect(function(character)
RunService.PostSimulation:Wait()
character.Parent = Characters
end)
end)
SimulateEvent.OnServerEvent:Connect(function(player, origin, direction, timestamp)
local rewind = player:GetNetworkPing() + 0.048
SimulateEvent:FireAllClients(player, "Bullet", origin, direction)
world:cast({
caster = player,
type = "Bullet",
origin = origin,
direction = direction,
timestamp = timestamp - rewind,
})
end)
Three things to notice:
autoCaptureCharacters(Characters)is what makes lag compensation work. Without it, rollback queries returnnilandonIntersectionnever fires.- Characters are reparented into
workspace.Charactersso a single auto-capture call covers everyone. - The server subtracts
playerPing + 0.048from the client's timestamp. The0.048covers Roblox's client-side render interpolation. Without it, shots consistently lead by ~48 ms.
A production setup would also validate timestamp and origin. See the full version in Wiring.
5. Client
Create StarterPlayer/StarterPlayerScripts/Client.client.lua:
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UserInputService = game:GetService("UserInputService")
local Hindsight = require(ReplicatedStorage.Hindsight)
local Player = Players.LocalPlayer
local Mouse = Player:GetMouse()
local PlayerScripts = Player:WaitForChild("PlayerScripts")
local SimulateEvent = ReplicatedStorage.Shared.Events.Simulate
local Characters = workspace:WaitForChild("Characters")
local Bullets = workspace:WaitForChild("Bullets")
local raycastFilter = RaycastParams.new()
raycastFilter.FilterType = Enum.RaycastFilterType.Exclude
raycastFilter.FilterDescendantsInstances = { Characters, Bullets }
local world = Hindsight.createWorld({
actorContainer = PlayerScripts,
definitionsModule = ReplicatedStorage.Shared.Definitions,
visualsContainer = Bullets,
excludeContainers = { Bullets, Characters },
defaultRaycastFilter = raycastFilter,
})
UserInputService.InputBegan:Connect(function(input, gpe)
if gpe or input.UserInputType ~= Enum.UserInputType.MouseButton1 then return end
local head = Player.Character and Player.Character:FindFirstChild("Head")
if not head then return end
local origin = head.Position
local direction = (Mouse.Hit.Position - origin).Unit
local timestamp = workspace:GetServerTimeNow()
SimulateEvent:FireServer(origin, direction, timestamp)
world:cast({
caster = Player,
type = "Bullet",
origin = origin,
direction = direction,
timestamp = timestamp,
})
end)
SimulateEvent.OnClientEvent:Connect(function(caster, type, origin, direction)
if caster == Player then return end
world:cast({
caster = caster,
type = type,
origin = origin,
direction = direction,
timestamp = workspace:GetServerTimeNow(),
})
end)
The client's World is for visuals only — no autoCapture* calls, no rollback writes. The reason it exists at all is to run projectile motion in parallel context so the main thread stays free.
6. Run it
Press F5 in Studio with two play windows. Click on the other player. The dummy or remote player loses 10 HP and the bullet vanishes. The server runs hit detection against the snapshot at the moment you fired, not against the position the dummy is in right now — that's the lag compensation half doing its job.
Where to go next
- Concepts — what's actually happening under the hood.
- Defining projectiles — every field on
ProjectileDefinition, plus per-cast modifiers. - Standalone rollback — using snapshots for melee, AoE, ability checks (or
world:hitscanfor stock single-ray hit-scans). - Configuration — every option on
WorldConfig/RollbackConfig/PenetrationConfig. - API reference — generated reference.