아래의 글들을 참고하였다
코딩표준 = 가독성
웬만하면 스네이크 케이스처럼 _ 언더바를 사용하지 않는다.
ex) IsAttack(), IsJumping(), ShouldRun()
// true일 경우 무슨 의미일까요?
bool CheckTea(FTea Tea);
// 이름을 통해 true일 경우 차가 신선하다는 것을 명확히 알 수 있습니다.
bool IsTeaFresh(FTea Tea);
interop기능을 사용하여 String과 언리얼 FString의 호환을 해야만한다.
코드 자체의 변수만 보고 어떤 내용인지 이해할 수 있어야 한다.
// 나쁜 예:
t = s + l - b;
// 좋은 예:
TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves;
주석은 코드를 보고 이해할 수 있는 내용보다
어떤 의도의 코드인지를 설명한다.
// 나쁜 예:
// Leaves 증가
++Leaves;
// 좋은 예:
// 찻잎이 더 있다는 것을 알았습니다.
++Leaves;
아래는 주석에 대한 설명을 종합해놓은 예시이다.
/** 마실 수 있는 오브젝트에 대한 인터페이스입니다. */
class IDrinkable
{
public:
/**
* 플레이어가 이 오브젝트를 마실 때 호출됩니다.
* @param OutFocusMultiplier - 반환되면 마신 사람의 포커스에 적용할 배수를 포함합니다.
* @param OutThirstQuenchingFraction - 반환되면 마신 사람의 갈증이 해소되는 프랙션을 포함합니다(0-1).
* @warning 마실 것이 적절히 준비된 이후에만 호출하세요.
*/
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(
const float VolumeOfWater,
const float TemperatureOfWater,
float& OutNewPotency
);
/** 차에 감미료를 추가합니다. 같은 당도를 내는 데 필요한 자당의 그램으로 측정합니다. */
void Sweeten(const float EquivalentGramsOfSucrose);
/** 일본에서 판매되는 차의 가치(엔화 단위)입니다. */
float GetPrice() const
{
return Price;
}
virtual void Drink(float& OutFocusMultiplier, float& OutThirstQuenchingFraction) override;
private:
/** 엔화 단위 가격입니다. */
float Price;
/** 현재 당도로, 자당 그램 단위입니다. */
float Sweetness;
};
float FTea::Steep(const float VolumeOfWater, const float TemperatureOfWater, float& OutNewPotency)
{
...
}
void FTea::Sweeten(const float EquivalentGramsOfSucrose)
{
...
}
void FTea::Drink(float& OutFocusMultiplier, float& OutThirstQuenchingFraction)
{
...
}
주석을 어떻게 달아야하는지 한눈에 볼 수 있다.
이 방식이 보다 깔끔하다고 함
(사실 사용해도 상관은 없다)
함수가
필드가 되었음에 유의.
TMap<FString, int32> MyMap;
// 기존 스타일
for (auto It = MyMap.CreateIterator(); It; ++It)
{
UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), It.Key(), *It.Value());
}
// 새 스타일
for (TPair<FString, int32>& Kvp : MyMap)
{
UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), *Kvp.Key, Kvp.Value);
}
위처럼 쓴다고 한다.
정확히 어떻게 동작하는지 모르겠다.
기존 스타일은 이터레이터를 통해, Map을 순환하면서 이터레이터의
Key(), Value()를 통해 값을 반환하는건 알겠는데,
새 스타일에선 이터레이터가 어떻게 쓰였는지 모르겠고,
다만 key, value를 통해 단순히 필드값으로 접근하는 것은 이해했다.
람다는 자유롭게 사용하라고 함.
(오히려 람다가 가독성이 떨어진다고 생각했는데..)
작성자의 의도를 선언하므로 코드 리뷰 과정에서 실수를 더욱 쉽게 잡아낼 수 있다고 한다.
/* 람다 종류에 대한 주의사항 설명 /
스테이풀 람다, 대규모 람다, 지연되지 않은 사소한 람다..
이해하고자 한다면 하겠지만 이런건
아직 하이레벨의 스킬로 보인다.
// 기존 열거형
UENUM()
namespace EThing
{
enum Type
{
Thing1,
Thing2
};
}
// **새 열거형**
UENUM()
enum class EThing : uint8
{
Thing1,
Thing2
}
// 기존 프로퍼티
UPROPERTY()
TEnumAsByte<EThing::Type> MyProperty;
// **새 프로퍼티**
UPROPERTY()
EThing MyProperty;
enum은 UENUM() 매크로를 붙여서
위와 같은 형식으로 사용하라고 한다.
// 열거형
UENUM()
enum class EThing : uint8
{
Thing1,
Thing2
}
// 플래그로 사용할 열거형
enum class EFlags
{
None = 0x00,
Flag1 = 0x01,
Flag2 = 0x02,
Flag3 = 0x04
};
ENUM_CLASS_FLAGS(EFlags)
UCLASS()
class UTeaOptions : public UObject
{
GENERATED_BODY()
public:
UPROPERTY()
int32 MaximumNumberOfCupsPerDay = 10;
UPROPERTY()
float CupWidth = 11.5f;
UPROPERTY()
FString TeaType = TEXT("Earl Grey");
UPROPERTY()
EDrinkingStyle DrinkingStyle = EDrinkingStyle::PinkyExtended;
};
위처럼 헤더에서 변수를 정의함과 동시에,
각 클래스 멤버변수의 디폴트 값을 선언하는 것을 말한다.
장단점이 존재한다.
장점
단점
ex) UObject의 SubObject, 베이스클래스, 여러단계에 걸쳐 얻어낸 값 등
경험에 의하면,
디폴트 멤버 이니셜라이저는 엔진의 코드보다 게임 코드에 적합.
그런데 굳이 사용하지 않아도 될 기능같다
그냥 모두 CPP 생성자에 적으면 그만인데..
switch (condition)
{
case 1:
...
// falls through
case 2:
...
break;
case 3:
...
return;
case 4:
case 5:
...
break;
default:
break;
}
음... 이해가 안된다.. 일단 스킵
// 기존 스타일
Trigger(TEXT("Soldier"), 5, true);.
// 새 스타일
const FName ObjectName = TEXT("Soldier");
const float CooldownInSeconds = 5;
const bool bVulnerableDuringCooldown = true;
Trigger(ObjectName, CooldownInSeconds, bVulnerableDuringCooldown);
// 기존 스타일 // default값을 지정함
FCup* MakeCupOfTea(
FTea* Tea,
bool bAddSugar = false,
bool bAddMilk = false,
bool bAddHoney = false,
bool bAddLemon = false
);
FCup* Cup = MakeCupOfTea(Tea, false, true, true);
// 새 스타일 // 플래그 enum을 생성하는 법 // 추가로 None을 제일 처음으로 둠
enum class ETeaFlags
{
None,
Sugar = 0x01,
Milk = 0x02,
Honey = 0x04,
Lemon = 0x08
};
ENUM_CLASS_FLAGS(ETeaFlags)
FCup* MakeCupOfTea(FTea* Tea, ETeaFlags Flags = ETeaFlags::None); //default
FCup* Cup = MakeCupOfTea(Tea, ETeaFlags::Milk | ETeaFlags::Honey); //bitwiseOR (비트연산자)
예시로 둔 두 코드가 같은 기능을 하는건 아닌 듯 보인다..
|
가 아니라 &
를 두어야 하지 않을까..
// 기존 스타일
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<
void Func(const FString& String);
void Func(bool bBool);
Func(TEXT("String")); // 부울 오버로드 호출!
위의 경우에 당연히 FString함수가 호출될 것으로 보이지만,
bool으로 인식해서 대입될 수도 있다고 한다.
주의
결론
내용을 적으면서 느낀거지만,
기능적인 요소보다는 가독성, 협업 위주의 조언이 많았다.
누군가 더 편하게 읽을 수 있도록 하는 것이 역시 코딩 표준인걸까
개발자로서 성장하는 데 큰 도움이 된 글이었습니다. 감사합니다.