So I initially completed the course, just following along to the requirements and completing (most) puzzles/challenges, and had a pretty functional if deeply unpolished version working.
I had set myself a strategy of multiple follow throughs as I wanted to add extra stuff to the earlier projects from what I had learned and to help solidfy understanding of the material. So for the shooter project I decided I would attempt to add the countess, as I wanted to do a melee character for the Crypt Raider game and add in enemies and stuff.
So using a combination of the docs, chatGPT, YouTube tutorials, this material and existing code from these and other projects; and a lot of trial and error, I was able to get to about this far in the course before there isn’t quite enough agreement on how to proceed under this model (presented by sam).
So basically I need help.
What I’ve managed to achieve so far is importing the countess content into the game, making this a default pawn, hiding bone names by socket and creating a new sword_bp. I made a pretty crappy blendspace and started putting that together although most material out there supports a blueprints approach.
Where I’m stuck on specifically is getting the melee attack animation to play. The sound plays, there are no FX or animation.
It does spawn the alternate sword objects correctly and when previously tested against the bots made in the game, even though the animation doesn’t blay the collision overlap seems function - as I was able to kill the bots. I’ve futzed with the code a bit since then. But that’s the method.
Sword.cpp
// Copyright (c)
#include "Sword.h"
#include "Components/SkeletalMeshComponent.h"
#include "Kismet/GameplayStatics.h"
#include "Engine/DamageEvents.h"
#include <Tasks/AITask.h>
#include "CountessChar.h"
// Sets default values
ASword::ASword()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
SwordRoot = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
SetRootComponent(SwordRoot);
SwordMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Mesh"));
SwordMesh->SetupAttachment(SwordRoot);
CollisionVolume = CreateDefaultSubobject<UBoxComponent>(TEXT("CollisionVolume"));
CollisionVolume->SetupAttachment(SwordRoot);
CollisionVolume->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
CollisionVolume->OnComponentBeginOverlap.AddDynamic(this, &ASword::OnOverlapBegin);
isAttacking = false;
}
//void ASword::Attack()
//{
// UE_LOG(LogTemp, Warning, TEXT("You've been shot!"));
// //UGameplayStatics::SpawnEmitterAttached(Effect, Mesh, TEXT("EffectSocket"));
// Cast<APawn>(GetOwner());
//
// APawn* OwnerPawn = Cast<APawn>(GetOwner());
// if (OwnerPawn == nullptr) return;
// FHitResult Hit;
//
// bool bSuccess = GetWorld()->LineTraceSingleByChannel(Hit, Location, End, ECollisionChannel::ECC_GameTraceChannel2);
// if (bSuccess)
// {
// DrawDebugPoint(GetWorld(), Hit.Location, 20, FColor::Red, true);
// }
//
// FPointDamageEvent DamageEvent(Damage, Hit, ShotDirection, nullptr);
// HitActor->TakeDamage(Damage, DamageEvent, OwnerController, this);
//
//
// FHitResult Hit;
// FRotator Rotation;
// AActor* HitActor = Hit.GetActor();
// FVector ShotDirection = -Rotation.Vector();
//
//}
//void ASword::Attack()
//{
// UE_LOG(LogTemp, Warning, TEXT("You've been shot!"));
//
// APawn* OwnerPawn = Cast<APawn>(GetOwner());
// if (OwnerPawn == nullptr) return;
// AController* OwnerController = OwnerPawn->GetController();
// if (OwnerController == nullptr) return;
//
// FVector Location;
// FRotator Rotation;
// OwnerController->GetPlayerViewPoint(Location, Rotation);
//
// FVector End = Location + Rotation.Vector() * MaxRange;
//
// FHitResult Hit;
//
// bool bSuccess = GetWorld()->LineTraceSingleByChannel(Hit, Location, End, ECollisionChannel::ECC_GameTraceChannel2);
// if (bSuccess)
// {
// DrawDebugPoint(GetWorld(), Hit.Location, 20, FColor::Red, true);
// }
//
// // Define the HitActor and ShotDirection variables
// AActor* HitActor = Hit.GetActor();
// FVector ShotDirection = -Rotation.Vector();
//
// if (HitActor)
// {
// FPointDamageEvent DamageEvent(Damage, Hit, ShotDirection, nullptr);
// HitActor->TakeDamage(Damage, DamageEvent, OwnerController, this);
// }
//}
float ASword::GetSwingAnimationLength() const
{
return SwingAnimation ? SwingAnimation->GetPlayLength() : 0.0f;
}
void ASword::SwingSword()
{
// Play the sword swing sound
UGameplayStatics::SpawnSoundAttached(SwingSound, SwordMesh, FName("SwingSoundSocket"));
// Play the sword swing animation
//APawn* OwnerPawn = Cast<APawn>(GetOwner());
ACharacter* OwnerCharacter = Cast<ACharacter>(GetOwner());
if (OwnerCharacter)
{
UAnimInstance* AnimInstance = OwnerCharacter->GetMesh()->GetAnimInstance();
if (AnimInstance && SwingAnimation)
{
AnimInstance->Montage_Play(SwingAnimation);
}
}
// Activate the sword's collision volume
CollisionVolume->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
// Set a timer to deactivate the sword's collision volume after a delay
GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &ASword::DeactivateCollision, SwingAnimation->GetPlayLength(), false);
}
void ASword::DeactivateCollision()
{
// Deactivate the sword's collision volume
CollisionVolume->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
// Called when the game starts or when spawned
void ASword::BeginPlay()
{
Super::BeginPlay();
//CollisionVolume->OnComponentBeginOverlap.AddDynamic(this, &ASword::OnOverlapBegin);
}
// Called every frame
void ASword::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
//void ASword::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
//{
//
// if (OtherActor && OtherActor != this && OtherComp)
// {
// // Apply damage or any other desired action when the sword collides with another actor
//
// // Check if the overlapping actor is an instance of ACountessChar
// CountessChar* DamagedCharacter = Cast<ACountessChar>(OtherActor);
// if (DamagedCharacter)
// {
// // Call the ApplyDamage function on the damaged character
// float DamageAmount = 10.0f; // Set the damage amount here
// DamagedCharacter->ApplyDamage(DamageAmount);
// }
// }
//}
//void ASword::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
//{
// if (OtherActor && OtherActor != this && OtherComp)
// {
// // Apply damage or any other desired action when the sword collides with another actor
// float DamageAmount = 10.0f; // Set the damage amount here
// OtherActor->TakeDamage(DamageAmount, FDamageEvent(), nullptr, this);
// }
//}
void ASword::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
// Return early if overlapped actor is the same as this actor or the owner of the sword
if (OtherActor == nullptr || OtherActor == this || OtherActor == GetOwner())
{
return;
}
// Set up the collision query parameters to ignore the sword and its owner
FCollisionQueryParams Params;
Params.AddIgnoredActor(this);
Params.AddIgnoredActor(GetOwner());
// Perform a custom trace (e.g., a sphere trace) to check if the OtherActor is within the sword's damage range
FVector Start = GetActorLocation();
FVector End = OtherActor->GetActorLocation();
float TraceRadius = 10.0f; // Adjust this value based on the sword's damage range
TArray<FHitResult> HitResults;
bool bHit = GetWorld()->SweepMultiByChannel(HitResults, Start, End, FQuat::Identity, ECollisionChannel::ECC_GameTraceChannel2, FCollisionShape::MakeSphere(TraceRadius), Params);
// Check if the trace hit the OtherActor
bool bHitOtherActor = false;
for (const FHitResult& HitResult : HitResults)
{
if (HitResult.GetActor() == OtherActor)
{
bHitOtherActor = true;
break;
}
}
// If the trace hit the OtherActor, apply damage or any other desired action
if (bHitOtherActor)
{
float DamageAmount = 10.0f; // Set the damage amount here
OtherActor->TakeDamage(DamageAmount, FDamageEvent(), GetOwner()->GetInstigatorController(), this);
}
}
Sword.h
// Copyright (c)
#pragma once
#include "Components/BoxComponent.h"
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Sword.generated.h"
class USoundBase;
UCLASS()
class SHOOTER1_API ASword : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
ASword();
UFUNCTION()
void SwingSword();
UPROPERTY()
bool isAttacking;
UPROPERTY(EditAnywhere)
float Damage = 10;
UFUNCTION(BlueprintCallable, Category = "Sword")
USceneComponent* GetSwordRoot() { return SwordRoot; }
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Sword")
UBoxComponent* CollisionVolume;
UFUNCTION(BlueprintCallable, Category = "Animation")
float GetSwingAnimationLength() const;
private:
FTimerHandle TimerHandle;
UPROPERTY(VisibleAnywhere)
USceneComponent* SwordRoot;
UPROPERTY(VisibleAnywhere)
USkeletalMeshComponent* SwordMesh;
UPROPERTY(EditAnywhere)
UParticleSystem* SwordFlash;
UPROPERTY(EditAnywhere)
float MaxRange = 100;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Sword", meta = (AllowPrivateAccess = "true"))
USoundBase* SwingSound;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Sword", meta = (AllowPrivateAccess = "true"))
UAnimMontage* SwingAnimation;
void DeactivateCollision();
UFUNCTION()
void OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
};`
Countess.cpp
// Copyright (c)
#include "CountessChar.h"
#include "Sword.h"
#include "Gun.h"
#include "Components/CapsuleComponent.h"
#include "Shooter1GameModeBase.h"
#include "Engine/DamageEvents.h"
// Sets default values
ACountessChar::ACountessChar()
{
// Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
}
// Called when the game starts or when spawned
void ACountessChar::BeginPlay()
{
Super::BeginPlay();
Sword = GetWorld()->SpawnActor<ASword>(SwordClass);
Sword2 = GetWorld()->SpawnActor<ASword>(SwordClass);
GetMesh()->HideBoneByName(TEXT("weapon_r"), EPhysBodyOp::PBO_None);
GetMesh()->HideBoneByName(TEXT("weapon_l"), EPhysBodyOp::PBO_None);
Sword->AttachToComponent(GetMesh(), FAttachmentTransformRules::KeepRelativeTransform, TEXT("SwordSock_L"));
Sword2->AttachToComponent(GetMesh(), FAttachmentTransformRules::KeepRelativeTransform, TEXT("right_sock"));
Sword->SetOwner(this);
Sword2->SetOwner(this);
EquipSword();
}
// Called every frame
void ACountessChar::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
// Called to bind functionality to input
void ACountessChar::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
PlayerInputComponent->BindAxis(TEXT("MoveForward"), this, &ACountessChar::MoveForward);
PlayerInputComponent->BindAxis(TEXT("LookUp"), this, &APawn::AddControllerPitchInput);
PlayerInputComponent->BindAxis(TEXT("MoveRight"), this, &ACountessChar::MoveRight);
PlayerInputComponent->BindAxis(TEXT("LookRight"), this, &APawn::AddControllerYawInput);
PlayerInputComponent->BindAction(TEXT("Jump"), EInputEvent::IE_Pressed, this, &ACharacter::Jump);
PlayerInputComponent->BindAction(TEXT("Shoot"), EInputEvent::IE_Pressed, this, &ACountessChar::Shoot);
PlayerInputComponent->BindAxis(TEXT("LookUpRate"), this, &ACountessChar::LookUpRate);
PlayerInputComponent->BindAxis(TEXT("LookRightRate"), this, &ACountessChar::LookRightRate);
}
void ACountessChar::MoveForward(float AxisValue)
{
AddMovementInput(GetActorForwardVector() * AxisValue);
}
void ACountessChar::MoveRight(float AxisValue)
{
AddMovementInput(GetActorRightVector()* AxisValue);
}
void ACountessChar::LookUpRate(float AxisValue)
{
AddControllerPitchInput(AxisValue * RotationRate * GetWorld()->GetDeltaSeconds());
}
void ACountessChar::LookRightRate(float AxisValue)
{
AddControllerYawInput(AxisValue * RotationRate * GetWorld()->GetDeltaSeconds());
}
void ACountessChar::Shoot()
{
if (Sword)
{
Sword->SwingSword();
IsAttacking = true;
GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &ACountessChar::ResetIsAttacking, Sword->GetSwingAnimationLength(), false);
}
else if (EquippedGun)
{
EquippedGun->PullTrigger();
}
}
void ACountessChar::ResetIsAttacking()
{
IsAttacking = false;
}
//void ACountessChar::EquipSword()
//{
// if (SwordClass)
// {
// // Spawn the sword
// FActorSpawnParameters SpawnParams;
// SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
// Sword = GetWorld()->SpawnActor<ASword>(SwordClass, FVector::ZeroVector, FRotator::ZeroRotator, SpawnParams);
//
// // Attach the sword to the character's hand socket
// FName SocketName = FName(TEXT("right_sock"));
// if (Sword)
// {
// Sword->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, SocketName);
// }
// }
//}
void ACountessChar::EquipSword()
{
if (SwordClass)
{
FVector Location = FVector::ZeroVector;
FRotator Rotation = FRotator::ZeroRotator;
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
// Spawn the sword actor
ASword* SpawnedSword = GetWorld()->SpawnActor<ASword>(SwordClass, Location, Rotation, SpawnParams);
if (SpawnedSword)
{
// Log that the sword has been spawned
UE_LOG(LogTemp, Warning, TEXT("Sword spawned successfully"));
// Attach the sword's root component to the character's hand socket
FName SocketName = TEXT("right_sock");
USceneComponent* SwordRoot = SpawnedSword->GetSwordRoot();
SwordRoot->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, SocketName);
// Log the attachment process
UE_LOG(LogTemp, Warning, TEXT("Sword attached to the character's hand socket"));
// Set the sword as a member variable
Sword = SpawnedSword;
}
else
{
// Log if the sword failed to spawn
UE_LOG(LogTemp, Error, TEXT("Failed to spawn the sword"));
}
}
else
{
// Log if the SwordClass is not set
UE_LOG(LogTemp, Error, TEXT("SwordClass is not set"));
}
}
bool ACountessChar::IsDead() const
{
return Health <= 0;
}
float ACountessChar::GetHealthPercent() const
{
return 100.0f;
}
float ACountessChar::TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser)
{
float DamageToApply = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
DamageToApply = FMath::Min(Health, DamageToApply);
Health -= DamageToApply;
UE_LOG(LogTemp, Warning, TEXT("Health %f"), Health);
if (IsDead())
{
// DetachFromControllerPendingDestroy();
// GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// ASimpleShooterGameModeBase* GameMode = GetWorld()->GetAuthGameMode<ASimpleShooterGameModeBase>();
AShooter1GameModeBase* GameMode = GetWorld()->GetAuthGameMode<AShooter1GameModeBase>();
if (GameMode != nullptr)
{
GameMode->PawnKilled(this);
}
DetachFromControllerPendingDestroy();
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
return DamageToApply;
}
Countesschar.h
// Copyright (c)
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "CountessChar.generated.h"
class ASword;
class AGun;
UCLASS()
class SHOOTER1_API ACountessChar : public ACharacter
{
GENERATED_BODY()
public:
// Sets default values for this character's properties
ACountessChar();
UFUNCTION(BlueprintCallable, Category = "Attack")
bool GetIsAttacking() const { return IsAttacking; }
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
// Called to bind functionality to input
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
private:
void MoveForward(float AxisValue);
void MoveRight(float AxisValue);
void LookUpRate(float AxisValue);
void LookRightRate(float AxisValue);
void Shoot();
void ResetIsAttacking();
//void Attack();
// Apply damage to the character
UFUNCTION(BlueprintCallable, Category = "Combat")
virtual float TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser) override;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Attack", meta = (AllowPrivateAccess = "true"))
bool IsAttacking;
void EquipSword();
UFUNCTION(BlueprintPure)
bool IsDead() const;
UFUNCTION(BlueprintPure)
float GetHealthPercent() const;
UPROPERTY(EditAnywhere)
float RotationRate = 10;
UPROPERTY(VisibleAnywhere)
float Health = 100.f;
UPROPERTY(EditDefaultsOnly)
float MaxHealth = 100;
UPROPERTY(EditDefaultsOnly)
TSubclassOf<ASword> SwordClass;
UPROPERTY()
ASword* Sword;
UPROPERTY()
ASword* Sword2;
//UPROPERTY(EditDefaultsOnly)
//TSubclassOf<AGun> GunClass;
//UPROPERTY()
//AGun* Gun;
UPROPERTY()
ASword* EquippedSword;
UPROPERTY()
AGun* EquippedGun;
FTimerHandle TimerHandle;
};
The restof this is plugged into very similar logic than that of the ShooterCharacter in the course material .
My anim_Bp for the melee character is however, a complete mess.
We attempted to follow along with the course, although the animations were sufficiently different to force me to get a bit creative to keep moving along. The event graph has obviously cannibalised pieces of the original BP and various detritus - and you can see some of my attempts with various state machines.
I guess what I need help with is, setting up the CountessChar on similarly modular system to the one in the course, correctly setting up the blendspaces and state machines for a melee attack; and I would generally hope that it would work like : Click Button>SwingSword>Sword overlaps on dynamic actor, runs Apply/Take Damage;
I tried hooking up the similar combo system from the original files but got a bit lost. WItht the animation blueprint I just need a bit if guidance where to/how to set up the countess blendspace and statemachines.
Sorry getting vsleepy as writing this.