[49. Pickups #2]

 

Item에 파티클을 적용시켜 본다.

 

// Item.h
public:
    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Item | Mesh")
    UStaticMeshComponent* Mesh;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item | Particles")
    class UParticleSystemComponent* IdleParticlesComponent;
    
// Item.cpp
#include "Particles/ParticleSystemComponent.h"

AItem::AItem()
{
    Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
    Mesh->SetupAttachment(GetRootComponent());

    IdleParticlesComponent = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("IdleParticlesComponent"));
    IdleParticlesComponent->SetupAttachment(GetRootComponent());
}

 

시각적으로 보이는 오브젝트와 파티클을 출력해야 하므로 Item 클래스에 메시와 파티클을 추가해준다.

 

파티클과 메시를 적당히 선택해주면 비주얼적으로 매우 괜찮게 보여진다.

 

좀 더 괜찮게 보이기 위해 해당 오브젝트와 겹침 이벤트 발생시 파티클을 하나 생성하고 오브젝트를 삭제해보자.

 

// Item.h	
public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item | Particles")
    class UParticleSystem* OverlapParticles;
    
// Item.cpp
#include "Kismet/GameplayStatics.h"

void AItem::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    UE_LOG(LogTemp, Warning, TEXT("Super::OnOverlapBegin()"));

    if (OverlapParticles)
    {
        UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), OverlapParticles, GetActorLocation(), FRotator(0.f), true);
    }
    Destroy();
}

 

코드에서 파티클을 따로 정하지 않았으므로 블루프린트에서 선택해주면 된다.

 

잘 적용되었다. 

 

// Item.h
public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item | Sounds")
    class USoundCue* OverlapSound;
    
// Item.cpp
#include "Sound/SoundCue.h"

void AItem::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    UE_LOG(LogTemp, Warning, TEXT("Super::OnOverlapBegin()"));

    if (OverlapParticles)
    {
        UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), OverlapParticles, GetActorLocation(), FRotator(0.f), true);
    }
    if (OverlapSound)
    {
        UGameplayStatics::PlaySound2D(this, OverlapSound);
    }
    Destroy();
}

 

효과음도 마찬가지로 블루프린트에서 선택해주고 실행하면 효과음까지 같이 재생된다.

 

SoundCue를 이용하기 때문에 wav 파일로 사운드 큐를 만들어서 사용해야 한다.

 

[50. Pickups #3]

 

 

// Item.h
public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item | ItemProperties")
    bool bRotate;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item | ItemProperties")
    float RotationRate;
    
// Item.cpp
void AItem::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    if (bRotate)
    {
        FRotator Rotation = GetActorRotation();
        Rotation.Yaw += DeltaTime * RotationRate;
        SetActorRotation(Rotation);
    }
}

 

코드나 에디터의 디테일 탭에서 true로 만들면 천천히 회전하게 된다. 

 

[정리]

 

  • ParticleSystemComponent는 컴포넌트이고 ParticleSystem은 파티클 효과 그 자체이다. 그래서 전자는 CreateDefaultSubobject에 의한 생성이 필요하고 후자는 필요하지 않다.
    파티클 지속의 필요성 / 일회성 정도로 판단하면 괜찮지 않을까 싶다.
  • Destroy()는 유니티의 Destroy()와 유사하다.

'언리얼 엔진 > UE4 C++' 카테고리의 다른 글

[UE C++] Gameplay Mechanics #6  (0) 2022.09.09
[UE C++] Gameplay Mechanics #5  (0) 2022.09.08
[UE C++] Gameplay Mechanics #3  (0) 2022.09.07
[UE C++] Gameplay Mechanics #2  (0) 2022.09.07
[UE C++] Gameplay Mechanics #1  (0) 2022.09.06

[47. Floating Platform]

 

(예시 스샷)

공중에 떠있는 발판이다. 보통 퍼즐 플래포머 게임의 기반이 된다.

예시 스샷처럼 목표 지점에 도착하면 일정시간 대기했다가 다시 목표지점으로 가는 방식 등등이 있다.

 

Actor를 상속하는 C++클래스 FloatingPlatform를 만든다. 

 

// FloatPlatform.h
public:
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Platform")
    UStaticMeshComponent* Mesh;

    UPROPERTY(EditAnywhere, Category = "Platform")
    FVector StartPoint;

    UPROPERTY(EditAnywhere, meta=(MakeEditWidget = "true"))
    FVector EndPoint;
    
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Platform")
    float InterpSpeed;
    
// FloatPlatform.cpp
AFloatingPlatform::AFloatingPlatform()
{
    Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
    RootComponent = Mesh;

    StartPoint = FVector(0.f);
    EndPoint = FVector(0.f);
    
    InterpSpeed = 4.f;
}

