본문 바로가기
Programming/C와 C++ 게임 코드로 알아보는 코딩의 기술(저자 오즈 모리하루)

01. 읽기 좋은 코드를 작성하는 기술

by minjunkim.dev 2020. 8. 22.

    모든 내용은 [오즈 모리하루 저, "C와 C++ 게임 코드로 알아보는 코딩의 기술", 한빛미디어] 를 기반으로 제 나름대로 이해하여 정리한 것입니다. 다소 부정확한 내용이 있을수도 있으니 이를 유념하고 봐주세요!


# 읽기 좋은 코드의 조건은 "의도를 명확하게 전달할 수 있는 코드" 라고도 할 수 있다.

- 다른 개발자가 읽더라도 쉽게 이해할 수 있는 코드를 작성하자.

- 읽기 좋은 코드는 가독성이라는 지표로 나타낼 수 있다.

- "코드의 가독성"은 코드의 보수성(코드의 변경, 추가, 테스트가 얼마나 용이한지를 나타내는 지표)에 영향을 줌

- "코드의 보수성을 높이는 방법"이 이 책의 주제이다.


# 복잡하게 커져 버린 코드의 문제를 단순화하고, 작게 나누는 기술이

코드의 가독성, 보수성 향상을 위한 핵심이다.

 

1. 변수와 상수

1) 다소 길더라도 의미와 사용목적이 명확한 변수명 붙이기

2) 본인만이 알고 있는 코드 내부에 들어 있는 상수값(매직넘버)에

이름 붙이기 : 매크로, 열거형 등을 통해 적절한 이름을 붙이자.

enum State
{
    STATE_WALK, // 걷기 상태
    STATE_JUMP, // 점프 상태
    STATE_ATTACK / 공격 상태
};

2. 조건식과 계산식

1) 설명 전용 변수를 사용하자.(가독성을 높일 수 있다.)

- 설명 전용 변수는 값을 변경할 필요가 없으므로 const를 사용하자.

- 조건식과 계산식 모두에 적용이 가능하다.

const bool isJump = y > 0.0f;
const bool isDamage = state == STATE_DAMAGE;
const bool isDash = (speed >= 10.0f) && !isJump && !isDamage;
if(isDash)
{
    ...
}

 

 

2) 계산식 또는 조건식을 함수화 하자.

- 의도를 명확히 하고, 재사용 및 단위 테스트가 용이해진다.

- 한 줄마다의 정보량이 최대한 적은 것이 가독성이 좋다.

- 함수 분할의 단위는 줄 수가 아니며 하나의 의미(기능)를 나타내는 코드라면

비록 한줄이라고 해도 함수화해야 하는 후보이다.

bool isJump()
{
    return y > 0.0f;
}

bool isDamage()
{
    return state == STATE_DAMAGE;
}

bool isDash()
{
    if(isJump()) return false;
    if(isDamage()) return false;
    if speed < 10.0f) return false
    return true;
}

if(isDash())
{
    ...
}

3. assert 활용

- 프로그램의 사전 조건과 사후 조건을 명시적으로 활용하기 위한

assert(명시)의 활용 방법을 소개한다.

- 매개변수의 조건식이 false인 경우, 오류 메시지를 표시하고

프로그램을 중지시키는 매크로로, 디버그를 지원하기 위한 기능이다.(디버그 모드 전용 기능)

- 주로 함수의 매개변수와 계산결과의 타당성 체크 등에 활용한다.

- assert 함수 내부에는 사전 조건을 충족(true)하는 조건을 입력한다.(오류 조건을 쓰는 것이 아님)

void display_week(int week)
{
    static const char* name[]=
    {"Sun", "Mon", "The", "Wed", "Thu", "Fri", "Sat"};
    assert(0 <= week && week <= 6}; // 사전 조건을 명시
    std::cout << name[week];
}

- switch 조건문의 default 절에 assert를 사용해서 조건의 불일치를 확인할 수도 있음

switch(state)
{
    case STATE_WAIT: wait(); break;
    case STATE_WALK: walk(); break;
    case STATE_JUMP: jump(); break;
    default: assert(!"부정확한 상태");
}

* C/C++에서 문자열 상수는 0번지 이외의 주소에 저장되므로,

!"주석" 의 형태는 항상 false로 평가됨

- assert((조건식) && "주석"); => 주석 부분은 항상 true이므로

조건식의 true/false에 의해 assert의 실행 유무가 결정됨
- assert로 정지되면 콜스택의 이력을 확인하여 어떤 함수 호출 경로가 문제였는지 확인하자.

- release 모드에서는 assert 내부의 코드를 실행하지 않는다.(비활성화 된다.)

- 실행속도에 제한이 없는 애플리케이션에서는, release 모드일 때도 무효화되지 않는

assert 함수를 만들어 사용하는 경우도 있다.


4. 제어문

- 코드를 복잡하게 만드는 근원은 if 조건문 또는 for 반복문 등의 제어문

- 제어문이 뒤섞여 버린 "스파게티 코드"는 처리의 흐름을 추적하기 어렵고 보수도 힘들다.

- C++에는 STL(표준 템플릿 라이브러리)이 있기 때문에

if 조건문, for 반복문을 사용하는 알고리즘을 대부분 일반적인 알고리즘 함수로 변환할 수 있다.

 

1) 하한값, 상한값 보정 단순화

template <typename T>
T clamp(int x, T low, T high)
{
    assert(low <= high); // 사전조건 체크
    return std::min(std::max(x, low), high);
}

 

2) 랩 어라운드의 함수화

- 랩 어라운드(wrap around) : 어떤 숫자가 상한에 이르면 하한값으로 되돌려주고 다시 계산한다.

하한값에서 상한값 사이의 숫자를 반복하고 싶을때 사용한다.

- 나머지 연산자로만 구현하면 하한값 아래로 내려갔을 경우

상한값으로 바꾸는 마이너스 방향의 랩 어라운드를 만들수 없다.

int wrap(int x, int low, int high)
{
    assert(low < high); // 사전조건 체크
    const int n = (x - low) % (high - low);
    return (n >= 0) ? (n + low) : (n + high);
}
/*
하한값과 상한값을 자유롭게 설정 가능,
음수값도 지정할 수 있음
*/

// 0-9 내부를 랩 어라운드
wrap(x, 0, 10); // 상한값은 포함하지 않음

- C++에서는 부동소수점 자료형(float, double 등)에 나머지 연산자를 사용할 수 없으므로,

부동소수점 숫자의 나머지를 구하는 fmod 함수를 사용해서 별도의 함수를 만들어야 한다.

float wrap(float x, float low, float high)
{
    assert(low < high); // 사전조건 체크
    const float n = std::fmod(x - low, high - low);
    return (n >= 0.0) ? (n + low) : (n + high);
}

 

3) 조기 리턴 활용

- 중첩된 if 조건문은 보기 좋지 않으며, 가독성도 매우 떨어진다.

- 함수의 입구에서 예외 조건을 확인하고, 조기에 리턴하는 것을 "조기 리턴"이라고 한다.

- if 조건문과 return을 함께 사용하자.

int bonus(int time, int hp)
{
    if(time > 100) return 0;
    if(time > 50) return 200;
    if(hp < 30) return 500;
    return 1000;
}

- 1개의 조건식을 리턴하는 경우에는,

1) 조건식을 bool로 리턴할 때는 조건식을 그냥 return에 입력

2) 숫자로 리턴할 때는 삼항 연산자를 사용

