본문 바로가기

Dev-Log

프로젝트 MC #02 - 말 이동 가능하게 구조 만들기

 

보드를 만든 다음 차례는, 말들을 움직일 수 있게 해주는 것이었습니다. ♟

 


 

구조 만들기

일단 게임에 필요한 큰 요소들을 3가지로 잡았습니다.
전체적인 흐름을 처리해주는 보드매니저, 보드의 한 타일, 그리고 말.

처음에는 매니저가 보드 타일을 들고있고, 타일은 말을 들고있는 순으로 상하가 명확한 구조로 생각을 했었습니다. 보드 매니저에서 말의 모든 움직임도 모두 처리해주고, 타일과 말은 각각의 속성값들과 자잘한 메소드만 들고 있는 구조로요.

2d 게임이었다면 위와 같은 구조에서, 타일 콜라이더만을 통해 터치를 처리해주면 상호작용에 문제가 없었겠지만 (모든 조작이 타일 터치를 통해서 처리, 말 선택 또한 해당 말이 위치한 타일을 터치하는 것으로 처리되도록) 3d 였기 때문에 말의 메시를 터치해서 조작할 수 있게끔 하고 싶었습니다.

따라서 말과 타일 모두 터치가 가능하도록 콜라이더가 적용되어 두 오브젝트를 왔다갔다 하며 상호작용 할 수 있도록 처리해야 했고, 열심히 고민한 끝에 아래 이미지와 같이 보드 매니저는 타일과 말 모두를 참조하고 있고, 타일과 말은 서로를 참조하는 구조로 만들었습니다.

 

 

보드 매니저는 콜라이더 터치 발생 시, 현재 상태가 말 선택 단계인지 타일 선택 단계인지를 구분해서 처리해주고, 보드 생성 및 말 위치 세팅과 같은 초기 세팅을 처리하도록 했습니다.

보드 타일은 간단하게 타일 좌표와 위치한 말을 들고있고, 이동 경로인 타일을 강조해주는 연출 처리용 함수를 가지고 있습니다.

말은 진영, 타입 그리고 현재 위치한 타일 정보를 가지고 있고, 이동 경로를 계산해줌과 더불어 현재 타일에서 이동 가능한 타일로 움직이는 실제 이동도 처리합니다.

 

처음 구조를 짰을 때엔 보드 매니저에서 말의 이동 로직과 이동 모두를 처리하는 것으로 생각했는데, 체스 말 별로 다른 이동 로직을 모두 보드 매니저에서 처리하려니 코드가 너무 길어져 보기도 불편하고 구조도 조금 이상한 것 같아 이동 관련은 모두 말에서 처리하도록 수정했습니다.

이 과정에서, 보드매니저를 말에서 편하게 사용할 수 있게 싱글톤 패턴으로 처리했습니다.
(보드매니저를 모든 말에 링크해줄까 생각했었는데... 따스한 조언을 주신 클라님 감사드립니다.)

 


 

체스 말 클래스 및 이동 로직

말로 이동 관련 처리를 옮기니 뭔가 객체지향적인(?) 느낌적인 느낌의 구조가 되어가는 것 같아, 형용키 어려운 뿌듯함과 함께 코드의 작성 방향을 전보다 수월하게 결정해나갔습니다.

작업한 이동 구조의 흐름은 아래와 같습니다.

 

 

룩, 비숍, 퀸, 킹은 모두 십자선+대각선 방향으로 직선 이동하는데, 이동 경로 계산 작업에선 이런 체스 말이 이동하는 규칙이 나름 비슷하다는 것에 착안했습니다. 이런 말들에서 공통으로 사용할 수 있는 경로 계산 함수를 만들고, 나이트와 같이 특수한 경우에 사용할 수 있는 로직을 별도로 처리하기로 했습니다.

ChessPiece 라는 부모 클래스를 상속받는 자식 클래스를 ChessPiece_Pawn 과 같이 말의 유형 별로 만들고, 부모 클래스의 함수를 사용해서 이동 로직을 말의 유형에 맞추어 처리했습니다.

 

public class ChessPiece : MonoBehaviour
{
    // prefab 단에서 설정
    [Header("Piece Initial Option")]
    public Constants.PieceType type;

    [Space]
    [SerializeField] private Constants.Team _team;
    public Constants.Team team { get { return _team; } }
    public BoardTile currentTile;

    // Moving Direction Enum
    public enum Direction
    {
        NEGATIVE = -1,
        ZERO = 0,
        POSITIVE = 1
    }

    // 말 배치
    public void Set(BoardTile setTile)
    {
        currentTile = setTile;
        currentTile.pieceOnTile = this;

        this.gameObject.transform.localPosition = new Vector3(setTile.transform.localPosition.x, 0.5f,
        setTile.transform.localPosition.z);
    }
    
    // 말 이동
    public void Move(BoardTile targetTile)
    {
        // Attack
        if (targetTile.isPieceOnTile)
        {
            targetTile.pieceOnTile.Death();
        }

        // Move
        currentTile.pieceOnTile = null;

        targetTile.pieceOnTile = this;
        this.gameObject.transform.localPosition = new Vector3(targetTile.transform.localPosition.x, 0.5f,
        targetTile.transform.localPosition.z);

        currentTile = targetTile;
    }