void AFloatingPlatform::BeginPlay()
{
    Super::BeginPlay();
    
    StartPoint = GetActorLocation();
    EndPoint += StartPoint;
}

void AFloatingPlatform::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    FVector CurrentLocation = GetActorLocation();
    FVector Interp = FMath::VInterpTo(CurrentLocation, EndPoint, DeltaTime, InterpSpeed);
    SetActorLocation(Interp);
}

 

블루프린트 클래스를 만들고 메시를 붙여주는데, 해당 메시에 [10면체 Z 단순화 콜리전 추가] 를 해준다.

*K면체 단순화 콜리전(K-DOP: K Discrete Oriented Polytope): 축에 평행한 면의 개수가 K개인 다면체

 

두 지점을 왕복하기 위해 FVector 변수 두개를 추가해준다.

그리고 이동속도를 위해 float 변수도 하나 추가해준다.

 

VInterpTo는 선형보간이다.

 

EndPoint의 UPROPERTY 속성에 있는 meta=(MakeEditWidget="true") 옵션은 위의 스샷과 같이 에디터 상에 변수명이 표시가 되며, 해당 변수를 끌어서 위치를 변경할수도 있다.

물론 디테일 탭에서도 수정이 가능하다 .

 

EndPoint를 적당한 거리에 두고 실행하면 해당 위치로 부드럽게 이동한다.

 

참고로 EndPoint는 월드 좌표가 아닌 로컬 좌표이다.

 

 

이제 StartPoint와 EndPoint 사이를 왕복해야 하기 때문에 추가 구현을 한다.

 

// FloatingPlatform.h
public:
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Platform")
    float InterpTime;

    FTimerHandle InterpTimer;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Platform")
    bool bInterping;
    
    float Distance;
    
    void ToggleInterping();

    void SwapVectors(FVector& VecOne, FVector& VecTwo);
    
// FloatingPlatform.cpp
AFloatingPlatform::AFloatingPlatform()
{
    bInterping = false;
    InterpTime = 1.f;
}

void AFloatingPlatform::BeginPlay()
{
    GetWorldTimerManager().SetTimer(InterpTimer, this, &AFloatingPlatform::ToggleInterping, InterpSpeed);
    
    Distance = (EndPoint - StartPoint).Size();
}

void AFloatingPlatform::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    if (bInterping)
    {
        FVector CurrentLocation = GetActorLocation();
        FVector Interp = FMath::VInterpTo(CurrentLocation, EndPoint, DeltaTime, InterpSpeed);
        SetActorLocation(Interp);
        
        float DistanceTraveled = (GetActorLocation() - StartPoint).Size();
        if (Distance - DistanceTraveled <= 1.f)
        {
            ToggleInterping();

            GetWorldTimerManager().SetTimer(InterpTimer, this, &AFloatingPlatform::ToggleInterping, InterpSpeed);
            SwapVectors(StartPoint, EndPoint);
        }
    }
}

void AFloatingPlatform::ToggleInterping()
{
    bInterping = !bInterping;
}

void AFloatingPlatform::SwapVectors(FVector& VecOne, FVector& VecTwo)
{
    FVector Temp = VecOne;
    VecOne = VecTwo;
    VecTwo = Temp;
}

 

타이머 매니저를 이용해서 두 점을 왕복하는 발판을 만들게 됐다.

다만 하나의 문제점이 있다. 발판을 여러개 배치해서 서로 거리를 다르게 두고 실행해보면 거리에 따른 도착시간의 오차때문에 동시에 진행되지 못하고 시간이 흐를수록 발판들이 따로 움직인다.

이 부분은 동기화 알고리즘을 찾아서 따로 해결해야 한다.

 

이동 속도나 시간 등등 발판의 기초적인 부분을 구현해두고 상속시켜서 사용하면 다양한 발판을 만들 수 있어서 좋을 것 같다.

 

[48. Pickups #1]

 

플레이어와 상호작용 하는 오브젝트는 다양하다.

어떤 아이템을 획득하는 것일수도 있고 함정을 밟는것일수도 있다.

트리거에 의한 이벤트 발생이라는점이 공통이므로 해당 부분을 부모 클래스로 한 파생 클래스들을 만들 예정이다.

 

영상에 나온대로 리소스를 가져온다.

 

// Item.h
class FIRSTPROJECT_API AItem : public AActor
{
    GENERATED_BODY()

public:	
    AItem();

    /* Base shape collision */
    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Item | Collision")
    class USphereComponent* CollisionVolume;

protected:
    virtual void BeginPlay() override;

public:	
    virtual void Tick(float DeltaTime) override;

    UFUNCTION()
    virtual void OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

    UFUNCTION()
    virtual void OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
};

// Item.cpp
#include "Components/SphereComponent.h"

AItem::AItem()
{
    PrimaryActorTick.bCanEverTick = true;

    CollisionVolume = CreateDefaultSubobject<USphereComponent>(TEXT("CollisionVolume"));
    RootComponent = CollisionVolume;
}

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

    CollisionVolume->OnComponentBeginOverlap.AddDynamic(this, &AItem::OnOverlapBegin);
    CollisionVolume->OnComponentEndOverlap.AddDynamic(this, &AItem::OnOverlapEnd);
}

