Unreal/Ureal Engine 5 정리

Chapter 07. Collision

GameStudy 2023. 8. 20. 14:56

7.1 Collision Setting

7.1-1 언리얼의 충돌 설정

  - 언리얼 엔진에서의 충돌체 제작 방법

    따라서 하나의 액터에는 아래 세 가지가 모두 들어갈수도 있음에 유의.

    보통은 루트 컴포넌트로 설정되는 기본 도형 컴포넌트(캡슐, 박스, ...)에 대한 이야기임.

    앞으로 나오는 "충돌체"라는 단어는 충돌을 담당하는 컴포넌트 혹은 그 영역을 의미함.

    스태틱매시 컴포넌트

    스태틱매시 애셋을 그대로 충돌체로 사용하는 방법.

    스태틱매시를 더블클릭하면 열리는 스태틱매시 에디터에서

    해당 충돌체 영역을 설정하고 확인할 수 있음.

    스태틱매시 애셋에 콜리전을 심으면 스태틱매시 컴포넌트에서

    비주얼과 충돌이라는 두 가지 기능을 설정할 수 있어서 관리가 간편해짐.

    기본 도형(Primitive) 컴포넌트

    구체, 박스, 캡슐의 기본 도형을 충돌체로 지정하는 방법.

    스태틱메시와 별도로 충돌체를 제작할 때 사용함.

    스켈레탈 메시를 움직일 때 주로 사용함.

    피직스 애셋

    일반적으로 캐릭터의 이동은 캡슐 컴포넌트를 사용해 처리함.

    하지만 특정 상황에서 캐릭터의 각 관절이 흐느적거리는

    헝겊 인형(RagDoll) 효과 등등을 구현할 때 이 피직스 애셋을 사용함.

      ex. FPS에서 헤드샷/바디샷, 차량 구현시 바퀴와 차체

    각 부위에 기본 도형으로 충돌체를 설정하고

    이를 연결해 캐릭터의 물리를 설정함.

    피직스 애셋은 스켈레탈 메시에만 사용 가능.

 

  - 충돌 설정의 세 가지 구분

    충돌체를 제작했다면, 관련된 설정들이 있음.

    스태틱메시 애셋에는 BlockAll이라는 기본 설정이 있어서 별도 설정 없이

    캐릭터 이동을 방해하는 레벨 콘텐츠를 제작할 수 있음.

    그러나, 플레이어가 레벨과 상호작용하려면 아래와 같은 세부적인 설정이 필요함.

    Collision Enabled(충돌체 반응)

    Object Type(충돌체 분류)

    Collision Reponses(다른 충돌체와의 반응)

    이 세 가지를 콜리전 프리셋이라고 부름. 콜리전 프리셋 == 콜리전 프로파일임을 참고.

 

  - 콜리전 프리셋 확인 방법

    Content Browser > BP_SPlayerCharacter를 Viewport에 드래그 드랍.

    Outliner > BP_SPlayerCharacter 클릭

    Details > CapsuleComponent 클릭 > Collision > Collision Presets

 

  - Collision Enabled

    액터마다 자신에게 적절한 충돌체 반응을 지정하는 것이

    성능 상의 부하를 줄이는 방법임.

    Query

    두 물체의 충돌체가 서로 겹칠 때 신호를 보냄으로써 반응함.

    충돌체의 겹침을 감지하는 것은 언리얼 엔진에서 오버랩(Overlap)이라 부름.

    충돌체가 겹치면 관련 컴포넌트에 BeginOverlap 이벤트가 발생함.

    지정한 선 상에 물체가 충돌하는지 탐지하는 레이캐스트(Raycast)나

    스윕(Sweep) 기능도 Query에 속함.

    Physics

    두 물체의 충돌체가 서로 겹칠 때 물리적으로 반응함.

    물리적인 시뮬레이션을 사용할 때 설정함. ex. Impulse

    중력 혹은 충돌에 대한 반작용은 Physics에 속함.

    Query and Physics

    위의 두 반응을 모두 나타냄.

    이 설정을 사용하면 모든 기능이 잘 동작하겠지만, 물리 엔진이 수행할 계산량 증가.

    

  - BP_PlayerCharacter의 Collision Enable

    Collision Enable 부분을 보면 Query and Physics가 설정되어 있음.

    그리고 Generate Overlap Events에 체크 되어 있다는 것은

    다른 충돌체와 겹침이 발생하면 관련 이벤트가 호출된다는 뜻.

 

  - Object Type의 대분류

    여기서 나오는 채널은 곧 분류(Category)를 의미함.

    오브젝트 채널

    정지한 충돌체에 대한 콜리전 채널.

    WorldStatic / WorldDynamic / Pawn / PhysicsBody / ...

    트레이스 채널

    이동하는 충돌체에 대한 콜리전 채널.

    충돌 관련 행동의 물리적 판정을 위한 콜리전 채널. 대표적인 예는 공격.

    Visibility / Camera

 

  - Object Type의 소분류

    아래 충돌 채널 중 하나를 충돌체(컴포넌트)에 부여함.

    WorldStatic

    움직이지 않는 정적인 배경 액터에 사용하는 콜리전 채널.

    주로 스태틱메시 액터에 있는 스태틱메시 컴포넌트에 부여됨.

    WorldDynamic

    움직이는 액터에 사용하는 콜리전 채널.

    블루프린트에서 만든 스태틱메시 컴포넌트에 부여됨.

    Pawn

    플레이어가 조종하는 물체에 주로 사용.

    캐릭터의 충돌을 담당하는 캡슐 컴포넌트에 부여됨.

    PhysicsBody

    물리 시뮬레이션으로 움직이는 컴포넌트에 부여됨.

    Visibility

    배경 물체가 시각적으로 보이는지 탐지하는 데 사용. 탐지에서 폰은 제외됨.

    마우스로 물체를 선택하는 피킹(Picking) 기능을 구현할 때 사용됨.

    Camera

    카메라 설정을 위해 카메라와 목표물 간에 장애물이 있는지 탐지하는데 사용됨.

    이전 백뷰 방식으로 캐릭터를 조작할 때 장애물이 시야를 가리면

    카메라를 장애물 앞으로 줌인하는 기능을 Camera 콜리전 채널으로 구현함.

 

  - Object Type 확인 중에 주의할 점

    Collision Presets의 Pawn과 Object Type의 Pawn은 서로 다른 설정 값임.

    Object Type을 봐야 정확한 채널을 확인 가능.

    Collision Presets은 충돌체 반응/충돌체 분류/다른 충돌체와의 반응 모두를 뜻함.

      ex. Collision Presets에 BlockAll을 선택하면 Object Type에는 WorldStatic이 설정됨.

 

  - Collision Responses

    해당 컴포넌트에 설정된 콜리전 채널이 상대방 컴포넌트의 콜리전 채널과 

    어떻게 반응할지 지정하는 작업. 세 가지로 지정 가능.

    무시(Ignore)

    충돌이 있어도 아무 반응이 일어나지 않음. 뚫고 지나감.

    겹침(Overlap)

    무시와 동일하게 물체가 뚫고 지나갈 수 있지만, 이벤트는 발생.

    블록(Block)

    충돌체가 뚫고 지나가지 못하도록 막음.

 

  - BP_PlayerCharacter의 Collision Responses

    거의 모든 채널과의 반응이 Block임을 알 수 있음.

 

  - Collision Responses에 대한 고찰

    언리얼 엔진의 물리는 무시 반응을 최대화하고, 블록 반응을 최소화하도록 동작함.

    A 컴포넌트의 콜리전 채널은 B 컴포넌트의 콜리전 채널과의 충돌에 블록 반응,

    B 컴포넌트의 콜리전 채널은 A 컴포넌트의 콜리전 채널과의 충돌에 겹침 반응이라면

    결국 B 컴포넌트에 블록 반응은 발생하지 않음.

    두 컴포넌트 간에 물리적 반응이 일어날 때는 각 컴포넌트에 특별한 이벤트가 발생.

    겹침 반응에는 BeginOverlap 이벤트, 블록 반응에는 Hit 이벤트가 발생함.

    또, 블록 반응을 설정하면 Hit 이벤트가 발생하지만, BeginOverlap 이벤트도 발생 가능.

    단, 해당 컴포넌트 콜리전 프리셋에 Generates Overlap Events 항목이 체크되어 있어야함.

 

  - 새로운 콜리전 채널 추가

    Toolbar > Project Settings > Collision 클릭

    Object Channels > Add Object Channel > "SCharacter"

    Default Response는 Block으로 지정.

    콜리전 채널이 컴포넌트의 Object Type에 해당.

 

  - SCharacterPreset의 다른 Collision Preset과의 반응 설정

    Preset > New 클릭 > 아래와 같이 설정

    Name에 들어가는 SCharacterPreset이 Collision Presets 옆에 적히는 이름.

    콜리전 프로파일 이름이 되기도 함. ex. SetCollisionProfileName()

 

  - 다른 Collision Preset의 SCharacterPreset과의 반응 설정

    새롭게 만든 콜리전 채널을 가지고 다른 콜리전 채널과의 반응을 설정해주고자 함.

    Default Response이 Block이라, Block 반응을 하면 안되는 콜리전 채널들이 있다보니

    이걸 직접 조작해줘야함.

    OverlapAll: Overlap

    OverlapAllDynamic: Overlap

    IgnoreOnlyPawn: Ignore

    OverlapOnlyPawn: Overlap

    Spectator: Ignore

    CharacterMesh: Ignore

    Trigger: Overlap

    RagDoll: Ignore

    UI: Overlap

 

  - 콜리전 프리셋 설정 실습

