본문 바로가기

게임프로그래밍/실습2

[실습2] 43. 발사체 생성 능력 만들기

1. 발사체 클래스 생성


발사체를 만들어 이를 발사하는 능력을 캐릭터에게 주려고 한다.

 

 

발사체는 Actor 클래스일 것이다.

 

class UProjectileMovementComponent;
class USphereComponent;

UCLASS()
class PRACTICE2_API AAuraProjectile : public AActor
{
    GENERATED_BODY()
    
public: 
    AAuraProjectile();

    UPROPERTY(VisibleAnywhere)
    TObjectPtr<UProjectileMovementComponent> ProjectileMovement;

protected:
    virtual void BeginPlay() override;

    UFUNCTION()
    void OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
    
private:
    UPROPERTY(VisibleAnywhere)
    TObjectPtr<USphereComponent> Sphere;

};

 

바인딩할 오버랩 함수와 스피어 컴포넌트를 만들자. 무브먼트 컴포넌트가 퍼블릭 섹션에 있는 이유는 나중에 움직임을 다른 클래스에서 제어할 일이 생길 수도 있기 때문이다.

 

이제 생성자를 설정하자.

 

AAuraProjectile::AAuraProjectile()
{
    PrimaryActorTick.bCanEverTick = false;
    Sphere = CreateDefaultSubobject<USphereComponent>("Sphere");
    SetRootComponent(Sphere);
    Sphere->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
    Sphere->SetCollisionResponseToAllChannels(ECR_Ignore);
    Sphere->SetCollisionResponseToChannel(ECC_WorldDynamic, ECR_Overlap);
    Sphere->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Overlap);
    Sphere->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);

    ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>("ProjectileMovement");
    ProjectileMovement->InitialSpeed = 550.f;
    ProjectileMovement->MaxSpeed = 550.f;
    ProjectileMovement->ProjectileGravityScale = 0.f;
}

 

이러한 기본 설정들을 발사체의 종류에 따라 바뀔수도 있다. 그걸 염두해놓고 있자.

 

 

비긴 플레이에서 오버랩 함수를 바인딩한다.

void AAuraProjectile::BeginPlay()
{
    Super::BeginPlay();

    Sphere->OnComponentBeginOverlap.AddDynamic(this, &AAuraProjectile::OnSphereOverlap);
    
}

 

바인딩 후 할 작업은 이후에 구현하겠다.


2. 발사체 생성 어빌리티 만들기

 

발사체를 정의하는 클래스는 만들었으니 이제 이 발사체를 생성할 수 있는 능력 클래스를 만들자.

 

 

베이스 클래스가 될 커스텀 어빌리티 클래스를 만들어놓았었다. 이를 베이스로 자식 클래스를 정의하자.

 

이 어빌리티에서 필요한 능력은 무엇일까? 그건 당연히도 발사체를 스폰할 수 있는 능력일 것이다.

 

함수 하나를 오버라이드 하자. 아래 함수는 능력이 실행될때 호출된다.

virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override;

매개변수 설명

  1. FGameplayAbilitySpecHandle Handle
    • 이 어빌리티가 AbilitySpec과 연결된 고유 핸들입니다.
    • AbilitySpec은 이 어빌리티의 인스턴스화된 데이터(레벨, 쿨다운 등)를 포함하며, 다수의 어빌리티 인스턴스 간에 구분을 위해 사용됩니다.
  2. const FGameplayAbilityActorInfo* ActorInfo
    • 어빌리티를 소유하고 있는 액터 정보를 나타냅니다.
    • 예: 캐릭터, 컨트롤러, 어빌리티 시스템 컴포넌트 등에 대한 참조를 포함합니다.
    • 주요 멤버:
      • OwnerActor: 어빌리티 소유 액터
      • AvatarActor: 어빌리티가 실제로 작용하는 아바타 액터(예: 캐릭터 메쉬)
      • AbilitySystemComponent: 어빌리티를 실행하는 시스템 컴포넌트
  3. const FGameplayAbilityActivationInfo ActivationInfo
    • 이 어빌리티가 활성화된 메타데이터입니다.
    • 활성화 방법에 대한 정보(예: 키 입력, 이벤트 호출 등)를 포함합니다.
    • 주요 멤버:
      • ActivationMode: 어빌리티 활성화 방식 (예: 직접, 예비 활성화 등)
      • bCanBeCanceled: 어빌리티를 취소할 수 있는지 여부
  4. const FGameplayEventData* TriggerEventData
    • 어빌리티가 활성화될 때 전달된 이벤트 데이터입니다.
    • 주로 이벤트 기반 어빌리티에서 사용되며, 추가적인 정보를 포함합니다.
    • 예: 공격의 방향, 대상, 발사체 데이터 등.

 

