SetNet.Ticks
1.2.0
dotnet add package SetNet.Ticks --version 1.2.0
NuGet\Install-Package SetNet.Ticks -Version 1.2.0
<PackageReference Include="SetNet.Ticks" Version="1.2.0" />
<PackageVersion Include="SetNet.Ticks" Version="1.2.0" />
<PackageReference Include="SetNet.Ticks" />
paket add SetNet.Ticks --version 1.2.0
#r "nuget: SetNet.Ticks, 1.2.0"
#:package SetNet.Ticks@1.2.0
#addin nuget:?package=SetNet.Ticks&version=1.2.0
#tool nuget:?package=SetNet.Ticks&version=1.2.0
SetNet.Ticks
One central update loop for a SetNet server. Instead of every system running its own System.Threading.Timer, or you calling Tick()/Update() by hand all over the place, register everything into named channels — each with its own rate (Hz) and priority — and drive them all from one place.
- Per-channel fixed timestep — deterministic;
30 Hzmovement,10 HzAI,1 Hzhousekeeping, all independent. - Two ways to drive it — an internal timer (
Start(), typical dedicated server) orPump(dt)from your own loop (UnityUpdate/FixedUpdate, a custom game loop). - Priority ordering — higher-priority channels tick first within a pump (e.g. movement before AI).
- Anti-spiral — a per-channel cap on catch-up substeps, so a stall can't trigger an unbounded burst.
- Automatic subscription — set one ambient host (
new TickScheduler().MakeCurrent()) and the library's game-loop systems (MobServer,LocomotionSystem,SpawningServer) subscribe themselves when created — you don't register each one. Behaviour trees / state machines plug in viaBind(context).
Zero dependencies. This is a foundation: the game-loop modules (SetNet.Mobs, SetNet.Locomotion, SetNet.Spawning) depend on this, not the other way round — so SetNet core stays a pure transport/data layer with no notion of ticks. No wire protocol; a pure server-side scheduler.
The interface (this package)
public readonly struct TickInfo
{
public double DeltaMs { get; } // this channel's fixed step, in ms (library modules)
public float DeltaSeconds { get; } // …and in seconds (Unity-friendly)
public long Frame { get; } // scheduler frame counter
}
public interface ITickable { void Tick(in TickInfo tick); }
public interface IAsyncTickable { Task TickAsync(TickInfo tick); }
ITickable, IAsyncTickable, the registration seam ITickHost, the ambient TickHost.Current, and TickScheduler (which implements ITickHost) all live in this package (namespace SetNet.Ticks). Modules that want to be tickable reference SetNet.Ticks — a tiny, zero-dependency foundation — rather than pulling the concept into SetNet core.
Quick start — automatic subscription (recommended)
Set one ambient host with MakeCurrent() before your UseXxx(...) calls. Each game-loop system then subscribes itself — into its own channel, at its own rate — and runs off the scheduler instead of its own timer. You register nothing by hand:
using SetNet.Ticks;
var ticks = new TickScheduler().MakeCurrent(); // ← everything created after this auto-subscribes
var loco = server.UseLocomotion(geo); // → channel "locomotion" @ Hz, priority 100
var mobs = server.UseMobs(); // → channel "mobs" @ TickRateHz, priority 50
var spawning = server.UseSpawning(mobs, opts); // → channel "spawning" @ 1000/TickIntervalMs, priority 10
ticks.Start(); // one place drives all of them
// …or, from your own loop each frame: ticks.Pump(realDeltaMs);
Every mob rides MobServer, every mover rides LocomotionSystem, so subscribing the systems is enough — you never touch individual mobs/movers. Behaviour trees used inside mob brains ride MobServer too. Standalone trees/machines plug in explicitly (they need a context):
ticks.Channel("ai", 10).Add(tree.Bind(context)); // a tree not owned by a mob
ticks.Channel("slow", 1).Add(t => RegenTick(t.DeltaSeconds));
Default channels/rates/priorities are overridable per system (MobOptions.TickChannel/TickPriority, LocomotionOptions.TickChannel/…). Opt one system out with AutoTick = false (it falls back to its own timer).
Manual registration (no ambient host)
If you'd rather wire it explicitly, skip MakeCurrent() and add systems to channels yourself (turn their own timers off so they don't double-tick):
var mobs = server.UseMobs(new MobOptions { AutoTick = false, UseInternalTimer = false });
var loco = server.UseLocomotion(geo, new LocomotionOptions { AutoTick = false, UseInternalTimer = false });
var ticks = new TickScheduler();
ticks.Channel("movement", 30, priority: 10).Add(loco); // LocomotionSystem : ITickable
ticks.Channel("ai", 10, priority: 5).Add(mobs); // MobServer : IAsyncTickable
ticks.Start(baseHz: 60); // base timer ≥ your fastest channel
The behaviour-tree pain point
A BehaviorTree<T> doesn't tick all your trees for you — you'd call tree.Tick(ctx, dt) per entity. Bind(ctx) turns a tree into an ITickable, so every tree updates from the one scheduler:
foreach (var mob in mobs)
aiChannel.Add(mob.Tree.Bind(mob.Context)); // registered once; ticked together at 10 Hz
API
TickScheduler
| Member | Meaning |
|---|---|
TickScheduler MakeCurrent() |
Make this the ambient TickHost.Current so systems auto-subscribe. Returns this; call before your UseXxx(...). Cleared on Dispose. |
IDisposable Register(ITickable / IAsyncTickable, channel, hz, priority) |
The ITickHost seam systems use to auto-subscribe; you can call it directly too. |
TickChannel Channel(string name, int hz = 10, int priority = 0) |
Get or create a channel. Hz/priority apply only on first creation. |
void Start(int baseHz = 60) |
Start the internal timer driver. Set baseHz ≥ your fastest channel. Restarts if already running. |
void Stop() |
Stop the internal timer (registrations kept). |
void Pump(double realDtMs) |
Advance every channel by real elapsed time. Re-entrant calls are ignored. Call this yourself when driving externally. |
bool Paused { get; set; } |
When true, pumps advance the frame counter but tick nothing. |
long Frame { get; } |
Current frame number. |
IReadOnlyList<TickChannel> Channels { get; } |
Channels, highest priority first. |
void Dispose() |
Stop the driver and clear all channels. |
TickChannel
| Member | Meaning |
|---|---|
string Name |
Channel identity. |
int Hz |
Ticks per second; items get a fixed 1000/Hz ms step. Changeable at runtime. |
int Priority |
Higher ticks earlier within a pump. Changeable at runtime. |
bool Enabled |
Toggle to pause just this channel. |
int MaxSubstepsPerPump |
Cap on catch-up steps per pump (default 5); backlog beyond it is dropped. |
int Count |
Registered item count. |
IDisposable Add(ITickable) / Add(IAsyncTickable) |
Register a (sync/async) tickable. Async is fire-and-forget per step — guard overruns (Mobs already does). |
IDisposable Add(Action<TickInfo>) / Add(Action) / Add(Func<TickInfo, Task>) |
Register a callback. |
Dispose the handle returned by any Add to unregister.
Notes
- Async tickables (
IAsyncTickable,Func<TickInfo,Task>) are invoked fire-and-forget per step, not awaited. If your work can overrun a step, guard against overlapping runs —MobServeralready self-guards, so a slow AI tick simply skips rather than piling up. - Fixed timestep: each item is called with
DeltaMs = 1000/Hz, regardless of the base drive rate. Real time is absorbed by a per-channel accumulator. - Housekeeping timers in other modules (Auth session sweep, Auction settle, Matchmaking, StatusEffects expiry) keep their own low-frequency timers — they're independent of the game loop. Route them through a
slowchannel only if you want a single clock; they don't need it.
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
- No dependencies.
NuGet packages (6)
Showing the top 5 NuGet packages that depend on SetNet.Ticks:
| Package | Downloads |
|---|---|
|
SetNet.Locomotion
A unified server-side movement simulator for SetNet: one system advances the position of everything that moves (players, mobs, NPCs, projectiles) along pathfound routes at a fixed rate — with automatic subscription (create a Mover and it's already ticking). It replicates NOTHING: you read positions and replicate them your own way, and a Started hook fires when a mover gets a new destination so you can send just the point to clients (L2-style, client re-paths locally). Depends on SetNet + SetNet.GeoData + SetNet.PathFinding. |
|
|
SetNet.Mobs
Server-authoritative hostile AI entities for SetNet. One IMobBrain per mob type (aggressive, passive-retaliate, ranged/kiting, caster) — or compose one from behaviour components — behind a uniform tick loop that handles perception, threat, movement (via SetNet.PathFinding, straight-line fallback), ability cooldowns/casts/telegraphs, damage, death and respawn. Replication is a seam (IMobReplication, no-op default; poll MobServer.Mobs or handle MobMoved) — a StateSync adapter ships separately as SetNet.Mobs.StateSync. server.UseMobs() + client.UseMobs(). Rides the unified SetNet.Protocol on the Channels.Mobs channel. Depends on SetNet + SetNet.GeoData + SetNet.PathFinding (NOT StateSync). |
|
|
SetNet.Projectiles
Server-authoritative travelling projectiles for SetNet: spawn with a velocity, advance each tick (optional gravity), and sweep-test the path with the SAME pluggable IHitDetector as SetNet.Hitscan — so you bring your own hit detection. Lifetime/range, Hit/Expired events, on-hit callback. Auto-subscribes to SetNet.Ticks (or drive Update(dt) yourself). Depends on SetNet.Hitscan + SetNet.GeoData + SetNet.Ticks. |
|
|
SetNet.StateMachine
A tiny generic finite-state machine for SetNet AI/gameplay: states with enter/update/exit, guarded transitions (and any-state transitions), driven by Update(context, dt). Engine-agnostic and allocation-light; wrap one in an IMobBrain for mob AI, or use it anywhere. No wire protocol. Depends only on SetNet. |
|
|
SetNet.BehaviorTree
A small generic behavior tree for SetNet AI: Sequence / Selector / Parallel composites, Inverter / Succeeder / Repeater / Cooldown decorators, and Action / Condition / Wait leaves, assembled with a fluent builder and ticked over your context type. Engine-agnostic; wrap one in an IMobBrain for mob AI, or use it anywhere. No wire protocol. Depends only on SetNet. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.2.0 | 394 | 8/5/2026 |