유니티에서 데이터를 저장하는 클래스 중에 ScriptableObject 라는 것이 있다. 쉽게 말하면 코드로 된 템플릿인데, 
로그라이크 게임에서 캐릭터나 무기, 아이템 등의 데이터(예를 들면, 특정 무기의 공격력 계수는 120이고 장전 시간은 4초)를 각각의 인스턴스로 만들고자 할 때 사용한다. 

ScriptalbeObject는 프로젝트 안에 독립적인 .asset 파일로 개별 저장되기 때문에 객체 관리에 용이하다고 할 수 있다. 

using UnityEngine;

[CreateAssetMenu(menuName = "Data/Achievement")]
public class AchievementData : ScriptableObject
{
    public string title;
    public string description;
    public Sprite icon;
    public bool unlocked;
}

위 코드를 예시로 들어보자. 

[CreateAssetMenu(menuName = "Data/Achievement")]을 해당 클래스 위에 붙이면 유니티 에디터의 프로젝트 창에서 해당 데이터 객체를 바로 생성할 수 있다. 

예시로 item, weapon, achievement, stage... 등등을 만들어주었다.

이런 식으로 데이터를 생성해 줄 수 있다. 

해당 ScriptableObject 객체를 사용했을 때의 장점은

1. 데이터 중심 설계로 캐릭터나 무기 등 데이터를 코드 수정 없이 바로바로 추가가 가능하다는 점

2. 런타임 중에는 단일 인스턴스로 작동하여 메모리 절약이 가능하다는 점

3. 작업자가 직접 데이터를 편집할 수 있다는 점 

등이 있다.

AchievementManager을 예시로 만들어 해당 오브젝트를 활용한다면

public class AchievementManager : MonoBehaviour
{
    public AchievementData[] achievements;

    void Start()
    {
        foreach (var a in achievements)
        {
            Debug.Log($"업적: {a.title} / 달성여부: {a.unlocked}");
        }
    }
}

achievements 배열에 ScriptableObject 파일들을 드래그해서 연결하면 되고, 게임 실행 중 해당 데이터값에 접근하여 달성 여부 같은 변수의 상태 변경이 가능하다.

게임에 주로 사용되는 일부 객체 데이터들을 미리 하드코딩해 두고, 필요한 곳에 바로바로 사용이 가능하므로 조금 더 효율적인 작업이 가능하다.

지난 글에서 랜덤으로 맵을 구성해 보았는데, 이번에는 해당 맵을 랜덤하게 전개하여 하나의 던전을 만들어 보려고 한다. 우선 코드는 다음과 같다.

public class DungeonManager : MonoBehaviour
{
    public GameObject mapPrefab; // MapGenerator 프리팹

    private Dictionary<Vector2Int, MapGenerator> dungeonMaps = new Dictionary<Vector2Int, MapGenerator>();
    private Vector2Int currentMapPos = Vector2Int.zero;

    void Start()
    {
        // 시작 맵 생성
        createMap(Vector2Int.zero);
        loadMap(Vector2Int.zero);
    }

    void createMap(Vector2Int pos)
    {
        if (dungeonMaps.ContainsKey(pos)) return;

        GameObject newMapObj = Instantiate(mapPrefab, Vector3.zero, Quaternion.identity);
        MapGenerator generator = newMapObj.GetComponent<MapGenerator>();
        generator.GenerateMap(); 
        newMapObj.SetActive(false);

        dungeonMaps[pos] = generator;
    }

    public void loadMap(Vector2Int pos)
    {
        // 기존 맵 숨기기
        if (dungeonMaps.ContainsKey(currentMapPos))
        {
            dungeonMaps[currentMapPos].gameObject.SetActive(false);
        }

        // 새로운 맵 생성/활성화
        if (!dungeonMaps.ContainsKey(pos))
            createMap(pos);

        dungeonMaps[pos].gameObject.SetActive(true);
        currentMapPos = pos;
    }

