이전 포스팅에 예고한대로 매칭, 제거, 리필 로직에 대해 포스팅하겠습니다.

포스팅은 각 로직에 해당하는 함수를 소개하고, 각 함수에 대해 설명하는 식으로 진행하겠습니다.


1. Match

 생성된 모든 타일에 대해서 매칭을 판별합니다.

    // 모든 타일에 대해 매칭 판별하는 함수
    // 매칭에 해당하는 모든 리스트를 반환한다.
    private int[,] tileArray;
    public List<TileLine> CheckAllMatch()
    {
        List<TileLine> matchLines = new ListList<TileLine>();
        // 좌 -> 우
        matchLines.AddRange(CheckLineMatchAll(HEIGHT,WIDTH,false,new IntVector2(1,0)));
        // 상 -> 하
        matchLines.AddRange(CheckLineMatchAll(WIDTH, HEIGHT, true, new IntVector2(0, 1)));

        return matchLines;
    }
    
    private List<TileLine> CheckLineMatchAll(int line1, int line2, bool isVertical, IntVector2 checkPos)
    {
        List<TileLine> matchLines = new List<TileLine>();
        for (int i = 0; i < line1; i++)
        {
            for (int j = 0; j < line2; j++)
            {
                int count = (isVertical) ? CheckLineMatchFromOneTile(i, j, checkPos) : CheckLineMatchFromOneTile(j, i, checkPos);
                if (count >= 2)
                {
                    TileLine line = new TileLine(isVertical, i, j, j+count);
                    matchLines.Add(line);
                }
                j += count;
            }
        }
        return matchLines;
    }
    // 기준타일로부터 방향(_checkPos)에 같은 타일이 몇개 있는지 반환
    private int CheckLineMatchFromOneTile(int _x, int _y, IntVector2 _checkPos)
    {
        int deltaX = _checkPos.x + _x;
        int deltaY = _checkPos.y + _y;
        int count = 0;
        while (!isOutArray(deltaX, deltaY))
        {
            if (tileArray[_y, _x] == tileArray[deltaY, deltaX])
            {
                count++;
            }
            else
            {
                return count;
            }
            deltaX += _checkPos.x;
            deltaY += _checkPos.y;
        }
        return count;
    }

 위의 함수는 현재 왼쪽 -> 오른쪽, 위쪽 -> 아래쪽으로 타일을 비교하며 3개 이상의 매칭을 판별하고 리턴해줍니다. (코드를 간결하게 짜야하는데...)

 CheckLineMatchFromOneTile 로직은 기준 타일에서 제시된 방향으로 차례로 돌며, 현재 타일이 기준 타일과 동일한 종류의 타일이면 count 갯수를 증가시켜 반환하고, CheckLineMatchAll 에서는 count가 2 이상일 경우 3match로 판단합니다.

 CheckLineMatchFromOneTile 의 경우는 유저의 swap에 따른 4방향 판별에도 사용됩니다.


2. Remove

    // 매칭된 타일을 제거하는 함수
    public void RemoveMatchTile(ref int[,] _tileArray,List<TileLine> _tileLines)
    {
        foreach (TileLine tile in _tileLines)
        {
            IntVector2[] points = tile.GetLinePoints();
            for(int i = 0; i < points.Length; i++)
            {
                _tileArray[points.y,points.x] = -1;
            }
        }
    }

TileLine
    public bool[] GetUseWidthTile(bool[] refillArray)
    {
        if (isVertical)
        {
            refillArray[lineIndex] = true;
        }
        else
        {
            for (int j = startIndex; j <= endIndex; j++)
            {
                refillArray[j] = true;
            }
        }
        return refillArray;
    }

 위의 함수는 매칭된 리스트를 읽어와  -1 ( 빈공간 취급) 으로 값을 변경하고 있습니다. 

 이건 로직은 너무 단순해서... TileLine 즉, 매칭 리스트를 어떻게 저장할 거냐에 따라 달라지겠네요. 저 같은 경우는 가로세로 여부, 라인 번호, 시작 인덱스, 종료 인덱스를 저장하고, 시작~종료까지 공란으로 바꾸는 식으로 진행했습니다.


3. Refill


    // 제거된 타일의 빈공간을 채우는 함수.
    public void RefillTile(ref int[,] _tileArray, List<TileLine> _tileLines)
    {
        // 리필을 할 영역(세로 라인) 표시
        bool[] refillArray = new bool[WIDTH];
        foreach (TileLine tile in _tileLines)
        {
            refillArray = tile.GetUseWidthTile(refillArray);
        }
        //리필 영역에 위에 있는 타일을 내려 빈공간을 채우고, 채우고 남은 최상단 빈 공간의 갯수를 저장한다.
        int[] spaceArray = new int[WIDTH];
        for(int i = 0; i < WIDTH; i++)
        {
            if (refillArray[i])
            {
                int space = 0;
                for(int j = HEIGHT - 1; j >= 0; j--)
                {
                    if(_tileArray[j,i] == -1)
                    {
                        space++;
                    }
                    else
                    {
                        if (space > 0)
                        {
                            _tileArray[j+space,i] = _tileArray[j, i];
                            _tileArray[j, i] = -1;
                        }
                    }
                }
                spaceArray[i] = space;
            }
        }
        // 최상단 빈 공간에 타일을 새로 채운다. 
        for (int i = 0; i < WIDTH; i++)
        {
            if (refillArray[i])
            {
                for (int j = 0; j < spaceArray[i]; j++)
                {
                    if (_tileArray[j, i] == -1)
                    {
                        int index = Random.Range(0, TileCount);
                        _tileArray[j, i] = index;

                    }
                }
                    
            }
        }
       
    }

TileLine
    public IntVector2[] GetLinePoints()
    {
        IntVector2[] points = new IntVector2[endIndex - startIndex +1];
        if (isVertical)
        {
            for (int i = startIndex; i <= endIndex; i++)
            {
                points[i - startIndex].x = lineIndex;
                points[i - startIndex].y = i;
            }
        }
        else
        {
            for (int i = startIndex; i <= endIndex; i++)
            {
                points[i - startIndex].x = i;
                points[i - startIndex].y = lineIndex;
            }
        }

        return points;
    }

 제거된 타일의 빈 공간을 채우는 함수는 세 단계로 이루어집니다.

1. 제거된 빈 공간을 채울 세로 라인 넘버를 저장한다.

2. 저장된 라인 넘버에 대해서 아래부터 위로 차례대로 타일을 내려 빈 공간을 채운다.

3. 빈 공간을 채우고 남은 최상단에 새로운 타일을 넣는다.



상기 코드는 로직만을 구현이 되어있고, 실질적으로 눈에 보이는 것이 없습니다. (오브젝트와 연출) 이 부분은 개별적으로 진행하는 것으로하고, 제가 가장 신경쓰이는 것은 이게 과연 효율적인 로직이냐 입니다.... 오브젝트, 연출까지해서 총 3일이 걸렸는데, 매 코드를 건들 때마다 못미덥네요....ㅠㅠ 쉽게 생각나는 알고리즘은 비효율일 경우가 많아서.. 조금이라도 더 효율적인 로직이 있을거라 생각됩니다. 우선 현재는 이렇게 진행하고, 앞으로는 좀 더 효율적인 로직이 있을지 리서칭을 해보고, 찾는다면 4번째 포스팅에서 로직 개선을 진행하겠습니다. ( 라는 말은 결국 좋은 로직을 찾을 때까지 무기한 연장이라는...)



완성된 후의 VS2015 클래스 다이어그램 (클릭하면 원본 이미지)


 주요 클래스만 설명하자면...

TilePanelModel - 타일들에 대한 로직 담당 (타일 생성,추가,리필 등)

