움직일 때 드래그 제한하기

타일을 맞췄을 때 드래그 제한하기

조건 : 여러 타일들이 코루틴으로 움직이는데 첫 타일이 움직이기 시작했을 때부터 마지막 타일이 멈췄을 때까지 드래그를 제한해야 함

- MainGame 클래스를 Tile에서 참조하는 방법 -> 은닉성이 떨어짐

- 변수를 static으로 쓰는 방법 -> 차라리 참조를 쓰는 게 나아보임

- 콜백으로 정보를 변경하고 움직일 수 있는지 받아옴 -> 이 방법을 사용해보지 않아서 사용해봤음

 

- Action<int>로 타일이 움직였으면 1을 더해주고 움직임이 끝났으면 1을 빼줬다.

- Func<bool>로 드래그가 가능한지를 받아왔다. (_movingCount == 0일 때 드래그 가능)

public class CharacterTile : MonoBehaviour, IBeginDragHandler
{
    private Action<int> _onMoveInfoChanged;
    private Func<bool> _getDragable;
	
    public void OnBeginDrag(PointerEventData eventData)
    {
        if (!_getDragable.Invoke())
            return;

        /* 위치 체크,갱신 로직
        	...
        */
    }
    
    public IEnumerator Move(Vector2 targetPos, float moveTime)
    {
        _onMoveInfoChanged.Invoke(ADD_VALUE);
        /* 움직임 로직
        	...
        */
        _onMoveInfoChanged.Invoke(-ADD_VALUE);
    }

    public IEnumerator GoRoundTrip(Vector2 targetPos, float moveTime)
    {
        _onMoveInfoChanged.Invoke(ADD_VALUE);

        /* 움직임 로직
        	...
        */
        _onMoveInfoChanged.Invoke(-ADD_VALUE);
    }
}
public class MainGame : MonoBehaviour
{
	private int _movingCount;
    
    private bool GetDragable()
    {
        return _movingCount == 0;
    }

    private void MoveInfoChangedCallback(int num)
    {
        _movingCount += num;
    }
    
}

 

 

결론

장점 : CharacterTile은 MiniGame의 정보를 모르므로 은닉성이 증가하였다.

단점 : 디버깅 시 코드의 흐름을 따라가기 조금 어렵다.

=> 핵심 로직이거나 은닉화가 필요한 정보를 다룰 때 또는 입력 등의 콜백으로 사용할 때 좋다고 생각한다.

 

 

순차적인 파괴 가능하도록 하기

보드가 변경되었을 때 연속적인 타일이 존재한다면 순차적으로 파괴 되도록 하기

- ReArrangeBoard 메서드에 bool 값 파라미터를 넣음으로써 순차적으로 파괴할 경우와 아닌 경우를 나눴다.

https://jcdevelop98.tistory.com/414

 

애니팡 클론코딩 (6) - 연속적인 타일이 3개 이상인지 확인하기

연속적인 타일이 3개 이상인지 확인하기 타일을 생성시에 연속적으로 3개가 존재하는지 확인 - 상하좌우 3개 이상인지 검사하므로 4칸은 검사하지 않아도 됨 - while문을 돌면서 3개 이상이라면 Exp

jcdevelop98.tistory.com

public class MainGame : MonoBehaviour
{
	private int _movingCount;

    private void MoveInfoChangedCallback(int num)
    {
        _movingCount += num;

        if (_movingCount == 0)
        {
            ReArrangeBoard(true);
        }
    }
    
}

 

 

버그

* 코루틴 중간에 오브젝트가 파괴되는 경우

* 파괴될 오브젝트가 드래그가 먹혀서 문제가 되는 경우

* 한 번에 여러 타일이 파괴될 때 구멍이 생기는 경우

연속적인 타일이 3개 이상인지 확인하기

타일을 생성시에 연속적으로 3개가 존재하는지 확인

- 상하좌우 3개 이상인지 검사하므로 4칸은 검사하지 않아도 됨

- while문을 돌면서 3개 이상이라면 Explode 메서드를 통해 타일을 파괴

private void ReArrangeBoard()
{
    bool isExploded = true;

    while (isExploded)
    {
        isExploded = false;

        Vector2Int boardIndex;
        for (int i = 0; i < boardSize; i++)
        {
            for (int j = 0; j < boardSize; j++)
            {
                if (i >= EXPLOSION_COUNT -1 || j >= EXPLOSION_COUNT -1)
                {
                    boardIndex = new Vector2Int(i, j);
                    if (CheckExplosion(boardIndex))
                        isExploded = true;
                }
            }
        }

        if (isExploded)
            Explode();
    }
}

 

 

