SOFLAM Integration Core
The designator Changed
One component on whatever emits the laser: a handheld device, a vehicle turret, an aircraft. It finds the point, reports it to the registry and keeps it current. What happens with the point afterwards is none of its business.
Two modes, as on the real device
Designate measures and reports: seekers lock on. Range only measures and nothing else – no registry entry, no network message, no seeker. Someone who only wants to know how far something is does not give himself away and does not trigger anything.
Both keys are attributes, and an empty field switches that mode off on a prefab. A second device with different
needs then edits data and never touches script. The component is GTG_SOFLAM_LaserDesignatorComponent; the one on the SOFLAM prefabs is its subclass
GTG_SOFLAM_SoflamDesignatorComponent, which replaces only how the target point is found.
class GTG_SOFLAM_LaserDesignatorComponent : ScriptComponent
{
[Attribute("GTG_SOFLAM_DesignatorAction", UIWidgets.Auto, "Name of the input action that switches the laser")]
protected string m_sToggleAction;
[Attribute("true", UIWidgets.Auto, "true = toggle (once on, once off), false = hold")]
protected bool m_bToggleMode;
[Attribute("GTG_SOFLAM_RangefindAction", UIWidgets.Auto, "Own key: ONLY measure. Empty = no rangefinder.")]
protected string m_sRangefindAction;
[Attribute("true", UIWidgets.Auto, "true = measures while held, false = one measurement per press")]
protected bool m_bRangefinderContinuous;
// ...
}
The keys are hooked up and removed in one method with a bool, SetActionsEnabled(bool enable, bool keepLaser = false). The optic calls it when the player starts and stops looking through the device:
void SetActionsEnabled(bool enable, bool keepLaser = false)
{
if (enable == m_bActionsEnabled)
return;
InputManager inputManager = GetGame().GetInputManager();
if (!inputManager)
return;
m_bActionsEnabled = enable;
if (enable)
{
inputManager.AddActionListener(m_sToggleAction, EActionTrigger.DOWN, OnToggleAction);
if (!m_bToggleMode)
inputManager.AddActionListener(m_sToggleAction, EActionTrigger.UP, OnToggleAction);
// ... rangefinder and tracking key the same way
}
else
{
inputManager.RemoveActionListener(m_sToggleAction, EActionTrigger.DOWN, OnToggleAction);
if (!m_bToggleMode)
inputManager.RemoveActionListener(m_sToggleAction, EActionTrigger.UP, OnToggleAction);
// ... the same lines mirrored
// keepLaser: the device stands on a mount and keeps lasing while the player walks away.
// Only the keys come off - otherwise the next sight would have two listeners per key.
if (!keepLaser)
{
SetRangefinderActive(false);
SetLaserActive(false);
}
}
}
AddActionListener again, and the listeners then pile up on every cycle.The attributes
All of these can be set on the prefab. Defaults are the values in the code.
| Attribute | Default | Meaning |
|---|---|---|
m_sToggleAction | GTG_SOFLAM_DesignatorAction | Input action that switches the laser. Empty = no designating. |
m_bToggleMode | true | Toggle, or hold. |
m_sRangefindAction | GTG_SOFLAM_RangefindAction | Input action that only measures. Empty = no rangefinder. |
m_bRangefinderContinuous | true | Measures while held; false = one measurement per press, the value stays. |
m_sTrackAction | GTG_SOFLAM_TrackingAction | Input action for tracking. Empty = no tracking. |
m_bTrackAimsHead | true | A device on a mount pans after the tracked target. |
m_bTrackFeedback | true | Show a message when tracking goes on or off. |
m_bTrackVehicleCentre | true | Aim at the geometric centre of the vehicle, not at the hit point. |
m_fMaxDistance | 4000 | Longest laser range in metres (the base class's single ray). |
m_sDefaultLaserCode | 1688 | Laser code at start. |
m_fUpdateInterval | 0.1 | Seconds between two position reports (0 = every frame). |
m_fMinMoveDistance | 0.5 | A new report only after the point moved this many metres. |
m_sOverlayLayout, m_sOverlayWidgetName | empty, LaserON | A layout shown while the laser runs, and the widget in it that is shown and hidden. |
m_bDrawDebugTarget | false | Draw the target point as a red sphere. |
m_fLaserPowerMin / Max / Step | 8 / 40 / 1 | Laser power (LV) limits and step per wheel notch. |
m_fSoflamRange (SOFLAM subclass only) | 3500 | Longest measuring range in metres. |
The code dials
The code is four digits, each 1 to 8 ("1688" by default). On the SOFLAM it is set with four copies of one user action, GTG_SOFLAM_LaserCodeDigitAction, which differ only in an attribute
(m_iDigitIndex 0 to 3, start values 1, 6, 8, 8). A device with a different number of wheels gets another copy (and a default code of that length), and no script.
The action does not set the code on the device it sits on. It goes through designator.SetLaserCode(newCode), because that is the one place that distributes a change to the server and everybody else. A wheel that only changed a local field would let the
operator turn it while the seeker kept looking for the old code, without any message. The action runs locally only (HasLocalEffectOnlyScript is true), otherwise the engine would
replicate the action and the designator would send the change: the digit jumped by two.
A point or a vehicle
Without tracking, the designator marks a point: every tick a fresh trace to wherever the device points. When the vehicle drives off, the mark stays on the ground.
With tracking (key U, GTG_SOFLAM_TrackingAction), remember which entity was hit and where on it – in the entity's own coordinate
system. The world point is recomputed from the entity's current transform every time, so the mark travels with the
sheet metal instead of with the line of sight.
// at lock-on (OnTrackAction), shortened
vector mins, maxs;
ziel.GetBounds(mins, maxs);
vector mitte = (mins + maxs) * 0.5;
// the hit point is the worse choice: it is wherever you happened to hold, say the edge of the roof,
// and it swings wildly when the vehicle turns. The centre of the bounds is stable.
if (vector.Distance(mins, maxs) > 0.5)
m_vTrackLocal = mitte;
// every tick
protected vector GTG_TrackedWorldPoint()
{
if (!m_TrackedEntity)
return vector.Zero;
vector mat[4];
m_TrackedEntity.GetWorldTransform(mat);
return mat[3]
+ mat[0] * m_vTrackLocal[0]
+ mat[1] * m_vTrackLocal[1]
+ mat[2] * m_vTrackLocal[2];
}
- Only on a tripod, only vehicles, only while lasing. Tracking needs a ball head to pan, a
Vehiclesomewhere up the parent chain of the entity that was hit, and a running laser. Otherwise a message says why (“Verfolgung nur auf dem Dreibein”, “Kein Fahrzeug unter dem Kreuz”, “Erst markieren, dann verfolgen”). - A trace of its own for the lock. The laser's trace uses
TraceFlags.VISIBILITYso that walls and hills block it. That flag also stopsTraceEntfrom being filled: visibility geometry counts as world, not as an entity. Measured on an armoured Humvee: the beam ended exactly on the vehicle and could not say on what. So the lock-on does a second trace without that flag. - The tracking state is replicated. It used to be a local field. The server ran its own copy that was never tracking, fell back to the camera, and sent that position as authoritatively as the operator sent the correct one. A missile flew correctly for seconds and
then turned away when one of these server messages arrived last. Now tracking goes through the same Ask/Do route as the target (
GTG_RpcAsk_SetTracking/GTG_RpcDo_SetTracking, with theRplIdof the tracked vehicle and the local point). - If you derive your own designator and override
ComputeTargetPosition(), askGTG_TrackingPoint()first, as the base class andGTG_SOFLAM_SoflamDesignatorComponentboth do. The tracking code first lived inside the base version and was dead code, because the prefabs use the subclass, which overrides without callingsuper.
Report rarely, but never stop
An early version sent a reliable packet every frame for every active designator. Now the report is throttled to the interval above and sent only when the point really moved – with one exception, a life sign every two seconds, so that a steady laser does not expire in the registry after five seconds. Calculating is separate from sending: while the target moves the point is recomputed every frame (and the mount follows smoothly); only the sending is throttled, so network traffic does not change.
// from EOnFrame, shortened
float jetztLz = GetGame().GetWorld().GetWorldTime();
bool lebenszeichen = jetztLz - m_fLetztesLebenszeichen >= 2000; // life sign every two seconds
if (!lebenszeichen && m_bHasSentPos && vector.DistanceSq(targetPos, m_vLastSentPos)
< m_fMinMoveDistance * m_fMinMoveDistance)
return; // standing still: no traffic
m_fLetztesLebenszeichen = jetztLz;
m_vLastSentPos = targetPos;
m_bHasSentPos = true;
GTG_SendeZielAktualisieren(targetPos);
All messages are reliable. The unreliable channel has no ordering relative to the reliable one: a position still in flight could arrive after the switch-off and create the point again – a laser that never goes out. The path of a message is described on Who may send.
Limits you measured, not guessed
The laser power is adjustable between two limits (8 and 40 LV, one step per wheel notch with H held). Both were found by driving the device through its range: at the
bottom the beam is clearly weak but still there (at zero the player could not tell a broken device from a switched-off
laser); at the top the bloom becomes noticeable from about 18 m, and beyond 40 the dot washes out half the picture.
The default is not in the script but at the light prefab (GTG_SOFLAM_IRLaserLight.et, LV 25): until someone turns the wheel, the designator's value is −1, “nothing set”, and the prefab value
applies. One truth in one place. Once set, the value replicates to everybody, late joiners included, because brightness belongs to the device and not to the viewer.