Project-grade integration

Integrating TerraInk into a fresh Lyra project.

Every change a Lyra-based game needs to make to integrate TerraInk after a clean drop of the plugin. Cloned Lyra, copied TerraInk into Plugins/, ready to wire it up so a ranged weapon paints zones on hit. Build.cs entries, Gameplay-Ability wiring, multiplayer authority, and a verification checklist.

UE 5.6+ Lyra Sample Game Gameplay Ability System Multiplayer
1

Prerequisites

  • UE 5.6 or newer (5.8 recommended, used during plugin development).
  • Lyra Sample Game source available. LyraGame module compiles cleanly before any TerraInk edits.
  • Engine-side plugins TerraInk depends on are enabled: Niagara, PCG, PCGFastGeoInterop, FastGeoStreaming. The TerraInk .uplugin already declares them.
2

Plugin install

Copy the entire TerraInk folder to:

<YourProject>/Plugins/TerraInk/

The plugin ships six modules:

Module Type Purpose
TerraInkRuntimeSubsystem, scatter manager, replicator, types.
TerraInkEditorEditorValidators, Configuration Checklist tool.
TerraInkPCGBridgeRuntimePCG dispatch + custom PCG nodes.
TerraInkFastGeoBridgeRuntimeFastGeo container interop.
TerraInkFastGeoComputeRuntimeFastGeo GPU compute path.
TerraInkTestsDeveloperToolStandalone tests, excluded from Shipping.

<YourProject>.uproject does not need a manual edit; the plugin registers itself when the engine scans Plugins/.

3

Module dependency in LyraGame

Open Source/LyraGame/LyraGame.Build.cs and add "TerraInk" to PublicDependencyModuleNames:

PublicDependencyModuleNames.AddRange(
    new string[] {
        // ...existing entries...
        "TerraInk",
    }
);

This is the only Lyra-side build dependency required. The other TerraInk modules (TerraInkPCGBridge, etc.) load themselves; LyraGame only needs the runtime module.

4

Wire paint into the ranged weapon ability

ULyraGameplayAbility_RangedWeapon is where every bullet impact resolves into an FHitResult – the natural place to call PaintZoneAtHit.

4a. Header: add three authored fields

In Source/LyraGame/Weapons/LyraGameplayAbility_RangedWeapon.h:

#include <TerraInkTypes.h>   // up with the other includes

// inside the class, in the public block
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Lyra|Net Paint")
float PaintRadius = 289.f;

UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Lyra|Net Paint")
ETerraInkPaintLayer PaintLayer = ETerraInkPaintLayer::PaintLayer_R;

UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Lyra|Net Paint")
float PaintIntensity = .15f;

PaintLayer is the fallback layer used when the avatar is not on a team (solo / no-team modes). Team-tagged avatars override it at runtime.

4b. Cpp: includes

Near the top of LyraGameplayAbility_RangedWeapon.cpp:

// TerraInk: weapon impact painting
#include <TerraInkSubsystem.h>
#include <TerraInkTypes.h>

#include "Teams/LyraTeamSubsystem.h"

4c. Cpp: paint at every blocking hit, server-authoritative

Inside OnTargetDataReadyCallback (or wherever OnRangedWeaponTargetDataReady is invoked), gate on authority and call PaintZoneAtHit for each blocking hit:

if (HasAuthority(&CurrentActivationInfo))
{
    if (UTerraInkSubsystem* TerraInkSub = GetWorld()->GetSubsystem<UTerraInkSubsystem>())
    {
        // Resolve team -> layer once per shot. Falls back to the ability's authored
        // PaintLayer for solo/no-team avatars
        ETerraInkPaintLayer ResolvedLayer = PaintLayer;
        if (const ULyraTeamSubsystem* Teams = GetWorld()->GetSubsystem<ULyraTeamSubsystem>())
        {
            const int32 TeamId = Teams->FindTeamFromObject(GetAvatarActorFromActorInfo());
            const ETerraInkPaintLayer Mapped = TerraInk::GetPaintLayerForTeamIndex(TeamId);
            if (Mapped != ETerraInkPaintLayer::None)
            {
                ResolvedLayer = Mapped;
            }
        }

        for (int32 TargetIdx = 0; TargetIdx < LocalTargetDataHandle.Num(); ++TargetIdx)
        {
            if (const FGameplayAbilityTargetData_SingleTargetHit* HitData =
                static_cast<const FGameplayAbilityTargetData_SingleTargetHit*>(LocalTargetDataHandle.Get(TargetIdx)))
            {
                if (HitData->HitResult.bBlockingHit)
                {
                    TerraInkSub->PaintZoneAtHit(
                        HitData->HitResult,
                        PaintRadius,
                        ResolvedLayer,
                        PaintIntensity);
                }
            }
        }
    }
}
Why server-authoritative:
  • The ranged-ability target-data callback fires on the predicting client and the server. Without an authority gate the paint multicasts twice and clients see double-stamped paint.
  • The server's PaintZoneAtHit routes through ATerraInkReplicator::MulticastSurfacePaint, which fans the stroke to every machine, including the originating client. Same latency story as any other replicated effect.

4d. Optional: per-shot style override

If you want different scatter/decals/VFX per weapon, pass a FGameplayTag as the 5th arg:

TerraInkSub->PaintZoneAtHit(HitData->HitResult, PaintRadius, ResolvedLayer, PaintIntensity, WeaponStyleTag);

WeaponStyleTag lives wherever the weapon stores it (PlayerState, weapon instance, ability spec). Empty tag = use the layer's DefaultStyleTag.

5

Gameplay tags

Add a few tags via Project Settings > Project > GameplayTags (or in Config/DefaultGameplayTags.ini):

+GameplayTagList=(Tag="Paint.Style.Default")
+GameplayTagList=(Tag="Paint.Style.Pistol")
+GameplayTagList=(Tag="Paint.Style.Rifle")
+GameplayTagList=(Tag="Paint.Style.Grenade")

Picking a Paint.Style.* namespace makes the meta = (Categories = "Paint.Style") filters on DefaultStyleTag and entry tags surface only relevant choices in the editor picker.

6

Author the data assets

Shortcut for the bundled Lyra demo: this project ships with the SDF-cluster paint path already applied, so it's just run, shoot, and see what's painted. The pre-authored textures, materials and data assets live at /Game/TerraInk — point your world setup actor at those configs to skip the authoring pass below. Read on if you want to build your own.

For a 2-team layout (Splatoon-style) you need two UTerraInkPaintConfig assets:

Asset PaintLayer SplatVisualColor DefaultStyleTag
DA_TeamA_PaintConfig PaintLayer_R red-ish Paint.Style.Default
DA_TeamB_PaintConfig PaintLayer_G blue-ish Paint.Style.Default

Each config carries a Styles map. One entry minimum (the Paint.Style.Default you set above) with at least one tier. Example tier:

Styles
  Paint.Style.Default
    Tiers
      [0]
        MinIntensity = 0.0
        Meshes.ScatterMeshes [optional]
        VFX.Systems          [optional]
        Decals.Materials     [optional]
        PCG.Graphs           [optional]
        bInstantOnHit = true (for one-shot hit scatter)

For per-weapon variation add Paint.Style.Pistol, Paint.Style.Rifle, etc. inside the same map. Both team configs share the same tag scheme; each has its own visuals.

Validation: Validators run on right-click > Validate Asset and surface in the Configuration Checklist tool (Editor > Tools > TerraInk Checklist).
7

Initialize the paint system

You need one of these two paths. The actor is the recommended fast path; manual init is for projects that want game-mode control.

7a. Recommended: drop ATerraInkWorldSetup in the level

ATerraInkWorldSetup is an optional convenience actor. It is not required, only the easiest entry point. Drop it into your gameplay map (or the persistent level for World Partition setups) and configure:

Property Value
ArenaBoundsWorld-space FBox covering the painted area.
PaintLayerConfigsBoth team configs (TeamA + TeamB).
GridResolution32 (default; raise for finer cells).
RTResolution2048 (default).
GroupSize4 (must divide GridResolution).
bSpawnDebugPlanetrue while iterating, off for ship.

The actor's BeginPlay registers each config with the subsystem and calls InitializePaintSystem. No further setup required.

7b. Alternative: manual init from a game mode / experience component