<hide/>

// SPlayerCharacter.h


...

ASPlayerCharacter::ASPlayerCharacter()
    ...
{
    ...

    GetCapsuleComponent()->InitCapsuleSize(CharacterRadius, CharacterHalfHeight);
    GetCapsuleComponent()->SetCollisionProfileName(TEXT("SCharacterPreset"));

    ...
}

...

 

  - 라이브 코딩의 문제점

    라이브 코딩 방식으로 컴파일을 진행하면 이미 레벨에 배치된 액터의 속성 값은 

    이전 코드의 값을 유지하게 됨. 그래서 캡슐 컴포넌트의 콜리전 프리셋 값이 Pawn일 수 있음.

    이런 경우 노란색 버튼 눌러서 기본값으로 변경하거나, 에디터를 재시작해야 함.

 

  - Collision Channel의 Enum 값 매핑

    프로젝트 폴더 > Config > DefaultEngine.ini 파일을 메모장으로 열기

    "SCharacter" 검색하면 아래와 같은 문구를 볼 수 있음.

    즉, SCharacter는 ECC_GameTraceChannel1에 매핑되어 있다는 뜻.

    C++ 코드에서 이 값을 이용해 특정 채널간의 콜리전 반응을 수정할 수 있음.

    언리얼 엔진은 게임에서 활용할 수 있도록 총 32개의 콜리전 채널을 제공함.

    32개 중 8개는 언리얼 엔진이 기본으로 사용하고, 6개는 다른 용도로 예약 되어 있음.

    우리 프로젝트에서는 이를 뺀 나머지 18개만 사용할 수 있음.

    게임 프로젝트에서 새로 생성하는 오브젝트 채널과 트레이스 채널은 

    ECC_GameTraceChannel 1번부터 18번까지 중에서 하나를 배정 받음.

    ECollisionChannel 열거형은 EngineTypes.h 헤더파일에서 볼 수 있음.

+DefaultChannelResponses=(Channel=ECC_GameTraceChannel1,DefaultResponse=ECR_Block,bTraceType=False,bStaticObject=False,Name="SCharacter")

 

7.1-2 충돌 감지

  - 충돌 감지 실습을 위한 준비

    새 C++ 클래스 > AnimNotify 부모 클래스 > "AnimNotify_CheckHit"

    아래와 같이 작성 후 컴파일.

    AM_Attack_Knife > 노티파이 섹션의 2번 트랙 노티파이들을 모두 교체.

<hide/>

// SPlayerCharacter.h

...
class STUDYPROJECT_API ASPlayerCharacter : public ACharacter
{
    GENERATED_BODY()

    friend class UAnimNotify_CheckHit;

public:
    ...
};
<hide/>

// AnimNotify_CheckHit.h

#pragma once

#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotify.h"
#include "AnimNotify_CheckHit.generated.h"

/**
 * 
 */
UCLASS()
class STUDYPROJECT_API UAnimNotify_CheckHit : public UAnimNotify
{
    GENERATED_BODY()

private:
    virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) override;
    
};
<hide/>

// AnimNotify_CheckHit.cpp


#include "Animations/AnimNotify_CheckHit.h"
#include "Characters/SPlayerCharacter.h"

void UAnimNotify_CheckHit::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
{
    Super::Notify(MeshComp, Animation, EventReference);

    if (nullptr != MeshComp)
    {
        if (ASPlayerCharacter* AttackingCharacter = Cast<ASPlayerCharacter>(MeshComp->GetOwner()))
        {
            AttackingCharacter->CheckHit();
        }
    }
}

 

  - 월드 콜리전 트레이싱 함수

    세 가지 카테고리로 원하는 함수 이름을 얻을 수 있음.

    처리방법

    LineTrace-/BoxTrace-/SphereTrace-/CapsuleTrace-/...

    Sweep-

    Overlap-

    대상

    Test: 무언가 감지되었는지를 테스트

    Single 또는 AnyTest: 감지된 단일 물체 정보를 반환

    Multi: 감지된 모든 물체 정보를 배열로 반환

    처리 설정

    ByChannel: 채널 정보를 사용해 감지

    ByObjectType: 물체에 지정된 물리 타입 정보를 사용해 감지

    ByProfile: 프로파일 정보를 사용해 감지

{처리방법}{대상}{처리설정}

 

월드가 제공하는 세 가지의 충돌 판정 서비스

 

  - 캐릭터 공격의 충돌 감지

    캐릭터의 위치에서 시선 방향으로 물체가 있는지 감지

    작은 구체를 제작하고 시선 방향으로 특정 거리까지 쓸기.

    하나의 물체만 감지

    트레이스 채널을 사용해 감지

{Sweep}{Single}{ByChannel}