TilePanelView - 타일들의 애니메이션 담당

TileObject - 유저와의 인터렉티브 담당 (클릭 드래그) / 페이즈(Phase) 상태에 따라 행동 위임

ScoreManager - 점수 처리 및 View 담당




* 함수별로 테스트는 완료되었으나 포스트로 옮기는 과정에서 오타가 있을 수 있습니다. 문제 있을시 알려주세요.


* 위 로직 + 오브젝트와 연출의 결과물입니다.














* 지적은 언제나 환영입니다.

Posted by 검은거북

우선 MatchManager를 통해서 전체 게임을 진행하겠습니다.

우선 필요한 로직을 함수 단위로 나누고, 함수를 채우는 식으로 진행할 예정입니다.

    
    public struct TileLine
    {
        private bool isVertical;
        private int lineIndex;
        private int startIndex;
        private int endIndex;
    }
    // 전체 타일을 랜덤하게 생성하는 함수
    // 최초 시작시와 매칭 할 수 있는게 없을 경우 호출한다.
    public void MakeAllTile(ref int[,] _tileArray)
    {

    }

    // 모든 타일에 대해 매칭 판별하는 함수
    // 매칭에 해당하는 모든 리스트를 반환한다.
    public List<TileLine> CheckAllMatch(int[,] _tileArray)
    {
        return null;
    }

    // 매칭된 타일을 제거하는 함수
    public void RemoveMatchTile(ref int[,] _tileArray,List<TileLine> _tileLines)
    {
        
    }

    // 제거된 타일의 빈공간을 채우는 함수.
    public void RefillTile(ref int[,] _tileArray, List<TileLine> _tileLines)
    {

    }

    // 채워진 타일이 매칭 가능한 패널이 존재하는지 확인.
    // 존재하지 않는다면 타일 생성을 다시해야한다.
    public bool CheckCanMatch(int[,] _tileArray)
    {
        return false;
    }
    
    // 특정 위치에 있는 타일에 대해 4방향으로 매칭이 있는지 확인하는 함수.
    // 유저가 swap 시 swap된 두 타일에 대해 사용한다.
    public List<TileLine> CheckOneMatch(int[,] _tileArray, int _x,int _y)
    {
        return null;
    }

이전 포스팅에서 나온 플로우에 따라 함수를 작성하였습니다. 위 함수는 아래와 같이 매칭됩니다.

1) 최초 타일 생성 - MakeAllTile

2) 매칭 판단 - CheckAllMatch

3) 매칭 제거 - RemoveMatchTile

4) 타일 리필 - RefillTile

5) 매칭 리스트가 없을때까지 1~4 반복

6) 매칭 할게 없는지 판단 - CheckCanMatch


7) 유저 인터렉트에 따른 부분 매칭 판단 - CheckOneMatch



위 함수들을 플로우에 맞게 최초 생성 로직 (1~6)을 짜보면 아래와 같습니다. 

    const int WIDTH = 9;
    const int HEIGHT = 9;

    int[,] tileGrid = new int[HEIGHT,WIDTH];
    
    void Start () {
        do
        {
            // 최초 전체 타일 생성.
            // 타일에 match 할 수 있는게 없다면 다시 타일 전체 생성.
            MakeAllTile(ref tileGrid);

            bool isMatch = true;
            while (isMatch)
            {
                // 전체 타일 중 match되는 타일 판별
                List<TileLine> matchLine = CheckAllMatch(tileGrid);

                //match가 있다면 제거하고 타일을 리필한 다음 다시 전체 match 판별 진행.
                if (match.Count > 0)
                {
                    isNotMatch = true;
                    RemoveMatchTile(ref tileGrid, matchLine);

                    RefillTile(ref tileGrid, matchLine);
                }
                else
                {
                    isMatch = false;
                }
            }
        } while (!CheckCanMatch(tileGrid));
    }


 7번 유저로직에 따른 진행은 GameManager가 아닌 Tile 클래스에서 터치이벤트에 따라 별도로 동작을 정의하였으나, 중요로직은 GameManager에 있다고 생각되어 앞으로 포스팅은 GameManager의 중요 로직이라고 생각하는 부분만 올리도록하겠습니다.

아마 매칭 로직과 제거, 리필 로직만 소개하게 될 거 같습니다.



* 지적은 언제나 환영입니다.

Posted by 검은거북

기획 

 타일 : 9 X 9 , 타일종류 : 5개

 1. 가로 세로로 3개 이상 동일한 문양이 연속되어 있다면 제거된다.

 2. 제거되는 문양은 각각 한 타일당 10점이다.

 3. 동시에 여러 문양의 연쇄가 발생한다면, 모두 제거되고, 동일한 콤보의 점수 (1콤보일때 10점)로 판정한다.

 4. 제거된 뒤에는 위에서부터 한 칸씩 내려온다. 빈 공간에는 새로운 문양들이 내려온다.

 5. 빈 공간이 다 채워진 뒤에도 가로 세로 3개 이상 동일한 문양이 있다면 제거되며 이후 부터는 콤보로 친다. (2콤보 - 20점)



플로우

1. 최초 시작시 9x9의 타일에 각각 랜덤한 타일을 생성한다.

2. 최초에는 전체 타일에 대해 3 매칭 판단을 해야한다.

 2-1)왼쪽부터 오른쪽으로 , 위부터 아래로 차례로 매칭 비교한다. ( 한 타일당 2번비교될듯.  총 162번)

 2-2) 각 타일별로 매칭 판별을 한다. (동서남북 - 한타일당 4번 비교)

 2-3) 2번 스타일로 하되, 동일한 크기의 bool 배열을 만들어 방문한 곳을 체크해 다시 체크하지 않도록 한다. 하나의 라인을 지나가며 주변의 자신과 같은 타일은 큐에 넣고, 매칭 라인을 돌면 큐에서 빼내며 다른 연결된 매칭라인이 있는지 확인한다.  결국 전체 타일을 돌아야한다. 2번에 비해서는 절약할 수 있으나, 1번과는 큰 차이가 없다. (오히려 더 연산이 많아 보인다.)

3. 매칭은 별도로 저장한다.

 3-1 ) 저장 방법 - 하나의 구조체가 가로세로여부, 시작 인덱스와 종료인덱스를 저장. 리스트 형식으로 해당 구조체를 저장.

3-2) - 각각의 매칭된 타일들을 모두 저장한다. 중복을 허용하지 않기위해 set (hashSet)으로 저장.

4. 저장된 매칭 리스트를 제거한다.

5. 빈 공간의 위에있는 타일들을 내려 빈 공간을 채우고, 남은 최상위의 빈 공간에 새로운 타일을 랜덤하게 생성하여 리필한다. 

 - 5-1)  아래에서 위로 탐색하여 빈공간이 나올때마다 내려가야할 칸 ++ 하여 내린다.

 - 5-2) 우리는 제거된 타일의 위치를 알고있다. 그러면 제거된 타일의 위치의 위에 타일들에 +1을 해주면 내려가야 할 칸을 알 수 있다. 이 값을 기반으로 타일을 내려준다.

 - 5-3) 1+2 형식으로 제거된 타일의 세로좌표만 1번 방식으로 진행한다. ( 2번 방식으로는 세로영역에 제거된 라인이 겹칠수록 비효율적인 연산이 늘어난다.)

    리필할 때 새로 타일을 생성하는 것에 사용할 것까지 생각하여 5-3으로 진행합니다.

6. 매칭 리스트가 0이 될때까지 2~5 반복.

 - 매칭 리스트가 0이면 매칭 할 게 더이상 없는지 확인.

- 아래 두 형태가 있는지를 확인하면 되겠네요.총 연산은 대략162*8 = 1296 정도 되겠네요.

                    


  - 매칭할게 없다면 1번으로 되돌아간다.


