들어가며
여러 게임에서 본 적은 많이 있지만, 단독으로는 나온 게임을 본 적은 많이 없는 바로 그 게임 해밀토니안 경로 퍼즐이다. 좀 더 우리말로 순화하자면, 한 붓 그리기라고 할 수 있을 것 같다.
이쯤에서 다시 한 번, 이 챌린지(프로젝트)의 목표에 대해서 다시 한 번 돌아볼 때가 왔다.
가장 큰 규칙이자 목적은 하루 안에 프로토타입을 구현하는 것을 우선적으로 하는 것이다. (이전에 MVP라고 표현한 적이 있는데, 이제 와서 생각해보니 프로토타입이 더 정확한 표현이라 생각한다.)
프로토타입 : 테스트 등의 이유로 최소한의 기능만을 구현한 제품
MVP : 배포 및 서비스를 위한 기능을 필요한 부분(?)만 구현한 제품
(정확한 표현은 아니고, 내가 느낀 뉘앙스를 적은 것일 뿐이므로 참고할 때 주의)
퍼즐이 동작하고 규칙이 명확하게 느껴지는 순간이 프로토타입의 완성이라고 볼 수 있을 것이다.
이를 위해서는 핵심 목표와 최소 기능을 정하는 과정이 반드시 필요하며, 지금까지 만들어오면서 나 역시 그러한 과정을 알게 모르게 겪으면서 만들어왔다.
그리고 이제 다시 돌아보니, 해당 과정들이 모두 공통적으로 이루어졌었다는 점으로 미루어보아.. 필요한 단계가 맞다는 생각이 든다.
오늘의 서론은 한 붓 그리기에 대한 설명이나 스몰토크보다 지금까지 만든 과정들에 대한 약간의 회고?를 얘기하는 것이 좋다고 생각했다.
왜냐하면, 한 붓 그리기라는 게임은 규칙과 방법이 이미 제목에서 부터 드러나기도 하고, 매우 직관적이기 때문이다.
개발 전 계획
이곳에서는 위에서 얘기한 두 가지에 대해서 얘기를 하고 넘어가겠다. 먼저, 첫 번째로 핵심 목표이다. 이 프로토타입을 완성하기 위해 달성해야 할 목표(구현해야할 기능)이다.
1. 플레이어는 한 칸씩 이동한다.
2. 지나간 길(블록 또는 타일)은 다시 지나갈 수 없다.
3. 목표 지점에 도달하면 클리어 (타이밍)
4. 특정 길을 제외한 모든 길을 플레이어가 지나야한다. (클리어 조건)
해당 목표는 최종적으로 구현해야할 기능들이다. 이 기능들이 모두 완성되었다고 판단하면 그 때가 프로토타입의 완성이다.
다음 두 번째로는 최소 기능 구성에 대해서이다. 핵심 목표를 달성하기 위해 구현해야할 기능들을 정리했다.
1. 타일 기반 맵 구현
2. 캐릭터의 상하좌우 이동
3. 이미 지나간 길 표시
4. 이동 불가 (교착상태) 실패 판정-> 까먹고 구현하지 않음
핵심 목표와 이를 달성하기 위한 최소 기능들에 대한 정의는 마무리가 된 것 같으니 실제로 개발하는 단계로 넘어가도록 하겠다.
프로젝트 세팅
이 단계에서는 타일 기반 맵 구현과 플레이어 프리팹의 생성, 그리고 필요한 스크립트들의 구조를 틀만 잡았다.(스켈레톤 코드의 작성)


먼저, 게임판 역할인 Board와 타일들의 부모인 Tiles를 만들어서 임시로 만들어본 타일 기반 맵이다.
모든 Tile의 부모인 Tiles는 Board의 자식이며, Grid Layout Group으로 Tile들을 정렬하고 있다.


타일을 밟을 플레이어도 만들어주고, 프리팹으로 만들어놓겠다.
플레이어 프리팹은 Unity의 기본 도형(Sprite) 중 하나인 Knob을 사용했다.
(동그라미라 플레이어를 표시하기에는 참 좋다고 생각했기 때문이다.)
스켈레톤 코드 작성