    public void moveToMap(string direction) // 맵 이동하는 메소드
    {
        Vector2Int targetPos = currentMapPos;
        switch (direction)
        {
            case "North": targetPos += Vector2Int.up; break;
            case "South": targetPos += Vector2Int.down; break;
            case "East": targetPos += Vector2Int.right; break;
            case "West": targetPos += Vector2Int.left; break;
        }

        loadMap(targetPos);
    }
}

MapGenerator을 property로 받아서, GenerateMap 함수를 DungeonManager에서 실행하는 식이다.

처음 코드를 실행하면 시작 맵을 만들어준다. property의 MapGenerator을 복제하고, 인자로 받은 pos를 키로 하여 dungeonMaps dictionary에 해당 프리팹을 저장한다. 

void Start()
    {
        // 시작 맵 생성
        createMap(Vector2Int.zero);
        loadMap(Vector2Int.zero);
    }

    void createMap(Vector2Int pos)
    {
        if (dungeonMaps.ContainsKey(pos)) return;

        GameObject newMapObj = Instantiate(mapPrefab, Vector3.zero, Quaternion.identity);
        MapGenerator generator = newMapObj.GetComponent<MapGenerator>();
        generator.GenerateMap(); 
        newMapObj.SetActive(false);

        dungeonMaps[pos] = generator;
    }
    
    public void loadMap(Vector2Int pos)
    {
        // 기존 맵 숨기기
        if (dungeonMaps.ContainsKey(currentMapPos))
        {
            dungeonMaps[currentMapPos].gameObject.SetActive(false);
        }

        // 새로운 맵 생성/활성화
        if (!dungeonMaps.ContainsKey(pos))
            createMap(pos);

        dungeonMaps[pos].gameObject.SetActive(true);
        currentMapPos = pos;
    }

createMap을 통해 vector.zero 포지션에 최초로 사용될 맵 정보를 기록하고 loadMap을 통해 vector.zero 포지션의 맵을 활성화한다.

추후 다른 맵으로 이동 시, pos 값을 새로 받아오고 해당 pos값이 dungeonMaps에 없다면 새로 맵을 만들고 위의 플로우를 반복하면 된다. 

public void moveToMap(string direction) // 맵 이동하는 메소드
    {
        Vector2Int targetPos = currentMapPos;
        switch (direction)
        {
            case "North": targetPos += Vector2Int.up * 10; break;
            case "South": targetPos += Vector2Int.down * 10 break;
            case "East": targetPos += Vector2Int.right * 10; break;
            case "West": targetPos += Vector2Int.left * 10; break;
        }

        loadMap(targetPos);
    }

moveToMap 메소드를 통해 이동 처리를 구현했는데, 해당 메소드는 각 맵에서 특정 부분(입구)에 들어갈 경우 targetPos에서 해당 방향으로 벡터값을 추가해 맵을 동적으로 확장해 나가는 방식으로 구현될 수 있다.

던전을 생성하는 최초 시점에 던전을 모두 구현하고 싶다면, 최초 맵 생성 이후 맵의 모든 방향으로 해당 확장 처리를 수행하도록 하거나(이 경우 랜덤 횟수만큼 확장 처리를 반복할 수 있을 것이다.) 구조를 미리 정해두고 해당 구조를 따라서 미리 맵을 초기화하는 방식을 사용할 수 있을 것이다.

void Start()
    {
        // 시작 맵 생성
        createMap(Vector2Int.zero);
        loadMap(Vector2Int.zero);
        moveToMap("East");
        moveToMap("East");
        moveToMap("East");
    }

GenerateMap에 pos를 전달해 해당 위치에서 맵이 생성되도록 하고, 임의로 동쪽으로 맵을 3번 확장시킨 결과 (GenerateMap 함수에 임의로 좌표값을 전달해 보기 편하게 해두었다)

이런 식으로 맵을 확장이 가능했다. 각 맵마다 입구를 구현해 기존 코드를 실행하게 된다면 실제로는 같은 위치에서 맵이 여러개 생성되고, 이동 시마다 해당 pos의 맵의 active가 켜지는 식으로 구현된다.

 