7. 유저 타일 터치 후 드래그 시 방향 판별을 통해 해당 방향의 타일과 swap하는 형태를 보여주고, 터치를 종료시 swap한다. ( 일정 길이 이상)

8. 유저가 타일을 변경한 경우 두 개의 타일에 대해서 4방향 매칭 판별을 진행한다.

 ( 동일한 문양의 타일일 경우 동일한 방향으로 연쇄 확인)

9. 매칭이 되지 않았을 경우 두 타일을 다시 swap 한다.

10. 매칭이 될 경우 3~5를 진행하고, 6번을 진행한다.


생각할만한 점 :

  •  2번의 알고리즘을 좀 더 개선할 방법은 없을까?  고려 - 마스크 기법
    • 2-1 방법은 총 162번의 연산을 하는데, 마스크 방식으로 하면 마스크에 따라 배수로 늘지 않을까..
  •  더이상 매칭될수 없다면? 단순하게 재배열 ㄱㄱ
  •  매칭할 수없는지 확인하는 로직을 개선할 방법은 없을까?



* 지적은 언제나 환영입니다.

Posted by 검은거북

abstract 란?

abstract 한정자의 의미는 abstract가 사용되는 곳에 누락되거나 불완전한 구현이 있다는 것을 의미합니다. 즉, 파생 클래스에서 그 불완전하거나 누락된 부분을 구현해야하는거죠. abstract 클래스의 목적은 여러 파생 클래스에서 공유할 수 있는 기본 클래스의 공통적인 정의를 제공하는 것입니다. 일반적으로는 클래스로 묶을 때, 하위 클래스가 반드시 구현해야하는 규칙을 명시하거나 상위 클래스를 통해 접근하고자 할 때 사용하는 것으로 알고있습니다.

쉽게 생각하면 골자만 만들어진 설계도 정도로 보면 되지 않을까요. 자동차를 예를 들면 차는 문이 있고 운전대와 좌석이 있어야 한다. (여기까지가 abstract) 차 종류별로 위의 규칙은 지키되 문이 위로 열릴수도 옆으로 열릴수도 있게끔 만드는 거죠. (하위 클래스의 구현)


특징

  • abstract 한정자는 클래스, 메서드, 프로퍼티, 인덱서, 이벤트와 함께 사용가능하다.
  • abstract가 사용되면 파생클래스에서 구현되어야 한다.
  • 추상 클래스 (abstract class)
    • 추상 클래스는 인스턴스화 될 수 없다.
      • 변수와 메서드 선언은 일반 클래스와 동일하게 할 수 있다. (물론 virtual도 사용가능)
    • 상속을 제한하는 sealed와는 같이 사용할 수 없다.
  • 추상 메서드 (abstract method)
    • 추상 메서드의 선언은 추상 클래스에서만 허용된다.
    • 추상 메서드는 암시적으로 가상(virtual) 함수이다. 
      • 기본적으로 동작은 virtual 함수와 같지만, virtual과 달리 abstract는 파생 클래스에서 반드시 구현해야한다.
    • 추상 메서드는 구현부가 존재하지 않는다. ex) public abstract void test();
    • 파생 클래스에서는 동일한 시그니쳐 메서드에 override 키워드를 통해 재정의한다.
    • private, static, virtual 키워드와는 사용 불가능하다.


예제


    
    public abstract class Monster
    {
        public int hp;
        public void setHp(int _hp)
        {
            hp = _hp;
        }
        public abstract void hit();
    }

    public class Orc : Monster
    {
        
        public override void hit()
        {
            Console.WriteLine("Orc hit HP: " + hp);
        }
    }

    public class Elf : Monster
    {
        public override void hit()
        {
            Console.WriteLine("Elf hit HP: " + hp);
        }
    }

Monster를 abstract 클래스로 만들고,  setHp라는 일반 메서드와 hit라는 추상 메소드를 선언했습니다.

Orc와 Elf는 Monster를 상속받아 hit를 재정의 하고 있습니다.

상속받는 클래스에서 hit를 override하지 않으면 오류가 발생합니다.

    
        static void Main(string[] args)
        {
            Orc monster1 = new Orc();
            Elf monster2 = new Elf();

            monster1.setHp(10);
            monster1.hit();
            monster2.setHp(20);
            monster2.hit();

            Monster monster3 = new Orc();
            Monster monster4 = new Elf();


            Console.WriteLine("////////////////////");
            monster3.setHp(30);
            monster3.hit();
            monster4.setHp(40);
            monster4.hit();

        }


위는 각 클래스 변수에 해당 클래스의 인스턴스를 담고, 아래는 Monster 변수에 하위 클래스 인스턴스를 담고 있습니다.

어느쪽도 abstract에서 구현된 setHp 함수가 잘 동작하고, hit역시 상속받은대로 잘 동작되고있습니다.

abstract 클래스는 자기 자신을 인스턴스로 삼을 수 없습니다.


abstract 의 활용

 이전 포스팅인 virtual에서 언급되었듯이 abstract는 여러 클래스들이 공통적으로 가져야 할 특징들을 정의해 놓는 것이죠. virtual과 크게 다른 점은 virtual은 선택형이고, abstract는 반드시 구현해야 한다는 점과 인스턴스로써 만들 수 있냐의 차이입니다. 때문에 abstract는 몬스터의 hit, attack, move 등 모든 몬스터가 반드시 가져야 할 함수를 선언하여 몬스터라는 그룹을 정의 할 때 쓰입니다.  모든 몬스터가 공통적으로 행동해야하는 함수라면 미리 구현을 하고, 모든 몬스터가 공통적으로 가지되 개별적으로 동작해야하는 함수라면 abstract로 선언하여 상속시 반드시 구현하도록 하죠. 

 예를들어 abstract를 활용해 RPG 게임의 몬스터를 만든다고 생각해보죠. 가장 상위 클래스인 Monster는 타격시 호출되는 Hit는 공통적으로 쓰기위해 구현하고, 나머지 move와 attack은 하위 클래스가 반드시 구현할 수 있도록 abstract로 만듭니다. 그리고 선공형, 비선공형 몬스터로 소분류를 만들어 Move에서 적을 쫒거나 무시하는 로직을 추가해 구현을 하고, 그 하위로 세부 몬스터인 Orc나 Slime에서 특징에 따라 Attack을 만듭니다. ( 몬스터별로 얼음을 쓰기도 독을 쓰기도 하니까요.)

  
    public abstract class Monster
    {
        public void Hit()
        {
            //Do Hit;
        }
        public abstract void Move();
        public abstract void attack();
    }
    
    public abstract class AggressiveMonster:Monster
    {
        public override void Move()
        {
            //Move Aggressive
        }
    }
    
    public class Slime:AggressiveMonster
    {
        public override void attack()
        {
            //Attack with poison
        }
    }

 이런식이지 않을까요. 이렇게 하게 몬스터를 분류별로 나누어 유지보수가 용이해지고, 다른 클래스를 작성하는 사람 입장에서는 Monster 클래스만 보고 어떤 함수를 호출해야할지 감을 잡을 수 있죠.

 * 활용 예시는 지극히 개인적인 생각입니다. 혹시 문제가 있거나 잘못된 점이 있으면 말씀부탁드립니다.


Posted by 검은거북

MSDN에서 여기저기 뒤져보다가 좋은 것을 찾았습니다. 물론 저한테 좋은거요.


https://docs.microsoft.com/ko-kr/dotnet/csharp/programming-guide/inside-a-program/coding-conventions


MS에서 말하는 C# 코딩 규칙입니다. 