Player.cs : 플레이어의 이동과 관련된 기능
PuzzleController.cs : 퍼즐을 전체적으로 총괄하는 기능
PuzzleGenerater.cs : 타일 맵 생성에 관련된 기능
Tile.cs : 각 타일의 영역에서 구현될 기능
스크립트들은 내용은 없고, 생성만 해둔 상태이다.
타일 맵 생성
우선 너무 커지지는 말고, 적당한 크기로 5*5 사이즈의 타일 맵을 생성하는 것을 해보도록 하겠다. 위에서 스크립트를 생성하면서 각각 어떤 기능을 넣을 것인지 대략적으로 정의한 바에 따라서, PuzzleController와 PuzzleGenerater에 코드를 작성해보도록 하겠다.
PuzzleController.cs
using UnityEngine;
namespace Hamiltonian
{
public class PuzzleController : MonoBehaviour
{
[SerializeField]
private PuzzleGenerator puzzleGenerator;
//타일들의 2D 배열 생성
private Tile[,] tiles;
//우선 인수로 건네줄 행과 열의 크기(임시)
[SerializeField]
private int rows = 5; //y, 행
[SerializeField]
private int cols = 5; //x, 열
void Start()
{
InitController();
GameStart();
}
private void InitController()
{
puzzleGenerator = GetComponent<PuzzleGenerator>();
}
public void GameStart()
{
puzzleGenerator = GetComponent<PuzzleGenerator>();
tiles = puzzleGenerator.GeneratePuzzle(rows, cols);
PlayerSpawn(SetStartTtile());
}
private Tile SetStartTtile()
{
return tiles[0, 0];
}
private void PlayerSpawn(Tile startPos)
{
}
}
}
PuzzleController에서는 타일들을 관리하기 위한 2차원 배열과 임시 행(5)과 열(5)을 선언하였다.
게임이 시작되면 PuzzleGenerater의 GeneraterPuzzle 메서드를 실행하고, 타일 타입의 2차원 배열을 반환 받는다. 그리고, 플레이어 생성을 위해 임시로 0,0 위치에 있는 타일을 시작 타일로 설정하고, 해당 타일 위치에 플레이어를 생성시키도록 구조만 만들었다.
PuzzleGenerater.cs
using UnityEngine;
namespace Hamiltonian
{
public class PuzzleGenerator : MonoBehaviour
{
[SerializeField]
GameObject tilePrefab;
[SerializeField]
Transform tilesParent;
private void OnEnable()
{
if (tilesParent != null)
{
tilesParent = GameObject.Find("TilesParent").transform;
}
}
public Tile[,] GeneratePuzzle(int r, int c)
{
Debug.Log($"{GetType()}::GeneratePuzzle");
Tile[,] tiles = new Tile[c, r];
for (int y = 0; y < r; y++)
{
for (int x = 0; x < c; x++)
{
GameObject tileObj = Instantiate(tilePrefab, tilesParent);
tileObj.name = $"Tile({x}, {y})";
Tile tile = tileObj.GetComponent<Tile>();
tile.Initialize(new Vector2Int(x, y));
tiles[x, y] = tile;
}
}
return tiles;
}
}
}
PuzzleGenerater에서는 그동안 만들었던 것과 같이 이중 for문을 돌면서 타일들을 개수에 맞게 생성하고, 이를 통해 만들어진 2차원 배열을 반환한다.
생성된 타일을 배열에 넣기 전에 Inspector에서 확인하기 쉽도록 생성된 타일의 이름에 좌표값을 포함하도록 이름을 바꿔주는 코드도 넣었다.
밑의 사진이 테스트한 결과이다.