로그라이크 스타일의 게임들에서 맵을 랜덤으로 이어붙여 던전을 구성하는 방식을 주로 사용하는데, 이를 간단하게나마 구현해보고자 하였다. 

맵 생성 플로우는 다음과 같다. 일반 바닥 타일을 정해진 크기(랜덤)로 배치하고, 주변에 통과 불가능한 벽을 설치한 후 랜덤한 장애물 타일 군집을 배치하는 식으로 코드를 구현해보았다. (맵 패턴 같은 경우 완전히 랜덤인 방식 이외에도, 특정 패턴을 설정해두고 해당 패턴들 중 랜덤으로 선택하여 맵을 구성하는 식도 나쁘지 않은 듯.) 

using System.Collections.Generic;
using UnityEngine;

public class MapGenerator : MonoBehaviour
{
    public int minSize = 30;
    public int maxSize = 30;

    public GameObject floorPrefab;
    public GameObject wallPrefab;
    public GameObject treePrefab;
    public GameObject rockPrefab;
    public GameObject waterPrefab;
    public GameObject voidPrefab;

    [Range(0f, 1f)] public float treeChance = 0.1f;
    [Range(0f, 1f)] public float rockChance = 0.07f;
    [Range(0f, 1f)] public float waterChance = 0.06f;
    [Range(0f, 1f)] public float voidChance = 0.05f;

    public int clusterMinSize = 3;  // 최소 군집 크기
    public int clusterMaxSize = 8;  // 최대 군집 크기

    private int width;
    private int height;
    private Transform mapParent;

    void Start()
    {
        GenerateMap();
    }

    void GenerateMap()
    {
        width = Random.Range(minSize, maxSize + 1);
        height = Random.Range(minSize, maxSize + 1);

        mapParent = new GameObject("Map").transform; // 맵 캔버스 생성

        // 1. 바닥 전체 깔기
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                Instantiate(floorPrefab, new Vector2(x, y), Quaternion.identity, mapParent);
            }
        }

        // 2. 벽 생성
        for (int x = 0; x < width; x++)
        {
            Instantiate(wallPrefab, new Vector2(x, 0), Quaternion.identity, mapParent);
            Instantiate(wallPrefab, new Vector2(x, height - 1), Quaternion.identity, mapParent);
        }
        for (int y = 0; y < height; y++)
        {
            Instantiate(wallPrefab, new Vector2(0, y), Quaternion.identity, mapParent);
            Instantiate(wallPrefab, new Vector2(width - 1, y), Quaternion.identity, mapParent);
        }

        // 3. 랜덤 군집 배치
        trySpawnClusters(treePrefab, treeChance);
        trySpawnClusters(rockPrefab, rockChance);
        trySpawnClusters(waterPrefab, waterChance);
        trySpawnClusters(voidPrefab, voidChance);

        // 4. 출입구 생성
        createEntrance(width / 2, 0);            
        createEntrance(width / 2, height - 1);   
        createEntrance(0, height / 2);           
        createEntrance(width - 1, height / 2);  
    }

    void trySpawnClusters(GameObject prefab, float chance)
    {
        int clusterCount = Mathf.RoundToInt(width * height * chance / clusterMaxSize);

        for (int i = 0; i < clusterCount; i++)
        {
            int clusterSize = Random.Range(clusterMinSize, clusterMaxSize + 1);
            int startX = Random.Range(1, width - 1);
            int startY = Random.Range(1, height - 1);

            Vector2Int pos = new Vector2Int(startX, startY);
            HashSet<Vector2Int> clusterTiles = new HashSet<Vector2Int> { pos };

            for (int j = 1; j < clusterSize; j++)
            {
                // 주변 타일 확장
                List<Vector2Int> neighbors = new List<Vector2Int>(clusterTiles);
                Vector2Int baseTile = neighbors[Random.Range(0, neighbors.Count)];
                Vector2Int newTile = baseTile + RandomDirection();

                if (newTile.x > 0 && newTile.x < width - 1 && newTile.y > 0 && newTile.y < height - 1)
                {
                    clusterTiles.Add(newTile);
                }
            }

            // 프리팹 배치
            foreach (var tile in clusterTiles)
            {
                Instantiate(prefab, new Vector2(tile.x, tile.y), Quaternion.identity, mapParent);
            }
        }
    }

    Vector2Int randomDirection()
    {
        Vector2Int[] dirs = { Vector2Int.up, Vector2Int.down, Vector2Int.left, Vector2Int.right };
        return dirs[Random.Range(0, dirs.Length)];
    }

    void createEntrance(int x, int y)
    {
        // 출입구는 벽 대신 바닥으로 열어줌
        Collider2D col = Physics2D.OverlapPoint(new Vector2(x, y));
        if (col != null) Destroy(col.gameObject);

        Instantiate(floorPrefab, new Vector2(x, y), Quaternion.identity, mapParent);
    }
}

 