bool isDead()
{
    return health <= 0; // 조건식의 결과를 불(bool)로 리턴
}

int bonus(int time)
{
    return (time < 10) ? 1000 : 0; // 값의 경우는 삼항 연산자 사용
}

 

4) 중복된 조건식, 복합 조건 통합

- 여러개의 조건문이 사용될 때 중복되는지 확인하고,
if 조건문의 순서를 변경하거나 조기 리턴을 활용하면 단순하게 바꿀 수 있다.

- 복합조건이 중복되면 하나로 통합하자.

if(state == STATE_FALL && wait_timer > WAIT_TIME)
    fall();

if(state == STATE_MOVE && wait_timer > WAIT_TIME)
    move();
    
/* 중복된 복합 조건을 분리해서
1개의 if 조건문으로 통합하거나,
조기 리턴 활용*/

if(wait_timer <= WAIT_TIMRE) return; // 조기리턴

if(state == STATE_FALL)
    fall();
if(state == STATE_MOVE)
    move();

6) 조건식이 직접적으로 관련된 부분을 국소화

- 조건식이 직접 관곟는 부분만 따로 뗴어내서 국소화하자. 

if 조건문이 작아지고 코드의 중복을 피하는 효과가 있다.
- if 조건문을 작고 단순하게 만들자.

if(isDash())
    position += direction * 10.0f; // 대시 중에는 속도가 2배
else
    position += direciton * 5.0f;

/* 계산식 부분이 중복되고 있고,
대시 조건식이 직접 관계하는 부분은 속도의 수치이므로
속도 변화 부분만 따로 빼서 국소화 */

enum Speed
{
    SPEED_DASH 10.0f,
    SPEED_NORMAL 5.0f
};

float speed()
{
    return isDash() ? SPEED_DASH0f : SPEED_NORMAL;
}

position += direction * speed();

 

7) 배열을 활용한 if 조건문 제거

- 배열 등의 자료구조를 사용하면 쓸데없이 긴 if 조건문을 제거할 수 있다.

/* 0 - 3까지의 순서를 특정한 값으로 교환하는 예 */

int id_to_num(int id)
{
    static const int table[] = {10, 15, 30, 50};
    // static 선언을 통해 쓸데없는 반복 초기화를 피함
    
    assert((0 <= id && id <= 3) && " 부정확한 ID");
    return table[id];
}

/* 위와 반대의 예 */

int num_to_id(int num)
{
    static const int table[] = {10, 15, 30, 50};
    for(int i = 0; i < 4; ++i)
    {
        if(table[i] == num)
            return i;
    }
    assert(!"부정확한 숫자");
    return 0;
}

/*
STL의 find 함수 사용 
find 함수는 배열 내부의 데이터를 검색하는 함수
*/

int num_to_id(int num)
{
    static const int table[] = {10, 15, 30, 50};
    assert(std::find(&table[0], &table[4], num) != &table[4]);
    return std::find(&table[0], &table[4], num) - &table[0]);
}

/* unordered_map 클래스 활용
C++11부터 추가된 연관 배열
순번의 수치뿐만 아니라, 문자열 또는 임의의 숫자를 인덱스로 사용할 수 있음
내부 자료 구조는 해시구조 */

int num_to_id(int num)
{
    static onst std::unordered_map<int, int> table = {
     {10, 0}, {15, 1}, {30, 2}, {50, 3}
    };
    assert((table.find(num) != table.end()) && "부정확한 숫자");
    return table.at(num);
}

- C++에서는 함수 내부에서 선언한 클래스 자료형의 static 변수는,

"처음 함수가 호출될 때" 한번만 초기화된다.

- 함수 외부에 선언된 클래스 자료형의 변수는 "프로그램을 실행할 때" 한번만 초기화된다.

따라서 main 함수가 호출되기 전에 초기화가 종료된다.

/* 아이템의 id를 통해 무기의 종류를 확인하는 함수 */
bool isWeapon(Item id)
{
    return (id == SWORD) || (id == SPEAR) || (id == KNIFE);
}

/* 확인하려는 항목이 많아지면 가독성이 떨어지므로..
배열을 활용하자. */

bool isWeapon(Item id)
{
    static const Item weapon[] = {SWORD, SPEAR, KNIFE};
    return std::find(&weapon[0], &weapon[3], id) != &weapon[3];
}

/* unordered_set 클래스는
unordered_map 클래스의 키 부분만을 가지고 있는 자료구조
*/

const std::unordered_set<Item> WeaponID = {SWORD, SPEAR, KNIFE};

bool isWeapon(Item id)
{
    return WeaponID.find(id) != WeaponID.end();
}

 

8) 결정표를 사용한 if 조건문 제거

enum Hand
{
    Rock = 0,
    Scissors = 1,
    Paper = 2
};

enum Result
{
    Win,
    Lose,
    Draw
};

Result judgement(Hand my, Hand target)
{
    static const Result result[3][3] = {
     {Draw, Win, Lose},
     {Lose, Draw, Win},
     {Win, Lose, Draw}
    };
    
    return result[my][target];
}

 

9) null 객체 도입

- null 객체 : null 포인터(nullptr)을 대신하는 더미 객체를 의미

- nullptr(null 포인터) 대신 null 객체를 도입하면 null 체크를 피할 수 있다.

- 이 기술을 사용하려면 대상 클래스에 부모 클래스가 있어야 하며,

모든 멤버함수는 가상함수여야 한다.

- 여러개의 null 체크를 해야하는 설계는 되도록 피하자.

 

class Actor
{
    ... 생략
    // 이동
    virtual void move() = 0;
    // 렌더링
    virtual void draw() = 0;
};

class NullActor : public Actor
{
    ... 생략
    // 이동
    virtual void move() override {} // 아무것도 하지 않음
    // 렌더링
    virtual void draw() override {} // 아무것도 하지 않음
};

// 더미 전용 null 객체를 넣어둠
player = new NullActor();

/* 객체의 존재유무를 따로 확인하지 않아도 됨 */
// 이동
player->move();
... 생략
player->draw();

 

10) for 반복문 감축과 단순화

- for 반복문의 기본 원칙은 "1개의 작업만 반복"

- for 반복문을 사용한 알고리즘 대부분은 STL 알고리즘 함수로 변환할 수 있음

- STL 함수 어댑터를 사용할 줄 알면 좋다.

- 독자적인 반복문을 작성하지 말고, STL 알고리즘을 사용하자.

코드가 자기 설명적이 되고 가독성도 높아진다.

- 버그 없는 신뢰성 높은 STL 함수를 사용하면 코드의 품질이 높아진다.

 

* STL 알고리즘 함수

예약어 설명
for_each 모든 요소를 지정한 함수로 조작
find 요소 검색
find_if 지정한 조건을 만족하는 요소 검색
min_element 최소 요소 리턴
max_element 최대 요소 리턴
sort 요소 정렬
count 지정한 숫자와 일치하는 요소 수 리턴
count_if 지정한 조건을 만족하는 요소 수 리턴
all_of 모든 요소가 지정한 조건을 만족하면 true 리턴
none_of 모든 요소가 지정한 조건을 만족하지 않으면 true 리턴
any_of 모든 요소 중에 어느 하나라도 지정한 조건을 만족하면 true 리턴
fill 모든 요소에 지정한 값을 대입
copy 모든 요소를 복사
copy_if 지정한 조건을 만족하는 요소만 복사
generate 모든 요소에 지정한 연산 결과를 대입
transform 모든 요소를 지정한 함수로 변환
remove 지정한 숫자에 일치하는 요소를 제거
remove_if 지정한 조건을 만족하는 요소를 제거
replace 지정한 숫자를 지정한 숫자로 변경
replace_if 지정한 조건에 만족하는 요소를 지정한 숫자로 변경
random_shuffle 모든 요소를 셔플
accumulate 모든 요소의 집계 계산

