타일맵 길찾기.

원래 블로그에는 평소에 공부했던 걸 저장하고 나중에 찾아서 참고하는 용도로 쓰고 있었는데, 최근 바빠서 그럴 시간이 없었다. 특히 NDC2016 발표를 준비하는 시간은 정말 지옥같았기 때문에(결과값이.. 결과값이 나와야 했다), 발표를 준비하면서 공부했던 내용, 그리고 발표 보충 자료를 올린다고 말해놓고 올리지 않은 채 반 년이 지나버렸다. 최근에 타일 맵을 다시 쓸 일이 생겨서 예전 자료를 참조하다가 NDC2016 발표에 썼던 기법이 기억이 나지 않는 걸 깨닫고, 시간이 나는 김에 하나씩 정리해 놓아야겠다고 생각했다.

타일 맵은 컴퓨터 게임의 초창기부터 지금까지 쓰여온 견고한 기법이다. 보통 균일한 이미지 조각인 타일(Tile) 을 이어붙여서 맵의 형태를 만든다. 보통 이렇게 만든 맵은 보기에 자연스럽고 타일 간의 이음새가 크게 눈에 띄지 않는 것이 좋다. 이렇게 만든 맵은 보통 주인공 캐릭터나 다른 오브젝트들의 활동 무대로 쓰인다.

타일 맵은 RPG Maker 같은 툴에서도 쉽게 찾아볼 수 있다. 타일을 선택하고 색칠하듯 마우스로 드래그하면 맵이 생성된다. 이렇게 생성된 맵은 자연스럽게 보인다. 즉 타일과 타일 간의 이음새가 분리되는 느낌이 아니라 부드럽게 연결된다. 이렇게 만들려면 어떻게 해야 할까?

링크

애초에 이 고민을 했던 것은 고전 게임인 <삼국지 영걸전>의 맵 파일을 누군가 뜯어놓은 것을 살펴볼 때였다. 맵 조각은 아래처럼 구성되어 있었다.

그에 비해 우리가 보통 알고 있는 맵은 아래와 같다.

이미지를 보면 금방 둘의 연관성을 알 수 있다. 실제의 맵을 구성하는 타일 조각의 최소 단위는 보통 플레이어가 “한 칸”이라고 인지하는 유닛이나 성채, 보물창고의 크기에 비해 가로, 세로가 각각 1/2 작은, 1/4 크기였다. 영걸전에서는 한 칸이 32 x 32 픽셀, 타일 하나는 16 x 16 픽셀이다.

예를 들어 위의 맵은 아래처럼 하얀 네모가 하나의 이미지 조각(타일)로, 그것이 모여서 우리가 인지할 수 있는 맵의 형태가 된다.

영걸전에서는 유닛이 이보다 가로 세로 2배 큰 단위로 배치된다. 물론 게임에 따라 맵 타일과 유닛 타일이 같은 단위일수도 있다.

이음새가 자연스럽게 보이기 위해서는 우리가 한 칸의 타일이라고 인식하는 것의 절반 정도의 스케일로 타일을 준비해야 한다. 이 페이지에 따르면 보통 RPG에서 많이 사용되는 타일 종류를 2-corner 타일이라고 부른다. 타일의 corner 부분이 변하기 때문이다. 땅과 물처럼 대비되는 두 가지의 지형이 자연스럽게 이어지도록 하는 것이다.

링크

영걸전에서는 대각선을 연결할 필요가 없었는지 아래 그림에서 빨간 네모로 표시한 이런 타일들은 쓰이지 않았다.

대신에 표현을 풍부하게 하기 위해서인지 연결 타일 일부가 2가지로 쓰였다(이음새와 면적이 차지하는 부분에 차이 있음).
영걸전의 초원 – 물 연결 부분을 정의하는 2-corner 타일은 아래와 같다.

다음 글에서는 내가 RPG 게임의 타일 맵을 어떻게 만들었는지를 보여줄 예정이다.출처:NDC2016 보충자료 – 1. 타일 맵 구현 기본

읽어볼만한 좋은 글
——————
1. http://chulin28ho.egloos.com/5097822 – 워크래프트3에서 사용된 타일 구조 연구

참고자료
——–
2. http://www.codeproject.com/Articles/106884/Implementing-Auto-tiling-Functionality-in-a-Tile-M – 2D 타일 맵의 구현 설명

3. http://s358455341.websitehome.co.uk/stagecast/wang/2corn.html – 보통 RPG 맵을 만들 때 사용하는 2-corner 타일에 대한 설명신고출처:NDC2016 보충자료 – 1. 타일 맵 구현 기본

지난 글에 이에 이번에는 실제 NDC2016 발표에서 타일 맵을 어떻게 구현했는지에 대해서 살펴보겠다. 타일 리소스는 게임업계의 저명한 인사인 Daniel Cook 이 무료로 공개한 타일셋을 가져왔다. 게임에 사용하는 무료 리소스가 올라오는 opengameart.org 에는 이 타일셋을 32 x 32 크기로 수정한 버전이 있는데 이것도 참고했다.

일단 내가 사용할 타일셋을 만들었는데 아래 그림과 같다. 가운데에 있는 벽돌은 이 예제에 사용한 유닛들과 느낌이 어울리지 않아서 결국 사용하지 않았기 때문에 기본 타일 종류는 풀(초원), 길, 물의 3종류다.

풀과 길은 이전 글에서 설명한 2-corner 타일로 서로 경계면이 부드럽게 연결되는 타일 조각들로 이루어져 있다. 하지만 물은 물과 다른 영역의 경계가 투명하게 처리되어 있는데, 이는 먼저 풀과 길을 정리한 다음에 물을 그 위에 덮어씌우기 위해서다. 풀이든 길이든 물을 그 위에 얹으면 자연스럽게 보인다. 3가지 타일이 만나는 경우의 수를 다 계산하는 것보다 이쪽이 간단하고 편하다.

간단히 생성해 본 맵은 아래와 같다.

길을 만드는 과정은 복잡하기 때문에 다음 글에 설명하기로 하고, 여기서는 물을 얹는 방법부터 설명하려고 한다. 맵의 어떤 장소에 물을 배치하기로 결정하면, 상하좌우, 좌상, 좌하, 우상, 우하의 8개 셀에 대해서 랜덤함수를 돌려서 75%의 확률로 물을 배치한다.

물을 배치한 후에는 Cellular Automata 기법을 이용해서 각 셀의 상하좌우 4방향 이웃을 체크, 물의 모양을 자연스럽게 만들어준다.
물 타일이 아닐 경우 상하좌우 이웃에 물 타일이 2개 이상 있으면 물 타일이 된다.

그 다음에는 물의 클러스터가 충분히 가깝지만 이어져 있지 않아서 어색해보이는 경우를 방지하기 위해서 1타일 떨어진 물의 경우 합쳐주는 작업을 한다.

이제 이 맵을 타일맵으로 바꿔줄 때가 왔다. 영역간의 연결을 생각하지 않는다면 아래와 같은 타일맵도 가능할 것이다.

하지만 연결을 생각하면 계산은 조금 복잡해진다. 아까의 타일셋을 살펴보면 각 타일은 4개의 corner에 물타일이 있는 경우와 그렇지 않은 경우로 나눌 수 있다(물 = w, 다른 타일 = o 로 표시).