SweepSingleByChannel()

 

  - SweepSingleByChannel() 함수

    트레이스 채널을 사용해 물리적 충돌 여부를 가리는 함수 중 하나.

    물리는 월드의 기능이므로 GetWorld() 함수를 사용해서 명령을 내려야 함.

    위 함수는 기본 도형을 인자로 받은 후 시작 지점에서 끝 지점까지 쓸면서 

    해당 영역 내에 물리 판정이 일어났는지를 조사함.

    HitResult

    물리적 충돌이 탐지된 경우 관련된 정보를 담을 구조체

    Start

    탐색을 시작할 위치

    End

    탐색을 끝낼 위치

    Rot

    탐색에 사용할 도형의 회전

    TraceChannel

    물리 충돌 감지에 사용할 트레이스 채널 정보

    CollisionShape

    탐색에 사용할 기본 도형 정보. 구체, 캡슐, 박스를 사용함.

    Params

    탐색 방법에 대한 설정 값을 모아둔 구조체

    ResponseParams

    탐색 반응을 설정하기 위한 구조체

 

  - 트레이스 채널 추가

    Toolbar > Settings > Project Settings > Collision > Trace Channels

    New Trace Channel > "Attack", Default Response는 Ignore.

    Preset 섹션 > SCharacterPreset 더블클릭 > Attack은 Block으로 설정.

    

  - Attack 트레이스 채널과 SCharacterPreset

    Attack 트레이스 채널은 SCharacterPreset을 사용하는 컴포넌트에 Ignore 반응.

    그러나 SCharacterPreset을 사용하는 컴포넌트는 Attack 트레이스 채널과 Block 반응함.

    따라서 충돌 계산에 대한 부하가 줄어들 수 있음.

 

  - Attack 채널의 Enum 값 가져오는 방법

    어떤 값을 배정받았는지는 Config 폴더 > DefaultEngine.ini에서 확인 가능.

    .ini 파일 하단을 자세히 살펴보면 우리가 생성한 Attack 트레이스 채널은

    ECC_GameTraceChannel2 열거형을 배정 받았음.

 

  - 공격 성공 판정 구현

    액터의 충돌이 감지된 경우, 충돌된 액터에 관련된 정보를 얻기 위해 구조체를 넘김.

    FHitResult 구조체로 지역 변수를 하나 생성하고, 이를 첫 번째 인자에 넣어줌.

    도형의 탐색 영역을 지정함. 탐색을 시작할 위치는 액터가 있는 곳.

    끝낼 위치는 액터 시선 방향으로 200cm 떨어진 곳.

    알아낸 트레이스 채널의 열거형값 전달.

    FCollisionShape::MakeSphere() 함수를 사용해서 탐지에 사용할 도형을 제작함.

    탐색할 도형으로는 50cm 반지름의 구체. 회전값은 기본값.

    탐색 방법에 대한 구조체를 설정. 공격 명령을 내리는 자신은

    이 탐색에 감지되지 않도록 this를 무시할 액터목록에 추가.

    탐색 반응에 대한 구조체가 마지막에 있지만, 기본 인자로 전달.

    다만, OnAttackHitCheck는 UGWGhostAnimInstance 클래스에서 멤버로 선언함.

    멤버로 선언된 델리게이트에 콜백함수를 추가할 때는 AddUObject()로 진행함.

<hide/>

// SPlayerCharacter.h


...

void ASPlayerCharacter::CheckHit()
{
    FHitResult HitResult;
    FCollisionQueryParams Params(NAME_None, false, this);

    bool bResult = GetWorld()->SweepSingleByChannel(
        HitResult,
        GetActorLocation(),
        GetActorLocation() + 200,
        FQuat::Identity,
        ECollisionChannel::ECC_EngineTraceChannel2,
        FCollisionShape::MakeSphere(50.f),
        Params
    );

    if (true == bResult)
    {
        if (true == ::IsValid(HitResult.GetActor()))
        {
            UE_LOG(LogTemp, Error, TEXT("Hit Actor Name: %s"), *HitResult.GetActor()->GetName());
        }
    }
}

...

 

  - 언리얼의 가비지 컬렉션과 FHitResult의 Actor 멤버

    FHitResult 멤버 변수 Actor의 선언이 로우 포인터라면

    해당 함수에서의 참조로 인해 제거 되어야 할 액터가

    메모리에 그대로 남아있는 문제가 발생할 수 있음.

    이런 문제를 방지하기 위해 FHitResult는 참조로부터 자유롭게 포인터 정보를

    전달해주는 TWeakObjectPtr 방식으로 멤버 변수를 선언했음.

    TWeakObjectPtr로 지정된 액터에 접근하려면 IsValid() 함수를 사용해서

    사용하려는 액터가 유효한지 먼저 점검하고 사용해야 함.

    위 예제에서 사용한 GetActor() 함수 내부에 IsActorValid() 함수가 호출됨.

 

7.1-3 DrawDebugCapsule()

  - 디버그 드로잉(Debug Drawing)

    공격할 때마다 로그 창을 열고 탐지하는 것은 번거로운 작업.

    공격 범위가 시각적으로 보이지 않아서 어떻게 맞았는지 

    어떻게 미스났는지 파악할 수 없는 문제도 있음.

    언리얼 엔진에서 제공하는 디버그 드로잉 기능으로 이를 해결 가능.

 

  - DrawDebugCapsule()

    #include "DrawDebugHelpers.h" 구문 추가.

    DrawDebugHelpers에는 다양한 그리기 함수가 있음.

    캡슐 모양을 그리는 기능인 DrawDebugCapsule() 함수 활용하고자 함.

    캡슐의 반지름을 50으로 설정하고 탐색 시작 위치에서 탐색 끝 위치로 

    향하는 벡터를 구한 후, 벡터의 중점 위치와 벡터 길이의 절반을 대입하면

    우리가 원하는 크기의 캡슐 모양을 구할 수 있음.

    캡슐은 상하로 서있는 모습을 가지므로, 회전 행렬을 적용해 캡슐 방향을

    캐릭터 시선 방향으로 눕힌 후 공격 범위에 맞게 길이를 설정함.

<hide/>

// SPlayerCharacter.h

...
class STUDYPROJECT_API ASPlayerCharacter : public ACharacter
{
    ...

private:
    ...

    float AttackRange = 200.f;

    float AttackRadius = 50.f;

};
<hide/>

// SPlayerCharacter.h


...

void ASPlayerCharacter::CheckHit()
{
    ...

    bool bResult = GetWorld()->SweepSingleByChannel(
        HitResult,
        GetActorLocation(),
        GetActorLocation() + AttackRange,
        FQuat::Identity,
        ECollisionChannel::ECC_EngineTraceChannel2,
        FCollisionShape::MakeSphere(AttackRadius),
        Params
    );

    if (true == bResult)
    {
        if (true == ::IsValid(HitResult.GetActor()))
        {
            UE_LOG(LogTemp, Error, TEXT("Hit Actor Name: %s"), *HitResult.GetActor()->GetName());
        }
    }

#pragma region CollisionDebugDrawing
    FVector TraceVec = GetActorForwardVector() * AttackRange;
    FVector Center = GetActorLocation() + TraceVec * 0.5f;
    float HalfHeight = AttackRange * 0.5f + AttackRadius;
    FQuat CapsuleRot = FRotationMatrix::MakeFromZ(TraceVec).ToQuat();
    FColor DrawColor = true == bResult ? FColor::Green : FColor::Red;
    float DebugLifeTime = 5.f;

    DrawDebugCapsule(
        GetWorld(),
        Center,
        HalfHeight,
        AttackRadius,
        CapsuleRot,
        DrawColor,
        false,
        DebugLifeTime
    );
#pragma endregion

}

...

 

7.2 Unreal Damage Framework