If you do not want a placed actor, do the same handful of calls yourself – for example on your ALyraGameMode::InitGame or an experience-action component's activation:

UTerraInkSubsystem* TerraInkSub = World->GetSubsystem<UTerraInkSubsystem>();
TerraInkSub->SetArenaBounds(MyArenaBounds);
TerraInkSub->RegisterPaintLayerConfig(TeamAConfig);
TerraInkSub->RegisterPaintLayerConfig(TeamBConfig);
TerraInkSub->InitializePaintSystem();

This is the path to take when arena bounds are computed from streamed-in level data, or when the game mode wants to swap configs based on selected map / mode.

8

Optional components

None of these are required for the basic shooting + scatter flow described in steps 1–7. Add them only when the listed feature is needed.

Component Status What it adds
UTerraInkPaintableSurfaceComponent Auto-attached on paint hit by default (bAutoAttachSurfaceOnPaint = true). Binds the live paint render targets into the actor's material slots so visible paint shows up on that mesh. Only needed manually if you turn the auto-attach off.
UTerraInkDynamicPaintComponent Opt-in. Per-actor splat ring buffer in local space, encoded into Custom Primitive Data. Use on movable actors that should carry paint when they move. Capped at ~6 splats per primitive by the 32-float CPD ceiling. See the Dynamic Paint section.
UTerraInkPaintZoneRegistry (world subsystem) Optional, registered automatically when used. Multicast Blueprint events for "actor entered/exited zone of layer X". Subscribe from gameplay BPs that need to react to paint coverage.
Default project posture: leave all components untouched. Add UTerraInkDynamicPaintComponent to specific actor BPs only when an artist asks "why is the enemy not getting painted when it walks through red zones?"
9

Multiplayer notes

  • Server authority on paint events. The authority gate in step 4c is required. ATerraInkReplicator (auto-spawned by the subsystem) handles the multicast.
  • Late-joining clients. Existing paint state on the zone-map RTs replicates via the standard replication graph; the splat cell texture follows the same path. New clients see the current paint state when they join.
  • Config swap mid-session. ATerraInkWorldSetup::SwitchToConfig(Tag) is server-only authority and multicasts to all clients. Used for phase changes (Day → BloodMoon, Calm → Storm).
Known limitation: AOE multicast (MulticastPaintZone) currently does not carry StyleTag; clients fall back to the layer's DefaultStyleTag for replicated paint. Server-local paint and dynamic-paint per-machine hits resolve the explicit tag correctly.
10

Verification checklist

Run through these before declaring the integration done.

  • LyraEditor compiles clean: Engine/Build/BatchFiles/Build.bat LyraEditor Win64 Development -Project=<...>.uproject.
  • Right-click DA_TeamA_PaintConfig > Validate Asset shows no errors.
  • PIE single-player: equipped weapon paints the ground at impacts; tier scatter spawns once intensity exceeds tier 0.
  • PIE 2-client: each client's avatar paints in its own team color (Team 0 → R, Team 1 → G). Both clients see both teams' paint.
  • terraink.DumpStats console command prints non-zero ISM / scatter counts after a few shots.
  • No log warnings from LogTerraInk matching *PaintZone called before InitializePaintSystem* (means the world setup actor never ran).
11

Common pitfalls

  • Forgot authority gate.

    Symptom: double scatter, doubled paint intensity per shot. Fix: wrap the call in if (HasAuthority(&CurrentActivationInfo)) as shown in step 4c.

  • PaintLayerConfigs empty on the world setup actor.

    Symptom: warnings on every paint hit "no config registered for layer N". Either wire the configs or accept the placeholder default config.

  • Team subsystem lookup returns INDEX_NONE.

    Solo modes hit this; the fallback to authored PaintLayer keeps the weapon working. Verify your game mode actually assigns teams via ULyraTeamSubsystem::ChangeTeamForActor (or your own equivalent).

  • DefaultStyleTag is unset on a config.

    Symptom: paint paints the RT but no scatter / VFX / decals spawn. Validator flags this as a Warning at edit time.

  • Tags missing from the gameplay tag manager.

    Tag pickers show empty lists. Add the Paint.Style.* entries to DefaultGameplayTags.ini before authoring configs.