맵의 타일 결정 방법은, 먼저 물 타일 각각에 대해서 8방향에 어떤 타일이 있는지 검사한다. 그리고 물 타일이 아니면 가중치를 둔다.

타일이 되는 빨간 타일을 확대해서 살펴보면 4개의 corner로 나눌 수 있는데, 어느 한쪽에서 영향을 받으면 인접한 영역에 1을 더해주게 된다.

예를 들어서 왼쪽 상단에 물 타일이 아닌 것이 있으면 왼쪽 위 코너에 1이 더해지고, 오른쪽에 물 타일이 아닌 것이 있으면 오른쪽 상단, 오른쪽 하단 코너에 1을 더한다.

이렇게 계산하면 위 맵의 타일에는 아래처럼 점수가 쌓이고, 그 중 1 이상인 값만 o(other) 타일로 치환하면 알맞은 타일을 찾을 수 있다. 4개의 corner가 모두 o 타일일 때는 이미 물 타일이 아니라고 판단하고 그리지 않는다.

최종적으로는 아래처럼 타일을 찾게 된다. 위와 비교하면 꽤 큰 차이다.

이렇게 타일맵의 실제 구현에 대한 내용을 살펴보았다. 다음 글에서는 위에서 약간 두리뭉실하게 넘어갔던 “어떤 장소에 물을 배치할지” 결정하는 문제에 대한 내용, 즉 맵의 노드와 경로를 정하는 기법에 대해 다룰 예정이다.출처:NDC2016 보충자료 – 2. 타일 맵 구현 실제

이번에는 지난 NDC2016 발표의 “맵 생성 절차”(59~69p.)에 대해서 좀 더 자세한 글을 써보려고 한다. 59페이지를 그대로 가져와보면 다음과 같다.

이 단계들에 대해 차례대로 설명하려고 한다.

1. Poisson Disc Sampling

맵에서 비교적 균일한 간격으로 노드를 선택하기 위한 방법이다. 완전한 랜덤이 아닌 이 방법으로 선택한 노드는 보기에도 좋고 활용도 편리하다. 하나하나의 노드가 도시나 별이라고 하면 서로 너무 멀거나 너무 가깝지 않은 적당한 거리를 유지할 수 있기 때문에 자연스럽기도 하다. 아래 그림에서 가장 오른쪽이 Poisson Disc Sampling 으로 생성한 노드들이다.

이미지 링크

Disc Sampling 이라는 이름대로 이 방법은 대기열에 속한 점(=노드) 주변에 있는 원형(Disc)의 영역에 다른 점을 추가할 수 있는지 검사한다.

새로 추가하는 점 주변에 다른 이웃이 없다면 그 점을 추가하고, 그 점을 대기열에 넣는다.
미리 정해진 tryCount 만큼 탐색하여 점을 여러 개 추가한다. 나는 tryCount를 30으로 설정했다.

tryCount 가 끝나면 대기열에 있던 점을 빼고 새로 추가된 점 중 하나를 대기열에 집어넣고 다음 탐색을 반복한다. 이때 주변에 다른 이웃이 없는지 체크해서 이웃이 있다면 그 점은 추가될 수 없다.

탐색을 빠르게 하기 위해 전체 맵을 그리드로 나누고, 그리드 안에는 하나의 점만 들어갈 수 있도록 제한한다. 따라서 원이 겹치는 영역에 있는 모든 점을 검사할 필요 없이, 원이 걸치는 그리드에 있는 점들만 검사하면 되기 때문에 탐색 시간이 줄어든다.

이해를 돕기 위해 예전에 만들었던 Poisson Disc Sampling 예제 링크를 올린다. 예제에서는 1프레임당 1개의 점을 탐색하도록 했기 때문에 점이 천천히 검색되는 것이지, 실제 검색 속도는 빠르다.

http://wonderfl.net/c/MMGa

2. Minimal Spanning Tree

트리란 두 개의 노드가 하나의 간선(edge)으로만 연결된 그래프를 말한다.

신장 트리(Spanning Tree)는 사이클(루프)가 없이 모든 노드를 연결하는 트리이다. 최소 신장 트리(Minimal Spanning Tree)는 이 중 모든 노드를 연결하면서 경로의 합이 가장 짧은 신장 트리를 말한다.

위에서 만든 노드들을 연결해서 그래프를 만드는데, 복잡한 계산을 줄이기 위해 일정 길이 이하의 간선만 남긴다. 그리고 여기서 최소 신장 트리를 찾는다.

3. 지형 배치

이렇게 최소 신장 트리를 찾은 다음에는 간선 연결이 하나뿐인 터미널 노드(terminal node)를 찾는데, 이곳에 물과 절벽을 배치한다. 터미널 노드에는 물이나 절벽 중 하나를 배치한다.

아래 그림에서 검은색으로 표시된 노드가 터미널 노드이다. 두번째 글에서 설명했던 Cellular Automata 기법으로 인접한 물, 인접한 절벽끼리는 서로 합쳐진 것을 확인할 수 있다.

이제 각 타일이 어느 지형에 속하는지 결정을 해야 한다. 어떤 타일은 풀이 먼저 그려진 다음 위에 물이 그려지고, 또 절벽까지 겹쳐져 있다. 일단 절벽(검은색)을 최우선으로 찾은 다음 물(파란색)을 찾고, 마지막으로 절벽과 물에 속하지 않은 곳을 이동 가능한 타일(하얀색)로 지정한다.

또한 경로에는 랜덤하게 길 타일을 배치해서 사실감을 높인다. 길 타일 역시 Cellular Automata 기법으로 너무 뚜렷하지 않게 자연스러운 모양을 유지하도록 했다.

4. 다시 Poisson Disc Sampling – 메인 경로 추출

지금까지의 작업이 그럴듯해 보이는 지형을 만들기 위한 것이었다면 이번 단계는 유닛들이 이동할 수 있는 메인 경로를 만드는 작업이다. 다시 Poisson Disc Sampling 을 이동 가능한 타일(하얀색)에 대해서만 수행한다. 그리고 경로는 이동 가능한 타일을 지나는 것만 인정하고, 중간에 절벽이나 물과 겹치는 경로는 제외한다.

5. 메인 노드 정하기 & 유닛 배치

그럼 이제 1:1 대결을 상정한 맵이기 때문에 각 팀의 유닛을 배치할 메인 노드를 결정한다. 메인 노드 2개는 서로 연결되어야 하고, 그 거리는 맵의 가로 길이 + 맵의 세로 길이의 40% 에서 100% 사이의 값이 되어야 한다. 적당한 메인 노드 2개를 발견하지 못했을 경우 맵 전체를 다시 생성한다.

메인 노드에는 각 팀이 지켜야 할 타워를 생성하고 그 주위에 유닛을 배치한다. 이제 양 팀은 서로의 타워를 파괴하기 위해 서로 대결을 펼치게 된다. 참고로 타워가 구석에 있을수록 유리한데, 보병과 기병은 인접해야만 타워에 공격이 가능하기 때문이다. 이 맵에서 Blue Team의 승률은 300번의 전투 시뮬레이션을 돌렸을 때 85.0%가 나왔다.