void AItem::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
}

void AItem::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    UE_LOG(LogTemp, Warning, TEXT("OnOverlapBegin()"));
}

void AItem::OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
    UE_LOG(LogTemp, Warning, TEXT("OnOverlapEnd()"));
}

 

구체 콜리전을 가진 트리거 기능만 구현해서 만들었다.

 

 

// Explosive.h
public:
    AExplosive();

public:
    virtual void OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) override;
    virtual void OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex) override;

// Explosive.cpp
AExplosive::AExplosive()
{
}

void AExplosive::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    Super::OnOverlapBegin(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex, bFromSweep, SweepResult);

    UE_LOG(LogTemp, Warning, TEXT("OnOverlapBegin()"));
}

void AExplosive::OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
    Super::OnOverlapEnd(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex);

    UE_LOG(LogTemp, Warning, TEXT("OnOverlapEnd()"));
}

 

// Pickup.h
public:
    APickup();
public:
    virtual void OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) override;
    virtual void OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex) override;

// Pickup.cpp
APickup::APickup()
{
}

void APickup::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    Super::OnOverlapBegin(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex, bFromSweep, SweepResult);

    UE_LOG(LogTemp, Warning, TEXT("Pickup::OnOverlapBegin()"));
}

void APickup::OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
    Super::OnOverlapEnd(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex);

    UE_LOG(LogTemp, Warning, TEXT("Pickup::OnOverlapEnd()"));
}

 

부모 클래스인 Item 클래스에서 OnOverlap 함수들에게 리플렉션을 설정해 주었으므로 자식 클래스에서는 리플렉션을 설정하지 않아도 된다. 상위버전 언리얼에서는 컴파일 에러가 난다 (하위버전에서는 해줘야함)

 

자식 클래스에서 Super로 상위 클래스를 호출하므로 부모->자식 순으로 실행된다.

다시 상기하자면 Tick이 필요없으므로 Item의 생성자에서 틱을 비활성화 시켜주면 된다.

특정 자식 클래스에서 필요하면 거기서 다시 활성화 시켜주면 된다.

 

 

 

[정리]

 

  • 부모 클래스의 함수가 UFUNCTION()으로 리플렉션 캡처가 되었다면 자식 클래스는 리플렉션을 빼야한다. 그렇지 않으면 컴파일 에러가 발생한다.
  • 당연한 얘기지만 오버라이딩 한 함수에서 Super를 호출하면 부모->자식 순으로 실행된다.
  • 상속성을 잘 이용하자.

'언리얼 엔진 > UE4 C++' 카테고리의 다른 글

[UE C++] Gameplay Mechanics #5  (0) 2022.09.08
[UE C++] Gameplay Mechanics #4  (0) 2022.09.08
[UE C++] Gameplay Mechanics #2  (0) 2022.09.07
[UE C++] Gameplay Mechanics #1  (0) 2022.09.06
[UE C++] The Character Class #2  (0) 2022.09.06