저는 개인적으로 코딩 규칙에 대해서 평소에 "대체 뭐가 맞는거야!?" 하는 생각이 있었거든요. 대학교 교수님이 말한 것과 이전 회사 시니어가 말한 것과 팀장님이 말한 것 매번 저게 옳다, 그딴 식으로 하지마라를 상반되게 들어서 우왕좌왕하게 되더라고요....ㅠㅠ 

이제는 MS에서 권장하는 규칙을 알았으니 MS에서 권장하는 코딩 대로 밀고 나가야죠ㅎㅎ ( 또 뭐라 그러면 만든사람이 그렇게 하래요 라고 똭! 무려 모범사례입니다!!)


내용은...대부분이 보기 좋은 코딩을 위한 내용이더군요.. 애초에 맨 위에 용도에서도 유지보수의 용이함을 내놓기도 하고 있네요.


 당연한 내용들이 대부분이긴 한데 내용을 보면서 드는 의문점이 "var를 의외로 자주쓰고, 권장한다?" 대체 var가 동작이 어떻게 되길래? 굳이 암시적을? 입니다. 특히 오른쪽에 변수 형식이 명확하거나 정확한 형식이 중요하지 않으면 var를 사용한다고 하는데....변수 형식이 명확한데 굳이 왜 var를 쓰지 라는 생각이 드네요....;;;

 그래서 좀 더 다른 섹터의 글들을 찾아보니, var는 편리함을 위해서 사용할 뿐이고, 다른 개발자가 보기에 더 이해하기 어렵기 때문에 꼭 필요할 때만 쓰라고 하네요. 음.....;;;

  (출처 : https://docs.microsoft.com/ko-kr/dotnet/csharp/programming-guide/classes-and-structs/implicitly-typed-local-variables)


상반되는 느낌이네요....

(지역변수에서)

1, 변수 형식이 명확하면 사용한다.

2. 정확한 형식이 중요하지 않으면 사용한다.

3. 다른 개발자가 보기에 더 이해하기 어렵기 때문에 꼭 필요할 때 사용한다.


인데, 다시 읽어보니... 

 1번은 중의적으로 해석될 수 있는 변수 형식에는 쓰지 말란 소리고, 명확한 변수 형식이기 때문에 var를 써서 편리함과 코드 가독성에 이득을 취하라는 말이더군요.

 2번은 거꾸로 말하면 정확한 형식이 중요하면 사용하지 말란건데, 암시적 형식이니까 당연한 말이고... 

 그럼 3번의 꼭 필요하다는 곳은 1,2번에 포함되고 코드 가독성 향상이 필요한 곳을 말하는 걸까요?


 그래서 제 결론은 var는 지역 변수를 사용함에 변수 형식이 명확하하거나 정확한 형식이 중요하지 않은 곳에서 코드 가독성 향상이 필요하다면 사용하기를 권장한다. 라는 것 같네요...


맞나.. 혹시 다른 의견 있으시거나 잘못된 점이 있다면 지적 부탁드립니다 ㅠ 


Posted by 검은거북


 개인적으로 한 줄 평가를 하자면 자동전투에 의한, 자동전투를 위한 게임이다...

(이미지는 참 멋있는데....)

일단! 컨텐츠가 별로 없다. 하지만 모델링은 잘 나왔다. 물론 신규 게임이라는 패널티가 있지만...... 아무래도 마블의 퓨쳐파이트랑 비슷해서 비교가 되는건 어쩔 수가 없네요.



우선 컨텐츠 소개부터 하자면...

  • 이야기모드 
  • 언체인드 - 이야기모드의 코옵버전?
  • 결투 - pvp
  • 가상전투 - 무한층 컨텐츠
  • 팀 훈련 - 조합 효과를 강화
  • 또다른 이야기 - 어.......그냥 템주기 위한 콘텐츠?
  • 특별임무 - 활력 보충제를 얻기 위한 전투
  • 기지 - offline 시 템 확보를 위한 컨텐츠
이정도입니다.


 단점부터 얘기하겠습니다.
음....pvp 빼고는 다 컨트롤이 필요없습니다. 자동전투면 되죠 ㅎㅎ
그 흔한 보스전도 없어요 ㅠㅠ...  그냥 게임키면 자동전투 돌려놓으면 되요..
 컨텐츠가 부족해요.. 핵심 컨텐츠인거 같은 언체인드부터 얘기를 해보죠. 이야기 모드의 코옵버전인데.. 보상이 그렇게 크지도 않고, 코옵이라곤 해도 컨트롤은 필요없습니다. 역시 자동전투죠. 이야기모드에 비해서는 보상이 많아서 이야기 모드를 다 깨면 언체인드만 돌게 되죠. 그래서 이야기모드는 한 번 깨면 다신 볼 일도 없습니다. 그리고 이야기 모드에 랭크 시스템이 있는데 이건 존재의미가 없어요...ㅜㅜ s랭크 받아봤자 아무것도 없습니다. 네 이야기모드는 컨텐츠 여는 용도가 끝이에요.
 차라리 스토리 모드를 언체인드의 보상 정도로 바꿔서 성장을 위한 반복전투로 유도하고, 언체인드는 보스전 같은 완전히 코옵 컨텐츠로 가는게 어땠을까 싶네요... DC에도 다크사이드같은 네임드 보스가 많은데...앞으로 업데이트 되겠죠?

 또 집고 넘어가고 싶은게 또다른 이야기... 진행방식은 난이도에 따라 포인트를 주고, 일정 포인트를 모으면 보상을 받는 방식입니다. 근데 여기서 사용하는 캐릭터는 본인의 캐릭터도 아닙니다. 잉? 네 그냥 자동전투 돌려놓으란 소리죠... 이걸 넣은 의도는 아마 캐릭터 맛보기의 용도가 아닐까 생각이 듭니다. 하지만 이미 캐릭터 창에서 맛보기는 할 수 있고, 이런 캐릭터의 매력을 무기로 삼는 게임에서는 뭐가됐든 자기의 애정캐가 곧 주캐가 되니까 좀 의미없게 느껴집니다.




 단점이 많지만 장점도 있습니다. 캐릭터 퀄리티가 좋아요!! 꽤나 좋아요. 스킨도 멋져요!! 위에 사진이 플래시의 스킨입니다. (사진으론 좀 별로네요..실제론 더 멋져요!) 아직은 나온지 얼마 안되서 스킨도 캐릭도 별로 없지만 마퓨파를 넘어설 전자피규어 게임을 기대해 볼 수 있을 것 같아요! (음.... 근데 솔직히 DC의 전자 피규어라면 인저스티스가 이미 있어서....)
 그리고 한줄평에 자동전투를 위한 게임이라고 한 이유! 바로 전술 시스템입니다. 자동전투시 캐릭터가 상황에 따라 어떻게 대처할지를 토큰으로 조정할 수가 있어요. (예를 들면, 주변에 적이 5명 이상이면 동료를 호출한다, 1,2번 스킬은 연계해서 쓴다.) 이거는 다른 게임에서는 시도하지 않은 신박한 생각 같더라고요. 어정쩡하게 컨트롤 넣지 말고 아예 자동전투에 모든걸 쏟아붇자는 느낌?

간단하게 정리해보겠습니다.

개인적인 평가 : 6.0 / 10.0 

장점 

  •  캐릭터 퀄리티가 좋다
  •  전술 시스템
  •  만화책 느낌이 물


단점

  •  특징없는 컨텐츠
  •  별로 없는 이벤트 
  •  일부 의미를 모르겠는 컨텐츠 (랭크 시스템)

추천 : 컨트롤은 하기 싫고 그냥 DC의 캐릭터를 키우고 싶다. DC 캐릭터용 전자피규어를 가지고 싶다.


Posted by 검은거북

virtual 함수란?

 부모 클래스에서 virtual 키워드를 사용하여 함수를 만들면, 자식 클래스에서 이 함수를 재정의 할 수 있도록 허용하겠다는 의미입니다.


 특징

 - virtual 이 붙은 함수는 자식 클래스에서 재정의가 가능합니다.

 - 자식 클래스에서는 new 또는 override 키워드가 사용가능하다.

    - override는 재정의를 하겠다는 확장의 의미이고, new 는 기본 클래스를 숨기는 의미이다. 

 - 자식클래스의 함수 시그니쳐가 동일해야 재정의가 가능하다.

 - 자식클래스의 함수는 base 키워드를 사용해 부모 클래스의 함수를 호출 할 수 있습니다.

 - abstract 와는 달리 자식클래스에서 구현은 선택이다. (구현 안하면 부모의 함수 사용)

 - static, abstract, private, override 키워드와는 사용이 불가능하다.

예제

    public class Monster
    {
        public virtual void hit()
        {
            Console.WriteLine("Monster hit");
        }
    }

    public class Orc : Monster
    {
        public override void hit()
        {
            Console.WriteLine("Orc hit");
        }
    }

    public class Elf : Monster
    {
        public new void hit()
        {
            Console.WriteLine("Elf hit");
        }
    }

    public class Wolf : Monster
    {
        public void hit()
        {
            Console.WriteLine("Wolf hit");
        }
    }

위에서 Monster 클래스는 hit 함수에 virtual 키워드를 지정해 재정의를 허용했습니다.

Orc는 override를, Elf는 new 키워드를, Wolf는 별도 지정없이 동일한 시그니쳐의 함수를 작성했습니다.


이제 사용을 해보죠.

   class Program
    {
        static void Main(string[] args)
        {
            Monster monster1 = new Monster();
            Orc monster2 = new Orc();
            Elf monster3 = new Elf();
            Wolf monster4 = new Wolf();

            monster1.hit();
            monster2.hit();
            monster3.hit();
            monster4.hit();

            Monster monster5 = new Orc();
            Monster monster6 = new Elf();
            Monster monster7 = new Wolf();


            Console.WriteLine("////////////////////");
            monster5.hit();
            monster6.hit();
            monster7.hit();

        }
    }

monster 1~4 는 각 클래스별로 몬스터를 만들고,  monster 5~7은 Monster라는 변수에 각 하위 클래스의 인스턴스를 생성해 담아서 출력시켰습니다.


결과는

monster 1~4 는 그대로 잘 출력되네요. 사실 각 클래스에 자기 자신을 인스턴스로 담으면 virtual의 의미가 전혀 없습니다. ( 이렇게 쓸거면 재정의가 필요없죠)

하지만 monster 5~7은 결과가 다르죠.

ovrride를 한 Orc만 제대로 재정의되어 자신의 hit를 출력하고 있고, 나머지는 상위 클래스의 hit를 출력합니다.

여기서 new 키워드의 기본 클래스를 숨긴다는 의미도 알 수 있죠. 상위 클래스 변수에 담기면 하위 클래스가 아닌 상위 클래스의 함수를 호출한다는 것입니다. ( new 키워드를 사용하지 않은 경우, 즉 Wolf의 경우에는 new와 동일한 동작을 합니다.)


그럼 만약에 상위 클래스가 virtual 키워드를 쓰지 않았다면?

virtual 함수를 쓰지않으면 하위 클래스에서는 override를 쓰지 못합니다. 당연히 monster 6~7과 동일하게 동작합니다. 


결국 virtual과 override를 사용하여 재정의하는 이유는 상위 클래스 변수에 하위 클래스 인스턴스를 담을 때, 하위 클래스의 함수를 호출하고 싶기 때문이죠.

(Monster monster5 = new Orc();)



virtual 함수의 활용


 그럼 이런 재정의는 언제 활용을 하는걸까요

 상위 클래스 변수에 하위 클래스 인스턴스를 담아야 할 경우는 언제일까요

 위의 hit 함수를 생각해보죠.

 모든 몬스터가 Monster라는 하나의 클래스로 만들어지면 좋겠지만, 각자의 특징에 따라 Monster 하위 클래스가 만들어질 수 있습니다. 
그리고 Player가 몬스터를 때렸을때, 해당 몬스터의 hit를 호출한다고 칩시다.

virtual 함수를 쓰지 않는다면 어떻게 될까요? 
       void attack(Orc monster)
        {
            monster.hit();
        }
        void attack(Elf monster)
        {
            monster.hit();
        }
        void attack(Wolf monster)
        {
            monster.hit();
        }
 헉... 모든 해당 몬스터 즉, orc, elf, wolf 별로 hit 함수를 오버로딩하여 별도로 호출해야 합니다. 
 왜냐하면... virtual을 사용하지 않으면 저렇게 해야만 해당 몬스터의 hit를 호출할 수 있으니까요. ( 사용하는 입장에서 Orc가 hit를 override를 했을지, 안했을지 모르잖아요?)

만약 virtual 함수를 사용하고, 다른 하위 클래스에서 hit함수를 override했다면?
        void attack(Monster monster)
        {
            monster.hit();
        }

이거 하나면 됩니다.


monster.hit() 가 호출될 때 Monster 변수에 담긴 인스턴트별로 재정의 되어있는 hit 함수를 호출해 줄테니까요.

몬스터 종류가 무수히 많아졌을 때, 마냥 몬스터별로 함수를 오버로딩해서 작성할 수는 없습니다. 반드시 virtual이 필요해지죠.



* 사실 위와 같은 방식은 abstract 또는 interface로도 가능은 하죠. 

 하지만 abstract, interface와는 달리 virtual은 선택적으로 재정의가 가능하다는 점virtual 함수를 가진 상위 클래스도 인스턴스 생성이 가능하다는 점이 있습니다.

 솔직히 위의 Hit함수만 보면 어차피 모든 클래스가 구현을 해야한다면 abstract로 만드는게 맞지 않냐고 할 수 있습니다. Hit만 있다면 맞는 말이죠. 그리고 Monster라는 하나의 그룹을 정의하는 거라면 abstract가 맞습니다. 하지만 Orc 중에서 엘리트 Orc는 Hit시 반격을 하고 싶다면?모든 Orc의 함수는 동일하게 상속받아 행동하고, Move만 재정의 해야 한다면, virtual이 필요해지게 되죠. Orc도 하나의 인스턴스로써 동작해야하고, 엘리트 Orc는 hit를 재정의해서 만들어져야 하니까요. 즉, 특정 개체로부터 특정 함수만을 선택적으로 재정의하고, 그 외에는 그대로 상속 해야할 경우인거죠.

 결국 각자의 특징에 알맞게 사용해야겠죠. 예를 들어 abstract는 하위 클래스가 반드시 구현해야하는 것을 명시하거나 하나로 묶을 때, interface는 class에 한정되지 않고, 범용적으로 사용할 때 또는 디자인 정의가 필요할 때, virtual은 일부 함수에 대해 선택적으로 재정의가 필요할 때.

 






Posted by 검은거북


몇 달째 꾸준히 하고 있는 액트파이브의 열혈강호 M.


 일단 게임의 메인 장르는 던전앤파이터와 같은 2D 횡스크롤 액션 게임입니다. 

원래 2D 횡스크롤 게임 자체는 잘 하지 않는 편인데, 우리나라에서 열강이 많이 유명하다보니 호기심에 한 번 접해봤다가... 지금 한 2달 넘게 하면서 레벨 62까지 키우게 되었네요.



 처음 접했을 때의 느낌은 국산게임임에도 불구하고 왠지 중국스멜이 느껴졌었습니다. 아무래도 만화가 무협장르라 그런 것도 있고, 중국 게임 특유의 특징들이 좀 보이기도 했거든요. 예를 들어 쓸데없이 큰 데이터나 데미지들, VIP 시스템, 컨텐츠 순차오픈, 많은 이벤트. (지극히 주관적인 생각입니다)

 전 개인적으로 중국 게임 좋아하는 편입니다 ㅎㅎ 큰 데미지는 시원시원한 느낌을 주고, 다른 시스템도 나쁠게 없죠.. 무과금러 입장에서 VIP 시스템은 차별을 받긴하지만, 현질러와 엄연히 다르게 대우하는 건 어쩔 수 없다고 생각합니다. 그러다보니 열강에 빠져든게 아닌가싶네요. 특히 열혈강호 M은 스토리를 따라가다보면 만화책 컷씬을 통해 스토리를 이해할 수 있게 해주기때문에 그 부분도 몰입도를 높이는데 한 몫 했다고 생각이 듭니다. 그 무엇보다 전투력을 야금야금 키우는 맛이 가장 좋은 것 같네요. (현재는 어느새 9900만으로 곧 1억이 눈 앞에...)



 

 게임의 특징은 아무래도 다양한 컨텐츠겠네요. 우선 컨텐츠들이 확실하게 제 역할을 가지고 있다는 느낌이 들어서 좋습니다. 

 주요 서브 컨텐츠로는 

  • 무림외전 ( 동료들을 이용한 턴제 전투)
  • 등천각 (층을 올라가는 도전 컨텐츠)
  • 신수 대전 ( 다른 유저들과 협동하여 보스(신수)를 잡는 컨텐츠)
  • 결투대회( pvp - 유저간 실시간 대전입니다. 전투력 100만이 차이나는 유저한테 진적도 있죠...)
  • 무림쟁패 (동료 pvp - 위와는 반대로 이건 선빵필승입니다.ㅎㅎ)
  • 등등...

 게임 내의 재화나 재료는 각 컨텐츠들에서 별도로 얻을 수 있습니다. 때문에 위에서 각 컨텐츠의 역할을 가지고 있다고 말씀드린 겁니다. (비무대회 빼고... 비무대회는 비무첩의 상위 컨텐츠로 둘이 역할이 많이 겹칩니다.) 


 

 게다가 동료들을 키우고, 동료들을 통해 무림외전을 플레이해야해서 턴제 게임의 성격도 가지고 있죠. 전 원래 턴제를 좋아하다보니 처음 할때는 무림외전에 더 신경을 썼었죠... 동료를 키우면 주인공이 강해지고, 주인공을 키우면 동료 진형이 강해지는 식으로 어느쪽이든 강해지고 싶으면 포기할 수 없게 만든 것도 꽤 좋은 컨텐츠 소비인거 같습니다. 대체로 모든 컨텐츠가 강해지고 싶으면 다 해야되도록 유도하고 있죠.  

 특히 요즘에는 환영지림이나 동료 업데이트를 통해 동료쪽에 신경을 많이 쓰는 것 같더라고요. 업데이트나 이벤트도 자주하는 편이고, 턴제나 수집요소를 좋아하는 저로써는 할 맛이 나서 계속 붙잡게 되더군요.. 


 끊임없는 이벤트도 하나의 큰 장점이겠네요. 컨텐츠 소비를 위한 이벤트가 매달 이름만 바껴서 나오고 있죠. 유저 입장에서는 계속 재화를 얻을 수 있는 좋은 서비스입니다.



 그리고 하나더 특징이자 장점을 뽑자면 플레이블 캐릭터가 3명입니다. 언제든 맘에 드는 캐릭터를 선택해 키울 수 있고, 3명 다 키운다면 업적을 통해 각 캐릭터별로 유료재화를 수급할 수 있죠...ㅎㅎ





 사실 저는 이 게임에 대해 단점이 잘 느껴지진 않습니다. 그만큼 잘 만든 게임이라고 생각해요. 하지만 단점을 굳이 뽑자면, 문파와 의상 시스템을 들 수 있겠네요.

 문파는 문파 토큰을 모으고 이를 통해 아이템을 살 수 있단건 알겠는데... 문파원들과 뭔가 한다는 개념도 별로 느껴지지 않고, 사실 문파 없어도 지장없는 듯해요. (엇! 장점이 될 수도 있겠네요)

 의상 시스템은....아직 부가 요소들을 개발중인거 같아요. 현재로썬 사실 캐릭터 꾸미기 + 소소한 능력치 업 정도밖에 되질 않아요. 굳이 비싼 유료재화 들여가면서 뽑고 싶진 않더라고요.


 그리고 엔드 컨텐츠가 부족하다고 생각하긴 하는데, 사실 이건 열강에만 국한되진 않는 문제인 듯하네요... 저는 스테이지 깨는데 목적을 두다보니 중천 난이도가 없을 때 ,극악까지 다 깨니까 "다깼네...지울까" 했거든요. 하지만 pvp를 많이 즐기시는 분이라면 얘기가 다르겠네요. pvp에서 1인자가 되겠어 하면서 열심히 반복전투 눌러서 강해지려 하실 수도 있으니.

 


개인 평점 : 8.6 / 10.0


추천 

  • 열혈강호를 좋아하는 분 또는 입문하고 싶은 분
  • 2D 횡스크롤 액션을 좋아하시는 분.
  • 조합 같은 복잡한거 싫다하시는 분 ( 동료시스템은 강한 동료가 선빵때리면 장땡입니다. )
  • 캐릭터 수집을 좋아하시는 분.


Posted by 검은거북

제목에는 정렬계의 노답 삼형제라고 적었지만, 다 쓸모가 있기에 존재하겠죠... (라고 말했지만 노답 삼형제 맞는거 같은데, 쓰기 쉽단거 빼면...)


1. 버블 정렬

정렬되는게 거품이 보글보글 올라오는 것과 같아서 버블 정렬이라고 합니다. 정렬해라 했을 때 가장 직관적으로 생각하기 쉬운 알고리즘이죠. 쉬운만큼 비효율적이기도 하고요.

 동작방식은 집합 내의 이웃 요소들끼리 비교하여 교환을 통해 정렬됩니다. 쉽게 말해 키순으로 정렬했을 때 내 앞에 있는 애가 나보다 키가 작으면 자리를 바꿔가며 키가 가장 큰 애를 맨 앞까지 오게 하는 거죠.


동작 예시 :

 백마디 말보다 예를 들어 보죠.

 자 밑에 3,2,4,1 의 정렬되지 않은 숫자가 있습니다.

버블정렬을 통해 오름차순으로 정렬해보죠. (뒤에 나올 정렬들도 동일한 수로 하겠습니다.)

빨간 네모는 비교를 의미합니다.


[1싸이클]

3을 기준으로 이웃한 2와 비교하여 3이 더 크므로 교환.


3을 기준으로 이웃한 4와 비교하여 4가 더 크므로 교환되지 않습니다.


1싸이클의 마지막으로 4와 이웃한 1을 비교하여 맨 마지막 요소에 4가 확정됩니다.

1싸이클을 통해 마지막은 4가 확정됬으므로 다음 싸이클부터는 4는 제외해도 되겠네요.



[2싸이클]

이미 이웃한 3이 더 크므로 교환하지 않습니다.

3이 이웃한 1보다 크므로 교환.

2싸이믈을 마지막을 확정된 3 역시 이후 부터는 제외됩니다.


[3싸이클]

마지막으로 2와 1을 비교하여 정렬을 마무리합니다.

총 3싸이클이 돌고, 비교연산은 6번, 교환은 4번 있었네요. 

최악의 경우 즉, 만약 역으로 정렬되어있었다면, 비교는 언제나 6번이고, 교환은 비교연산 할 때마다 일어나겠죠 (6번)



장점 : 

만들기 쉽다...

단점 :

비교 연산도 교환 연산도 많다.



시간복잡도 : O(n^2)


활용:

언제나 중요한 건 "그래서 이걸 어따 써먹을수 있는데?" 라고 생각합니다. 이게 곧 배우는 이유니까요.

 버블 정렬은 비효율의 대명사죠...

실제로 대학교에서 버블정렬을 접할 때, 이걸 배우는 이유는 비효율을 알아야 효율을 알 수 있기때문에 배우는거라고도 교수님이 그러셨죠.



2. 선택 정렬

 선택 정렬은 정렬되지 않은 집합 내 요소 중 가장 크거나 작은 것을 선택해 차례로 맨 앞이나 뒤로 보내 정렬하는 알고리즘입니다.

  쉽게 생각해서 포커나 고스톱을 칠 때, 카드를 다 받고나면 뒤죽박죽 섞여있잖아요. 그 때 카드들을 작은 것부터 뽑아서 앞으로 옮기면서 보기 좋게 바꾸죠. (버블 정렬처럼 옆으로 차례차례 옮기진 안잖아요?) 그런 방식을 생각하면 됩니다.


동작 예시:

빨간 네모는 최소값으로 선택된 요소입니다.


[1싸이클]

각 요소들의 최소값을 판별하면 1이 최소값인걸 알 수 있죠.

선택된 최소값인 1을 맨 앞으로 이동시킵니다.


[2싸이클]


[3싸이클]


3싸이클에 비교연산은 총 6번, 교환은 2번 일어났네요.

최악의 경우엔, 비교는 동일하고 (매번 동일하겠죠), 교환은 3번 일어날겁니다.


장점 :

버블에 비해 교환 연산이 적다. (최대 n번)

단점 :

안정성을 보장하지 않는다.

(같은 값이 있을 때 상대적으로 같은 위치에 있을거라 보장하지 못한다. 즉 정렬 후에는 순서가 뒤바껴 있을 수 있다.)

비교연산이 많다. (매번 최소값을 구해야하므로)

위와 같은 이유로 집합이 클수록 속도가 크게 느려진다.


시간복잡도 : O(n^2)


활용:

집합이 작은 정렬  + 안정성이 보장될 필요 없는 경우 



3. 삽입 정렬

 삽입 정렬은 정렬이 필요한 요소를 뽑아 적당한 곳에 삽입하는 정렬입니다. 적당히라는 말이 들어가있네요. 가장 어려운 말이죠.... 좀 더 풀어 설명하죠. 정렬되지 않은 요소를 순서대로 뽑아 이미 정렬되어 있는 집합 내 올바른 위치에 삽입하는 정렬입니다. 그렇기 때문에 우선 맨 앞의 요소를 정렬된 집합으로 취급하여 두 번째 요소부터 비교가 진행되죠. 

 바닥에 흩어진 트럼프 카드를 주우며 정렬하고자 할 때, 이 방법이 쓰이겠네요. 한 장씩 주우면서 이미 정렬되어 내 손에 쥐어진 카드에 올바른 위치에 넣는 식으로 정렬을 하겠죠.


동작 예시:

빨간 네모는 비교대상

빨간 선은 비교입니다.


[1싸이클]

3은 이미 정렬된 집합으로 판단하고, 두 번째 요소인 2와 비교해 정렬된 집합 내에 맨 앞으로 이동됩니다.


[2싸이클]

4를 이미 정렬된 집합의 맨 마지막과 먼저 비교하여 4보다 큰 수가 없으므로 맨 뒤에 정렬됩니다.  


[3싸이클]

1을 이미 정렬된 집합의 맨 앞의 2와 비교했을 때. 1일 작으므로 바로 맨 앞으로 이동시킨다.

이로써 싸이클 종료.

3싸이클에 비교연산은 총 3번, 교환은 2번 일어났네요.

최악의 경우엔? 4,1,2,3가 될거 같네요. 비교 6번에 교환은 3번 일어납니다.


장점 :

교환 연산이 비교적 적다. (최대 n번)

이미 일부 정렬된 상태라면 비교연산이 줄어 속도가 빠르다.

단점 :

정렬상태에 따라 속도가 민감하다.


시간복잡도 : O(n^2)


활용:

이미 일정부분 정렬되어 있는 집합.

정렬된 요소에 새 요소의 추가가 빈번할 때.



*항상 선택정렬하고, 삽입정렬하고 헷갈린다... 선택정렬은 최소값이나 최대값을 선택하여 정렬하고, 삽입정렬은 앞에서부터 순서대로 삽입하면서 정렬한다라고는 생각하지만... 아마 한 달 뒤면 또 헷갈리겠지... (삽입과 선택이라는 단어가 바꾸고자 하면 충분히 바꿀 수가 있는지라....)

Posted by 검은거북

처음 유니티의 C#을 접했을 때 가장 궁금했던 점이 델리게이트의 존재 이유였다.

델리게이트는 콜백함수로 사용할 때 주로 사용되는 대리자 변수 개념인데, 이전에 JAVA를 주로 사용하던 저로써는 인터페이스로 콜백함수를 구현 할 수 있는데 왜 델리게이트가 또 필요하지 싶었다.


뭐 그런거를 궁금해하냐 할 수도 있는데.. 저같은 경우 뭐든 반드시 그게 필요한 경우가 있기때문에 존재한다고 생각하는 편이라 의문을 가지게 되었다.


혹시 저와 비슷한 생각을 가진 사람들에게 조금이라도 도움이 됐으면하는 생각에 여기저기 조사하고, 사용해보며 느낀 것을 좀 적고자 한다.


우선 두 개념부터 간단하게 살펴보면


1. 인터페이스

클래스의 형태를 디자인 / 정의하는 것이죠

다시말하면 "이 인터페이스를 구현하는 클래스는 이 함수를 반드시 가져야해" 하고 껍데기를 정의하고 있는거죠.


주요특징은

기본적으로 메소드 정의를 하면 public 권한에 추상이기 때문에 인터페이스내에서 구현을 할 순 없고, 때문에 인터페이스 자체로 변수를 생성해내진 못합니다 (new) 

그리고 인터페이스는 클래스와는 달리 다중 상속이 가능하죠.


기능상으로는 껍데기를 정의하는 기능밖에 없지만 실제 활용면에서는 범용적으로 함수를 호출하기에 굉장히 좋죠. 왜냐! 이 인터페이스를 구현하고있는 클래스는 무조건 이 함수를 가지고 있다는 것을 알기때문이죠.

그래서 호출부 입장에서는 실제 내부는 어떻게 구현했는지 몰라도 해당 인터페이스의 함수를 필요에 의해 호출해 사용하죠.

게다가 클래스나 추상클래스와는 달리 다중 상속이 가능하기 때문에 필요한 함수가 속해있는 인터페이스를 필요에따라 여러개 가져다 쓸 수 있죠.


이런 점을 이용해서 JAVA에서는 주로 콜백함수로도 많이 활용하죠.

아래부터는 예전에 DB 콜백함수를 구현할 때 사용한 코드 일부입니다. (언어는 C#인데....)

public interface DB_callback
{
   void callback_result(string str);
}

위와 같이 DB_callback 인터페이스를 생성해놓고, DB 콜백이 필요한 클래스에서는 해당 인터페이스를 구현하는 방식으로 만들었죠.

open_db 함수에서 send_query 함수에 자기자신을 매개변수로 보내고 있습니다.

  public class MatchQuizManager : MatchManager, DB_callback
  {
         void open_db()
         {
               // 유저 퀴즈 씬.
               AmazonDBManager db_manager = new AmazonDBManager();
               Dictionary<string, string> fields = new Dictionary<string, string>();
               fields.Add("type", "0");
               fields.Add("db", "0");
               fields.Add("resultType", "1");
               fields.Add("quiz_index", index.ToString());
               fields.Add("sub", "0");
               StartCoroutine(db_manager.send_query(fields,this)); // DB 연결시 자기자신 전달
         }
         //콜백함수
         public void callback_result(string str)
        {
               Quiz quiz = Split_quiz(str);
               Split_index(quiz.quiz_question);
               phase = Phase.answer;
               split_answer_index(quiz.quiz_answer);
               subject_text.text = quiz.quiz_subject;
               explain_text.text = quiz.quiz_content;
         }
  }


그리고 실제 DB 클래스에서는 DB_callback 인터페이스를 전달받아 DB_callback의 함수를 호출하기만 하면 실제 호출하고 있는 클래스의 DB 콜백함수들을 호출하게 되죠.

아래 소스에서 전달받은 callback의 callback_result를 호출하고 있죠.

public class AmazonDBManager : MonoBehaviour { public IEnumerator send_query(Dictionary<string,string> fields,DB_callback callback) { WWWForm form = new WWWForm(); foreach(KeyValuePair<string,string> field in fields) { form.AddField(field.Key, field.Value); } WWW webRequest = new WWW("서버경로", form); yield return webRequest; if (webRequest.isDone) { if (callback != null) { callback.callback_result(webRequest.text); //인터페이스의 콜백함수 호출 } } else { Debug.Log("webRequest error"); } } }

*콜백 외에는 별도로 설명하지 않겠습니다.    


과정을 요약하면

1) callback 함수를 가진 DB_callback 인터페이스 생성

2) DB를 사용할 클래스에서 DB_callback 인터페이스의 callback 함수를 구현 (인터페이스에서 제공하는 함수만 구현)

3) DB를 사용할 클래스에서 DB 연결 클래스로 DB 연결 클래스에 자기 자신을 넘김.

3) DB를 연결하는 클래스에서 DB_callback 인터페이스를 전달받아 callback 함수 호출.


2. 델리게이트

C#의 델리게이트는 말그대로 대리자 개념으로, 반환형과 매개변수가 동일한 함수를 등록해놓으면 대신 함수를 호출해주는 변수이다.

단순하게 생각하면 실행해야될 함수를 저장해놓는다? 라고 생각하면 될 것 같다.


변수이기 때문에 우선 반환형과 매개변수를 정의해야한다.

그리고 해당 델리게이트 타입으로 변수를 생성해서 변수에 함수를 참조시키면 연결 끝!

  public class AmazonDBManager : MonoBehaviour {
        public delegate void DBFunc(string str);    //타입 선언 (반환형 void, 매개변수 string)
        //DBFunc amazonDBFunc        // 일반적인 델리게이트 변수 선언
        // 델리게이트를 매개변수로 받는다. (함수를 전달)
        public IEnumerator send_query(Dictionary<string,string> fields,DBFunc amazonDBFunc)
        {
              //중복 생략
             if (webRequest.isDone)
             {
                    if (callback != null)
                    {
                          amazonDBFunc(webRequest.text);      //전달된 콜백 함수 호출
                    }
             }
        }
  }


아래는 위의 MatchQuizManager의 함수들을 델리게이트 형식으로 변경했을 때.

   void open_db()
  {
       //중복 생략
       StartCoroutine(db_manager.send_query(fields,callback_result));        // 함수를 전달

       // 필요에 따라 다른 함수를 콜백함수로 전달.
       //StartCoroutine(db_manager.send_query(fields,callback_result_another));
  }
  //콜백함수
  public void callback_result(string str)
  {
        //중복 생략
  }
  public void callback_result_another(string str)
  {
        //다른 일
  }


변수타입이기 때문에 델리게이트를 매개변수로 전달받는 함수를 만들어놓으면 동일한 시그니쳐를 같는 함수를 전달 할 수 있고, 여러 함수를 하나의 델리게이트 변수에 연결하는 델리게이트 체인도 가능하다.

ex)

amazonDBFunc += callback_result_select

amazonDBFunc += callback_reault_insert

amazonDBFunc(str) //실행시 체인된 함수들을 모두 실행시킨다.


딱 기능만 봐도 콜백함수에 특화되어 있는게 느껴진다.

그럼 마찬가지로 과정을 요약하면

1) callback 받을 함수 형태를 delegate 변수로 선언