7.2-1 TakeDamage()

  - TakeDamage() 함수

    감지된 액터에 데미지를 전달 하기 위해 데미지 프레임워크가 제공됨.

    AActor 클래스는TakeDamage()라는 함수가 구현 되어 있음. 

    TakeDamage() 함수는 총 네 개의 인자를 가지고 있음.

    DamageAmount

    전달할 데미지의 세기

    DamageEvent

    데미지의 종류

    EventInstigator

    공격 명령을 내린 가해자

    DamageCauser

    데미지 전달을 위해 사용한 도구

 

  - 데미지 전달 구현

    데미지를 전달하는 행위에는 항상 가해자와 피해자가 있음.

    여기서 데미지를 가한 진정한 가해자는 폰이 아니라

    폰에게 명령을 내린 플레이어 컨트롤러라고 할 수 있음.

    따라서 EventInstingator에는 폰이 아닌 컨트롤러의 정보를 보내줘야 함.

    또한 폰은 플레이어가 데미지 전달을 위해 사용하는 도구라고도

    해석할 수 있기 때문에 마지막 파라미터에 지정.

    대상 액터에 데미지를 전달했다면, 피해를 입은 액터에 관련 로직을 구성해야함.

    AActor::TakeDamage() 함수를 오버라이드 해서 액터가 받은 데미지를 처리함.

    마찬가지로 부모 클래스의 로직을 먼저 실행해줘야 함.

<hide/>

// SPlayerCharacter.h

...
class STUDYPROJECT_API ASPlayerCharacter : public ACharacter
{
    ...

public:
    ...

    virtual float TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;

protected:
    ...

};
<hide/>

// SPlayerCharacter.cpp

...
#include "Engine/EngineTypes.h"
#include "Engine/DamageEvents.h"

...

void ASPlayerCharacter::CheckHit()
{
    ...

    if (true == bResult)
    {
        if (nullptr != HitResult.GetActor())
        {
            UE_LOG(LogTemp, Error, TEXT("Hit Actor Name: %s"), *HitResult.GetActor()->GetName());

            FDamageEvent DamageEvent;
            HitResult.GetActor()->TakeDamage(50.f, DamageEvent, GetController(), this);
        }
    }
}

...

float ASPlayerCharacter::TakeDamage(float Damage, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
    float FinalDamageAmount = Super::TakeDamage(Damage, DamageEvent, EventInstigator, DamageCauser);

    UE_LOG(LogTemp, Log, TEXT("%s took damage: %.3f"), *GetName(), FinalDamageAmount);

    return FinalDamageAmount;
}

 

  - Can be Damaged 속성

    모든 액터에는 Can be Damaged 속성이 있음.

    false로 설정하면 캐릭터에 전달된 데미지의 결과가 모두 0이 되는 무적상태.

 

  - 사망 애니메이션 구현

    ABAnimInstance::IsDead 멤버 추가 후 컴파일

    애니메이션 블루프린트 > 애님 그래프 > 사망 애니메이션 노드 추가

    반복 되지 않게끔 Loop Animation 옵션 체크 해제

    캐릭터에 관련 로직 추가.

    죽은 후에는 SetActorCollisionEnabled() 함수를 사용해서

    액터의 충돌 설정 비활성. 앞으로 캐릭터에는 충돌 이벤트가 발생하지 않음.

<hide/>

// SPlayerCharacter.h

...
class STUDYPROJECT_API ASPlayerCharacter : public ACharacter
{
    ...

public:
    ...

    float GetCurrentHP() const { return CurrentHP; }

protected:
    ...

private:
    ...

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="ASPlayerCharacter", meta=(AllowPrivateAccess=true))
    float CurrentHP = 100.f;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="ASPlayerCharacter", meta=(AllowPrivateAccess=true))
    float MaxHP = 100.f;

};
<hide/>

// SPlayerCharacter.cpp

...

float ASPlayerCharacter::TakeDamage(float Damage, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
    float FinalDamageAmount = Super::TakeDamage(Damage, DamageEvent, EventInstigator, DamageCauser);

    CurrentHP = FMath::Clamp(CurrentHP - FinalDamageAmount, 0.f, MaxHP);

    return FinalDamageAmount;
}
<hide/>

// SAnimInstance.h

...
class STUDYPROJECT_API USAnimInstance : public UAnimInstance
{
    ...

public:
    ...

private:
    ...

    void PlayDeadMontage();

private:
    ...

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="USAnimInstance", meta=(AllowPrivateAccess=true))
    class UAnimMontage* DeadMontage;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="USAnimInstance", meta=(AllowPrivateAccess=true))
    uint8 bIsDead : 1;

};
<hide/>

// SAnimInstance.cpp

...
#include "SPlayerCharacter.h"

...

void USAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
    Super::NativeUpdateAnimation(DeltaSeconds);

    if (MovementComponent && OwnerCharacter && false == bIsDead)
    {
        Velocity = MovementComponent->Velocity;
        GroundSpeed = Velocity.Size2D();
        bIsIdle = GroundSpeed < MovingThreshold;
        bIsFalling = MovementComponent->IsFalling();
        bIsJumping = bIsFalling & (JumpingThreshold < Velocity.Z);
        
        if (OwnerCharacter->GetCurrentHP() < KINDA_SMALL_NUMBER)
        {
            bIsDead = true;
        }
    }
}

...

 

7.2-2 Point Damage 

 

7.3 충돌의 활용

7.3-1 상자 구현

  - OnComponentOverlap 델리게이트

    델리게이트에 바인드할 함수를 선언할 때, 정확한 함수 시그니쳐를 어떻게 알 수 있을까.

    해당 델리게이트를 Ctrl + 클릭하면 선언쪽으로 이동할 수있음.

    다시 자료형(FOnComponentOverlap)을 Ctrl + 클릭하면 델리게이트 정의구문으로 이동가능.

    해당 정의 구문을 복사해서 바인드할 함수를 선언하면 편리함.

    DYNAMIC 키워드가 붙어 있음에 주의.

// PrimitiveComponent.h

UPROPERTY(BlueprintAssignable, Category="Collision")
FComponentBeginOverlapSignature OnComponentBeginOverlap;
// PrimitiveComponent.h

DECLARE_DYNAMIC_MULTICAST_DELEGATE_SixParams(
    FComponentBeginOverlapSignature,
    UPrimitiveComponent* OverlappedComponent,
    AActor*              OtherActor,
    UPrimitiveComponent* OtherComp,
    int32                OtherBodyIndex,
    bool                 bFromSweep,
    const FHitResult&    SweepResult
);

 

  - 아이템 박스 클래스 생성

    새 C++ 클래스 > Actor 부모 클래스 > "SItemBox"

    Path > WorldStatics

    아래와 같이 작성 후 컴파일.

    새 블루프린트 애셋 > SItemBox 부모 클래스 > "BP_ItemBox" 생성

    스켈레탈 메시와 파티클 템플릿을 지정 후 Viewport에 드래그 드랍.

<hide/>

// SItemBox.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SItemBox.generated.h"

UCLASS()
class STUDYPROJECT_API ASItemBox : public AActor
{
    GENERATED_BODY()
    
public:	
    ASItemBox();

private:
    UPROPERTY(VisibleAnywhere, Category = ASItemBox)
    TObjectPtr<class UBoxComponent> BoxComponent;

    UPROPERTY(EditAnywhere, Category = ASItemBox)
    TObjectPtr<class UStaticMeshComponent> StaticMeshComponent;

    UPROPERTY(EditAnywhere, Category = ASItemBox)
    TObjectPtr<class UParticleSystemComponent> ParticleSystemComponent;

    UFUNCTION()
    void OnOverlapBegun(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult);

    UFUNCTION()
    void HandleEffectFinished(class UParticleSystemComponent* ParticleSystem);

};
<hide/>

// SItemBox.cpp


