Intro

Timeframe: 3 months (Unity) + 2 months (Unreal 5.2)

I solo developed a 3rd-person souls-like combat, stylized, action RPG game in Unity and Unity Version Control using Unity asset store assets. My goal was to learn Unreal Engine while restructuring the project, integrating metahumans, environment foliage, and Niagara effects into the UI and overall game.

Primary features include:
  • Modular weapon combat (staggering, stat-based blocking, stamina-based parries)
  • Responsive item inventory
  • Enemies
  • Player + Enemy stats
  • Consumables
  • UI
  • Enemy Combat + Detection AI
  • Level event triggers
  • (and more)...

  • Early Unreal Programming

    C++ & Blueprints

    Environment

    I developed a breakable object system where the meshes of "BreakableActors" explode in the direction they are hit with an impact sound. I used Unreal Chaos Module for the randomized break behavior in the meshes. Note the breakable object spawns a collectible treasure item on destruction.

    CODE SNIPPET - BreakableActor.cpp

    The following code demonstrates how a breakable actor is set up with necessary geometry components on specific collision layers, a random treasure object spawns from the break site, and the BreakableActor's mesh disappears after breaking.

    // Sets default values
    ABreakableActor::ABreakableActor()
    {
     	// Set this actor to call Tick() every frame.  Can turn this off to improve performance if necessary it.
    	PrimaryActorTick.bCanEverTick = false;
    
    	GeometryCollection = CreateDefaultSubobject(TEXT("Geometry Component"));
    	SetRootComponent(GeometryCollection);
    	GeometryCollection->SetGenerateOverlapEvents(true);
    	GeometryCollection->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECR_Ignore);
    	GeometryCollection->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECR_Ignore);
    
    	Capsule = CreateDefaultSubobject(TEXT("Capsule"));
    	Capsule->SetupAttachment(GetRootComponent());
    	Capsule->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECR_Ignore);
    	Capsule->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECR_Block);
    
    }
    
    // Called when the game starts or when spawned
    void ABreakableActor::BeginPlay()
    {
    	Super::BeginPlay();
    
    	GeometryCollection->OnChaosBreakEvent.AddDynamic(this, &ABreakableActor::OnBreak);
    }
    
    // Called every frame
    void ABreakableActor::Tick(float DeltaTime)
    {
    	Super::Tick(DeltaTime);
    
    }
    
    void ABreakableActor::GetHit_Implementation(const FVector& ImpactPoint, AActor* Hitter)
    {
    	if(bBroken) return;
    	bBroken = true;
    	UWorld* World = GetWorld();
    
    	if(World && TreasureClasses.Num() > 0)
    	{
    		FVector Location = GetActorLocation();
    		Location.Z += 75.f;
    		int32 Selection = FMath::RandRange(0, TreasureClasses.Num() - 1);
    		World->SpawnActor(TreasureClasses[0], Location, GetActorRotation());
    	}
    
    
    }
    
    void ABreakableActor::OnBreak(const FChaosBreakEvent& BreakEvent)
    {
    	this->SetLifeSpan(5.f);
    	Capsule->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore);
    }
                            
    CODE SNIPPET - BreakableActor.h

    The following code demonstrates the code used in the header file for BreakableActor.cpp

    
    #include "CoreMinimal.h"
    #include "GameFramework/Actor.h"
    #include "Interfaces/HitInterface.h"
    #include "BreakableActor.generated.h"
    
    struct FChaosBreakEvent;
    class UGeometryCollectionComponent;
    
    UCLASS()
    class ABreakableActor : public AActor, public IHitInterface
    {
    	GENERATED_BODY()
    
    public:
    	ABreakableActor();
    	virtual void Tick(float DeltaTime) override;
    	virtual void GetHit_Implementation(const FVector& ImpactPoint, AActor* Hitter) override;
    
    	UFUNCTION()
    	virtual void OnBreak(const FChaosBreakEvent& BreakEvent);
    
    protected:
    	virtual void BeginPlay() override;
    
    	UPROPERTY(VisibleAnywhere)
    	UGeometryCollectionComponent* GeometryCollection;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
    	class UCapsuleComponent* Capsule;
    
    private:
    	UPROPERTY(EditAnywhere, Category = "Breakable Properties")
    	TArray> TreasureClasses;
    
    	bool bBroken = false;
    };
                            
    Characters

    I developed an modular character system that uses animation instance states, montages, and EnhancedInput. The player can get hit and respond with an animation from the corresponding direction. Enemies may equip a sword, shield, none, or both to hand sockets in their skeletons and adjust animations accordingly.

    CODE SNIPPET - PlayerCharacter.cpp

    The following code demonstrates the setup of the player character actor with a camera, appropriate collision layers, and input with Unreal 5's new EnhancedInput module.

  • A game hud is setup using the players attributes (health, stamina, gold, exp).
  • Currently the player moves using keyboard directional movement, a mouse to look, space to jump, 'E' to interact, and clicking to attack.
  • Interacting with a weapon causes it to be placed on the players back and can ten be unsheathed or sheathed for combat.
  • Finally the player may die when health is 0.
  •                             // Sets default values
    APlayerCharacter::APlayerCharacter()
    {
    	PrimaryActorTick.bCanEverTick = true;
    
    	bUseControllerRotationPitch = false;
    	bUseControllerRotationYaw = false;
    	bUseControllerRotationRoll = false;
    
    	GetCharacterMovement()->bOrientRotationToMovement = true;
    	GetCharacterMovement()->RotationRate = FRotator(0.f, 400.f, 0.f);
    
    	GetMesh()->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic);
    	GetMesh()->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
    	GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Visibility, ECollisionResponse::ECR_Block);
    	GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_WorldDynamic, ECollisionResponse::ECR_Overlap);
    	GetMesh()->SetGenerateOverlapEvents(true);
    
    	CameraBoom = CreateDefaultSubobject(TEXT("CameraBoom"));
    	CameraBoom->SetupAttachment(GetRootComponent());
    	CameraBoom->TargetArmLength = 300.f;
    
    	ViewCamera = CreateDefaultSubobject(TEXT("ViewCamera"));
    	ViewCamera->SetupAttachment(CameraBoom);
    
    }
    
    void ASlashCharacter::BeginPlay()
    {
    	Super::BeginPlay();
    
    	Tags.Add(FName("EngageableTarget"));
    	Tags.Add(FName("SlashCharacter"));
    
    	if(APlayerController* PlayerController = Cast(GetController()))
    	{
    		if(UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem(PlayerController->GetLocalPlayer()))
    		{
    			Subsystem->AddMappingContext(SlashContext, 0);
    		}
    		if (AGameHUD* GameHUD = Cast(PlayerController->GetHUD()))
    		{
    			GameOverlay = GameHUD->GetGameOverlay();
    			if (GameOverlay && Attributes)
    			{
    				GameOverlay->SetHealthBarPercent(Attributes->GetHealthPercent());
    				GameOverlay->SetStaminaBarPercent(1.f);
    				GameOverlay->SetGold(0);
    				GameOverlay->SetExp(0);
    			}
    		}
    	}
    
    }
    
    void ASlashCharacter::Move(const FInputActionValue& Value)
    {
    	if(ActionState != EActionState::EAS_Unoccupied) return;
    
    	const FVector2D MovementVector = Value.Get();
    
    	const FRotator Rotation = Controller->GetControlRotation();
    	const FRotator YawRotation(0.f, Rotation.Yaw,0.f);
    
    	const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
    	AddMovementInput(ForwardDirection, MovementVector.Y);
    	const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
    	AddMovementInput(RightDirection, MovementVector.X);
    
    
    }
    
    void ASlashCharacter::Look(const FInputActionValue& Value)
    {
    	const FVector2D LookAxisVector = Value.Get();
    	if(GetController())
    	{
    		AddControllerPitchInput(LookAxisVector.Y);
    		AddControllerYawInput(LookAxisVector.X);
    	}
    }
    
    void ASlashCharacter::Tick(float DeltaTime)
    {
    	Super::Tick(DeltaTime);
    	if (Attributes && GameOverlay)
    	{
    		Attributes->RegenStamina(DeltaTime);
    		GameOverlay->SetStaminaBarPercent(Attributes->GetStaminaPercent());
    	}
    
    }
    
    void ASlashCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
    {
    	Super::SetupPlayerInputComponent(PlayerInputComponent);
    
    	if(UEnhancedInputComponent* EnhancedInputComponent = CastChecked(PlayerInputComponent))
    	{
    		// TODO: why use address
    		EnhancedInputComponent->BindAction(MovementAction, ETriggerEvent::Triggered, this, &ASlashCharacter::Move);
    		EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &ASlashCharacter::Look);
    		EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Triggered, this, &ASlashCharacter::Jump);
    		EnhancedInputComponent->BindAction(EKeyAction, ETriggerEvent::Triggered, this, &ASlashCharacter::EKeyPressed);
    		EnhancedInputComponent->BindAction(AttackAction, ETriggerEvent::Triggered, this, &ASlashCharacter::Attack);
    		EnhancedInputComponent->BindAction(DodgeAction, ETriggerEvent::Triggered, this, &ASlashCharacter::Dodge);
    
    	}
    }
    
    void ASlashCharacter::Jump()
    {
    	if (IsUnoccupied())
    	{
    		Super::Jump();
    	}
    }
    
    float ASlashCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator,
                                      AActor* DamageCauser)
    {
    	HandleDamage(DamageAmount);
    	SetHUDHealth();
    	return DamageAmount;}
    
    void ASlashCharacter::GetHit_Implementation(const FVector& ImpactPoint, AActor* Hitter)
    {
    	Super::GetHit_Implementation(ImpactPoint, Hitter);
    
    	// disable weapon collision if getting hit
    	SetWeaponCollisionEnabled(ECollisionEnabled::NoCollision);
    	if (Attributes && Attributes->GetHealthPercent() > 0.f)
    	{
    		ActionState = EActionState::EAS_HitReaction;
    	}
    }
    
    void ASlashCharacter::SetOverlappingItem(AItem* Item)
    {
    OverlappingItem = Item;
    }
    
    void ASlashCharacter::AddSouls(ASoul* Exp)
    {
    	if (Attributes && GameOverlay)
    	{
    		Attributes->AddExp(Exp->GetExp());
    		GameOverlay->SetExp(Attributes->GetExp());
    	}
    }
    
    void ASlashCharacter::AddGold(ATreasure* Treasure)
    {
    	if (Attributes && GameOverlay)
    	{
    		Attributes->AddGold(Treasure->GetGold());
    		GameOverlay->SetGold(Attributes->GetGold());
    	}
    }
    
    void ASlashCharacter::MoveForward(float Value)
    {
    	if(ActionState != EActionState::EAS_Unoccupied) return;
    	if(Controller && (Value != 0.f))
    	{
    		FVector Forward = GetActorForwardVector();
    		AddMovementInput(Forward, Value);
    	}
    }
    
    void ASlashCharacter::EKeyPressed()
    {
    	AWeapon* OverlappingWeapon = Cast(OverlappingItem);
    	if(OverlappingWeapon)
    	{
    		EquipWeapon(OverlappingWeapon);
    	}
    	else
    	{
    		if(CanDisarm())
    		{
    			Disarm();
    		}
    		else if(CanArm())
    		{
    			Arm();
    		}
    	}
    }
    
    void ASlashCharacter::Attack()
    {
    	if(CanAttack())
    	{
    		PlayAttackMontage();
    		ActionState = EActionState::EAS_Attacking;
    	}
    }
    
    void ASlashCharacter::EquipWeapon(AWeapon* Weapon)
    {
    	Weapon->Equip(GetMesh(), FName("RightHandSocket"),this, this);
    	CharacterState = ECharacterState::ECS_EquippedOneHandedWeapon;
    	OverlappingItem = nullptr;
    	EquippedWeapon = Weapon;
    }
    
    void ASlashCharacter::AttackEnd()
    {
    	ActionState = EActionState::EAS_Unoccupied;
    }
    
    void ASlashCharacter::DodgeEnd()
    {
    	Super::DodgeEnd();
    	ActionState = EActionState::EAS_Unoccupied;
    }
    
    bool ASlashCharacter::CanAttack()
    {
    	return ActionState == EActionState::EAS_Unoccupied &&
    		CharacterState != ECharacterState::ECS_Unequipped;
    }
    
    bool ASlashCharacter::CanDisarm()
    {
    	return ActionState == EActionState::EAS_Unoccupied &&
    		CharacterState != ECharacterState::ECS_Unequipped;
    }
    
    bool ASlashCharacter::CanArm()
    {
    	return ActionState == EActionState::EAS_Unoccupied &&
    		CharacterState == ECharacterState::ECS_Unequipped && EquippedWeapon;
    }
    
    void ASlashCharacter::Disarm()
    {
    	PlayEquipMontage(FName("Unequip"));
    	CharacterState = ECharacterState::ECS_Unequipped;
    	ActionState = EActionState::EAS_EquippingWeapon;
    }
    
    void ASlashCharacter::Arm()
    {
    	PlayEquipMontage(FName("Equip"));
    	CharacterState = ECharacterState::ECS_EquippedOneHandedWeapon;
    	ActionState = EActionState::EAS_EquippingWeapon;
    }
    
    void ASlashCharacter::AttachWeaponToBack()
    {
    	if (EquippedWeapon)
    	{
    		EquippedWeapon->AttachMeshToSocket(GetMesh(), FName("SpineSocket"));
    	}
    }
    
    void ASlashCharacter::AttachWeaponToHand()
    {
    	if (EquippedWeapon)
    	{
    		EquippedWeapon->AttachMeshToSocket(GetMesh(), FName("RightHandSocket"));
    	}
    }
    
    void ASlashCharacter::FinishEquipping()
    {
    	ActionState = EActionState::EAS_Unoccupied;
    }
    
    void ASlashCharacter::HitReactEnd()
    {
    	ActionState = EActionState::EAS_Unoccupied;
    }
    
    bool ASlashCharacter::IsUnoccupied()
    {
    	return ActionState == EActionState::EAS_Unoccupied;
    
    }
    
    void ASlashCharacter::InitializePlayerEnhancedInput()
    {/*
    	if(UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem(PlayerController->GetLocalPlayer()))
    		{
    			Subsystem->AddMappingContext(SlashContext, 0);
    		}*/
    }
    
    void ASlashCharacter::InitializeGameOverlay()
    {
    	/*if (AGameHUD* GameHUD = Cast(PlayerController->GetHUD()))
    	{
    		GameOverlay = GameHUD->GetGameOverlay();
    		if (GameOverlay && Attributes)
    		{
    			GameOverlay->SetHealthBarPercent(Attributes->GetHealthPercent());
    			GameOverlay->SetStaminaBarPercent(1.f);
    			GameOverlay->SetGold(0);
    			GameOverlay->SetExp(0);
    		}
    	}*/
    }
    
    void ASlashCharacter::SetHUDHealth()
    {
    	if (GameOverlay && Attributes)
    	{
    		GameOverlay->SetHealthBarPercent(Attributes->GetHealthPercent());
    	}
    }
    
    bool ASlashCharacter::IsOccupied()
    {
    	return ActionState != EActionState::EAS_Unoccupied;
    }
    
    bool ASlashCharacter::HasEnoughStamina()
    {
    	return Attributes && Attributes->GetStamina() > Attributes->GetDodgeCost();
    }
    
    void ASlashCharacter::Dodge()
    {
    	if (IsOccupied() || !HasEnoughStamina()) return;
    	PlayDodgeMontage();
    	ActionState = EActionState::EAS_Dodge;
    	if(Attributes && GameOverlay)
    	{
    		Attributes->DrainStamina(Attributes->GetDodgeCost());
    		GameOverlay->SetStaminaBarPercent(Attributes->GetStaminaPercent());
    	}
    }
    
    void ASlashCharacter::PlayEquipMontage(const FName& SectionName)
    {
    	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
    	if(AnimInstance && EquipMontage)
    	{
    		AnimInstance->Montage_Play(EquipMontage);
    		AnimInstance->Montage_JumpToSection(SectionName, EquipMontage);
    	}
    
    }
    
    void ASlashCharacter::Die()
    {
    	Super::Die();
    	ActionState = EActionState::EAS_Dead;
    	DisableMeshCollision();
    	Tags.Remove(FName("EngageableTarget"));
    }
    
                            
    CODE SNIPPET - BaseCharacter.cpp

    The following code demonstrates the setup of a character actor with attributes (health, stamina, exp).

  • When hit, the character reacts with a directional animation corresponding to the hit direction along with hit particles and a hit sound.
  • Characters also have random death montages that link to dead poses and randomized attack montages that use translation-rotation-warp to targets.
  • Characters have weapon hit boxes that may collide with other objects or characters.
  •                           ABaseCharacter::ABaseCharacter()
    {
    	PrimaryActorTick.bCanEverTick = true;
    	Attributes = CreateDefaultSubobject(TEXT("Attibutes"));
    }
    
    void ABaseCharacter::BeginPlay()
    {
    	Super::BeginPlay();
    
    }
    
    void ABaseCharacter::GetHit_Implementation(const FVector& ImpactPoint, AActor* Hitter)
    {
    	if (IsAlive() && Hitter)
    	{
    		DirectionalHitReact(Hitter->GetActorLocation());
    	}
    	else Die();
    
    	PlayHitSound(ImpactPoint);
    	SpawnHitParticles(ImpactPoint);
    }
    
    void ABaseCharacter::Attack()
    {
    }
    
    int32 ABaseCharacter::PlayAttackMontage()
    {
    	return PlayRandomMontageSection(AttackMontage, AttackMontageSections);
    }
    
    int32 ABaseCharacter::PlayDeathMontage()
    {
    	const int32 Selection = PlayRandomMontageSection(DeathMontage, DeathMontageSections);
    	TEnumAsByte Pose(Selection);
    	if (Pose < EDeathPose::EDP_MAX)
    	{
    		DeathPose = Pose;
    	}
    
    	return Selection;
    }
    
    void ABaseCharacter::StopAttackMontage()
    {
    	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
    	if (AnimInstance)
    	{
    		AnimInstance->Montage_Stop(0.25f, AttackMontage);
    	}
    }
    
    FVector ABaseCharacter::GetTranslationWarpTarget()
    {
    	if (CombatTarget == nullptr) return FVector();
    
    	const FVector CombatTargetLocation = CombatTarget->GetActorLocation();
    	const FVector Location = GetActorLocation();
    
    	FVector TargetToMe = (Location - CombatTargetLocation).GetSafeNormal();
    	TargetToMe *= WarpTargetDistance;
    
    	return CombatTargetLocation + TargetToMe;
    }
    
    FVector ABaseCharacter::GetRotationWarpTarget()
    {
    	if (CombatTarget)
    	{
    		return CombatTarget->GetActorLocation();
    	}
    	return FVector();
    }
    
    void ABaseCharacter::DisableCapsule()
    {
    	GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
    }
    
    bool ABaseCharacter::CanAttack()
    {
    	return false;
    }
    
    bool ABaseCharacter::IsAlive()
    {
    	return Attributes && Attributes->IsAlive();
    }
    
    void ABaseCharacter::DisableMeshCollision()
    {
    	GetMesh()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
    }
    
    void ABaseCharacter::PlayHitReactMontage(const FName& SectionName)
    {
    	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
    	if (AnimInstance && HitReactMontage)
    	{
    		AnimInstance->Montage_Play(HitReactMontage);
    		AnimInstance->Montage_JumpToSection(SectionName, HitReactMontage);
    	}
    }
    
    void ABaseCharacter::DirectionalHitReact(const FVector& ImpactPoint)
    {
    	const FVector Forward = GetActorForwardVector();
    	// Lower Impact Point to the Enemy's Actor Location Z
    	const FVector ImpactLowered(ImpactPoint.X, ImpactPoint.Y, GetActorLocation().Z);
    	const FVector ToHit = (ImpactLowered - GetActorLocation()).GetSafeNormal();
    
    	// Forward * ToHit = |Forward||ToHit| * cos(theta)
    	// |Forward| = 1, |ToHit| = 1, so Forward * ToHit = cos(theta)
    	const double CosTheta = FVector::DotProduct(Forward, ToHit);
    	// Take the inverse cosine (arc-cosine) of cos(theta) to get theta
    	double Theta = FMath::Acos(CosTheta);
    	// convert from radians to degrees
    	Theta = FMath::RadiansToDegrees(Theta);
    
    	// if CrossProduct points down, Theta should be negative
    	const FVector CrossProduct = FVector::CrossProduct(Forward, ToHit);
    	if (CrossProduct.Z < 0)
    	{
    		Theta *= -1.f;
    	}
    
    	FName Section("FromBack");
    
    	if (Theta >= -45.f && Theta < 45.f)
    	{
    		Section = FName("FromFront");
    	}
    	else if (Theta >= -135.f && Theta < -45.f)
    	{
    		Section = FName("FromLeft");
    	}
    	else if (Theta >= 45.f && Theta < 135.f)
    	{
    		Section = FName("FromRight");
    	}
    
    	PlayHitReactMontage(Section);
    
    }
    
    void ABaseCharacter::PlayHitSound(const FVector& ImpactPoint)
    {
    	if (HitSound)
    	{
    		UGameplayStatics::PlaySoundAtLocation(
    			this,
    			HitSound,
    			ImpactPoint
    		);
    	}
    }
    
    void ABaseCharacter::SpawnHitParticles(const FVector& ImpactPoint)
    {
    	if (HitParticles && GetWorld())
    	{
    		UGameplayStatics::SpawnEmitterAtLocation(
    			GetWorld(),
    			HitParticles,
    			ImpactPoint
    		);
    	}
    }
    
    void ABaseCharacter::HandleDamage(float DamageAmount)
    {
    	if (Attributes)
    	{
    		Attributes->ReceiveDamage(DamageAmount);
    	}
    }
    
    void ABaseCharacter::PlayMontageSection(UAnimMontage* Montage, const FName& SectionName)
    {
    	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
    	if(AnimInstance && Montage)
    	{
    		AnimInstance->Montage_Play(Montage);
    		AnimInstance->Montage_JumpToSection(SectionName, Montage);
    	}
    }
    
    int32 ABaseCharacter::PlayRandomMontageSection(UAnimMontage* Montage, const TArray& SectionNames)
    {
    	if(SectionNames.Num() <= 0) return -1;
    	const int32 MaxSectionIndex = SectionNames.Num() - 1;
    	const int32 Selection = FMath::RandRange(0, MaxSectionIndex);
    	PlayMontageSection(Montage, SectionNames[Selection]);
    	return Selection;
    }
    
    void ABaseCharacter::AttackEnd()
    {
    }
    
    void ABaseCharacter::Die()
    {
    	PlayDeathMontage();
    }
    
    void ABaseCharacter::Tick(float DeltaTime)
    {
    	Super::Tick(DeltaTime);
    
    }
    
    void ABaseCharacter::SetWeaponCollisionEnabled(ECollisionEnabled::Type CollisionEnabled)
    {
    	if(EquippedWeapon && EquippedWeapon->GetWeaponBox())
    	{
    		EquippedWeapon->GetWeaponBox()->SetCollisionEnabled(CollisionEnabled);
    		EquippedWeapon->IgnoreActors.Empty();
    	}
    }
    Heads-Up Display (HUD)

    I developed an heads-up display that shows player health, enemy health bars above their heads, and pick up prompts. Note Niagara systems for weapon trail, blood splatter, items, and soul that spawns on character death.

    CODE SNIPPET - GameOverlay.cpp

    The following code demonstrates how the player character attributes are set (as regenerating, progress bars, and numbers).

    void UGameOverlay::SetHealthBarPercent(float Percent)
    {
    	if (HealthProgressBar)
    	{
    		HealthProgressBar->SetPercent(Percent);
    	}
    }
    
    void UGameOverlay::SetStaminaBarPercent(float Percent)
    {
    	if (StaminaProgressBar)
    	{
    		StaminaProgressBar->SetPercent(Percent);
    	}
    }
    
    void UGameOverlay::SetGold(int32 Gold)
    {
    	if (GoldCountText)
    	{
    		// format  int32 into string into text
    		const FString String = FString::Printf(TEXT("%d"), Gold);
    		const FText Text = FText::FromString(String);
    		GoldCountText->SetText(Text);
    	}
    }
    
    void UGameOverlay::SetExp(int32 Exp)
    {
    	if (ExpCountText)
    	{
    		const FString String = FString::Printf(TEXT("%d"), Exp);
    		const FText Text = FText::FromString(String);
    		ExpCountText->SetText(Text);
    	}
    }
                            
    CODE SNIPPET - HealthBarComponent.cpp

    The following code demonstrates the code used

    void UHealthBarComponent::SetHealthPercent(float Percent)
    {
    	if(HealthBarWidget == nullptr)
    	{
    		HealthBarWidget = Cast(GetUserWidgetObject());
    	}
    
    	if(HealthBarWidget && HealthBarWidget->HealthBar)
    	{
    		HealthBarWidget->HealthBar->SetPercent(Percent);
    	}
    }
                            
    Enemy AI

    I improved the enemy AI combat system with several states to motion warp and have clearer attack radius checks. Enemies can also have attack frequency modified in editor (AttackTimer). The video shows two enemies, one with a larger pursuit radius that runs after player when close and walks when character is far

  • Patrolling State: enemies patrol along a list of specified targets in-level, unaware of player.
  • Chase State: enemies notice player based on line of sight and hearing, or player attack, then chase player.
  • Attack State: enemy attacks player.
  • Dead State: enemy lost all health and soon disappears from level.

  • CODE SNIPPET - Enemy AI

    The following code demonstrates the primary enemy AI.

    AEnemy::AEnemy()
    {
     	PrimaryActorTick.bCanEverTick = true;
    
    	GetMesh()->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic);
    	GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Visibility, ECollisionResponse::ECR_Block);
    	GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
    	GetMesh()->SetGenerateOverlapEvents(true);
    
    
    	HealthBarWidget = CreateDefaultSubobject(TEXT("HealthBar"));
    	HealthBarWidget->SetupAttachment(GetRootComponent());
    
    	GetCharacterMovement()->bOrientRotationToMovement = true;
    	bUseControllerRotationPitch = false;
    	bUseControllerRotationYaw = false;
    	bUseControllerRotationRoll = false;
    
    	PawnSensing = CreateDefaultSubobject(TEXT("PawnSensing"));
    	PawnSensing->SightRadius = 4000.f;
    	PawnSensing->SetPeripheralVisionAngle(45.f);
    }
    
    void AEnemy::Tick(float DeltaTime)
    {
    	Super::Tick(DeltaTime);
    	if (IsDead()) return;
    	if (EnemyState > EEnemyState::EES_Patrolling)
    	{
    		CheckCombatTarget();
    	}
    	else
    	{
    		CheckPatrolTarget();
    	}
    }
    
    float AEnemy::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
    {
    	HandleDamage(DamageAmount);
    	CombatTarget = EventInstigator->GetPawn();
    
    	if(IsInsideAttackRadius())
    	{
    		EnemyState = EEnemyState::EES_Attacking;
    	}
    		else if(IsOutsideAttackRadius())
    		{
    			ChaseTarget();
    		}
    	return DamageAmount;
    }
    
    void AEnemy::Destroyed()
    {
    	if (EquippedWeapon)
    	{
    		EquippedWeapon->Destroy();
    	}
    }
    
    void AEnemy::GetHit_Implementation(const FVector& ImpactPoint, AActor* Hitter)
    {
    	Super::GetHit_Implementation(ImpactPoint, Hitter);
    	if (!IsDead()) ShowHealthBar();
    	ClearPatrolTimer();
    	ClearAttackTimer();SetWeaponCollisionEnabled(ECollisionEnabled::NoCollision);
    
    	StopAttackMontage();
    
    }
    
    void AEnemy::BeginPlay()
    {
    	Super::BeginPlay();
    
    	if (PawnSensing) PawnSensing->OnSeePawn.AddDynamic(this, &AEnemy::PawnSeen);
    	InitializeEnemy();
    	Tags.Add(FName("Enemy"));
    }
    
    void AEnemy::Die()
    {
    	Super::Die();
    	EnemyState = EEnemyState::EES_Dead;
    
    	ClearAttackTimer();
    	HideHealthBar();
    	DisableCapsule();
    	SetLifeSpan(DeathLifeSpan);
    	GetCharacterMovement()->bOrientRotationToMovement = false;
    	SetWeaponCollisionEnabled(ECollisionEnabled::NoCollision);
    }
    
    void AEnemy::Attack()
    {
    	EnemyState = EEnemyState::EES_Engaged;
    	Super::Attack();
    	PlayAttackMontage();
    }
    
    bool AEnemy::CanAttack()
    {
    	bool bCanAttack =
    		IsInsideAttackRadius() &&
    		!IsAttacking() &&
    		!IsEngaged() &&
    		!IsDead();
    	return bCanAttack;
    }
    
    void AEnemy::AttackEnd()
    {
    	EnemyState = EEnemyState::EES_NoState;
    	CheckCombatTarget();
    }
    
    void AEnemy::HandleDamage(float DamageAmount)
    {
    	Super::HandleDamage(DamageAmount);
    
    	if (Attributes && HealthBarWidget)
    	{
    		HealthBarWidget->SetHealthPercent(Attributes->GetHealthPercent());
    	}
    }
    
    void AEnemy::InitializeEnemy()
    {
    	EnemyController = Cast(GetController());
    	MoveToTarget(PatrolTarget);
    	HideHealthBar();
    	SpawnDefaultWeapon();
    }
    
    void AEnemy::CheckPatrolTarget()
    {
    	if (InTargetRange(PatrolTarget, PatrolRadius))
    	{
    		PatrolTarget = ChoosePatrolTarget();
    		const float WaitTime = FMath::RandRange(PatrolWaitMin, PatrolWaitMax);
    		GetWorldTimerManager().SetTimer(PatrolTimer, this, &AEnemy::PatrolTimerFinished, WaitTime);
    	}
    }
    
    void AEnemy::CheckCombatTarget()
    {
    	if (IsOutsideCombatRadius())
    	{
    		ClearAttackTimer();
    		LoseInterest();
    		if (!IsEngaged()) StartPatrolling();
    	}
    	else if (IsOutsideAttackRadius() && !IsChasing())
    	{
    		ClearAttackTimer();
    		if (!IsEngaged()) ChaseTarget();
    	}
    	else if (CanAttack())
    	{
    		StartAttackTimer();
    	}
    }
    
    void AEnemy::PatrolTimerFinished()
    {
    	MoveToTarget(PatrolTarget);
    }
    
    void AEnemy::HideHealthBar()
    {
    	if (HealthBarWidget)
    	{
    		HealthBarWidget->SetVisibility(false);
    	}
    }
    
    void AEnemy::ShowHealthBar()
    {
    	if (HealthBarWidget)
    	{
    		HealthBarWidget->SetVisibility(true);
    	}
    }
    
    void AEnemy::LoseInterest()
    {
    	CombatTarget = nullptr;
    	HideHealthBar();
    }
    
    void AEnemy::StartPatrolling()
    {
    	EnemyState = EEnemyState::EES_Patrolling;
    	GetCharacterMovement()->MaxWalkSpeed = PatrollingSpeed;
    	MoveToTarget(PatrolTarget);
    }
    
    void AEnemy::ChaseTarget()
    {
    	EnemyState = EEnemyState::EES_Chasing;
    	GetCharacterMovement()->MaxWalkSpeed = ChasingSpeed;
    	MoveToTarget(CombatTarget);
    }
    
    bool AEnemy::IsOutsideCombatRadius()
    {
    	return !InTargetRange(CombatTarget, CombatRadius);
    }
    
    bool AEnemy::IsOutsideAttackRadius()
    {
    	return !InTargetRange(CombatTarget, AttackRadius);
    }
    
    bool AEnemy::IsInsideAttackRadius()
    {
    	return InTargetRange(CombatTarget, AttackRadius);
    }
    
    bool AEnemy::IsChasing()
    {
    	return EnemyState == EEnemyState::EES_Chasing;
    }
    
    bool AEnemy::IsAttacking()
    {
    	return EnemyState == EEnemyState::EES_Attacking;
    }
    
    bool AEnemy::IsDead()
    {
    	return EnemyState == EEnemyState::EES_Dead;
    }
    
    bool AEnemy::IsEngaged()
    {
    	return EnemyState == EEnemyState::EES_Engaged;
    }
    
    void AEnemy::ClearPatrolTimer()
    {
    	GetWorldTimerManager().ClearTimer(PatrolTimer);
    }
    
    void AEnemy::StartAttackTimer()
    {
    	EnemyState = EEnemyState::EES_Attacking;
    	const float AttackTime = FMath::RandRange(AttackMin, AttackMax);
    	GetWorldTimerManager().SetTimer(AttackTimer, this, &AEnemy::Attack, AttackTime);
    }
    
    void AEnemy::ClearAttackTimer()
    {
    	GetWorldTimerManager().ClearTimer(AttackTimer);
    }
    
    bool AEnemy::InTargetRange(AActor* Target, double Radius)
    {
    	if (Target == nullptr) return false;
    	const double DistanceToTarget = (Target->GetActorLocation() - GetActorLocation()).Size();
    	return DistanceToTarget <= Radius;
    }
    
    void AEnemy::MoveToTarget(AActor* Target)
    {
    	if (EnemyController == nullptr || Target == nullptr) return;
    	FAIMoveRequest MoveRequest;
    	MoveRequest.SetGoalActor(Target);
    	MoveRequest.SetAcceptanceRadius(50.f);
    	EnemyController->MoveTo(MoveRequest);
    }
    
    AActor* AEnemy::ChoosePatrolTarget()
    {
    	TArray ValidTargets;
    	for (AActor* Target : PatrolTargets)
    	{
    		if (Target != PatrolTarget)
    		{
    			ValidTargets.AddUnique(Target);
    		}
    	}
    
    	const int32 NumPatrolTargets = ValidTargets.Num();
    	if (NumPatrolTargets > 0)
    	{
    		const int32 TargetSelection = FMath::RandRange(0, NumPatrolTargets - 1);
    		return ValidTargets[TargetSelection];
    	}
    	return nullptr;
    }
    
    void AEnemy::SpawnDefaultWeapon()
    {
    	UWorld* World = GetWorld();
    	if (World && WeaponClass)
    	{
    		AWeapon* DefaultWeapon = World->SpawnActor(WeaponClass);
    		DefaultWeapon->Equip(GetMesh(), FName("RightHandSocket"), this, this);
    		EquippedWeapon = DefaultWeapon;
    	}
    }
    
    void AEnemy::PawnSeen(APawn* SeenPawn)
    {
    	const bool bShouldChaseTarget =
    		EnemyState != EEnemyState::EES_Dead &&
    		EnemyState != EEnemyState::EES_Chasing &&
    		EnemyState < EEnemyState::EES_Attacking &&
    		SeenPawn->ActorHasTag(FName("EngageableTarget"));
    
    	if (bShouldChaseTarget)
    	{
    		CombatTarget = SeenPawn;
    		ClearPatrolTimer();
    		ChaseTarget();
    	}
    }
    
                                
    Items

    I developed the item system so that collectible items float in air before being collected. Here a sword weapon is picked up and automatically equipped, then sheathed and unsheathed.


    CODE SNIPPET - Item.cpp

    The following code demonstrates the general item functionality.

                                AItem::AItem()
    {
    	PrimaryActorTick.bCanEverTick = true;
    
    	ItemMesh = CreateDefaultSubobject(TEXT("ItemMeshComponent"));
    	ItemMesh->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
    	ItemMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
    	RootComponent = ItemMesh;
    
    	Sphere = CreateDefaultSubobject(TEXT("Sphere"));
    	Sphere->SetupAttachment(GetRootComponent());
    
    	EmbersEffect = CreateDefaultSubobject(TEXT("Embers"));
    	EmbersEffect->SetupAttachment(GetRootComponent());
    }
    
    void AItem::BeginPlay()
    {
    	Super::BeginPlay();
    
    	Sphere->OnComponentBeginOverlap.AddDynamic(this, &AItem::OnSphereOverlap);
    	Sphere->OnComponentEndOverlap.AddDynamic(this, &AItem::OnSphereEndOverlap);
    }
    
    float AItem::TransformedSin()
    {
    	return Amplitude * FMath::Sin(RunningTime * TimeConstant);
    }
    
    float AItem::TransformedCos()
    {
    	return Amplitude * FMath::Cos(RunningTime * TimeConstant);
    }
    
    void AItem::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
    	UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
    {
    	APlayerCharacter* PlayerCharacter = Cast(OtherActor);
    	if(PlayerCharacter)
    	{
    		PlayerCharacter->SetOverlappingItem(this);
    	}
    
    }
    
    void AItem::OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
    	UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
    {
    	APlayerCharacter* PlayerCharacter = Cast(OtherActor);
    	if(PlayerCharacter)
    	{
    		PlayerCharacter->SetOverlappingItem(nullptr);
    	}
    }
    
    void AItem::Tick(float DeltaTime)
    {
    	Super::Tick(DeltaTime);
    	RunningTime += DeltaTime;
    	if(ItemState == EItemState::EIS_Hovering)
    	{
    		AddActorWorldOffset(FVector(0.f,0.f, TransformedSin()));
    
    	}
    
    	FVector AvgVector = Avg(GetActorLocation(),FVector::Zero());
    	DRAW_POINT_SingleFrame(AvgVector);
    
                            
    CODE SNIPPET - Weapon.cpp

    The following code demonstrates weapon item functionality.

    AWeapon::AWeapon()
    {
    	WeaponBox = CreateDefaultSubobject(TEXT("Weapon Box"));
    	WeaponBox->SetupAttachment(GetRootComponent());
    	WeaponBox->SetCollisionEnabled(ECollisionEnabled::NoCollision);
    	WeaponBox->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Overlap);
    	WeaponBox->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Ignore);
    
    	BoxTraceStart = CreateDefaultSubobject(TEXT("Box Trace Start"));
    	BoxTraceStart->SetupAttachment(GetRootComponent());
    	BoxTraceEnd = CreateDefaultSubobject(TEXT("Box Trace End"));
    	BoxTraceEnd->SetupAttachment(GetRootComponent());
    
    }
    
    void AWeapon::BeginPlay()
    {
    	Super::BeginPlay();
    	WeaponBox->OnComponentBeginOverlap.AddDynamic(this, &AWeapon::AWeapon::OnBoxOverlap);
    }
    
    void AWeapon::PlayEquipSound()
    {
    	if(EquipSound)
    	{
    		UGameplayStatics::PlaySoundAtLocation(
    			this,
    			EquipSound,
    			GetActorLocation()
    		);
    	}
    }
    
    void AWeapon::DisableSphereCollision()
    {
    	if(Sphere)
    	{
    		Sphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
    	}
    }
    
    void AWeapon::DeactivateEmberEffect()
    {
    	if(EmbersEffect)
    	{
    		EmbersEffect->Deactivate();
    	}
    }
    
    void AWeapon::Equip(USceneComponent* InParent, FName InSocketName, AActor* NewOwner, APawn* NewInstigator)
    {
    	ItemState = EItemState::EIS_Equipped;
    	SetOwner(NewOwner);
    	SetInstigator(NewInstigator);
    	AttachMeshToSocket(InParent, InSocketName);
    	DisableSphereCollision();
    	PlayEquipSound();
    	DeactivateEmberEffect();
    }
    
    void AWeapon::OnBoxOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
    {
    	if (ActorIsSameType(OtherActor)) return;
    
    	FHitResult BoxHit;
    	BoxTrace(BoxHit);
    
    	if (BoxHit.GetActor())
    	{
    		if (ActorIsSameType(BoxHit.GetActor())) return;
    
    		UGameplayStatics::ApplyDamage(BoxHit.GetActor(), Damage, GetInstigator()->GetController(), this, UDamageType::StaticClass());
    		ExecuteGetHit(BoxHit);
    		// for destructible objects
    		CreateFields(BoxHit.ImpactPoint);
    	}
    }
    
    bool AWeapon::ActorIsSameType(AActor* OtherActor)
    {
    	return GetOwner()->ActorHasTag(TEXT("Enemy")) && OtherActor->ActorHasTag(TEXT("Enemy"));
    }
    
    void AWeapon::ExecuteGetHit(FHitResult& BoxHit)
    {
    	IHitInterface* HitInterface = Cast(BoxHit.GetActor());
    	if (HitInterface)
    	{
    		HitInterface->Execute_GetHit(BoxHit.GetActor(), BoxHit.ImpactPoint, GetOwner());
    	}
    }
    
    void AWeapon::BoxTrace(FHitResult& BoxHit)
    {
    	const FVector Start = BoxTraceStart->GetComponentLocation();
    	const FVector End = BoxTraceEnd->GetComponentLocation();
    
    	TArray ActorsToIgnore;
    	ActorsToIgnore.Add(this);
    
    	for (AActor* Actor : IgnoreActors)
    	{
    		ActorsToIgnore.AddUnique(Actor);
    	}
    
    	UKismetSystemLibrary::BoxTraceSingle(
    		this,
    		Start,
    		End,
    		BoxTraceExtent,
    		BoxTraceStart->GetComponentRotation(),
    		ETraceTypeQuery::TraceTypeQuery1,
    		false,
    		ActorsToIgnore,
    		bShowBoxDebug ? EDrawDebugTrace::ForDuration : EDrawDebugTrace::None,
    		BoxHit,
    		true
    	);
    	IgnoreActors.AddUnique(BoxHit.GetActor());
    }
    
    void AWeapon::AttachMeshToSocket(USceneComponent* InParent, const FName& InSocketName)
    {
    	FAttachmentTransformRules TransformRules(EAttachmentRule::SnapToTarget, true);
    	ItemMesh->AttachToComponent(InParent, TransformRules, InSocketName);
    }
                            
    CODE SNIPPET - Treasure.cpp

    The following code demonstrates the treasure item functionality.

    void ATreasure::OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
    	UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
    {
    	APlayerCharacter* PlayerCharacter = Cast(OtherActor);
    	if(PlayerCharacter)
    	{
    		if(PickupSound)
    		{
    			UGameplayStatics::PlaySoundAtLocation(
    				this,
    				PickupSound,
    				GetActorLocation());
    		}
    		Destroy();
    	}
    }
                            

    Early Unity Programming

    C#

    Gameplay Demo 1:
  • Unity store assets
  • Player 8 directional movement + sprint
  • Basic + combo attack
  • Block, blocked + unblocked enemy attack
  • Poison build up + damage.

  • Enemy AI

    I developed an enemy AI combat system with several states.

  • Idle State: enemies patrol unaware
  • Ambush State: enemies lie stationary, unaware, get up and react
  • Combat State: enemies stand guard
  • Pursue Target State: enemies chase player
  • Attack State: enemy attacks player
  • Boss Combat State: boss switches attack animations based on state change

  • CODE SNIPPET - Attack State

    The following code demonstrates the code used while an enemy is attacking the player

    public class AttackState : State
    {
        public RotateTowardsTargetState rotateTowardsTargetState;
        public CombatStanceState combatStanceState;
        public PursueTargetState pursueTargetState;
        public EnemyAttackAction currentAttack;
    
        private bool willDoComboOnNextAttack = false;
        public bool hasPerformedAttack = false;
        public override State Tick(EnemyManager enemyManager, EnemyStatsManager enemyStatsManager, EnemyAnimationManager enemyAnimationManager)
        {
            float distanceFromTarget = Vector3.Distance(enemyManager.currentTarget.transform.position,
                enemyManager.transform.position);
            RotateTowardsTargetWhileAttacking(enemyManager);
    
            if (distanceFromTarget > enemyManager.maximumAggroRadius)
            {
                return pursueTargetState;
            }
    
            if (willDoComboOnNextAttack && enemyManager.canDoCombo)
            {
                AttackTargetWithCombo(enemyAnimationManager, enemyManager);
            }
    
            if (!hasPerformedAttack)
            {
                AttackTarget(enemyAnimationManager, enemyManager);
                RollForComboChance(enemyManager);
            }
    
            if (willDoComboOnNextAttack && hasPerformedAttack)
            {
                return this; // goes back to perform combo
            }
    
            // always recenter to target player
            return rotateTowardsTargetState;
    
        }
    
        private void AttackTarget(EnemyAnimationManager enemyAnimationManager, EnemyManager enemyManager)
        {
            enemyAnimationManager.animator.SetBool("isUsingRightHand", currentAttack.isRightHandAction);
            enemyAnimationManager.animator.SetBool("isUsingLeftHand", !currentAttack.isRightHandAction);
    
            enemyAnimationManager.PlayTargetAnimation(currentAttack.actionAnimation, true);
            enemyAnimationManager.PlayWeaponTrailFX();
            // only regular attacks have combo revoery ties
            enemyManager.currentRecoveryTime = currentAttack.recoveryTime;
            hasPerformedAttack = true;
        }
        private void AttackTargetWithCombo(EnemyAnimationManager enemyAnimationManager, EnemyManager enemyManager)
        {
            enemyAnimationManager.animator.SetBool("isUsingRightHand", currentAttack.isRightHandAction);
            enemyAnimationManager.animator.SetBool("isUsingLeftHand", !currentAttack.isRightHandAction);
            willDoComboOnNextAttack = false;
            enemyAnimationManager.PlayTargetAnimation(currentAttack.actionAnimation, true);
            enemyAnimationManager.PlayWeaponTrailFX();
            enemyManager.currentRecoveryTime = currentAttack.recoveryTime;
            currentAttack = null;
        }
        private void RotateTowardsTargetWhileAttacking(EnemyManager enemyManager)
        {
    
            // rotate manually
            if (enemyManager.canRotate && enemyManager.isInteracting)
            {
                Vector3 direction = enemyManager.currentTarget.transform.position - enemyManager.transform.position;
                direction.y = 0;
                direction.Normalize();
    
                if (direction == Vector3.zero)
                {
                    direction = transform.forward;
                }
    
                Quaternion targetRotation = Quaternion.LookRotation(direction);
                enemyManager.transform.rotation = Quaternion.Slerp(transform.rotation,targetRotation, enemyManager.rotationSpeed / Time.deltaTime);
            }
            // want a hybrid system that uses navmesh and is brainless to be able to follow you easily on ground and off a cliff
    
        }
        private void RollForComboChance(EnemyManager enemyManager)
        {
            // create a random generator
            float comboChance = Random.Range(0, 100);
    
            if (enemyManager.allowAIToPerformCombos && comboChance <= enemyManager.comboLikelihood)
            {
                // only perform combo if attack has calid combo setup
                if (currentAttack.comboAction != null)
                {
                    willDoComboOnNextAttack = true;
                    currentAttack = currentAttack.comboAction;
                }
                else
                {
                    willDoComboOnNextAttack = false;
                    currentAttack = null;
                }
            }
        }
    }
    }
    
                                
    Player Combat Behavior

    I developed a combat system with a series of combat colliders that detect various forms of damage.

  • Damage Collider: general collider for when characters are hit
  • Spell Damage Collider: detects spell damage and elemental damage types (fire, poison, physical)
  • Bomb Damage Collider: impacts characters in a radius of explosion and adjusts damage based on proximity to blast impact
  • Blocking Collider: detects when character is blocking and receive reduced damaged based on shield stats and stagger attacker if timed correctly
  • Critical Damage Collider: detects when a critical attack is attempted and possible; allows for special assassination animation
  • Posion: poisons character with a damage-over-time status after a limit of poison resistance is reached

  • CODE SNIPPET - Bomb Damage Collider

    The following code demonstrates the code used for bomb damage

    
       public class BombDamageCollider : DamageCollider
    {
       [Header("Explosive Damage & Radius")]
       public int explosiveRadius = 1;
       public int explosionDamage;
       public int explosionSplashDamage; // other damage types etc
    
       public Rigidbody bombRigidbody;
       private bool hasCollided = false;
       public GameObject impactParticles;
    
       protected override void Awake()
       {
          damageCollider = GetComponent();
          bombRigidbody = GetComponent();
       }
    
       private void OnCollisionEnter(Collision collision)
       {
          if (!hasCollided)
          {
             hasCollided = true;
             impactParticles = Instantiate(impactParticles, transform.position, Quaternion.identity);
             Explode();
    
             CharacterStatsManager character = collision.transform.GetComponent();
    
             if (character != null)
             {
                if (character.teamIDNumber != teamIDNumber)
                {
                   // check for friendly fire
                   character.TakeDamage(physicalDamage, explosionDamage, true);
                }
             }
    
             Destroy(impactParticles, 5f);
            Destroy(transform.parent.gameObject);
          }
       }
    
       private void Explode()
       {
          // iterate through characters in radius to deal damage to
          Collider[] characters = Physics.OverlapSphere(transform.position, explosiveRadius);
          foreach (Collider objectsInExplosion in characters)
          {
             CharacterStatsManager character = objectsInExplosion.GetComponent();
             if (character != null)
             {
                if (character.teamIDNumber != teamIDNumber)
                {
                   // deal fire damage
                   character.TakeDamage(0, explosionSplashDamage, true);
                }
             }
          }
       }
    }
    }
                                
    User Interface (UI)

    I developed a user interface system with a pause menu with three options and in-game UI.

  • Inventory: view current inventory items, replace and equip new ones
  • Game Settings: template buttons for volume and language
  • Equipped Items: view character items
  • Pop Up UI: pop up messages that show item icon and name
  • Quick Slots: cyclable items to use in combat and gameplay for left and right hand, spell, and consumable.
  • Player Stats: Health, Magic, Stamina, Experience, Current Poison Effects

  • CODE SNIPPET - UI Manager

    The following code shows in-game player UI

    public class UIManager : MonoBehaviour
    {
        [HideInInspector] public PlayerInventoryManager playerInventoryManager;
        public EquipmentWindowUI equipmentWindowUI;
        public QuickSlotsUI quickSlotsUI;
    
        [Header("UI Windows")]
        public GameObject hudWindow;
        public GameObject selectWindow;
        public GameObject equipmentScreenWindow;
        public GameObject weaponInventoryWindow;
    
        [Header("Equipment Window Slot Selected")]
        public bool rightHandSlot01Selected;
        public bool rightHandSlot02Selected;
        public bool leftHandSlot01Selected;
        public bool leftHandSlot02Selected;
    
        [Header("Weapon Inventory")]
        public GameObject weaponInventorySlotPrefab;
        public Transform weaponInventorySlotsParent;
        private WeaponInventorySlot[] weaponInventorySlots;
    
    
        private void Awake()
        {
            playerInventoryManager = FindObjectOfType();
            quickSlotsUI = GetComponentInChildren();
            //TODO: Unity doesnt like this:
            //equipmentWindowUI = FindObjectOfType();
        }
    
        private void Start()
        {
            weaponInventorySlots = weaponInventorySlotsParent.GetComponentsInChildren();
            // load weapons on screen
            equipmentWindowUI.LoadWeaponOnEquipmentScreen(playerInventoryManager);
            quickSlotsUI.UpdateCurrentSpellIcon(playerInventoryManager.currentSpell);
            quickSlotsUI.UpdateCurrentConsumableIcon(playerInventoryManager.currentConsumableItem);
    
        }
    
        public void UpdateUI()
        {
            #region Weapon Inventory Slots
            for (int i = 0; i < weaponInventorySlots.Length; i++)
            {
                if (i < playerInventoryManager.weaponsInventory.Count)
                {
                    if (weaponInventorySlots.Length < playerInventoryManager.weaponsInventory.Count)
                    {
                        // instantiate a prefab
                        Instantiate(weaponInventorySlotPrefab, weaponInventorySlotsParent);
                        weaponInventorySlots = weaponInventorySlotsParent.GetComponentsInChildren();
                    }
                    weaponInventorySlots[i].AddItem(playerInventoryManager.weaponsInventory[i]);
                }
                else
                {
                    weaponInventorySlots[i].ClearInventorySLot();
                }
            }
            #endregion
        }
        public void OpenSelectWindow()
        {
            selectWindow.SetActive(true);
        }
    
        public void CloseSelectWindow()
        {
            selectWindow.SetActive(false);
        }
    
        public void CloseAllInventoryWindows()
        {
            ResetAllSelectedSlots();
            weaponInventoryWindow.SetActive(false);
            equipmentScreenWindow.SetActive(false);
        }
    
        public void ResetAllSelectedSlots()
        {
            rightHandSlot01Selected = false;
            rightHandSlot02Selected = false;
            leftHandSlot01Selected = false;
            leftHandSlot02Selected = false;
    
        }
    }
                                
    Player Character Managers

    I developed a player system with input and camera behavior managers. Note: Enemies have corresponding modularized versions that have similar functionality for non-playable characters.

  • Camera Manager: Determines camera behavior when player is moving or locking onto enemy targets
  • Input Handler: Determines input behavior based on states and defined inputs
  • Player Animator Manager: determines which animations and overlays will play based on player state (attacking, blocking, sprinting, etc)
  • Player Combat Manager: determines enemy "teams", player combat states (attacking, blocking, dying, staggering, etc), enemy lock on targets, and available attacks based on current weapons, stamina, and magic
  • Player Effects Manager: instantiates blood splatter and fire effects
  • Player Inventory Manager: holds UI information for left hand, right hand, current spell, current consumable, pause menu, and picked-up items
  • Player Stats Manager: holds health, stamina, magic, experience points, and poise (likelihood to stagger from enemy attack). Player shrugs when out of stamina or magic
  • Player Equipment Manager: holds equipment information for inventory and combat use
  • Player Locomotion Manager: holds movement behavior data based on combat and movement behavior such as dodging, jumping, sprinting, etc.

  • CODE SNIPPET - Input Handler

    The following code demonstrates input handling for movement, attacks, and consumables in various game contexts

        public class InputHandler : MonoBehaviour
        {
            public float horizontal;
            public float vertical;
            public float moveAmount;
            public float mouseX;
            public float mouseY;
    
            public bool aInput;
            public bool b_input;
            public bool consume_Input;
            public bool y_Input;
            public bool rightbumper_Input;
            public bool righttrigger_Input;
            public bool leftbumper_Input;
            public bool lefttrigger_Input;
            public bool critical_Attack_Input;
            public bool jump_Input;
            public bool inventory_Input;
            public bool lockOn_Input;
    
            public bool rightStick_Right_Input;
            public bool rightStick_Left_Input;
    
            public bool d_Pad_Up;
            public bool d_Pad_Down;
            public bool d_Pad_Left;
            public bool d_Pad_Right;
    
            public bool rollFlag;
            public bool twoHandFlag;
            public bool sprintFlag;
            public bool comboFlag;
            public bool lockOnFlag;
            public bool inventoryFlag;
            public float rollInputTimer;
    
            // need a specific transform for critical attack (or else raycast will come from ground (default))
            public Transform criticalAttackRaycatStartPoint;
    
            private PlayerControls inputActions;
            private PlayerCombatManager playerCombatManager;
            private PlayerInventoryManager playerInventoryManager;
            private PlayerManager playerManager;
            private PlayerFXManager playerFXManager;
            private PlayerStatsManager playerStatsManager;
            private BlockingCollider blockingCollider;
            private PlayerWeaponSlotManager playerWeaponSlotManager;
            private CameraHandler cameraHandler;
            private PlayerAnimatorManager playerAnimatorManager;
            private UIManager uIManager;
    
            private Vector2 movementInput;
            private Vector2 cameraInput;
    
            private void Awake()
            {
                playerCombatManager = GetComponent();
                playerInventoryManager = GetComponent();
                playerManager = GetComponent();
                playerFXManager = GetComponent();
                playerStatsManager = GetComponent();
                // calling this bc we must reload weapons on addition of new wepon
                playerWeaponSlotManager = GetComponent();
                blockingCollider = GetComponentInChildren();
                cameraHandler = FindObjectOfType();
                playerAnimatorManager = GetComponent();
                uIManager = FindObjectOfType();
            }
    
            public void OnEnable()
            {
                if (inputActions == null)
                {
                    inputActions = new PlayerControls();
                    inputActions.PlayerMovement.Movement.performed +=
                        inputActions => movementInput = inputActions.ReadValue();
                    inputActions.PlayerMovement.Camera.performed += i => cameraInput = i.ReadValue();
    
                    inputActions.PlayerActions.SelectButton.performed += inputActions => aInput = true;
    
                    // handle attack input
                    inputActions.PlayerActions.RB.performed += i => rightbumper_Input = true;
                    inputActions.PlayerActions.RT.performed += i => righttrigger_Input = true;
                    inputActions.PlayerActions.LeftBumper.performed += i => leftbumper_Input = true;
                    inputActions.PlayerActions.LeftBumper.canceled += i => leftbumper_Input = false;
                    inputActions.PlayerActions.LeftTrigger.performed += inputActions => lefttrigger_Input = true;
    
                    // handle quick slot
                    inputActions.PlayerActions.DPadRight.performed += i => d_Pad_Right = true;
                    inputActions.PlayerActions.DPadLeft.performed += i => d_Pad_Left = true;
    
                    // handle select
                    inputActions.PlayerActions.SelectButton.performed += i => aInput = true;
                    inputActions.PlayerActions.X.performed += i => consume_Input = true;
                    // handle roll input
                    // when input is cancelled
                    inputActions.PlayerActions.Roll.performed += i => b_input = true;
                    inputActions.PlayerActions.Roll.canceled += i => b_input = false;
    
                    // handle jump input
                    inputActions.PlayerActions.Jump.performed += i => jump_Input = true;
    
                    //handle inventory input
                    inputActions.PlayerActions.Inventory.performed += i => inventory_Input = true;
    
                    // handle lock on input
                    inputActions.PlayerActions.LockOn.performed += i => lockOn_Input = true;
    
                    //handle lock on right and left
                    inputActions.PlayerMovement.LockOnTargetRight.performed += i => rightStick_Right_Input = true;
                    inputActions.PlayerMovement.LockOnTargetLeft.performed += i => rightStick_Left_Input = true;
    
                    //handle
                    inputActions.PlayerActions.Y.performed += i => y_Input = true;
    
                    // handle critical attack
                    inputActions.PlayerActions.CriticalAttack.performed += i => critical_Attack_Input = true;
    
                    Debug.DrawRay(criticalAttackRaycatStartPoint.position,transform.TransformDirection(Vector3.forward), Color.red, 0.5f);
    
                }
    
                inputActions.Enable();
            }
    
            private void OnDisable()
            {
                inputActions.Disable();
            }
    
            public void TickInput(float delta)
            {
                if (playerStatsManager.isDead)
                {
                    return;
                }
                HandleMoveInput(delta);
                HandleRollInput(delta);
                HandleCombatInput(delta);
                HandleQuickSlotInput();
                HandleInventoryInput();
                HandleLockOnInput();
                HandleTwoHandInput();
                HandleCriticalAttackInput();
                HandleConsumableInput();
            }
    
            public void HandleMoveInput(float delta)
            {
                horizontal = movementInput.x;
                vertical = movementInput.y;
                moveAmount = Mathf.Clamp01(Mathf.Abs(horizontal) + Mathf.Abs(vertical));
                mouseX = cameraInput.x;
                mouseY = cameraInput.y;
            }
    
            private void HandleRollInput(float delta)
            {
                // will detect when key is pressed and make bool true
                //b_input = inputActions.PlayerActions.Roll.phase == UnityEngine.InputSystem.InputActionPhase.Started;
                //b_input = inputActions.PlayerActions.Roll.IsPressed();
    
                if (b_input)
                {
                    rollInputTimer += delta;
                    if (playerStatsManager.currentStamina <= 0)
                    {
                        b_input = false;
                        sprintFlag = false;
                    }
    
                    if (moveAmount > 0.5f && playerStatsManager.currentStamina > 0)
                    {
                        sprintFlag = true;
                    }
                }
                else
                {
                    sprintFlag = false;
    
                    // tapping b
                    if (rollInputTimer > 0 && rollInputTimer < 0.5f)
                    {
                        rollFlag = true;
                    }
    
                    //reset the timer
                    rollInputTimer = 0;
                }
            }
    
            private void HandleCombatInput(float delta)
            {
                if (rightbumper_Input == true)
                {
                    playerCombatManager.HandleRBAction();
                }
                if (righttrigger_Input == true)
                {
                    playerCombatManager.HandleHeavyAttack(playerInventoryManager.rightWeapon);
                }
    
    
                if (lefttrigger_Input)
                {
                    // if two handing handle weapon animation
                    if (twoHandFlag)
                    {
                        // else handle light attack if melee weapon handle weapon art
                        // handle sheild attack
                    }
                    else
                    {
                        playerCombatManager.HandleLTAction();
                    }
                }
                if (leftbumper_Input)
                {
                    // do a block
                    playerCombatManager.HandleLBAction();
                }
                else
                {
                    playerManager.isBlocking = false;
                    if (blockingCollider.blockingBoxCollider.enabled)
                    {
                        blockingCollider.DisableBlockingCollider();
                    }
                }
            }
    
            private void HandleQuickSlotInput()
            {
                if (d_Pad_Right)
                {
                    playerInventoryManager.ChangeRightWeapon();
                }
                else if (d_Pad_Left)
                {
                    playerInventoryManager.ChangeLeftWeapon();
                }
            }
    
            private void HandleInventoryInput()
            {
                if (inventory_Input)
                {
                    inventoryFlag = !inventoryFlag;
                    if (inventoryFlag)
                    {
                        uIManager.OpenSelectWindow();
                        uIManager.UpdateUI();
                        uIManager.hudWindow.SetActive(false);
                    }
                    else
                    {
                        uIManager.CloseSelectWindow();
                        uIManager.CloseAllInventoryWindows();
                        uIManager.hudWindow.SetActive(true);
                    }
                }
    
            }
    
            private void HandleLockOnInput()
            {
                // if no target is currently locked on
                if (lockOn_Input && lockOnFlag == false)
                {
                    lockOn_Input = false;
                    // find nearest lock on target to move camer to
                    cameraHandler.HandleLockOn();
                    if (cameraHandler.nearestLockOnTarget != null)
                    {
                        cameraHandler.currentLockOnTarget = cameraHandler.nearestLockOnTarget;
                        lockOnFlag = true;
                    }
                }
                else if (lockOn_Input && lockOnFlag)
                {
                    lockOn_Input = false;
                    lockOnFlag = false;
                    cameraHandler.ClearLockOnTargets();
                }
    
                if (lockOnFlag && rightStick_Left_Input)
                {
                    rightStick_Left_Input = false;
                    cameraHandler.HandleLockOn();
                    if (cameraHandler.leftLockTarget != null)
                    {
                        // assign current lock on target to target in left
                        cameraHandler.currentLockOnTarget = cameraHandler.leftLockTarget;
                    }
                }
    
                else if (lockOnFlag && rightStick_Right_Input)
                {
                    rightStick_Right_Input = false;
                    cameraHandler.HandleLockOn();
                    if (cameraHandler.rightLockTarget != null)
                    {
                        // assign current lock on target to target in right
                        cameraHandler.currentLockOnTarget = cameraHandler.rightLockTarget;
                    }
                }
                cameraHandler.SetCameraHeight();
            }
    
            private void HandleTwoHandInput()
            {
                if (y_Input)
                {
                    // switch to false so that it activates only once per frame
                    y_Input = false;
                    // switch state of flag
                    twoHandFlag = !twoHandFlag;
                    if (twoHandFlag)
                    {
                        //enable two handed w. right weapon
                        playerManager.isTwoHandingWeapon = true;
                        playerWeaponSlotManager.LoadWeaponOnSlot(playerInventoryManager.rightWeapon, false);
                        playerWeaponSlotManager.LoadTwoHandIKTargets(true);
                    }
                    else
                    {
                        //disable two handed
                        playerManager.isTwoHandingWeapon = false;
                        playerWeaponSlotManager.LoadWeaponOnSlot(playerInventoryManager.rightWeapon, false);
                        playerWeaponSlotManager.LoadWeaponOnSlot(playerInventoryManager.leftWeapon, true);
                        playerWeaponSlotManager.LoadTwoHandIKTargets(false);
                    }
                }
            }
    
            private void HandleCriticalAttackInput()
            {
                if (critical_Attack_Input)
                {
                    // disable after use
                    critical_Attack_Input = false;
                    playerCombatManager.AttemptBackstabOrRiposte();
                }
            }
    
            private void HandleConsumableInput()
            {
                if (consume_Input)
                {
                    consume_Input = false;
                    // sue current consumable
                    playerInventoryManager.currentConsumableItem.AttemptToConsumeItem(playerAnimatorManager, playerWeaponSlotManager, playerFXManager);
                }
            }
        }
    }
    
                                
    Items (weapons, armor, consumables)

    I developed an item system collection system where weapons and consumables can be collected and appear in chests, producing a sparkle effect until collected. Each item has its own consume animation an effects. Weapons and consumables exist as scriptable objects with customizable damage, animaitons, and meshes. Equipment can be equipped and replaced. Consumable and equipment types include:

  • Bomb: a throwable object that explodes on impact with modifiable arc, damage, and explosion
  • Cure: a potion that heals health and stops poison effects
  • Flask: a potion that heals magic and boosts regeneration
  • Equipment Items: Helmet, Feet, Torso, Legs, Hands

  • CODE SNIPPET - Bomb Item

    The following code demonstrates bomb item functionality

    
    public class BombConsumableItem : ConsumableItem
    {
        [Header("Velocity")]
        public int upwardVelocity = 50;
        public int forwardVelocity = 50;
        public int bombMass = 1;
    
        [Header("Live Bomb Model")]
        public GameObject liveBombModel;
    
        [Header("Base Damage")]
        public int baseDamage = 200;
        public int explosiveDamage = 75;
    
        public override void AttemptToConsumeItem(PlayerAnimatorManager playerAnimatorManager, PlayerWeaponSlotManager playerWeaponSlotManager,
            PlayerFXManager playerFXManager)
        {
            if (currentItemAmount > 0)
            {
                playerWeaponSlotManager.rightHandSlot.UnloadWeapon();
                playerAnimatorManager.PlayTargetAnimation(consumeAnimation,true);
                GameObject bombModel = Instantiate(itemModel, playerWeaponSlotManager.rightHandSlot.transform.position,
                    Quaternion.identity, playerWeaponSlotManager.rightHandSlot.transform);
                playerFXManager.instantiatedFXModel = bombModel;
            }
            else
            {
                playerAnimatorManager.PlayTargetAnimation("Shrugging", true);
            }
        }
    }
                                
    CODE SNIPPET - Equipment Model Changer

    The following code demonstrates template equipment change functionality

    
        public class EquipmentModelChanger : MonoBehaviour
        {
            public List bodyPartModels;
    
            private void Awake()
            {
                GetAllBodyPartModels();
            }
    
            private void GetAllBodyPartModels()
            {
                int childrenGameObjects = transform.childCount;
                for (int i = 0; i < childrenGameObjects; i++)
                {
                    bodyPartModels.Add(transform.GetChild(i).gameObject);
                }
            }
    
            public void UnequipAllBodyPartModels()
            {
                foreach (GameObject torsoModel in bodyPartModels)
                {
                    torsoModel.SetActive(false);
                }
            }
    
            public void EquipBodyPartModelByName(string bodyPartName)
            {
                for (int i = 0; i < bodyPartModels.Count; i++)
                {
                    if (bodyPartModels[i].name == bodyPartName)
                    {
                        bodyPartModels[i].SetActive(true);
                    }
                }
            }
        }
    Inventory

    I developed an inventory system with weapon slots, inventory consumable slots, and item pick up funcitonality.

    CODE SNIPPET - Inventory Manager

    The following code demonstrates the code used

    
     public class PlayerInventoryManager : CharacterInventoryManager
        {
    
            public List weaponsInventory;
    
            public void ChangeRightWeapon()
            {
    
                currentRightWeaponIndex = currentRightWeaponIndex + 1;
    
                if (currentRightWeaponIndex > weaponsInRightHandSlots.Length - 1)
                {
                    currentRightWeaponIndex = -1;
                    rightWeapon = characterWeaponSlotManager.unarmedWeapon;
                    characterWeaponSlotManager.LoadWeaponOnSlot(characterWeaponSlotManager.unarmedWeapon, false);
                }
                else if (weaponsInRightHandSlots[currentRightWeaponIndex] != null)
                {
                    rightWeapon = weaponsInRightHandSlots[currentRightWeaponIndex];
                    characterWeaponSlotManager.LoadWeaponOnSlot(weaponsInRightHandSlots[currentRightWeaponIndex], false);
                }
                else
                {
                    currentRightWeaponIndex = currentRightWeaponIndex + 1;
                }
    
            }
            public void ChangeLeftWeapon()
            {
                currentLeftWeaponIndex = currentLeftWeaponIndex + 1;
    
                if (currentLeftWeaponIndex > weaponsInLeftHandSlots.Length - 1)
                {
                    currentLeftWeaponIndex = -1;
                    leftWeapon = characterWeaponSlotManager.unarmedWeapon;
                    characterWeaponSlotManager.LoadWeaponOnSlot(characterWeaponSlotManager.unarmedWeapon, true);
                }
                else if (weaponsInLeftHandSlots[currentLeftWeaponIndex] != null)
                {
                    leftWeapon = weaponsInLeftHandSlots[currentLeftWeaponIndex];
                    characterWeaponSlotManager.LoadWeaponOnSlot(weaponsInLeftHandSlots[currentLeftWeaponIndex], true);
                }
                else
                {
                    currentLeftWeaponIndex = currentLeftWeaponIndex + 1;
                }
            }
    
        }
                                
    Events

    I developed several events systems that trigger on command.

  • Open Chest: player opens chest and collects item
  • Enter Boss Arena: player enters an arena by diving over fire and is blocked from re-entering
  • Fog Wall/Illusionary Door: a pathway is blocked by fog until player steps through

  • CODE SNIPPET - Disappearing Door/Wall

    The following code demonstrates a triggered disappearing door functionality

    
    public class IllusionaryDoor : MonoBehaviour
    {
        public bool doorHasBeenHit;
        public Material illusionaryDoorMaterial;
        public float alpha;
        public float fadeTimer = 2.5f;
    
        public BoxCollider doorCollider;
        public AudioSource audioSource;
        public AudioClip illusionaryDoorSound;
    
        private void Awake()
        {
            doorCollider = GetComponent();
            illusionaryDoorMaterial = GetComponent();
            audioSource = GetComponent();
        }
    
        private void Update()
        {
            if (doorHasBeenHit)
            {
                FadeIllusionaryDoor();
            }
        }
    
        public void FadeIllusionaryDoor()
        {
            alpha = illusionaryDoorMaterial.color.a;
            alpha = alpha - Time.deltaTime / fadeTimer;
            // color will keep changing over time, only chage transperancy
            Color fadeDoorColor = new Color(1, 1, 1, alpha);
            illusionaryDoorMaterial.color = fadeDoorColor;
    
            if (doorCollider.enabled)
            {
                doorCollider.enabled = false;
                audioSource.PlayOneShot(illusionaryDoorSound);
            }
    
            // detroy object when its faded
            if (alpha <= 0)
            {
                Destroy(this);
            }
        }
    }
                                
    Misc Gameplay Managers

    I developed a series of managers to modularize all characters' inventories, locomotion, animation, effects, stats, UI, and behavior.

    CODE SNIPPET - Character Manager

    The following code demonstrates the code used

    
    public class CharacterManager : MonoBehaviour
    {
       private CharacterAnimatorManager characterAnimatorManager;
       private CharacterWeaponSlotManager characterWeaponSlotManager;
       [Header("Lock On Transform")]
       public Transform lockOnTransform;
    
       [Header("Combat Collider")]
       public CriticalDamageCollider backstabCollider;
       public CriticalDamageCollider riposteCollider;
    
       [Header("Interaction")]
       public bool isInteracting;
    
       [Header("Combat Flags")]
       public bool canBeRiposted;
       public bool canBeParried;
       public bool canDoCombo;
       public bool isParrying;
       public bool isBlocking;
       public bool isInvulnerable;
       public bool isUsingRightHand;
       public bool isUsingLeftHand;
       public bool isTwoHandingWeapon;
    
       [Header("Movement Flags")]
       public bool isRotatingWithRootMotion;
       public bool canRotate;
       public bool isSprinting;
       public bool isGrounded;
       public bool isInAir;
    
       [Header("Spells")]
       public bool isFiringSpell;
    
       // damage will be inflicted during an animation event
       // used in backstab or riposte animations
       public int pendingCriticalDamage;
    
       protected virtual void Awake()
       {
          characterAnimatorManager = GetComponent();
          characterWeaponSlotManager = GetComponent();
       }
       protected virtual void FixedUpdate()
       {
          // reset two handing status if need be
          characterAnimatorManager.CheckHandIKWeight(characterWeaponSlotManager.rightHandIKTarget, characterWeaponSlotManager.leftHandIKTarget, isTwoHandingWeapon);
       }
    }