Avatar
LV. 999+

Parsa Rahimian

Gameplay Programmer & Technical Designer

Focused on systems-first design and gameplay programming, skilled in designing and developing gameplay mechanics. 4 years of experience in game development, emphasizing teamwork and meaningful gameplay experiences.

Expertise

Unreal Engine Unreal Engine
C++ C++
Optimization Optimization
Game Design Technical Game Design

Projects

Indie game development, prototyping, research-driven design, experimental mechanics and narrative systems.


Surrena's GIF
Surrena's Level Up System

// Copyright Parsa Rahimian

#include "AbilitySystem/SurrenaAbilitySystemComponent.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "SurrenaGameplayTags.h"
#include "AbilitySystem/SurrenaAbilitySystemLibrary.h"
#include "AbilitySystem/Abilities/SurrenaGameplayAbility.h"
#include "AbilitySystem/Data/AbilityInfo.h"
#include "Interaction/PlayerInterface.h"

void USurrenaAbilitySystemComponent::AbilityActorInfoSet()
{
    // Bind delegate to detect when a gameplay effect is applied to self
    OnGameplayEffectAppliedDelegateToSelf.AddUObject(this, &USurrenaAbilitySystemComponent::ClientEffectApplied);

    const FSurrenaGameplayTags& GameplayTags = FSurrenaGameplayTags::Get();
    // Could initialize or cache tags here if needed
}

// Add abilities to the character on startup
void USurrenaAbilitySystemComponent::AddCharacterAbilities(const TArray>& StartupAbilities)
{
    for (const TSubclassOf AbilityClass : StartupAbilities)
    {
        int32 PlayerLevel{1};

        // Get player level if actor implements the PlayerInterface
        if (GetAvatarActor()->Implements()) {
            PlayerLevel = IPlayerInterface::Execute_GetPlayerLevel(GetAvatarActor());
        }

        // Create ability spec with level
        FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(AbilityClass, PlayerLevel);

        // If ability is a SurrenaGameplayAbility, assign tags
        if (const USurrenaGameplayAbility* SurrenaAbility = Cast(AbilitySpec.Ability)) 
        {
            AbilitySpec.DynamicAbilityTags.AddTag(SurrenaAbility->StartupInputTag);
            AbilitySpec.DynamicAbilityTags.AddTag(FSurrenaGameplayTags::Get().Abilities_Status_Equipped);
            GiveAbility(AbilitySpec);
        }
    }

    bStartUpAbilitiesGiven = true;
    AbilitiesGivenDelegate.Broadcast();
}

// Add passive abilities that activate once on startup
void USurrenaAbilitySystemComponent::AddCharacterPassiveAbilities(const TArray>& StartupPassiveAbilities)
{
    for (const TSubclassOf AbilityClass : StartupPassiveAbilities)
    {
        FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(AbilityClass, 1);
        GiveAbilityAndActivateOnce(AbilitySpec);
    }
}

// Server-side function to spend a spell point on a specific ability
void USurrenaAbilitySystemComponent::ServerSpendSpellPoint_Implementation(const FGameplayTag& AbilityTag)
{
    if (FGameplayAbilitySpec* AbilitySpec = GetSpecFromAbilityTag(AbilityTag))
    {
        if (GetAvatarActor()->Implements())
        {
            IPlayerInterface::Execute_AddToSpellPoints(GetAvatarActor(), -1);
        }

        const FSurrenaGameplayTags GameplayTags = FSurrenaGameplayTags::Get();
        FGameplayTag Status = GetStatusFromSpec(*AbilitySpec);

        // Upgrade ability if eligible
        if (Status.MatchesTagExact(GameplayTags.Abilities_Status_Eligible))
        {
            AbilitySpec->DynamicAbilityTags.RemoveTag(GameplayTags.Abilities_Status_Eligible);
            AbilitySpec->DynamicAbilityTags.AddTag(GameplayTags.Abilities_Status_Unlocked);
            Status = GameplayTags.Abilities_Status_Unlocked;
        }
        // Level up ability if already equipped or unlocked
        else if (Status.MatchesTagExact(GameplayTags.Abilities_Status_Equipped) || Status.MatchesTagExact(GameplayTags.Abilities_Status_Unlocked))
        {
            AbilitySpec->Level += 1;
        }

        ClientUpdateAbilityStatus(AbilityTag, Status, AbilitySpec->Level);
        MarkAbilitySpecDirty(*AbilitySpec);
    }
}

