In this post we're going to create a simple cover system in the Third Person…
Simple Skills Tree breakdown
In this post we’re going to create a simple skills tree system. Please note that this is not a complete system, nor a step by step tutorial. This post is a breakdown of a simple skills tree system. You can use it as a starting point in order to create your own skills tree system.
Before we get started, here is the end result:
In order to replicate the system shown above you will have to create some blueprints (corresponding screenshots are included both in the previous link and this post).
Assets used
For this post, I’ve used Luos’s Four Elements Pack however any particles will suffice. Please note that based on the particles you will use, you may have to modify the Skill Class.
For example, in this post I’ve used the water and fire attack from Luos’s pack. This pack contains two particle systems for the water and fire attack:
- One particle system which is essentially the projectile for our skill
- One particle system which is the collision of the skill
Having said that, your particles may be differ.
Setting up our project
This post was created using Unreal Engine 4.12.
In order to create the system demonstrated in the video above, I’ve created two classes:
- A skill class which contains the properties of each skill, such as its particles, texture, etc..*
- A skills component class that is assigned to our character, which holds an array of different skills. This component acts as an intermediary between the character and his skills.
*In case you’re developing a fairly complex skills tree, it is highly advised, to create a skill class as an abstract class and then create sub – class for each skill.
So, let’s start our project!
For this post, I have created a Third Person C++ Template project.
Then, I added the Skill class (which inherits from the Actor class):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
UENUM(BlueprintType) enum class ESkillType : uint8 { WaterBlob, FireBall }; UCLASS() class TLSKILLSTREE_API ASkill : public AActor { GENERATED_BODY() private: UFUNCTION() void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit); public: // Sets default values for this actor's properties ASkill(); // Called when the game starts or when spawned virtual void BeginPlay() override; virtual void OnConstruction(const FTransform& Transform) override; /*Increases the level by one - clamps on max level*/ UFUNCTION(BlueprintCallable,Category=TLSkillsTree) void AdvanceLevel() { CurrentLevel = (CurrentLevel + 1 > MaxLevel) ? 1 : ++CurrentLevel; } /*Resets the level of the skill - 0 means that the player will not be able to fire*/ UFUNCTION(BlueprintCallable,Category=TLSkillsTree) void ResetLevel() { CurrentLevel = 0; } /*Returns the level of the current skill*/ UFUNCTION(BlueprintCallable,Category=TLSkillsTree) int32 GetLevel() { return CurrentLevel; } /*Returns the skill's texture*/ UFUNCTION(BlueprintCallable,Category=TLSkillsTree) UTexture* GetSkillTexture() { return SkillTexture; } /*Returns the skill type*/ ESkillType GetSkillType() { return SkillType; } /*Returns true if the level is maxed out*/ bool IsMaxLevel() { return CurrentLevel == MaxLevel; } private: int32 CurrentLevel = 1; int32 MaxLevel = 3; protected: /*Sphere comp used for collision*/ UPROPERTY(VisibleAnywhere) USphereComponent* SphereComp; /*This component is used to simulate the movement of our skill*/ UPROPERTY(VisibleAnywhere) UProjectileMovementComponent* ProjectileMovementComp; /*The particle comp which emits the active particle system*/ UPROPERTY(VisibleAnywhere) UParticleSystemComponent* ParticleComp; /*The particle system for our projectile when traveling*/ UPROPERTY(EditDefaultsOnly) UParticleSystem* ProjectileFX; /*The particle system for our collision*/ UPROPERTY(EditDefaultsOnly) UParticleSystem* ProjectileCollisionFX; /*The skill texture*/ UPROPERTY(EditDefaultsOnly) UTexture* SkillTexture; /*The time (after a collision has happened) that our skill will get destroyed*/ UPROPERTY(EditAnywhere) float DestroyDelay = 1.5f; /*The skill type of the skill*/ UPROPERTY(EditDefaultsOnly) ESkillType SkillType; }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
void ASkill::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit) { if (ProjectileCollisionFX) { //Activate the collision FX and start the destroy timer ParticleComp->SetTemplate(ProjectileCollisionFX); ParticleComp->Activate(true); FTimerHandle TimerHandle; FTimerDelegate TimerDel; TimerDel.BindLambda([&]() { Destroy(); }); GetWorld()->GetTimerManager().SetTimer(TimerHandle, TimerDel, DestroyDelay, false); } } // Sets default values ASkill::ASkill() { // 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; SphereComp = CreateDefaultSubobject<USphereComponent>(FName("SphereComp")); SphereComp->SetCollisionProfileName(FName("BlockAll")); SetRootComponent(SphereComp); //Initializing the movement component and its properties ProjectileMovementComp = CreateDefaultSubobject<UProjectileMovementComponent>(FName("ProjectileMovementComp")); ProjectileMovementComp->ProjectileGravityScale = 0.f; ProjectileMovementComp->InitialSpeed = 3000.f; ParticleComp = CreateDefaultSubobject<UParticleSystemComponent>(FName("ParticleComp")); ParticleComp->SetupAttachment(SphereComp); } // Called when the game starts or when spawned void ASkill::BeginPlay() { Super::BeginPlay(); SphereComp->OnComponentHit.AddDynamic(this, &ASkill::OnHit); if (ProjectileFX) { ParticleComp->SetTemplate(ProjectileFX); ParticleComp->Activate(); } } void ASkill::OnConstruction(const FTransform& Transform) { Super::OnConstruction(Transform); //Used in order to have a visual feedback in the editor when we //assign a new particle if (ProjectileFX) { ParticleComp->SetTemplate(ProjectileFX); ParticleComp->Activate(); } } |
When you’re done with that, add an Actor Component C++ class and name it SkillsComponent. Here is the header and source files:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) ) class TLSKILLSTREE_API USkillsComponent : public UActorComponent { GENERATED_BODY() public: // Sets default values for this component's properties USkillsComponent(); // Called when the game starts virtual void BeginPlay() override; /*An array which contains all the available skills*/ UPROPERTY(EditAnywhere) TArray<TSubclassOf<ASkill>> SkillsArray; /*Returns the texture of the given skill's index (searches SkillsArray)*/ UFUNCTION(BlueprintCallable, Category = TLSkillsTree) UTexture* GetSkillTexture(int32 SkillNum); UFUNCTION(BlueprintCallable, Category = TLSkillsTree) int32 GetSkillLevel(int32 SkillNum); /*Returns the first matching skill from SkillsArray*/ UFUNCTION(BlueprintCallable, Category = TLSkillsTree) ASkill* GetSkillByType(ESkillType SkillType); private: /*The Available Skill Points which can be spent in total*/ int32 AvailableSkillPoints; public: /*Returns the new level of the skill*/ UFUNCTION(BlueprintCallable, Category = TLSkillsTree) int32 AdvanceSkillLevel(ASkill* SkillToLevelUp); /*Resets the skill points and unlearns all the skills*/ UFUNCTION(BlueprintCallable, Category = TLSkillsTree) void ResetSkillPoints(); protected: /*The amount of available skill points when starting the game*/ UPROPERTY(EditDefaultsOnly) int32 InitialAvailableSkillsPoints; }; |
Don’t forget to add the Skill.h reference right before the .generated.h include.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
// Sets default values for this component's properties USkillsComponent::USkillsComponent() { // Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features // off to improve performance if you don't need them. bWantsBeginPlay = true; PrimaryComponentTick.bCanEverTick = true; } // Called when the game starts void USkillsComponent::BeginPlay() { Super::BeginPlay(); //Reseting the level of each skill for (auto Skill : SkillsArray) Skill->GetDefaultObject<ASkill>()->ResetLevel(); AvailableSkillPoints = InitialAvailableSkillsPoints; } UTexture* USkillsComponent::GetSkillTexture(int32 SkillNum) { if (SkillsArray.IsValidIndex(SkillNum)) { return SkillsArray[SkillNum]->GetDefaultObject<ASkill>()->GetSkillTexture(); } return nullptr; } int32 USkillsComponent::GetSkillLevel(int32 SkillNum) { if (SkillsArray.IsValidIndex(SkillNum)) { return SkillsArray[SkillNum]->GetDefaultObject<ASkill>()->GetLevel(); } return 0; } ASkill* USkillsComponent::GetSkillByType(ESkillType SkillType) { for (auto It : SkillsArray) { ASkill* Skill = It->GetDefaultObject<ASkill>(); if (Skill->GetSkillType() == SkillType) return Skill; } return nullptr; } int32 USkillsComponent::AdvanceSkillLevel(ASkill* SkillToLevelUp) { if (SkillToLevelUp && AvailableSkillPoints > 0 && !SkillToLevelUp->IsMaxLevel()) { AvailableSkillPoints--; SkillToLevelUp->AdvanceLevel(); return SkillToLevelUp->GetLevel(); } else if (SkillToLevelUp) return SkillToLevelUp->GetLevel(); else return 0; } void USkillsComponent::ResetSkillPoints() { AvailableSkillPoints = InitialAvailableSkillsPoints; for (auto It : SkillsArray) { It->GetDefaultObject<ASkill>()->ResetLevel(); } } |
So far, we have created a Skill class which has a max level of 3. For this post, each skill level will be the number of skills that the character will spawn with a single button.
This means that in a single click a skill of level:
- 1 – will get spawned one time right in front of the player
- 2 – will spawn two identical skills in a cone right in front of the player
- 3 – will spawn three identical skills
Then, open up the header file of your character and add the following properties and functions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
private: /*Returns a fixed transform based on the given spring arm comp*/ FTransform GetFixedSpringArmTransform(USpringArmComponent* SpringArm); /*Returns an array of transform in order to determine how many skills will get spawned*/ TArray<FTransform> GetSpawnTransforms(int32 level); protected: /*The root component in which the spring arm components will be attached*/ UPROPERTY(VisibleAnywhere) USceneComponent* SkillsRootComp; UPROPERTY(VisibleAnywhere) USpringArmComponent* LevelOneSpringArm; UPROPERTY(VisibleAnywhere) USpringArmComponent* LevelTwoSpringArm; UPROPERTY(VisibleAnywhere) USpringArmComponent* LevelThreeSpringArm; /*Skills Component reference*/ UPROPERTY(VisibleAnywhere/*, meta = (AllowPrivateAccess = "true")*/) USkillsComponent* SkillsComponent; /*Fires a skill*/ UFUNCTION(BlueprintCallable, Category = TLSkillsTree) void Fire(bool bShouldFireSecondary = false); |
Don’t forget to include the SkillsComponent.h header file before the .generated.h include.
We will modify the transform of the spring arm components in the editor in order to achieve the desired spawn location of each skill level.
In order to initialize our components, inside the character’s constructor, type in the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
//Create the root component for our spring arms SkillsRootComp = CreateDefaultSubobject<USceneComponent>(FName("SkillsRootComp")); //Attach it to our root SkillsRootComp->SetupAttachment(RootComponent); //Create the spring arm components and attach them to their root LevelOneSpringArm = CreateDefaultSubobject<USpringArmComponent>(FName("LevelOneSpringArm")); LevelTwoSpringArm = CreateDefaultSubobject<USpringArmComponent>(FName("LevelTwoSpringArm")); LevelThreeSpringArm = CreateDefaultSubobject<USpringArmComponent>(FName("LevelThreeSpringArm")); LevelOneSpringArm->SetupAttachment(SkillsRootComp); LevelTwoSpringArm->SetupAttachment(SkillsRootComp); LevelThreeSpringArm->SetupAttachment(SkillsRootComp); //Initializing the skills component SkillsComponent = CreateDefaultSubobject<USkillsComponent>(FName("SkillsComponent")); |
Then, provide the logic for the rest of our functions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
FTransform ATLSkillsTreeCharacter::GetFixedSpringArmTransform(USpringArmComponent* SpringArm) { FTransform result; if (SpringArm) { result = SpringArm->GetComponentTransform(); //We want a fixed location for our transform, since we don't want to spawn our skills //right on top of our character. result.SetLocation(result.GetLocation() + SpringArm->GetForwardVector() * 100); } return result; } void ATLSkillsTreeCharacter::Fire(bool bShouldFireSecondary) { //This is a dummy logic - we will only have 2 skills for this post TSubclassOf<ASkill> SkillBP = (bShouldFireSecondary && SkillsComponent->SkillsArray.IsValidIndex(1)) ? SkillsComponent->SkillsArray[1] : SkillsComponent->SkillsArray[0]; if (SkillBP) { FActorSpawnParameters ActorSpawnParams; TArray<FTransform> SpawnTransforms = GetSpawnTransforms(SkillBP->GetDefaultObject<ASkill>()->GetLevel()); for (int32 i = 0; i < SpawnTransforms.Num(); i++) { GetWorld()->SpawnActor<ASkill>(SkillBP, SpawnTransforms[i]); } } } TArray<FTransform> ATLSkillsTreeCharacter::GetSpawnTransforms(int32 level) { TArray<FTransform> SpawnPoints; switch (level) { case 1: { SpawnPoints.Add(GetFixedSpringArmTransform(LevelOneSpringArm)); break; } case 2: { SpawnPoints.Add(GetFixedSpringArmTransform(LevelTwoSpringArm)); SpawnPoints.Add(GetFixedSpringArmTransform(LevelThreeSpringArm)); break; } case 3: { SpawnPoints.Add(GetFixedSpringArmTransform(LevelOneSpringArm)); SpawnPoints.Add(GetFixedSpringArmTransform(LevelTwoSpringArm)); SpawnPoints.Add(GetFixedSpringArmTransform(LevelThreeSpringArm)); } default: break; } return SpawnPoints; } |
Compile and save your code.
Creating Blueprint Classes
Create two new Blueprints which inherit from our Skill class and assign the corresponding properties based on your setup:
Then, open up your character’s Blueprint and adjust the position of your spring arm components:
When you’re done with that, open up the SkillsComponent and assign two skills in your skills array and the initial available skill point:
We will get back on our character after we create some widgets first.
Creating a skills menu
Create a Widget Blueprint, named SkillWidget and create the following interface:
Set the red marked items as variables. Then, open up the graph and enter the following logic:
Don’t forget to add a SkillReference variable, since we will use it later.
Compile and save your widget.
Create a new Widget Blueprint, named SkillsPanel and create the following interface:
Make sure to set the marked items as variables. The WaterBlob and FireBall are both SkillWidget instances.
Then. open up the graph of your widget and enter the following logic:
Compile and save your widget.
Setting up inputs
Open up the graph of your character’s Blueprint and enter the following logic:
The SkillsPanelRef is a SkillsWidget Reference.
Compile and save your Blueprint. Then, test your result.
Great tutorial but it is missing one thing – parent system. Currently system you have created can be called skill leveling/advancing system.
A real skill tree is based on roots (called parents) which are needed to level up child skills. For example parent is FireBall and child is FireMeteor – bigger version of FIreBall. In terms of skill tree, common things are strong parents (all of them are needed to level up child) and weak parent (one of them is needed). That system give’s ability for developer to make branches of skills and in effect – skill trees.
You can make it, for example, by creating TArray’s: WeakParents, StrongParents, and check if skills in them are mastered when advancing a level.
You have a valid point, however depending on your game you may or may not need to create a parent (see Torchlight for example). That’s why I mentioned in the top of the post that this is not a complete game system.
-Orfeas
Hi! Does this code
FTimerHandle TimerHandle;
FTimerDelegate TimerDel;
TimerDel.BindLambda([&]()
{
Destroy();
});
GetWorld()->GetTimerManager().SetTimer(TimerHandle, TimerDel, DestroyDelay, false);
equals
SetLifeSpan(DestroyDelay); ? =) Thanks for tutorials.
In this case, yes.
-Orfeas