위의 함수가 실행되면 발사체를 생성할 것이다. 그러기 위해서 생성할 발사체 정보를 담을 변수도 선언해야한다.

 

UPROPERTY(EditAnywhere, BlueprintReadOnly)
TSubclassOf<AAuraProjectile> ProjectileClass;

 

발사체를 생성하는 것은 오직 서버에서만 진행할 것이고 클라이언트는 서버에서 복제된 발사체를 보는 방식으로 진행할 것이다. 발사체 클래스로 가서 복제가 허용되게 하자.

 

AAuraProjectile::AAuraProjectile()
{
    PrimaryActorTick.bCanEverTick = false;
    bReplicates = true;
    
    // 이하 생략

 

이제 다시 함수를 정의하자.

 

const bool bIsServer = HasAuthority(&ActivationInfo);
if (!bIsServer) return;

FTransform SpawnTransform;
GetWorld()->SpawnActorDeferred<AAuraProjectile>(
    ProjectileClass,
    SpawnTransform,
    GetOwningActorFromActorInfo(),
    Cast<APawn>(GetOwningActorFromActorInfo()),
    ESpawnActorCollisionHandlingMethod::AlwaysSpawn
);

 

서버일때만 스폰을 한다. 이때 스폰을 바로 하는 것이 아니라 세부설정을 하고 스폰을 할 것이기 때문에 SpawnActorDeferred() 함수를 사용하였다.

 

이제 스폰할 위치를 정하는 방법부터 알아보자. 특정 소켓의 위치에서 스폰을 할 것인데 소켓을 얻기 위해 캐릭터 클래스에 직접 캐스팅하는 것은 피할 것이다.

 

인터페이스를 구현하여 이를 구현해보자.

 

컴뱃 인터페이스에서 소켓의 위치를 구하는 함수를 선언하고 각각의 캐릭터 클래스에서 이를 구현하도록 하자.

 

소켓 이름을 저장할 클래스를 만든다.

UPROPERTY(EditAnywhere, Category="Combat")
FName WeaponTipSocketName;

 

이제 소켓의 위치를 함수를 완성시키면 된다.

FVector AAuraCharacterBase::GetCombatSocketLocation()
{
    return Weapon->GetSocketLocation(WeaponTipSocketName);
}

 

 

원래 함수로 가 이 인터페이스를 캐스트하자. 인터페이스 덕분에 쉽게 스폰할 위치를 알 수 있다.

ICombatInterface* CombatInterface = Cast<ICombatInterface>(GetAvatarActorFromActorInfo());
if (CombatInterface)
{
    const FVector SocketLocation = CombatInterface->GetCombatSocketLocation();
    
    FTransform SpawnTransform;
    SpawnTransform.SetLocation(SocketLocation);
    
    GetWorld()->SpawnActorDeferred<AAuraProjectile>(
       ProjectileClass,
       SpawnTransform,
       GetOwningActorFromActorInfo(),
       Cast<APawn>(GetOwningActorFromActorInfo()),
       ESpawnActorCollisionHandlingMethod::AlwaysSpawn
    );
}

 

이제 방향을 알아야한다. 우리가 클릭한 그 장소가 방향이다. 그 장소를 아는 방법은 차후에 정하도록 하겠다. 그 위치를 현재 클래스의 추상성을 유지하면서 알아내는 것이 쉽지는 않다고 한다.

 

이제 스폰을 완성하자.

 

void UAuraProjectileSpell::ActivateAbility(const FGameplayAbilitySpecHandle Handle,
                                           const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo,
                                           const FGameplayEventData* TriggerEventData)
{
    Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);

    const bool bIsServer = HasAuthority(&ActivationInfo);
    if (!bIsServer) return;

    ICombatInterface* CombatInterface = Cast<ICombatInterface>(GetAvatarActorFromActorInfo());
    if (CombatInterface)
    {
       const FVector SocketLocation = CombatInterface->GetCombatSocketLocation();
       
       FTransform SpawnTransform;
       SpawnTransform.SetLocation(SocketLocation);
       
       AAuraProjectile* Projectile = GetWorld()->SpawnActorDeferred<AAuraProjectile>(
          ProjectileClass,
          SpawnTransform,
          GetOwningActorFromActorInfo(),
          Cast<APawn>(GetOwningActorFromActorInfo()),
          ESpawnActorCollisionHandlingMethod::AlwaysSpawn
       );

       Projectile->FinishSpawning(SpawnTransform);
    }
}

 

발사체의 방향을 정하던가 데미지를 주는 등의 작업은 차후에 완성하도록 하겠다.