[45. Spawn Volume #1]

 

어떤 액터들을 맵에 직접 배치하기 보다는 스폰 볼륨을 통해 자동적으로 생성되게 하면 좀 더 좋을 것이다.

 

전의 실습에서 트리거 박스를 이용한것처럼 보이지 않는 박스를 만들어서 해당 범위 내의 랜덤한 위치에 액터를 스폰하게 만들 예정이다.

 

우선 Actor를 상속받는 SpawnVolume C++ 클래스를 만든다.

 

 

// SpawnVolume.h
public:
    // ≒const
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Spawning")
    class UBoxComponent* SpawningBox;

    UFUNCTION(BlueprintPure, Category = "Spawning")
    FVector GetSpawnPoint();
    
// SpawnVolume.cpp
#include "Kismet/KismetMathLibrary.h"

ASpawnVolume::ASpawnVolume()
{
    // 늘 하던 초기화
    SpawningBox = CreateDefaultSubobject<UBoxComponent>(TEXT("SpawningBox"));
}

FVector ASpawnVolume::GetSpawnPoint()
{
    // 박스의 크기를 반환받는다
    FVector Extent = SpawningBox->GetScaledBoxExtent();
    // 박스의 원점을 반환받는다
    FVector Origin = SpawningBox->GetComponentLocation();

    // 원점, 크기를 인자로 넘겨줘서 해당 범위 내의 랜덤한 좌표를 반환한다
    FVector Point = UKismetMathLibrary::RandomPointInBoundingBox(Origin, Extent);

    return Point;
}

 

UFUNCTION의 BlueprintPure는 const와 같은 개념이다. 해당 함수를 호출해도 객체에 아무런 영향도 미치지 못한다. 그래서 주로 위와 같이 getter에 사용한다.

 

 

// SpawnVolume.h
public:
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawning")
    TSubclassOf<class ACritter> PawnToSpawn;

 

TSubclassOf 템플릿은 UClass 타입에 대한 안정성을 제공하는 템플릿 클래스이다.

받을 수 있는 타입을 한정짓는다.

 

만약 TSubclassOf가 아닌 UClass*라면 드롭다운 메뉴에서 모든 클래스가 다 나오게 된다. 그러다가 잘못된 클래스를 선택해서 사용하게 되면 런타임 도중 오류가 발생하게 된다.

 

하지만 TSubclassOf로 타입을 한정 지어주면 해당 타입만 표시되고 사용할 수 있다. 설령 잘못 선택했다 하더라도 타입은 맞기때문에 런타임 오류가 발생하지 않는다.

 

 

UClass* ClassA = UDamageType::StaticClass();
TSubclassOf<UDamageType> ClassB;
ClassB = ClassA; // 런타임 중에 체크
TSubclassOf<UDamageType_Lava> ClassC;
ClassB = ClassC; // 컴파일 중에 체크

 

블루프린트에서의 문제 방지 뿐만 아니라 C++ 작업시에도 서로 호환되지 않는 타입을 할당할 때, 오류를 검출하는 단계가 다르다.

 

 

이벤트그래프를 작성하고 SpawnVolume을 월드에 배치하면 플레이 할 때마다 박스 범위 내의 랜덤한 위치에 거미가 스폰된다. 

 

가운데 오브젝트를 다 날리고 적당히 배치하면 된다.

 

[46. Spawn Volume #2]

 

스폰 볼륨을 블루프린트가 아닌 C++에서 구현하는 것으로 변경해본다.

 

// SpawnVolume.h
public:	
    UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Spawning")
    void SpawnOurPawn(UClass* ToSpawn, const FVector& Location);
    
// SpawnVolume.cpp
void ASpawnVolume::SpawnOurPawn_Implementation(UClass* ToSpawn, const FVector& Location)
{
    if (ToSpawn)
    {
        UWorld* World = GetWorld();
        FActorSpawnParameters SpawnParams;

        if (World)
        {
            ACritter* CritterSpawned = World->SpawnActor<ACritter>(ToSpawn, Location, FRotator(0.f), SpawnParams);
        }
    }
}

 

UFUNCTION의 속성이 BlueprintNativeEvent라면 SpawnOurPawn을 구현하는것이 아니라 _Implimentation을 구현해야 한다.

블루프린트에서 해당 함수를 사용 시 재정의가 되지 않았다면 Implementation이 실행 될 것이다.

하지만 재정의가 된다면 해당 이벤트가 실행이 된다.

 

아래쪽에 있는 SpawnOurPawn 이벤트가 없다면 C++에서 구현한 것만 실행이 될 것이고 재정의를 했다면 해당 내용이 실행 될 것이다.

 

실습에서는 크리터가 생성되는 위치에 파티클을 하나 생성하는 것 뿐이므로 중간에 파티클 생성만 끼워넣었다.

 

이런 식으로 작동하게 된다.

 

 

[정리]

 

  • TSubclassOf는 UClass 타입의 안정성을 보장해 주는 템플릿 클래스이다.
    특정 UObject/UClass의 하위 클래스로 한정 짓고 런타임 에러를 방지한다.
    http://egloos.zum.com/sweeper/v/3225016
  • BlueprintNativeEvent는 블루프린트에서 오버라이딩 가능하게 해준다.

'언리얼 엔진 > UE4 C++' 카테고리의 다른 글

[UE C++] Gameplay Mechanics #4  (0) 2022.09.08
[UE C++] Gameplay Mechanics #3  (0) 2022.09.07
[UE C++] Gameplay Mechanics #1  (0) 2022.09.06
[UE C++] The Character Class #2  (0) 2022.09.06
[UE C++] The Character Class #1  (0) 2022.09.05

[42. Floor Switch #1]

 

상호작용(interaction) 가능한 구성요소를 만든다.

 

트리거는 액터를 상속받아서 만든다. 이름은 FloorSwitch로 짓는다.

보이지는 않지만 충돌을 감지할 트리거, 그리고 눈에 보여지는 메시를 가지고 있어야 한다.

 

// FloorSwitch.h
public:
    /* Overlap volume for functionality to be triggered */
    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Floor Switch")
    class UBoxComponent* TriggerBox;

    /* Switch for the character to step on */
    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Floor Switch")
    UStaticMeshComponent* FloorSwitch;

    /* Door to move when the floor switch is stepped on */
    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Floor Switch")
    UStaticMeshComponent* Door;
    
// FloorSwitch.cpp
#include "Components/BoxComponent.h"

AFloorSwitch::AFloorSwitch()
{
    // 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;

    TriggerBox = CreateDefaultSubobject<UBoxComponent>(TEXT("TriggerBox"));
    SetRootComponent(TriggerBox);

    FloorSwitch = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("FloorSwitch"));
    FloorSwitch->SetupAttachment(GetRootComponent());

    Door = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Door"));
    Door->SetupAttachment(GetRootComponent());
}

 

앞서 하던대로 컴포넌트들을 생성해주고 TriggerBox를 루트 컴포넌트로 지정하고 계층 구조를 설정한다.

 

이제 TriggerBox와 충돌시 실행할 함수를 바인딩 할 차례이다.

 

 

// FloorSwitch.h
public:
    UFUNCTION()
    void OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
    
// FloorSwitch.cpp
void AFloorSwitch::BeginPlay()
{
    Super::BeginPlay();
    TriggerBox->OnComponentBeginOverlap.AddDynamic(this, &AFloorSwitch::OnOverlapBegin);
}

 

OnOverlapBegin의 매개변수가 저렇게 많은 이유는 동적 멀티캐스팅 델리게이트가 저렇게 만들어져 있기 때문이다.

임의로 지정한 것이 아니고 OnComponentBeginOverlap의 정의를 따라서 들어가보면 확인할 수 있다.

 

바인딩은 생성자보다는 그 후에 해주는것이 좋다.

이 실습에서는 BeginPlay에서 했지만 여기에서는 PostInitializeComponents를 오버라이딩 해서 바인딩 했다.

PostIntializeComponents가 BeginPlay보다 더 일찍 실행된다. 우선 여기서는 실습을 따라가도록 한다.

 

충돌이 일어났다가 벗어날 때 실행할 함수에 대한 바인딩도 필요하다.

 

// FloorSwitch.h
UFUNCTION()
void OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
    
// FloorSwitch.cpp
TriggerBox->OnComponentEndOverlap.AddDynamic(this, &AFloorSwitch::OnOverlapEnd);

 

매개변수가 조금 다르다. 마찬가지로 정의로 이동해서 확인하면 된다.

 

참고로 해당 이벤트들은 블루프린트 디테일 탭에서도 확인할 수 있다.

구현은 C++로 할 것이기 때문에 이벤트그래프에는 나오지 않는다.

 

 

// FloorSwitch.cpp
void AFloorSwitch::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    
    UE_LOG(LogTemp, Warning, TEXT("Overlap Begin."));
}

void AFloorSwitch::OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
    
    UE_LOG(LogTemp, Warning, TEXT("Overlap End."));
}

 