약간 길었지만 발표 중에 가장 정리하고 싶었던 부분인 맵 생성 절차를 정리해 보았다. 다음에는 시간이 된다면 딥러닝 실행 부분을 정리해보려고 한다. 양이 많아서 아마 글 하나로는 안될 것 같다.출처:NDC2016 보충자료 – 3. 맵 생성 절차

유니티에서 사용되는 코루틴(Coroutine)은 왜 필요한가?

unity_logo

유니티에서 화면의 변화를 일으키기 위해서는 Update() 함수 내에서 작업을 하게 됩니다. 이 Update() 함수는 매 프레임을 그릴때마다 호출되며 60fps의경우라면 초당 60번의 Update() 함수 호출이 발생하게 됩니다. 하나의 프레임 안에서 어떤 작업을 한다면 이 Update() 함수에 코드를 작성하면 될 것입니다.

하지만 다수의 프레임을 오가며 어떤 작업을 수행해야 한다면 어떻게 해야 할까요? 혹은 특정 시간, 가령 3초 동안 특정 작업을 수행해야 한다면 어떻게 해야 할까요? 3초니깐 3 x 60 = 180 프레임동안 작업을 수행하도록 하면 될까요?

안타깝게도 기기의 성능이나 상황에 따라 프레임 드랍(Frame drop)이라는 상황이 발생하게 됩니다. 60fps의 게임일지라 하더라도 디바이스의 성능에 따라 그 이하로 떨어질 수 있다는 의미가 됩니다. 이렇게 되면 더더욱 3초 동안 작업을 수행한다는게 쉽지 않은 일이 됩니다.

예로 다음의 코드를 준비하였습니다. 특정 오브젝트를 페이드 아웃(Fade out) 시키는 예제 코드입니다. 이 코드를 수행하면 스프라이트의 알파값이 점점 작아져서 결국 화면에서 사라지게 됩니다.

public class FadeOut : MonoBehaviour
{
    private SpriteRenderer spriteRenderer;
    void Start ()
    {
        spriteRenderer = GetComponent<SpriteRenderer>();
    }
    void Update ()
    {
        Color color = spriteRenderer.color;

        if ( color.a > 0.0f )
        {
            color.a -= 0.1f; spriteRenderer.color = color;
        }
    }
}

위의 코드를 보면 매 프레임마다 스프라이트 렌더러의 알파값을 0.1씩 감소시키고 있습니다. Update() 함수가 10번 호출되면 사라지게 되겠네요. 이는 1/6 초만에 사라지게 된다는것을 의미합니다. 이것도 1/6초만에 사라질지 보장받기가 어렵습니다.

그럼 혹시, 1초에 걸쳐 (60fps가 정상적으로 보장될 경우 60 프레임에 걸쳐) 사라지게 하려면 어떻게 하면 될까요? 대충 알파값을 0.017씩 감소시키면 될까요? 프레임이 아닌 시간 단위로 특정 작업을 수행할 수 있을까요? 여기서 생각할 수 있는 수단은 Time.deltaTime 이 있습니다.

하지만 우리가 여기서 알아보고자 하는것은 델타 타임이 아닌 코루틴이므로 코루틴에 대해서 알아보도록 하겠습니다. 코루틴은 프레임과 상관없이 별도의 서브 루틴에서 원하는 작업을 원하는 시간만큼 수행하는 것이 가능합니다.

다음은 코루틴을 사용하여 1초동안 페이드 아웃을 진행하는 예제 코드입니다.

public class FadeOut : MonoBehaviour
{
    private SpriteRenderer spriteRenderer;
    void Start ()
    {
        spriteRenderer = GetComponent<SpriteRenderer>(); 
        StartCoroutine( "RunFadeOut" );
    }

    IEnumerator RunFadeOut ()
    {
        Color color = spriteRenderer.color;
        while ( color.a > 0.0f )
        {
            color.a -= 0.1f;
            spriteRenderer.color = color;
            yield return new WaitForSeconds( 0.1f );
        }
    }
}

이전 코드에서는 Update() 에서 모든 작업을 처리하던것을 Start() 에서 RunFadeOut() 코루틴을 실행하는것으로 변경된 것을 볼 수 있습니다. 여기서 주목해야 하는 부분은 yield return new WaitForSeconds(0.1f); 부분입니다.

이 복잡해 보이는 코드는 0.1초 동안 잠시 멈추라는 의미를 가진 코드입니다. 이제 위의 코드를 통해서 페이드 아웃 효과는 0.1초씩 10번을 수행하며 1초동안 사라지는 모습을 보여주게 됩니다. 이 코루틴은 Update() 함수에 종속적이지 않으며 마치 별도의 쓰레드와 같이 동작을 하게 됩니다. 이와 같은 코드로 프레임율에 영향을 받지 않는 시간 기반의 서브루틴을 구동할 수 있게 되었습니다.

IEnumerator와 yield는 무엇이며 어떤 관계가 있는가?

그렇다면 여기서 궁금증을 유발하는 부분이 몇가지 있는데요 RunFadeOut의 리턴 타입은 IEnumerator(열거자) 입니다. 또한 while 문 내부에 보면 yield(양보)라는 구문이 보이는군요. 그 뒤로 return이 따라나오는 것도 일반적인 언어에서 보기 힘든 문법입니다. 이것들이 어떤 관계를 가지고 있는지 알아보겠습니다.

우선 다음의 일반적인 C# 코드를 한번 살펴보도록 하겠습니다.

void Main ()
{
    IEnumerator enumerator = SomeNumbers();
    while ( enumerator.MoveNext() )
    {
        object result = enumerator.Current;
        Debug.Log( "Number: " + result );
    }
}

IEnumerator SomeNumbers ()
{
    yield return 3;
    yield return 5;
    yield return 8;
}

위의 Main() 함수를 실행하게 되면 다음과 같은 결과물이 출력됩니다.

Number: 3
Number: 5
Number: 8

