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.


Step 10. Set up a ISKM Proxy for an ISM.