자연스러운 타일 이동 연출하기

타일이 파괴 된 후 자연스러운 이동 구현

1. 먼저 기존에 있던 타일 위치들 옮기는 것을 구현 (ArrangeBoard 메서드)

- 2차원 배열 Board를 기준으로 윗 타일이 null인지 확인하고 아래로 한 칸씩 위치를 저장

- 인덱스에 해당하는 목표 위치가 필요하므로 Vector2Int를 Key로 Vector2를 Value 값으로 하는 Dictionary에 저장 (_targetDict)

2. 추가적으로 생성해야 하는 타일들을 시작 위치로 생성 뒤 목표 위치 저장

3. _targetDict를 순회하며 코루틴으로 이동

- _nullCountArr라는 배열으로 해당 컬럼에 타일이 몇 개 생성 되었는지 카운트하고 다음 생성되는 타일의 위치 정보를 갱신

private void ArrangeBoard(bool isSequencial)
{
    Vector2Int tempIndex = Vector2Int.zero;


    for (int i = 0; i < boardSize; i++)
    {
        for (int j = 0; j < boardSize; j++)
        {
            tempIndex.x = j;
            tempIndex.y = i;
            if (!_board[tempIndex])
            {
                _nullQueue.Enqueue(tempIndex);
            }
            else if (_nullQueue.Count > 0)
            {
                Vector2Int nullIndex = _nullQueue.Dequeue();
                _board[nullIndex] = _board[tempIndex];
                _board[nullIndex].SetIndex(nullIndex);

                _board[tempIndex] = null;
                _nullQueue.Enqueue(tempIndex);

                Vector2 targetPos = _boardPosArr[nullIndex.x, nullIndex.y];

                if (isSequencial)
                {
                    if (!_targetPosDict.ContainsKey(nullIndex))
                        _targetPosDict.Add(nullIndex, targetPos);
                    else
                        _targetPosDict[nullIndex] = targetPos;
                }
                else
                {
                    _board[nullIndex].Transform.position = targetPos;
                }
            }
        }


        while (_nullQueue.Count > 0)
        {
            CreateAdditionalTile(isSequencial);
        }

        Array.Fill(_nullCountArr, 0);
        _nullQueue.Clear();
    }

    if (isSequencial)
    {
        foreach (var item in _targetPosDict)
        {
            StartCoroutine(_board[item.Key].Move(item.Value, arrangeTime));
        }
    
        _targetPosDict.Clear();
    }
}

private void CreateAdditionalTile(bool isSequencial)
{
    /* 타일 생성 및 초기화 로직 
    	...
    */
    
    int columnNullCount = _nullCountArr[nullIndex.y]++;
    Vector2 startPos = tile.Transform.position;
    startPos += new Vector2(_halfUnit + _unit * nullIndex.y, _halfUnit + _unit * (boardSize + columnNullCount));

    Vector2 targetPos = _boardPosArr[nullIndex.x, nullIndex.y];

    if (isSequencial)
    {
        _board[nullIndex].Transform.position = startPos;
        _targetPosDict.Add(nullIndex, targetPos);
    }
    else
    {
        _board[nullIndex].Transform.position = targetPos;
    }
}

결과

- 떨어지는 위치가 다른 버그 존재 (코루틴 또는 코루틴 중 터치 문제로 보임)

- 터지고 나서 추가 타일에 대한 체크도 필요

파괴 시 생성과 정렬

파괴 시 생성과 정렬

- 이전과 이어서 Queue를 이용하여 다음 타일 파괴

- 파괴된 개수 만큼 생성하고 인덱스를 검사하면서 정렬

- 빈 인덱스에 추가

* 생성과 파괴가 잦으므로 ObjectPooling이 필요

* 주변 타일들도 검사하여 파괴하는 로직 필요

* 정렬 시 자연스럽게 이동하는 로직 필요

// 배열의 크기만큼 세로 줄 당 자리를 채워주게끔 정렬한 뒤
// while문에서 새롭게 타일들을 생성하여 자리에 채워줌

for (int i = 0; i < boardSize; i++)
{
    for (int j = 0; j < boardSize; j++)
    {
        tempIndex.x = j;
        tempIndex.y = i;
        if (!_board[tempIndex])
        {
            _nullQueue.Enqueue(tempIndex);
        }
        else if (_nullQueue.Count > 0)
        {
            Vector2Int nullIndex = _nullQueue.Dequeue();
            // 교환 세팅 로직...
            _nullQueue.Enqueue(tempIndex);
        }
    }



    while (_nullQueue.Count > 0)
    {
        CreateAdditionalTile();
    }

    _nullQueue.Clear();
}

