static 키워드는 변수나 함수, 클래스에 정적 속성을 부여하는 것으로 클래스로부터 객체를 생성하지 않고 변수나 함수를 호출할 수 있도록 해주는 것이다.
정적 변수
public class StaticTestClass
{
public static int score;
}
정적 변수를 선언하기 위해서는 위의 예시 코드와 같이 static 키워드를 붙여서 변수를 정의하면 된다. 이렇게 선언한 정적 변수는 클래스로부터 객체를 생성하지 않아도 [클래스명.변수이름]의 형식으로 곧바로 사용할 수 있게 된다.
public class MainClass
{
public void Main()
{
StaticTestClass.score = 10;
}
}
클래스의 일반 멤버 변수는 클래스의 객체가 생성될 때, 각 객체마다 따로 생기지만, 정적 변수는 해당 클래스가 처음으로 사용되는 때에 한 번만 초기화되어 계속 동일한 메모리를 사용하게 된다.
도식으로 보면 위의 그림과 같다. 정적 변수를 포함한 클래스 A의 객체를 두 개를 생성하여 각 이름을 object1, object2라고 했을 때, 각 인스턴스에는 정적 변수가 포함되지 않으며, 일반 멤버 변수만 포함된다. 클래스 A의 정적 변수는 클래스 A가 처음 사용되는 시점에 별도의 메모리 공간에 할당된다.
생성된 객체에 정적 변수가 포함되지 않는 것은 실제로 객체를 생성해서 멤버 변수를 찾았을 때, 목록에 나오지 않는 것을 보면 확인할 수 있다.
정적 함수
public class StaticTestClass
{
public static int score;
public int memberInt;
public static void StaticFunction()
{
score = 10; // static 변수는 호출할 수 있다.
memberInt = 10; // static 함수 내에서 멤버변수는 호출할 수 없다.
}
}
public class MainClass { public void Main() { StaticTestClass.score = 10; StaticTestClass.StaticFunction(); } }
함수를 선언할 때, static 키워드를 붙여서 함수를 정의하면 정적 함수를 만들 수 있다. 이 정적 함수 역시 [클래스명.함수이름]의 형식으로 객체를 생성하지 않고 곧바로 호출할 수 있다.
단, 정적 함수는 객체가 생성되기 전에 호출이 가능하기 때문에, 정적 함수 내에서는 정적 변수가 아닌 일반 멤버 변수를 호출할 수 없다.
정적 클래스
public static class StaticTestClass
{
public static int score;
static StaticTestClass()
{
score = 10;
}
public static void StaticFunction()
{
score = 20;
}
}
정적 클래스는 모든 멤버가 정적 변수 혹은 정적 함수로 이루어진 것으로 객체를 생성할 수 없는 클래스이다. 모든 정적 멤버 변수 및 정적 멤버 함수는 [클래스명.변수이름] 혹은 [클래스명.함수이름]으로 호출된다.
정적 클래스는 정적 생성자를 가질 수 있는데 이 정적 생성자는 public, protected, private 등의 액세스 한정자를 사용할 수 없으며, 매개변수 역시 가질 수 없다.
[유니티 어필리에이트 프로그램]
아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.
게임을 제작하는 과정에서 객체를 초기화할 때, 프로젝트에 포함된 다른 클래스나 오브젝트, 리소스를 가져와야하는 경우가 종종 생긴다.
그런 경우 블루프린트의 이벤트 그래프에서 작업하는 경우라면 위의 이미지와 같이 콘텐츠 브라우저에 있는 리소스나 블루프린트 클래스 등을 곧바로 선택할 수 있지만, C++ 코드에서는 직접 경로를 지정해서 코드를 작성해야 한다.
단, C++ 코드에서 직접 경로를 지정해서 리소스나 블루프린트 클래스를 가져올 때, 주의할 점은 리소스나 블루프린트 클래스의 경로나 파일명이 자주 바뀌는 상황을 피하는 게 좋다. 경로를 지정한 이후에 경로가 바뀌지 않을 것이 확실하다면 C++ 코드로 경로를 지정해서 가져오는게 낫겠지만 자주 바뀌는 상황이라면 바뀐 리소스를 불러오는 모든 코드를 일일이 찾아서 수정하고 컴파일하는 문제가 발생한다.
그렇기 때문에, 경로나 리소스의 파일명이 자주 바뀔 상황이라면 위의 이미지처럼 블루프린트를 이용해서 초기화를 진행하거나, 별도의 기능을 만들어서 일일이 경로를 지정하고 바꾸는 작업을 자동화시키는 것이 좋다.
우선 C++ 코드에서 콘텐츠 브라우저의 리소스나 블루프린트 클래스를 가져오기 위해서는 다음의 헤더를 전처리기로 포함시켜주어야 한다.
#include "UObject/ConstructorHelpers.h"
ConstructorHelpers는 생성자에 도움을 주는 클래스로 생성자에서 콘텐츠 브라우저의 리소스나 블루프린트 클래스를 불러오는 작업을 도와주는 기능들을 가지고 있다. ConstructorHelpers는 생성자에서 사용되는 기능이기 때문에 생성자 이외의 장소에서 ConstructorHelpers를 사용하려고 시도하면 컴파일 에러가 발생하게 된다.
C++ 코드에서 블루프린트 클래스 가져오기
콘텐츠 브라우저 패널에 Blueprints 폴더 안에 TestBlueprintClass라는 이름의 APawn 클래스를 상속받은 블루프린트 클래스가 있다고 가정할 때, 그것을 C++ 코드에 가져오기 위해서는 다음 예시와 같이 코드를 작성하면 된다.
static ConstructorHelpers::FClassFinder<APawn> BPClass(TEXT("/Game/Blueprints/TestBlueprintClass")); if (BPClass.Succeeded() && BPClass.Class != NULL) { // 가져온 BPClass.Class를 통한 작업 }
FString 경로를 통해서 불러오는 것이니 만큼, 오타나 변경된 경로나 파일명으로 인해서, 클래스가 제대로 불러와지지 않는 경우가 발생할 수 있기 때문에, Succeeded() 함수와 Class의 NULL 체크를 통해서 성공적으로 클래스가 불러와졌는지 체크하고 사용해야 한다.
클래스 탐색자(Class Finder)는 성공적으로 블루프린트 클래스를 가져온 경우, Class 멤버 변수 안에 TSubclassOf<T> 타입으로 해당 클래스를 가지고 있게 된다. 이것을 이용해서 필요한 작업을 진행하면 된다.
C++ 코드에서 리소스 가져오기
이번에 알아볼 것은 C++ 코드에서 콘텐츠 브라우저 패널의 리소스를 가져오는 과정이다. 리소스의 종류는 여러가지가 될 수 있는데 대표적인 것으로는 스태틱 메시나 텍스처를 예로 들 수 있다.
아래의 예시코드는 드롭된 아이템의 메시가 아이템의 종류에 따라서 달라진다는 가정하에 만들어졌다. 콘텐츠 브라우저의 Item/StaticMesh 폴더 안에 SM_Helmet 이라는 이름을 가진 헬멧 모양의 스태틱 메시가 있을 때, FObjectFinder를 통해서 가져올 수 있다.
FObjectFinder를 통해서 가져온 오브젝트 역시 Succeeded() 함수와 Object 변수의 null 체크를 통해서 리소스가 제대로 불러와졌는지 체크를 한 뒤 사용해야 한다.
C++ 코드에서 C++ 클래스 가져오기
C++ 코드에서 블루프린트 클래스가 아닌 직접 작성한 C++ 클래스를 가져와서 사용하고 싶을 수도 있다. 예를 들어 게임 모드 클래스에서 기본 폰이나 기본 플레이어 컨트롤러를 설정하려고 할 때, C++로 작성한 폰 클래스나 플레이어 컨트롤러 클래스를 기반으로 블루프린트 클래스를 생성해서 넣어주는게 아니라 C++ 클래스를 곧바로 코드에서 넣어주고자 한다면 다음 예시 코드와 같이 작성하면 된다.
게임을 제작할 때 레벨업에 필요한 경험치량이나 스킬의 계수 등 추후에 밸런스 수정 작업이 필요한 값들은 함부로 코드에 상수로 넣어서는 안된다. 이런 부분은 기획자가 손쉽게 접근이 가능해야 하기 때문에, 기획자들이 주로 사용하는 엑셀이나 스프레드시트의 데이터를 언리얼 엔진으로 임포트해서 사용하는 방식을 지원한다. 이것을 데이터 주도형 접근법이라고 한다.
언리얼 엔진에서는 기획자들이 주로 사용하는 엑셀이나 스프레드시트에서 손쉽게 만들어낼 수 있는 .CSV 파일이나 서버 프로그램에서 주로 사용되는 JSON 파일을 손쉽게 임포트하는 기능을 제공한다.
데이터 테이블 임포트
데이터 테이블은 유용한 방식으로 짜여진 표를 의미한다. .CSV 파일을 임포트하기 위해서는 우선 프로그래머가 데이터를 엔진이 인식할 수 있게 Row 컨테이너를 만들어서 엔진에 데이터 해석 방식을 알려줘야 한다.
우리가 예시로 사용할 .CSV 파일은 다음 레벨업까지 필요한 경험치의 양에 대한 것이고 그 내용은 다음과 같다.
이런 컨테이너를 만드는 방법은 두 가지가 있는데 블루프린트를 이용하는 방식과 C++ 코드를 통해 만드는 방식이 있다.
블루프린트
데이터 테이블 로우를 만들기 위해서는 구조체를 생성해야 한다. 구조체의 이름은 BP_LevelUpTableRow로 한다.
블루프린트 구조체가 생성되면 더블클릭해서 블루프린트 구조체 에디터를 열고 변수를 추가한다. 추가하는 변수의 이름은 ExpToNextLevel과 TotalExp로 각 열의 이름과 순서가 일치해야 한다. 제일 첫 열인 Name은 게임 내에게 각 행에 접근하는 이름이 되는 것으로 따로 변수를 추가하지 않아도 된다.
변수를 모두 추가한 뒤에는 구조체를 저장하고 에디터를 닫는다. 그리고 콘텐츠 브라우저 패널에서 파일 창에 우클릭하여 /Game에 임포트... 를 선택한다.
CSV 파일을 임포트한다.
데이터 테이블 옵션 창이 뜨면 데이터 테이블 행 유형 선택을 방금 추가한 구조체로 설정하고 확인을 누른다.
추가된 데이터 테이블을 열어보면 .CSV 파일의 내용이 훌륭하게 임포트된 것을 확인할 수 있다.
C++ 코드
행 컨테이너를 블루프린트 구조체로 만들 경우, C++ 코드에서는 사용할 수 없다는 단점이 있다. C++ 코드에서 사용하기 위해서는 USTRUCT로 만들어야 되는데 언리얼 구조에 대한 설명은 C++ / USTRUCT 사용자 정의 구조체 만들기 문서에서 참고할 수 있다.
우선 Actor 클래스를 상속받아서 CustomDataTables라는 더미 클래스를 생성한다.
클래스가 생성되면 전처리기와 클래스 선언 사이에 구조체를 선언하는 코드를 추가해준다. 행 컨테이너로 사용되는 구조체는 FTableRowBase를 상속받아야만 한다.
// Fill out your copyright notice in the Description page of Project Settings.
프로그래밍 작업을 할 때 캐스팅, 즉 형 변환은 상당히 중요하다. 특히 여러 클래스가 상속으로 엮여있는 상황이라면 더더욱 중요해진다. 언리얼 엔진에서는 대부분의 클래스가 AActor 클래스를 상속받고 있고, 개발자가 만들어내는 클래스 역시 상당수는 AActor를 상속받게 된다.
C++
그렇기 때문에 몇몇 함수들은 이렇게 메인이 되는 부모 클래스를 매개변수로 받거나 돌려준다. 간단한 예를 들자면 다음 함수가 있다.
NotifyActorBeginOverlap() 함수는 액터의 콜리전에 콜리전을 가진 다른 액터가 들어오기 시작했을 때 호출되는 함수로, 매개변수를 통해서 자신의 콜리전과 접촉한 액터를 알려준다. 언리얼 엔진에서는 레벨에 배치되는 모든 오브젝트는 AActor 클래스를 상속받기 때문에, 콜리전과 접촉한 액터가 어떤 클래스던지 상관없이 무조건 AActor 클래스로 보내주는 것이다.
만약 콜리전 체크를 하는 액터가 겹침 이벤트가 발생할때마다 데미지를 입는 클래스인데 데미지를 입힐 수 있는 클래스가 AProjectile 클래스라고 가정했을 때, 위의 예시 코드처럼 별도의 검사를 하지 않는다면, 액터가 아무 물체에나 스칠 때마다 데미지를 입어버릴 것이다.
그래서 필요한 것이 바로 캐스팅이다. 언리얼 엔진에서는 Cast<T>() 라는 함수로 기본적인 캐스팅을 제공한다.
void AActor::NotifyActorBeginOverlap(AActor* OtherActor)
{
AProjectile* Projectile = Cast<AProjectile>(OtherActor);
if (Projectile)
{
// Damage Process
}
}
바로 위의 예시 코드처럼 캐스팅을 진행하면 된다. 만약 콜리전에 검출된 액터가 AProjectile 클래스가 아니라면 캐스팅에 실패할 것이고 Projectile 변수의 값을 nullptr이 되기 때문에 if문 안으로 진행하지 못해서 Damage Process가 진행되지 않는다.
블루프린트
블루프린트 작업에서도 캐스팅이 가능하다.
블루프린트 컨텍스트 메뉴에서 "형변환"이나, 캐스팅하고자 하는 타입의 클래스 명을 검색하면 해당 클래스로 형변환할 수 있는 노드를 추가할 수 있다.
위의 코드는 틱이 작동하는 동안 bSeeingThrough의 상태에 따라서 다이내믹 머티리얼 인스턴스의 머티리얼 파라미터 "Opacity"를 0에서 1로 만들거나 1에서 0으로 만들고 작동이 끝나면 Tick() 함수의 작동을 멈추게 한다. 참고로 머티리얼 파라미터 "Opacity"는 이후 작업에서 추가한다.
SetShowSeeingThrough() 함수는 bShow 변수를 받아서 액터가 투명해지기 시작하는지 불투명해지기 시작하는지 결정한 뒤 Tick() 함수를 작동시킨다.
코드 작업이 끝났다면 솔루션 탐색기에서 프로젝트를 빌드한 뒤, 에디터로 돌아간다.
투명해지는 만들기
이번에는 SeeingThroughActor가 사용할 머티리얼을 만들 차례이다. Props 폴더 안에 Materials 폴더를 만든 다음, 콘텐츠 브라우저 패널의 파일 창에 우클릭해서 머티리얼을 선택한다. 그리고 생성된 머티리얼의 이름을 M_SeeingThrough로 한다.
머티리얼을 더블클릭해서 머티리얼 에디터를 열고 디테일 패널에서 Material 카테고리의 Blend Mode를 Translucent로 설정한다.
그리고 TextureSamleParameter2D와 ScalarParameter를 추가하고 각각 이름을 Texture와 Opacity로 한다.
그리고 Opacity 파라미터 노드를 선택한 뒤, 디테일 패널에서 Default Value를 1로 설정한다.
그 다음, 적용과 저장을 하고 머티리얼 에디터를 닫는다.
언리얼 에디터로 돌아와서, 콘텐츠 브라우저 패널에서 방금 만든 머티리얼을 우클릭하고 머티리얼 인스턴스 생성을 선택한다.
머티리얼 인스턴스가 만들어지면 더블클릭해서 머티리얼 인스턴스 에디터를 열고 디테일 패널에서 Opacity를 체크해주고 머티리얼 인스턴스를 저장한 뒤, 머티리얼 인스턴스 에디터를 닫는다.
SeeingThroughActor 배치
이제 SeeingThroughActor를 배치할 차례이다. 콘텐츠 브라우저 패널에서 SeeingTroughActor를 찾아서 벽 토대 위에 배치한다.
예를 들어, 데미지 타입을 지정할 수 있는 발사체 클래스를 만든다고 가정했을 때, 다음 코드처럼 UClass 타입의 UPROPERTY를 만들어서 에디터에 노출시킨 뒤, 에디터 작업자에게 이 프로퍼티에 UDamageType의 파생 클래스만 할당해 달라고 한다면 어떻게 될까?
블루프린트 에디터의 디테일 패널에서 Damage Type 프로퍼티에 클래스를 할당하기 위해서 드롭다운 메뉴를 확장해보면 클래스 타입에 상관없이 모든 클래스가 표시되고 있음을 볼 수 있다. 이런 상황에서 UDamageType의 파생 클래스만 할당해달라고 한다면, 낮은 확률으로라도 언젠가는 잘못된 클래스를 할당하는 일이 분명 생길 수 밖에 없다.
이러한 문제를 예방하기 위해서 존재하는 것이 바로 TSubclssOf 클래스이다. 다음 코드와 같이 TSubclassOf<UDamageType>으로 UPROPERTY를 만든다.
그렇게 하면, 블루프린트 창의 디테일 패널에서 Damage Type의 드롭다운 메뉴에서는 UDamageType의 파생 클래스만 표시된다. 이렇게 되면 개발자가 가끔 잘못된 데미지 타입을 골라서 넣는 실수는 할 수 있겠지만, 애초에 잘못된 클래스를 선택하는 문제는 발생하지 않을 것이다.
또한 TSubclassOf 클래스는 이런 UPROPERTY에 대한 안정성 이외에 C++ 수준의 타입 안정성 역시 제공하기 때문에 서로 호환되지 않는 TSubclassOf 타입을 서로 할당하려고 하면 컴파일 오류가 발생하고, UClass 타입을 할당하려고 하면 할당을 수행할 수 있는지 런타임중에 검사한다. 런타임 검사에 실패하면 결과값은 nullptr이 된다.
제대로 따라가기 (8) C++ 프로그래밍 튜토리얼 :: 일인칭 슈팅 C++ 튜토리얼 (3)
작성버전 :: 4.21.0
언리얼 엔진 튜토리얼인 일인칭 슈팅 C++ 튜토리얼에서는 C++ 코드 작업을 통해서 기본적인 일인칭 슈팅(FPS) 게임을 만드는 법을 배울 수 있다.
이번 튜토리얼은 각 하위 섹션들의 길이가 길어서 분할되어 작성된다.
튜토리얼대로 하면 문제가 발생해서 제대로 따라갈 수 없는 부분으로 동작이 가능하게 수정해야하는 부분은 빨간 블럭으로 표시되어 있다.
이번 튜토리얼에서 새로 배우게 되는 내용은 글 제일 끝에 "이번 섹션에서 배운 것"에 정리된다.
수정
지난 섹션에서 VisibleDefaultOnly는 버전이 바뀌어서 사라진 지정자라고 했던 부분은 잘못된 부분입니다.
VisibleDefaultsOnly는 정상적으로 존재하는 UPROPERTY 지정자입니다. 제가 실수로 VisibleDefaultOnly로 오타를 내서 컴파일러가 지정자가 없다고 에러를 띄웠었습니다. 잘못된 정보로 혼동을 드린 점에 대해서 사과드립니다. 다음부터는 제대로된 확인을 거친 후, 글을 올리도록 하겠습니다.
이전 섹션에서 캐릭터 구성을 마쳤으니, 이제 발사체 무기를 구현하여 발사하면 단순한 수류탄 같은 발사체가 화면 중앙에서 발사되어 월드에 충돌할 때까지 날아가도록 만들어보자. 이번 단계에서는 발사체(Projectile)에 쓸 입력을 추가하고 새 코드 클래스를 만들 것이다.
발사 액션 매핑 추가
편집 메뉴에서 프로젝트 세팅 창을 연다. 그리고 엔진 섹션에서 입력을 선택한 뒤, 액션 매핑에 아래와 같이 "Fire" 라는 입력 세팅을 추가 한다.
발사체(Projectile) 클래스 추가
파일 메뉴에서 새로운 C++ 클래스... 를 선택하고 Actor 클래스를 부모 클래스로 선택하고 다음을 클릭한다.
새 클래스 이름을 "FPSProjectile"로 하고 클래스 생성을 클릭한다.
USphereComponent 추가
FPSProjectile.h로 가서 USphereComponent의 선언을 다음처럼 추가해준다.
ProjectileMovementComponent에서 함수를 호출하려고 할 때, 불완전한 형식은 사용할 수 없다는 에러가 발생하면 "Engine/Classes/GameFramework/ProjectileMovementComponent.h"를 cpp의 전처리기에 추가해주자.
일반적으로 축 매핑(Axis Mappings)을 통해서 키보드, 마우스, 컨트롤러 입력을 "친근한 이름"으로 매핑시킨뒤 나중에 이동 등의 게임 동작에 바인딩할 수 있다. 축 매핑은 지속적으로 폴링되어, 부드러운 전환 및 게임 동작이 가능하다. 컨트롤러의 조이스틱 같은 하드웨어 축은 "눌렸다", "안눌렸다" 같은 식의 구분되는 입력이 아닌 연속적인 입력 수치를 제공한다. 컨트롤러 조이스틱 입력 방법이 스케일식 동작 입력을 제공해 주기는 하지만, 축 매핑으로 WASD 처럼 지속적 폴링되는 게임 동작을 위한 일반 이동 키 매핑도 가능하다.
프로젝트 세팅 창을 열고 엔진 섹션의 입력을 선택한다. 그리고 입력 매핑 세팅을 다음처럼 구성한다.
함수 위에 붙여준 UFUNCTION() 매크로는 엔진에게 이 함수들을 인식시켜 직렬화(Serialization), 최적화, 기타 엔진 함수성에 포함될 수 있도록 해준다.
동작 함수 구현
전형적인 FPS 조작법에서, 캐릭터의 동작 축은 카메라에 상대적이다. "전방"이란 "카메라가 향하는 방향"을, "우측"이란 "카메라가 향하는 방향의 우측"을 뜻한다. 캐릭터의 제어 방향을 구하는 데는 PlayerController를 사용할 것이다. 또한 MoveForward() 함수는 제어 회전의 피치 컴포넌트를 무시하고 입력을 XY 면으로 제한시켜 위아래를 쳐다보더라도 캐릭터는 땅과 평행으로 움직일 수 있도록 한다.
FPSCharacter.cpp에서 AFPSCharacter::SetupPlayerInputComponent() 함수의 하단에 다음 코드를
회전과 쳐다보기에 대한 마우스 입력 처리를 하는 코드를 추가할 차례이다. Character 베이스 클래스는 카메라 회전 컨트롤에 대해서 다음과 같은 필수 함수 둘을 제공한다. 그렇기 때문에 FPSCharacter 클래스에 별도의 함수를 정의하고 구현할 필요없이 바로 AFPSCharacter::SetupPlayerInputComponent() 함수에 바인딩하는 코드를 추가하면 된다.
액션 매핑은 별도의 이벤트에 대한 입력을 다루며, "친근한 이름"에 매핑시켜 나중에 이벤트 주도형 동작에 바인딩시킬 수 있도록 해준다. 최종 효과는 키나 마우스 버튼, 혹은 키패드 버튼에 대해서 누르기/떼기를 통해서 게임 동작을 실행시키도록 하는 거이다.
이번 단계에서는, 스페이스 바에 대한 액션 매핑을 구성하여 캐릭터에 점프 능력을 추가하는 것이다.
점프 액션 매핑
프로젝트 세팅 창을 열고 엔진 섹션에서 입력을 선택한다. 그리고 액션 매핑을 다음과 같이 추가한다.
입력 처리 구현
Character 베이스 클래스의 인터페이스(*.h) 파일 안을 보면, 캐릭터 점프에 대한 지원이 내장되어 있는 것을 볼 수 있다. 캐릭터 점프는 bPressedJump 변수에 묶여 있어서, 점프 키를 누르면, 이 변수를 true로, 떼면 false로 설정해주기만 하면 된다.
이 코드는 카메라의 위치를 캐릭터의 눈 살짝 위쪽으로 잡으면서 폰이 카메라 로테이션을 제어할 수 있도록 해준다.
이대로 빌드하면 Camera Component에서 에러가 발생해서 컴파일에 실패할 수 있다. 코드를 작성할 때는 에러가 뜨지 않아서 방심했지만 이 에러는 충분이 아는 에러일 것이다. 지금 비주얼 스튜디오가 한글 버전이라 로그가 깨져있지만 튜토리얼을 진행하면서 생긴 경험으로 미루어 짐작하건데, 헤더의 30라인에서 발생하는 에러는 UCameraComponent가 정의되지 않았다는 에러일 것이다. UCameraComponent 선언 앞에 class를 붙여주자.
UPROPERTY(VisibleAnywhere)
class UCameraComponent* FPSCameraComponent;
FPS에서 흔히 사용되는 방법은, 전신 바디 메시 하나, 무기와 손 메시하나, 이렇게 별개의 캐릭터 메시 두 개를 사용하는 것이다. 전신 메시는 삼인칭 시첨에서 캐릭터를 보거나 다른 캐릭터를 볼대 사용되고, 플레이어가 일인칭 시점에서 게임을 볼 때는 이 전신 메시를 숨긴다. 그리고 "무기와 손" 메시는 전형적으로 카메라에 붙여 플레이어가 일인칭 시점에서 맵을 볼 때 플레이어에게만 보이는 것이다. 이 파트에서는 캐릭터에 일인칭 메시를 추가해보자.