조금 헷갈리지만 알고보면 어렵지 않은 코드입니다. 이 코드는 다음과 같은 순서로 동작하게 됩니다.

  1. SomeNumbers() 함수를 실행한 결과를 IEnumerator 열거자로 받습니다. 정확히는 실행된 결과가 아닙니다. enumerator 에 함수의 실행결과가 할당 되었다고 생각될만한 코드지만 여기서는 SomeNumbers() 함수는 한줄도 실행되지 않은 상태입니다. 함수의 포인터를 받았다고 생각하시는게 이해하시기 편할 것 같습니다.
  2. while 문을 만나면서 처음으로 enumerator의 MoveNext()가 호출됩니다. 여기서 SomeNumbers()가 실행이 되며 딱 yield 문을 만날때까지 실행이 됩니다.
  3. 첫번째 yield 문인 yield return 3; 을 만납니다. 여기서는 표현식 그대로 return 3에게 양보한다는 느낌으로 코드를 읽으시면 될 것 같습니다. 우선 여기까지 오면 3을 리턴하는것에 양보가 일어납니다. 이때에 리턴되는 값이 존재하므로 MoveNext()의 결과값으로 true가 반환됩니다.
  4. 이제 enumerator의 Current를 통해서 현재 반환된 값을 꺼내올 수 있습니다. MoveNext()를 통해서 yield return 되는 값이 있는지를 알 수 있고 반환된 값이 있다면 Current에 담겨 있게 됩니다.
  5. Debug.Log를 사용하여 Current를 출력해보면 처음으로 양보 반환된 3이 출력되게 됩니다.
  6. 다시한번 while문이 실행되며 MoveNext()가 호출되면 정말 재미있게도 가장 마지막에 yield return이 일어났던 위치의 다음줄부터 재실행이 되게 됩니다. 다시한번 yield 문을 만날때까지 진행이 됩니다.
  7. 이번에는 두번째 yield문인 yield return 5를 만나게 됩니다. 결과적으로 MoveNext() 의 결과값은 true가 되게 됩니다.
  8. 현재 Current에 할당된 값은 MoveNext()의 양보 반환된 값인 5가 될 것입니다.
  9. Debug.Log를 통해 값을 출력해보면 5가 출력됩니다.
  10. 다시한번 while문의 MoveNext()를 호출하면 yield return 5; 다음줄부터 재시작이 되게 되면 yield return 8;까지 진행이 되게 됩니다.
  11. 8이 양보 반환되었으므로 MoveNext()의 값은 true가 되며 Current에는 8이 들어가있게 됩니다.
  12. Debug.Log로 8이 출력됩니다.
  13. 다시한번 MoveNext() 가 호출되며 yield return 8; 이후의 코드부터 실행이 되지만 함수의 끝을 만나게 되므로 더이상 yield가 일어나지 않습니다.
  14. MoveNext() 의 결과 값으로 false가 반환되며 while 문이 종료됩니다.

조금 특이하지만 함수의 반환값이 IEnumerable, IEnumerable<T>, IEnumerator, IEnumerator<T> 인 경우에는 위와 같은 동작을 하게 됩니다. 함수의 동작이 비동기적으로 동작하게 되므로 파라미터에 ref나 out을 사용할 수 없다는 제약 사항이 있습니다. 위의 코드 동작 예시는 코루틴이 어떻게 동작하는지 알기위한 기본적인 코드라고 생각됩니다. 이제 다시 코루틴으로 돌아가 보겠습니다.

void Start ()
{
    //StartCoroutine ("RunCoroutine")
    IEnumerator runCoroutine = RunCoroutine();
    while ( runCoroutine.MoveNext() )
    {
        object result = runCoroutine.Current;

        if ( result is WaitForSeconds )
        {
            // 원하는 초만큼 기다리는 로직 수행
            // 여기 예제에서는 1초만큼 기다리게 될 것임을 알 수 있음
        }
        else if ...

    }
}

IEnumerator RunCoroutine ()
{
    yield return new WaitForSeconds( 1.0f );
    yield return new WaitForSeconds( 1.0f );
    yield return new WaitForSeconds( 1.0f );
}

StartCoroutine을 직접 구현해 본다면 위와 같은 형태의 코드가 될 것 같습니다. 먼저 코루틴 함수의 포인터 역할을 하는 열거자를 받은 다음에 MoveNext()를 통해 첫번째 yield를 만날때까지 수행하고 그 결과값을 받습니다. 그리고 그 결과값에 맞는 작업을 수행해줍니다. 그리고 이것을 함수가 완료될 때까지 반복합니다.

위의 코드에서는 4번의 MoveNext()가 호출될 것이며 3번의 yield문을 만날 것입니다. 마지막 MoveNext()에서는 false가 반환될 것이므로 코루틴이 종료됩니다. 만약 함수의 실행이 완료되기 이전에 임의로 코루틴을 종료시키고 싶다면 yield break를 호출하면 됩니다. 즉시 MoveNext()에서 false가 반환되어 종료됩니다.

결론적으로 StartCoroutine은 IEnumerator를 반환하는 함수를 인자로 받으며 이 함수는 특이하게도 실행된 결과를 의미하는것이 아니라 함수 포인터와 같은 개념으로 사용이 됩니다.

void Start ()
{
    StartCoroutine( RunCoroutine() );
}

IEnumerator RunCoroutine ()
{
    yield return new WaitForSeconds( 1.0f );
    yield return new WaitForSeconds( 1.0f );
    yield return new WaitForSeconds( 1.0f );
}

이 코드를 한번 봐보겠습니다. 일반적인 함수의 개념으로 보자면 StartCoroutine에는 RunCoroutine() 함수의 결과값이 파라미터로 넘겨지게 되어있습니다. 하지만 여기서 RunCoroutine()은 단 한줄도 실행이 되지 않습니다. 함수의 포인터 역할을 하는 IEnumerator가 넘겨지게 되고 MoveNext()를 호출할 때마다 yield 문을 만날때까지 수행됩니다. 만나게 되면 MoveNext()가 true를 반환하고 함수가 끝나거나 yield break; 를 만나게 되면 false를 반환하게 됩니다. true를 반환할 경우 Current를 통해 반환된 값을 꺼내볼 수 있습니다.

StartCoroutine을 수행할 때 사용할 수 있는 두가지 방법

public Coroutine StartCoroutine(IEnumerator routine);

일반적으로 사용할 수 있는 방법입니다. 수행하고자 하는 코루틴의 IEnumerator 열거자를 넘겨서 실행되도록 합니다. 다음과 같은 방법으로 사용이 가능합니다.

void Start ()
{
    StartCoroutine( WaitAndPrint( 2.0F ) );
}
IEnumerator WaitAndPrint ( float waitTime )
{
    yield return new WaitForSeconds( waitTime );
    Debug.Log( "Done. " + Time.time );
}

위와 같은 방법은 일반적인 방법으로 waitTime 파라미터 값을 넘길 수 있으며 코루틴이 실행되는데에 추가적인 오버헤드가 전혀 없는 방법입니다. 뿐만 아니라 Start() 함수의 반환값을 IEnumerator로 변경하여 아예 코루틴이 실행 완료될때까지 기다리도록 의존적인 방법으로 실행하는 것도 가능합니다.

IEnumerator Start()
{
    yield return StartCoroutine( WaitAndPrint( 2.0F ) );
    Debug.Log( "Done " + Time.time );
}
IEnumerator WaitAndPrint ( float waitTime )
{
    yield return new WaitForSeconds( waitTime );
    Debug.Log( "WaitAndPrint " + Time.time );
}

위의 코드는 WaitAndPrint(waitTime) 코루틴이 실행 완료된 이후에야 Done이 출력되는 과정을 보여줍니다.

public Coroutine StartCoroutine(string methodName, object value = null);

대부분의 경우는 StartCoroutine을 사용하기 위해 전자의 방법을 사용합니다. 하지만 StartCoroutine을 문자열 형태의 코루틴 함수 이름으로도 호출하는 것이 가능합니다. 이렇게 호출하면 StopCoroutine 역시 함수 이름만으로 호출하는것이 가능해 집니다.

IEnumerator Start ()
{
    StartCoroutine( "DoSomething", 2.0F );
    yield return new WaitForSeconds( 1 );
    StopCoroutine( "DoSomething" );
}
IEnumerator DoSomething ( float someParameter )
{
    while ( true )
    {
        Debug.Log( "DoSomething Loop" );
        yield return null;
    }
}