#include "WorldStatics/SItemBox.h"
#include "Components/BoxComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Particles/ParticleSystemComponent.h"

ASItemBox::ASItemBox()
{
    PrimaryActorTick.bCanEverTick = false;

    BoxComponent = CreateDefaultSubobject<UBoxComponent>(TEXT("BoxComponent"));
    SetRootComponent(BoxComponent);
    BoxComponent->SetCollisionProfileName(FName("Trigger"));
    BoxComponent->SetBoxExtent(FVector(40.0f, 42.0f, 30.0f));
    BoxComponent->OnComponentBeginOverlap.AddDynamic(this, &ASItemBox::OnOverlapBegun);

    StaticMeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMeshComponent"));
    StaticMeshComponent->SetupAttachment(GetRootComponent());
    StaticMeshComponent->SetRelativeLocation(FVector(0.0f, -3.5f, -30.0f));
    StaticMeshComponent->SetCollisionProfileName(TEXT("NoCollision"));
    /*
    static ConstructorHelpers::FObjectFinder<UStaticMesh> BoxMesh(TEXT("오브젝트 패스"));
    if (BoxMesh.Object)
    {
        StaticMeshComponent->SetStaticMesh(BoxMesh.Object);
    }
    */

    ParticleSystemComponent = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("ParticleSystemComponent"));
    ParticleSystemComponent->SetupAttachment(GetRootComponent());
    ParticleSystemComponent->SetAutoActivate(false);
    /*
    static ConstructorHelpers::FObjectFinder<UParticleSystem> EffectTemplate(TEXT("오브젝트 패스"));
    if (EffectTemplate.Object)
    {
        ParticleSystemComponent->SetTemplate(EffectTemplate.Object);
        ParticleSystemComponent->bAutoActivate = false;
    }
    */

}

void ASItemBox::OnOverlapBegun(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
    ParticleSystemComponent->Activate(true);
    StaticMeshComponent->SetHiddenInGame(true);
    SetActorEnableCollision(false);
    ParticleSystemComponent->OnSystemFinished.AddDynamic(this, &ASItemBox::HandleEffectFinished);
}

void ASItemBox::HandleEffectFinished(UParticleSystemComponent* ParticleSystem)
{
    Destroy();
}

 

  - 아이템 박스 전용 콜리전 프리셋 생성

    Project Settings > Collision > New Object Channel > "SItemBox"

    Default Response로 Ignore 지정.

    Preset > New > "SItemBoxPreset" 생성

    CollisionEnabled에는 Query Only.

    Object Type에는 SItemBox

    Collision Responses에서 SCharacter만 Overlap

    SCharacterPreset에서도 SItemBox를 Overlap 처리.

<hide/>

// SItemBox.cpp


...

ASItemBox::ASItemBox()
{
    ...
    BoxComponent->OnComponentBeginOverlap.AddDynamic(this, &ASItemBox::OnOverlapBegun);
    BoxComponent->SetCollisionProfileName(FName(TEXT("SItemBoxPreset")));

    ...

}

...

 

7.3-2 무기 구현

  - 무기 애셋 구해오기

    Epic Games Launcher > 마켓플레이스 > Military Weapons Silver 검색

    무료 클릭 > 프로젝트 추가 > 모든 프로젝트 표시 > StudyProject 선택 후 버전 선택에 제일 최신 버전 선택.

 

  - 무기 클래스 생성

    새 C++ 클래스 > Actor 부모 클래스 > "SWeapon" 생성

    Path > Items

    아래와 같이 작성 후 컴파일.

    Content Browser > StudyProject 우클릭 > 새 폴더 "Items"

    Items > 새 블루프린트 애셋 > SWeapon 부모 클래스 > "BP_Knife"

<hide/>

// SWeapon.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SWeapon.generated.h"

UCLASS()
class STUDYPROJECT_API ASWeapon : public AActor
{
    GENERATED_BODY()
    
public:	
    ASWeapon();

private:
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = ASWeapon, meta = (AllowPrivateAccess))
    TObjectPtr<class USkeletalMeshComponent> SkeletalMeshComponent;

};
<hide/>

// SWeapon.cpp


#include "Items/SWeapon.h"

ASWeapon::ASWeapon()
{
    PrimaryActorTick.bCanEverTick = false;

    SkeletalMeshComponent = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("SkeletalMeshComponent"));
    SetRootComponent(SkeletalMeshComponent);

    static ConstructorHelpers::FObjectFinder<USkeletalMesh> WeaponMeshAsset(TEXT("/Script/Engine.SkeletalMesh'/Game/MilitaryWeapSilver/Weapons/Knife_A.Knife_A'"));
    if (true == WeaponMeshAsset.Succeeded())
    {
        SkeletalMeshComponent->SetSkeletalMesh(WeaponMeshAsset.Object);
    }
}

 

  - Socket

    무기 액터를 장착할 때, 무기 액터의 트랜스폼을 실시간으로 변경해서 캐릭터에 부착하는게 아님.

    스켈레탈 메시에 소켓이란 것을 생성해서 해당 소켓에 무기를 부착하는 방식.

    그럼 자동으로 애니메이션에 따라 움직임.

    Content Browser > Mixamo > Meshes > Steve_Skeleton 더블클릭 > Skeleton Tree

    RightHand 검색 후 우클릭 > Add Socket > "WeaponSocket"

    WeaponSocket 우클릭 > Add Preview Asset > Knife_A 지정

    이 기능은 미리보기 기능이라, 실제 캐릭터에도 부착되는건 아님.

 

  - WeaponSocket의 트랜스폼 조정

    WER을 통해서 위치를 조정하면 됨. 

    무기를 부착한 캐릭터가 자연스럽게 애니메이션을 재생하도록 조정도 가능

    Toolbar > Preview Animation 클릭 해서 선택 가능 혹은

    Preview Scene Setting > Animation > Preview Controller를

    "Use Specific Animation" 지정.

    Animation에 원하는 애니메이션을 지정하고 무기 트랜스폼값을 지정.

 

  - SpawnActor() 함수

    월드에 새롭게 액터를 생성하는 명령은 SpawnActor() 함수임.

    액터는 월드에 존재하는 물체이므로, 이는 월드의 명령어임. GetWorld() 함수로

    월드의 포인터를 가져와서 해당 함수를 실행한다. SpawnActor()의 인자에는

    생성할 액터의 클래스와 액터가 앞으로 생성할 위치 및 회전을 지정.

<hide/>

// SPlayerCharacter.h

...
class STUDYPROJECT_API ASPlayerCharacter : public ACharacter
{
    ...

public:
    ...

private:
    ...

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = ASPlayerCharacter, meta = (AllowPrivateAccess))
    TObjectPtr<class ASWeapon> Weapon;

};
<hide/>

// SPlayerCharacter.h


...
#include "Items/SWeapon.h"

...

void ASPlayerCharacter::PostInitializeComponents()
{
    ...

    FName WeaponSocket(TEXT("WeaponSocket"));
    if (true == GetMesh()->DoesSocketExist(WeaponSocket))
    {
        Weapon = GetWorld()->SpawnActor<ASWeapon>(FVector::ZeroVector, FRotator::ZeroRotator);
        if (true == ::IsValid(Weapon))
        {
            Weapon->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetIncludingScale, WeaponSocket);
        }
    }
}

...

 

