DISCLAIMER: This is a C++ tutorial if someone get interested on implement the Blueprint part, feel free to.
Hi!
Now, let’s suppose you are doing a game where you’ll need a “LOT” of character customization, imagine that your characters will need to exchange
gloves, boots, chest armor, legs armor, and helmet, but it’s not just this, you’ll have also gender selection and classes… Well, I don’t wanna
be on such situation!
But I am.
And I thought that some of you also would, well, how to manage such variety? And make it flexible enough to your artists do the items’
model database while you are focusing into coding the “remaining”? Well, my first problem on port my UDK game to U4 was exactly rewrite my “modular
pawn”, allowing some dynamic loads. U4 has a nice way on doing this asyncronously but to implement the system… Wow, you’ll have to deal at least
with three other features.
This “tour” to anyone starting to deal with U4C++ is a nice way to get introduced to the current state of the engine, of course it’s a “just working”
thing, I don’t know if it’s perfect, so if you get ideas about some optimizations feel free to share, also to the Epic Guys, and more experienced
programmers if I did something wrong please point! I don’t wanna be distributing and using “leaking code” lol.
STAGE 1 - THE ITEMS DATABASE CLASS
One nice thing about Async Loading is that it’s based on classes that are elegant on the editor: FStringAssetReference, and TAssetPtr<class> being
this second a “filtered allowed” way to look for and assign just objects from some class as reference to a resource. On editor it shows more or
less on this way:
By filtering on your class, your buddies artists will not assign a Skeleton or any other kind of asset inside a list that should just to contain
SkeletalMeshes or as you know, our code would break, so “filter” is a nice way to prevent further problems.
To implement the class you’ll need a struct that boxes a TAssetPtr and also an ID (I’m using int 32 to it) to don’t need doing searchs by
strings, you’ll declare both this struct and class on the same file.
The Database class is derived from UDataAsset what allows to be “creatable, fillable and usable” trough the U4 editor.
Here is the code:
ItemInfoDatabase.h
//Holds the information about a single character "part"
USTRUCT()
struct FItemInfo
{
GENERATED_USTRUCT_BODY()
UPROPERTY(EditAnywhere, Category = "DATA", meta = (ToolTip = "The Mesh ID"))
int32 MeshID;
UPROPERTY(EditAnywhere, Category = "DATA", meta = (ToolTip = "Related Asset"))
TAssetPtr<USkeletalMesh> MeshResource;
FVCharPartInfo()
{
MeshID = 0;
MeshResource = FStringAssetReference("");
}
};
//Holds a dynamic collection of character parts
UCLASS()
class UItemInfoDatabase : public UDataAsset
{
GENERATED_UCLASS_BODY()
UPROPERTY(EditAnywhere, Category = "Model List", meta = (ToolTip = "Asset Info")) //Exposes the array as editable on editor
TArray<FItemInfo> MeshList;
public:
UItemInfoDatabase(UItemInfoDatabase&);
UItemInfoDatabase();
};
The cpp file just holds the constructor.
ItemInfoDatabase.cpp
#include "ItemInfoDatabase.h"
UItemInfoDatabase::UItemInfoDatabase(const class FPostConstructInitializeProperties& PCIP):Super(PCIP)
{
}
If you compile this, on next time your artists try to create a UDataAsset, they can point ItemInfoDatabase class as base and so, go adding new
items to the list and just needs to assign the same LIST INDEX to IDs to allow you to easy call everything after, if they keep an Excel table
about what’s going where will also make your life a lot easier.
While our artists works we still have lots of work to do…
STAGE 2 - FStreamableManager and the Singleton of Doom
If we do a check on the Async Loading page we’ll see that the author points that a nice place to put the FStreamableManager is on a “Singleton”
class, well, now I know that this is a class that can holds the Game StateMachine and our global variables, but to discover this and get a sample
to implement… OMG… Well, I’ll save your time and put the entire thing below, don’t think that I figured by myself, it’s based on UGameShooterKing
from Epic and after 05/14/2014 we got a review from Ben Zeigler to improve it.
MyGameSingleton.h
#include "object.h"
//A Singleton class based on Epic Sample
UCLASS(Config=Game, notplaceable) // < needed flags
class UMyGameSingleton : public UObject , public FTickerObjectBase // Notice the multiple inheritance, C++ is a monster :D
{
GENERATED_UCLASS_BODY()
private:
UMyGameSingleton(); // On privatize the constructor it will be just built by the engine once
public:
static UMyGameSingleton& Get(); // Get method to access this object
FStreamableManager AssetLoader; // Your asset loader
// Override from FTicker::Tick()
// if you forget to Override this and implement on cpp file you'll get Error 2259
virtual bool Tick(float DeltaSeconds) OVERRIDE;
};
MyGameSingleton.cpp
#include "OurGameModule.h" // this file is that one with the call to "IMPLEMENT_PRIMARY_GAME_MODULE"
#include "MyGameSingleton.h"
// Default constructor
UMyGameSingleton::UMyGameSingleton(const class FPostConstructInitializeProperties& PCIP) : Super(PCIP)
{
}
//Get method to access the instance
UMyGameSingleton& UMyGameSingleton::Get()
{
UMyGameSingleton *Singleton = Cast<UMyGameSingleton>(GEngine->GameSingleton);
if (Singleton)
{
return *Singleton;
}
else
{
return *ConstructObject<UMyGameSingleton>(UMyGameSingleton::StaticClass()); // never calls this
}
}
// OVERRIDE from FTICKER TICK (Preventing Error 2259)
bool UMyGameSingleton::Tick(float DeltaSeconds)
{
return true;
}
Well, this almost finishes our work with the Singleton, but you’ll notice that if the engine doesn’t knows which class to use
as Singleton, our cast will fail. Ben Zeigler said the most effective way to assign a Singleton class is on the editor.
So you can access this option on MENU EDIT > Project Settings:
After do the assignment, press the Set As Default button and this will override the DefaultEngine.ini inserting the line about
the Singleton class.
Ok! Finaly we got all pre-requisites to implement the customization system on our character class…
STAGE 3 - Some last moment preparations
Before we begin here, let’s remember that to make an Async loading we’ll not use JUST ONE, BUT TWO METHODS.
This is because FStreamableManager if we watch on docs does Async Loading on a 2 part process:
-
- On the first, we’ll “request” the asset loading and on the same point a second method (delegate) to the machine execute when loading is ready.
-
- On the second, we’ll really do the SkeletalMesh assignment extracting it from where the first method loaded it.
Also, remember that we are doing this not just to deal with a single collection of character parts, but so many as we need.
I do preffer “hardcode” the paths to collections, thinking that these characters will be spawned by code and will need “to pick” a collection
path based on the data from the “savefile”. If I need to have one character Blueprint to each class and gender my game will need to
consider around 10 different class spawns if we consider just a scenario of 5 races (let’s suppose: elf, dwarf, human, martian and goblins) and 2 genders
OR I can simply have N “hardcoded path databases” on a single Character Class and just make a “switch” to load the one I’ll need.
So, the first thing I’ll do before begin our CharacterClass will be go back to the Singleton file (do you remember that we can use it to store things
that we’ll need watch from other places?) and define a new “enum” to help me sort the different character’s setups we could have, to this example,
I’ll just use one option, but keep in mind that we can have any number of them.
Back to…
MyGameSingleton.h
#include "object.h"
//A Singleton class based on Epic Sample
// *** I like to declare enums and structs before the classes
UENUM()
enum FCharacterRace
{
RACESEX_ElfMale,
RACESEX_ElfFemale // You could proceed just typing "," and inserting new races-***
};
// *** done!
UCLASS(Config=Game, notplaceable) // < needed flags
Ok, now we need to define how many “slots” you need to assemble a character, let’s suppose 5 (gloves, boots, chest armor, legs armor, and helmet),
so you tell your artists to assume at least the “first armor tier” to ALL characters on this way:
- 0 - Helmet
- 1 - Chest
- 2 - Hands
- 3 - Legs
- 4 - Feet
And assign the right asset to each, of course you can write your save file to store 5 different “armor parts codes”, read it and just do a direct
assignment, but “by now” we’ll use these numbers as the basic ones to start a “NEW” character, now we are ready to code a bit more.
STAGE 4 - The MultiPartCharacter class
We need to begin calling the Singleton Header and also the ItemInfoDatabase one to our character class understand the FCharacterRace enum, and also
be capable to understand our UDataAsset, with this, we could just manage on getting the pieces we need.
Character_MultiPart.h
#pragma once
#include "MyGameSingleton.h"
#include "ItemInfoDatabase.h"
#include "GameFramework/Character.h"
#include "Character_MultiPart.generated.h"
UCLASS() //Based on Shootergame Character from Epic
class ACharacter_MultiPart : public ACharacter
{
GENERATED_UCLASS_BODY()
//MESHES AND NAMES
UPROPERTY(Category = "Body", VisibleAnywhere, BlueprintReadOnly)
TSubobjectPtr<class USkeletalMeshComponent> HeadMSHComponent;
static FName HeadComponentName;
UPROPERTY(Category = "Body", VisibleAnywhere, BlueprintReadOnly)
TSubobjectPtr<class USkeletalMeshComponent> ChestMSHComponent;
static FName ChestComponentName;
UPROPERTY(Category = "Body", VisibleAnywhere, BlueprintReadOnly)
TSubobjectPtr<class USkeletalMeshComponent> LegsMSHComponent;
static FName LegsComponentName;
UPROPERTY(Category = "Body", VisibleAnywhere, BlueprintReadOnly)
TSubobjectPtr<class USkeletalMeshComponent> FeetMSHComponent;
static FName FeetComponentName;
UPROPERTY(Category = "Body", VisibleAnywhere, BlueprintReadOnly)
TSubobjectPtr<class USkeletalMeshComponent> HandsMSHComponent;
static FName HandsComponentName;
FCharacterRace ThisCharRace; // enum to switch control
FString DatabasePath; // possible hardcoded paths
//-------MESHES AND NAMES
public:
/** spawn inventory, setup initial variables */
virtual void PostInitializeComponents() OVERRIDE;
// Direct ID mesh changing "request" methods, usable with inventory loading, swaping systems... etc
bool ChangeHeadMeshByID(int32 IDCode);
bool ChangeChestMeshByID(int32 IDCode);
bool ChangeLegsMeshByID(int32 IDCode);
bool ChangeFeetMeshByID(int32 IDCode);
bool ChangeHandsMeshByID(int32 IDCode);
private:
// We need different places to load each part to don't risk override a loading process
FStringAssetReference HeadAssetToLoad;
FStringAssetReference ChestAssetToLoad;
FStringAssetReference LegsAssetToLoad;
FStringAssetReference FeetAssetToLoad;
FStringAssetReference HandsAssetToLoad;
// Method to setup/initialize skeletal mesh components
void InitSkeletalMeshComponent(TSubobjectPtr<class USkeletalMeshComponent> SMeshPointer, bool AttachToParent);
// Method to setup the new character
void InitDefaultMeshes(int32 HeadID, int32 ChestID, int32 HandsID, int32 LegsID, int32 FeetID);
// Delegates to be "shoot" at end of loading processes
void DoAsyncHeadMeshChange();
void DoAsyncChestMeshChange();
void DoAsyncLegsMeshChange();
void DoAsyncFeetMeshChange();
void DoAsyncHandsMeshChange();
};
OK, now let’s put all this together…
Character_MultiPart.cpp
#include "VIDA.h"
#include "Character_MultiPart.h"
FName ACharacter_MultiPart::HeadComponentName(TEXT("HeadMeshComponent"));
FName ACharacter_MultiPart::ChestComponentName(TEXT("ChestMeshComponent"));
FName ACharacter_MultiPart::LegsComponentName(TEXT("LegsMeshComponent"));
FName ACharacter_MultiPart::FeetComponentName(TEXT("FeetMeshComponent"));
FName ACharacter_MultiPart::HandsComponentName(TEXT("HandsMeshComponent"));
ACharacter_MultiPart::ACharacter_MultiPart(const class FPostConstructInitializeProperties& PCIP) :
Super(PCIP.DoNotCreateDefaultSubobject(TEXT("CharacterMesh0"))) // <- First let's get rid from the "default" Mesh
{
// Components' initialization and creation
HeadMSHComponent = PCIP.CreateOptionalDefaultSubobject<USkeletalMeshComponent>(this, ACharacter_MultiPart::HeadComponentName);
if (HeadMSHComponent)
InitSkeletalMeshComponent(HeadMSHComponent, false); // On each, we call the setup method below
ChestMSHComponent = PCIP.CreateOptionalDefaultSubobject<USkeletalMeshComponent>(this, ACharacter_MultiPart::ChestComponentName);
if (ChestMSHComponent)
InitSkeletalMeshComponent(ChestMSHComponent, true);
LegsMSHComponent = PCIP.CreateOptionalDefaultSubobject<USkeletalMeshComponent>(this, ACharacter_MultiPart::LegsComponentName);
if (LegsMSHComponent)
InitSkeletalMeshComponent(LegsMSHComponent, true);
HandsMSHComponent = PCIP.CreateOptionalDefaultSubobject<USkeletalMeshComponent>(this, ACharacter_MultiPart::HandsComponentName);
if (HandsMSHComponent)
InitSkeletalMeshComponent(HandsMSHComponent, true);
FeetMSHComponent = PCIP.CreateOptionalDefaultSubobject<USkeletalMeshComponent>(this, ACharacter_MultiPart::FeetComponentName);
if (FeetMSHComponent)
InitSkeletalMeshComponent(FeetMSHComponent, true);
// Just initializing...
ThisCharRace = FCharacterRace::RACESEX_ElfFemale;
DatabasePath = " ";
}
// Initializes a SkeletalMeshComponent, if AttachToParent is true, assigns the head as Animation master
void ACharacter_MultiPart::InitSkeletalMeshComponent(TSubobjectPtr<class USkeletalMeshComponent> SMeshPointer, bool AttachToParent)
{
SMeshPointer->AlwaysLoadOnClient = true;
SMeshPointer->AlwaysLoadOnServer = true;
SMeshPointer->bOwnerNoSee = false;
SMeshPointer->MeshComponentUpdateFlag = EMeshComponentUpdateFlag::AlwaysTickPose;
SMeshPointer->bCastDynamicShadow = true;
SMeshPointer->bAffectDynamicIndirectLighting = true;
SMeshPointer->PrimaryComponentTick.TickGroup = TG_PrePhysics;
// force tick after movement component updates
if (CharacterMovement)
{
SMeshPointer->PrimaryComponentTick.AddPrerequisite(this, CharacterMovement->PrimaryComponentTick);
}
SMeshPointer->bChartDistanceFactor = false;
SMeshPointer->AttachParent = CapsuleComponent;
static FName CollisionProfileName(TEXT("CharacterMesh"));
SMeshPointer->SetCollisionObjectType(ECC_Pawn);
SMeshPointer->SetCollisionProfileName(CollisionProfileName);
SMeshPointer->bGenerateOverlapEvents = false;
// Mesh acts as the head, as well as the parent for both animation and attachment.
if (AttachToParent)
{
SMeshPointer->AttachParent = HeadMSHComponent;
SMeshPointer->SetMasterPoseComponent(HeadMSHComponent);
SMeshPointer->bUseBoundsFromMasterPoseComponent = true;
}
}
// Mesh assignment should be done after not just character, but other game objects created, if a save file system does exist, we could
// be getting the MeshIDs also here
void ACharacter_MultiPart::PostInitializeComponents()
{
Super::PostInitializeComponents();
ThisCharRace = FCharacterRace::RACESEX_ElfMale; //<- This could be coming from a save file overriding
// Picks the right Database Path and name;
switch (ThisCharRace)
{
case FCharacterRace::RACESEX_ElfMale:
DatabasePath = "/Game/SubDirectory/ElfMaleData.ElfMaleData"; // <- You need databases to load from!
break;
default:
DatabasePath = "/Game/SubDirectory/ElfFemaleData.ElfFemaleData"; // <- You need databases to load from!
break
}
// Call to load library and setup meshes
InitDefaultMeshes(0,1,2,3,4);
}
void ACharacter_MultiPart::InitDefaultMeshes(int32 HeadID, int32 ChestID, int32 HandsID, int32 LegsID, int32 FeetID)
{
// Registers the loading requests
ChangeHeadMeshByID(HeadID);
ChangeHeadMeshByID(ChestID);
ChangeHeadMeshByID(HandsID);
ChangeHeadMeshByID(LegsID);
ChangeHeadMeshByID(FeetID);
}
// Head Change Request
bool ACharacter_MultiPart::ChangeHeadMeshByID(int32 IDCode)
{
// Loads the desired database trough StaticLoad, deferencing a FString results on TCHAR that is the Path text format
UItemInfoDatabase* LoadedMeshesDatabase = Cast<UItemInfoDatabase>(StaticLoadObject(UVMultiPartCharDATA::StaticClass(), NULL, *DatabasePath, NULL, LOAD_None, NULL));
if (LoadedMeshesDatabase != NULL && LoadedMeshesDatabase->MeshList.Num() >= IDCode)
{
TArray<FStringAssetReference> ObjToLoad;
FStreamableManager& BaseLoader = MyGameSingleton::Get().AssetLoader;
HeadAssetToLoad = LoadedMeshesDatabase->MeshList[IDCode].MeshResource.ToStringReference();
ObjToLoad.AddUnique(HeadAssetToLoad);
BaseLoader.RequestAsyncLoad(ObjToLoad, FStreamableDelegate::CreateUObject(this, &ACharacter_MultiPart::DoAsyncHeadMeshChange));
return true;
}
return false;
}
// Chest Change Request
bool ACharacter_MultiPart::ChangeChestMeshByID(int32 IDCode)
{
// Loads the desired database trough StaticLoad, deferencing a FString results on TCHAR that is the Path text format
UItemInfoDatabase* LoadedMeshesDatabase = Cast<UItemInfoDatabase>(StaticLoadObject(UVMultiPartCharDATA::StaticClass(), NULL, *DatabasePath, NULL, LOAD_None, NULL));
if (LoadedMeshesDatabase != NULL && LoadedMeshesDatabase->MeshList.Num() >= IDCode) // Prevents out of index access
{
TArray<FStringAssetReference> ObjToLoad; // Unfortunatelly, Asyncs just accepts arrays
FStreamableManager& BaseLoader = MyGameSingleton::Get().AssetLoader; // Gets our Asset Loader
ChestAssetToLoad = LoadedMeshesDatabase->MeshList[IDCode].MeshResource.ToStringReference(); //Extracts the path/obj
ObjToLoad.AddUnique(ChestAssetToLoad); // Puts our loading task into array
BaseLoader.RequestAsyncLoad(ObjToLoad, FStreamableDelegate::CreateUObject(this, &ACharacter_MultiPart::DoAsyncChestMeshChange)); //Assigns the delegate to the task end
return true;
}
return false;
}
// Legs Change Request
bool ACharacter_MultiPart::ChangeLegsMeshByID(int32 IDCode)
{
// Loads the desired database trough StaticLoad, deferencing a FString results on TCHAR that is the Path text format
UItemInfoDatabase* LoadedMeshesDatabase = Cast<UItemInfoDatabase>(StaticLoadObject(UVMultiPartCharDATA::StaticClass(), NULL, *DatabasePath, NULL, LOAD_None, NULL));
if (LoadedMeshesDatabase != NULL && LoadedMeshesDatabase->MeshList.Num() >= IDCode)
{
TArray<FStringAssetReference> ObjToLoad;
FStreamableManager& BaseLoader = MyGameSingleton::Get().AssetLoader;
LegsAssetToLoad = LoadedMeshesDatabase->MeshList[IDCode].MeshResource.ToStringReference();
ObjToLoad.AddUnique(LegsAssetToLoad);
BaseLoader.RequestAsyncLoad(ObjToLoad, FStreamableDelegate::CreateUObject(this, &ACharacter_MultiPart::DoAsyncLegsMeshChange));
return true;
}
return false;
}
// Feet Change Request
bool ACharacter_MultiPart::ChangeFeetMeshByID(int32 IDCode)
{
// Loads the desired database trough StaticLoad, deferencing a FString results on TCHAR that is the Path text format
UItemInfoDatabase* LoadedMeshesDatabase = Cast<UItemInfoDatabase>(StaticLoadObject(UVMultiPartCharDATA::StaticClass(), NULL, *DatabasePath, NULL, LOAD_None, NULL));
if (LoadedMeshesDatabase != NULL && LoadedMeshesDatabase->MeshList.Num() >= IDCode)
{
TArray<FStringAssetReference> ObjToLoad;
FStreamableManager& BaseLoader = MyGameSingleton::Get().AssetLoader;
FeetAssetToLoad = LoadedMeshesDatabase->MeshList[IDCode].MeshResource.ToStringReference();
ObjToLoad.AddUnique(FeetAssetToLoad);
BaseLoader.RequestAsyncLoad(ObjToLoad, FStreamableDelegate::CreateUObject(this, &ACharacter_MultiPart::DoAsyncFeetMeshChange));
return true;
}
return false;
}
// Hands Change Request
bool ACharacter_MultiPart::ChangeHandsMeshByID(int32 IDCode)
{
// Loads the desired database trough StaticLoad, deferencing a FString results on TCHAR that is the Path text format
UItemInfoDatabase* LoadedMeshesDatabase = Cast<UItemInfoDatabase>(StaticLoadObject(UVMultiPartCharDATA::StaticClass(), NULL, *DatabasePath, NULL, LOAD_None, NULL));
if (LoadedMeshesDatabase != NULL && LoadedMeshesDatabase->MeshList.Num() >= IDCode)
{
TArray<FStringAssetReference> ObjToLoad;
FStreamableManager& BaseLoader = MyGameSingleton::Get().AssetLoader; // Colhe referência para o Asset Loader
HandsAssetToLoad = LoadedMeshesDatabase->MeshList[IDCode].MeshResource.ToStringReference(); // Desencapsula o path
ObjToLoad.AddUnique(HandsAssetToLoad);
BaseLoader.RequestAsyncLoad(ObjToLoad, FStreamableDelegate::CreateUObject(this, &ACharacter_MultiPart::DoAsyncHandsMeshChange));
return true;
}
return false;
}
// DELEGATE - Do the Async Head Change
void ACharacter_MultiPart::DoAsyncHeadMeshChange()
{
check(HeadAssetToLoad.ResolveObject() != nullptr)
{
UObject* NewHeadMesh = HeadAssetToLoad.ResolveObject(); // Creates a pointer to store the loaded object
HeadMSHComponent->SetSkeletalMesh(Cast<USkeletalMesh>(NewHeadMesh)); // Casts and assigns
// ******** UPDATE 4.11.2 Check Notes *****
ChestMSHComponent->SetMasterPoseComponent(HeadMSHComponent);
LegsMSHComponent->SetMasterPoseComponent(HeadMSHComponent);
FeetMSHComponent->SetMasterPoseComponent(HeadMSHComponent);
FeetMSHComponent->SetMasterPoseComponent(HeadMSHComponent);
}
}
// DELEGATE - Do the Async Chest Change
void ACharacter_MultiPart::DoAsyncChestMeshChange()
{
check(ChestAssetToLoad.ResolveObject() != nullptr)
{
UObject* NewChestMesh = ChestAssetToLoad.ResolveObject();
ChestMSHComponent->SetSkeletalMesh(Cast<USkeletalMesh>(NewChestMesh));
}
}
// DELEGATE - Do the Async Legs Change
void ACharacter_MultiPart::DoAsyncLegsMeshChange()
{
check(LegsAssetToLoad.ResolveObject() != nullptr)
{
UObject* NewLegsMesh = LegsAssetToLoad.ResolveObject();
LegsMSHComponent->SetSkeletalMesh(Cast<USkeletalMesh>(NewLegsMesh));
}
}
// DELEGATE - Do the Async Feet Change
void ACharacter_MultiPart::DoAsyncFeetMeshChange()
{
check(FeetAssetToLoad.ResolveObject() != nullptr)
{
UObject* NewFeetMesh = FeetAssetToLoad.ResolveObject();
FeetMSHComponent->SetSkeletalMesh(Cast<USkeletalMesh>(NewFeetMesh));
}
}
// DELEGATE - Do the Async Hands Change
void ACharacter_MultiPart::DoAsyncHandsMeshChange()
{
check(HandsAssetToLoad.ResolveObject() != nullptr)
{
UObject* NewHandsMesh = HandsAssetToLoad.ResolveObject();
HandsMSHComponent->SetSkeletalMesh(Cast<USkeletalMesh>(NewHandsMesh));
}
}
Phew! Now if you have the database with at least one asset, made a blueprint from this class and place on your level
you’ll see just the Capsule Component, but if you press Play or Simulate on editor the magic will happens! Notice that you
can use this method to load anything anywhere you wants.
Enjoy!