우선 GenerateMap 함수를 통해 기본 맵을 생성해준다. width 와 height는 랜덤으로 정하고, 해당 값들을 바탕으로 바닥 프리팹들을 isntantiate한다. 이후 같은 방식으로 벽 프리팹을 모서리 부분에 생성한다.

// 1. 바닥 전체 깔기
for (int x = 0; x < width; x++)
{
    for (int y = 0; y < height; y++)
    {
        Instantiate(floorPrefab, new Vector2(x, y), Quaternion.identity, mapParent);
    }
}

// 2. 벽 생성
for (int x = 0; x < width; x++)
{
    Instantiate(wallPrefab, new Vector2(x, 0), Quaternion.identity, mapParent);
    Instantiate(wallPrefab, new Vector2(x, height - 1), Quaternion.identity, mapParent);
}
for (int y = 0; y < height; y++)
{
    Instantiate(wallPrefab, new Vector2(0, y), Quaternion.identity, mapParent);
    Instantiate(wallPrefab, new Vector2(width - 1, y), Quaternion.identity, mapParent);
}

 

다음으로 장애물들을 생성하는 과정이다. 특정 오브젝트 프리팹과 확률을 받아서 생성할 군집의 갯수를 구하고, 랜덤한 사이즈와 좌표를 계산하여 해당 좌표에서 장애물 배치를 시작하는 식이다.

HashSet<Vector2Int> clusterTiles 에 첫 위치를 저장하고, 주변으로 정해진 사이즈만큼 좌표를 확장해 나가며 clusterTiles에 저장한다. 확장되는 방향은 RandomDirection() 메소드를 통해 상하좌우 랜덤으로 반환되고, 맵을 벗어난 경우 저장하지 않는다. 사이즈만큼 확장이 완료된 경우 clusterTiles에 저장된 좌표에 따라 프리팹을 생성한다.

void trySpawnClusters(GameObject prefab, float chance)
{
    int clusterCount = Mathf.RoundToInt(width * height * chance / clusterMaxSize);

    for (int i = 0; i < clusterCount; i++)
    {
        int clusterSize = Random.Range(clusterMinSize, clusterMaxSize + 1);
        int startX = Random.Range(1, width - 1);
        int startY = Random.Range(1, height - 1);

        Vector2Int pos = new Vector2Int(startX, startY);
        HashSet<Vector2Int> clusterTiles = new HashSet<Vector2Int> { pos };

        for (int j = 1; j < clusterSize; j++)
        {
            // 주변 타일 확장
            List<Vector2Int> neighbors = new List<Vector2Int>(clusterTiles);
            Vector2Int baseTile = neighbors[Random.Range(0, neighbors.Count)];
            Vector2Int newTile = baseTile + randomDirection();

            if (newTile.x > 0 && newTile.x < width - 1 && newTile.y > 0 && newTile.y < height - 1)
            {
                clusterTiles.Add(newTile);
            }
        }

        // 프리팹 배치
        foreach (var tile in clusterTiles)
        {
            Instantiate(prefab, new Vector2(tile.x, tile.y), Quaternion.identity, mapParent);
        }
    }
}