위의 코드는 DoSomething(someParameter) 코루틴 함수를 함수 이름과 넘겨질 파라미터를 통해 호출하는 과정을 보여주고 있습니다. 그리고 1초 기다린 뒤에 실행했었던 DoSomething 코루틴을 종료시킵니다. 이러한 함수 이름을 문자열로 넘겨 실행하는 방법은 StartCoroutine을 수행하는데에 오버헤드가 크고 파라미터를 한개밖에 넘길 수 없다는 제약사항이 있습니다. 물론 배열을 넘기는것 역시 가능합니다.

object[] parms = new object[2] { floatParameter, stringParameter };
StartCoroutine ("MyCoroutine", parms);

yield return에서 사용할 수 있는 것들

위에서 본 예시에는 WaitForSeconds 클래스를 양보 반환함으로써 원하는 시간(초)만큼 기다리는 것이 가능하다는것을 알 수 있었습니다. 추가로 더 알아 보도록 하겠습니다.

yield return new WaitForSecondsRealtime (float time);

WaitForSeconds와 하는 역할은 동일하지만 결정적으로 다른것이 있습니다. 유니티상의 시간은 임의로 느리게 하거나 빠르게 하는 것이 가능합니다. 이를 Time.timeScale을 통해서 조정을 할 수 있습니다. 매트릭스에서 보던 총알이 느리게 날아오면서 그것을 피하는 모션을 구현해 본다면 이 값을 1보다 낮추게 되면 현재 시간의 진행보다 느려지게 되며 1보다 빠르게 변경하면 현재의 시간의 진행보다 빨라지게 됩니다. 하지만 WaitForSecondsRealtime는 이러한 Scaled Time의 영향을 받지 않고 현실 시간 기준으로만 동작을 하게 됩니다.

yield return new WaitForFixedUpdate ();

다음 FixedUpdate() 가 실행될때까지 기다리게 됩니다. 이 FixedUpdate()는 Update()와 달리 일정한 시간 단위로 호출되는 Update() 함수라고 생각하시면 됩니다.

yield return new WaitForEndOfFrame ();

하나의 프레임워 완전히 종료될 때 호출이 됩니다. Update(), LateUpdate() 이벤트가 모두 실행되고 화면에 렌더링이 끝난 이후에 호출이 됩니다. 특수한 경우에 사용하면 될 것 같습니다만 잘 모르겠군요.

yield return null;

WaitForEndOfFrame를 이야기 했다면 이것을 꼭 이야기 해야 할 것 같습니다. yield return null; 을 하게 되면 다음 Update() 가 실행될때까지 기다린다는 의미를 갖게 됩니다. 좀 더 정확하게는 Update()가 먼저 실행되고 null을 양보 반환했던 코루틴이 이어서 진행 됩니다. 그 다음에 LateUpdate()가 호출됩니다.

yield return new WaitUntil (System.Func<Bool> predicate);

이번엔 특정 조건식이 성공할때까지 기다리는 방법입니다. WaitUntil에 실행하고자 하는 식을 정의해 두면 매번 Update() 와 LateUpdate() 이벤트 사이에 호출해 보고 결과값이 true면 이후로 재진행을 하게 됩니다. 다음의 예제 코드를 보겠습니다.

public class WaitUntilExample : MonoBehaviour
{
    public int frame = 0;

    void Start ()
    {
        StartCoroutine( Example() );
    }

    IEnumerator Example ()
    {
        Debug.Log( "공주를 구출하기 위해 기다리는 중..." );
        yield return new WaitUntil( () => frame >= 10 );
        Debug.Log( "공주를 구출했다!" );
    }

    void Update ()
    {
        if ( frame <= 10 )
        {
            Debug.Log( "Frame: " + frame );
            frame++;
        }
    }
}

이 코드는 Update() 함수를 통해 매 프레임마다 frame 멤버 변수값을 1씩 최대 10까지 증가시키게 됩니다. 실행중인 코루틴은 frame값이 10또는 10보다 커질때까지 기다리다가 이 식이 충족되게 되면 다음으로 진행을 하게 됩니다. 여기서 사용되는 식은 람다 표기법이 사용됩니다. 다음과 같은 느낌이라고 생각하시면 될 것 같습니다.

Func<int, int> func1 = ( int x ) => x + 1;
Func<int, int> func2 = ( int x ) => { return x + 1; };

yield return new WaitWhile(System.Func<Bool> predicate);

WaitWhile은 WaitUntil과 동일한 목적을 가지고 있지만 한가지만 다릅니다. WaitUntil은 람다식 실행 결과값이 true가 될때까지 기다린다면 WaitWhile은 false가 될때까지 기다립니다. 즉 WaitWhile은 결과가 true인 동안 계속 기다리게 됩니다.

public class WaitWhileExample : MonoBehaviour
{
    public int frame = 0;

    void Start ()
    {
        StartCoroutine( Example() );
    }

    IEnumerator Example ()
    {
        Debug.Log( "공주를 구출하기 위해 기다리는 중..." );
        yield return new WaitWhile( () => frame < 10 );
        Debug.Log( "공주를 구출했다!" );
    }

    void Update ()
    {
        if ( frame <= 10 )
        {
            Debug.Log( "Frame: " + frame );
            frame++;
        }
    }
}

위의 코드는 첫프레임부터 람다식의 결과가 true이게 됩니다. 10프레임에 도달하면 false가 되어서 이후 진행이 되겠네요.

yield return StartCoroutine (IEnumerator coroutine);

이번에는 심지어 코루틴 내부에서 또다른 코루틴을 호출할 수 있습니다. 물론 그 코루틴이 완료될 때까지 기다리게 됩니다. 의존성 있는 여러작업을 수행하는데에 유리하게 사용 될 수 있습니다.

void Start ()
{
    StartCoroutine( TestRoutine() );
}

IEnumerator TestRoutine ()
{
    Debug.Log( "Run TestRoutine" );
    yield return StartCoroutine( OtherRoutine() );
    Debug.Log( "Finish TestRoutine" );
}

IEnumerator OtherRoutine ()
{
    Debug.Log( "Run OtherRoutine #1" );
    yield return new WaitForSeconds( 1.0f );
    Debug.Log( "Run OtherRoutine #2" );
    yield return new WaitForSeconds( 1.0f );
    Debug.Log( "Run OtherRoutine #3" );
    yield return new WaitForSeconds( 1.0f );
    Debug.Log( "Finish OtherRoutine" );
}

위와 같은 코드를 실행해 본다면 결과는 다음과 같이 출력됩니다.

Run TestRoutine
Run OtherRoutine #1
Run OtherRoutine #2
Run OtherRoutine #3
Finish OtherRoutine
Finish TestRoutine

Coroutine 중단하기

public void StopCoroutine(IEnumerator routine);

이 방법은 기존에 StartCoroutine을 실행할 때 넘겨주었던 코루틴 함수의 열거자를 파라미터로 사용하여 그것을 중단시키는 방법입니다. 다음과 같은 사용이 가능합니다.

public class Example : MonoBehaviour
{

    private IEnumerator coroutine;

    void Start ()
    {
        coroutine = WaitAndPrint( 1.0f );
        StartCoroutine( coroutine );
    }