내용은 우선 로그를 찍는걸로 간단히 작성한다.

 

 

// FloorSwitch.cpp
AFloorSwitch::AFloorSwitch()
{
    // 쿼리만 처리
    TriggerBox->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
    // 오브젝트 타입을 WorldStatic으로 설정
    TriggerBox->SetCollisionObjectType(ECollisionChannel::ECC_WorldStatic);
    // 모든 채널에 대한 충돌 처리를 무시함 (최적화)
    TriggerBox->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
    // ECC_Pawn에 대한 충돌만 허용하고 겹침이 가능하게 함
    TriggerBox->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap);
}

 

아직 TriggerBox의 충돌 처리를 어떻게 할지 결정하지 않았으므로 충돌 처리를 어떻게 할지 정해준다.

 

NoCollision은 충돌처리X, PhysicsOnly는 물리만 적용, QuetyAndPhysics는 물리와 쿼리(Overlap시 콜백호출) 둘다, QueryOnly는 물리는 무시하지만 쿼리만 처리한다.

 

SetCollisionObjectType의 열거형들은 위와 같다.

 

모든 충돌처리를 무시하면 최적화가 되지만 모두 무시하면 기본적으로 겹침 이벤트가 발생하지 않는다.

하지만 바로 아래 코드를 통해 개별적으로 충돌 처리를 지정할 수 있다.

 

 

위에서 작성한 코드는 블루프린트의 디테일 탭에서도 조절할 수 있다.