플레이어의 위치는 메서드는 호출하지만, 실제로 위치하는 코드는 없기 때문에 의미 없는 상황이다.
현재 테스트는 타일이 동적으로 잘 생성되고 2차원 배열에 담겨 PuzzleController가 반환을 받는 과정까지 오류가 없는지 테스트를 해본 것이다.
플레이어의 생성
이제 플레이어의 생성 로직에 대한 고려사항을 보면 다음과 같다.
지금, Grid Layout Group 컴포넌트를 사용하여 Tile들을 정렬시켰는데, 이 경우에 고려해야하는 주의점들이 존재한다.
이 주의점은 현재 상황의 특수한 상황만이 아닌, Grid Layout Group 계열의 모든 컴포넌트를 사용할 때에도 적용되는 사항들이다.
해당 컴포넌트를 통해 제어되고 있는 UI의 위치로 다른 UI를 위치시키는 경우, 다음과 같은 사항들을 고려해야한다.
1. Layout Group은 RectTransform을 런타임에 재배치한다. (awake나 start 이후에 재배치 됨)
2. 월드 좌표와 로컬 좌표가 혼동 될 수 있다. (anchoredPosition 대신 position을 써야하는 경우 존재)
첫 번째의 "런타임에 재배치한다." 라는 문장의 뜻이 와닿지 않을 수 있다. 좀 더 자세히 설명하자면, awake 또는 start 이후에 자식 UI들의 RectTransform을 재배치한다는 뜻이다.
그래서, 보통의 초기화를 진행하는 awake나 start에서 Layout Group의 제어를 받는 UI의 위치 값을 가져와 무언가 행동을 취한다면, 재배치 되기 전의 위치값을 가져오는 것이기 때문에 엉뚱한 곳의 좌표가 전달될 수 있다는 것이다.
두 번째의 "월드 좌표와 로컬 좌표의 혼동"은 같은 부모이냐, 아니면 다른 부모이냐에 따라 사용하는 좌표 프로퍼티가 달라질 수 있다는 뜻이다.
같은 캔버스(부모) 내의 다른 UI들 끼리는 anchoredPosition을 사용하고, 다른 부모(또는 다른 RectTransform 계층) 기준으로 배치할 때는 position이나 TransformPoint(), InverseTransformPoint() 등의 메서드를 통해 변환이 필요하다.
다음은 변환 예시이다.
Vector3 worldPos = targetRectTransform.position;
Vector3 localPos = otherRectTransform.parent.InverseTransformPoint(worldPos);
otherRectTransform.localPosition = localPos;
other을 target의 위치로 이동시키는 코드이다.
otherRectTransform은 target과는 다른 부모를 두고 있다고 가정한다면, 이때 InverseTransformPoint 메서드를 활용해서 로컬좌표 값을 얻어서 other의 로컬 좌표에 대입한 코드이다.
위와 같은 방법도 있겠지만, Layout Group 컴포넌트가 RectTransform을 런타임에 배치하는 타이밍을 강제로 지정할 수도 있다. 이는 Canvas.ForcedUpdateCanvases() 메서드를 사용하면 된다.
플레이어의 위치가 시작하자마자 생성되는 구조로 되어있기 때문에, 타이밍에 더 관련된 문제라고 생각해서 강제로 배치시키는 방법을 사용했다.
코드는 다음과 같다.
PuzzleGenerater.cs
//플레이어 스폰
public Player PlayerSpawn()
{
Player player = Instantiate(playerPrefab , board).GetComponent<Player>();
}
(타일 맵 생성 뿐 아니라, 플레이어의 생성도 PuzzleGenerater에게 일임하도록 함. 클래스의 이름의 의미랑 잘 맞는다고 생각해서)
PuzzleController.cs
private void PlayerSpawn(Tile startPos)
{
player = puzzleGenerator.PlayerSpawn();
//UI 강제 갱신
Canvas.ForceUpdateCanvases();
player.Initialize(startPos);
}
Player.cs
using Unity.VisualScripting;
using UnityEngine;
namespace Hamiltonian
{
public class Player : MonoBehaviour
{
RectTransform myTransform;
public void Initialize(Tile startTile)
{
myTransform = GetComponent<RectTransform>();
RectTransform startTransform = startTile.GetComponent<RectTransform>();
MoveToStartPos(startTransform);
}
private void MoveToStartPos(RectTransform start)
{
//myTransform.anchoredPosition = start.anchoredPosition; // 앵커 포지션
myTransform.position = start.position; // 로컬 포지션
}
private void Update()
{
//이동 로직 추가 구현
}
}
}
위 코드에서 앵커 포지션과 그냥 포지션을 사용했는데, 처음 설명했던 anchoredPosition과 로컬 position에 대한 설명에 대해 부연 설명을 하겠다.