    public IEnumerator WaitAndPrint ( float waitTime )
    {
        while ( true )
        {
            yield return new WaitForSeconds( waitTime );
            Debug.Log( "WaitAndPrint " + Time.time );
        }
    }

    void Update ()
    {
        if ( Input.GetKeyDown( "space" ) )
        {
            StopCoroutine( coroutine );
            Debug.Log( "Stopped " + Time.time );
        }
    }
}

Start() 함수에서 WaitAndPrint(waitTime) 코루틴 함수의 열거자를 획득하여 클래스의 멤버 변수로 설정해 두고 이 코루틴을 실행합니다. 이 코루틴은 1초에 한번씩 WaitAndPrint 를 출력하게 되며 유저가 스페이스키를 누르게 되면 멤버 변수에 담겨 있는 기존 코루틴의 열거자를 이용하여 실행중인 코루틴을 중단시킵니다.

public void StopCoroutine(string methodName);

이 방법은 이전 방식보다 오버헤드는 크지만 간편하게 사용할 수 있는 방법입니다. 다음과 같이 멤버 변수 없이도 간편하게 사용할 수 있습니다.

public class Example : MonoBehaviour
{

    void Start ()
    {
        StartCoroutine( "WaitAndPrint", 1.0f );
    }

    public IEnumerator WaitAndPrint ( float waitTime )
    {
        while ( true )
        {
            yield return new WaitForSeconds( waitTime );
            Debug.Log( "WaitAndPrint " + Time.time );
        }
    }

    void Update ()
    {
        if ( Input.GetKeyDown( "space" ) )
        {
            StopCoroutine( "WaitAndPrint" );
            Debug.Log( "Stopped " + Time.time );
        }
    }
}

이때에 주의할 점으로는 StopCoroutine을 문자열로 종료시키려면 StartCoroutine 역시 문자열로 실행했었어야 한다는 점입니다. StartCoroutine(IEnumerator routine) 으로 실행한 다음에 StopCoroutine(string methodName) 으로 종료시킬 수 없습니다.

public void StopAllCoroutines();

마지막으로 현재 Behaviour (클래스라고 이해하면 될 것 같습니다)에서 실행한 모든 코루틴을 한번에 종료시키는 함수입니다. 이와 같은 방법으로 현재 클래스에서 실행한 모든 코루틴을 한번에 중단시키게 됩니다.

public class ExampleClass : MonoBehaviour
{
    IEnumerator DoSomething ()
    {
        while ( true )
        {
            yield return null;
        }
    }
    void Example ()
    {
        StartCoroutine( "DoSomething" );
        StopAllCoroutines();
    }
}

어디선가 Example()을 실행하게 되면 DoSomething 코루틴이 실행되게 되면 곧바로 StopAllCoroutines() 이 호출되어 모든 코루틴이 종료됩니다.

참고 :
http://docs.unity3d.com/kr/current/Manual/Coroutines.html
http://docs.unity3d.com/ScriptReference/MonoBehaviour.StartCoroutine.html

idisposable

Unity C# – IDisposable 알아보기

unity_logo

이번에는 IDisposable 인터페이스에 대해서 알아보겠습니다. C#은 가비지콜랙터(Garbage Collector)를 가지고 있습니다. 이 GC는 기본적으로 관리되는 모든 객체들의 참조 링크를 관리하며 더이상 참조되지 않는 객체들을 자동으로 메모리에서 소거하는 작업을 수행합니다. 하지만 GC는 창 핸들, 열린 파일, 스트림과 같이 관리되지 않는 리소스들을 인식하지 못합니다.

다음은 문제가 발생할 가능성이 있는 StreamReader의 사용 예 입니다.

하지만 위의 코드는 ReadToEnd() 메소드를 수행하는 과정에서 오류가 발생할 가능성이 있습니다. 이때에 Close()가 호출되지 않고 반환될 가능성이 있습니다. 이러한 문제를 대응하기 위해 흔히들 try-catch-finally 구문을 사용해 볼 수 있습니다.

위의 코드는 ReadToEnd() 메소드를 수행중에 예외가 발생되면 발생한 예외의 내용을 로그에 출력하게 됩니다. 그리고 예외가 발생하거나 성공하거나 상관없이 Close() 메소드를 정상적으로 수행하게 됩니다. 하지만 이러한 복잡한 과정 없이 Close() 호출을 알아서 호출해주는 구문이 있습니다. 위와 같은 방법이 아닌 using 블록을 사용하여 다음과 같이 처리할 수 있습니다.

위의 코드에서 우선 눈에 띄는 것은 using 키워드를 사용했다는 점과 Close() 를 명시적으로 호출하지 않고 있다는 부분입니다. 이제 IDisposable에 대해 이야기를 해볼 때인 것 같습니다.

스크린샷 2016-08-11 오후 6.22.38

SteamReader는 TextReader의 자식 클래스입니다. 그리고 이 TextReader는 IDisposable 인터페이스를 구현하고 있습니다. 스크린샷 2016-08-11 오후 6.25.08

이 IDisposable 인터페이스는 Dispose() 메소드 하나만을 가지고 있네요. 이 IDisposable 인터페이스를 구현한 클래스는 Dispose() 클래스를 구현해야 하며 여기서 자신의 메모리 할당 내역을 정리해야 합니다. 위에서 보여준 예제에서 보여지는 StreamReader 역시 Dispose() 클래스를 구현하고 있으며 여기서 리소스를 정리하는 적절한 처리가 되어있을 것입니다.

이제 위의 코드는 IDisposable 인터페이스를 구현하고 있는 클래스를 인스턴스화 하여 사용하며 using 블록을 나가는순간 (심지어 오류가 발생하더라도) Dispose() 가 호출되어 사용한 리소스가 자동으로 정리됩니다.

그렇다면 using 블록 내부에서 발생한 예외는 어떻게 처리할 수 있을까요? 다음과 같은 방법으로 처리할 수 있습니다.

using 바깥쪽에 try-catch 문을 사용해도 using 블록을 빠져나갈 때 Dispose() 가 호출됩니다.

참고 : https://msdn.microsoft.com/ko-kr/library/system.idisposable(v=vs.110).aspx

Navier-Stokes 방정식

유체방정식인 Navier-Stokes 방정식을 푸는 것이 게임에 구현된 것은 거의 못 본것 같습니다. 아직까지는 단순한 노이즈 기반의 식을 쓰는 것 정도로 충분한것 같고요. 저는 지금까지는 이런 물리식을 게임에 쓰는일은 전혀 안해봤네요. 논문상으로는 파티클을 생성시켜서 물결을 일으키는 수준까지는 있습니다만 대부분 이펙트로 처리하는 것 같습니다.

참고로 Navier-Stokes방정식을 영화 CG쪽에서는 Newtonian 방법이나 Lagrangian 방법으로 나뉘는데 Newtonian은 물의 공간을 표현하는데 좋고 (일반적인 프로그램에서 지원하는 것 같더군요) Lagrangian은 물뿌리는 것에 좋습니다. 점성도 표현 가능하죠. (Realflow라는 프로그램이 많이 쓰입니다.) 다만 실시간이 절대 아니고요 시간이 아주 오래오래 걸려요.