1) for_each 함수 사용법

// 모든 캐릭터 렌더링
for(iter i = actors.begin(); i != actors.end(); ++i)
{
    (*i)->draw();
}

/* for 반복문을 for_each 함수로 변경 */

list<Actor*> actors; // Actor 포인터의 컨테이너

std::for_each(actors.begin(), actors.end(), std::mem_fun(&Actor::draw));
/* 컨테이너에 포인터가 저장되어 있으므로,
mem_fun 템플릿을 사용
만약 컨테이너에 객체나 레퍼런스가 저장되어 있다면,
mem_fun_ref 템플릿을 사용해야 한다.
*/

2) 람다식(무명 함수) 사용

- C++11부터 사용 가능

- 작은 일회용 함수를 만드는데 편리한 기능

- 함수의 매개변수 부분에 작은 함수를 쓸수 있게 해준다.

- 람다식을 사용하면 복잡한 STL 함수객체를 쓰지 않아도 된다.

따라서 STL 알고리즘 함수와 람다식을 함께 사용하면 매우 강력하다.

 

* 람다식

[ 캡쳐리스트(외부에 있는 변수) ] (람다식의 인자) ->반환형(void시 생략가능) { 람다식 내부; }
- 람다식을 사용하면 메모리상에 임시로 존재하는 "클로저 객체"가 생성된다.
- 클로저 객체는 함수 객체처럼 동작한다.

- [&]() : 외부의 모든 변수들을 레퍼런스로 가져옴

- [=]() : 외부의 모든 변수들을 값으로 가져옴

e.g. [](Actor* actor) ->void(void시 생략가능) { actor->draw(); }
- 비슷한 형태의 for문이 반복되는 경우,
이를 함수로 일반화시키고 람다식을 인자로 전달하는 형태로도 구현 가능하다.

// 람다식을 사용한 예
std::for_each(actors.begin(), actors.end(),
[](Actor* actor) { actor->draw(); });
/* find_if 함수 : 조건에 일치하는 것을 검색하는 함수
검색 성공시 => 해당 요소의 반복자를 리턴
검색 실패시 => 반복자의 end()가 리턴
*/

// 플레이어 캐릭터 검색
auto player = std::find_if(actors.begin(), actors.end(),
[](Actor *actor) ->bool { return actor->id() == PLAYER; });

if(player != actors.end())
    attack(*player); // 공격
/* min_element 함수는 전체 요소에서 최솟값을 검색 */

// 가장 가까운 거리에 있는 캐릭터 검색
auto i = std::min_element(actors.begin(), actors.end(),
[&](Actor* actor, Actor* min) ->bool 
{ return actor->distance(position) < min->distance(position); });

attack(*i);
/* all_of 함수는 C++11에서 추가된 함수
전체 요소에 대해 조건이 true인지를 확인
none_of, any_of 함수도 있음 */

// 모두 죽었는지 확인
bool all_dead = std::all_of(actors.begin(), actors.end(),
[](Actor* actor) ->bool { return actor->isDead(); });

if(all_dead)
{
    // 모두 죽은 경우
}
/* count_if 함수는 조건이 true인 요소가 몇개 있는지 확인 */

// 생존자 수 계산
int count = std::count_if(actors.begin(), actors.end(),
[](Actor* actor) ->bool { return !actor->isDead(); });
/* accumulate 함수는 각각의 요소를 집계함
주로 합계를 구할 때 사용 */

// 점수 합계 계산
int totalScore = std::accumulate(actors.begin(), actors.end(),
[](int acc, Actor* actor) ->int { return acc + actor->score(); });

// 최고 점수 계산
int maxScore = std::accumulate(actors.begin(), actors.end(),
[](int acc, Actor* actor) ->int { return std::max(acc, actor->score()); });
/* copy_if 함수는 조건에 맞는 요소만을 복사하는 함수
C++11에서 새로 추가된 함수
조건에 맞는 요소만을 추출할 때 사용 */

std::vector<Actor*> nearActors; // 인접 범위 내부에 있는 캐릭터 저장 전용
std::copy_if(actors.begin(), actors.end(),
std::back_insertor(nearActors),
[&](Actor* actor) ->bool { return actor->distance(position) <= 5; });

- back_inserter 함수 : 매개변수로 지정한 컨테이너에 요소가 복사될 때마다

push_back 함수를 자동적으로 호출하는 삽입 이터레이터 함수

e.g. std::copy_if(begin(),end(),std::back_insterter(원본이 아닌 복사본 컨테이너를 전달),(람다식)) :

람다식 내부의 조건문이 true인 원본을 복사본에 copy함

 

11) 검색 반복문 분리

- 두 작업을 동시에 반복하고 있다면 분리해주자.

// 공격 범위 내부에 있는 캐릭터 검색

Actor* findTarget(const Vector2& position, float range)
{
    auto target = std::find_if(actors.begin(), actors.end(),
[&](Actor* actor) ->bool { return actor->distance(position) <= range; });
    return (target != actors.end()) ? *target : nullptr;
}

// 공격 범위 내부에 있다면 공격

auto target = findTarget(position, 5);
if(target != nullptr)
{
    // 공격
    attack(*target);
}

 

12) 반복문 내부의 불필요한 조건 분리

- 초보자의 경우, 불필요하게 반복문 안에 조건문을 넣어
불필요한 조건문 반복처리를 하는 경우가 있으므로, 반복문 밖으로 조건문을 빼주자.
- 반복문 내부의 처리를 최소화한 후, STL 알고리즘 함수로 치환하자.

 

13) 반복문 분할

- 하나의 for 반복문에 여러개의 처리를 집어넣으면 STL 알고리즘 함수로 치환이 불가능

- 반복문 내부의 처리는 최소화하자.

// 소지금의 합계를 구하는 반복문
int totalGold = 0;
for(iter i = party.begin(); i != party.end(); ++i)
{
    totalHealth += (*i)->gold();
}


// 체력의 합계를 구하는 반복문
int totalHealth = 0;
for(iter i = party.begin(); i != party.end(); ++i)
{
    totalHealth += (*i)->health();
}

/* STL의 accumulate 함수로 코드를 변경 */

int totalGold()
{
    return std::accumulate(party.begin(), party.end(), 0,
    [](int acc, Actor* actor) ->int { return acc + actor->gold(); });
}

int totalHealth()
{
    return std::accumulate(party.begin(), party.end(), 0,
    [](int acc, Actor* actor) ->int { return acc + actor->health(); });
}

 

14) 람다식을 활용한 반복문 일반화

// 블록 업데이트
for (int y = 0; y < HEIGHT; ++y)
{
    for(int x = 0; x < WIDTH; ++x)
    {
        block[y][x].update(deltaTime);
    }
}

// 블록 렌더링
for (int y = 0; y < HEIGHT; ++y)
{
    for(int x = 0; x < WIDTH; ++x)
    {
        block[y][x].draw();
    }
}

/* for 반복문의 이중 반복문 부분을 함수화해서 일반화 */

void each(std::functon<void (Block&)> action)
{
    for (int y = 0; y < HEIGHT; ++y)
    {
        for(int x = 0; x < WIDTH; ++x)
        {
            action(block[y][x]);
        }
    }
}