참고로 Generate Overlap Events가 체크되어있어야 겹침 이벤트가 일어난다.

 

 

 

FloorSwitch라는 블루프린트 클래스로 만들어주고 메시에 큐브를 적용시키고 적당히 조정한다.

 

 

이제 월드에 배치 후 캐릭터와 겹치면 로그가 잘 뜬다.

 

추가로 실습에서 언급하지는 않았지만 트리거로 사용하는 액터는 동적 바인딩으로 이벤트 발생 시에만 콜백이 호출되기 때문에 Tick이 필요가 없다.

생성자의 bCanEverTick을 false로 설정해주고 틱 함수도 날려버리면 된다.

 

[43. Floor Switch #2]

 

// FloorSwitch.h
public:
    UFUNCTION(BlueprintImplementableEvent, Category = "Floor Switch")
    void RaiseDoor();

    UFUNCTION(BlueprintImplementableEvent, Category = "Floor Switch")
    void LowerDoor();
    
// FloorSwitch.cpp
void AFloorSwitch::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    UE_LOG(LogTemp, Warning, TEXT("Overlap Begin."));
    RaiseDoor();
}

void AFloorSwitch::OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
    UE_LOG(LogTemp, Warning, TEXT("Overlap End."));
    LowerDoor();
}

 

눈여겨 볼 점은 RaiseDoor와 LowerDoor의 구현부가 따로 없다는 것이다.

선언 시 UFUNTION의 속성으로 BlueprintInplementableEvent를 지정해주면 C++ 코드로 구현이 불가능하고 블루프린트에서만 구현이 가능해진다.

 

기능이 직관적이다. Door(StaticMeshComponent)의 월드 좌표를 받아와서 x, y, z 세 성분으로 쪼갠 후 z축만 450 더한 후 다시 벡터로 조립해 해당 벡터값으로 월드 좌표를 설정하는 것이다.

 

 

작동 방식을 확인했으니 구조 개선을 위해 그래프를 모두 지우고 C++ 코드로 넘어간다.

 

// FloorSwitch.h
public:
    /* Initial location for the door */
    UPROPERTY(BlueprintReadWrite, Category = "Floor Switch")
    FVector InitialDoorLocation;

    /* Initial location for the floor switch */
    UPROPERTY(BlueprintReadWrite, Category = "Floor Switch")
    FVector InitialSwitchLocation;

public:
    UFUNCTION(BlueprintImplementableEvent, Category = "Floor Switch")
    void RaiseFloorSwitch();

    UFUNCTION(BlueprintImplementableEvent, Category = "Floor Switch")
    void LowerFloorSwitch();
    
// FloorSwitch.cpp
void AFloorSwitch::BeginPlay()
{
    InitialDoorLocation = Door->GetComponentLocation();
    InitialSwitchLocation = FloorSwitch->GetComponentLocation();
}

void AFloorSwitch::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    UE_LOG(LogTemp, Warning, TEXT("Overlap Begin."));
    RaiseDoor();
    LowerFloorSwitch();
}

void AFloorSwitch::OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
    UE_LOG(LogTemp, Warning, TEXT("Overlap End."));
    LowerDoor();
    RaiseFloorSwitch();
}

 

위의 블루프린트에서 작성했던 내용은 모두 지우고 타임라인을 사용해보도록 한다.

 

 

더블클릭 해서 들어가면 해당 창이 뜨게된다.

좌상단의 이름을 DoorElevation으로 지정한다.

Shift를 누른 상태로 클릭하면 키 프레임이 설정 된다. 2개를 만들어주고 [0초, 0] [0.75초, 450] 으로 설정한다.

키프레임 두개를 선택하고 [우클릭-자동]을 누르면 곡선으로 만들어진다.

 

밖으로 나오면 Door Elevation이 생성된 것을 볼 수 있다.

 

LowerDoor는 역재생을 하면 되므로 Reverse에 연결시켜 준다.

아직 UpdateDoorLocation을 구현하지 않았으니 C++로 돌아간다.

 

 

// FloorSwitch.h
public:
    UFUNCTION(BlueprintCallable, Category = "Floor Switch")
    void UpdateDoorLocation(float Z);

    UFUNCTION(BlueprintCallable, Category = "Floor Switch")
    void UpdateFloorSwitchLocation(float Z);
    
// FloorSwitch.cpp
void AFloorSwitch::UpdateDoorLocation(float Z)
{
    FVector NewLocation = InitialDoorLocation;
    NewLocation.Z += Z;

    Door->SetWorldLocation(NewLocation);
}

void AFloorSwitch::UpdateFloorSwitchLocation(float Z)
{
    FVector NewLocation = InitialSwitchLocation;
    NewLocation.Z += Z;

    FloorSwitch->SetWorldLocation(NewLocation);
}

 