2) DB를 사용할 클래스에서 1)의 델리게이트와 동일한 형태로 콜백함수 작성 (콜백함수를 여러개 작성 가능)

3) DB를 사용할 클래스에서 콜백함수를 DB 연결 클래스에 전달

4) DB 연결 클래스에서 델리게이트 변수를 실행.


음....전체적인 사용방법과 2)번의 과정에서 큰 차이가 보이네요.


개념은 전혀 다른데요?

네, 개념은 다르지만 역할면에서 공통분모가 있습니다. 둘 다 콜백함수에 사용되죠...

JAVA에서 인터페이스를 콜백함수를 위해서 자주 사용하다보니 그것에 길들여져 델리게이트의 필요성을 잘 못느꼈죠.


개인적인 결론
델리게이트를 여러번 사용해보면서 이제와서 생각해보면 콜백의 역할을 수행하는건 델리게이트가 맞는거 같다고 생각한다. JAVA에는 저런게 없어서 인터페이스를 사용해서 매꾼걸까...


특히 델리게이트의 체인과 함수를 넘길 수 있다는 점이 굉장히 좋은 점인 것 같다.

인터페이스를 이용해서 콜백함수를 만들게 되면 하나의 클래스에는 하나의 콜백함수만 존재하게 되고, 호출부에서는 하나의 함수만을 호출한다. 하지만 델리게이트처럼 필요에 따라 함수를 넘기는 식으로 하면 하나의 클래스에서도 다양한 함수를 넘길 수 있다. 콜백함수에 특화되어 있는 느낌?