private void CreateAdditionalTile()
{
    Vector2Int nullIndex = _nullQueue.Dequeue();
    // 타일 교환, 초기화 로직...
}

 

 

타일 초기화

커스텀 에디터를 이용한 타일 초기화

- 테스트 효율성을 위해 버튼으로 타일 초기화

* 초기 세팅에서 연결된 타일을 부시는 로직 필요

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(MainGame))]
public class BoardEditor : Editor
{
    private MainGame _board;

    private void OnEnable()
    {
        _board = (MainGame)target;
    }

    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();
        
        if (GUILayout.Button("ResetBoard"))
        {
            _board.InitBoard();
        }
    }
}

실패 이동

실패 시 이동 (왔다 갔다) 구현

- 실패 시 이동은 Cos을 이용하여 구현하였다.

- Cos의 분자의 경우에는 ㅠ만큼 지나면 다시 0이 되는 것이니 경과 시간에 ㅠ를 곱해줬다.

- Cos의 분모의 경우에는  목표 시간을 넣어줬는데 목표 시간이 클수록 경과 시간도 커지기 때문이다.

- 경과 시간이 ㅠ부터 시작하기 위해 (ratio가 0부터 시작하기 위해) ㅠ를 더해줬다.

 

public IEnumerator GoRoundTrip(Vector2 targetPos, float moveTime)
{
    Vector2 offsetVec = Transform.position;
    float elapsedTime = 0;
    float ratio = 0;
    while (elapsedTime < moveTime)
    {
        elapsedTime += Time.deltaTime;
        ratio = (MathF.Cos(elapsedTime * PI / (moveTime * HALF) + PI) + 1) * HALF;

        if (elapsedTime >= moveTime)
            ratio = 0f;

        Transform.position = Vector2.Lerp(offsetVec, targetPos, ratio);
        yield return null;
    }
}

 

 

파괴 로직

이동 후 파괴

- 이동 후 파괴시켜야 하므로 코루틴 이후 타겟의 이동이 끝나면 호출 하는 방식

- 이동이 끝나면 콜백으로 호출하게 만들었다.

private void Explode()
{
    foreach (var index in _explosionSet)
    {
        _boardPosArr[index.x, index.y] = _board[index].Transform.position;
        Destroy(_board[index].gameObject);
        _board[index] = null;
    }

    ArrangeBoard();
}
public IEnumerator Move(Vector2 targetPos, float moveTime)
{
    // 이동 로직

    if (_destroyOrder)
        _destroyCallback.Invoke();
}

 

 

다음 타일 내려오게 하기

파괴가 완료 되었다면 파괴된 타일 기준으로 위의 타일이 내려와야 한다.

- 선입 선출이 필요해서 Queue를 사용 (계속 다음 타일과 바꿔 기존의 타일을 null로 해줘야 함)

for (int j = 0; j < boardSize; j++)
{
    tempIndex.x = j;
    tempIndex.y = i;
    if (!_board[tempIndex])
    {
        _nullQueue.Enqueue(tempIndex);
    }
    else if (_nullQueue.Count > 0)
    {
        Vector2Int nullIndex = _nullQueue.Dequeue();
        _board[tempIndex].Transform.position = _boardPosArr[nullIndex.x, nullIndex.y];
        _board[nullIndex] = _board[tempIndex];
        _board[tempIndex] = null;
    }
}

 

 

결과

- 실패시 다시 타일이 돌아옴

- 성공 시 타일이 파괴되고 위의 타일들이 자리를 채워줌

 

랜덤 타일 세팅

첫 세팅 시 타일 랜덤 배치

- ScriptableObject를 사용하여 타일 스프라이트 사용 -> 오늘 작업

- 3개 이상 안 겹치게 확인하여 다른 타일로 바꾸는 로직 필요 -> 추후 작업

public class MainGame : MonoBehaviour
{
	
    [SerializeField] private CharacterSO characterSO;

    private void InitBoard()
    {
        int characterSOIndex = UnityEngine.Random.Range(0, boardSize);
        characterImage.sprite = characterSO.Characters[characterSOIndex];
    }
}

 

 

 

여러 개일 때 움직이는 로직 구현 (1차 구현)

맞았을 때 테스트용으로 움직이는 로직을 구현하기로 했다.

- 아래처럼 구현하였는데 조건에 따라 총 6개의 경우의 수를 검사하기로 했다.

 

