UDamageType how to implement a proper damage system

Hi guys,

I’ve been working with the damage system and am struggling to see a clean way for implementation.
For example, i have a fire damage type, this needs to spread to other players when they touch each, have a vfx particle system that attaches to the character on fire, and do interval based damage for a specific amount of time. Also this fire can affect more than just characters.

Now i recently created a fire damagetype subclass and passed in the world and the actor affected, i then setup timers inside this subclass and controlled all the affects that should happen to the character from within this class. This worked, however, when the fire started to affect another character (after touching each other) the timers ended for the first character on fire and began on the second, so i assume because damagetypes are not state based they don’t create their own instance so cannot run or apply functions to more than one character at a time.

My real question is, whats the best way to create a damage system, should the character have a function to check the damage type and run all functionality on itself, only querying the damage type for values like damage amount and particle vfx to spawn. Or should this all be controlled by the damagetype. I’ve not found any good examples of a damage system and would really appreciate some direction on the way to approach this.

Here’s the code i used for my damagetype:
H File

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/DamageType.h"
#include "IGCBaseDamageType.generated.h"

class AIGCCharacter;

/**
 * The base damage class, all other damage classes should overwrite the process damage function and implement
 the relevant logic there, checking the class of the item they are applying damage too.
 */
UCLASS()
class IGC_API UIGCBaseDamageType : public UDamageType
{
	GENERATED_BODY()
	
public:

	// constructor
	UIGCBaseDamageType(const FObjectInitializer& ObjectInitializer);

	UFUNCTION()
		void ProcessDamage(AActor* TheActor, UWorld* TheWorld);

	UFUNCTION()
		void ApplyFireDamage();

	UFUNCTION()
		void EndFireDamage();

	/** Particle to play when crabs clash claws */
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "VFX")
	UParticleSystem* FireEffectParticle;

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "VFX")
	UParticleSystem* FireOutParticle;
	
private:

	FTimerHandle DamageLengthTimerHandle;
	
	FTimerHandle DamageApplyTimerHandle;
	
	AActor* ActorToDamage;

	UWorld* World;

	AIGCCharacter* FireCharacter;
};

Cpp File

#include "IGCBaseDamageType.h"
#include "IGCFireDamageType.h"
#include "IGCCharacter.h"
#include "Runtime/Engine/Classes/Particles/ParticleSystemComponent.h"
    
    UIGCBaseDamageType::UIGCBaseDamageType(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
    {
    	// fire is caused by the world
    	bCausedByWorld = true;
    
    	//GEngine->AddOnScreenDebugMessage(-1, 4.5f, FColor::Purple, "Base Damage: INITIALIZED");
    }
    
    void UIGCBaseDamageType::ProcessDamage(AActor* TheActor, UWorld* TheWorld)
    {
    	//GEngine->AddOnScreenDebugMessage(-1, 4.5f, FColor::Purple, "Base Damage: Damage Processed");
    
    	// setup access to the actor to damage and the world for the timer
    	if (TheActor)
    	{
    		ActorToDamage = TheActor;
    	}
    
    	if (TheWorld)
    	{
    		World = TheWorld;
    	}
    	
    	// If the timer is already active theres no need to start it again
    	if (!World->GetTimerManager().IsTimerActive(DamageLengthTimerHandle))
    	{
    
    		// Is the fire damage applier timer active already
    		if (!World->GetTimerManager().IsTimerActive(DamageApplyTimerHandle))
    		{
    			// apply fire damage to the player every x seconds
    			World->GetTimerManager().SetTimer(DamageApplyTimerHandle, this, &UIGCBaseDamageType::ApplyFireDamage, 0.5, true);
    
    			GEngine->AddOnScreenDebugMessage(-1, 4.5f, FColor::Purple, "Base Damage: Damage Timer Started");
    		}
    
    		// Cancel the fire damage applier after x seconds
    		World->GetTimerManager().SetTimer(DamageLengthTimerHandle, this, &UIGCBaseDamageType::EndFireDamage, 10, true);
    	}
    
    }
    
    void UIGCBaseDamageType::ApplyFireDamage()
    {
    	// Setup damage to continue applying for fire duration
    	FPointDamageEvent TheDamage;
    	TheDamage.DamageTypeClass = (UIGCFireDamageType::StaticClass());
    	TheDamage.Damage = 40;
    
    	if (ActorToDamage)
    	{
    		// TODO cast to all affected pawn types and handle their damage behaviour
    		FireCharacter = Cast<AIGCCharacter>(ActorToDamage);
    
    		// if its a character
    		if (FireCharacter)
    		{
    			// character is on fire
    			FireCharacter->bIsOnFire = true;
    
    			// if the character is dead end the damage 
    			if (FireCharacter->bIsDead)
    			{
    				// character is no longer on fire because they are DEAD
    				FireCharacter->bIsOnFire = false;
    
    				// stop applying the damage
    				World->GetTimerManager().ClearTimer(DamageApplyTimerHandle);
    
    				// clear the damage length timer
    				World->GetTimerManager().ClearTimer(DamageLengthTimerHandle);
    
    				GEngine->AddOnScreenDebugMessage(-1, 4.5f, FColor::Purple, FString::Printf(TEXT("Fire Damage: FIRE Damage Ended to %s DEAD"), *FireCharacter->GetName()));
    			}
    
    			GEngine->AddOnScreenDebugMessage(-1, 4.5f, FColor::Purple, FString::Printf(TEXT("Fire Damage: APPLYING FIRE Damage to %s"), *FireCharacter->GetName()));
    			FireCharacter->TakeDamage(TheDamage.Damage, TheDamage, nullptr, nullptr);
    		}
    	}
    
    }
    
    void UIGCBaseDamageType::EndFireDamage()
    {
    	
    	// are we applying damage using a timer
    	if (World->GetTimerManager().IsTimerActive(DamageApplyTimerHandle))
    	{
    		// character is no longer on fire
    		FireCharacter->bIsOnFire = false;
    
    		// stop applying the damage
    		World->GetTimerManager().ClearTimer(DamageApplyTimerHandle);
    		
    		// clear the damage length timer
    		World->GetTimerManager().ClearTimer(DamageLengthTimerHandle);
    
    		GEngine->AddOnScreenDebugMessage(-1, 4.5f, FColor::Purple, FString::Printf(TEXT("Fire Damage: FIRE Damage Ended to %s "), *FireCharacter->GetName()));
    	}
    }
1 Like

So the eventual idea was the following:

  • Create a health component for the character (ActorComponent), this handles the characters health by overriding the character damage behaviours (TakeAnyDamage) and processes it based on the damage type. (e.g. applies fire damage every x seconds). This health component also communicates back to the character their current health so they can respond visually to it and pass any information up the chain if required. E.g. dead from fire damage on the HUD.
  • Create a base UDamageType and then extend off this base with values for each damage types, e.g. damage per second, can proliferate. etc
  • Visual setup and behaviours for the character e.g. running around on fire are driven via the character class and triggered when the health component tells the character they have been damage.

I’ve not given code examples of this but if some are required i’m sure i can add some.