/* 함수 템플릿의 매개변수에도 람다식을 전달하는
매개변수를 지정할 수 있음 */

template <typename T> // F는 람다식 또는 함수 포인터
void each(F action)
{
    for(int y = 0; y < HEIGHT; ++y)
    {
        for(int x = 0; x < WIDTH; ++x)
        {
            action(block[y][x]);
        }
    }
}

// 블록 업데이트
each([&](Block& block) { block.update(deltaTime); });
// 블록 렌더링
each([](Block& block) { block.draw(); });

- std::function : 람다식, 함수포인터, 함수객체를 대입할 수 있는 범용적인 함수래퍼클래스

람다식 또는 함수포인터를 대입해 변수를 생성하는 기능이라고 생각하면 편함

e.g. std::function<(반환형) (인자)> (람다식내부)

 

- 구현상의 문제로 STL의 컨테이너 클래스를 사용할 수 없을 때,

독자적인 자료구조를 만들어 사용하는 경우도 있는데,

독자적인 자료구조의 내부에 접근하는 과정에서도 람다식 활용이 가능하다.

void each(std::function<void (Node&)> action)
{
    for(Node* node = list.head; node != NULL; node = node->next)
    {
        action(*node):
    }
}

// 노드 업데이트
each([](Node& node) { node.update(); });
// 노드 렌더링
each([](Node& node) { node.draw(); });

// 검색
Node* find(std::function<bool (Node&)> predicate)
{
    for(Node* node = list.head; node != NULL; node = node->next)
    {
        if(predicate(*node))
        {
            return node;
        }
    }
    return NULL;
}

// 최댓값
Node* max_element(std::function<bool (Node&, Node&)> predicate)
{
    Node *max = list.head;
    if(max == NULL) return NULL;
    
    for(Node* node = max->next; node != NULL; node = node->next)
    {
        if(predicate(*max, *node))
        {
            max = node;
        }
    }
    return max;
}

// 합계
int accumulate(int seed, std::function<int (int, Node&)> f)
{
    int result = seed;
    each([&](Node& node) { result = f(result, node); });
    return result;
}

 

15) case 내부의 함수화

- 상태 변화 처리시 switch 조건문을 많이 사용하지만,

switch 조건문을 남용하면 코드가 복잡해진다.

- case 내부에는 복잡한 코드를 작성하지 말고, 분기에만 집중하자.

void update(float deltaTime)
{
    switch(state)
    {
    case STATE_RUN: run(deltaTime); break;
    case STATE_JUMP: jump(delatTime); break;
    case STATE_DAMAGE: damage(deltaTime); break;
    }
}

- case 내부가 여러줄로 구성되면 가독성이 떨어지므로, 가능한 case 내부는 함수화하자.

 

16) default에 assert

- case에 반드시 일치해야 하는 경우에는 default 부분에 assert를 사용하자.

switch(state)
{
    case STATE_RUN: run(deltaTime); break;
    case STATE_JUMP: jump(delatTime); break;
    case STATE_DAMAGE: damage(deltaTime); break;
    default: assert(!"부정확한 상태"); break;
}

 

17) 다형성

- switch 조건문의 대부분은 다형성을 사용한 분기로 변경할수 있다.

- 가상함수를 사용한 분기로 변경해보자.

- 상태변화를 switch 조건문으로 처리하는 경우에는 State 패턴으로 변경할 수 있다.

 

* State 패턴

- 각 상태를 클래스화해서 다룬다.

- 상태를 나타내는 부분을 추상 인터페이스로 만들고, 상태의 종류에 따라 클래스화해서 구현한다.

- 추상 인터페이스를 각 상태의 종류에 해당하는 클래스가 상속 받아 오버라이드

=> 다형성으로 각 상태 처리를 분기하면 된다.

// 상태 추상 인터페이스
class State
{
public:
    virtual ~State() {}
    // 업데이트
    virtual void update(float deltaTime) = 0; // 순수 가상함수
    ... 생략
};

// 걷기 상태 구현 클래스
class RunState : public State
{
public:
    // 업데이트
    virtual void update(float deltaTime) override;
    ... 생략
};

// 점프 상태 구현 클래스
class JumpState : public State
{
public:
    // 업데이트
    virtual void update(float deltaTime) override;
    ... 생략
};


// 데미지 상태 구현 클래스
class DamageState : public State
{
public:
    // 업데이트
    virtual void update(float deltaTime) override;
    ... 생략
};

// 플레이어 클래스
class Player
{
public:
    ... 생략
    // 업데이트
    void update(float deltaTime)
    {   
         // 다형성으로 각 클래스의 update 함수 호출
         currentState_->update(deltaTime);
    }
private:
    State* currentState_; // 현재 상태
    ... 생략
};

- 다루는 상태의 수가 적거나, 각 상태의 처리가 단순하다면

switch 조건문을 사용하는 편이 좋을 수 있으므로,

프로그램의 규모에 따라 판단해서 사용하자.

 


5. 함수

- 함수화 기술은 보수성 높은 코드를 작성하는 기본

- 함수의 기본 원칙

1) 1개의 함수에는 1개의 역할

기능이 적은 함수일수록 재사용하기 쉽고, 변경에 대해 영향도 적게 받음 => 보수성이 높아짐

 

* std::accumulate(begin(),end(),init_value(반환값이 이 변수의 타입을 따라감), (생략가능) )

// 합계 계산
int sum(std::vector<int>& a)
{
    return std::accumulate(a.begin(), a.end(), 0);
}

// 평균 계산
float average(std::vector<int>& a)
{
    return !a.empty() ? ((float)sum(a) / a.size()): 0.0f;
}

// 합계와 평균 계산
// 두 개의 함수를 조합
void sumAndAverage(std::vector<int>& a, int& total, float& avg)
{
    total = sum(a);
    avg = average(a);
}

2) 함수를 두 종류로 구분

- 첫번째는 계산과 알고리즘을 실행하고 실제 작업을 수행하는 함수

- 두번째는 첫번째 함수들을 조합해서 흐름을 만드는 함수

// 이동(실제 작업)
void move(const GameTime& deltaTime)
{
    for(iter i = actors.begin(), i != actors.end(); ++i)
    {
        (*i)->move(deltaTime);
    }
}

// 충돌 판정(실제 작업)
void collide()
{
    for(iter i = actors.begin(); i != actors.end(); ++i)
    {
        for(iter j = std::next(i); j != actors.end(); ++j)
        {
            (*i)->collide(*j);
        }
    }
}

// 업데이트
void update(const GameTime& deltaTime)
{
    // 이동 처리(실제 작업 부분)
    move(deltaTime);
    
    // 충돌 판정(다른 함수를 사용하는 부분)
    collide();
}

- 큰일을 하는 복잡한 함수는 작은 일을 하는 단순한 함수로 분할해주자.

 

 

* 함수화 패턴 :

코드에 중복되는 부분이 있다면, 해당 부분은 함수화해야 하는 부분이나

그것만이 함수화 대상은 아니다.

 

1) 조건식 함수화

- 조건식에 의미 있는 이름을 붙일 수 있다.

- 복합 조건을 if 조건문과 return의 조합으로 쓸 수 있다.(조기 리턴 활용)

- 3개 이상의 조건이 복합적으로 이루어지는 조건식은 나눠서 작성하면 가독성이 높아진다.

- 삼항연산자( ? : ;)의 남용은 금물(가독성이 떨어짐)

 