Vector2Int randomDirection()
{
    Vector2Int[] dirs = { Vector2Int.up, Vector2Int.down, Vector2Int.left, Vector2Int.right };
    return dirs[Random.Range(0, dirs.Length)];
}

이 작업을 다른 장애물 프리팹들에도 똑같이 수행한다. 이렇게 되었을 경우 장애물끼리 겹치는 문제가 발생할 수 있는데, 이를 방지하고자 할 경우, HashSet<Vector2Int> clusterTiles을 전역 변수로 설정하고 장애물을 배치하는 모든 값을 저장하면 된다. 중복되는 값이 있는 경우에는 저장하지 않도록 변경해 보았다. (clusterTiles는 다른 함수에서 꼭 선언해주어야 한다.)

private HashSet<Vector2Int> clusterTiles;

void trySpawnClusters(GameObject prefab, float chance)
{
    int clusterCount = Mathf.RoundToInt(width * height * chance / clusterMaxSize);

    for (int i = 0; i < clusterCount; i++)
    {
        int clusterSize = Random.Range(clusterMinSize, clusterMaxSize + 1);
        int startX = Random.Range(1, width - 1);
        int startY = Random.Range(1, height - 1);

        Vector2Int pos = new Vector2Int(startX, startY);
        HashSet<Vector2Int> tempClusterTiles = new HashSet<Vector2Int>();


        if (clusterTiles.Contains(pos) == true) // 기존 cluster 좌표와 중복되는 경우 중복되지 않는 좌표를 다시 찾는다.
        {
            pos = checkOverlaped(pos);
        }

        clusterTiles.Add(pos);
        tempClusterTiles.Add(pos);

        for (int j = 1; j < clusterSize; j++)
        {
            // 주변 타일 확장
            List<Vector2Int> neighbors = new List<Vector2Int>(clusterTiles);
            Vector2Int baseTile = neighbors[Random.Range(0, neighbors.Count)];
            Vector2Int newTile = baseTile + randomDirection();

            if (newTile.x > 0 && newTile.x < width - 1 && newTile.y > 0 && newTile.y < height - 1)
            {
                if(clusterTiles.Contains(newTile) == true) // 기존 cluster 좌표와 중복되는 경우 건너뜀.
                {
                    continue;
                }
                clusterTiles.Add(newTile);
                tempClusterTiles.Add(newTile);
            }
        }

        // 프리팹 배치
        foreach (var tile in tempClusterTiles)
        {
            Instantiate(prefab, new Vector2(tile.x, tile.y), Quaternion.identity, mapParent);
        }
    }
}

Vector2Int checkOverlaped(Vector2Int pos)
{

    if (clusterTiles.Contains(pos) == true)
    {
        int clusterSize = Random.Range(clusterMinSize, clusterMaxSize + 1);
        int startX = Random.Range(1, width - 1);
        int startY = Random.Range(1, height - 1);

        Vector2Int posNew = new Vector2Int(startX, startY); // 새로 좌표를 정하고 다시 검사함.
        return checkOverlaped(posNew);
    }
    return pos;
}

checkOverlaped 함수는 인자로 pos를 받아서 해당 좌표가 기존 clusterTile 좌표와 중복되는지 확인하고, 중복되는 경우 시작점을 다시 뽑아서 반환하는 함수이다. 이렇게 변경하고, 확장되는 타일도 clusterTile 좌표와 중복되는 경우 추가하지 않는 식으로 수정하면 중복을 피할 수 있을 것이다. 

마지막으로 동서남북에 입구를 뚫는 과정이다.

// 4. 출입구 생성
createEntrance(width / 2, 0);            // 남쪽
createEntrance(width / 2, height - 1);   // 북쪽
createEntrance(0, height / 2);           // 서쪽
createEntrance(width - 1, height / 2);   // 동쪽


