강의 목표 :
- 언리얼 C++ 코딩 표준 이해 / 주의점 확인
강의 내용 :
- public 에서 private 으로 이어지는 클래스 체계
- 명명규칙 : 파스칼 케이싱(Pascal Casing), 언더스코어(_) 사용x, 클래스와 구조체의 고유 접두사
- 코드의 명확성 : 파라미터 In/Out 접두사 명시, const 지시자 적극 활용, 레퍼런스(&)를 활용한 복사 방지, auto 키워드 사용 자제
- Find In Files 활용
- #include 구문 사용시 의존성 고려할 것 (의존성 최소화 시키기)
[ ✔️코딩 표준 Coding Standard ]
= 코딩 스타일(Style), 코딩 컨베션(Convention)
- 프로그래밍을 작성하는데 지켜야 하는 프로그래밍 이름 규칙과 작성법 등을 지정한 가이드라인
- 프로젝트를 진행함에 있어 코딩 표준을 정하는건 중요함
- 절대적으로 좋은 코딩 표준이란 존재하지 않는다. → 특정 코딩 표준이 항상 옳은건 아니라는 의미
- 중요한 것은 코딩 표준을 정하고 잘 따르는 행동이다.
- 프로젝트의 모든 코드는 한 사람이 만든 것처럼 보여야 한다.
- 언리얼 엔진의 경우 자체적으로 코딩 표준을 정했기 때문에, 기존 C++ 코딩 방법을 버리고 언리얼 엔진 코딩 표준을 따라야 한다. → 권고사항 x, 반드시 MUST 지켜야 하는 규칙
코딩 표준 예시 (구글 C++ 코딩 표준) :
Google C++ Style Guide
google.github.io
[ ✔️언리얼 코딩 표준 ]
https://dev.epicgames.com/documentation/ko-kr/unreal-engine/epic-cplusplus-coding-standard-for-unreal-engine/?application_version=5.1
dev.epicgames.com
📍코딩 규칙이 프로그래머에게 중요한 이유 :
(출처 : 언리얼_코딩 표준_서론)
- 소프트웨어의 총 수명 비용 중 80%는 유지보수에 소모됨
- 최조 작성자가 해당 소프트웨어의 수명이 다할 때까지 유지보수하는 경우는 드물다.
- 코딩 규칙을 사용하면 소프트웨어의 가독성이 향상되며, 엔지니어가 새로운 코드를 더 빠르게 잘 이해할 수 있다.
- 에픽에서 소스 코드를 모드 개발자 커뮤니티에 공개할 경우 코딩 규칙을 알고 있으면 이해하기 수월하다.
- 대다수의 코딩 규칙이 크로스 컴파일러 호환성에 필요하다.
📍클래스 체계 :
클래스는 작성자보다 읽는 사람을 염두에 두고 작성되어야한다.
읽는 사람은 대부분 클래스의 퍼블릭 인터페이스를 사용할 것이기에, 퍼블릭 인터페이스에서 먼저 선언한 후 클래스의 프라이빗 구현이 뒤따라야 한다.
즉, 아래의 코드와 같이 public 영역을 private 영역보다 위에 두고 작성하라는 의미다.
UCLASS
class TASKPROJECT_API UMyGamInstance : public UGameInstance
{
GENERATED_BODY()
public:
private:
};
📍저작권 고지 :
베포용으로 에픽에서 제공한 모든 소스 파일은 파일 첫번째 줄에 저작원 고지를 포함해야한다.
저작권 고지의 포맷은 다음과 정확히 일정해야한다.
// Copyright Epic Games, Inc. All Rights Reserved.
📍명명규칙 :
= 네이밍 컨벤션
언리얼은 "파스칼 케이싱" 을 표준으로 사용한다.
각 단어의 첫 번째 글자(타입 이름 또는 변수 이름)는 대문자여야 하며, 일반적으로 단어 사이에는 언더스코어를 사용하지 않습니다. 예를 들어 Health 및 UPrimitiveComponent 는 올바르지만, lastMouseCoordinates 또는 delta_coordinates 는 올바르지 않습니다.
(출처 : 언리얼_코딩 표준_명명규칙)
명명규칙의 종류 :
- 파스칼 케이싱 : 합성어의 첫 자를 대문자로 사용하는 방법 (UnrealEngine)
- 카멜 케이싱 : 첫 합성어는 소문자, 나머지는 대문자 (unrealEngine)
- 스네이킹 케이싱 : 합성어 사이에 언더바를 사용 (uneral_Engine)
📍접두사 규칙:
타입 이름에는 추가적으로 대문자로 이루어진 접두사를 포함하여 변수 이름과 구분한다.
ex) FSkin 은 타입이름 이며, Skin 은 인스턴스의 이름(변수 명)이다.
- 접두사 T : 템플릿 클래스
- 접두사 U : UObject(언리얼 오브젝트) 를 상속받는 클래스
- 접두사 A : AActor 를 상속받는 클래스
- 접두사 S : SWidget 을 상속받는 클래스
- 접두사 I : 추상적인 인터페이스 클래스
- 접두사 C : 에픽의 개념이 유사한 클래스 타입 (???? 무슨 말인지 모르겠다...ㅠ)
- 접두사 E : 열거형
- 접두사 b : 부울(bool)변수
- 접두사 F : 그 외 대부분의 클래스 (언리얼 객체를 상속받지 않는 클래스의 경우 이에 해당) , 그러나 일부 서브시스템은 다른 글자를 사용하기도 한다.
- Typedef의 경우 해당 타입에 적합한 접두사를 사용한다. 예를 들어 구조체의 typedef인 경우 F, UObject의 typedef인 경우 U를 사용한다.
📍네이밍 컨벤션 추가 규칙 :
- 타입 및 변수 이름은 명사
- 메서드 이름은 메서드의 영향을 설명하거나, 메서드의 반환 값을 설명하는 동사여야한다.
- 매크로 이름은 모두 대문자로 구성되고, 단어가 언더스코어로 분리되며, 접두사 UE_ 가 사용되어야 한다.
- 변수의 의미에 대한 코멘트를 제공할 수 있도록 모든 변수는 한 번에 하나씩 선언해야 한다. 이는 JavaDoc 스타일에도 필요한 부분이다. 변수의 앞에 여러 줄 또는 한 줄의 코멘트를 사용할 수 있으며, 변수 그룹화를 위한 빈 줄은 선택사항이다.
- bool 을 반환하는 모든 함수는 IsVisible() 또는 ShouldClearBuffer() 등의 true/false 질문 형식이어야한다.
- 파라미터의 경우 용도에 따라 In/Out 접두사를 붙여준다. 함수에서 사용만 될 것이라면 In, 함수에서 반환값을 적어줄 용도면 Out을 붙여준다.
ex) bOutResult : 함수에서 반환값을 적어줄 bool 타입 변수(파라미터)
// 변수명
float TeaWeight;
int32 TeaCount;
bool bDoesTeaStink;
FName TeaName;
FString TeaFriendlyName;
UClass* TeaClass;
USoundCue* TeaSound;
UTexture* TeaTexture;
// 함수명
// true일 경우 무슨 의미일까요?
bool CheckTea(FTea Tea);
// 이름을 통해 true일 경우 차가 신선하다는 것을 명확히 알 수 있습니다.
bool IsTeaFresh(FTea Tea);
📍포터블 C++ 코드 :
C++의 int 및 부호 없는 int 타입은 정수 너비가 중요치 않은 경우라면 코드에서 사용해도 괜찮다.
명시적으로 크기가 정해진 타입은 여전히 "시리얼라이즈(Serialize, 직렬)" 또는 "리플리케이트"된 포맷으로 사용해야한다.
(리플리케이트 : 서버와 클라이언트 사이에서 데이터와 명령을 주고받는 프로세스)
bool - 부울 값(부울 크기 추정 금지). BOOL 은 컴파일되지 않습니다.
TCHAR - character(문자) (TCHAR 크기 추정 금지)
uint8 - unsigned byte(부호 없는 바이트) (1바이트)
int8 - signed byte(부호 있는 바이트) (1바이트)
uint16 - unsigned 'shorts'(부호 없는 'short') (2바이트)
int16 - signed 'short'(부호 있는 'short')(2바이트)
uint32 - unsigned int(부호 없는 int) (4바이트)
int32 - signed int(부호 있는 int) (4바이트)
uint64 - unsigned 'quad word'(부호 없는 '쿼드 단어') (8바이트)
int64 - signed 'quad word'(부호 있는 '쿼드 단어') (8바이트)
float - 단정밀도 부동 소수점(4바이트)
double - 배정밀도 부동 소수점(8바이트)
PTRINT - 포인터를 가질 수 있는 정수(PTRINT 크기 추정 금지)
// 출처 : 언리얼_코딩 표준_포터블 C++ 코드
📍표준 라이브러리 사용 :
표준 라이브러리는 범용적으로 설계되어 있다.
때문에 범용적으로 사용하기 좋지만 게임 프로그래밍에 사용하기엔 적합하지 않은면도 있다.
게임 제작에 포커싱된 언리얼 엔지의 라이브러리를 사용하는걸 지향한다.
📍const 정확도 :
const는 컴파일러 지시어(directive) 이므로, 모든 코드는 const 정확도를 맞춰야한다.
- 함수 아규먼트가 함수에 의해 수정되지않아 함수 아규먼트를 const 포인터 또는 const 참조로 전달하는 경우
- 메서드가 오브젝트를 수정하지 않아 const로 플래그를 지정하는 경우
- 루프에서 컨테이너 수정을 하지 않아 const를 사용하여 컨테이너에 반복작업을 하는 경우
아규먼트?
아규먼트(Argument)와 파라미터(Parameter) 를 구분하지 않고 사용하는 경우가 많다.
파라미터는 "매개변수"로써 함수를 정의할 때 외부로부터 받아들이는 임의의 값을 의미한다.
void Func(int x, int y);
// x, y 는 파라미터
아규먼트는 "인수" 로써 함수를 호출할 때 사용되는 일련의 값들을 의미한다.
Func(2, 3);
// Func 함수 호출시 넣어준 값 2, 3 은 아규먼트
📍중괄호 :
중괄호 논쟁은 오랫동안 이어져 왔다. 에픽에서는 새 줄에 중괄호를 사용하는 것이 오래된 관행이기에 이를 준수하도록 하자.
단일 구문 블록에도 항상 중괄호를 포함시켜 주자.
if (bThing)
{
return;
}
📍물리적 종속성 :
- 파일 이름에는 가급적 접두사를 붙이지 않아야 한다.
- 예를 들어 UScene.cpp → Scene.cpp 가 좋다
- 모든 헤더는 #pragma once 지시어로 복수의 include 를 방지해야한다.
- 헤더 include 대신 "전방 선언 (forward declaration)" 이 가능한 경우 그렇게 한다.
📍일반적인 스타일의 문제 :
복잡한 표현식은 중간 변수를 사용하여 간소화 하자
// 기존 코드
if ((Blah->BlahP->WindowExists->Etc && Stuff) &&
!(bPlayerExists && bGameStarted && bPlayerStillHasPawn &&
IsTuesday())))
{
DoSomething();
}
// 변경된 코드
const bool bIsLegalWindow = Blah->BlahP->WindowExists->Etc && Stuff;
const bool bIsPlayerDead = bPlayerExists && bGameStarted && bPlayerStillHasPawn && IsTuesday();
if (bIsLegalWindow && !bIsPlayerDead)
{
DoSomething();
}
포인트와 레퍼런스의 공백은 오른쪽에 딱 한 칸만 두어야한다.
// 옳은 예
FShaderType* Ptr
// 잘못된 예
FShaderType *Ptr
FShaderType * Ptr
함수 호출에서 익명 리터럴 사용은 지양하자.
아래와 같이 코드를 변경하면 함수의 선언을 조회하지 않아도 이해할 수 있음 일반적인 독자가 의도를 쉽게 파악할 수 있다.
// 기존 스타일
Trigger(TEXT("Soldier"), 5, true);.
// 새 스타일
const FName ObjectName = TEXT("Soldier");
const float CooldownInSeconds = 5;
const bool bVulnerableDuringCooldown = true;
Trigger(ObjectName, CooldownInSeconds, bVulnerableDuringCooldown);
헤더에 특수한 스태틱 변수를 정의하지 않도록 한다. 해당 헤더가 포함된 모든 이동 단위로 인스턴스가 컴파일 되기 때문
// 기존 코드
// SomeModule.h
static const FString GUsefulNamedString = TEXT("String");
// 변경된 코드
// SomeModule.h
extern SOMEMODULE_API const FString GUsefulNamedString;
// SomeModule.cpp
const FString GUsefulNamedString = TEXT("String");
📍API 디자인 가이드라인 :
bool 타입 파라미터는 피해야 하며, 함수에 전달되는 플래그의 경우 특히 그러하다.
일반적인 스타일 문제에서 등장했던 익명 리터럴의 문제가 그대로 발생한다.
bool 타입 파라미터 대신 열거형을 플래그 값으로 사용하는 것을 추천한다.
위와 같은 형태는 플래그 값이 실수로 반전(true↔false)되는 것을 막아준다.
// 기존 스타일
FCup* MakeCupOfTea(FTea* Tea
, bool bAddSugar = false
, bool bAddMilk = false
, bool bAddHoney = false
, bool bAddLemon = false);
FCup* Cup = MakeCupOfTea(Tea, false, true, true);
// 새 스타일
enum class ETeaFlags
{
None,
Milk = 0x01,
Sugar = 0x02,
Honey = 0x04,
Lemon = 0x08
};
ENUM_CLASS_FLAGS(ETeaFlags)
FCup* MakeCupOfTea(FTea* Tea, ETeaFlags Flags = ETeaFlags::None);
FCup* Cup = MakeCupOfTea(Tea, ETeaFlags::Milk | ETeaFlags::Honey);
너무 긴 함수 파라미터 리스트는 피하자.
함수가 파라미터를 많이 받는 경우 전용 구조체 전달을 고려해 보자
// 기존 스타일
TUniquePtr<FCup[]> MakeTeaForParty(const FTeaFlags* TeaPreferences
, uint32 NumCupsToMake
, FKettle* Kettle
, ETeaType TeaType = ETeaType::EnglishBreakfast
, float BrewingTimeInSeconds = 120.0f);
// 새 스타일
struct FTeaPartyParams
{
const FTeaFlags* TeaPreferences = nullptr;
uint32 NumCupsToMake = 0;
FKettle* Kettle = nullptr;
ETeaType TeaType = ETeaType::EnglishBreakfast;
float BrewingTimeInSeconds = 120.0f;
};
TUniquePtr<FCup[]> MakeTeaForParty(const FTeaPartyParams& Params);
bool 및 FString 을 파라미터로 받는 함수의 오버로드(overload)는 피하도록 하자
void Func(const FString& String);
void Func(bool bBool);
Func(TEXT("String")); // 부울 오버로드 호출!
================================
개인 공부 기록용 포스팅입니다.
댓글, 질문 환영해요~!
Unreal Engine Ver : 5.1.1
Refer : Inflearn_이득우의 언리얼 프로그램
================================
'Unreal > Inflearn' 카테고리의 다른 글
Part1.6 언리얼 오브젝트 리플렉션 시스템 Ⅱ⭕ (0) | 2024.04.08 |
---|---|
Part1.5 언리얼 오브젝트 리플렉션 시스템 Ⅰ⭕ (0) | 2024.04.08 |
Part1.4 언리얼 오브젝트 소개 ⭕ (0) | 2024.04.08 |
Part1.3 언리얼 C++ 기본 타입과 문자열 ⭕ (0) | 2024.04.08 |
Unreal 초기설정⭕ (0) | 2024.02.21 |