2) 계산식 함수화

- 계산식에 의미 있는 이름을 붙일 수 있다.

float length(float x, float y)
{
    return std::sqrt(x * x + y * y);
}

// 벡터의 길이 계산
float len = length(x, y);

3) 조건 분기의 블록 내부 함수화

- if 조건문 또는 switch 조건문의 블록 내부를 함수화

- if 조건문과 swicth 조건문 내부는 가능한 단순하게 만들고,

특히 다른 제어문을 중첩하는 것은 되도록 피하자.

 

4) 반복문 함수화

- 1개의 반복문에 대해서 1개의 함수를 만들어주자.

- 특히, STL 알고리즘 함수로 변환할 수 없는 특수한 반복문의 경우에는 반드시 함수화하자.

// 이동(실제 작업)
void move(const GameTime& deltaTime)
{
    for(iter i = actors.begin(), i != actors.end(); ++i)
    {
        (*i)->move(deltaTime);
    }
}

// 충돌 판정(실제 작업)
void collide()
{
    for(iter i = actors.begin(); i != actors.end(); ++i)
    {
        for(iter j = std::next(i); j != actors.end(); ++j)
        {
            (*i)->collide(*j);
        }
    }
}

/* STL 알고리즘 함수로 변경 */
void move(const GameTime& deltaTime)
{
    std::for_each(actors.begin(), actors.end(),
    [&](Actor* actor) { actor->update(deltaTime); });
}

void collide()
{
    for(iter i = actors.begin(); i != actors.end(); ++i)
    {
        std::for_each(std::next(i), actors.end(),
        [&](Actor* actor) { (*i)->collide(actor); });
    }
}

 

5) 반복문의 블록 내부 함수화

// 공격 가능한지 확인
bool canAttack(const Player& player, const Actor& target)
{
    if(target.isDead()) return false;
    if(player.group_id() == target.group_id()) return false;
    if(player.state() != PLAYER_ATTACK) return false;
    if(!player.isCollide(target)) return false;
    return true;
}

void attack(Player & player, Actor& target)
{
    if(!canAttack(player, target)) return;
    player.attack(target);
}

for(iter i = actors.begin(), i != actor.end(); ++i)
{
    attack(*player, **i);
}

/* STL 알고리즘을 사용해 변경 */
std::for_each(actors.begin(), actors.end(),
[&](Actor* target) { attack(*player, *target); });

- 반복문 블록 내부를 함수화하기 힘들 때는,

반복문 내부에서 2가지 이상의 일을 하고있지는 않은지 체크하자.

- 반복문 블록 내부를 함수화해서 STL 알고리즘 함수를 사용할 수 있는 상태까지 단순화하자.

 

6) 데이터 변환 함수화

- 데이터 변환은 if 조건문을 사용하거나, 변환 전용 자료구조를 사용한다.

// 요일 열거형
enum class Week // C++11부터의 열거형
{
    Sunday,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday
};

// 열거형을 문자열로 변환하는 자료구조(cpp 파일 내부)
const std::unorderd_map<Week, std::string> WeekToName =
{
    {Week::Sunday, "Sun"},
    {Week::Monday, "Mon"},
    {Week::Tuesday, "Tue"},
    {Week::Wednesday, "Wed"},
    {Week::Thursday, "Thu"},
    {Week::Friday, "Fri"},
    {Week::Saturday, "Sat"}
};

// 요일 자료형을 문자열로 변환
std::string to_string(Week week)
{
    assert(WeekToName.find(week) != WeekToName.end());
    return WeekToName.at(week);
}

// 문자열을 열거형으로 변환하는 자료구조(cpp 파일 내부)
const std::unorderd_map<std::string, Week> NameToWEEK =
{
    {"Sun", Week::Sunday},
    {"Mon", Week::Monday},
    {"Tue", Week::Tuesday},
    {"Wed", Week::Wednesday},
    {"Thu", Week::Thursday},
    {"Fri", Week::Friday},
    {"Sat", Week::Saturday}
};

// 문자열을 요일 자료형으로 변환
std::string to_week(const std::string& name)
{
    assert(NameToWEEK.find(name) != NameToWEEK.end());
    return NameToWEEK.at(name);
}

// 상태를 이미지로 변환
Image* to_image(State state)
{
    if(state == STATE_STAND) return imageStand;
    if(state == STATE_RUN) return imageRun;
    if(state == STATE_JUMP) return imageJump;
    assert(!"정해지지 않은 상태");
    return nullptr;
}

// 상태에 따라 렌더링하는 이미지 변경
renderer.drawImage(to_image(state), position);

7) 데이터 확인 함수화

- 데이터 변환과 마찬가지로, 여러개의 if 조건문 또는 확인 전용 자료구조가 필요하다.

// 적인지 확인
bool isEnemy(ActorID id)
{
    if(id == ActorID::Slime) return true;
    if(id == ActorID::Goblin) return true;
    if(id == ActorID::Dragon) return true;
    return false;
}

// EnemyID로 확인하기 위한 자료구조
const std::unordered_set<ActorID> EnemyID = 
{
    ActorID::Slime,
    ActorID::Goblin,
    ActorID::Dragon
};

// 적인지 확인
bool isEnemy(ActorID id)
{
    return EnemyID.find(id) != EnemyID.end();
}

- 데이터가 범위 내부에 있는지 확인하는 부분도 함수화하면 좋다.

// 좌표가 맵 내부에 있는지 확인
bool isInside(int x, int y)
{
    return (0 <= x && x <= MAP_WIDTH)
        && (0 <= y && y <= MAP_HEIGHT);
}

8) 배열 접근 함수화

- 1차원 배열 또는 2차원 배열에 랜덤 접근하는 경우에는 배열에 접근하는 함수를 만들자.

// 접근 전용 함수
Block getBlock(int x, int y)
{
    assert((0 <= x && x < WIDTH) && (0 <= y && y < HEIGHT));
    return blocks[y][x];
}

void setBlock(int x, int y, Block block)
{
    assert((0 <= x && x < WIDTH) && (0 <= y && y < HEIGHT));
    blocks[y][x] = block;
}

if(getBlock(x-1, y) == EMPTY)
{
    setBlock(x-1, y, BLOCK);
}

- 배열 접근 함수화가 어려운 경우, STL의 vector 클래스 또는 C++11에 추가된 array 클래스를 사용하자.

비주얼 스튜디오의 STL 컨테이너는 Debug 모드 때에 인덱스의 범위 체클르 자동으로 수행한다.

따라서, 접근 부분을 따로 함수화하지 않고, STL 컨테이너를 변경하는 것만으로도 오류 체크가 가능하다.

// array 클래스를 사용한 2차원 배열의 예
typedef std::array<Block, 10> RowBlock;
typedef std::array<RowBlock, 20> Blocks;
Blocks blocks; // Block blocks[20][10]과 같은 2차원 배열

- C++의 기본 자료형을 사용한 배열은 버그가 일어나기 쉽기 때문에, 특별한 이유가 없다면 사용을 피하고,

어쩔수 없는 경우에는 배열의 랜덤 접근 부분을 함수화하자.

 

9) 주석 부분 함수화

- 주석을 쓰는 것은 좋은 습관이지만, 주석을 쓰기 전에 함수화부터 진행하자.

- 긴 코드 블록을 주석으로 구분하는 코드도 있는데, 그러한 코드도 함수화하자.

- 함수화해도 이해하기 어려운 코드, 수정할 때 주의가 필요한 코드의 경우 주석을 사용하자.

 

 