void createEntrance(int x, int y)
{
    // 출입구는 벽 대신 바닥으로 열어줌
    Collider2D col = Physics2D.OverlapPoint(new Vector2(x, y));
    if (col != null) Destroy(col.gameObject);

    Instantiate(floorPrefab, new Vector2(x, y), Quaternion.identity, mapParent);
}

해당 좌표에 collider이 있어 통과가 안되는 경우, floorPrefab (또는 문 프리팹)을 배치하여 준다. 

위 프리팹들로 실험해 본 결과

이렇게 실행 때 마다 랜덤으로 맵을 생성할 수 있게 되었다.

다음 포스팅에서는 해당 맵들을 연결하여 하나의 큰 던전을 만드는 것을 구현해 보도록 하겠다.

Cocos2D를 설치하고 프로젝트를 만들어 실행해보자.

1. 파이썬 설치

우선 파이썬을 설치해야 한다. cocos2D-X는 파이썬 2.x 버전을 사용하여야 하며, 3.x 와 2.x버전의 파이썬은 크게 다르므로 3.x 버전을 사용할 시 터미널에서 raw input을 인식하지 못한다는 오류가 발생하게 된다.

https://www.python.org/downloads/release/python-2715/

 

Python Release Python 2.7.15

The official home of the Python Programming Language

www.python.org

필자는 2.7.15 버전을 사용하였다.

설치가 완료되면 환경 변수 설정을 해 주어야 한다. 

제어판 -> 시스템 및 보안 -> 시스템 

-> 고급 시스템 설정 -> 고급 -> 환경 변수 

로 들어가 사용자 변수와 시스템 변수 둘 다의 path를 편집하여 파이썬 2.x 버전이 설치된 경로를 추가해 주어야 한다.

파이썬 2.7의 경우 c:\Python27 에 설치된다.

설치 후 터미널에서 python을 입력해 2.x버전이 설치되었는지 확인하길 바란다.

2. cocos2d-x 다운로드

https://www.cocos.com/en/cocos2dx-download

 

Cocos - The world's top 2D&3D engine, game / smart cockpit /AR/VR/ virtual character / education

The world's top lightweight, efficient, cross-platform digital content development platform can meet different development needs for 3D, 2D, AR&VR and other unique content creation, and can provide complete solutions in frontier fields such as smart cockpi

www.cocos.com

cocos creator과 다르다.

cocos2dx는 3.x버전이 문서가 가장 많다고 하여 3.17.2로 진행하였다. 파일을 다운로드하고 압축을 해제한 뒤 파일 내의 setup.py를 실행한다.

 

이것을 파이썬으로 실행해 주면 터미널로 진입한다. 

ndk는 안드로이드 ndk를 말하는 것으로 안드로이드 개발 시 필요한 패키지인데 추후 필요할 경우 설치하면 된다. 지금은 기본적인 프로젝트를 위해 엔터를 눌러 스킵해준다.

과정이 끝나고 pc를 재부팅하면

사용자 변수에 COCOS_CONSOL...등 변수가 추가된다. 이 과정 후에도 터미널에 cocos를 입력하여 제대로 설치되었는지 확인하여야 한다.

제대로 설치되었다면 위처럼 나오게 된다. 

3. 프로젝트 생성

터미널에서 [cd 경로] 를 입력해 해당 경로로 이동하거나 원하는 경로에서 터미널을 열고, 새로운 프로젝트를 생성해 줄 것이다. 필자는 cpp으로 진행하였다.

cocos new [프로젝트 이름] -p [패키지 이름] -l [언어] 를 입력하면 정해진 경로에 프로젝트 폴더가 생성된다.

만들어진 폴더 내의 proj.win32로 진입하면 

이런식으로 파일들이 생성되어 있는 것을 확인할 수 있다. sln (솔루션)파일을 열고 확인을 눌러준 뒤 디버그를 진행하면