    // 말 사망 처리
    public void Death()
    {
        currentTile.pieceOnTile = null;

        if (type == Constants.PieceType.KING)
        {
            // End Game
        }

        Destroy(this.gameObject);
    }

    public virtual void CheckPath()
    {
        // 하위 말에서 상속받아 override 해 사용
    }

    // 일반적인 경로 계산
    public void CheckGeneralPath(Vector2Int boardCoord, Direction dirX, Direction dirY, int moveCount = 1,
    bool isPawn = false)
    {
        int count = 1;

        while(true)
        {
            boardCoord.x += 1 * (int) dirX;
            boardCoord.y += 1 * (int) dirY;
            
            if(boardCoord.x < 0 || boardCoord.x >= BM.boardManager.size.x || boardCoord.y < 0
            || boardCoord.y >= BM.boardManager.size.y || count > moveCount)
            {
                break;
            }

            if(BM.boardManager.board[boardCoord.x, boardCoord.y].isPieceOnTile)
            {
                if(BM.boardManager.board[boardCoord.x, boardCoord.y].pieceOnTile.team != team && !isPawn)
                {
                    BM.boardManager.moveableArea.Add(BM.boardManager.board[boardCoord.x, boardCoord.y]);
                }

                break;
            }
            else
            {
                BM.boardManager.moveableArea.Add(BM.boardManager.board[boardCoord.x, boardCoord.y]);
            }

            count++;
        }
    }

    // 한 타일이 이동 가능한 타일인지 확인
    public void CheckTileMoveable(Vector2Int boardCoord)
    {
        if (boardCoord.x > -1 && boardCoord.x < BM.boardManager.size.x
        && boardCoord.y > -1 && boardCoord.y < BM.boardManager.size.y)
        {
            if (BM.boardManager.board[boardCoord.x, boardCoord.y].isPieceOnTile)
            {
                if (BM.boardManager.board[boardCoord.x, boardCoord.y].pieceOnTile.team != team)
                {
                    BM.boardManager.moveableArea.Add(BM.boardManager.board[boardCoord.x, boardCoord.y]);
                }
            }
            else
            {
                BM.boardManager.moveableArea.Add(BM.boardManager.board[boardCoord.x, boardCoord.y]);
            }
        }
    }
    
}

 

공통 경로 계산 함수인 CheckGeneralPath() 는 매개변수로 이동방향과 이동 칸 수를 받아서, 해당 방향으로 이동 칸 수만큼 직선으로 나아가며 보드의 끝이나 말을 만나면 멈추도록 짰습니다.

방금 전에 이야기했던 나이트나, 폰의 이동 (전방 대각선 한 칸에 적이 있는 경우 해당 타일로 이동 가능) 과 같은 특수한 이동은 뭔가 깔끔하고 센스있게 처리할 수 있는 방법이 없을까 조금 고뇌했지만... 그냥 좌표의 타일 하나가 이동 가능한 타일인지만 체크해주는 단순한 함수를 여러번 사용해서 처리하는 무시무시한 방식으로 가버렸습니다.

 

public class Piece_Knight : ChessPiece
{
    public override void CheckPath()
    {
        CheckTileMoveable(new Vector2Int(currentTile.coordinate.x + 1, currentTile.coordinate.y + 2));
        CheckTileMoveable(new Vector2Int(currentTile.coordinate.x + 2, currentTile.coordinate.y + 1));

        CheckTileMoveable(new Vector2Int(currentTile.coordinate.x - 1, currentTile.coordinate.y + 2));
        CheckTileMoveable(new Vector2Int(currentTile.coordinate.x - 2, currentTile.coordinate.y + 1));

        CheckTileMoveable(new Vector2Int(currentTile.coordinate.x + 1, currentTile.coordinate.y - 2));
        CheckTileMoveable(new Vector2Int(currentTile.coordinate.x + 2, currentTile.coordinate.y - 1));

        CheckTileMoveable(new Vector2Int(currentTile.coordinate.x - 1, currentTile.coordinate.y - 2));
        CheckTileMoveable(new Vector2Int(currentTile.coordinate.x - 2, currentTile.coordinate.y - 1));
    }
}

(나이트 클래스의 코드)

 


 

마무리

말의 콜라이더가 다른 말이나 타일 터치를 막아서 생기는 문제 때문에 카메라 회전을 추가했습니다.
처음엔 카메라 회전에 가속도를 주었었는데, 스와이프가 완료된 뒤에도 카메라 회전이 감속되며 정지해서 터치 조작에 악영향을 주는 것 같아 제거했습니다.

 

 

최종적으로 말의 이동과 플레이어의 차례가 구현된 모습입니다.

이동과 차례를 구현하고 나니 '어려운 건 끝났다! 프로토타이핑이 거의 끝에 다다랐다!'는 안도의 마음과 함께 게을러져 버렸습니다... 다음 차례인 덱 빌딩 시스템 만들기는 언젠가의 미래에 진행하려고 합니다. 🐢

 


 

GitHub - rkato0128/Chess: MC Prototype

MC Prototype. Contribute to rkato0128/Chess development by creating an account on GitHub.

github.com

 

 

 

'Dev-Log' 카테고리의 다른 글

프로젝트 MC #01 - 체스 보드 생성하기  (0) 2022.09.18