Kinematics
Deterministic segment bodies: a body travels in straight legs and deflects off game-supplied surfaces by a configured rule set (hit-offset steer, capped surface-motion transfer, speed gain with a floor and a ceiling). The wire carries one leg per CONTACT, not a pose per tick, and both tiers evaluate the same line against the shared clock — so there is no interpolation delay and nothing to smooth at a bounce. For pong and its family, air hockey, breakout, arcade football.
Seams you implement 1
Crossplay calls these; your game supplies them. Each ships an inert or permissive default, so register yours before services.AddCrossplayKinematics(); and it wins.
IKinematicSurfaces game SPI
The game's answer to "what can this body bounce off right now?" — the piece's one required SPI.
// An arena game whose bats are world entities it already tracks:
sealed class BatSurfaces : IKinematicSurfaces
{
public void Collect(string zone, List<KinematicSurface> into)
{
foreach (Bat bat in _bats.In(zone))
into.Add(new KinematicSurface(bat.EntityId, bat.X, bat.Z, bat.HalfLength, bat.HalfThickness, bat.Yaw, bat.SlideSpeed));
}
} -
void Collect(string zone, List<KinematicSurface> into)Appends the surfaces currently in play in a zone.
Services you call 1
Crossplay implements these. Resolve them from DI and call them from your own systems.
IKinematicBodies
Bodies that travel in straight legs and deflect off game-supplied surfaces by a deterministic rule set — the alternative to a solver for games whose feel IS their rules.
-
event Action<KinematicContactEvent> ContactedRaised after each resolved contact — where a game scores, reacts and plays a sound.
-
bool Launch(long entityId, float dirX, float dirZ, float speed = 0)Puts a body into play along a direction.
-
bool Park(long entityId)Stops a body where it is and tells the zone.
-
bool TryGetSegment(long entityId, out KinematicSegment segment)The body's current leg.
Configuration 2
Every tunable lives in an options object — there are no magic numbers to hunt for.
KinematicBodyOptions
How one kind of kinematic body behaves. Keyed by the first byte of an entity's appearance blob — the same "appearance kind" convention every other composition rule in the framework uses — so a game declares its ball, its puck and its grenade as three rows and writes no code to tell them apart.
-
float ContactEpsilon { get; set; }How far clear of the contact point the next leg starts.
-
float HitOffsetInfluence { get; set; }How hard a hit away from a surface centre steers the body toward that end, as a fraction of body speed. 0 = the surface is a plain mirror.
-
float MaxSpeed { get; set; }The ceiling SpeedGainPerHit climbs toward.
-
float MaxSurfaceMotionTransfer { get; set; }The most along-axis velocity a surface's motion may impart — the guard that keeps a REPOSITIONED surface (which measures as impossibly fast) inert. See MaxSurfaceMotionTransfer.
-
float MinSeparationFraction { get; set; }The least separating component (0..1) an outgoing direction may have along the surface normal, so a graze cannot run the body along the wall forever.
-
float MinSpeed { get; set; }Launch speed, and the floor a contact can never take the body below.
-
float Radius { get; set; }The body radius, world units. Surfaces are expanded by it, so the body sweeps as a point.
-
float SpeedGainPerHit { get; set; }Speed multiplier applied on each contact — the rally accelerator. 1 = no change.
-
float SurfaceMotionInfluence { get; set; }How much of a moving surface's along-axis speed is imparted to the body.
KinematicsOptions
Kinematics tuning (defaults here, never inline).
-
Dictionary<byte, KinematicBodyOptions> Bodies { get; set; }Body kind, keyed by the first byte of the entity's appearance blob. An entity whose kind is not listed is never adopted, so the piece is inert by default.
-
int MaxContactsPerStep { get; set; }The most contacts one body may resolve within a single step, after which the remainder of the step is flown straight.
-
List<string> Zones { get; set; }Zones the piece watches. Empty = every zone.
Wire messages 1
The protocol this piece speaks. Ids are allocated per piece so they can never collide.
KinematicSegmentChanged Server to everyone in the zone: a kinematic body began a new straight leg. Sent on CONTACT, not on a timer — between contacts there is nothing to say, because both tiers can evaluate the same line.
Unity services you inject 1
Crossplay binds these in the client context. Inject and call them from your own MonoBehaviours and presenters.
IKinematicsClient
The Kinematics piece, client side: the current straight leg of every kinematic body, ready to be evaluated at PRESENT time.
-
event Action<KinematicSegmentChanged> SegmentChangedRaised whenever a body starts a new leg — a launch, a contact, or a park.
-
bool TryGetSegment(long entityId, out KinematicSegment segment)The body's current leg.
-
bool TryGetPositionAt(long entityId, long serverTimeMs, out float x, out float z)Where the body is at a given reading of the shared server clock — the whole client-side job.