게임쪽에 물리가 들어오기엔 좀 어려워보이는게 아무래도 해외 진출을 해야하는 형편이다보니 저사양을 고려하지 않을수 없어서 물리를 넣기에는 좀 어려울것 같아요.

결국 서버에서 물리를 자세히 하는 것은 오버라고 보고요 잘못된 위치를 보정해주는 정도만 해도 될것 같네요. 이런 일은 클라이언트 프로그래머가 가능한 업무 범위가 아닌가 생각됩니다.

Graveyard Keeper: How the graphics effects are made

Hello! I’m a lead programmer of the game “Graveyard Keeper”. I’d like to share with you some techniques and tricks that we used to make our game look as you see it from the GIF below.

We are very passionate about graphics in our games. That’s why we put so much time and effort into different effects and other stuff that makes our pixel art as attractive as possible. Maybe you will find here something helpful for your work.

First I want to say some words about big parts that compose the visual part of our game:

  • Dynamic ambient light is just change of illumination depending on the time of the day.
  • LUT color-correction is responsible for the change of color shades depending (again) on the time of the day (or world zone).
  • Dynamic light sources — torches, ovens, lamps.
  • Normal maps — make objects look like they have a real volume, especially when light sources move.
  • The math of light 3D distribution — a light source centered on the screen should illuminate a higher object properly and should not illuminate an object below (turned to the camera with its dark side).
  • Shadows are made with sprites, they turn and react to light source positions.
  • Object altitude simulation — for the fog to be displayed correctly.
  • Other stuff: rain, wind, animations (including shader animation of leaves and grass) and so on.

Let’s talk about those in more details now.

Dynamic ambient light

Nothing special here. Darker at night, lighter in the daytime. The light’s color set with a gradient. By nightfall a light source not only becomes darker but gets blue tint.

It looks like this:

LUT color correction


LUT (Look-up table) is a table of color change. Roughly speaking it’s a three-dimensional  RGB array where each element (a) corresponds to a color with its coordinates as RGB values; (b) contains the color value that the color associated with the element should be changed to. So if there is a red point at coordinates  (1, 1, 1), it means that all the white color from the picture will be replaced with red. But if there is white color at the same coordinates (R=1, G=1, B=1), the change doesn’t happen. So LUT by default has coordinates associated with a particular color. I mean that point with coordinates (0.4, 0.5, 0.8) is associated with color (R=0.4, G=0.5, B=0.8).

It should also be pointed here that for convenience that 3D texture is represented as two-dimensional. That’s how for example default LUT (that doesn’t change color) looks:

Easy to make. Easy to use. Works really fast.

It is very easy to set up — you give an artist a picture from your game and say: “Make it look like it’s evening”. Then you apply all color layers plus the default LUT. Congratulations! You get an Evening LUT.

Our artist was actually very passionate about it. He created 10 different LUTs for different time of day (night, twilight, evening and so on…) That’s how the final set of LUTs looks:

As a result one location may look different at different time of day:

The picture also shows how the intensity of the light sprites changes depending on the time of day.

Dynamic light sources and normal maps

We use regular light sources, the default Unity ones. Also, every sprite has its own normal map. This helps to feel the volume.

These normals are pretty easy to draw. An artist roughly paints light on 4 sides with a brush:

And then we merge them with a script in a normal map:

If you are looking for a shader (or software) that can do that, pay attention to Sprite Lamp.

3D light simulation

This is where things start to be a bit more complicated. You can’t just light the sprites. It’s very important whether a sprite is “behind” a light source or “in front of” it.

Take a look at this picture.

These two trees are at the same distance from the light source but the back one is illuminated while the front one is not (because the camera faces its dark side).

I solved this problem very easily. There is a shader that calculates the distance between a light source and a sprite on the vertical axis. If it’s positive (the light source is in front of the sprite), we illuminate the sprite like we always do, but if it’s negative (the sprite is blocking the light source), then the intensity of lighting is fading depending on the distance at a very quick rate. There is a rate, not just “not to illuminate”. So if the light source behind the sprite is moving, the sprite is blackening gradually, not at once. Yet it’s still very fast.

Shadows

Shadows are made with sprites rotating around a point. I tried to add a skew there, but it turned out to be unnecessary.

Every object may have a maximum of 4 shadows. The shade from the sun and the additional three from dynamic light sources. So the image below shows this concept:

The problem “how to find the closest 3 light sources and to calculate the distance and an angle was solved with a script running in the Update() loop.

Yes, it’s not the quickest way considering how much math is involved. If I programmed it today, I would use that modern Unity Jobs System. But when I did it there was no such thing so I had to optimize regular scripts we had.

The most important thing here is that I did the sprite rotation not modifying the transform but inside a vertex shader. So the rotation isn’t involved here. You just put a parameter to a sprite (I used the color channel for that, all the shadows are black anyway), while the shader is responsible for sprite rotations. It turns out to be quicker — you don’t have to use Unity geometry.

There is a minus to this approach. The shadows are to be adjusted (and sometimes drawn) separately for each object. Yet actually, we used about ten different more or less universal sprites (thin, thick, oval etc.)

The next disadvantage is that it’s difficult to make a shadow for an object that is stretched along one axis. For instance, look at the fence shadow:

Not ideal. That’s how it looks if you make a fence sprite translucent:

It should be noted here, by the way, that the sprite is highly distorted vertically (the shadow sprite original looks like a circle). That’s why its rotation looks like not just a simple rotation but also like a distortion.

The fog and the altitude simulation


There is also fog in the game. It looks like this (the regular version above and and an extreme 100% fog to demonstrate the effect).

As you see, the tops of houses and trees are seen from the fog. In fact, this effect is really easy to make. The fog consists of a great deal of horizontal clouds spread across all the picture. As a result the upper part of all the sprites is covered with a fewer amount of fog sprites:

The wind

The wind in a pixel art game is a completely different story. There are not many options here. To animate manually (it’s not possible considering how much art we have) or to make a deforming shader, but then you’ll have to deal with some ugly distortions. You can also don’t do any animation, but then the picture will look static and lifeless.

We chose the deforming shader. It looks like that:

It’s pretty obvious what’s happening here if we apply the shader to the chequered texture:

It should also be pointed here that we do not animate the whole crown of a tree but some particular leaves:

There are also an animation of a wheat field shake and things are pretty simple here too. The vertex shader changes the shape of the x-coordinates taking y-coordinate into account. The highest point will be shaken the most intensely. The intention here is that the top should move while the root shouldn’t. Plus the phase of shaking varies according to the x/y coordinates to make different sprites move separately.

This shader is also used to create a swinging effect when a player goes through wheat or grass.

I think that’s all for now. I haven’t spoken about scene construction and about its geometry, because there is a lot to speak about in a new entry. Beyond that I’ve spoken about all the main solutions that we applied developing our game.

유니티에서 가비지 컬렉션 최적화 하기 – 번역글 공유

안녕하세요. 유니티에서 가비지 컬렉션에 대한 글을 번역한 것 공유합니다. 
원문 링크 – https://unity3d.com/kr/learn/tutorials/topics/performance-optimization/optimizing-garbage-collection-unity-games?playlist=44069 –