* 매개변수가 너무 많은 문제

- 대부분 설계 문제이므로, 설계를 개선하면 매개변수를 줄일 수 있다.

1) 욕심쟁이 함수

- 1개의 함수가 너무 많은 일을 하려하면 매개변수의 수도 많아지므로 작게 분할하자.

- 리턴값이 2개 이상 있는 함수는 되도록 피하자. 기능이 2개 이상일 가능성이 크다.

 

2) 부적절한 클래스화

- 클래스화가 부적절한 경우 매개변수의 수도 많아지므로,

매개변수를 모아 클래스화하면 매개변수를 줄일 수 있다.

- 클래스 내부의 멤버변수는 매개변수로 전달하지 않아도 참조할 수 있으므로,

적절한 클래스화는 함수의 매개변수를 줄일 수 있다.

- 클래스 멤버함수의 매개변수는 적을수록 좋으며, 이상적인 수치는 0 - 2개이다.

- 매개변수가 많다면, 매개변수를 클래스화할 수 없을지 고민해보자.

 

* 작은 함수의 필요성

1) 자기 설명적인 코드(의미 있는 이름을 갖게됨 => 쉽게 읽을 수 있는 코드가 됨)

- 코드의 최소 단위는 함수이다. 복잡한 함수는 작은 함수를 조합해서 만들자.

2) 개별 테스트 기능

- 함수화를 통해 개별적인 단위테스트가 쉽게 가능해지고, 테스트이 정밀도도 높일 수 있다.

 

 

* 작은 함수로 인한 실행 속도 저하?

- 작은 함수로 분할하면 "함수 호출 오버헤드로 실행 속도 저하"가 일어나서,

게임 프로그래밍에는 적합하지 않다라는 의견도 있다. => 과연 그럴까?

- 일반적으로 디버그 모드에서는 컴파일러가 코드를 최적화하지 않으므로,

실행 속도 차이가 있을 수 있다.

- 그러나 릴리즈 모드에서는 작은 함수는 컴파일러에 의해

인라인 전개(함수를 호출하는 대신, 컴파일러가 해당 위치에 함수 내부의 처리를 복사해서 전개하는 기능)된다.

따라서, 인라인 전개되면 함수 호출의 오버헤드가 전혀 없으므로,

함수화로 인한 실행 속도 저하도 나타나지 않는다.

- 컴파일러의 최적화 기능을 잘 활용하면 함수 호출로 인한 오버헤드 문제가 생기지 않는다.

e.g.

비쥬얼 스튜디오의 "링크 때 코드 생성(LTCG)" 최적화 기능 : 컴파일 때가 아닌, 링크 때에 함수를 인라인 전개

=> 프로그램 전체의 함수가 최적화 대상

"Profiled-Guided Optimization" 최적화 방법 : 프로그램을 작동시키고 실행중의 정보를 수집해야 함,

정보가 수집되면 실행 정보를 바탕으로 프로그램 전체를 최적화

(컴파일 때에 최적화가 가장 어렵다는 가상함수까지도 최적화 대상이 됨)

- 작고 단순한 함수일수록 컴파일러의 최적화 혜택을 쉽게 받을 수 있다.

- 실행 속도를 올리고자 한다면 알고리즘 또는 자료구조 차원의 개선을 생각하자.

- 대폭적인 속도 향상을 원할 때는 멀티코어를 활용할 수 있는 구조로 애플리케이션을 재설계하는 편이 좋다.

 

* 함수화의 목적

1) 코드 재사용

2) 코드 가독성 및 보수성

3) assert로 오류 체크

4) 복잡한 부분을 국소화

5) 복잡한 부분을 함수 내에 은폐해서 코드의 추상도를 높임

 

* 함수를 사용할 때의 마음가짐

1) 일단 작성해보기

- 처음 작성한 함수는 밑그림이므로, 절대 한번으로 만족하지 말자.

2) 문제의 본질을 이해

- 함수를 분할하려면 문제의 본질을 제대로 이해하고 있어야 한다.

3) 읽는 사람의 관점에서 확인

- 남이 읽을 때도 잘 이해할 수 있는지 확인하자.

- 적절한 네이밍이 되어 있는지, 복잡한 부분이 남아있는지, 유연성 및 확장성이 있는지 등)


6. 클래스

 

* 클래스화 요령

- 프로그램 설계의 요령은 문제를 작게 나누는 것이다.

코드를 작은 클래스로 나누어 조합하자.

- 큰 함수를 작은 함수로 분할하고, 적절한 이름을 붙이자.

- 클래스 이름을 붙일 때, 멤버함수 이름을 기반으로 정하자.

 

* 클래스화 패턴

1) 중복부분 클래스화

- 여러개의 클래스에 중복되는 부분을 클래스화

1) 상속 : 중복부분을 정리하여 부모클래스를 생성하고 이를 상속하게 한다.

2) 이양 : 중복부분을 정리하여 다른 클래스로 아예 빼낸다.

- 상속과 이양 둘 다 가능한 경우에는 이양이 더 유리하다.
상속은 부모자식관계인 경우에만 재사용이 가능하지만 이양은 아니기 때문이다.
또한, 상속에서는 부모클래스를 변경하면 자식클래스 전부에 영향을 준다.

 

* 기본 자료형으로 구성된 멤버변수 클래스화

- 기본자료형은 모두 클래스 후보이다.(기본 자료형은 반드시 어떤 역할을 가지고 있을 것이기 때문이다.)

=> 1개의 기본 자료형을 가진, 1개의 클래스로 만들어보자.

- 클래스화해서 분리하면 다른 클래스에서 재사용이 가능 및 단위 테스트도 가능하다.
- 멤버변수가 너무 많은 경우에는 관련성이 있는 멤버변수를 모아서 새로운 클래스를 만들자.
- 기본 자료형은 최소한으로 해주는 것이 좋은 클래스를 만드는 요령이다.

class Game
{
    ... 생략
private:
    Timer timer_; // 제한 시간을 클래스화(float)
    Score score_; // 점수를 클래스화(int)
};

 

* 함수의 매개변수 클래스화

- 함수의 매개변수가 너무 많으면 클래스화 후보이다.
- 항상 set로 전달하는 매개변수가 있다면 이들을 클래스화 하자.

class Circle
{
private:
    Vector2 center_; // 중심
    float radius_; // 반지름
public:
    Circle(const Vector2& center, float radius):
    center_(center), radius_(radius)
    {}
    
    // 충돌 판정
    bool isCollide(const Circle& that) const
    {
        return center_.distance(that.center_) <= (radius_ + that.radius_);
    }
};

 

* 컨테이너 클래스화

- 컨테이너 조작은 반복문을 수반하므로 복잡해지기 쉽다.
- 전용클래스로 컨테이너를 감싸면 부적절한 조작을 방지할 수 있다.

- 컨테이너 조작함수는 컨트롤 클래스에 모아주자.
- new로 생성한 인스턴스를 자동으로 제거해주는 shared_ptr 클래스
=> shared_ptr 클래스가 사용된 컨테이너의 객체가 제거될때, 객체의 인스턴스도 자동으로 제거된다.
- 결론 : 컨테이너 조작을 위한 전용클래스를 만들어 사용하자.


7. 작은 클래스

- 작은 클래스를 만들지 않으면 작은 중복들이 모여 프로그램 전체가 복잡해질 수 있다.

- 작은 클래스를 사용하여 추상화 레벨을 한 단계 올릴 수 있다.