예를 들어) A 클래스에서 DB를 통해 하고자 하는 행동이 DB에서 Select해서 얻은 걸 출력하는 것과 Update한 결과 행 수를 받는 것 두 가지라고 했을 때, 

 - 인터페이스를 사용하면 인터페이스에 정의된 함수에 모든걸 작성해야 합니다. 하나의 함수내에서 select 결과와 update를 분기처리해서 처리하는 것이죠. 설령 클래스에서 하고자하는 행동이 3~4가지가 되어도 하나의 함수내에서 해결을 해야하죠. (물론 여러 콜백을 만들어도 되지만 이러면 다른 구현클래스도 통일해줘야하여 쓸모없는 코드가 생긴다.)

 - 델리게이트를 쓰게되면 select 용 함수, update용 함수를 별도로 작성하여 필요에 맞춰 적절하게 넘겨주면 되죠. DB에 접근하여 하는 전혀 다른 행동이 10개면 10가지의 함수를 반환형과 매개변수만 동일하게 갖추고 DB에 연결할 때 필요한 함수를 전달하면 됩니다.


개인적으로 이런 범용성과 편리성이 델리게이트의 존재 이유가 아닐까 생각합니다. 
(혹시 다른 의견이 있으신 분이 계시다면 댓글 부탁드립니다!)


*가르치기 위한 글보다는 공부하며 생각을 정리하며 쓰는 글입니다. 잘못된 정보가 있을 수도 있으며, 혹시 잘못된 부분을 발견하거나 의아한 점이 있을시 피드백을 주시면 큰 힘이 됩니다!


Posted by 검은거북
이전버튼 1 2 3 4 5 이전버튼

블로그 이미지
프로그래밍 공부 요약 및 공부하며 발생한 궁금증과 해결과정을 포스팅합니다.
검은거북

공지사항

Yesterday
Today
Total

달력

 « |  » 2025.1
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31

최근에 올라온 글

최근에 달린 댓글

글 보관함