7.3-3 무기 습득

  - UClass 포인터와 TSubclassof 키워드

    배치한 아이템 상자에 클래스 정보를 저장할 멤버 선언.

    이 값을 기반으로 플레이어가 아이템 상자의 영역에 들어왔을 때 

    아이템을 생성하도록 기능을 구현

    클래스 정보를 저장하는 변수를 선언할 때 UClass 포인터를 사용할 수 있지만,

    이를 사용하면 현재 프로젝트에 사용하는 모든 언리얼 오브젝트의 선언이 보이게 됨.

    특정 클래스와 상속 받은 클래스들로 목록을 한정하는 TSubclassof 키워드를 제공함.

    이를 사용하면 목록에서 아이템 상자와 이를 선언한 클래스 목록만 보임.

    생성자에서 해당 멤버에 대한 기본 클래스 값을 지정해줌.

    컴파일 후 Outliner > ASItemBox 클릭 > Details > ASItemBox 섹션을 살펴보자.

<hide/>

// SItemBox.h

...
class STUDYPROJECT_API ASItemBox : public AActor
{
    ...

private:
    ...

    UPROPERTY(EditInstanceOnly, BlueprintReadOnly, Category = ASItemBox, meta = (AllowPrivateAccess))
    TSubclassOf<class ASWeapon> WeaponClass;

};
<hide/>

// SItemBox.cpp


...
#include "Items/SWeapon.h"

ASItemBox::ASItemBox()
{
    ...

    WeaponClass = ASWeapon::StaticClass();

}

...

 

  - 캐릭터에 무기 장착 시키기

    캐릭터에 무기를 장착시키는 SetWeapon()이라는 멤버 함수를 선언.

    여기에는 현재 캐릭터에 무기가 없으면 소켓에 무기를 장착시키고

    무기 액터의 소유자를 캐릭터로 변경하는 로직을 넣음.

    기존에 PoseInitializeComponent()에서 시작할 때 무기 액터를 장착시킨 로직은 삭제함.

    이제 배치한 상자에 Overlap 이벤트가 발생할 때 아이템 상자에 설정된

    클래스 정보로부터 무기를 생성하고 이를 캐릭터에게 장착시키는 기능 구현

<hide/>

// SPlayerCharacter.h

...
class STUDYPROJECT_API ASPlayerCharacter : public ACharacter
{
    ...

public:
    ...

    bool CanEquipWeapon();

    void EquipWeapon(class ASWeapon* InWeapon);

protected:
    ...

};
<hide/>

// SPlayerCharacter.h


...

...

bool ASPlayerCharacter::CanEquipWeapon()
{
    return (nullptr == Weapon);
}

void ASPlayerCharacter::EquipWeapon(ASWeapon* InWeapon)
{
    FName WeaponSocket(TEXT("WeaponSocket"));
    if (true == ::IsValid(InWeapon) && true == GetMesh()->DoesSocketExist(WeaponSocket))
    {
        if (false == CanEquipWeapon())
        {
            Weapon->DetachFromActor(FDetachmentTransformRules::KeepRelativeTransform);
            Weapon->SetActorHiddenInGame(true);
        }

        InWeapon->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetIncludingScale, WeaponSocket);
        InWeapon->SetOwner(this);
        Weapon = InWeapon;
    }
}

...
<hide/>

// SItemBox.cpp


...
#include "Characters/SPlayerCharacter.h"

...

void ASItemBox::OnOverlapBegun(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
    ...

    ASPlayerCharacter* OtherPlayerCharacter = Cast<ASPlayerCharacter>(OtherActor);
    if (true == ::IsValid(OtherPlayerCharacter) && nullptr != WeaponClass)
    {
        ASWeapon* NewWeapon = GetWorld()->SpawnActor<ASWeapon>(WeaponClass, FVector::ZeroVector, FRotator::ZeroRotator);
        if (true == ::IsValid(NewWeapon))
        {
            OtherPlayerCharacter->EquipWeapon(NewWeapon);
        }
    }
}

...

 

  - 액터의 컴포넌트에서 시각적인 기능 켜고 끄는 방법

    SetVisibility()

    해당 컴포넌트의 시작적인 기능을 아예 없애는 함수.

    에디터 화면과 플레이 화면에서 모두 사라짐.

    HiddenInGame()

    에디터 화면에서 작업할때는 보여줌.

 

  - 죽음 구현

 

  - 숙제

    아이템 상자에서 다른 무기도 습득 되게끔 구현 해보기

 

7.3-4 아이템 클래스 구조 개선

  - 의존성에 대한 고찰

    A 클래스와 B 클래스가 매개 클래스 없이 직접 참조한다면,

    한쪽 클래스를 유지보수 할때 다른 쪽 클래스도 똑같이 유지보수 해주거나 신경써야함.

    이건 일대일 이야기지만, 만약 일대다이거나 다대다의 경우엔 상당히 위험하고 골치아픔.

    실제로 기능 구현은 2~3일이면 완료되지만, 유지보수에는 몇달이 걸림.

 

  - 인터페이스 구조

    A 클래스와 B 클래스 사이에 매개 클래스(==인터페이스)를 두어서 의존성을 낮춰주는 구조.

    한 쪽 클래스가 에러나도 매개 클래스만 제대로 동작한다면 다른 쪽은 신경 안써도됨.

      ex. 편지를 주는 클래스와 받는 클래스가 있다면, 주는 클래스는 주기만 잘하면 되고

        받는 클래스는 받기만 잘하면 됨. 그 둘 사이에 우체부 클래스를 두는 것.

        편지를 주는 클래스가 받는 클래스 개체를 직접 참조해서 구조를 짜는 것과는 다름.

    또한 다른 한 쪽 클래스의 코드가 수정되어도 다른 쪽 코드까지 컴파일 되지 않아서 시간 절약.

 

  - 의존성 분리를 위한 설계 규칙

    프로젝트의 주요 레이어를 분리함.

    상위 레이어 클래스는 하위 레이어 클래스를 직접 참조 하되,

    하위 레이어 클래스는 상위 레이어 클래스를 인터페이스를 통해 접근하게끔 구조를 설계.

    게임 레이어

    게임 로직에 필수적인 클래스들.

    미들웨어 레이어

    게임 로직에 부수적으로 필요한 클래스들

    데이터 레이어

    게임을 구성하는 기본 데이터

 

  - ILootable 인터페이스

    새 C++ 클래스 > Unreal Interface 부모 클래스 > "SLootable"

    Path > Interfaces

<hide/>

// SLootable.h

#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "SLootable.generated.h"

UINTERFACE(MinimalAPI)
class USLootable : public UInterface
{
    GENERATED_BODY()
};

/**
 * 
 */
class STUDYPROJECT_API ISLootable
{
    GENERATED_BODY()

public:
    virtual bool CanLootItem() = 0;

    virtual void LootItem() = 0;
    
};
<hide/>

// SLootable.cpp


#include "Interfaces/SLootable.h"
<hide/>

// SPlayerCharacter.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "InputActionValue.h"
#include "Interfaces/SLootable.h"
#include "SPlayerCharacter.generated.h"

UCLASS()
class STUDYPROJECT_API ASPlayerCharacter 
    : public ACharacter
    , public ISLootable
{
    ...

public:
    ...

    // bool CanEquipWeapon();

    // void EquipWeapon(class ASWeapon* InWeapon);

    virtual bool CanLootItem() override;

    virtual void LootItem() override;

protected:
    ...

};
<hide/>

// SPlayerCharacter.h


...

bool ASPlayerCharacter::CanLootItem()
{
    return false;
}

void ASPlayerCharacter::LootItem()
{
}

...
<hide/>

// SItemBox.cpp


...

