Post List

2017/11/29

Unreal4__3_몬스터추가__충돌처리

이번에는 몬스터를 레벨에 스폰하고, 무기와 몬스터간의 충돌처리를 만들어 보았습니다.

[ 몬스터 스폰 ]
몬스터 스폰에 사용되는 볼륨박스는 게임상에는 보이진 않지만 레벨에 배치되는 형태이기때문에 AActor를 상속받는 클래스로 추가해주어야 합니다.

클래스 이름은 AJSpawnVoulme으로 만들어주면 위와 같이 스폰볼륨 클래스가 추가됩니다. 이제 새로 추가한 클래스를 비쥬얼 스튜디오에서 작업을 해보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
UCLASS()
class JOYLAND_API AJSpawnVolume : public AActor
{
public:
    UFUNCTION(BlueprintPure, Category = "Spawning")
    FVector GetRandomPointInVolume();
private:
    // 몬스터 스폰 위치를 지정할 박스 컴포넌드
    UPROPERTY(VisibleAnyWhere, BlueprintReadOnly, Category="Spawning", meta = (AllowPrivateAccess = "true"))
    class UBoxComponent* WhereToSpawn;
cs
스폰 볼륨 클래스의 멤버변수로 몬스터의 스폰 위치와 범위를 지정할 수 있는 박스 컴포넌트를 추가하였습니다.
UBoxComponent* WhereToSpawn에 해당하며, 에디터상에서 보여지기 위해 UPROPERTY 메크로를 붙여주었습니다.

해당 변수는 private인데 어떻게 에디터에서 접근이 가능하냐인데! 여기에 meta = (AllowPrivateAccess = "true") 를 추가해주면 가능하답니다!

변수위에 보시면 GetRandomPointToVolume()이라는 멤버 함수가 있습니다.
이 함수는 박스안에 몬스터가 스폰할 위치를 랜덤으로 반환해주는 함수입니다.

그럼 다시 에디터로 돌아와서 JSpawnVolume 클래스를 레벨에 드래그해서 놓으면 위와 같이 박스가 놓이게 됩니다.
에디터에서 박스의 크기와 위치를 조정하여 몬스터의 스폰 위치를 지정해줄 수 있습니다.

ps: 여기서! 문제점. WhereToSpawn은 VisibleAnyWhere로 설정되어있습니다.
언리얼 문서에 따르면 VisibleAnyWhere은 에디터에서 수정이 불가능하다고 나오지만 에디터에서
박스의 Transform 변경이 가능했답니다.. 자세한 내용은 아래 블로그를 참고해보자.
VisibleAnyWhere, EditAnyWhere의 차이점
http://lifeisforu.tistory.com/300

다시 비쥬얼스튜디오로 돌아와서 어떤 몬스터들을 레벨에 배치할것인지, 몇마리를 배치할것인지에 대한 추가작업을 하겠습니다.

1
2
3
4
5
6
7
8
9
10
protected:
    UPROPERTY(EditAnywhere, Category="Spawing")
        TSubclassOf<class AJMonster> WhatToSpawn;
    UPROPERTY(EditAnywhere, Category = "Spawing")
        int maxSpawnCount;
private:
    // 스폰된 몬스터들을 관리하는 컨터이너
    TArray<class AJMonster*> SpawnObjects;
cs
코드를 보면 TSubclassOf<class AJMonster> WhatToSpawn 변수는 에디터에서 어떤 몬스터를 스폰할지 지정할 변수입니다.

그 아래 int maxSpawnCount는 해당 볼륨에 몇마리의 몬스터를 스폰할지를 지정해주는 변수입니다. 

두 변수 모두 UPROPERTY 메크로를 사용하여 에디터에서 수정하고 값을 입력할 수 있도록 해주었습니다.

그리고 마지막에 TArray<class AJMonster*> SpawnObjects 멤버 변수가 있는데,
이 변수는 스폰볼륨박스안에 스폰된 몬스터들을 저장하는 배열입니다.

이번 시간에 소개할 내용은 아니지만 추후에 몬스터가 죽으면 객체를 지우고 다시 생성하는 방식보다 맨 처음에 스폰했던 객체를 그대로 사용하여, 상태에 따라 레벨에 보여주고 안보여줌을 컨트롤할 수 있도록 배열에 저장하였습니다. 

최대 스폰 카운트에 맞춰서 몬스터 1마리가 죽었을 시 몇초 후 다시 스폰되도록 하기 위함이기도 합니다.

1
2
private:
    void SpawnObject();
cs
이제 몬스터를 스폰하는 코드를 추가해보겠습니다. 몬스터 스폰은 외부에서의 호출로 이루어지는것이 아니라 내부적으로 이루어 지는것이기 떄문에 private로 선언하여 추가하였습니다.

구현부를 보시면,
1
2
3
4
5
6
7
8
9
10
FActorSpawnParameters    SpawnParams;
SpawnParams.Owner                           = this;
SpawnParams.Instigator                       = Instigator;
FVector                   SpawnLocation  = GetRandomPointInVolume();
FRotator                 SpawnRotation;
SpawnRotation.Pitch                        = 0.0f;
SpawnRotation.Yaw                          = 0.0f;
SpawnRotation.Roll                         = 0.0f;
cs
위에 보이는 변수들은 스폰시 전달될 인자들로 각 변수에 대해서 알아보겠습니다.
SpawnLocation은 스폰될 위치로 아까 위에서 보았던 GetRandomPointVolume() 함수를 호출하여, 스폰볼륨박스안에 랜덤된 좌표를 얻어옵니다.

그리고 SpawnRotation은 몬스터의 회전값인데 우선은 회전값을 적용하지 않겠습니다.

1
2
3
AJMonster* const        SpawnMon        = World->SpawnActor<AJMonster>(WhatToSpawn, SpawnLocation, SpawnRotation, SpawnParams);
cs
인자들을 넘겨주면 스폰된 몬스터의 주소값을 넘깁니다. nullptr을 반환할 경우에는 당연히 몬스터 스폰에 실패한 상황일 테고,
nullptr이 아닐경우에는 이 메모리값을 따로 보관하지 않아도 레벨에 보여지기는 하지만 저는 최초 생성된 몬스터를 가지고 쭈욱 사용할 예정이라 반환된 객체의 값들을 SpawnObjects 배열에 추가하겠습니다. 

우선 몬스터가 스폰되는것을 확인해야하기 때문에 SpawnVolume::Begin() 함수에서 방금 추가한 SpawnObjects()를 호출하도록 하겠습니다.

그럼 빌드를 하고 에디터로 돌아가서 레벨에 배치된 스폰볼륨박스를 클릭하면 아까 코드로 추가했던 What ToSpawn과 Max Spawn Count 변수가 에디터상에 나타난게 보이실겁니다.

그러면 What to Spawn은 이전에 작업했던 몬스터 블루프린터로 지정해주고,
Max Spawn Count는 4마리로 지정해보겠습니다. 

그리고 게임을 실행하면 아래와 같이 스폰볼륨박스안에 4마리가 스폰이 된 것을 확인할 수 있습니다.

* 여기서 런타임중 콜리젼 박스를 보고싶을 때는 키보드에서 ` 를 누르면 명령어 입력창이 나타납니다. 그러면 show collision 이라고 입력해주면 아래와 같이 실행중 콜리젼 박스가 보이게 됩니다.

[ 충돌 처리 ]
플레이어의 무기와 몬스터의 콜리전박스간의 충돌여부를 판정하여 몬스터에게 데미지를 입힐 수 있도록 하였는데요,
우선 AJWeapon클래스에 USphereComponent타입으로 무기에 구 형태의 콜리전 컴포넌트를 추가하였습니다.
1
2
3
protected:
    UPROPERTY(VisibleAnyWhere, Category = "Collision")
    class USphereComponent*        m_pCollision;
cs

1
2
3
4
5
6
7
8
9
10
11
12
// class JOYLAND_API AJArrow : public AJWeapon
AJArrow::AJArrow()
{
     // 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;
    // 화살 콜리전 생성
    m_pCollision = CreateDefaultSubobject<USphereComponent>(TEXT("ArrowCollision"));
    m_pCollision->InitSphereRadius(5.0f);
    RootComponent = m_pCollision;
cs
 반지름이 5.0의 크기로 지정해주고, 생성된 콜리전을 RootComponent로 지정해줍니다.
RootComponent는 액터의 위치와 회전 크기를 정의하는 컴포넌트랍니다.
추가된 구형태의 콜리전은 몬스터의 콜리전과 충돌 여부를 판정하여 데미지를 입히기 위한 용도로 사용되지요.

다시 컴파일을 하고 에디터로 돌아온 후 AJArrow 클래스를 상속받은 블루프린트를 생성합니다. 생성된 블루프린트를 열면 위와 같이
C++에서 추가했던 콜리전이 나타나게 된답니다.

이제 충돌 판정에 대한 처리를 추가하겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
void AJWeapon::NotifyActorBeginOverlap(AActor* OtherActor)
{
    Super::NotifyActorBeginOverlap(OtherActor);
    if (OtherActor->IsA(AActor::StaticClass()))
    {
        UGameplayStatics::ApplyDamage(OtherActor, 50.0f, nullptr, this, UDamageType::StaticClass());
        // 메세지 출력
        GEngine->AddOnScreenDebugMessage(-12.0f, FColor::Green, "ApplyDamage");
    }
}
cs
NotifyActorBeginOverlap()함수를 추가해줍니다. 이 함수는 Actor의 가상함수이며 액터간 충돌이 일어났을 때 호출이 되며, 인자값으로 충돌된 액터의 주소값이 넘어오게됩니다.
무기와 다른 액터간 충돌이 되었을 때 데미지를 입혀야 하기 때문에 JWeapon클래스에 재정의해서 액터에 데미지를 입히는 코드를 추가하였습니다.

위 함수가 호출되었을 때, OtherActor는 nullptr이 아니어야 하며, 무기를 소유한 폰이 아니어야 합니다. 그래야 플레이어의 콜리전와 플레이어가 들고있는 무기의 콜리전끼리 충돌이 일어났을 때 데미지가 들어가면 안되기 떄문이지요.

1
 UGameplayStatics::ApplyDamage(OtherActor, 50.0f, nullptr, this, UDamageType::StaticClass());
cs
몬스터에게 데미지를 적용해보겠습니다.
저는 ApplyDamage()함수를 사용하였는데 이 함수는 지정된 액터에 데미지를 입히는 함수입니다. 이 함수를 호출하게 되면 데미지를 입는 액터의 TakeDamage()함수가 호출됩니다.

ApplyDamage에 전달되는 인자는 아래 링크를 참조하시면 되겠습니다.
https://docs.unrealengine.com/latest/INT/BlueprintAPI/Game/Damage/ApplyDamage/

1
2
3
4
// AJCharacter.h
    virtual float TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
    virtual void OnHit(float DamageTaken, struct FDamageEvent const& DamageEvent, class APawn* PwanInstigator, class AActor* DamageCauser);
    virtual void Die(float killingDamage, struct FDamageEvent const& DamageEvent, AController* killer, AActor* DamageCauser);
cs
몬스터이든 플레이어든 데미지를 받는건 동일하기 때문에 JCharacter 클래스에 TakeDamage() 함수를 재정의하여, 데미지 적용에 따른 Hit및 Die 상태처리를 추가했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
float AJCharacter::TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
    if (fCurrentHealth <= 0.f)
        return 0.f;
    const float ActualDamage = Super::TakeDamage(Damage, DamageEvent, EventInstigator, DamageCauser);
    if (ActualDamage > 0.f)
    {
        fCurrentHealth -= ActualDamage;
        m_isDamage = true;
    }
    else
    {
        m_isDamage = false;
    }
    if (fCurrentHealth <= 0.f)
    {
        Die(ActualDamage, DamageEvent, EventInstigator, DamageCauser);
    }
    else
    {
        OnHit(ActualDamage, DamageEvent, EventInstigator ? EventInstigator->GetPawn() : nullptr, DamageCauser);
    }
cs
데미지를 받았을 때 m_isDamage값을 true로 설정해주고, 현재 HP에 따라 Hit처리할지 Die처리할지에 대한 구문입니다.

m_isDamage값은 에디터에서 읽어올 수 있도록 메크로를 사용하여 확장된 변수입니다.
이 값에 따라 데미지 모션을 보여줄지에 대한 판단이 이루어집니다.

에디터로 돌아와서 JMonster의 애니메이션 블루프린트를 열어줍니다.
m_isDamage는 JCharacter의 멤버변수이기 때문에, JCharacter이나 JMonster로 형변환을 해야만 읽어올 수 있는 데이터입니다. m_isDamage값을 가져와 에디터 변수 isDamage에 Set해주는 노드를 추가해 줍니다.


이벤트 그래프로 와서 State를 추가해주었습니다. 이름은 Damage로 변경해주고, idle_walk_run State에서 회색 테두리를 클릭한 채 Damage State로 드래그해 노드를 연결해 줍니다. 그러면 위와같이 [idle_walk_run]->[damage] 이런식으로 연결이 되지요.


화살표를 더블클릭하면 새로운 창이 열리는데 아까 위에서 추가했던 isDamage 변수 노드를 추가하여 Can Enter Transition 결과 노드에 연결해줍니다. 그러면 'isDamage가 true일 때 Damage State에 지정된 애니메이션을 플레이 시켜라'가 됩니다.
다시 애님 그래프로 돌아와 Damage 노드를 더블클릭하면, 어떤 애니메이션을 플레이 시킬지에 대한 추가 작업을 할 수 있는 창이 뜨게 되지요.

여기서 오른쪽 하단을 보시면 애니메이션 리스트가 쭈욱 나옵니다.
플레이 시킬 애니메이션을 드래그해 화면에 놓으면 노드가 하나 생성되고 이 생성된 노드를 Final Animation Pose노드의 Result 부분에 연결시켜줍니다.

그러면 isDamage가 true일때 연결된 애니메이션이 플레이가 된답니다.

설명은 간략하고 생략된 부분들도 많지만 핵심적인 부분만 소개 해드렸으며,
구현에 따르면 위와 같은 동작을 보실 수 있습니다!.