Tile과 Player는 다른 부모의 오브젝트를 두고 있다. 그렇기 때문에 위에서 설명했듯이, 좌표의 변환이 필요하거나 그냥 position을 사용하면 해결이 된다.
그렇다면 Player를 Tile과 같은 부모로 넣으면 되는거 아니냐라고 할 수 있는데, 물론 그래도 된다. 하지만, 지금 내가 만드는 이 게임에서는 Tile의 부모라면 Tiles를 말하는 건데, 현재 Tiles는 Grid Layout Group 컴포넌트를 가지고 있어서, 하위로 들어가면 Player도 정렬이 되어버린다.
즉, 이동이 불가능한 상태가 되어버릴 것이다.
이런 연유로 position을 사용하여 다른 부모를 둔 상태로 위치를 동기화 시켜주었다.
플레이어의 이동
처음 Unity를 배울 때 가장 신기했던 부분 중 하나인 키 입력을 받아 오브젝트를 움직이게 하는 부분이다. (이제는 그러려니 하지만..)
플레이어의 이동을 가장 최적화한 방법들이 여럿 있지만, 챌린지의 목표에 따라 가장 기본적이고 원초적인 방법(Update에서 계속 키를 입력했는지 확인하는 방법)으로 플레이어의 이동을 구현해 보았다.
Player.cs
using Unity.VisualScripting;
using UnityEngine;
namespace Hamiltonian
{
public enum MoveDirection
{
Up,
Down,
Left,
Right
}
public class Player : MonoBehaviour
{
RectTransform myTransform;
[SerializeField]
PuzzleController puzzleController;
private void Update()
{
InptutMoveDirection();
}
public void Initialize(Tile startTile)
{
myTransform = GetComponent<RectTransform>();
puzzleController = GameObject.Find("@Controller").GetComponent<PuzzleController>();
RectTransform startTransform = startTile.GetComponent<RectTransform>();
MovePlayer(startTransform);
}
//특정 타일로 이동하는 함수
private void MovePlayer(RectTransform pos)
{
//myTransform.anchoredPosition = start.anchoredPosition;
myTransform.position = pos.position;
}
public void MovePlayer(Tile tile)
{
//myTransform.anchoredPosition = start.anchoredPosition;
myTransform.position = tile.gameObject.GetComponent<RectTransform>().position;
}
//입력 방향에 따라 플레이어를 이동시키는 함수
public void InptutMoveDirection()
{
if (Input.GetKeyDown(KeyCode.W))
{
Debug.Log("Up");
puzzleController.GetTilePosToMoveDirection(MoveDirection.Up);
}
else if (Input.GetKeyDown(KeyCode.S))
{
Debug.Log("Down");
puzzleController.GetTilePosToMoveDirection(MoveDirection.Down);
}
else if (Input.GetKeyDown(KeyCode.A))
{
Debug.Log("Left");
puzzleController.GetTilePosToMoveDirection(MoveDirection.Left);
}
else if (Input.GetKeyDown(KeyCode.D))
{
Debug.Log("Right");
puzzleController.GetTilePosToMoveDirection(MoveDirection.Right);
}
}
}
}
Player 클래스의 수정 사항을 정리하면 다음과 같다.
1. 플레이어의 키 입력에 따른 방향을 나타내는 Enum 추가 ( MoveDirection )
2. 시작 타일로 움직이는 함수를 수정하여 특정 타일로 이동하는 함수로 수정 (MoveToStartPos -> MovePlayer)
3. 입력 방향에 따라 플레이어를 이동시키는 함수 추가 ( InputMoveDirection )
PuzzleController.cs
using UnityEngine;
namespace Hamiltonian
{
public class PuzzleController : MonoBehaviour
{
[SerializeField]
private PuzzleGenerator puzzleGenerator;
[SerializeField]
private Player player;
private Vector2Int playerPos;
//타일들의 2D 배열 생성
private Tile[,] tiles;
//우선 인수로 건네줄 행과 열의 크기(임시)
[SerializeField]
private int rows = 5; //y, 행
[SerializeField]
private int cols = 5; //x, 열
void Start()
{
InitController();
GameStart();
}
private void InitController()
{
puzzleGenerator = GetComponent<PuzzleGenerator>();
}
public void GameStart()
{
puzzleGenerator = GetComponent<PuzzleGenerator>();
tiles = puzzleGenerator.GeneratePuzzle(rows, cols);
PlayerSpawn(SetAndGetStartTile());
}
//임시 함수
private Tile SetAndGetStartTile() //이름 수정
{
//임시로 (0,0) 타일을 시작 타일로 설정
//이후 데이터를 받아 시작 타일을 설정하도록 변경 필요
return tiles[0, 0];
}
public Tile GetTile(Vector2Int position) //범위 밖인지 확인 겸 쉽게 가져오기 위해서
{
if (position.x < 0 || position.x >= cols || position.y < 0 || position.y >= rows)
{
Debug.LogError($"{GetType()}: Tile Position out of bounds");
return null;
}
return tiles[position.x, position.y];
}
private void PlayerSpawn(Tile startPos)
{
player = puzzleGenerator.PlayerSpawn();
//UI 갱신
Canvas.ForceUpdateCanvases();
playerPos = startPos.gridPosition;
player.Initialize(startPos);
}
public void GetTilePosToMoveDirection(MoveDirection direction)
{
Tile target = null;
Vector2Int tempPos = playerPos;
//플레이어의 현재 위치 기준 방향에 위치한 타일의 좌표 구하기
switch (direction)
{
case MoveDirection.Up:
tempPos = new Vector2Int(playerPos.x, playerPos.y - 1);
break;
case MoveDirection.Down:
tempPos = new Vector2Int(playerPos.x, playerPos.y + 1);
break;
case MoveDirection.Left:
tempPos = new Vector2Int(playerPos.x - 1, playerPos.y);
break;
case MoveDirection.Right:
tempPos = new Vector2Int(playerPos.x + 1, playerPos.y);
break;
}
try //여기서 배열 밖의 인덱스에 접근하려고 하면 Exception이 발생하니
//try-catch문으로 검사
{
target = GetTile(tempPos);
}
catch(System.Exception e)
{
//catch에 걸린다면 플레이어가 타일 맵 바깥으로 이동하려고 하는 것이니 제어
Debug.LogError($"{GetType()}: GetTilePosToMoveDirection - Out of Bounds");
}
player.MovePlayer(target);
playerPos = tempPos; //마지막의 플레이어의 위치 최신화
}
}
}
PuzzleController 클래스의 수정 사항 및 추가된 내용을 정리하면 다음과 같다.
1. SetStartTile 메서드의 이름 변경 ( -> SetAndGetStartTile)
2. 범위 밖인지 체크 + 해당 타일의 위치 가져오기 위한 GetTile 메서드 추가
3. 이동하려는 타일의 위치를 가져오고, 해당 타일의 위치로 플레이어를 이동시키는 GetTilePosToMoveDirection 메서드 추가
여기서 try-catch문을 사용했는데, 이유는 플레이어가 타일 맵 밖으로 이동하려할 때 이를 막기 위해서 사용했다.
GetTile 메서드에서 만약, 가져오려는 타일의 위치가 범위 밖(2차원 배열에 존재하지 않는 인덱스로 접근)이라면 Null을 반환한다. 그냥 접근을 하게 두었다면, 이미 Exception(예외)이 발생해서 게임이 멈췄을 것이다.
GetTile 메서드의 반환 값을 target이라는 지역변수에 담았는데 여기서 두 가지의 방법이 있다.
if문으로 처리하거나, try문으로 Exception이 발생한 후에 처리를 하거나 둘 중 하나이다.
이 코드에서는 둘 중 아무거나 사용해도 상관없지만, "이렇게 처리해도 가능할까?"란 생각이 들어 시도를 해보았던 것이고, 테스트를 해보니 의도한 대로 결과가 잘 나왔다.
아쉽게도 영상은 찍는 것을 깜빡하는 바람에 플레이어의 이동 단계는 영상이 없다...
지나온 타일 표시
플레이어가 지나온 타일이 뭔지 표시하고, 지나온 타일로 이동하는 것을 막아보도록 하겠다.
이전에 했던 파이프 연결 게임을 참고하여 타일의 상태를 표시하는 코드를 만들었다.
Tile.cs
using UnityEngine;
using UnityEngine.UI;
namespace Hamiltonian
{
public enum TileType
{
Empty, //플레이어가 아직 안지나간 길
Fill, //플레이어가 지나간 길
Obstacle, //플레이어가 지나갈 수 없는 길
Start, //시작 지점
End //도착 지점
}
public class Tile : MonoBehaviour
{
[SerializeField]
private TileType tileType = TileType.Empty;
public TileType TileType
{
get => tileType;
set
{
tileType = value;
switch (tileType)
{
case TileType.Empty:
GetComponent<Image>().color = Color.white;
break;
case TileType.Fill:
GetComponent<Image>().color = Color.green;
break;
case TileType.Obstacle:
GetComponent<Image>().color = Color.black;
break;
case TileType.Start:
GetComponent<Image>().color = Color.blue;
break;
case TileType.End:
GetComponent<Image>().color = Color.red;
break;
}
}
}
public Vector2Int gridPosition;
public void Initialize(Vector2Int position)
{
gridPosition = position;
transform.position = new Vector3(position.x, 0, position.y);
}
}
}
타일의 상태를 추가했으니, 플레이어가 이동하려는 타일이 이동 가능한 타일인지 판단하는 단계( IsVaildMove() )를 추가하도록 한다.
PuzzleController.cs
public void GetTilePosToMoveDirection(MoveDirection direction)
{
Tile target = null;
Vector2Int tempPos = playerPos;
//플레이어의 현재 위치 기준 방향에 위치한 타일의 좌표 구하기
switch (direction)
{
case MoveDirection.Up:
tempPos = new Vector2Int(playerPos.x, playerPos.y - 1);
break;
case MoveDirection.Down:
tempPos = new Vector2Int(playerPos.x, playerPos.y + 1);
break;
case MoveDirection.Left:
tempPos = new Vector2Int(playerPos.x - 1, playerPos.y);
break;
case MoveDirection.Right:
tempPos = new Vector2Int(playerPos.x + 1, playerPos.y);
break;
}
//범위 밖이면 null이 반환되어 catch문으로 이동
try
{
target = GetTile(tempPos);
}
catch(System.Exception e)
{
Debug.LogError($"{GetType()}: GetTilePosToMoveDirection - Out of Bounds");
return;
}
if (IsValidMove(target))
{
player.MovePlayer(target);
playerPos = tempPos;
//target.TileType = TileType.Fill;
if (GetTile(playerPos).TileType == TileType.End) //이동한 타일이 End인지 확인
{
GameEnd();
}
else
{
target.TileType = TileType.Fill;
}
}
else
{
Debug.LogWarning($"{GetType()}: GetTilePosToMoveDirection - Invalid Move");
}
}
private bool IsValidMove(Tile targetTile)
{
if (targetTile == null || targetTile.TileType == TileType.Obstacle ||
targetTile.TileType == TileType.Fill || targetTile.TileType == TileType.Start)
{
return false;
}
return true;
}
테스트한 결과는 다음과 같다.
테스트 결과는 의도한대로 나왔다.
(우측에 로그가 뜨는 것은 잘 읽어보면 일부러 try-catch문을 이용해 잡은 null 관련 exception에 의해서 뜨는 것이라 무시해도 된다)
클리어 판정
이제는 마지막으로 프로토타입의 마무리인 클리어 판정 처리를 내리도록 하겠다.
처음에는 BFS / DFS를 이용해서 해결해야하나 생각을 했다. 하지만, 클리어 조건은 보드 타일 중에 Empty가 있으면 실패(false)가 나오면 된다. 그렇다면 현재 수준의 프로토타입을 구현하기 위해 굳이 복잡한 알고리즘을 사용하지 않아도, 해결할 방법이 있다는 것을 깨달았다.
단순하게 이중 for문을 이용해서 클리어 판정을 내렸다.
플레이어가 End 타일에 도달하면, 바로 2차원 배열을 순회하면서 Tile 중에 Empty가 있는지 확인한다. 만약, Empty가 있다면 실패처리를 하면 그만이다.
코드로 작성한 것은 다음과 같다.
PuzzleController.cs
private void GameEnd()
{
Debug.Log($"{GetType()}: Game Ended - Player reached the End Tile!");
if(IsGameClear())
{
Debug.Log($"{GetType()}: Game Cleared - All tiles filled!");
//게임 클리어 처리
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#else
Application.Quit();
#endif
}
else
{
Debug.LogWarning($"{GetType()}: Game Not Cleared - Some tiles are still empty.");
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
}
private bool IsGameClear()
{
for (int y = 0; y < rows; y++)
{
for (int x = 0; x < cols; x++)
{
if (tiles[x, y].TileType == TileType.Empty)
{
return false;
}
}
}
return true;
}
그리고 테스트 결과는.....
의도한 대로 잘 나왔다!
마무리
오늘 이렇게 한 붓 그리기 (해밀토니안 경로 퍼즐) 게임의 프로토타입을 만들어 봤다. 생각보다 짧고 별 것 없어 보이는데, 그게 맞다. 그렇게 프로토타입을 제작하면서 깊게 생각을 하진 않았다. 대충 골자만 잡히면 적당히 기회비용을 대보고 더 효율적이거나, 비용이 덜 들어가는 쪽을 선택했다.
만들고 나서 까먹었던 것들이 몇몇 보이기도 하고, 개선 사항도 몇가지 존재한다.
랜덤으로 스테이지가 생성되는 경우를 완벽히 배제하지는 않고 만들기는 했지만, 랜덤 스테이지가 필요한 경우(예를 들어 무한 모드라거나)에는 조금 수정을 해야할 것이다. 또한 추가해야할 로직(생성된 퍼즐이 해결 가능한지 판단 등)도 존재할 것이다.
(이런 경우에는 해밀토니안 경로 퍼즐이라는 수학 이론에 대해 좀 더 깊이 알아볼 필요가 있다.)
하지만, 이미 다른 장르의 게임을 만들 때 해당 요소들을 넣어서 구현한 경험이 있기에 어렵지 않을 것이라 생각한다. 일단은 기존의 목표대로 프로토타입을 완성 했다는 것에 의의를 두고 있다.
일단 다음 게임은 두 가지를 고민하고 있다. 첫 번째는 소코반(Sokoban) 퍼즐 게임으로, 상자 밀기 퍼즐이다. 두 번째로는 지뢰찾기(MineSweeper)이다.
상자 밀기 퍼즐은 완벽하게 랜덤으로 만들기에는 어려움이 있다. 하지만, 그렇다고 파이프 연결 게임처럼 완벽하게 에디터로 제어하고 싶은 마음도 없다. 그렇기에 현재로써는 어떻게 해야할지 좀 생각을 해야할 것 같다.
아직 정리되지 못한 자료들을 잘 다듬어서, 다음에는 지뢰찾기 게임으로 찾아오도록 하겠다.
'Game Develop > 소규모 프로젝트' 카테고리의 다른 글
| "하루만에 퍼즐 게임 만들기" 챌린지 (7일차) : 사과 게임(AppleSum) (1) | 2025.12.21 |
|---|---|
| "하루만에 퍼즐 게임 만들기" 챌린지 (5일차) : 카드 뒤집기(상) (0) | 2025.12.10 |
| "하루만에 퍼즐 게임 만들기" 챌린지 (4일차) : 파이프 연결 (Connect Pipe) - 에디터 만들기 [외전] (0) | 2025.12.01 |
| "하루만에 퍼즐 게임 만들기" (4일차) : 파이프 연결 (Connect Pipe) [하] (0) | 2025.11.23 |
| "하루만에 퍼즐 게임 만들기" (4일차) : 파이프 연결 (Connect Pipe) [중] (0) | 2025.11.23 |