전체 원문 내용이 많아서 6개의 글로 나눠서 포스팅했습니다.
유니티에서 메모리를 관리하는 방법과 가비지 컬렉터(Garbage Colletor)의 동작 방식 및 주의할 내용을 정리하기 좋은글이니 참고하세요~

가비지 컬렉션 개요 – http://bit.ly/optimizinggc1
가비지 컬렉션과 동반되는 문제 – http://bit.ly/optimizinggc2
가비지 컬렉션에 의한 피해 줄이기 – http://bit.ly/optimizinggc3
불필요한 힙 할당을 발생시키는 주요 원인1 – http://bit.ly/optimizinggc4
불필요한 힙 할당을 발생시키는 주요 원인2 – http://bit.ly/optimizinggc5
가비지 컬렉션의 영향을 최소화하기 위한 코드의 구조화 – http://bit.ly/optimizinggc6

블로그 방문 많이 해주시고, 배너도 많이 눌러주세요 🙂
감사합니다.

서버 프로그래밍 공부에 대한 생각

어떤 분께서 클라이언트신데 서버를 어떻게 배워야 할지 모르겠다고 쓰신 글을 보고 작성해 봅니다.C++ 윈도우 서버를 처음부터 만든다면 다음과 같은 내용을 공부해야 할것으로 생각됩니다.지극히 주관적인 생각이니까 참고만 부탁드려요.

1. 준비학습
1-1. win socket (tcp/udp)
1-2. iocp
1-3. overlapped io
1-4. 멀티스레드
1-5. 메모리/오브젝트 풀
1-6. 3,5를 활용한 브로드캐스팅
1-0. 별도로 간단히 SQL의 사용법
이 밖에도 TCP의 전송 및 혼잡제어 부분은 udp를 만들때 응용되기도 하고, 전송 중 예외처리를 위해 중요하다고 생각합니다.보통 이쯤 될때까지 혼자 만들다보면 어쩐지 udp를 슬슬 버리게 됩니다. 이핑계 저핑계 대면서요 ㅎ중국에서 udp를 쓰면 다 털린다던가…암호화 알고리즘에 대해 나와야 할것같은데? 라고 생각하시는 분도 계시겠지만 공부하며 혼자 하다보면 저것도 벅차요 ㅋ디비는 살짝 논외로 쳤는데 결국은 필요해집니다. 레디스라거나 하는걸 추가로 학습하면 좋을듯!

공부가 끝난 뒤 개발해야 할 내용은
2. 모듈 개발
2-1. 로깅시스템
2-2. 메모리/오브젝트 풀
2-3. 스레드 풀 (TLS랑 전역적인거 둘 다 되는걸로)
2-4. iocp 모듈 (프로엑터 패턴. tcp, udp)
2-5. 패킷 프로세싱 (전송 및 혼잡제어)
2-6. 테스트로 사용할 서버/클라
2-7. 우아한 접속/종료 및 예외처리
2-8. 스트레스 테스트 툴
이렇게 만들어보면 조금 감이 올 것이고 그 다음에 해야 할 일은 모듈화 해서 라이브러리로 만드는 것이겠죠.그런데 라이브러리화가 끝나면 ‘자 이제 시작이야~(내꿈을) 내꿈을 위한여행~(피카츄)’ 가 시작됩니다.바로 서버 구조란 무엇인가… 에 대해 고민해야 할 차례거든요.

3. 서버 구조 설계
3-1. 한대의 서버가 클라이언트들을 받아들이는 가장 단순한 구조 (유저가 많아지면 서버를 나눔)
3-2. 서버가 많아짐에 따라 접속/인증만 처리하는 로비서버 + 여러대의 게임서버 구조
3-3. 인증을 만들다보면 필요한 페킷 암호화 및 인증키 처리 (여기서 이걸 우겨넣다가 라이브러리를 재설계 하는 일도;;)
3-4. 그런데 요즘 유저들은 서버 자체가 나뉘면 싫어하지 않을까요? 체널링에 대한 고민
3-5. 체널링에 따른 인증 이동기능
3-6. 서버간 통신 및 브로드캐스팅 (ex 메이플의 메가폰. 운영자공지 등)
뭐 이정도 공부하고 이정도 만들면 되지 않을까… 그렇게 생각하고 있습니다.그런데 과연… 자신이 서버 프로그래머로 취직할게 아니거나, 컨텐츠 서버 프로그래머로 취직하려고 해도 이런 내용들이 전부 필요할까요? 저는 그것에 대해서는 상당히 회의적입니다. 간단히 클라이언트만 봐도 요즘 DirectX12 배우시는 학생분들 있으신가요? 여기서 만약 부스트 asio가 등장한다고 해도 대분류 1번 준비학습 정도만 생략되는 수준입니다.뿐만 아니라 아무리 asio나 부스트가 좋다고 해도 사실 1번에서 누군가 이끌어주지 않으면 많은 시행착오를 겪게됩니다.어떻게 만들어야 쓰기 편하고 표준적인지 알기 어렵기 때문이죠.DirectX 처음 배울때 3D MAX 파싱해서 물체 띄우는 수준으로 게임엔진을 만들 수 없는것과 같습니다.

4. 권장 사항이때 제가 권할 수 있는 방법은 일단 오픈소스나 상용엔진을 학습해보는 것입니다.여기서 서버엔진이란 무엇이며, 어떻게 쓰는것이 표준적이고 효율적인 설계인지를 엿볼 수 있습니다.그 뒤에 자신이 무엇을 하고싶은지 고민해보세요.컨텐츠를 만드시고 싶다면 서버엔진을 배우시고 잘 활용하는 쪽으로 학습하시는걸 추천합니다.그러나 나는 ‘서버 엔진 프로그래머가 되겠어!’라고 결심하셨다면 위의 것들을 하나씩 정복해 나가시면 될 것 같습니다 ㅎ

5. 핵심?프로그래머로 일하는데 있어서 서버를 지향하시는 분들께서는 이정도면 되지 않을까.. 그렇게 생각합니다.라우터라던가 L4스위치 등 하드웨어에 대한 지식이 풍부하면 도움은 되겠지만 프로그래밍 이외의 범위임으로 일단 생략…우리 마음대로 만들겠다! 라며 상용 엔진을 사용하지 않는 회사들도 재야의 고수들이 기본기 있고 범용적인 라이브러리드ㄹ을 자체적으로 개발해서 사용하고 있기 때문에 무료/상용 엔진으로 기본기와 구조를 익혀도 좋습니다.넷트워크에서 중요한 신뢰성등은 사실 지식도 필요하지만 많은 노하우가 중요한 만큼 상용엔진을쓰는것이 안정적이고 편안하고 안락한 퇴근을 보장하죠… ㅇ<-<세롭게 공부하면서 개발해 보는것도 중요하지만 상용 엔진을 권하는 이유는 이밖에도 많습니다.요즘 트렌드는 엔진을 처음부터 개발하기 보다는 그간 만들어진 바퀴들도 얼마나 멋진것을 만들어내느냐에 있으니까요!

두서없고 주관적인 글 읽어주셔서 감사합니다. 위에 살짝 요약해뒀고요. 질문이나 부드러운 의견은 언제나 환영합니다!