Hello, For bullet replication! This is how I do it.
Shared
-- BounceGunBehaviors (ModuleScript)
-- Place in ReplicatedStorage so both server and client can require it.
--
-- Both sides MUST require this and call Register() in identical order.
-- VetraNet transmits only the 2-byte hash — if the order diverges, every
-- fire request will be rejected as RejectedUnknownBehavior.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Vetra = require(ReplicatedStorage.Vetra)
local BehaviorRegistry = require(ReplicatedStorage.Vetra.VetraNet.Transport.BehaviorRegistry)
-- ─── Shared RaycastParams ────────────────────────────────────────────────────
-- No FilterDescendantsInstances here — the server and each client fill in
-- their own character / bullet container at fire time via the Behavior's
-- RaycastParams field. Leave it open so the registry entry is reusable.
local RaycastFilter = RaycastParams.new()
RaycastFilter.FilterType = Enum.RaycastFilterType.Exclude
-- ─── Behavior Definitions ────────────────────────────────────────────────────
local BounceBehavior = {
MaxDistance = 6000,
MaxSpeed = 500, -- required by BehaviorRegistry for speed validation
MaxBounces = 30,
Restitution = 0.98,
Gravity = Vector3.new(0, -20, 0),
RaycastParams = RaycastFilter,
HighFidelitySegmentSize = 0,
CanBounceFunction = function() return true end,
}
-- ─── Registry ────────────────────────────────────────────────────────────────
local Registry = BehaviorRegistry.new()
Registry:Register("BounceGun", BounceBehavior)
return Registry
Client
-- ─── Vetra ───────────────────────────────────────────────────────────────────
local Vetra = require(ReplicatedStorage.Vetra)
local VetraNet = Vetra.VetraNet
-- ─── Shared BehaviorRegistry ─────────────────────────────────────────────────
-- Must register behaviors in the exact same order as the server.
local Registry = require(ReplicatedStorage.BounceGunBehaviors)
local ClientSolver = Vetra.newParallel({ ShardCount = 4 })
local Net = VetraNet.new(ClientSolver, Registry)
-- Inject the cosmetic provider into the registered behavior so VetraNet
-- uses it when spawning local cosmetics.
local BounceBehavior = Registry:Get(Registry:GetHash("BounceGun"))
BounceBehavior.CosmeticBulletProvider = createCosmeticBullet
BounceBehavior.CosmeticBulletContainer = BulletContainer
-- This fire! and replicate to all :)
Net:Fire(origin, direction.Unit, CONFIG.BulletSpeed, "BounceGun")
Server
-- ─── Services ────────────────────────────────────────────────────────────────
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- ─── Vetra ───────────────────────────────────────────────────────────────────
local Vetra = require(ReplicatedStorage.Vetra)
local VetraNet = Vetra.VetraNet
-- ─── Shared BehaviorRegistry ─────────────────────────────────────────────────
-- Must be required BEFORE VetraNet.Server.new() so behaviors are registered
-- before the first fire request can arrive from a client.
local Registry = require(ReplicatedStorage.BounceGunBehaviors)
-- ─── Parallel Solver ─────────────────────────────────────────────────────────
-- newParallel distributes bullet simulation across Roblox Actors.
-- ShardCount = 4 is a sensible default; tune upward if you have many
-- concurrent bullets (>200) and your server has more cores available.
-- Falls back to serial automatically if Actor construction fails.
local Solver = Vetra.newParallel({
ShardCount = 4,
ActorParent = script.Parent
})
-- ─── Server Network ───────────────────────────────────────────────────────────
local Net = VetraNet.new(Solver, Registry, {
MaxOriginTolerance = 20, -- studs; bounce gun fires forward so be generous
MaxConcurrentPerPlayer = 30, -- 30 bullets in flight per player
TokensPerSecond = 8, -- sustained fire rate (shots/sec)
BurstLimit = 15, -- allows initial burst before throttling
ReplicateState = true, -- send per-frame position updates to clients
})
-- ─── Hit Handler ─────────────────────────────────────────────────────────────
-- OnValidatedHit fires only after the server confirms the hit is legitimate.
-- Apply damage, sound effects, etc. here.
Net.OnValidatedHit:Connect(function(Player, Context, Result, Velocity, _Force)
if not Result or not Result.Instance then return end
local hitPart = Result.Instance
local model = hitPart:FindFirstAncestorOfClass("Model")
local hum = model and model:FindFirstChildOfClass("Humanoid")
if hum and hum.Health > 0 then
-- Scale damage with remaining speed — faster bullet = more damage
local speed = Velocity.Magnitude
local damage = math.clamp(speed * 0.1, 5, 40)
hum:TakeDamage(damage)
end
end)
Do note that the hash order must be same, so you can sort them first.
But the behavior between client and server doesnt have to be same, this means that client cosmetic can be different with server.
Depends on you!