SOFLAM Integration Core
A telescopic tripod Changed
Nine parts, three legs that extend by different amounts, a column and a ball head. The design rule: one calculator, one place where a mistake can show.
One calculator for all parts
A single component on the root, GTG_SOFLAM_TripodComponent, moves all child entities. Nine parts cannot drift apart that way, and a fault has one
place to appear. Each child carries a small marker, GTG_SOFLAM_TripodSegmentComponent, that says what the part is (enum EGTG_TripodSegment: LEG_MID, LEG_LOW, STEM, HEAD, FRAME), along which axis it moves and where it sits when retracted.
Three independent quantities, each with its own control: leg extension, centre column, ball head.
The marker uses an enum and not a name search because child entities have no reliable name at runtime. The rest position (m_vBaseOffset) is an attribute and is not read from the entity: reading it in the first tick after OnPostInit returned garbage on a just-created child, and the legs lay flat at random angles.
Why the structure rises when the legs extend
The feet should stay on the ground while the head goes up. Mathematically those are the same thing seen from two sides, and you have to pick one. Chosen: the legs extend downwards, along their own slanted axes, and the structure above rises by exactly the distance the feet move down.
The alternative – hold the feet and push everything above up – fails on geometry. The frame is one rigid piece above all three legs and can only move vertically, but the tubes sit in sleeves inclined about 21°. At 0.5 m of lift every upper tube would run about 20 cm sideways out of its sleeve. It is obvious immediately.
What moves, though, is not the root. The first version lifted the whole entity. The gunner's seat hangs on the root and rose with it, measured:
legs 0.94 | lift 0.5045 | root above ground 0.5045 | gunner 0.5045
A compartment slot cannot be moved at runtime. So the root stays on the ground (and has no model any more), the yoke is a part of its own (role FRAME), and every part gets the lift added to its local offset. Same world positions, different frame of reference:
protected float GTG_LegLift()
{
foreach (GTG_SOFLAM_TripodSegmentComponent segment : m_aSegments)
{
if (segment.GetSegment() != EGTG_TripodSegment.LEG_LOW)
continue;
vector axis = segment.GetAxis(); // ~21 degrees from vertical: 0.93 m of height per metre of extension
return m_fLegExtension * m_fLegStageTravel * segment.GetStageCount() * axis[1];
}
return 0;
}
// in GTG_ApplyNow, for every part: the root stays, the part rises
mat[3] = segment.GetBaseOffset() + offset + hub;
part.SetLocalTransform(mat);
Stage travel m_fLegStageTravel is 0.29 m in the turret prefab (the script default is 0.28), the column m_fStemTravel 0.25 m, the head tilt limit m_fHeadTiltLimit 45°. The inner tube (LEG_LOW) moves two stages. The limit is the telescope overlap: below 5 cm of overlap a tube holds nothing.
Numbers on the wire
Almost nothing is a transform. Every machine computes the same nine positions from a few numbers. That is less traffic than nine transforms, and it cannot arrive partially: either the state is right or it is wrong everywhere.
- Leg and column extension are signals on the entity. The user actions
GTG_SOFLAM_TripodHeightAction(“Set Height Legs” / “Set Height Center”) extendSCR_AdjustSignalAction, which already keeps the mouse wheel, the value range, the rounding and the replication to everybody including late joiners. Four hooks pass the value on (OnSaveActionData,OnLoadActionData,OnRplSave,OnRplLoad); miss one and the tripod is wrong for exactly one group of players. The tripod component itself only applies it (ApplyLegExtension,ApplyStemExtension) and does not send it again. - Head angles (yaw, pitch, roll) go from the server as a broadcast (
GTG_RpcHead), the base yaw likewise (GTG_RpcBaseYaw), and the flag “a device is installed” (GTG_RpcBestueckt). When a gunner sits in the turret, the head is not sent at all:GTG_SOFLAM_TurretHeadComponentreads the turret's own aim (TurretComponent.GetAimingRotation(), or the signalsAimTurnTurret/AimElevationTurretas a fallback) and applies it on every machine. The turret replicates its aim itself; a second transmission would be double traffic and a second truth. - Late joiners fetch head angles, base yaw and the installed flag through
RplSave/RplLoad(extension values come with the actions). Otherwise a fully extended tripod would stand collapsed for everybody who connected later.
There is deliberately no client-to-server Rpc in the tripod component. A client does not own the tripod, so a message to the server would be dropped (see Who may send). Operation goes through user actions that run on the server anyway; the result goes out as a broadcast.
The base always stands level
The tripod keeps its pitch and roll at zero whatever the ground does (m_fBasePitch = 0, m_fBaseRoll = 0); only the yaw comes from the placement. Taking over the slope of the ground meant the head tilt needed for a target changed just by panning, and the 45° clamp was reached much earlier in some directions. Like a real tripod with a spirit level, the legs make up the difference.
Every leg follows its own ground
On sloped ground one common extension makes at least one leg hang in the air or stand in the ground. The fix keeps the frame rigid and level, but gives each leg its own extension along its own unchanged axis – exactly what a telescopic leg with a fixed hinge angle does.
The first version asked for the ground height once, at the leg's mount point. But the leg extends along an axis inclined about 21°, so its foot lands 20–35 cm to the side of that point, and on a slope the ground is a different height there. The answer is a short fixed-point iteration: foot position from the current extension, ground at that foot, new extension, repeat. Four passes leave about 2 % error, and it converges as long as the slope times the tangent of the leg tilt (0.38) is below 1, which holds well past a 45° slope.
// per leg, from GTG_BeinFraktionenBeiHub (shortened)
float fraktion = 0;
for (int durchlauf = 0; durchlauf < 4; durchlauf++)
{
// where does the foot land with this extension?
vector fussWelt = montageWelt - achseWelt * (fraktion * maxReichweite);
// what is the ground there? (a trace, not the height map - see below)
float bodenY = GTG_BodenY(welt, Vector(fussWelt[0], montageWelt[1] + 0.3, fussWelt[2]));
// the new extension that reaches it
fraktion = (montageWelt[1] - bodenY) / (achseLokal[1] * maxReichweite);
if (fraktion < 0)
fraktion = 0;
}
If a leg cannot reach the ground at full length, the common height is lowered in steps of 5 % of the wanted lift (at most 40 steps) until all three legs are within range. The lowest leg decides the maximum height, as on a real tripod.
while (schlechtesteFraktion > 1.0 && effektiverHub > 0.0001 && versuch < 40)
{
effektiverHub -= wunschHub * 0.05;
if (effektiverHub < 0)
effektiverHub = 0;
schlechtesteFraktion = GTG_BeinFraktionenBeiHub(baseMat, effektiverHub, achsen, montagepunkte,
maxReichweite, welt, fraktionen);
versuch++;
}
This geometry is not recomputed for every aim update. It is only measured again when the wanted lift, the placement or the yaw changes; before that, aiming cost frame rate, because the tripod re-applied itself every frame and each pass fired a dozen traces.
GetSurfaceY() is the height map. The surface a player actually stands on can be higher. In one measurement the player stood 0.55 m and the tripod root 0.63 m above the height-map value. If the feet or a door point follow the height map, they end up inside the surface. The Core measures the ground with a downward trace (SCR_TerrainHelper.GetTerrainY with a trace on the layers Terrain | Static, the tripod's own parts excluded; where the trace hits nothing it falls back to the height map).A watchdog, honestly named
The device sitting on the ball head moves only on events: mounting, height change, aiming. If something else moves it (a weapon slot, the inventory, physics) nothing corrects it, and the log looks healthy while the device lies at the player's feet. A watchdog compares actual against expected position every 250 ms (tolerance 5 cm), reports it once per incident with both positions, and resets it. A summary follows every five seconds while corrections continue.
[GTG_SOFLAM][WACHHUND] Aufgesetztes Geraet 0.8 m von seinem Platz -- ist <...> soll <...> (Kugelkopf <...>). Verschoben hat es NICHT diese Komponente; wird zurueckgesetzt.