MyPawn 클래스의 생성이 성공적으로 끝났다면, 게임이 시작되었을 때 MyPawn이 자동으로 플레이어의 입력에 반응하도록 설정해보자. Pawn 클래스에는 초기화 중에 자동으로 플레이어의 입력에 반응하도록 설정해주는 변수를 제공한다. MyPawn.cpp의 AMyPawn::AMyPawn() 생성자를 다음과 같이 수정하자.
AMyPawn::AMyPawn() { // Set this pawn to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true;
AutoPossessPlayer = EAutoReceiveInput::Player0; }
컴포넌트의 기록 유지를 위해서[각주:2] 다음의 코드를 MyPawn.h 의 클래스 정의 하단부에 추가하자.
그리고 MyPawn.cpp로 돌아와서 폰에 카메라를 붙이고 위치와 회전을 설정하기 위해 다음과 같이 코드를 수정한다.
AMyPawn::AMyPawn() { // Set this pawn to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true;
게임에서 특정한 키를 눌렀을 때, 특정 동작을 하도록 만드는 것을 언리얼에서는 입력 매핑이라고 한다. 이러한 입력 매핑에는 두 가지 종류가 있다.
액션 매핑(Action Mapping) - 마우스나 조이스틱, 패드, 키보드 버튼처럼 누르거나, 떼거나, 더블 클릭하거나, 특정 시간동안 누르고 있을 때 보고한다. 점프, 공격, 상호작용 등이 액션 매핑의 예시이며, X를 눌러서 조이를 표하는 것도 액션 매핑에 속한다.
축 매핑(Axis Mapping) - 연속적인 것으로 마우스의 위치나 조이스틱 막대의 기울기 같은 것으로 "일정량"의 입력으로 생각하면 된다. 움직이지 않더라도 매 프레임 값을 보고한다. 걷기, 달리기, 둘러보기, 탈 것의 방향조절 같은 것들이 주로 축 매핑으로 처리된다.
코드에서도 직접 입력 매핑을 할 수 있지만, 일반적으로는 에디터에서 정의하는 경우가 많으니, 이 튜토리얼에서는 그 방식을 따른다.
1. 언리얼 엔진 에디터에서 편집 드롭다운 메뉴에서 프로젝트 세팅 옵션을 선택한다.
2. 왼쪽의 엔진 섹션의 입력 항목을 선택하고 바인딩(Binding) 카테고리에 다음과 같이 하나의 액션 매핑과 두 개의 축 매핑을 추가한다.
3. 입력 환경 설정이 모두 끝났다면, 레벨에 MyPawn을 배치한다. 콘텐츠 브라우저에 있는 MyPawn 클래스를 레벨 에디터에 끌어다 놓으면 된다.
4. 레벨에 MyPawn을 배치한 뒤에는, 우리가 배치한 Pawn이 움직이는 것을 볼 수 있게 하기 위해서 OurVisibleComponent의 스태틱 메시(Static Mesh) 카테고리에 "Shape_Cylinder"를 넣어야 한다고 언리얼 튜토리얼 문서에 나와있다.
하지만 우리가 배치한 MyPawn의 OurVisibleComponent에서는 스태틱 메시 카테고리가 보이지 않는 것을 알 수 있다.
이 문제의 원인을 추측해보자면 언리얼 튜토리얼의 예시 코드에는 CreateDefaultSubobject() 함수로 컴포넌트를 생성할 때, 명시적인 컴포넌트 타입이 없었기 때문에 헤더에 추가한 OurVisibleComponent의 타입에 맞춰서 USceneComponent로 생성했기 때문에 발생한 문제로 보인다.
그렇다면 스태틱 메시 카테고리가 나오도록 하려면 어떻게 해야할까? 바로 CreateDefaultSubobject() 함수로 UStaticMeshComponent를 생성해서 OurVisibleComponent에 대입시켜 주면 될 것 같다. 언리얼 엔진 문서에 따르면 UStaticMeshComponent는 USceneComponent를 상속받고 있기 때문에 충분히 가능한 코드이다. 여기까지 유추했다면 코드를 다음과 같이 수정해보자.
UStaticMeshComponent가 USceneComponent를 상속받고 있기 때문에 충분히 대입이 가능할거라고 생각했는데 할당할 수 없다는 에러가 발생한다.
이 경우는 타이머를 배울 때, GetWorldTimerManager() 함수를 호출해서 기능을 사용하려고 했을 때를 생각해보자. 그 때 불완전한 형식은 사용할 수 없다는 에러가 떴었던 것과 그 문제를 해결하기 위해서 "TimerManager.h"를 포함시켜주었던 것을 기억할 수 있다.
그와 같이 MyPawn.cpp의 헤더 포함 전처리기 부분에 "Engine/Classes/Components/StaticMeshComponent.h"를 포함시키면 CreateDefaultSubobject()로 생성한 UStaticMeshComponent가 성공적으로 OurVisibleComponent에 대입되는 것을 확인할 수 있다.
// Fill out your copyright notice in the Description page of Project Settings.
축 입력 매핑에 대한 동작을 구현할 때, FMath::Clamp()함수를 사용했는데 이것은 입력된 값이 -1.0과 1.0 사이를 벗어나지 않도록 만들어 준다. 전 파트에서 우리가 축 매핑을 추가할 때, MoveX의 입력을 W와 S만을 추가했는데 만약 다른 입력 방식도 사용하기 위해서 위쪽 화살표와 아래쪽 화살표로도 MoveX 입력을 받도록 만들었을 때, 만약 Clamp로 입력의 범위를 제한하지 않았다면 W와 위쪽 화살표를 동시에 누른다면 캐릭터가 두 배의 속도로 빠르게 움직이는 버그가 발생할 것이다.
입력 함수의 정의와 구현을 모두 끝냈으니, 적합한 입력에 반응하도록 바인딩을 진행할 차례다. AMyPawn::SetupPlayerInputComponent() 함수 안에 다음 코드를 작성하자.
// Called to bind functionality to input void AMyPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) { Super::SetupPlayerInputComponent(PlayerInputComponent);
처음부터 끝까지 설계가 완벽하고 수정할 일이 없다면 그럴 일이 없겠지만, 코드 작업을 하다보면 기존에 있던 클래스를 삭제해야하는 일이 가끔 발생한다. 특히 아직 프로토타입 작업을 하는 과정이라면 작성해둔 클래스가 필요없어져서 삭제해야하는 일이 생각보다 자주 발생할 수 있다.
하지만 위의 이미지와 같이 간단하게 삭제할 수 있는 블루프린트 클래스와 달리 C++ 클래스는 에디터 내에서 삭제할 수 있는 방법이 존재하지 않는다. 그렇다고 더이상 사용하지 않게된 C++ 클래스를 무작정 쌓아두고 있을 수만은 없는 법이다.
1. 필요 없어진 C++ 클래스를 삭제하기 전에 에디터를 닫는다.
2. 비주얼 스튜디오로 가서 솔루션 탐색기에서 지우고자 하는 클래스의 헤더(.h)와 소스파일(.cpp)를 선택한 뒤 제거한다.
3. 프로젝트 폴더의 Source 폴더 안에 남아있는 클래스의 헤더(.h)와 소스파일(.cpp) 역시 삭제해준다.
4. 비주얼 스튜디오로 돌아가서 [빌드 > 솔루션 다시 빌드]를 선택해서 프로젝트를 다시 빌드한다.
5. 프로젝트 빌드가 성공적으로 끝났다면 에디터를 다시 실행시킨다. 그렇게 하고 콘텐츠 브라우저를 보면 필요없는 C++ 클래스가 성공적으로 삭제된 것을 확인할 수 있다.
주의사항
블루프린트 클래스는 관련되어 있거나 레퍼런스가 있는 상태라면 삭제하기 전에 경고창을 띄워주고 정말로 삭제할 것인지 확인을 하지만, C++ 클래스는 그런 과정이 없기 때문에 지우고자하는 클래스가 레벨에 배치되어있는지, 다른 곳에서의 레퍼런스가 있는지, 또는 다른 클래스에서 헤더를 포함시켜서 사용하고 있는 것은 아닌지 신중하게 확인하고 삭제하는 것이 좋다.
또한 필요없어진 C++ 클래스를 삭제함으로서 신텍스 에러가 발생한다면 4번 과정에서 프로젝트를 리빌드가 실패하게 될 것이다. 그렇기 때문에 클래스를 삭제한 뒤에 오류목록을 살펴서 클래스를 삭제한 여파로 발생한 에러가 없는지 확인하는 과정 역시 필요하다.
구조체는 기존에 존재는 데이터 타입을 조합하여 새로운 데이터 타입을 만들어내는 유용한 개념이다.
struct UserDefinedStruct
{
public:
int i;
float f;
};
일반적인 C++ 프로젝트에서는 구조체를 위와 같이 정의하고 사용하게 된다.
하지만 언리얼 엔진 프로젝트에서 이러한 정규 구조체는 C++ 코드 내부에서는 사용될 수 있지만, 에디터의 디테일 패널에 노출되지 않고, 블루프린트에서도 사용이 불가능하다.
에디터에서 사용가능한 구조체를 만들고자 한다면 언리얼 구조체 즉, USTRUCT를 만들어야 한다.
블루프린트에서만 사용할 구조체라면 위의 이미지와 같은 방법으로 구조체를 생성할 수 있는데, 블루프린트 구조체는 C++ 코드에서는 사용할 수 없다. 하지만 C++ 코드에서 만든 구조체는 C++ 코드는 물론 블루프린트에서도 사용할 수 있다는 장점이 있다.
C++ 언리얼 구조체는 간단한 블루프린트 구조체 생성 방법과 비교했을 때, 엔진 내부에서 명시적인 생성 방법이 없기 때문에 생성 과정이 조금 복잡하다.
언리얼 구조체 만들기
언리얼 구조체를 만드는 과정을 따라가보도록 하자.
우선 사용자가 정의한 UStruct를 담을 헤더를 만들어야 한다. 만약 구조체가 특정한 클래스에서만 자주 사용될 것이라면 그 클래스의 헤더 파일 하단에 구조체를 정의하는 편이 좋지만, 범용적으로 여러 곳에서 사용될 구조체라면 사용자가 정의한 헤더에 몰아서 정의하는 편이 좋다.
CustomStruct00 클래스의 추가가 끝났다면 아래의 예시 코드와 같이 클래스 정의 아래 쪽에 커스텀 구조체를 정의해보자.
// Fill out your copyright notice in the Description page of Project Settings.
UCLASS() class CUSTOMSTRUCTTEST_API ACustomStruct00 : public AActor { GENERATED_BODY() public: // Sets default values for this actor's properties ACustomStruct00();
protected: // Called when the game starts or when spawned virtual void BeginPlay() override;
public: // Called every frame virtual void Tick(float DeltaTime) override; };
클래스에는 UCLASS() 매크로가 붙지만 구조체의 경우에는 USTRUCT() 매크로가 붙는다. 그리고 구조체 지정자는 Atomic과 BlueprintType으로 지정해뒀는데 Atomic은 이 구조체가 항상 하나의 단위로 직렬화(Serialize)됨을 의미하고 BlueprintType은 이 구조체가 블루프린트에서 사용될 수 있음을 의미한다.
만약 이 구조체가 에디터의 디테일 창에서 표시되고 수정 가능하기만 원한다면 지정자를 Atomic으로만 설정하기를 권한다. 또한 모든 멤버 변수의 UPROPERTY() 매크로의 지정자를 EditAnywhere로 설정해야 한다.
혹은 구조체가 디테일 창에서는 보이지 않고 코드 내부나 블루프린트에서만 사용되기를 원한다면 USTRUCT() 매크로의 지정자를 BlueprintType으로, UPROPERTY() 매크로의 지정자를 BlueprintReadWrite로 설정해야 한다.
그리고 구조체의 이름은 F로 시작되게 작성해야 하며, 댕글링(Dangling) 포인터 문제에 대해서 보호받기 위해서 구조체의 모든 멤버 변수들에 UPROPERTY() 매크로를 붙이는 것을 권장한다.
또한 구조체의 멤버 변수에 포인터를 사용한다면 깊은 복사 얕은 복사 문제에 주의를 기울여야 한다.
사용할 구조체를 모두 정의했다면, 이 구조체를 사용할 코드의 헤더에 CustomStruct00.h를 포함시켜준다.
// Fill out your copyright notice in the Description page of Project Settings.
UCLASS() class CUSTOMSTRUCTTEST_API ATestActor : public AActor { GENERATED_BODY() public: // Sets default values for this actor's properties ATestActor();
protected: // Called when the game starts or when spawned virtual void BeginPlay() override;
public: // Called every frame virtual void Tick(float DeltaTime) override;
UPROPERTY(EditAnywhere) FCustomStruct st; };
이렇게 구조체를 테스트 액터의 멤버 변수로 추가시킨 후 에디터로 돌아가서 컴파일을 해주고, 액터를 레벨에 배치하고 선택해보면 위의 이미지처럼 구조체가 디테일 패널에서 수정가능하록 노출된 것을 확인할 수 있다.
Tip :: 이후에 구조체의 멤버 변수 종류를 수정하고 컴파일했을 때, 디테일 패널에 곧바로 적용이 되지 않는 문제가 가끔있는데 이런 경우 해당 구조체를 가진 클래스의 멤버 변수에 임시 변수 하나를 추가하고 컴파일하면 적용이 된다.
생성한 구조체 블루프린트에서 사용하기(Use Custom Struct at Blueprint)
C++ 코드에서 정의한 구조체를 블루프린트에서 사용하는 방법은 간단하다.
블루프린트에서 정의한 CustomStruct 변수 유형으로 변수를 추가할 수 있다.
이미지와 같이 이벤트 그래프에서 우클릭을 한 뒤 정의한 구조체의 이름을 검색하면 이벤트 플로우 도중에 CustomStruct를 만들거나 구조체를 분해해서 구조체의 변수를 따로 뽑아내서 사용할 수도 있다.