// Helper: Get ability status from a tag
FGameplayTag USurrenaAbilitySystemComponent::GetStatusFromAbilityTag(const FGameplayTag& AbilityTag)
{
    if (const FGameplayAbilitySpec* Spec = GetSpecFromAbilityTag(AbilityTag))
    {
        return GetStatusFromSpec(*Spec);
    }
    return FGameplayTag();
}

// Get the input tag (slot) associated with a spec
FGameplayTag USurrenaAbilitySystemComponent::GetInputTagFromSpec(const FGameplayAbilitySpec& AbilitySpec)
{
    for (FGameplayTag Tag : AbilitySpec.DynamicAbilityTags)
    {
        if (Tag.MatchesTag(FGameplayTag::RequestGameplayTag(FName("InputTag"))))
        {
            return Tag;
        }
    }
    return FGameplayTag();
}

// Get the input tag from an ability tag
FGameplayTag USurrenaAbilitySystemComponent::GetInputTagFromAbilityTag(const FGameplayTag& AbilityTag)
{
    if (const FGameplayAbilitySpec* Spec = GetSpecFromAbilityTag(AbilityTag))
    {
        return GetInputTagFromSpec(*Spec);
    }
    return FGameplayTag();
}

// Called when an input tag (like a hotkey) is held
void USurrenaAbilitySystemComponent::AbilityInputTagHeld(const FGameplayTag& InputTag)
{
    if (!InputTag.IsValid()) { return; }

    for (auto ActivatableAbility : GetActivatableAbilities()) {
        if (ActivatableAbility.DynamicAbilityTags.HasTagExact(InputTag)) {
            AbilitySpecInputPressed(ActivatableAbility);
            if (!ActivatableAbility.IsActive()) {
                TryActivateAbility(ActivatableAbility.Handle);
            }
        }
    }
}

// Equip an ability to a slot
void USurrenaAbilitySystemComponent::ServerEquipAbility_Implementation(const FGameplayTag& AbilityTag, const FGameplayTag& Slot)
{
    if (FGameplayAbilitySpec* AbilitySpec = GetSpecFromAbilityTag(AbilityTag))
    {
        const FSurrenaGameplayTags& GameplayTags = FSurrenaGameplayTags::Get();
        const FGameplayTag& PrevSlot = GetInputTagFromSpec(*AbilitySpec);
        const FGameplayTag& Status = GetStatusFromSpec(*AbilitySpec);

        const bool bStatusValid = Status == GameplayTags.Abilities_Status_Equipped || Status == GameplayTags.Abilities_Status_Unlocked;
        if (bStatusValid)
        {
            // Remove this slot from other abilities
            ClearAbilitiesOfSlot(Slot);
            // Clear this ability's previous slot
            ClearSlot(AbilitySpec);
            // Assign new slot
            AbilitySpec->DynamicAbilityTags.AddTag(Slot);
            if (Status.MatchesTagExact(GameplayTags.Abilities_Status_Unlocked))
            {
                AbilitySpec->DynamicAbilityTags.RemoveTag(GameplayTags.Abilities_Status_Unlocked);
                AbilitySpec->DynamicAbilityTags.AddTag(GameplayTags.Abilities_Status_Equipped);
            }
            MarkAbilitySpecDirty(*AbilitySpec);
        }
        ClientEquipAbility(AbilityTag, GameplayTags.Abilities_Status_Equipped, Slot, PrevSlot);
    }
}

// Notify client that an ability was equipped
void USurrenaAbilitySystemComponent::ClientEquipAbility(const FGameplayTag& AbilityTag, const FGameplayTag& Status, const FGameplayTag& Slot, const FGameplayTag& PreviousSlot)
{
    AbilityEquipped.Broadcast(AbilityTag, Status, Slot, PreviousSlot);
}