cocos2dx가 실행된다. 

1052번: 물병 (acmicpc.net)

 

1052번: 물병

지민이는 N개의 물병을 가지고 있다. 각 물병에는 물을 무한대로 부을 수 있다. 처음에 모든 물병에는 물이 1리터씩 들어있다. 지민이는 이 물병을 또 다른 장소로 옮기려고 한다. 지민이는 한 번

www.acmicpc.net

1리터의 물이 들어있는 n개의 물병을 이용해 k개를 넘지 않는 비어있지 않은 물병을 만드는 문제이다. k개 이하의 물병을 만들기 위해서 상점에서 추가적으로 1리터의 물이 들어있는 n개의 물병을 사올 수 있는데, 이때 최소한으로 사오는 갯수를 구하여야 한다.

물은 아래와 같이 재분배한다.

먼저 같은 양의 물이 들어있는 물병 두 개를 고른다. 그 다음에 한 개의 물병에 다른 한 쪽에 있는 물을 모두 붓는다. 이 방법을 필요한 만큼 계속 한다.

.

#include <iostream>

using namespace std;

int test(int a) {
	int b = 0;
	while (1) {
		if (a == 1) {
			b++;
			break;
		}
		if (a % 2 == 1) {
			a--;
			b++;
		}
		else {
			a = a / 2;
		}
	}
	return b;
}

int main() {
	int n, k,a=0,b=1;
	cin >> n >> k;
	if (k >= n) {
		cout << 0;
	}
	else {
		while (1) {
			if (test(n)<=k) {
				break;
			}
			if (n % 2 == 0) {
				n = n / 2;
				b = b * 2;
			}
			else {
				n = n + 1;
				a = a + b;
			}
			
		}
		cout << a;
	}
}

 

나의 접근 방식은, 우선 같은 양의 물을 합칠 수 있을 만큼 합친 후, 홀수개가 되어 물병이 남게 될 경우 상점에서 추가로 물병을 사와 짝수개로 만들어주어 다시 합치는 방식으로 구현하였다. 예를 들어 100개의 물병이 있을 때 25개까지 합칠 수 있는데, 이 25개의 물병 각각의 물 양은 4리터가 될 것이다. 따라서 물병을 26개로 만들기 위해 상점에서 1리터 *4개의 물병을 사왔다.  이 과정에서 2,4,8,16,... 등 2의 제곱수만큼의 물병이 사올 때 마다 더해지게 된다.

루프 중 합쳐진 물병의 개수가 주어진 k개 이하로 나누어질 수 있는 경우 (이는 test함수를 구현하여 확인함. test의 경우 n을 2로 나눌 수 있을 만큼 나누고, 홀수가 된 경우 물병 하나를 빼는것과 동일하게 n에 -1을 해주었다. 최종적으로 n을 1까지 나누면 빼둔 물병에 n+1을 해주면 n개 물병을 합친 최솟값을 구할 수 있다.) 루프를 탈출하고 최종적으로 값을 출력하면 된다.

아이디어


개구리가 점프할 때 힘을 모아 한번에 멀리 점프하는 것을 보고 게임 진행 방식을 떠올림.

과정


개발 언어 : 파이썬

추후 유니티로 개발할 게임의 프로토타입으로 개발하였음.

아래는 원본 코드

froggame.py
0.02MB

 

게임 디자인과 구조

우선 게임을 시작하기 전 따로 바탕화면을 만들어서 play 버튼을 눌러 게임을 시작할 수 있도록 하였습니다.

조작할 키는 스페이스 바 뿐이기에, 게임이 시작되면 게임 내에 메시지를 띄워 간단한 조작법을 알려주었습니다. 스페이스바를 꾹 누르면 아래 게이지가 충전됩니다.

기본 땅과 공중에 떠있는 땅 두가지와 장애물들을 통해 스테이지를 구성했고, 보라색의 장애물에 닿으면 캐릭터가 일정 거리만큼 뒤로 밀려납니다. 점프하는 순간과 장애물에 닿는 순간에는 효과음을 추가해 주었습니다.

