정말 많은 시간을 들였고 많은 고민을 했다.
결국 대부분의 코드를 갈아엎었고 코드 흐름에 상태를 도입해 동작하도록 구성했다.
시작부터 흐름을 차례대로 설명하겠다.
//Board.cs
private void Awake()
{
panelSize = new Vector2Int(8, 16);
NodeList = nodeSpawner.SpawnNode(this, panelSize);
blockList = new List<Block>();
dragStartNode = null;
dragEndNode = null;
updateNodeList = new List<Node>();
}
private void Start()
{
UnityEngine.UI.LayoutRebuilder.ForceRebuildLayoutImmediate(nodeSpawner.GetComponent<RectTransform>());
foreach (Node node in NodeList)
{
node.localPosition = node.GetComponent<RectTransform>().localPosition;
}
CheckNodesBlank();
StartCoroutine(BlockDrop());
}
가장 처음 실행되는 과정이다.
각종 오브젝트를 초기화해준다.
CheckNodesBlank() 로 빈칸에 과일을 스폰시켜주고
BlockDrop() 코루틴을 통해 블럭을 아래로 떨어트린다.
private void Update()
{
//Debug.Log(updateNodeList.Count());
switch(currentState)
{
case STATUS.IDLE:
IdleProcess();
break;
case STATUS.DROP:
StartCoroutine(BlockDrop());
break;
case STATUS.SWAP:
StartCoroutine(checkSwapStatus());
break;
case STATUS.DESTROY:
StartCoroutine(blockDestroyProcess());
break;
case STATUS.PROCESS:
break;
}
}
게임은 이 상태에 따라 컨트롤된다.
IDLE
모든 동작을 마치고 마우스 입력을 대기받는 상태
DROP
빈칸이 생겼을 경우 블럭을 아래로 내려주는 작업
SWAP
마우스를 통한 블럭 스왑을 처리하는 작업
DESTROY
이동한 블럭이 있을 경우 블럭의 상태를 체크하고 파괴, 아이템을 생성하는 작업
PROGRESS
작업중인 상태.
대부분 코루틴으로 구현되어있으므로 Update가 돌면서 코루틴을 무한하게 걸어버리면 곤란하기 때문에, 코루틴이 실행되자마자 일단 상태를 PROGRESS로 바꿔주고 작업이 끝나면 다음 상태로 바꿔주는 식으로 구현했다.
void CheckNodesBlank() //생성칸 빈칸체크 후 블록 스폰
{
for (int y = 0; y < panelSize.y / 2; y++)
{
for (int x = 0; x < panelSize.x; x++)
{
if (NodeList[y * panelSize.x + x].placedBlock == null)
{
SpawnBlock(y * panelSize.x + x);
}
}
}
}
모든 블럭은 일단 화면에서 보이지 않는 8x8 구역에서 생성되고 떨어지기 시작하기 때문에 범위를 잘 설정해준다.
public IEnumerator BlockDrop()
{
updateNodeList.Clear();
currentState = STATUS.PROCESS;
for(int y = panelSize.y-1;y>=0;y--)
{
for(int x = 0;x<panelSize.x;x++)
{
Node node = NodeList[y * panelSize.x + x];
if (node.placedBlock == null)
{
continue; //현재 Node에 block이 없다면 패스
}
Node targetNode = node.FindTarget(); //현재 노드의 목적지 탐색
if (targetNode == node)
{
continue; // 현재 노드와 목적지가 같으면 패스
}
else // 이동
{
Move(node, targetNode); //위치정보의 이동
updateNodeList.Add(targetNode);
}
}
}
foreach (Node node in updateNodeList)
{
if(node.placedBlock.target != null)
{
node.placedBlock.StartMove(); //실질적으로 보여지는 이동
}
}
CheckNodesBlank();
yield return new WaitForSeconds(0.5f);
if(updateNodeList.Count > 0) currentState = STATUS.DESTROY;
else currentState = STATUS.IDLE;
}
코루틴이 시작되어 이 함수가 작업에 들어가면 일단 게임의 상태를 PROGRESS로 바꿔준다.
전체 보드를 좌하단부터 우상단까지 돌며 블럭들을 가능한 한 아래로 내려준다.
이때 이동해야 할 블럭은 updateNodeList에 저장하여 추후 족보에 따른 블럭 파괴에 사용한다.
(현재 상태는 완전무결하며, 움직이지 않으면 파괴할 일도 없다)
다 내려줬으면 생긴 빈칸을 CheckNodesBlank()로 채워준다.
일련이 과정이 안전하게 끝나기를 잠깐 기다려준 후
움직인 블럭이 있다면 파괴할 블럭이 있는지 체크하러 상태를 DESTROY로 바꿔주거나
없다면 IDLE로 복귀한다.
void IdleProcess()
{
if (dragStartNode != dragEndNode && dragEndNode != null && dragStartNode != null)
{
if (dragStartNode.Value.y >= panelSize.y / 2 && dragEndNode.Value.y >= panelSize.y / 2)
{
StartCoroutine(SwapBlock());
}
}
}
IDLE일때는 화면에 보이는 8x8 보드판에 마우스 입력이 있는지 항상 체크한다.
유효한 움직임이 있을경우 SwapBlock() 코루틴을 실행한다.
private IEnumerator SwapBlock()
{
currentState = STATUS.PROCESS;
updateNodeList.Clear();
//시작, 끝 블럭 확인
Node from = NodeList[dragStartNode.Value.y * panelSize.x + dragStartNode.Value.x];
Node to = NodeList[dragEndNode.Value.y * panelSize.x + dragEndNode.Value.x];
//스왑
Swap(from, to);
yield return new WaitForSeconds(1.0f);
currentState = STATUS.SWAP;
}
스왑할 블럭 두개를 특정한 후 일단 스왑한다.
그리고 상태를 SWAP으로 바꿔 본격적인 SWAP 프로세스를 진행하게 된다.
private IEnumerator checkSwapStatus()
{
currentState = STATUS.PROCESS;
Node from = NodeList[dragStartNode.Value.y * panelSize.x + dragStartNode.Value.x];
Node to = NodeList[dragEndNode.Value.y * panelSize.x + dragEndNode.Value.x];
from.FindSame3();
to.FindSame3();
if (from.sameCount[0] + from.sameCount[2]>=2 || from.sameCount[1] + from.sameCount[3] >= 2 ||
to.sameCount[0] + to.sameCount[2] >= 2|| to.sameCount[1] + to.sameCount[3] >= 2)
{
updateNodeList.Add(to);
updateNodeList.Add(from);
currentState = STATUS.DESTROY;
}
else //원상복귀
{
Swap(from, to);
from.InitsameCount();
to.InitsameCount();
yield return new WaitForSeconds(1.0f);
currentState = STATUS.IDLE;
}
//초기화
dragEndNode = null;
dragStartNode = null;
}
스왑한 두 블럭 from과 to 각각 탐색을 돌려 스왑 후 파괴가능한 상태가 되었는지 확인한다.
sameCount 배열은 4방향으로 각각 본인블럭과 같은 모양의 블럭이 몇개 배치되어있는지 저장해주는 배열이므로
본인1 + 2 이상이면 파괴 가능한 상태가 된다.
파괴해야 한다면 스왑한 블럭을 updateNodeList에 넣고 상태를 DESTROY로 바꿔준다.
파괴할것이 없으면 의미없는 swap을 한 것이므로 다시 swap해 원래대로 돌려준 후IDLE로 돌아간다.
IEnumerator blockDestroyProcess()
{
currentState = STATUS.PROCESS;
updateNodeList.Reverse();
Debug.Log(updateNodeList.Count);
foreach(Node node in updateNodeList)
{
if ((node.point.y < panelSize.y / 2) || node.placedBlock == null) continue;
node.FindSame3();
int r = node.sameCount[0];
int d = node.sameCount[1];
int l = node.sameCount[2];
int u = node.sameCount[3];
if (r + l >= 4) //가로 5
{
node.Destroysame();
node.ToItem(IMAGES.five);
}
else if (d + u >= 4) //세로 5
{
node.Destroysame();
node.ToItem(IMAGES.five);
}
else if (r + l >= 2 && u + d >= 2) //T
{
node.Destroysame();
node.ToItem(IMAGES.bomb);
}
node.InitsameCount();
}
foreach (Node node in updateNodeList)
{
if (node.placedBlock == null) continue;
node.FindSame3();
int r = node.sameCount[0];
int d = node.sameCount[1];
int l = node.sameCount[2];
int u = node.sameCount[3];
if (r + l >= 3) //가로 4
{
node.Destroysame();
node.ToItem(IMAGES.four);
}
else if (d + u >= 3) //세로 4
{
node.Destroysame();
node.ToItem(IMAGES.four);
}
else if (r + l >= 2) // 가로 3
{
node.Destroysame();
node.DestroyBlockObject();
}
else if(u + d >= 2) //세로 3
{
node.Destroysame();
node.DestroyBlockObject();
}
node.InitsameCount();
}
updateNodeList.Clear();
yield return new WaitForSeconds(1.0f);
currentState = STATUS.DROP;
}
updateNodeList에는 움직인 블럭이 차례대로 들어있다
마지막에 넣은 블럭부터 처리하고 싶으므로 배열을 일단 반전시킨다.
노드를 하나씩 꺼내며 처리하는데, 주의할점은 이전 노드 처리에서 파괴된 블럭이 위치한 노드일 수 있으므로, 블럭의 유무를 항상 체크해줘야한다.
족보에 따라 처리하는데 전체 노드를 두번 순회하게 했다.
이는 족보의 우선도와 T형, 혹은 L형 족보의 예외때문인데,
일반적으로 한번에 처리해버리면
가로로 4블럭, 세로로 3블럭인 T형 블럭에 경우 T형 족보인 폭탄을 생성하지 못하고
먼저 조건을 만족할 가로 4블럭, 혹은 세로 3블럭을 파괴해버릴 것이다.
순회 중간에 만나게될 4,3 교차점 블럭은 처리가 불가능하다.
고로 일단 모든 족보에서 가장 우선인 5연속 블럭과
예외덩어리 T,L 배치를 처리해준 후,
일반적일 4연속 블럭, 3연속 블럭을 처리해준다.
내가 쓴 탐색방법의 특성상 탐색을 시작하는 본인 노드는 건드리지 않으므로
아이템을 생성해야 하면 생성해주고, 3연블럭이라 그냥 파괴해야하면 직접 파괴해준다.
sameCount는 꼬박꼬박 초기화 해주어야 문제가 생기지 않는다.
처리가 끝나길 잠깐 기다린 후, 파괴가 일어났으면 빈칸이 생겼을테니 다시 DROP을 진행한다.
DROP->DESTROY를 반복하다 더이상 움직임이 없어지면 IDLE로 전환하여 대기한다.
//Node.cs
public void FindSame3()
{
if(placedBlock == null) return;
InitsameCount();
IMAGES type = placedBlock.blockType;
for (int i = 0; i < 4; i++)
{
Node node = this;
while (true)
{
if (!node.NeighborNodes[i].HasValue) break;
Vector2Int nextNodePoint = node.NeighborNodes[i].Value;
if (nextNodePoint.y < board.panelSize.y / 2) break;
node = board.NodeList[nextNodePoint.y * board.panelSize.x + nextNodePoint.x];
if (node.placedBlock == null) break;
if (node.placedBlock.blockType != type) break;
else
{
sameCount[i]++;
}
}
}
}
sameCount를 채워주는 함수
3이붙은 이유는 구조를 3번 갈아엎었기 때문..
현재 노드를 기준으로 사방에 현재 노드에 위치한 블럭과 같은 모양의 블럭의 갯수를 세주는 단순한 구조이다.
원래 재귀로 구현했지만 굳이 그럴 필요 없어보여서 단순하게 바꿨다.
//Node.cs
public void Destroysame()
{
if (placedBlock == null) return;
IMAGES type = placedBlock.blockType;
if (sameCount[0] + sameCount[2] >= 2)
{
Node node = this;
for (int i = 0; i < sameCount[0];i++)
{
Vector2Int nextNodePoint = node.NeighborNodes[0].Value;
node = board.NodeList[nextNodePoint.y * board.panelSize.x + nextNodePoint.x];
if (node.placedBlock == null) continue;
node.DestroyBlockObject();
node.InitsameCount();
}
node = this;
for (int i = 0; i < sameCount[2]; i++)
{
Vector2Int nextNodePoint = node.NeighborNodes[2].Value;
node = board.NodeList[nextNodePoint.y * board.panelSize.x + nextNodePoint.x];
if (node.placedBlock == null) continue;
node.DestroyBlockObject();
node.InitsameCount();
}
}
if (sameCount[1] + sameCount[3] >= 2)
{
Node node = this;
for (int i = 0; i < sameCount[1]; i++)
{
Vector2Int nextNodePoint = node.NeighborNodes[1].Value;
node = board.NodeList[nextNodePoint.y * board.panelSize.x + nextNodePoint.x];
if (node.placedBlock == null) continue;
node.DestroyBlockObject();
node.InitsameCount();
}
node = this;
for (int i = 0; i < sameCount[3]; i++)
{
Vector2Int nextNodePoint = node.NeighborNodes[3].Value;
node = board.NodeList[nextNodePoint.y * board.panelSize.x + nextNodePoint.x];
if (node.placedBlock == null) continue;
node.DestroyBlockObject();
node.InitsameCount();
}
}
}
파괴에 사용하는 함수이다.
이 함수는 FindSame3 함수가 실행되어 sameCount가 올바르게 채워져있다는것을 전로 한 함수이므로 조금 과감하게 동작한다.
상하, 좌우를 따로 나눠서 3연속 이상이면 하나씩 바로바로 파괴해준다.
sameCount 초기화도 잊지 않는다.
참고로 기준이되는 본인 노드의 블럭은 파괴되지 않는다.
이렇게 핵심 코드는 전부 설명한거 같다.
쉬울줄 알았는데 상태를 짜는것은 너무나도 어려웠고 수 주간의 숱한 갈아엎기와 수정으로 겨우 볼만한 코드를 만들 수 있었다.
그리고 대학 강의 다닐때 잘 썼던 내 갤럭시 북은 유니티를 돌리기엔 너무나도 버거웠다
결국 4060 그래픽 카드를 탑재한 노트북으로 교체했고 쾌적하게 작업할 수 있게 되었다.
해결해야 할 과제는
DROP 알고리즘을 개선하여 떨어지는 속도를 일정하게 바꾸고,
가끔 알 수 없는 이유로 화면밖의 블럭과 계산되어 블럭이 파괴되는 버그를 잡는 것이다.
'Unity 게임개발' 카테고리의 다른 글
2. 블럭 스왑 구현 (0) | 2025.01.14 |
---|---|
1. 게임화면 구성 (0) | 2025.01.11 |
0. 유니티 밍글맹글 만들기 (0) | 2025.01.11 |
기획 - 1. 장르 정하기 (0) | 2023.08.04 |
팀프로젝트 진행 - 1 (0) | 2023.08.04 |