// Clear the input slot of a spec
void USurrenaAbilitySystemComponent::ClearSlot(FGameplayAbilitySpec* Spec)
{
    const FGameplayTag Slot = GetInputTagFromSpec(*Spec);
    Spec->DynamicAbilityTags.RemoveTag(Slot);
    MarkAbilitySpecDirty(*Spec);
}

// Clear all abilities assigned to a specific slot
void USurrenaAbilitySystemComponent::ClearAbilitiesOfSlot(const FGameplayTag& Slot)
{
    FScopedAbilityListLock ActiveScopeLock(*this);
    for (FGameplayAbilitySpec& Spec : GetActivatableAbilities())
    {
        if (AbilityHasSlot(&Spec, Slot))
        {
            ClearSlot(&Spec);
        }
    }
}

// Check if a spec has a slot
bool USurrenaAbilitySystemComponent::AbilityHasSlot(FGameplayAbilitySpec* Spec, const FGameplayTag& Slot)
{
    for (FGameplayTag Tag : Spec->DynamicAbilityTags)
    {
        if (Tag.MatchesTagExact(Slot))
        {
            return true;
        }
    }
    return false;
}

// Called when input tag is released
void USurrenaAbilitySystemComponent::AbilityInputTagRelease(const FGameplayTag& InputTag)
{
    if (!InputTag.IsValid()) { return; }

    for (FGameplayAbilitySpec ActivatableAbility : GetActivatableAbilities()) {
        if (ActivatableAbility.DynamicAbilityTags.HasTagExact(InputTag)) {
            AbilitySpecInputReleased(ActivatableAbility);
        }
    }
}

// Get spec from ability tag
FGameplayAbilitySpec* USurrenaAbilitySystemComponent::GetSpecFromAbilityTag(const FGameplayTag& AbilityTag)
{
    FScopedAbilityListLock ActiveScopeLoc(*this);
    for (FGameplayAbilitySpec& AbilitySpec : GetActivatableAbilities())
    {
        for (FGameplayTag Tag : AbilitySpec.Ability.Get()->AbilityTags)
        {
            if (Tag.MatchesTag(AbilityTag))
            {
                return &AbilitySpec;
            }
        }
    }
    return nullptr;
}

// Update abilities based on player level
void USurrenaAbilitySystemComponent::UpdateAbilityStatuses(int32 Level)
{
    UAbilityInfo* AbilityInfo = USurrenaAbilitySystemLibrary::GetAbilityInfo(GetAvatarActor());
    for (const FSurrenaAbilityInfo& Info : AbilityInfo->AbilityInformation)
    {
        if (!Info.AbilityTag.IsValid()) continue;
        if (Level < Info.LevelRequirement) continue;
        if (GetSpecFromAbilityTag(Info.AbilityTag) == nullptr)
        {
            FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(Info.Ability, 1);
            AbilitySpec.DynamicAbilityTags.AddTag(FSurrenaGameplayTags::Get().Abilities_Status_Eligible);
            GiveAbility(AbilitySpec);
            MarkAbilitySpecDirty(AbilitySpec);
            ClientUpdateAbilityStatus(Info.AbilityTag, FSurrenaGameplayTags::Get().Abilities_Status_Eligible, 1);
        }
    }
}

// Notify client about ability status changes
void USurrenaAbilitySystemComponent::ClientUpdateAbilityStatus_Implementation(const FGameplayTag& AbilityTag, const FGameplayTag& StatusTag, int32 AbilityLevel)
{
    AbilityStatusChanged.Broadcast(AbilityTag, StatusTag, AbilityLevel);
}

// Iterate over all activatable abilities
void USurrenaAbilitySystemComponent::ForEachAbility(const FForEachAbility& Delegate)
{
    FScopedAbilityListLock ActiveScopeLock(*this);
    for (const FGameplayAbilitySpec& AbilitySpec : GetActivatableAbilities())
    {
        if (!Delegate.ExecuteIfBound(AbilitySpec)) 
        {
            UE_LOG(LogTemp, Error, TEXT("Failed To Execute Delegate In %hs"), __FUNCTION__);
        }
    }
}

