Start
Architecture & networking
A walker touches physics, animation, crew input and replication at the same time. Getting the split of responsibilities right is most of the work.
Who computes what
| Machine | Does | Never does |
|---|---|---|
| Server | Validates requests, owns replicated state (park, jump counter, module occupancy, marks), runs the AI crew. | Trust a client position or target without checking it. |
| Physics owner | Writes velocities every frame (RplComponent.IsOwner()). Usually the server, sometimes a driver client. | Decide game state on its own. |
| Every machine | Animates legs, torso, arms locally from the observed motion; plays effects and sound. | Send per-frame animation data over the network. |
Leg poses are not replicated. Each machine derives them from how the root moves, so a remote walker costs no bandwidth beyond the vehicle's own transform.
Client requests go through the PlayerController
A client may only send a server RPC through an object it owns. Your vehicle component is not owned by the client, so
an RPC sent from it silently disappears – no error, no warning. Route every request through the player's own
SCR_PlayerController and pass the vehicle's RplId.
modded class SCR_PlayerController
{
//! Called on the client by the walker's input component.
void MyWalker_RequestPark(IEntity walker, bool park)
{
RplComponent rpl = RplComponent.Cast(walker.FindComponent(RplComponent));
if (!rpl)
return;
if (Replication.IsServer()) // listen server / singleplayer: no RPC needed
{
MyWalker_ServerPark(rpl.Id(), park);
return;
}
Rpc(MyWalker_RpcAsk_Park, rpl.Id(), park);
}
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
protected void MyWalker_RpcAsk_Park(RplId walkerId, bool park)
{
MyWalker_ServerPark(walkerId, park);
}
protected void MyWalker_ServerPark(RplId walkerId, bool park)
{
RplComponent rpl = RplComponent.Cast(Replication.FindItem(walkerId));
if (!rpl || !rpl.GetEntity())
return;
IEntity walker = rpl.GetEntity().GetRootParent();
// Validate: is the sender really the driver of THIS walker?
if (!MyWalker_Seats.IsDriverOf(walker, GetControlledEntity()))
return;
MyWalker_LegRig rig = MyWalker_LegRig.Cast(walker.FindComponent(MyWalker_LegRig));
if (rig)
rig.ServerSetParked(park); // writes an RplProp + Replication.BumpMe()
}
}
RplChannel.Unreliable for continuous input (driver heading 15×/s) – the next packet comes soon anyway. Use Reliable for one-shot actions (park, fire, mark).Replicated state, local reaction
Events that every machine has to see (a jump, a module swap) are replicated as a small state change. Each
machine reacts in the onRplName callback. Remember that the callback does not fire on the machine that
wrote the value – the server has to call the reaction itself.
[RplProp(onRplName: "OnJumpReplicated")]
protected int m_iJumpCounter; // increments per jump: a counter never "misses" an event
protected int m_iJumpSeen;
void ServerStartJump()
{
m_iJumpCounter++;
Replication.BumpMe();
OnJumpReplicated(); // the server does not get its own callback
}
protected void OnJumpReplicated()
{
if (m_iJumpCounter == m_iJumpSeen)
return;
m_iJumpSeen = m_iJumpCounter;
PlayJumpEffects(); // every machine
if (IsPhysicsOwner())
ApplyJumpImpulse(); // only the machine that simulates the body
}
The fallback tick
On a dedicated-server client the walker you are not driving may never get EOnFrame. A watchdog
counts real frames; if none arrived for two seconds it re-arms the event mask and, if that does not help, drives all
walker components from the callqueue.
protected int m_iRealFrames;
protected bool m_bFallbackRunning;
override void EOnFrame(IEntity owner, float timeSlice)
{
m_iRealFrames++;
Tick(owner, timeSlice);
}
//! Every 2 s (CallLater, repeat).
protected void Watchdog()
{
IEntity owner = GetOwner();
if (m_iRealFrames > 0)
{
m_iRealFrames = 0;
if (m_bFallbackRunning) // real tick is back
{
GetGame().GetCallqueue().Remove(FallbackTick);
m_bFallbackRunning = false;
}
return;
}
// step 1: wake the entity
owner.SetFlags(EntityFlags.ACTIVE);
SetEventMask(owner, EntityEvent.FRAME);
// step 2: tick it ourselves
if (!m_bFallbackRunning)
{
m_bFallbackRunning = true;
GetGame().GetCallqueue().CallLater(FallbackTick, 0, true);
}
}
protected void FallbackTick()
{
float dt = GetGame().GetWorld().GetTimeSlice();
Tick(GetOwner(), dt); // same code path as EOnFrame
}
!System.IsConsoleApp()) unless you need the animation on the server too, e.g. because leg parts carry hit-zone colliders.