- enum배열 요소를 사용하여 bool배열을 체크하려 했지만 인덱스 형변환이 가비지를 발생시키므로 딕셔너리로 체크하였다.

- 위의 내용처럼 가운데가 먼저 체크 되었을 때 연산을 줄이려고 했다.

Dictionary<Direction, bool> _checkDict = new Dictionary<Direction, bool>();

private bool CheckExplosion(Vector2Int index)
{
    int targetSOIndex = _board[index].CharacterSOIndex;

    _checkDict[Direction.Horizontal] = CheckHorizontalIndex(index, targetSOIndex);
    _checkDict[Direction.Vertical] = CheckVerticalIndex(index, targetSOIndex);

    if (!_checkDict[Direction.Horizontal])
    {
        _checkDict[Direction.Right] = CheckRightIndex(index, targetSOIndex);
        _checkDict[Direction.Left] = CheckLeftIndex(index, targetSOIndex);
    }

    if (!_checkDict[Direction.Vertical])
    {
        _checkDict[Direction.Up] = CheckUpIndex(index, targetSOIndex);
        _checkDict[Direction.Down] = CheckDownIndex(index, targetSOIndex);
    }

    return _checkDict[Direction.Horizontal] || _checkDict[Direction.Vertical] || 
        _checkDict[Direction.Left] || _checkDict[Direction.Right] ||
        _checkDict[Direction.Up] || _checkDict[Direction.Down] || _checkDict[Direction.Up];
}

 

 

파괴할 오브젝트 추가

- 위의 조건을 돌면서 파괴할 오브젝트를 추가하는데 중복을 제거해야하므로 HashSet 자료구조를 사용했다.

- 이미지가 3개 이상이 같은지 확인했다.

private bool CheckHorizontalIndex(Vector2Int index, int targetSOIndex)
{
    // 가로
	// ... 로직 ...
    
    if (isHorizontalExploded)
    {
        for (int i = tempLeftIndex.y; i <= tempRightIndex.y; i++)
            _explosionSet.Add(new Vector2Int(index.x, i));
    }

    return isHorizontalExploded;
}

 

 

결과

- 타일 랜덤 배치

- 연속적인 타일이 3개 이상일 때 타일이 움직이도록 구현

* 다음 목표 : 처음 세팅 시 3개 붙어있는 것을 제거하도록 구현

보드 구현

보드를 구현하기 위한 위치 저장소

- Vector2[,]를 사용했는데 보드의 크기가 (7,7)로 정해져 있다는 조건 하에 배열을 선택했다.

보드판의 실제 객체들

- 딕셔너리로 구현했고 키값은 Vector2Int타입으로 받았는데 위의 배열의 인덱스로 사용하려는 의도였다.

- Value 값은 터치를 받을 CharacterTile 타입으로 받았다.

private Vector2[,] _boardPosArr;
private Dictionary<Vector2Int, CharacterTile> _board = new Dictionary<Vector2Int, CharacterTile>();

 

터치

터치 받기

- IBeginDragHandler를 사용 했는데 애니팡 게임에서 드래그가 일어나마자 반응하는 것을 구현하기 위해 해당 인터페이스를 사용했다.

- enum 타입의 Direction으로 상하좌우를 구분하게 했다.

public void OnBeginDrag(PointerEventData eventData)
{
    bool isLeft = eventData.delta.x < 0;
    bool isDown = eventData.delta.y < 0;
    float deltaXAbs = isLeft ? -eventData.delta.x : eventData.delta.x;
    float deltaYAbs = isDown ? -eventData.delta.y : eventData.delta.y;
    if (deltaXAbs > deltaYAbs)
    {
        if (isLeft)
            _dragInputted.Invoke(Direction.Left, _index);
        else
            _dragInputted.Invoke(Direction.Right, _index);
    }
    else
    {
        if (isDown)
            _dragInputted.Invoke(Direction.Down, _index);
        else
            _dragInputted.Invoke(Direction.Up, _index);
    }
}

public enum Direction
{
    Right,
    Left, 
    Up, 
    Down
}

 

드래그 콜백

Action<Direction, Vector2Int>

- 드래그가 일어났을 때 콜백으로 다음 객체와의 위치 교환을 하기 위해 방향 정보와 인덱스를 받았다.

class CharacterTile
{
	private Action<Direction, Vector2Int> _dragInputted;
}

class MainGame
{
	private void ChangeBoard(Vector2Int index, Vector2Int nextIndex)
	{
    	// 교환 로직 ...
	}
}

 

 

결과

상하좌우 교환이 가능하게 됨

+ Recent posts