// Get main ability tag from a spec
FGameplayTag USurrenaAbilitySystemComponent::GetAbilityTagFromSpec(const FGameplayAbilitySpec& AbilitySpec)
{
    if (AbilitySpec.Ability) 
    {
        for (FGameplayTag Tag : AbilitySpec.Ability.Get()->AbilityTags) 
        {
            if (Tag.MatchesTag(FGameplayTag::RequestGameplayTag(FName("Abilities"))) )
            {
                return Tag;
            }
        }
    }
    return FGameplayTag();
}

// Get status tag from a spec
FGameplayTag USurrenaAbilitySystemComponent::GetStatusFromSpec(const FGameplayAbilitySpec& AbilitySpec)
{
    for (FGameplayTag StatusTag : AbilitySpec.DynamicAbilityTags) {
        if (StatusTag.MatchesTag(FGameplayTag::RequestGameplayTag(FName("Abilities.Status")))) {
            return StatusTag;
        }
    }
    return FGameplayTag();
}

// Replication hook for activated abilities
void USurrenaAbilitySystemComponent::OnRep_ActivateAbilities()
{
    Super::OnRep_ActivateAbilities();

    if (!bStartUpAbilitiesGiven)
    {
        bStartUpAbilitiesGiven = true;
        AbilitiesGivenDelegate.Broadcast();
    }
}

// Client-side callback when a gameplay effect is applied
void USurrenaAbilitySystemComponent::ClientEffectApplied_Implementation(UAbilitySystemComponent* AbilitySystemComponent,
    const FGameplayEffectSpec& EffectSpec, FActiveGameplayEffectHandle ActiveEffectHandle)
{
    FGameplayTagContainer TagContainer;
    EffectSpec.GetAllAssetTags(TagContainer);
    EffectAssetTags.Broadcast(TagContainer);
}

// Get ability descriptions for UI or tooltips
bool USurrenaAbilitySystemComponent::GetDescriptionsByAbilityTag(const FGameplayTag& AbilityTag, FString& OutDescription, FString& OutNextLevelDescription)
{
    if (const FGameplayAbilitySpec* AbilitySpec = GetSpecFromAbilityTag(AbilityTag))
    {
        if (USurrenaGameplayAbility* SurrenaAbility = Cast(AbilitySpec->Ability))
        {
            OutDescription = SurrenaAbility->GetDescription(AbilitySpec->Level);
            OutNextLevelDescription = SurrenaAbility->GetNextLevelDescription(AbilitySpec->Level + 1);
            return true;
        }
    }

    const UAbilityInfo* AbilityInfo = USurrenaAbilitySystemLibrary::GetAbilityInfo(GetAvatarActor());
    if (!AbilityTag.IsValid() || AbilityTag.MatchesTagExact(FSurrenaGameplayTags::Get().Abilities_None))
    {
        OutDescription = FString();
    }
    else
    {
        OutDescription = USurrenaGameplayAbility::GetLockedDescription(AbilityInfo->FindAbilityInfoForTag(AbilityTag).LevelRequirement);
    }
    OutNextLevelDescription = FString();
    return false;
}

  

// Copyright Parsa Rahimian
#include "Actor/SurrenaEffectActor.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "AbilitySystemComponent.h"

// Sets default values
ASurrenaEffectActor::ASurrenaEffectActor()
{
    // Disable Tick by default to improve performance
    PrimaryActorTick.bCanEverTick = false;

    // Create a root scene component
    SetRootComponent(CreateDefaultSubobject("SceneRoot"));
}

// Called when the game starts or when spawned
void ASurrenaEffectActor::BeginPlay()
{
    Super::BeginPlay();
}