중간에 비어 있는 구멍으로 떨어지면 아래에 용암이 기다리고 있고, 이 용암에 닿으면 Game Over 가 화면 중앙에 표시됩니다. 이 경우 Restart 버튼을 눌러 게임을 다시 시작할 수 있도록 하였습니다.

모든 장애물을 통과한 뒤 결승 지점까지 도달하면, 화면 중간에 Clear!!!을 표시하고 움직임을 멈추게 됩니다.

2022년 1학년 1학기 여름방학 때 과 동기 5명과 모여 참가하였습니다.

복셀 에디터로 직접 랜드마크 건물들을 만들고, 이 건물들을 이용해 Unity에서 직접 미래의 서울시를 구현하는 과제를 맡았습니다.

아래는 당시 본인이 제작했던 건물들입니다.

 

 

시연영상 유튜브 링크 : https://youtu.be/5oRRIJMuRW4

깃허브 : 

프로젝트명 : Dignite

개발 기간 : 13일

Unity 엔진을 통해 개발하였음.

게임 소개 : '차원 변환 3D 플랫폼 퍼즐 게임'

아이디어


  • 슈퍼마리오와 같은 2d 플랫폼 게임에서 2.5d로 바뀌는 기믹을 생각해 내었고, 특정 장치를 통해 중력을 바꾸어 평소에 가지 못하던 곳에 갈 수 있도록 설계함.

조작법


이동: 방향키

점프: 스페이스 바

상호작용: F 키

게임 특징


  • 스토리 애니메이션 존재

게임 시작 시 1회에 한해 스토리와 함께 맵 전체를 보여줌

  • 세 가지 테마의 맵 (각 테마별 맵 4~5개 존재)
  1. Forest: 기본 테마로, 특별한 요소 없이 게임에 적응하는 단계이다.

간단한 퍼즐 위주이며, 게임에 적응할 수 있는 단계

    2. Winter: 두번째 테마로, 눈이 내려서 바닥이 미끄럽다는 특징이 있다.

컨트롤 실력을 조금 더 요구하는 맵

    3. Dungeon: 마왕에게 도달하기 직전의 테마로, 유령이 소환되어 플레이어를 쫓아간다.

높은 컨트롤 실력 뿐 아니라 동선을 효율적으로 잘 구성해야 클리어가 가능한 맵.

  • 차원을 바꿔주는 불
  1. 빨간불: 차원을 3D로 전환한다.
  2. 파란불: 차원을 2D로 전환한다.
  • 클리어 방법

장애물을 피하며 마왕이 만든 상자를 모두 열고, 골인 지점이 요구하는 차원의 상태로 골인 지점으로 가서 상호작용

(마왕이 만든 상자는 2D 상태에서만 열 수 있다.)

  • 레벨 선택 공간

레벨을 직접 선택하는 방식으로, 아직 클리어하지 못한 맵이 있다면 더 이상 나아가지 못한다.

이미 클리어한 맵도 다시 플레이가 가능하며, 모두 클리어 시 1회에 한해 엔딩 크레딧 이벤트 발생.

맵을 클리어하거나 중간에 다시 나오면 마지막에 들어간 맵에서 시작하도록 구현

  • 게임 시작 시 페이드 아웃 및 접근성 UI 구현, 배경음악과 효과 사운드 전체 구현, 자동 세이브 로드 기능 구현
  • 게임의 전체적인 시스템 구현에 초점을 두어 실제 완성된 콘솔 게임을 즐기는 것과 같은 경험을 제공하고자 하였음.

팀원 : 박형준(경희대 22, 본인), 김수연(경희대 22)

깃허브 링크 : https://github.com/spoiuy3/PuzzleGame

데모영상 유튜브 링크 : https://www.youtube.com/watch?v=dlWiOy2vUEU

+ Recent posts