Announcement

Collapse
No announcement yet.

[TUTORIAL] C++ - Runtime Async Load Modular Character (Intermediate)

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

    [TUTORIAL] C++ - Runtime Async Load Modular Character (Intermediate)

    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:

    Click image for larger version

Name:	IMG_01.jpg
Views:	1
Size:	121.5 KB
ID:	1134634

    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
    Code:
    //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
    Code:
    #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.

    Click image for larger version

Name:	IMG_02.jpg
Views:	1
Size:	67.1 KB
ID:	1134635

    Click image for larger version

Name:	IMG_03.jpg
Views:	1
Size:	37.3 KB
ID:	1134636

    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
    Code:
    #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
    Code:
    #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:

    Click image for larger version

Name:	Singleton_Assignment.jpg
Views:	1
Size:	105.5 KB
ID:	1134687

    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
    Code:
    #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
    Code:
    #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
    Code:
    #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!
    Last edited by creasso; 04-21-2016, 01:18 PM. Reason: Code revision 4... 4.11.2 Update
    VIDA - U4 Indie Goth Horror Game
    Project Thread

    #2
    it'll be a while before I use this but I definately will sometime in the future
    thanks for sharing this!

    btw have you tested it for multiplayer purposes? how well does it behave?
    Follow me on Twitter!
    Developer of Elium - Prison Escape
    Local Image-Based Lighting for UE4

    Comment


      #3
      Good tutorial Creasso! This is pretty similar to how we're doing it in fortnite. I have a few suggestions for a cleaner setup:

      Instead of creating the object in your module, I would suggest using GameSingletonClassName. If you set that path in your ini or project settings to the class you want to be your singleton, it'll spawn it for you at game creation time. Then you can access it directly off of the engine. Your case with static variable will behave strangely in certain situations. Here's the relevant code from Fortnite to get our singleton.

      Code:
      UFortGlobals& UFortGlobals::Get()
      {
      	UFortGlobals *Singleton = Cast<UFortGlobals>(GEngine->GameSingleton);
      
      	if (Singleton)
      	{
      		return *Singleton;
      	}
      	else
      	{
      		UE_LOG(LogFort, Fatal, TEXT("Invalid singleton"));
      		return *ConstructObject<UFortGlobals>(UFortGlobals::StaticClass()); // never calls this
      	}
      }
      Also, just as a heads up to people following this tutorial, FStringClassReference and TAssetSubclassOf now exist in the latest version of the engine, and you would use those to refer to blueprints, using basically the same method as the skeletal mesh component here

      Comment


        #4
        @Chosker: Man, I didn't tested on Multiplayer, but I wrote it assuming that to assemble a character on each client we'll need send the enum + 7 int32s (considering 2 more to left and right hand weapon), all this logic will stay on client. Maybe you can improve the needed info, but by now my goal was just to put async dlos to work.

        @Ben:

        Thank you by your input, I did a review on the tutorial and also made changes on my project following your tip, works like a charm.

        Also, reviewing the code seen some misstypes from variable's names, I use "VTypeandHelpers" here instead of "UMyGameSingleton", so I apologize to all if I did some mistakes on replace it on the tutorial.

        Hope that with this review it's OK now.
        VIDA - U4 Indie Goth Horror Game
        Project Thread

        Comment


          #5
          Another thing to consider is to use skeletal mesh merging. While this method allows maximum flexibility each character would use a lot of draw calls. Skeletal mesh merging allows you to merge the skeletal meshes together to form a new skeletal mesh and it will be a single draw call (dependent on the number of materials the final skeletal mesh uses of course). FSkeletalMeshMerge is the class you want to check out.

          Comment


            #6
            I've done basic character customisation in the past with a single mesh.
            As clothing is added, areas that would be covered and/or clip through are masked out.



            Not sure what affect masking has on performance, but the idea works fine.
            Rule#21: Be polite, be professional, but have a plan to kill everyone you meet.

            Comment


              #7
              Hi,

              @Ben,
              What's the difference between : FStringAssetReference and FStringClassReference? because BP is considered as an asset isn't it? You can set them in a FStringAssetReference in the Editor, so what the purpose of those 2 class?

              TAssetSubclassOf is to use for BP, and I suppose for asset also, is there any other class for BP only like String Reference?

              I'm a little bit lost on this point. Might a beginner question, but I didn't get it

              Thanks

              Comment


                #8
                @Solid Snake: Wow! First I must welcome and thank you learnt a lot from your posts in the past regarding UScript (respect ).

                The only "trouble" with mesh merging was that (by what I've seen) the assets should be prepared on a very specific way to this work (following JamesG tip).

                https://forums.unrealengine.com/show...sh-compositing

                So this "one draw call could" come with a lot of other things to consider, I don't know if worths on games where the player will be switching character parts frequently sort of: Ow! New drop! Let me equip this!

                But I'll check anyway and maybe try to implement it.

                Thank you by your tip (as usual)
                VIDA - U4 Indie Goth Horror Game
                Project Thread

                Comment


                  #9
                  @Ben,

                  Regarding FStringClassReference, there is no RequestAsyncLoad that take an array of FStringClassReference as parameter. Is it in the next engine version? I check GitHub and there is no update on StreamableManager since 2 month.

                  Thanks,

                  Comment


                    #10
                    No problems creasso.

                    Yes, there are a lot of rules with mesh merging but it is really handy when used right

                    Comment


                      #11
                      Hi,

                      Did someone succeed in getting this tutorial working with FStringClassReference?

                      Async seems not to be possible as StreamLoader can't manage FStringClassReference.

                      I tried a non async implementation and the editor crash when editing the FStringClassReference on my DataAsset. I raised a bug in the answer hub, but it could be my mistake so I'm opened to suggestion or feedback on wether it's workingon your side.

                      Thanks,

                      Comment


                        #12
                        i dont uderstud how it work, mb some one do video tutor step by step ?

                        Comment


                          #13
                          One question: In ItemInfoDatabase.h, shouldn't FVCharPartInfo() have a data type declared? Visiual Studio had a compiler error when compiling this function.

                          I've set the data type to void FVCharPartInfo() for testing and to get it compiled, but I just start with this tutorial, so I'm not sure if this function will need a return value later on.

                          Comment


                            #14
                            Originally posted by LestatDeLioncourt View Post
                            One question: In ItemInfoDatabase.h, shouldn't FVCharPartInfo() have a data type declared? Visiual Studio had a compiler error when compiling this function.

                            I've set the data type to void FVCharPartInfo() for testing and to get it compiled, but I just start with this tutorial, so I'm not sure if this function will need a return value later on.
                            That looks like a constructor, but it doesn't match the name of the struct. Try charging it to FItemInfo() (without the void), so that it is a constructor for the struct it is part of.

                            Comment


                              #15
                              DISCLAIMER: This is a C++ tutorial if someone get interested on implement the Blueprint part, feel free to.
                              Hi creasso. I desire modular everything in my game (aka lots-of-customization). I would expect a modular pawn to be dynamic or on-demand swappable parts during run-time. Perhaps I misunderstand your context of dynamic. Can you elaborate what this Modular Character C++ implementation is accomplishing, that can not be accomplished with Blueprints Components. I'm a C++ Coder, however, Blueprints Components appears ideal for modular entity construction. I appreciate your time and insight.
                              HeadlessStudios.com is now part of TheGameDevStore.com

                              Comment

                              Working...
                              X