You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
626 lines
33 KiB
Plaintext
626 lines
33 KiB
Plaintext
INTSourceChangelist:2604445
|
|
Availability:Public
|
|
Title: 코딩 표준
|
|
Crumbs:%ROOT%, Programming, Programming\Development
|
|
Description:언리얼 엔진 4 코드베이스에 에픽게임스가 사용하는 표준과 규칙입니다.
|
|
Version: 4.5
|
|
|
|
[TOC(start:2)]
|
|
|
|
|
|
## 소개
|
|
|
|
에픽에서 저희는 단순한 코딩 표준과 규칙이 몇가지 있습니다. 이 문서는 논의중이거나 작업중인 내용이라기 보다는, 현재 에픽에서 시행중인 코딩 표준 상태를 반영한 것입니다.
|
|
|
|
프로그래머들에게 있어서, 코딩 규칙은 매우 중요합니다. 그 이유 중의 몇 가지 는 다음과 같습니다:
|
|
|
|
* 하나의 소프트웨어가 그 수명을 지속하는 동안 들어가는 경비 가운데 80%는 유지 보수비입니다.
|
|
* 원저자가 그 소프트웨어의 수명이 다할 때까지 관리하는 일은 거의 없습니다.
|
|
* 코딩 규칙은 그 소프트웨어를 한층 읽기 쉽도록 해주므로, 엔지니어들은 새 코드를 보다 빨리 그리고 철저하게 이해할 수 있습니다. 저희는 이 프로젝트가 지속되는 동안 틀림없이 새 엔지니어 및 수습사원들을 채용하게 될 것이며, 아마도 엔진에 새로 변경한 사항들을 다음 프로젝트에도 계속 사용하게 될 것입니다.
|
|
* 만일 저희가 mod 커뮤니티 개발자들께 소스 코드를 공개하기로 결정한다면, 이해하기 쉬운 것이기를 바랍니다.
|
|
* 사실 이 규칙 가운데 상당수는 컴파일러간의 호환을 위해 필요한 것이기도 합니다.
|
|
|
|
|
|
## 클래스 체계
|
|
|
|
클래스 체계는 작성하는 사람 보다는 읽는 사람을 염두에 두고 체계를 잡아야 합니다. 읽는 사람 대부분은 클래스의 공용 인터페이스를 쓸 것이기에, public 을 먼저 선언하고, 그 후 클래스의 private 구현이 뒤따릅니다.
|
|
|
|
|
|
## 저작권 공지
|
|
|
|
에픽이 배포용으로 제공한 (`.h`, `.cpp`, `.xaml`, 등의) 소스 파일은 반드시 파일의 첫 줄에 저작권 공지를 포함시켜야 합니다. 공지의 포맷은 반드시 다음과 같아야 합니다:
|
|
|
|
// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
|
|
|
|
이 줄이 없거나 포맷이 다르게 되어 있다면, CIS 가 오류를 내고 중단시킬 것입니다.
|
|
|
|
## 작명 규칙
|
|
|
|
* (유형이나 변수 등의) 이름 내 각 단어의 첫 글자는 대문자로 써야 하며, 단어 사이에 보통은 공백을 띄우지 않습니다. Health 와 UPrimitiveComponent 정도를 예로 들 수는 있지만, lastMouseCoordinates 나 delta_coordinates 같은 것은 아닙니다.
|
|
* 변수 이름과 구분하기 위해 유형 이름을 대문자 한 글자로 나타내는 접두사를 붙입니다. 예를 들어 FSkin 이 유형 이름이고, Skin 은 FSkin 의 인스턴스 입니다.
|
|
* 템플릿 클래스 접두사는 T 입니다.
|
|
* UObject 에서 상속하는 클래스 접두사는 U 입니다.
|
|
* AActor 에서 상속하는 클래스 접두사는 A 입니다.
|
|
* SWidget 에서 상속하는 클래스 접두사는 S 입니다.
|
|
* 추상 인터페이스인 클래스 접두사는 I 입니다.
|
|
* 그 외 대부분 클래스의 접두사는 F 이나, 일부 서브시스템은 다른 글자를 사용하기도 합니다.
|
|
* 유형과 변수명은 명사입니다.
|
|
* 메소드 이름은 동사로 그 메소드가 하는 일이나, 하는 일이 딱히 없는 경우 반환값을 설명합니다.
|
|
|
|
|
|
변수, 메소드, 클래스 이름은 명확하고 애매하지 않으며 서술적이여야 합니다. 이름의 범위가 클 수록, 서술적인 좋은 이름의 중요성 역시 커집니다. 과도한 축약은 피하시기 바랍니다.
|
|
|
|
모든 변수는 변수에 대한 설명을 코멘트로 붙일 수 있도록 한 번에 하나씩만 선언해야 합니다. 이난 JavaDocs 스타일에서도 요구하는 바입니다. 변수 앞에 여러줄짜리든 한줄짜리든 코멘트를 남기면 되며, 변수 그룹 목적으로 빈 줄을 띄워도 됩니다.
|
|
|
|
모든 불리언은 _True_ / _False_ 값을 묻습니다. 불리언을 반환하는 함수도 모두 마찬가지입니다. 모든 불리언 값의 접두사는 "b" 를 붙여야 합니다.
|
|
|
|
프로시져(반환값이 없는 함수)는 강한 동사 뒤에 오브젝트를 붙여 써야 합니다. 예외는 메소드의 오브젝트가 그 안에 있는 오브젝트일 때인데, 그런 경우 오브젝트는 맥락에서 이해를 합니다. "Handle" 이나 "Process" 같은 것으로 시작하는 이름은 애매하니 피해 주시기 바랍니다.
|
|
|
|
필수는 아니지만, 함수 파라미터 중 레퍼런스로 전달된 이후 함수가 그 값에 출력할 것으로 기대되는 것의 경우 이름 앞에 "Out" 접두사를 붙일 것을 추천합니다. 그래야 이 인수로 전다로디는 값은 함수가 대체시킬 것임이 명확해 집니다.
|
|
|
|
값을 반환하는 함수는 반환값에 대한 설명을 해야 합니다. 이름을 통해 함수가 반환하게 될 값을 명확히 알 수 있어야 하지요. 이는 불리언 함수의 경우 특히나 중요합니다. 다음 두 예제 메소드를 살펴 봅시다:
|
|
|
|
bool CheckTea(FTea Tea) {...} // _True_ 면 뭐라는 걸까요?
|
|
bool IsTeaFresh(FTea Tea) {...} // _True_ 면 차가 신선하다는 것이 명확해집니다.
|
|
|
|
|
|
### Examples
|
|
|
|
/* 차의 무게, 킬로그램 */
|
|
float TeaWeight;
|
|
|
|
/* 찻잎 수 */
|
|
int32 TeaCount;
|
|
|
|
/* _True_ 면 냄새나는 차입니다. */
|
|
bool bDoesTeaStink;
|
|
|
|
/* 사람이 읽을 수 없는 차 FName 입니다. */
|
|
FName TeaName;
|
|
|
|
/* 사람이 읽을 수 있는 차 이름입니다. */
|
|
FString TeaFriendlyName;
|
|
|
|
/* 사용할 차 등급입니다. */
|
|
UClass* TeaClass;
|
|
|
|
/* 차 따르는 소리입니다. */
|
|
USoundCue* TeaSound;
|
|
|
|
/* 차 그림입니다. */
|
|
UTexture* TeaTexture;
|
|
|
|
|
|
## 기본 C++ 유형에 이식가능 alias
|
|
|
|
* bool - boolean (4 바이트). BOOL은 컴파일되지 않습니다. true / flase 가 아니라 true / false 로 써야 합니다.
|
|
* TCHAR - character (TCHAR 크기 추정 금지)
|
|
* uint8 - unsigned byte (1 바이트)
|
|
* int8 - signed byte (1 바이트)
|
|
* uint16 - unsigned "short" (2 바이트)
|
|
* int16 - signed "short" (2 바이트)
|
|
* uint32 - unsigned int (4 바이트)
|
|
* int32 - signed int (4 바이트)
|
|
* uint64 - unsigned "quad word" (8 바이트)
|
|
* int64 - signed "quad word" (8 바이트)
|
|
* float - single precision floating point (4 바이트)
|
|
* double - double precision floating point (8 바이트)
|
|
* PTRINT - 포인터를 가질 수 있는 integer (PTRINT 크기를 추정 금지)
|
|
|
|
|
|
이식가능 코드에서 C+\+ int 형은 컴파일러에 따라 크기가 달라지니 사용하지 마시기 바랍니다.
|
|
|
|
|
|
## 코멘트
|
|
|
|
코멘트는 소통이고, 소통은 _중요합니다_. 코멘트에 대해 명심하실 점이 몇 가지 있습니다 (Kernighan & Pike 의 _The Practice of Programming_ 에서):
|
|
|
|
|
|
### 지침
|
|
|
|
* 자체적으로 설명이 되는 코드를 작성하세요:
|
|
|
|
// 나빠요:
|
|
t = s + l + b;
|
|
|
|
// 좋아요:
|
|
TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves;
|
|
|
|
|
|
* 도움이 되는 코멘트를 작성하세요.
|
|
|
|
// 나빠요:
|
|
// iLeaves 증가
|
|
++Leaves;
|
|
|
|
// 좋아요:
|
|
// 찻잎이 더 있다는 것을 알았습니다.
|
|
++Leaves;
|
|
|
|
|
|
* 나쁜 코드에 코멘트를 달지 마세요 - 그냥 다시 작성하세요:
|
|
|
|
// 나빠요:
|
|
// 잎의 총 갯수는
|
|
// 작은 잎과 큰 잎을 더한 것에서
|
|
// 둘 다인 것을 뺀 것입니다.
|
|
t = s + l + b;
|
|
|
|
// 좋아요:
|
|
TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves;
|
|
|
|
|
|
* 코드를 모순되게 만들지 마세요:
|
|
|
|
// 나빠요:
|
|
// iLeaves 절대 증가 아님!
|
|
++Leaves;
|
|
|
|
// 좋아요:
|
|
// 다른 잎이 있다는 것을 압니다.
|
|
++Leaves;
|
|
|
|
|
|
### 예제 포맷
|
|
|
|
저희는 JavaDoc 기반 시스템을 사용하여 코드에서 코멘트를 자동으로 추출한 뒤 문서를 만들기 때문에, 코멘트에는 따라야 하는 특수한 포맷 규칙이 몇 가지 있습니다.
|
|
|
|
다음 예제는 클래스, 스테이트, 메소드, 변수 코멘트의 포맷을 선보입니다. 기억하실 것은, 코멘트는 코드를 증강시켜야 합니다. 코드는 구현을 설명하고, 코멘트는 그 의도를 설명합니다. 코드 한 줄의 의도를 바꾸더라도 반드시 코멘트를 업데이트하시기 바랍니다.
|
|
|
|
참고로 지원되는 파라미터 코멘트 스타일은 두 가지로, Steep 와 Sweeten 메소드로 구체화되어 있습니다. Steep 이 사용하는 @param 스타일은 전형적인 스타일이지만, 단순 함수의 경우 파라미터 문서를 함수에 대한 설명 코멘트로 통합시키는 것이, Sweeten 예제에서 보듯이 더욱 깔끔할 수 있습니다.
|
|
|
|
UE3 코딩 표준과는 달리, 메소드 코멘트는 딱 한번, 메소드가 공개적으로 선언되는 곳에 include 시켜야 합니다. 메소드 코멘트는 다른 호출자에게 관련이 있을 메소드 오버라이드 관련 정보를 포함해서, 메소드 호출자에 관련된 정보만을 담아야 합니다. 메소드 구현에 대한 세부사항이나 호출자에 관련이 없는 오버라이드는 메소드 구현 안에 코멘트를 달아야 할 것입니다.
|
|
|
|
|
|
/* 마실 수 있는 오브젝트에 대한 인터페이스 */
|
|
class IDrinkable
|
|
{
|
|
public:
|
|
|
|
/*
|
|
* 플레이어가 이 오브젝트를 마실 때 호출.
|
|
* @param OutFocusMultiplier - 반환되면 마신 사람의 포커스에 적용할 배수가 들어갑니다.
|
|
* @param OutThirstQuenchingFraction - 반환되면 마신 사람의 갈증 해소 정도가 들어갑니다 (0-1).
|
|
*/
|
|
virtual void Drink(float& OutFocusMultiplier,float& OutThirstQuenchingFraction) = 0;
|
|
};
|
|
|
|
/* (차) 한 곱뿌 */
|
|
class FTea : public IDrinkable
|
|
{
|
|
public:
|
|
|
|
/*
|
|
* 우려내는 데 사용한 물의 용량과 온도가 주어진 경우 차에 대한 델타-테이스트 값을 계산합니다.
|
|
* @param VolumeOfWater - 우려내는 데 사용할 물의 양 mL 입니다.
|
|
* @param TemperatureOfWater - 물의 온도 켈빈입니다.
|
|
* @param OutNewPotency - 담그기 시작한 이후의 차의 효능으로, 0.97 에서 1.04 까지입니다.
|
|
* @return 차 강도의 변화를 분당 차 맛 단위(TTU) 로 반환합니다.
|
|
*/
|
|
float Steep(
|
|
float VolumeOfWater,
|
|
float TemperatureOfWater,
|
|
float& OutNewPotency
|
|
);
|
|
|
|
// 차에 감미료를 추가, 같은 당도를 내는 수크로오스 그램 단위별 감미료를 차에 추가합니다.
|
|
void Sweeten(float EquivalentGramsOfSucrose);
|
|
|
|
/* 일본에서 판매되는 차의 가치 옌 단위입니다. */
|
|
float GetPrice() const
|
|
{
|
|
return Price;
|
|
}
|
|
|
|
//- IDrinkable 인터페이스입니다.
|
|
virtual void Drink(float& OutFocusMultiplier,float& OutThirstQuenchingFraction);
|
|
|
|
private:
|
|
|
|
/* 차의 가치 옌 단위입니다. */
|
|
float Price;
|
|
|
|
// 차의 당도로, 같은 당도를 내는 만큼의 수크로오스 그램 단위입니다.
|
|
float Sweetness;
|
|
};
|
|
|
|
float FTea::Steep(float VolumeOfWater,float TemperatureOfWater,float& OutNewPotency)
|
|
{
|
|
...
|
|
}
|
|
|
|
void FTea::Sweeten(float EquivalentGramsOfSucrose)
|
|
{
|
|
...
|
|
}
|
|
|
|
void FTea::Drink(float& OutFocusMultiplier,float& OutThirstQuenchingFraction)
|
|
{
|
|
...
|
|
}
|
|
|
|
|
|
클래스 코멘트에 포함되는 것은?
|
|
* 이 클래스가 해결하는 문제에 대한 설명입니다. 왜 이 클래스를 생성했는가 겠죠?
|
|
|
|
그런 메소드 코멘트 부분이 뜻하는 바는?
|
|
* 함수의 목표가 첫 번째입니다. 여기서는 _이 함수가 해결하는 문제_ 를 설명합니다. 위에서 말씀드린 것처럼, 코멘트는 _의도_ 를 설명하며, 코드는 구현을 설명합니다.
|
|
* 그런 다음 파라미터 코멘트가 옵니다. 각 파라미터 코멘트에는 측량 단위, 예상되는 값 범위, "불가능한" 값, 상태/오류 코드의 의미가 포함되어야 합니다.
|
|
* 그런 다음 반환 코멘트가 옵니다. 출력 변수 설명과 마찬가지로 예상되는 반환값을 설명합니다.
|
|
|
|
|
|
## C++ 11 및 최신 언어 문법
|
|
|
|
언리얼 엔진은 다수의 C++ 컴파일러로 대규모 이식이 가능하도록 만들어 졌기에, 기능을 사용할 때는 지원하게 될 수도 있다고 생각되는 컴파일러와의 호환성을 신중히 따져 봅니다. 가끔은 매우 유용한 기능이라 매크로에 저장하여 많이 사용하는 경우도 있지만, 보통은 지원하게 될 거라 생각하는 모든 컴파일러가 최신의 표준을 지원할 때까지는 기다립니다.
|
|
|
|
"auto" 키워드, 범위 기반 for, 람다처럼 최신 컴파일러에서 잘 지원되는 것으로 보이는 C++ 11 언어 기능을 활용하고 있습니다. 어떤 경우에는 (컨테이너의 rvalue 레퍼런스같은) 전처리기 조건문에서 이러한 기능을 묶어 사용할 수 있도록 하고 있습니다. 그러나 새 플랫폼에서 문법을 소화시키지 못하여 혼란이 야기될 수 있는 기능에 대해서는, 확신이 들기 전까지 채택하지 않을 수 있습니다.
|
|
|
|
아래에 지원되는 최신 C++ 컴파일러 기능이라 명시한 것 이외의 컴파일러 전용 언어 기능에 대해서는, 전처리기 매크로나 조건문에 묶어두지 않고서야 사용을 삼가야 하며, 그랬다 해도 조심히 사용해야 합니다.
|
|
|
|
|
|
### 예전 매크로에 대한 새 키워드
|
|
|
|
'checkAtCompileTime', 'OVERRIDE', 'FINAL', 'NULL' 등의 예전 매크로는 이제 'static_assert', 'override', 'final', 'nullptr' 로 대체하면 됩니다. 이 매크로 중 일부는 여전히 만연하게 사용되므로, 정의된 채 놔둬도 됩니다.
|
|
|
|
이에 대한 한 가지 예외라면, C++/CX 빌드(예: Xbox One)의 nullptr 은 사실 매니지드 널 레퍼런스 유형입니다. 유형이나 어떤 템플릿 인스턴스화 맥락을 제외하고는 네이티브 C++ 의 nullptr 과 거의 호환되므로, 호환성을 위해서는 좀 더 일반적인 decltype(nullptr) 대신 TYPE_OF_NULLPTR 매크로를 사용해야 합니다.
|
|
|
|
|
|
### 'auto' 키워드
|
|
|
|
'auto' 키워드는 UE4 타겟 모든 컴파일러에 지원되며, 적합하다 생각되면 코드 어디서든 사용할 것을 권장합니다.
|
|
|
|
auto 를 사용할 때는 유형 이름과 사용할 때와 마찬가지로 const 나 & 나 * 와 함께 사용해야 합니다. auto 를 쓰면 추론되는 유형을 원하는 것으로 강제시켜 줍니다.
|
|
|
|
auto 키워드 사용을 권장하는 곳은 반복처리 루프 (구절 중복 제거) - 또는 범위 기반 for 루프가 더 나으며, 그리고 변수를 새 인스턴스로 초기화시킬 때 (유형 이름 중복 제거) 입니다. 그 외의 경우는 조금 논란의 소지가 있으나, 현재로서는 원하는 대로 써 보시기 바랍니다. 그러면서 실전 사례를 발전시켜 나가겠습니다.
|
|
|
|
꼼수: Visual Studio 에서 변수 위에 마우스 커서를 올리면 보통 추론되는 유형을 알려 줍니다.
|
|
|
|
|
|
### 범위 기반 For
|
|
|
|
(Range Based For) 코드의 가독성과 유지보수성 향상에 도움이 되는 경우, 모든 엔진 및 에디터 코드에 사용하셔도 됨은 물론, 추천하는 바입니다. 예전 TMap 이터레이터를 사용하는 코드를 이주할 때는, 예전 이터레이터 유형 메소드였던 Key() 와 Value() 함수가 이제 단순히 내재된 키-값 TPair 의 Key 와 Value 칸이 되었음에 유의하세요:
|
|
|
|
TMap<FString, int32> MyMap;
|
|
|
|
// Old style
|
|
for (auto It = MyMap.CreateIterator(); It; ++It)
|
|
{
|
|
UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), It.Key(), *It.Value());
|
|
}
|
|
|
|
// New style
|
|
for (auto& Kvp : MyMap)
|
|
{
|
|
UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), Kvp.Key, *Kvp.Value);
|
|
}
|
|
|
|
몇몇 독립형 이터레이터 유형에 대해 범위로 대체한 것도 있습니다:
|
|
|
|
// Old style
|
|
for (TFieldIterator<UProperty> PropertyIt(InStruct, EFieldIteratorFlags::IncludeSuper); PropertyIt; ++PropertyIt)
|
|
{
|
|
UProperty* Property = *PropertyIt;
|
|
UE_LOG(LogCategory, Log, TEXT("Property name: %s"), *Property->GetName());
|
|
}
|
|
|
|
// New style
|
|
for (UProperty* Property : TFieldRange<UProperty>(InStruct, EFieldIteratorFlags::IncludeSuper))
|
|
{
|
|
UE_LOG(LogCategory, Log, TEXT("Property name: %s"), *Property->GetName());
|
|
}
|
|
|
|
|
|
### 람다 및 무명 함수
|
|
|
|
이제 람다(Lambdas)가 허용되지만, 스택 변수를 캡처하는 스테이트풀(stateful, 네트웍 연결 상태를 추적할 수 있는) 람다 사용에 대해서는 조심스럽습니다 -- 아직도 어디에 쓰면 적합할지 고심하고 있습니다. 또 저희가 많이 사용하는 편인 함수 포인터에는 스테이트풀 람다를 할당할 수 없습니다.
|
|
|
|
람다는 범용 알고리즘의 술부(predicates)와 함께 사용할 때 가장 좋습니다. 예를 들어:
|
|
|
|
// Find first Thing whose name contains the word "Hello"
|
|
Thing* HelloThing = ArrayOfThings.FindByPredicate([](const Thing& Th){ return Th.GetName().Contains(TEXT("Hello")); });
|
|
|
|
// Sort array in reverse order of name
|
|
AnotherArray.Sort([](const Thing& Lhs, const Thing& Rhs){ return Lhs.GetName() > Rhs.GetName(); });
|
|
|
|
나중에 실전 사례가 확립되면 이 문서를 업데이트하겠습니다.
|
|
|
|
|
|
## 강 유형 열거형
|
|
|
|
Enum 클래스는 모든 컴파일러에 지원되며, 일반 열거형이든 UENUM 이든 구식 네임스페이스 열거형에 대한 대체품으로 추천합니다. 예:
|
|
|
|
// Old enum
|
|
UENUM()
|
|
namespace EThing
|
|
{
|
|
enum Type
|
|
{
|
|
/* Description of Thing1 */
|
|
Thing1,
|
|
// Description of Thing2
|
|
Thing2
|
|
};
|
|
}
|
|
|
|
// New enum
|
|
UENUM()
|
|
enum class EThing : uint8
|
|
{
|
|
/* Description of Thing1 */
|
|
Thing1,
|
|
// Description of Thing2
|
|
Thing2
|
|
};
|
|
|
|
이들은 uint8 기반인 한 UPROPERTY 로 지원되기도 하며, 구형 TEnumAsByte<> 우회법을 대체합니다:
|
|
|
|
// Old property
|
|
UPROPERTY()
|
|
TEnumAsByte<EThing::Type> MyProperty;
|
|
|
|
// New property
|
|
UPROPERTY()
|
|
EThing MyProperty;
|
|
|
|
플래그로 사용되는 Enum 클래스는 새로운 ENUM_CLASS_FLAGS(EnumType) 매크로를 사용하여 비트단위 연산자 전부를 자동 정의합니다:
|
|
|
|
enum class EFlags
|
|
{
|
|
Flag1 = 0x01,
|
|
Flag2 = 0x02,
|
|
Flag3 = 0x04
|
|
};
|
|
|
|
ENUM_CLASS_FLAGS(EFlags)
|
|
|
|
여기에 한 가지 예외라면, 'truth' 맥락에서 플래그를 사용하는 것인데, 이는 언어상의 한계로 'double bang' 연산자를 사용하여 우회 가능합니다:
|
|
|
|
// Old
|
|
if (Flags & EFlags::Flag1)
|
|
|
|
// New
|
|
if (!!(Flags & EFlags::Flag1))
|
|
|
|
|
|
## Move 시맨틱
|
|
|
|
모든 주요 컨테이너 유형, TArray, TMap, TSet, FString 에는 move 생성자와 move 할당 연산자가 있습니다. 이러한 유형의 값을 전달/반환할 때 종종 자동으로 사용되지만, std::move 의 UE4 해당 버전인 MoveTemp 를 통해 명시적으로 실행 가능합니다.
|
|
|
|
값으로 컨테이너나 스트링을 반환하는 것은, 보통 임시로 복사하는 비용 없어 표현성에 이득이 될 수 있습니다. 값 전달 관련 규칙 및 MoveTemp 사용법은 아직도 확립중이지만, 최적화된 코드베이스 영역 일부에서는 이미 찾아볼 수 있습니다.
|
|
|
|
|
|
## 써드 파티 코드
|
|
|
|
엔진에서 사용하는 라이브러이에 코드를 수정할 때마다, 변경내용에 //@UE4 코멘트는 물론 왜 변경했는지에 대한 설명이 되는 태그를 꼭 달아주세요. 그래야 그 라이브러리의 새 버전으로 변경내용을 병합하는 작업이 쉽게 이루어지며, 라이선시 역시 우리가 가한 수정 내용을 쉽게 찾을 수 있습니다.
|
|
|
|
그리고 엔진에 포함되는 써드 파티 코드는 쉽게 검색되는 포맷의 코멘트로 마킹해야 합니다. 예:
|
|
|
|
// @third party code - BEGIN PhysX
|
|
#include <PhysX.h>
|
|
// @third party code - END PhysX
|
|
|
|
// @third party code - BEGIN MSDN SetThreadName
|
|
// [http://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx]
|
|
// Used to set the thread name in the debugger
|
|
...
|
|
//@third party code - END MSDN SetThreadName
|
|
|
|
|
|
## 코드 포맷
|
|
|
|
|
|
### 대괄호 { }
|
|
|
|
대괄호 전쟁은 신물이 날 정도입니다. 때문에 에픽에서는 새 줄에 괄호를 넣는 것이 오래된 관행처럼 이어져 오고 있으니, 가급적 준수하여 주시기 바랍니다.
|
|
|
|
|
|
### if - else
|
|
|
|
if-else 문의 각 실행 블록은 대괄호로 묶어야 합니다. 이는 편집상의 실수를 방지하기 위함으로, 대괄호를 사용하지 않은 경우 다른 사람이 의도치 않게 if 블록에다 다른 줄을 추가하게 될 수가 있습니다. if 문의 영향을 받지 말아야 할 줄이라면 안좋은 일이겠지요. 더욱 안좋은 예라면 조건에 따라 컴파일되는 항목이 if/else 문을 깨지게 만드는 것입니다. 그러니 항상 대괄호로 묶어 주시기 바랍니다.
|
|
|
|
if(HaveUnrealLicense)
|
|
{
|
|
InsertYourGameHere();
|
|
}
|
|
else
|
|
{
|
|
CallMarkRein();
|
|
}
|
|
|
|
여러 갈래 if 문에서 각각의 else if 문은 첫 번째 if 문과 같은 양만큼 들여써 줘야 합니다. 그래야 읽는 사람이 구조를 쉽게 이해할 수 있습니다:
|
|
|
|
if(TannicAcid < 10)
|
|
{
|
|
Log("Low Acid");
|
|
}
|
|
else if(TannicAcid < 100)
|
|
{
|
|
Log("Medium Acid");
|
|
}
|
|
else
|
|
{
|
|
Log("High Acid");
|
|
}
|
|
|
|
|
|
### 탭
|
|
|
|
* 실행 블록별로 코드를 들여쓰세요.
|
|
|
|
* 줄 시작부분의 공백은 스페이스가 아니라 탭을 사용해 주시구요. 그래도 탭을 스페이스 몇 칸으로 지정했는지와 무관하게 코드 줄을 맞추기 위해 스페이스를 써야 할 때가 있습니다. 이를테면 탭 이외의 캐릭터에 코드 줄을 맞출 필요가 있을 때겠지요.
|
|
|
|
* C# 으로 코드를 작성하신다면 공백이 아니라 탭을 사용해 주시기 바랍니다. C# / C++ 사이의 전환은 프로그래머에게 자주 있는 일이고, 대부분은 그 탭 설정에 일관성이 있기 때문입니다. Visual Studio 기본값으로는 C# 파일에 공백을 사용하고 있으니, 언리얼 엔진 코드 작업을 할 때는 이 세팅을 바꿔줘야 한다는 점 기억해 주시기 바랍니다.
|
|
|
|
|
|
### Switch 문
|
|
|
|
빈 case 를 제외하고 (똑같은 코드를 갖는 다중 케이스의 경우), switch case 문에서는 다음 케이스로 넘어가는지를 명시적으로 밝혀줘야 합니다. 각각의 경우마다 break 를 넣던가, fall through 코멘트를 달아 주세요. 다른 코드 제어-이동 명령(return, continue 등)도 괜찮습니다.
|
|
|
|
default 케이스는 항상 만들어 두시고, 다른 사람이 그 디폴트 뒤에 새로운 케이스를 추가할 때에 대비해 break 도 넣어 두시기 바랍니다.
|
|
|
|
switch (condition)
|
|
{
|
|
case 1:
|
|
...
|
|
// falls through
|
|
case 2:
|
|
...
|
|
break;
|
|
case 3:
|
|
...
|
|
return;
|
|
case 4:
|
|
case 5:
|
|
...
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
|
|
## 네임스페이스
|
|
|
|
네임스페이스(Namespace)는 아래 규칙만 준수한다면 클래스, 함수, 변수의 체계를 적절히 잡는 데 사용할 수 있습니다.
|
|
|
|
* 언리얼 코드는 현재 글로벌 네임스페이스에 쌓여있지 않습니다. 전영 범위에서의 충돌을, 특히나 써드 파티 코드를 끌어들일 때는 주의를 기울여야 합니다.
|
|
* 전역 범위에는 "using" 선언을, .cpp 파일에서도 넣지 마시기 바랍니다 ("unity" 빌드 시스템에 문제가 생깁니다).
|
|
* 다른 네임스페이스 안이나 함수 본문 안에서는 "using" 선언을 넣어도 괜찮습니다.
|
|
* 참고로 네임스페이스 안에서 "using" 을 사용한다면, 동일 이동 단위 내의 또다른 해당 네임스페이스로 이어지게 됩니다. 일관되기만 하다면야 괜찮기는 합니다.
|
|
* 위의 규칙을 따른다면 헤더 파일에서만은 안전히 "using" 을 사용할 수 있습니다.
|
|
* 참고로 앞서 선언된 형은 각각의 네임스페이스 안에서 선언해 줘야 하며, 그렇지 않으면 링크 오류가 납니다.
|
|
* 한 네임스페이스 안에 다수의 클래스/유형을 선언하면, 다른 전역 범위의 클래스에서 사용하기가 어려워 집니다 (이를테면 함수 시그너처는 클래스 선언에 나타날 때 명시적 네임스페이스를 사용해야 합니다).
|
|
* (using Foo:FBar 과 같이) "using" 을 사용해서 네임스페이스 안의 특정 변수만 자신의 범위로 앨리어싱할 수도 있습니다만, 언리얼 코드에서는 보통 그렇게 하지 않습니다.
|
|
* C++ enum (열거형) 선언의 경우 enum 값 이름이 전역 범위에 있지 않도록 네임스페이스에 감싸두는 것이 좋습니다.
|
|
|
|
|
|
## C++ Enum 및 네임스페이스 범위
|
|
|
|
* 언리얼 엔진 코드에서 enum 유형 앞에는 항상 "E" 글자를 붙입니다.
|
|
|
|
* 모든 enum 은 영역 관리에 있어 네임스페이스(나 빈 구조체)를 사용합니다. 그 이유는 C++ 에서 enum 값은 enum 유형 자체와 같은 영역으로 제한되기 때문입니다. 이로 인해 이름 충돌이 생길 수 있고, 그렇게 되면 프로그래머는 그 값을 고유하게 만들기 위해 enum 값에 이상한 이름이나 접두사를 붙여야 하게 됩니다. 그 대신 항상 네임스페이스를 사용하여 새로운 enum 영역을 명시적으로 지정합니다. 네임스페이스 안의 실제 enum 유형 이름은 반드시 "Type" 으로 선언해야 합니다.
|
|
|
|
* 네임스페이스로 영역을 지정한 enum 예제입니다:
|
|
|
|
/* 네임스페이스 안에 enum 을 정의하여 C# 스타일 enum 영역 규칙 이뤄내기 */
|
|
namespace EColorChannel
|
|
{
|
|
/* EColorChannel::Type 을 이 enum 에 대한 실제 유형으로 선언합니다. */
|
|
enum Type
|
|
{
|
|
Red,
|
|
Green,
|
|
Blue
|
|
};
|
|
}
|
|
|
|
/* 컬러 채널이 주어지면, 그 채널의 이름을 반환합니다. */
|
|
FString GetNameForColorChannel(const EColorChannel::Type ColorChannel)
|
|
{
|
|
switch(ColorChannel)
|
|
{
|
|
case EColorChannel::Red: return TEXT("Red");
|
|
case EColorChannel::Green: return TEXT("Green");
|
|
case EColorChannel::Blue: return TEXT("Blue");
|
|
default: return TEXT("Unknown");
|
|
}
|
|
}
|
|
|
|
|
|
* 참고로 로컬 선언된 enum 의 경우 영역 지정에 네임스페이스를 사용할 수 없을 것입니다. 그러한 경우 멤버 변수 없이 로컬 enum type 만으로 로컬 구조체를 선언한 다음 그 구조체를 영역 지정에 사용하는 방법을 택합니다.
|
|
|
|
/* 구조체를 사용하여 로컬 영역 지정된 열거형 정의 */
|
|
class FObjectMover
|
|
{
|
|
public:
|
|
|
|
/* 이동 방향 */
|
|
struct EMoveDirection
|
|
{
|
|
enum Type
|
|
{
|
|
Forward,
|
|
Reverse,
|
|
};
|
|
};
|
|
|
|
/* 지정된 이동 방향으로 FObjectMover 생성 */
|
|
FObjectMover( const EMoveDirection::Type Direction );
|
|
}
|
|
|
|
|
|
## 물리적 종속성
|
|
|
|
* 파일 이름은 가급적 접두사를 붙이지 않는 것이 좋습니다. 예를 들면 UnScene.cpp 보다는 Scene.cpp 가 좋습니다. 그래야 Workspace Whiz 나 Visual Assist 같은 툴에서 Open File in Solution 같은 기능을 사용할 때, 원하는 파일을 명확히 구분해 내는 데 필요한 글자수를 줄이는 등 사용하기가 용이해 집니다.
|
|
* 모든 헤더는 #pragma once 디렉티브(지시자)로 복수의 include 를 방지해야 합니다. 참고로 요즘 사용하는 모든 컴파일러는 #pragma once 를 지원합니다.
|
|
|
|
#pragma once
|
|
|
|
<파일 내용물>
|
|
|
|
* 일반적으로는 물리적 결합을 최소화시켜 보세요.
|
|
** 헤더 include 대신 앞선 선언이 가능하면, 그리 하세요.
|
|
** 가급적이면 세세한 부분을 include 하세요. Core.h 를 include 하지 마시고, Core 의 헤더 중 정의가 필요한 특정 부분을 include 시키세요.
|
|
* 세세한 include 작업을 쉽게 하기 위해, 필요한 헤더는 전부 직접 include 해 주세요.
|
|
** 자신이 include 시킨 다른 헤더를 간접적으로 include 시키는 헤더에 의존하지 마세요.
|
|
** 다른 헤더를 통해 include 시키는 것 보다는, 필요한 것을 전부 include 하세요.
|
|
* 모듈에는 Private 와 Public 소스 디렉토리가 있습니다. 다른 모듈이 필요로 하는 정의는 Public 디렉토리의 헤더에 있어야 하나, 그 외 모든 것은 Private 디렉토리에 있어야 할 것입니다. 참고로 구형 언리얼 모듈의 경우 이 디렉토리는 Src 와 Inc 로 불리기도 하는데, 이름만 그렇다 뿐 같은 방식으로 프라이빗 코드와 퍼블릭 코드를 구분하기 위함일 뿐이지, 헤더 파일을 소스 파일과 구분하기 위함은 아닙니다.
|
|
* 미리컴파일된 헤더 생성용 헤더 셋업에 대해서는 걱정하지 마세요. UnrealBuildTool 이 더욱 잘 처리해 줄 것입니다.
|
|
|
|
|
|
## 일반적인 스타일 문제
|
|
|
|
* *종속성 거리를 최소화하세요*. 코드가 특정 값을 갖는 변수에 의존할 때는, 변수를 사용하기 직전에 그 값을 설정해 보도록 하세요. 실행 블록 상단에 변수 값을 초기설정해 둔 상태로 코드 수백 줄 동안 사용하지 않는다면, 그 종속성을 모르는 사람이 그 값을 바꾸게 될 여지가 많이 있습니다. 바로 다음 줄에 사용한다면 변수 초기설정을 왜 그렇게 했는지, 어디서 사용되는지를 명확히 알 수 있는 것입니다.
|
|
* *메소드는 가급적 서브-메소드로 분할하세요*. 인간은 세밀한 부분부터 시작해서 큰 그림을 재구성하기 보다는, 큰 그림을 먼저 그린 후 흥미를 끄는 세밀한 부분으로 파내려가는 것을 더 잘합니다. 마찬가지로, 모든 코드가 통째로 들어있는 메소드 보다는, 이름을 잘 지어둔 다수의 서브-메소드를 연속적으로 호출하는 단순한 메소드를 이해하기가 수월합니다.
|
|
* 함수 선언이나 함수 호출 위치에서 함수의 이름과 인수 목록에 선행되는 괄호 사이에 공백을 두지 마세요.
|
|
* *컴파일러 경고에 주의를 기울여 주세요.* 컴파일러 경고 메시지는 무언가 잘못되었다는 것을 뜻합니다. 컴파일러의 고민에 귀를 기울여 주세요. 전혀 그럴 수가 없다면 #pragma 를 억제시키면 되긴 하는데, 이는 최후의 방법입니다.
|
|
* *파일 끝에 빈 줄을 하나 놔두세요.* 모든 .cpp 와 .h 파일은 빈 줄이 있어야 gcc 에 제대로 돌아갑니다.
|
|
* *절대 float 가 int32 로 묵시적 변환되도록 하지 마세요.* 연산이 느릴 뿐만 아니라 모든 컴파일러에서 컴파일되지도 않습니다. 그 대신 항상 appTrunc() 함수를 사용하여 int32 로 변환하세요. 여러 컴파일러에 대한 호환성이 높아질 뿐만 아니라 더 빠른 코드가 생성됩니다.
|
|
* *보호 키워드로 캡슐화를 공고히 하세요.* 클래스의 공용 인터페이스 일부가 아니고서야, 클래스 멤버는 private 으로 선언해야 합니다.
|
|
* Interface (접두사 "I") 클래스는 항상 추상(abstract) 상태여야 하며 멤버 변수를 가져서는 안됩니다. Interface 는 순수 가상이 아닌 메서드, 심지어 가상이 아니거나 스태틱인 메서드도 내부적으로(inline) 구현하지 않는 한 포함시킬 수 있습니다.
|
|
* *가급적 const 를 사용하세요.* 특히 레퍼런스 파라미터나 클래스 메소드에서는요. const 는 컴파일러 지시자이기도 하면서 자체적인 설명이 되기도 합니다.
|
|
* *디버그 코드는 유용하며 잘 다듬어진 상태거나, 체크인을 하지 않거나 중 하나여야 합니다.* 디버그 코드가 다른 코드와 섞이면 다른 코드 읽기가 훨씬 힘들어 집니다.
|
|
* *복잡한 표현식은 중간 변수를 사용하여 단순화시키세요.* 복잡한 표현식을, 부모 표현식 내에서 그 의미가 잘 설명되도록 이름지은 작은 표현식들을 가진 중간 변수에 할당된 하위 표현식으로 나눌 수 있다면, 이해하기가 수월해질 것입니다. 예를 들어:
|
|
|
|
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();
|
|
}
|
|
|
|
|
|
* *오버라이딩 메소드 선언시에는 virtual 과 override 키워드를 사용하세요.* 부모 클래스의 가상 함수를 덮어쓰는 파생 클래스에서 가상 함수를 선언할 때는, *반드시* virtual 과 override 키워드를 둘 다 사용해야 합니다. 예:
|
|
|
|
class A
|
|
{
|
|
public:
|
|
virtual void F() {}
|
|
};
|
|
class B : public A
|
|
{
|
|
public:
|
|
virtual void F() override;
|
|
};
|
|
|
|
|
|
참고로 override 키워드는 최근에 추가된 관계로 이를 지키지 않은 코드가 많이 있는데, 편할 때 그런 코드에다 이 override 키워드를 추가해야 주면 좋을 것입니다.
|
|
|
|
* *포인터와 레퍼런스의 공백은 그 오른쪽에 딱 한 칸만 둬야 합니다.* 그래야 특정 유형에 대한 모든 포인터나 레퍼런스를 빠르게 Find in Files 할 수 있습니다.
|
|
|
|
이렇게 사용하시되:
|
|
|
|
FShaderType* Type
|
|
|
|
이렇게는 안됩니다:
|
|
|
|
FShaderType *Type
|
|
FShaderType * Type |