- 작은 클래스를 조합하여 만드는 것이 객체지향 프로그래밍의 요령이다.

 

1) 변수를 클래스화해서 의도를 명확하게 전달하기(e.g. 기본 자료형의 클래스화)

class Timer
{
public:
    Timer(float limitTime):
    time_(0), limitTime_(limitTime)
    {}
    
    // 업데이트
    void update(float deltaTime)
    {
        time_ = std::min(time_ + deltaTime, limitTime_);
    }
    
    // 리셋
    void reset()
    {
        time_ = 0;
    }
    
    // 타임아웃 되었는지 확인
    bool isTimeout() const
    {
        return time_ >= limitTime_;
    }
private:
    float time_; // 현재 시각
    float limitTime_; // 제한 시간
};

timer_.update(deltaTime);
if(timer_.isTimeout())
{
    // 일정 시간마다 처리
    ... 생략
    timer_.reset();
};

2) 변수를 클래스화해서 제한을 주자. 코드의 보수성이 높아진다.

class Score
{
public:
    Score(int score = 0):
    score_(score)
    {
        clamp();
    }
    // 덧셈
    void add(int score)
    {
        // 더하는 점수를 확인
        assert(0 <= score && score <= 10000);
        score_ += score;
        clamp();
    }
    // 초기화
    void clear()
    {
        score_ = 0;
    }
    // 득점
    int get() const
    {
        return score_;
    }
private:
    // 점수의 상한보정
    void clamp()
    {
        score_ = std::min(score_, 9999999);
    }
    
    int score_; // 점수
};

3) 범위 클래스를 통해서 상하한값을 다루자.

template <typename T>
T clamp(int x, T low, T high)
{
    assert(low <= high);
    return std::min(std::max(x, low), high);
}

int wrap(int x, int low, int high)
{
    assert(low < high);
    const int n = (x - low) % (high - low);
    return (n >= 0) ? (n + low) : (n + high);
}

template <typename T>
class Range
{
public:
    Range(T min, T max):
    min_(min), max_(max)
    {
        assert(min_ <= max_);
    }
    // 범위 내부에 있는지 확인
    bool isInside(T value) const
    {
        return (min_ <= value && value <= max_);
    }
    // 범위 내부로 클램프
    T clamp(T value) const
    {
        return ::clamp(value, min_, max_); // 외부함수
    }
    // 범위 내부로 랩 어라운드
    T wrap(T value) const
    {
        return ::wrap(value, min_, max_); // 외부함수
    }
    // 하한 득점
    T min() const
    {
        return min_;
    }
    // 상한 득점
    T max() const
    {
        return max_;
    }
private:
    T min_; // 하한
    T max_; // 상한
};

 

// 스크린 범위
const Range<float> SCREEN_RANGE(0, SCREEN_WIDTH);

// x 좌표가 화면 내부에 있는지 확인
if(SCREEN_RANGE.isInside(x))
{
    ... 생략
}

// 커서의 이동 범위
const Range<int> CURSOR_RANGE(0, 4);

if(input.isUp())
{
    cursor = CURSOR_RANGE.clamp(cursor - 1);
}

if(input.isDown())
{
    cursor = CURSOR_RANGE.clamp(cursor + 1);
}

5) 수학클래스 사용
- C++ 연산자 오버로드 및 계산전용함수 등을 사용하면 효과적이다.
- 간략한 계산식이라도 범용적이라면 함수화하자.
- 부동소수점 2D 벡터 클래스는 대부분의 라이브러리에 구현되어 있으나,
정수 2D 벡터 클래스는 대부분의 라이브러리에 없다.

// 2D 벡터 클래스
class Vector2
{
public:
    float x; // x 좌표
    float y: // y 좌표
    
    Vector2(float x = 0, float y = 0):
    x(x), y(y)
    {}
    
    // 내적 계산
    float dot(const Vector2& other) const
    {
        return (x * other.x) + (y * other.y);
    }
    
    // 외적 계산
    float cross(const Vector2& other) const
    {
        return (x * other.y) + (y * other.x);
    }
    
    // 길이 계산
    float length() const
    {
        return std::sqrt(dot(*this));
    }
    
    // 거리 계산
    float distance(const Vector2& other) const
    {
        return (*this - other).length();
    }
    
    // 정규화
    Vector2& normalize() const
    {
        const float len = length();
        if(len < FLT_EPSILON)
            return Vector2::ZERO;
        return *this / len;
    }
    
    // 제로 벡터인지 확인
    bool isZero() const
    {
        return *this == Vector2::ZERO;
    }
    
    // 연산자 오버로드
    Vector2& operator+=(const Vector2& other)
    {
        return *this = *this + other;
    }
   
    Vector2& operator-=(const Vector2& other)
    {
        return *this = *this - other;
    }
    

    Vector2& operator*=(float scalar)
    {
        return *this = *this * scalar;
    }
    

    Vector2& operator/=(float scalar)
    {
        return *this = *this / scalar;
    }
    
    const Vector2 operator+(const Vector2& other) const
    {
        return Vector2(x + other.x, y + other.y);
    }
    
    const Vector2 operator-(const Vector2& other) const
    {
        return Vector2(x - other.x, y - other.y);
    }
    
    const Vector2 operator*(float scalar) const
    {
        return Vector2(x * other.x, y * other.y);
    }
    
    const Vector2 operator/(float scalar) const
    {
         return Vector2(x / other.x, y / other.y);
    }
    
    const Vector2 operator-() const
    {
        return Vector2(-x, -y);
    }
    
    bool operator==(const Vector2& other) const
    {
        return (x == other.x) && (y == other.y)
    }
    
    bool operator!=(const Vector2& otehr) const
    {
        return !(*this == other);
    }
    
    /* 슈팅 게임 또는 액션 게임에서는 삼각함수를 매우 많이 사용하므로,
    다음과 같은 계산 전용함수도 추가해두면 편리함 */
    
    // 각도를 기반으로 벡터 생성
    static Vector2 fromAngle(float degree)
    {
        const float rad = to_radian(degree); // 호도법으로 변환
        return Vector2(std::cos(rad), std::sin(rad));
    }
    
    // 벡터가 향하는 각도 계산
    float toAngle() const
    {
        if(isZero()) return 0.0f; // 제로 벡터는 각도를 구하지 않음
        return to_degree(std::atan2(y, x)); // 도수법으로 변환
    }
    
    // 회전
    Vector2 rotate(float range) const
    {
        const float rad = to_radian(degree); // 호도법 변환
        return Vector2(x * std::cos(rad) - y * std::sin(rad),
                       x * std::sin(rad) + y * std::cos(rad));
    }
    
    // 상수
    static const Vector2 ZERO;
    static const Vector2 LEFT;
    static const Vector2 RIGHT;
    static const Vector2 DOWN;
    static const Vector2 UP;
}

// 상수의 값(cpp 파일에 작성)
const Vector2 Vector2::ZERO(0.0f, 0.0f);
const Vector2 Vector2::LEFT(-1.0f, 0.0f);
const Vector2 Vector2::RIGHT(1.0f, 0.0f);
const Vector2 Vector2::UP(0.0f, -1.0f);
const Vector2 Vector2::DOWN(0.0f, 1.0f);
// 정수형 2D 벡터 클래스
class Point
{
public:
    int x; // x 좌표
    int y; // y 좌표
    
    Point(int x = 0, int y = 0)
    :x(x), y(y)
    {}
    