// Apply a specific gameplay effect to a target actor
void ASurrenaEffectActor::ApplyEffectToTarget(
    AActor* TargetActor,
    const FEffectType& Effect
)
{
    // Get the target's Ability System Component
    UAbilitySystemComponent* TargetAbilitySystemComponent = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);
    if (TargetAbilitySystemComponent == nullptr) return;

    // Ensure the GameplayEffect is valid
    check(Effect.GameplayEffect);

    // Create an effect context with this actor as the source
    FGameplayEffectContextHandle EffectContextHandle = TargetAbilitySystemComponent->MakeEffectContext();
    EffectContextHandle.AddSourceObject(this);

    // Create the GameplayEffectSpec with actor level
    const FGameplayEffectSpecHandle EffectSpecHandle = TargetAbilitySystemComponent->MakeOutgoingSpec(
        Effect.GameplayEffect,
        ActorLevel,
        EffectContextHandle
    );

    // Apply the effect to the target
    const FActiveGameplayEffectHandle ActiveEffectHandle = TargetAbilitySystemComponent->ApplyGameplayEffectSpecToSelf(
        *EffectSpecHandle.Data.Get()
    );

    // If the effect is infinite and should be removed on end overlap, track it
    if (EffectSpecHandle.Data.Get()->Def.Get()->DurationPolicy == EGameplayEffectDurationType::Infinite)
    {
        if (Effect.RemovalPolicy == EEffectRemovalPolicy::RemoveOnEndOverlap)
        {
            ActiveEffectHandles.Add(ActiveEffectHandle, TargetAbilitySystemComponent);
        }
    }
}

// Called when an actor begins overlap
void ASurrenaEffectActor::OnOverlap(AActor* TargetActor)
{
    for (const FEffectType& Effect : Effects)
    {
        // Apply effects that trigger on overlap
        if (Effect.ApplicationPolicy == EEffectApplicationPolicy::ApplyOnOverlap)
        {
            ApplyEffectToTarget(TargetActor, Effect);
        }
    }
}

// Called when an actor ends overlap
void ASurrenaEffectActor::OnEndOverlap(AActor* TargetActor)
{
    for (const FEffectType& Effect : Effects)
    {
        // Apply effects that trigger on end overlap
        if (Effect.ApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap)
        {
            ApplyEffectToTarget(TargetActor, Effect);
        }

        // Handle removal of infinite duration effects
        if (Effect.GameplayEffect.GetDefaultObject()->DurationPolicy == EGameplayEffectDurationType::Infinite)
        {
            if (Effect.RemovalPolicy == EEffectRemovalPolicy::RemoveOnEndOverlap)
            {
                UAbilitySystemComponent* TargetAbilitySystemComponent =
                    UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);

                if (!IsValid(TargetAbilitySystemComponent)) return;

                // Collect handles to remove
                TArray HandlesToRemove;
                for (TTuple HandlePair : ActiveEffectHandles)
                {
                    if (TargetAbilitySystemComponent == HandlePair.Value)
                    {
                        TargetAbilitySystemComponent->RemoveActiveGameplayEffect(HandlePair.Key, 1);
                        HandlesToRemove.Add(HandlePair.Key);
                    }
                }

                // Remove handles from tracking map
                for (FActiveGameplayEffectHandle& Handle : HandlesToRemove)
                {
                    ActiveEffectHandles.FindAndRemoveChecked(Handle);
                }
            }
        }
    }
}

  

Game Developer / Lead Programmer / Gameplay Designer

Designed combat systems with emphasis on player feedback and emotional pacing using UE5 Gameplay Ability System.

Collaborated with narrative team to integrate player choices into gameplay systems.

Instagram
Shooter GIF

Last Invention

Solo Project (All by me)

Created immersive Shooter Gameplay Mechanics.

Implemented advanced mechanics for smooth player interaction.

Youtube
More itch.io

Learnings

Trophies

UE~Blueprints
UE~C++
HTML/CSS/JS
Systems Design
Optimization
3D/2D Tools (Blender, ZBrush, Aseprite)

Contact Me:

Telegram: @ParsaRahimian

Discord: parsarh_

Email: parsarahimian@outlook.com

Steam: ParsaRH