void ASItemBox::OnOverlapBegun(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
    ...
    if (true == ::IsValid(OtherPlayerCharacter) && nullptr != WeaponClass)
    {
        ...
        if (true == ::IsValid(NewWeapon))
        {
            //OtherPlayerCharacter->EquipWeapon(NewWeapon);
        }
    }
}

...

 

  - 아이템 데이터 관련 클래스

    새 C++ 클래스 > PrimaryDataAsset 부모 클래스 > "SItemData"

    Path > Items

    아래와 같이 작성 후 컴파일. BP_Knife 혹은 다른 무기 블루프린트 애셋에서 ItemType 지정.

<hide/>

// SItemData.h

#pragma once

#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "SItemData.generated.h"

UENUM(BlueprintType)
enum class EItemType : uint8
{
    None = 0,
    Weapon = 1,
    End
};

/**
 * 
 */
UCLASS()
class STUDYPROJECT_API USItemData : public UPrimaryDataAsset
{
    GENERATED_BODY()

public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = USItemData)
    EItemType ItemType;
    
};
<hide/>

// SItemData.cpp


#include "Items/SItemData.h"

 

  - SWeapon 클래스 삭제하고 다시 만들기

    언리얼 에디터에서는 C++ 클래스 삭제 기능을 제공하지 않음.

    1. 프로젝트 폴더 > Source > StudyProject > Public 폴더에서 SWeapon.h 파일 검색 후 삭제.

      Private 폴더에서 SWeapon.cpp 파일 검색 후 삭제.

    2. .vs 폴더, Binaries 폴더, Intermediate 폴더, Saved 폴더, .sln 파일 삭제

    3. .uproject 파일 우클릭 > Generate Visual Studio project files 클릭

    4. .sln 파일 더블 클릭 후 재빌드.

    5. SWeapon을 상속 받은 블루프린트 애셋(BP_Knife, ...) 삭제

<hide/>

// SPlayerCharacter.h

...
class STUDYPROJECT_API ASPlayerCharacter 
    ...
{
    ...

private:
    ...

    // UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = ASPlayerCharacter, meta = (AllowPrivateAccess))
    // TObjectPtr<class ASWeapon> Weapon;

};
<hide/>

// SPlayerCharacter.h

...
//#include "Items/SWeapon.h"

...

//bool ASPlayerCharacter::CanEquipWeapon()
//{
//    return (nullptr == Weapon);
//}
//
//void ASPlayerCharacter::EquipWeapon(ASWeapon* InWeapon)
//{
//    FName WeaponSocket(TEXT("WeaponSocket"));
//    if (true == ::IsValid(InWeapon) && true == GetMesh()->DoesSocketExist(WeaponSocket))
//    {
//        if (false == CanEquipWeapon())
//        {
//            Weapon->DetachFromActor(FDetachmentTransformRules::KeepRelativeTransform);
//            Weapon->SetActorHiddenInGame(true);
//        }
//
//        InWeapon->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetIncludingScale, WeaponSocket);
//        InWeapon->SetOwner(this);
//        Weapon = InWeapon;
//    }
//}

...
<hide/>

// SItemBox.h

...
class STUDYPROJECT_API ASItemBox : public AActor
{
    ...

private:
    ...

    // UPROPERTY(EditInstanceOnly, BlueprintReadOnly, Category = ASItemBox, meta = (AllowPrivateAccess))
    // TSubclassOf<class ASWeapon> WeaponClass;

};
<hide/>

// SItemBox.cpp


...
// #include "Items/SWeapon.h"
// #include "Characters/SPlayerCharacter.h"

ASItemBox::ASItemBox()
{
    ...

    // WeaponClass = ASWeapon::StaticClass();

}

void ASItemBox::OnOverlapBegun(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
    ...

    // ASPlayerCharacter* OtherPlayerCharacter = Cast<ASPlayerCharacter>(OtherActor);
    // if (true == ::IsValid(OtherPlayerCharacter) && nullptr != WeaponClass)
    // {
    //     ASWeapon* NewWeapon = GetWorld()->SpawnActor<ASWeapon>(WeaponClass, FVector::ZeroVector, FRotator::ZeroRotator);
    //     if (true == ::IsValid(NewWeapon))
    //     {
    //         OtherPlayerCharacter->EquipWeapon(NewWeapon);
    //     }
    // }
}

...

 

  - 하드 레퍼런싱 Vs. 소프트 레퍼런싱

    액터 로딩시 TObjectPtr로 선언한 언리얼 오브젝트도 따라서 메모리에 로딩됨.

    이를 하드 레퍼런싱이라고 함.

    게임 진행에 필수적인 언리얼 오브젝트는 이렇게 선언해도 되지만 아이템의 경우?

    데이터 라이브러리에 1000종 아이템 목록이 있을 때 이를 모두 다 로딩할 것인가?

    필요한 데이터만 로딩하도록 TSoftObjectPtr로 선언하고 대신 애셋 주소 문자열을 지정함.

    필요시에 애셋을 로딩하도록 구현을 변경할 수 있으나, 애셋 로딩 시간이 소요됨.

 

  - SWeapon 클래스 생성

    새 C++ 클래스 > SItemData 부모 클래스 > "SWeapon"

    Path > Items

    Content Browser > StudyProject > Items 우클릭 > Miscellaneous > 새 Data Asset 애셋

    SWeapon 부모 클래스 > "DA_Knife"

    DA_Knife > SkeletalMesh에 Knife_A, ItemType에는 Weapon 지정.

<hide/>

// SWeapon.h

#pragma once

#include "CoreMinimal.h"
#include "Items/SItemData.h"
#include "SWeapon.generated.h"

/**
 * 
 */
UCLASS()
class STUDYPROJECT_API USWeapon : public USItemData
{
    GENERATED_BODY()
    
public:
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = USWeapon)
    TSoftObjectPtr<class USkeletalMesh> SkeletalMesh;

};
<hide/>

// SWeapon.cpp


#include "Items/SWeapon.h"

 

  - 현재 게임에서 로딩되어 있는 스켈레탈 메시의 목록을 살펴보기

    콘솔 명령어: Obj List Class=SkeletalMesh

 

  - 새로운 구조에 맞게끔 코드 리팩토링 실습

    월드에 기존에 만들어 두었던 ItemBox는 삭제했다가 다시 배치해야함.

    다시 배치한 ItemBox의 ItemClass에 BP_Knife 지정.

<hide/>

// SLootable.h

...
class STUDYPROJECT_API ISLootable
{
    ...

public:
    virtual bool CanLootItem(class USItemData* InItemData) = 0;

    virtual void LootItem(class USItemData* InItemData) = 0;

    virtual bool CanEquipWeapon(class USItemData* InItemData) { return true; };

    virtual void EquipWeapon(class USItemData* InItemData) {};
    
};
<hide/>

// SPlayerCharacter.h

...
class STUDYPROJECT_API ASPlayerCharacter 
    ...
{
    ...

public:
    ...

    virtual bool CanLootItem(class USItemData* InItemData) override;

    virtual void LootItem(class USItemData* InItemData) override;

    virtual bool CanEquipWeapon(class USItemData* InItemData) override;

    virtual void EquipWeapon(class USItemData* InItemData) override;

protected:
    ...

};
<hide/>

// SPlayerCharacter.h


...
#include "Items/SItemData.h"
#include "Items/SWeapon.h"

...

bool ASPlayerCharacter::CanLootItem(USItemData* InItemData)
{
    return (true == ::IsValid(InItemData));
}