    // 제로 벡터인지 확인
    bool isZero() const
    {
        return *this == Point::ZERO;
    }
    
    // 연산자 오버로드
    Point& operator+=(const Point& other)
    {
        return *this = *this + other;
    }
    
    Point& operator-=(const Point& other)
    {
        return *this = *this - other;
    }
    
    const Point operator+(const Point& other) const
    {
        return Point(x + other.x, y + other.y);
    }
    
    const Point operator-(const Point& other) const
    {
        return Point(x - other.x, y - other.y);
    }
    
    const Point operator-() cosnt
    {
        return Point(-x, -y);
    }
    
    bool operator==(const Point& other) const
    {
        return (x == other.x) && (y == other.y);
    }
    
    bool operator!=(const Point& other) const
    {
        return !(*this == other);
    }
    
    bool operator<(const Point& other) const
    {
        if(x != other.x) return x < other.x;
        if(y != other.y) return y < other.y;
        return false;
    }
    
    void move(const Point& position, const Input& input)
    {
        const Point destination = position + toDirection(input);
        if(canMoveBlock(destination))
        {
            moveBlock(position, destination);
        }
    }
    
    
    // 상수 선언
    static const Point ZERO;
    static const Point LEFT;
    static const Point RIGHT;
    static const Point UP;
    static const Point DOWN;
    
private:
    // 이동 방향 계산
    Point toDirection(const Input& input)
    {
        if(input.isLeft()) return Point::LEFT;
        if(input.isRight()) return Point::RIGHT;
        if(input.isUp()) return Point::UP;
        if(input.isDown()) return Point::DOWN;
        return Point::ZERO;
    }
    
    // 좌표가 맵 내부에 있는지 확인
    bool isInside(const Point& position)
    {
        return (0 <= position.x && position.x < MAP_WIDTH)
            && (0 <= position.y && position.y < MAP_HEIGHT);
    }
    
    // 블록 추출
    BlockCode getBlock(const Point& position)
    {
        assert(inInside(position) && "맵의 범위를 벗어났음");
        return map[position.y][position.x];
    }
    
    // 블록 설정
    void setBlock(const Point& position, BlockCode block)
    {
        assert(isInside(position) && "맵의 범위를 벗어났음");
        map[position.y][position.x] = block;
    }
    
    // 블록이 이동 가능한지
    bool canMoveBlock(const Point& destination)
    {
        return isInside(destination) && getBlock(destination) == EMPTY;
    }
    
    // 블록 이동
    void moveBlock(const Point &position, const Point& destination)
    {
        assert(canMoveBlock(destination) && "이동 불가");
        setBlock(position, EMPTY);
        setBlock(desination, BLOCK);
    }
};

// 상수의 값(cpp 파일에 작성)
const Point Point::ZERO(0, 0);
const Point Point::LEFT(-1, 0);
const Point Point::RIGHT(1, 0);
const Point Point::UP(0, -1);
const Point Point::DOWN(0, 1);


 

6) 사각형 클래스

- 2D 게임의 충돌판정에 자주 사용한다.
- 클래스화 해두면 충돌 판정 또는 영역 내부에 있는지 판정을 간략하게 작성 가능하다.

// 사각형 클래스
class Rectangle
{
public:
    Rectangle(float left, float top, float right, float bottom)
    : min_(left, top), max_(right, bottom)
    {}
    
    Rectangle(const Vector2& min, const Vector2& max)
    : min_(min), max_(max)
    {}
    
    // 점이 사각형 내부에 들어있는지 확인
    bool contains(const Vector2& position) const
    {
        return (min_.x <= position.x && position.x <= max_.x)
            && (min_.y <= position.y && position.y <= max_.y);
    }
    
    // 사각형이 중첩되어 있는지 확인
    bool intersects(const Rectangle& other) const
    {
        if(min_.x > other.max_.x) return false;
        if(max_.x < other.min_.x) return false;
        if(min_.y > other.max_.y) return false;
        if(max_.y < other.min_.y) return false;
        return true;
    }
    
    // 평행 이동
    Rectangle translate(const Vector2& position) const
    {
        return Rectangle(min_ + position, max_ + position);
    }
    
    // 크기 변경
    Rectangle expand(const Vector2& size) const
    {
        return Rectangle(min_ - size, max_ + size);
    }
    
    // 너비
    float width() const
    {
        return max_.x - min_.x;
    }
    
    // 높이
    float height() const
    {
        return max_.y - min_.y;
    }
    
    // 왼쪽 위 좌표
    const Vector2& min() const
    {
        return min_;
    }
    
    // 오른쪽 아래 좌표
    const Vector2& max() const
    {
        return max_;
    }
    
private:
    Vector2 min_; // 사각형의 왼쪽 위 좌표
    Vector2 max_; // 사각형의 오른쪽 아래 좌표
};


// 화면 범위
const Rectangle SCREEN(0, 0, 640, 480);

// 좌표가 화면 내부에 있는지 확인
if(SCREEN.cotains(position))
{
    // 화면 내부에 있는 경우의 처리
}

// 충돌했는지 확인
bool isCollide(const Actor& other) const
{
    return body_.translate(position_)
                .intersects(other.body_.translate(other.position_));
}

 

7) 이외의 수학 관련 클래스

- 게임 프로그래밍에는 지금까지 소개한 클래스 이외에도 3차원 벡터 클래스, 행렬 클래스, 평면 클래스, 사원수(quaternion) 클래스 등 수학 계열의 작은 클래스를 사용한다.

- 라이브러리가 마련되어 있는 경우에는 적절하게 사용하고, 없는 경우에는 인터넷이나 책을 참고하자.

- 계산 전용 라이브러리가 C언어로 작성된 경우에는 C++ 연산자 오버로딩이 구현되어 있지 않지만,

C++에서는 기존의 C언어 구조체에 연산자 오버로드를 추가하는 것이 가능하다.

- C++의 연산자 오버로드를 통해 코드의 가독성을 높이자.

 

8) typedef 활용

- 작은 클래스를 만드는 것이 어려울 경우, typedef를 사용해서 사용자 정의 자료형을 작성하자.

e.g.

typedef int Score;

typedef float Timer;

- C++11의 using을 사용하면,

using Score = int;

using Timer = float;

처럼 작성도 가능하다.

- 클래스처럼 변수에 행위를 부여하거나 제한을 추가하는 것은 불가능하므로,

클래스화가 불가능할 때 최후의 수단으로 사용하자.


* 마치며
1. 복잡한 것을 분할해서 간략화한 후, 이들을 조합하자.
2. 코드에 좋은 이름을 붙이는 것을 습관화하자.

이를 위해 많이 고민하고, 다른 사람의 코드를 많이 읽고 참고하는 것도 좋다.


# 책의 내용과는 무관한, 공부하면서 배운 내용

- 함수, 쓰레드는 간단명료하게 작성하자.(역할을 명확히 하면 자연스럽게 달성된다.)

- 필요에 의해 재귀로 코드를 작성했다면, 비재귀로 구현해보자.

(재귀는 보다 직관적이나, 재귀적인 함수호출로 인한 오버헤드 발생이 있음을 고려하자.)

- 개발 전 설계 및 구조화를 철저하게 진행하자. 그리고 리팩토링 및 피드백을 반드시 제대로 하자.

- 적절한 예외처리는 도움이 될 수 있다.(유지, 보수 등에 유리하다.)


[출처] : 오즈 모리하루 저, "C와 C++ 게임 코드로 알아보는 코딩의 기술", 한빛미디어