← Home SEEBIE90 DOCS
Download PDF
No copies or uploads of M.A.W.S. assetsExample code is free to useNo printed reproduction or modification of this documentationSuggestions: Discord to the authorLicense

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.

In plain wordsThe server makes the decisions, the one machine that simulates the walker's physics moves it, and every machine draws the legs by itself. Players never talk to the walker directly - their requests go through their own player object to the server.

Who computes what

MachineDoesNever does
ServerValidates requests, owns replicated state (park, jump counter, module occupancy, marks), runs the AI crew.Trust a client position or target without checking it.
Physics ownerWrites velocities every frame (RplComponent.IsOwner()). Usually the server, sometimes a driver client.Decide game state on its own.
Every machineAnimates 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.

Example code – free to useNet/MyWalker_PlayerController.c
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()
	}
}
Channel choiceUse 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.

Example code – free to use
[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.

Example code – free to use
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
}
PitfallGate the watchdog to machines that actually render (!System.IsConsoleApp()) unless you need the animation on the server too, e.g. because leg parts carry hit-zone colliders.
Example code on this site is free to use in your own projects (credit appreciated). The M.A.W.S. mods, their code and assets are covered by the M.A.W.S. License – no copies or uploads. This documentation may not be reproduced in print or modified. Suggestions: Discord. Arma Reforger / Enfusion are trademarks of Bohemia Interactive.
Impressum · Privacy Policy