rdInst Tutorial 3.8 – Using Instanced Skinned Meshes
Last Updated: 4th September 2026
Tutorial created with rdInst version 1.54
rdInst supports Instanced Skinned Meshes in rdECS containers, raw spawning from BP and C++ and as Proxies (the Promotion and/or distant object).
This Tutorial will spawn some on the level in both BP and C++ and move them around – then set up a simple ISM swapping to a ISKM when close.
Step 1. Add a new level and create a new BP based on Actor
The first step is to create a Basic Level, delete the floor, and create a new BP based on Actor – call it “BP_SpawnISKMs”. Drag one into the level (at around 0,0,0 – but it doesn’t matter where it is).
Step 2. Create the Variables
We just have a couple of variables to store the rdInst BaseActor reference and the sid for the skm. The movement is rotation based so when we set up the objects, it’s by a height, a radius and an angle – these are stored in an array of vectors and an array of floats for the angles.

Step 3. Create the Functions
Here we have a spawn function and a move function


Step 4. Setup the calls to the Functions

Step 5. Done for the BP side – Play the Level

Step 6. Create a C++ class based on AActor
Create a new C++ class in your project. Depending on the IDE you use the options will be different – in Rider, it’s “New UE Class” for instance. Call it “ArdSpawnISKMs”.

Step 7. Add the properties and function definitions to the header
UCLASS()
class RDTOOLS_580_API ArdSpawnISKMs : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
ArdSpawnISKMs();
UFUNCTION(BlueprintCallable,Category="ISKMs")
void SpawnISKMs(USkeletalMesh* mesh,int32 num);
void RotateISKMs(float DeltaTime);
void RotateISKMsBGT(float DeltaTime);
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="ISKMs")
USkeletalMesh* skeletalMesh=nullptr;
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="ISKMs")
int32 numToSpawn=10000;
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="ISKMs")
float speed=1.0;
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="ISKMs")
bool bUseBackgroundThread=false;
private:
ArdInstBaseActor* base=nullptr;
FName sid;
TArray<FVector> locations;
TArray<FTransform> transforms;
};
Step 8. Implement the GameThread and Background Thread routines in the C++ file
#include "ArdSpawnISKMs.h"
// Sets default values
ArdSpawnISKMs::ArdSpawnISKMs()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
}
// Called when the game starts or when spawned
void ArdSpawnISKMs::BeginPlay()
{
Super::BeginPlay();
UrdInstSubsystem* rdInstSubsystem=GEngine?GEngine->GetEngineSubsystem<UrdInstSubsystem>():nullptr;
if(rdInstSubsystem)
{
base=rdInstSubsystem->rdGetBase();
SpawnISKMs(skeletalMesh,numToSpawn);
}
}
void ArdSpawnISKMs::SpawnISKMs(USkeletalMesh* mesh,int32 num)
{
if (!base) return;
locations.Empty();
transforms.Empty();
base->rdRemoveAllISKInstances();
sid=base->rdGetSKMsid(mesh,ErdSpawnType::NiagaraMesh);
FTransform t(FRotator(0,0,0),FVector(0,0,0),FVector(1,1,1));
locations.SetNum(num);
transforms.SetNum(num);
ParallelFor(num,[this,&t](int32 i)
{
float angle=FMath::FRandRange(0.0f,359.9f);
float radius=FMath::FRandRange(100.0f,5000.0f);
float height=FMath::FRandRange(0.0f,5000.0f);
locations[i]=FVector(radius,angle,height);
t.SetTranslation(FVector(radius,0.0f,height).RotateAngleAxis(angle,FVector(0.0f,0.0f,1.0f)));
transforms[i]=t;
});
int32 rnum=base->rdAddISKInstancesBatch(sid,transforms);
}
// Called every frame
void ArdSpawnISKMs::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (bUseBackgroundThread)
{
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask,[this,DeltaTime]() {
RotateISKMsBGT(DeltaTime);
});
}
else
{
RotateISKMs(DeltaTime);
}
}
void ArdSpawnISKMs::RotateISKMs(float DeltaTime)
{
if (!base || locations.Num()!=transforms.Num()) return;
int32 num=locations.Num();
ParallelFor(num,[this,DeltaTime](int32 i)
{
FTransform& t=transforms[i];
FVector& vec=locations[i];
vec.Y+=DeltaTime*speed;
t.SetTranslation( FVector(vec.X, 0.0f, vec.Z).RotateAngleAxis(vec.Y, FVector(0.0f, 0.0f, 1.0f)));
});
base->rdUpdateISKTransforms(sid,0,transforms);
}
void ArdSpawnISKMs::RotateISKMsBGT(float DeltaTime)
{
if (!base || locations.Num()!=transforms.Num()) return;
int32 num=locations.Num();
TArray<TTuple<int32,FTransform>> tmoveArray;
tmoveArray.SetNum(num);
ParallelFor(num,[this,DeltaTime,&tmoveArray](int32 i)
{
FTransform& t=transforms[i];
FVector& vec=locations[i];
vec.Y+=DeltaTime*speed;
FVector oldVec=t.GetTranslation();
FVector newVec=FVector(vec.X, 0.0f, vec.Z).RotateAngleAxis(vec.Y, FVector(0.0f, 0.0f, 1.0f));
if (base->rdHasTimeSlice(oldVec,newVec))
{
tmoveArray[i]=TTuple<int32,FTransform>(i,t);
t.SetTranslation(newVec);
}
});
if (tmoveArray.Num()>0)
{
rdLock writeLock(base->scopeLock);
TArray<TTuple<int32,FTransform>>& moveArray=base->tmoveMap.FindOrAdd(sid);
moveArray.Append(MoveTemp(tmoveArray));
}
}
Step 9. Done.