뒤에 쓸 FloorSwitch까지 같이 구현해준다.

이번에는 BlueprintCallable 속성을 지정해주었기 때문에 구현을 해야 한다.

 

 

FloorSwitchTimeline은 [0초, 0] [0.75초, 12] 로 지정해준다.

OnOverlap이 발생하면 타임라인으로 만들어준 보간값이 UpdateDoorLocation에 전달되어서 Z축으로 부드럽게 이동하게 된다.

 

 

[44. Floor Switch #3]

 

트리거가 발동되었을 때, 해당 트리거에서 벗어나도 문이 바로 닫히지 않고 잠시 유지된 후 일정 시간이 지나고 닫히는 문을 구현하려고 한다.

TimerManager를 이용해서 구현해보도록 한다.

 

// FloorSwitch.h
public:
    FTimerHandle SwitchHandle;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Floor Switch")
    float SwitchTime;

    void CloseDoor();
    
// FloorSwitch.cpp
AFloorSwitch::AFloorSwitch()
{
    SwitchTime = 2.f;
}

void AFloorSwitch::OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
    UE_LOG(LogTemp, Warning, TEXT("Overlap End."));
    // 매개변수(타이머 핸들, 대상 객체, 콜백함수, 지연시간)
    GetWorldTimerManager().SetTimer(SwitchHandle, this, &AFloorSwitch::CloseDoor, SwitchTime);
}

void AFloorSwitch::CloseDoor()
{
    LowerDoor();
    RaiseFloorSwitch();
}

 

OnOverlapEnd에 있던 부분을 CloseDoor로 옮기고 대신 타이머 매니저의 SetTimer를 통해 일정 시간(2초)뒤에 CloseDoor가 호출되게 변경하였다.

 

다만 여기서 문제점이 하나 있다. OnOverlapEnd 이벤트가 발생할때마다 CloseDoor 등록되어 2초 뒤에 호출되기 때문에 몇번 밟았다 떼고 트리거 위에 올라가면 CloseDoor가 실행된다.

이것은 우리가 의도하지 않은 결과이다.

 

간단하게 해결하는 방법 중 하나는 bool을 이용해 캐릭터가 트리거 위에 올라가 있는지 여부를 판별해서 실행하는 것이다.

 

 

// FloorSwitch.h
public:
    bool bCharacterOnSwitch;
    
// FloorSwitch.cpp
AFloorSwitch::AFloorSwitch()
{
    bCharacterOnSwitch = false;
}

void AFloorSwitch::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    UE_LOG(LogTemp, Warning, TEXT("Overlap Begin."));
    if (!bCharacterOnSwitch) bCharacterOnSwitch = true;
    RaiseDoor();
    LowerFloorSwitch();
}

void AFloorSwitch::OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
    UE_LOG(LogTemp, Warning, TEXT("Overlap End."));
    if (bCharacterOnSwitch) bCharacterOnSwitch = false;
    GetWorldTimerManager().SetTimer(SwitchHandle, this, &AFloorSwitch::CloseDoor, SwitchTime);
}

void AFloorSwitch::CloseDoor()
{
    if (!bCharacterOnSwitch)
    {
        LowerDoor();
        RaiseFloorSwitch();
        bCharacterOnSwitch = false;
    }
}

 

직관적이지만 너무 원시적인 방법이라는 느낌을 지울 수가 없다.

우선은 넘어가도록 한다.

 

메시도 그럴싸하게 바꿔준 뒤 다시 테스트를 해보면 이제는 여러번 오르락 내리락 한 뒤 트리거에 올라서도 문이 내려오지 않게 된다.

 

[정리]

 

  • 트리거 이벤트를 처리할 때는 동적 멀티캐스팅 델리게이트를 사용한다. 매개변수 형식은 미리 정해져 있으니 확인 후 똑같이 맞춰주면 된다. 콜리전 설정도 별도로 해준다.
  • UFUNCTION의 BlueprintImplementableEvent 속성을 통해 구현을 블루프린트로 떠넘길 수 있다.
  • 블루프린트의 타임라인을 이용해서 문의 자연스러운 움직임을 쉽게 구현할 수 있다.
  • 일정시간 뒤에 함수를 실행하려면 GetWorldTimerManager().SetTimer()를 사용하자.

'언리얼 엔진 > UE4 C++' 카테고리의 다른 글

[UE C++] Gameplay Mechanics #3  (0) 2022.09.07
[UE C++] Gameplay Mechanics #2  (0) 2022.09.07
[UE C++] The Character Class #2  (0) 2022.09.06
[UE C++] The Character Class #1  (0) 2022.09.05
[UE C++] The Pawn Class #2  (0) 2022.09.05