void ASPlayerCharacter::LootItem(USItemData* InItemData)
{
    if (false == CanLootItem(InItemData))
    {
        return;
    }

    //@TODO
}

bool ASPlayerCharacter::CanEquipWeapon(USItemData* InItemData)
{
    return (true == ::IsValid(InItemData));
}

void ASPlayerCharacter::EquipWeapon(USItemData* InItemData)
{
    if (false == CanEquipWeapon(InItemData))
    {
        return;
    }

    //@TODO
}

...
<hide/>

// SItemBox.h

...
class STUDYPROJECT_API ASItemBox : public AActor
{
    ...

private:
    ...

    UPROPERTY(EditInstanceOnly, BlueprintReadOnly, Category = ASItemBox, meta = (AllowPrivateAccess))
    TObjectPtr<class USItemData> ItemClass;

};
<hide/>

// SItemBox.cpp


...
#include "Interfaces/SLootable.h"
#include "Items/SItemData.h"

...

void ASItemBox::OnOverlapBegun(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
    ...
    
    ISLootable* OtherLootableActor = Cast<ISLootable>(OtherActor);
    if (nullptr != OtherLootableActor && nullptr != Item)
    {
        OtherLootableActor->LootItem(Item);
    }
}

...

 

  - 델리게이트 어레이를 활용한 아이템별 로직 분기

<hide/>

// SPlayerCharacter.h

#pragma once

...

DECLARE_DELEGATE_OneParam(FOnItemLooted, class USItemData* InItemData);

USTRUCT(BlueprintType)
struct FOnItemLootedDelegateWrapper
{
    GENERATED_BODY()

    FOnItemLootedDelegateWrapper() {}

    FOnItemLootedDelegateWrapper(const FOnItemLooted& InDelegate)
        : OnItemLooted(InDelegate)
    {
    }

    FOnItemLooted OnItemLooted;
};

UCLASS()
class STUDYPROJECT_API ASPlayerCharacter 
    : public ACharacter
    , public ISLootable
{
    ...

private:
    ...

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = ASPlayerCharacter, meta = (AllowPrivateAccess))
    TObjectPtr<class USkeletalMeshComponent> CurrentWeaponSkeletalMeshComponent;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = ASPlayerCharacter, Meta = (AllowPrivateAccess))
    TArray<FOnItemLootedDelegateWrapper> LootItemDelegates;

};
<hide/>

// SPlayerCharacter.h


...

ASPlayerCharacter::ASPlayerCharacter()
    ...
{
    ...

    LootItemDelegates.Add(FOnItemLootedDelegateWrapper()); // 패딩.
    LootItemDelegates.Add(FOnItemLootedDelegateWrapper(FOnItemLooted::CreateUObject(this, &ASPlayerCharacter::EquipWeapon)));

    CurrentWeaponSkeletalMeshComponent = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("CurrentWeaponSkeletalMeshComponent"));
    CurrentWeaponSkeletalMeshComponent->SetupAttachment(GetMesh(), TEXT("WeaponSocket"));
}

...

bool ASPlayerCharacter::CanLootItem(USItemData* InItemData)
{
    return (true == ::IsValid(InItemData));
}

void ASPlayerCharacter::LootItem(USItemData* InItemData)
{
    if (false == CanLootItem(InItemData))
    {
        return;
    }

    LootItemDelegates[(uint8)InItemData->ItemType].OnItemLooted.ExecuteIfBound(InItemData);
}

bool ASPlayerCharacter::CanEquipWeapon(USItemData* InItemData)
{
    return (true == ::IsValid(InItemData));
}

void ASPlayerCharacter::EquipWeapon(USItemData* InItemData)
{
    if (false == CanEquipWeapon(InItemData))
    {
        return;
    }

    if (USWeapon* InWeapon = Cast<USWeapon>(InItemData))
    {
        if (true == InWeapon->SkeletalMesh.IsPending())
        {
            InWeapon->SkeletalMesh.LoadSynchronous();
        }
        CurrentWeaponSkeletalMeshComponent->SetSkeletalMesh(InWeapon->SkeletalMesh.Get());
    }
}

...

 

8.6 전투 관련 부가 요소

  - 무기 장착을 통한 공격 범위 및 데미지 수정

    [SWeapon -> SWeaponData로 변경 필수. SWeaponActor 생성 후 SpawnActor() 예제 필요]

<hide/>

// SWeapon.h

#pragma once

#include "CoreMinimal.h"
#include "SItemData.h"
#include "SWeapon.generated.h"

UCLASS()
class STUDYPROJECT_API USWeapon : public USItemData
{
    GENERATED_BODY()

public:
    USWeapon();

    float GetAttackRange() const { return WeaponAttackRange; };

    float GetFinalWeaponDamage() const;

public:
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = USWeapon)
    TSoftObjectPtr<class USkeletalMesh> SkeletalMesh;

protected:
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = USWeapon)
    float WeaponAttackRange = 100.f;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = USWeapon)
    float WeaponMinimumDamage = 5.f;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = USWeapon)
    float WeaponMaximumDamage = 10.f;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = USWeapon)
    float WeaponMinimumDamageModifier = 0.7f;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = USWeapon)
    float WeaponMaximumDamageModifier = 1.7f;

};
<hide/>

// SWeapon.cpp

#include "SWeapon.h"

USWeapon::USWeapon()
{
}

float USWeapon::GetFinalWeaponDamage() const
{
    float FinalModifier = FMath::RandRange(WeaponMinimumDamageModifier, WeaponMaximumDamageModifier);
    float FinalDamage = FMath::RandRange(WeaponMinimumDamage, WeaponMaximumDamage) * FinalModifier;

    return FinalDamage;
}
<hide/>

// SPlayerCharacter.h

...
class STUDYPROJECT_API ASPlayerCharacter 
    ...
{
    ...

private:
    ...

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = ASPlayerCharacter, meta = (AllowPrivateAccess = true))
    TObjectPtr<class USWeapon> CurrentWeapon;

};
<hide/>

// SPlayerCharacter.cpp

...

void ASPlayerCharacter::EquipWeapon(USItemData* InItemData)
{
    ...

    if (USWeapon* InWeapon = Cast<USWeapon>(InItemData))
    {
        ...
        CurrentWeapon = InWeapon;
    }
}

...

void ASPlayerCharacter::CheckHit()
{
    ...

    AttackRange = (nullptr == CurrentWeapon) ? 50.f : CurrentWeapon->GetAttackRange();
    float Damage = (nullptr == CurrentWeapon) ? StatComponent->GetAttack() : StatComponent->GetAttack() + CurrentWeapon->GetFinalWeaponDamage();

    ...

    if (true == bResult)
    {
        if (nullptr != HitResult.GetActor())
        {
            ...
            HitResult.GetActor()->TakeDamage(Damage, DamageEvent, GetController(), this);
        }
    }
}

...

 

 

 

 

 

  - [숙제] 맵 이동 포탈

    포탈 BeginOverlap 델리게이트와 OpenLevel 활용.

    맵 이동하게 되면 이전 레벨에서 생성된 GameMode와 그 휘하의 모든 액터들이 초기화됨.

    즉, 아이템을 얻고 이동하거나 특정 상태에서 이동하면 모두 날아감.

    GameInstance

 

  - [숙제] 캐릭터로의 빙의 예제

    B키 생성, SphereOverlapActors에 this ignore, PlayerCharacter만 탐지

    탐지된 액터로의 Possess 시도

    이를 통해 관전 시스템의 가능성도 알려줌.

    탈 것도 가능해지는 걸 알림.