[39. The Animation Blueprint #1]

 

블렌드 스페이스: 애니메이션을 섞는다.

1D는 축이 한개이다.

 

좌측부터 Idle, Walk, Running을 배치하고 가로 축 이름은 Speed, 최대값은 375로 설정한다.

 

애니메이션 그래프에서는 매 틱마다 무언가를 할 수 있다.

 

AnimInstance를 상속하는 MainAnimInstance C++ 클래스를 만들어준다.

참고로 애니메이션 인스턴스는 틱 함수가 없지만 비슷한 것은 있다.

 

// MainAnimInstance.h
class FIRSTPROJECT_API UMainAnimInstance : public UAnimInstance
{
public:
    virtual void NativeInitializeAnimation() override;

    UFUNCTION(BlueprintCallable, Category = AnimationProperties)
    void UpdateAnimationProperties();

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Movement)
    float MovementSpeed;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Movement)
    bool bIsInAir;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Movement)
    class APawn* Pawn;
};

// MainAnimInstance.cpp
#include "GameFramework/CharacterMovementComponent.h"

void UMainAnimInstance::NativeInitializeAnimation()
{
    if (Pawn == nullptr)
    {
        // 이 애니메이션 인스턴스의 소유자를 가져옴
        Pawn = TryGetPawnOwner();
    }
}

void UMainAnimInstance::UpdateAnimationProperties()
{
    if (Pawn == nullptr)
    {
        Pawn = TryGetPawnOwner();
    }

    if (Pawn)
    {
        FVector Speed = Pawn->GetVelocity();
        FVector LateralSpeed = FVector(Speed.X, Speed.Y, 0.f);

        MovementSpeed = LateralSpeed.Size();

        bIsInAir = Pawn->GetMovementComponent()->IsFalling();
    }
}

 

우선은 코드를 작성하고 끝내도록 한다. 아직 바뀌는건 아무것도 없다.

 

NativeInitializeAnimation은 AnimInstance에 선언 및 정의가 되어있기는 한데 아무것도 구현되어있지 않다.

필요하면 재정의 해서 쓰라고 만들어 둔 듯 하다.

 

[40. The Animation Blueprint #2]

 

MainAnimInstance기반의 애니메이션 블루프린트 클래스를 하나 만들어 준다. 이름은 MainAnim_BP이다.

이번에는 따로 코드를 작성하지 않고 블루프린트로만 애니메이션이 작동하게 만든다.

 

먼저, 이벤트그래프에서 만들어둔 UpdateAnimationProperties 함수를 이벤트에 연결시킨다.

해당 이벤트는 매 틱 실행되어 캐릭터의 속도의 크기를 MovementSpeed에 대입한다.

 

AnimGraph에서 스테이트 머신을 만들고 안으로 들어간다.

 

Idle/Walk/Run이라는 스테이트를 만들어서 엔트리와 연결시켜준다.

다시 Idle/Walk/Run 안으로 들어간다.

 

애셋 브라우저에서 기존에 만들었던 블렌드 스페이스 1D를 가져와서 출력 애니메이션 포즈에 연결시키고 MovementSpeed를 매개변수로 넘겨준다.

 

UpdateAnimationProperties 함수가 매 틱 실행되어 캐릭터의 속도에 따라 MovementSpeed 값이 변하기 때문에 블렌드 스페이스에서 설정한 값(0~375)에 따라 애니메이션 블렌드가 적절하게 이뤄진다.

 

이제 실행 후 캐릭터를 이리저리 움직여보면 어느정도 의도한 대로 애니메이션이 재생된다.

 

[41. The Animation Blueprint #3]

 

점프 애니메이션을 추가할 때이다.

 

스테이트 머신을 위와 같이 설정한다.

JumpStart에는 Jumping_Up, InAir에는 Falling_Idle, JumpEnd에는 Jumping_Down 애니메이션을 연결시켜준다.

이제 현재 상태나 애니메이션 재생 비율에 따라 장면 전환을 정하면 된다.

 

어느정도 의도한 대로 애니메이션 전환이 잘 이뤄진다.

원하는 동작이 복잡해질수록 전환 규칙을 많이 정의해야 한다.

'언리얼 엔진 > UE4 C++' 카테고리의 다른 글

[UE C++] Gameplay Mechanics #2  (0) 2022.09.07
[UE C++] Gameplay Mechanics #1  (0) 2022.09.06
[UE C++] The Character Class #1  (0) 2022.09.05
[UE C++] The Pawn Class #2  (0) 2022.09.05
[UE C++] The Pawn Class #1  (0) 2022.09.04

+ Recent posts