//구글콘솔 광고 추가가
728x90
반응형

바로 다음날 작업한 걸 정리하려던 나는 내일 할까 미뤘다가 파일정리를 하면서 Shift + Delete로 프로젝트를 삭제시키는 끔찍한 실수를 저질렀다. 잠시 잠깐 멘붕에 빠져있다가 PuranUtilitiesSetup이라는 복원프로그램을 사용해 봤다.(이 프로그램은 공짜로 풀린 복원 프로그램인 것 같은데 안타깝게도 cs파일은 찾지 못하는 것 같았다. 텍스트 파일이나 사진파일 같은 건 바로 찾을 수 있는 것 같았음.)

https://nomatter-me.tistory.com/209

 

이것 저것 ) 무료 복원 프로그램 Puran Utilities를 아는가.

Puran Utilities 프로그램을 알게 되었다. 복원 프로그램인데 무료로 이용이 가능하다. 현재의 내 경우에는 살리지 못했지만 언젠가 혹시 모를 참담하고도 참담할 상황인, 중요 문서를 쉬프트 딜리

nomatter-me.tistory.com

 

나의 경우는 파일을 찾지도 못했을 뿐더러 중간에 갑자기 노트북이 업데이트되는 해프닝이 있었다. 업데이트가 되길래 약간은 불안했다만 "그저 기우겠지."란 생각으로 더 이상 생각하지 않기로 했다. 그렇게 노트북이 힘겨워하기에 그만두고 다시 구현하는 쪽을 선택해서 지금 다시 올린다. 그래도 한번 만들어 봤다고 시간이 엄청 투자되지는 않아서 다행이었다.

using UnityEngine;
using System.Collections.Generic;

public class ObstacleManager : MonoBehaviour
{
    // 도로의 전체 너비와 카메라 높이 설정
    [Header("Road Settings")]
    public int roadWidth = 1;
    public float cameraHeight = 1000;
    public float speed = 0.8f;
    
    [Header("Obstacle Settings")]
    public GameObject[] obstaclePrefabs;
    [SerializeField] private float obstacleSpeed = 0.8f;
    [SerializeField] private float spawnY = 3.16f;
    [SerializeField] private float endYPosition = -6f;
    [SerializeField] private int poolSize = 5;
    [SerializeField] private int maxObstacles = 3; // 동시에 존재할수 있는 최대 장애물 수
    [SerializeField] private float minObstacleSpacing = 0.5f; // 장애물 X간 최소 간격
    [SerializeField] private float heightCheck = 1f; // 장애물 Y간 최소 간격
    [SerializeField] private int consecutiveNum = 3; //연속 장애물 허용 범위
    [SerializeField] private float spawnRate = 0.02f;
    
    private Queue<GameObject>[] obstaclePools;
    private List<GameObject> activeObstacles = new List<GameObject>();
    private int consecutiveSpawnCount = 0;    // 연속 생성 횟수
    private bool? lastSpawnSide = null;       // 마지막 생성 방향 (null: 초기상태)

    private float spawnTimer;
    private readonly Vector3 scaleVector =Vector3.one;
    private void Awake()
    {
        activeObstacles = new List<GameObject>(maxObstacles);
        InitializeObstaclePools();
    }

    private void InitializeObstaclePools()
    {
        obstaclePools = new Queue<GameObject>[obstaclePrefabs.Length];
    
        for (int i = 0; i < obstaclePrefabs.Length; i++)
        {
            obstaclePools[i] = new Queue<GameObject>();
            for (int j = 0; j < poolSize; j++)
            {
                CreateObstacles(i);
            }
        }
    }

    private void CreateObstacles(int typeIndex)
    {
        GameObject obj = Instantiate(obstaclePrefabs[typeIndex], transform);
        obj.SetActive(false);
        obj.AddComponent<ObstacleData>();
        obstaclePools[typeIndex].Enqueue(obj);
    }

    private void Update()
    {
        spawnTimer += Time.deltaTime;
        if (spawnTimer >= spawnRate)
        {
            spawnTimer = 0f;
            if (Random.value < spawnRate)
            {
                SpawnObstacle();
            }
        }
        UpdateObstacles();
    }

    private void UpdateObstacles()
    {
        Vector3 position;
        float progress, scale, xOffset, newX, newY;
        
        for (int i = activeObstacles.Count - 1; i >= 0; i--)
        {
            GameObject obstacle = activeObstacles[i];
            ObstacleData data = obstacle.GetComponent<ObstacleData>();
            position = obstacle.transform.position;
            
            // 장애물의 진행도 계산 (0~1 사이 값)
            progress = (spawnY - position.y) / (spawnY - (transform.position.y + endYPosition));
            // 진행도에 따른 크기 보간
            scale = Mathf.Lerp(0.1f, 3f, progress);
            // 바깥쪽으로 이동하는 힘을 더 강하게 수정
            xOffset = data.isLeftSide ? -progress * 4f : progress * 4f;
            newX = data.initialX + (xOffset * roadWidth);
            // 스케일이 커질수록 더 빠르게 이동 //scale 0.1_기본속도 1, scale 1.5_기본속도 4.5, scale 최대_기본속도 8.25
            newY = position.y - (obstacleSpeed * (1f + (scale - 0.1f) * 2.5f) * Time.deltaTime);
            
            position.x = newX;  
            position.y = newY;
            obstacle.transform.position = position;
            obstacle.transform.localScale = scaleVector * scale;

            if (newY < transform.position.y + endYPosition)
            {
                ReturnToPool(obstacle);
                activeObstacles.RemoveAt(i);
            }
        }
    }
    

    public class ObstacleData : MonoBehaviour // 장애물의 초기 x위치와 방향을 저장하는 컴포넌트
    {
        public float initialX;
        public bool isLeftSide;
    }
    
    private bool IsValidSpawnPosition(Vector3 newPosition)
    {
        
        foreach (GameObject obstacle in activeObstacles)
        {
            // y축 간격이 heightCheck의 간격보다 작은지 체크
            if (Mathf.Abs(obstacle.transform.position.y - newPosition.y) < heightCheck)
            {
                // x축 간격이 최소 간격보다 작으면 생성 불가
                if (Mathf.Abs(obstacle.transform.position.x - newPosition.x) < minObstacleSpacing)
                {
                    return false;
                }
            }
        }
        return true;
    }

    private void SpawnObstacle()
    {
        if (activeObstacles.Count >= maxObstacles)
        {
            return;
        }

        //50% 확률로 장애물 왼쪽, 오른쪽으로 스폰
        bool spawnOnLeft = Random.value > 0.5f;
        HandleConsecutiveSpawns(ref spawnOnLeft);

        int typeIndex = Random.Range(0, obstaclePrefabs.Length);
        GameObject obstacle = GetObstacleFromPool(typeIndex);

        if (obstacle == null)
        {
            return;
        }

        // 초기 생성 위치를 도로 중앙 근처로 수정 //0.1 수치 증가시키면 장애물이 도로의 바깥쪽 생성,감소 >>중앙에 가깝게 생성.
        float xPos = spawnOnLeft ? -roadWidth * 0.1f : roadWidth * 0.1f;
        Vector3 newPosition = new Vector3(xPos, spawnY, 0);

        // 새로운 위치가 적절한 간격을 유지하는지 확인
        if (!IsValidSpawnPosition(newPosition))
        {
            ReturnToPool(obstacle);
            return;
        }

        SetupObstacle(obstacle, xPos, spawnOnLeft);
    }

    private void HandleConsecutiveSpawns(ref bool spawnOnLeft) //연속으로 같은 방향에 장애물이 생성되는 것을 제어
    {
        // 이전과 같은 방향으로 생성하려는 경우
        if (lastSpawnSide.HasValue && spawnOnLeft == lastSpawnSide.Value)
        {
            consecutiveSpawnCount++;
            // consecutiveNum번 연속 생성되면 반대편에 생성
            if (consecutiveSpawnCount >= consecutiveNum)
            {
                spawnOnLeft = !lastSpawnSide.Value;
                consecutiveSpawnCount = 0;
            }
        }
        else
        {
            consecutiveSpawnCount = 0;
        }
        lastSpawnSide =spawnOnLeft;
    }

    private void SetupObstacle(GameObject obstacle, float xPos, bool isLeft) // 생성된 장애물의 초기 설정을 담당
    {
        obstacle.transform.position = new Vector3(xPos, spawnY, 0);
        obstacle.transform.localScale = scaleVector * 0.1f;

        var data = obstacle.GetComponent<ObstacleData>();
        data.initialX = xPos;
        data.isLeftSide = isLeft;
        
        obstacle.SetActive(true);
        activeObstacles.Add(obstacle);
    }

    private void ReturnToPool(GameObject obstacle)
    {
        obstacle.SetActive(false);
        for (int i = 0; i < obstaclePrefabs.Length; i++)
        {
            if (obstacle.name.Contains(obstaclePrefabs[i].name))
            {
                obstaclePools[i].Enqueue(obstacle);
                break;
            }
        }
    }

    private GameObject GetObstacleFromPool(int typeIndex)
    {
        return obstaclePools[typeIndex].Count > 0 ? obstaclePools[typeIndex].Dequeue() : null;
    }
}

 

장애물은 왼쪽과 오른쪽에 생성되는 장애물에 따라 도로 벽 쪽으로 붙어서 떨어지게 작업했다. 

만들다 보니까 겹쳐서 생성될 때도 있길래 최소 x, y값의 간격을 정해서 생성되게 작업했다. 그렇게 만들었더니 간격은 마음에 드는데 뜸하게 만들어지는 것 같기도 하고 이건 수치 조절하면 될 것 같은데 그 적정한 선을 찾기가 쉽지가 않구만.

장애물이 연속해서 나오는 것도 별로길래 몇 개 같은 쪽으로 나오면 다시 만들어질 때는 다른 쪽으로 만들어지게 만들어 줬다.

주석처리를 많이 해두어서 코드를 보는 데는 문제가 없을 듯하다.

하지만 솔직하게 이 코드는 pseudo 3d game라 할 수 없다. 그래서 시간이 될 때 다시 도전해 보는 걸로 마무리 지었다. 

 

https://github.com/h8man/TurboTrack2D

 

GitHub - h8man/TurboTrack2D: Prototype of 2D arcade style racing game

Prototype of 2D arcade style racing game. Contribute to h8man/TurboTrack2D development by creating an account on GitHub.

github.com

여기 있는 프로젝트는 누군가가 만들어둔 pseudo 3d game 유니티 프로젝트이다. 

이 코드를 좀 뜯어보면서 다시 공부를 해봐야겠다.

728x90
반응형
728x90
반응형

Puran Utilities 프로그램을 알게 되었다. 복원 프로그램인데 무료로 이용이 가능하다. 현재의 내 경우에는 살리지 못했지만 언젠가 혹시 모를 참담하고도 참담할 상황인, 중요 문서를 쉬프트 딜리트로 지우는 만행을 저지를 수도 있는 나를 위해 남겨둔다. 

 

2016년 이후로 업데이트가 되고 있지 않는 프로그램이라 전부를 찾는 것은 불가능해 보여도 찾고자 하는 파일의 확장자를 추가시켜 두면 찾을 수도 있을 것 같은 부분은 발견했다. 문서나 사진, 동영상 등은 복원이 가능한 것 같다. 

 

PuranUtilitiesSetup.exe
9.63MB

 

일단 프로그램을 다운 받아서 열면 저 화면이 먼저 나오는데 ok를 눌러준다.

 

 

현재 상황이 중요 파일을 쉬프트 딜리트로 지웠거나, 휴지통에서 파일을 지웠을 때 복원시키고 싶은 거라면 File Recovery를 눌러주자.

옆에 있는 Data Recovery는 안열리는 파일을 복원시키는 기능인 것 같았다.

 

 

더블클릭하면 나오는 언어선택창에선 한국어를 지원하지 않으니 영어로  OK 해주자.

 

 

영어로 설명이 나오는데 우리는 읽어볼 필요 없이 OK 버튼 누르면 된다. 

 

 

삭제시킨 파일이 있던 장소를 선택해 주고 그냥 scan 버튼을 눌러서 스캔해도 되지만 이경우엔 다 나오지 않는 것 같다.

어차피 시간이 들여지는 거니 한 번에 한다면 Deep Scan 버튼을 클릭해서 깊게 스캔해 주자. 

 

 

그냥 스캔을 눌러도 꽤 오래 걸린다. 기다려 주자. 혹시나 기다리다 시간이 없어진다면 stop버튼을 눌러서 지금까지 훑은것들만 확인할 수도 있다. 

 

 

스캔이 끝나면 저렇게 지워졌던 파일들이 나오고 확인 버튼을 클릭해 주면 된다.

 

 

이미지를 보면 알겠지만 저기서 찾기엔 쉽지 않다. 아래쪽에 Tree View를 클릭해 주자.

 

 

그럼 간단하게 볼 수 있게 바뀐다. 여기서 원하는 파일을 선택해서 Recover버튼으로 복구시켜 주면 된다.

 

 

이미지에서 보이는 것처럼 파일 뒤에 Excellent Condition, Good Condition은 복원이 가능하지만 Poor Condition은 아쉽게도 복원되지 않는다. 

 

프로그램 사용 방법은 생각보다 쉽지만 못 찾을수도 있으니 쉬프트 딜리트는 신중하게 사용하자.

728x90
반응형
728x90
반응형

첫눈이 왔다. 그제 밤부터 하늘이 흐리기 시작하더니 어제 아침이 되자 온 세상이 눈 속에 덮여 하얀 세상이 되어있었다. 나무에는 눈꽃이 피었고, 땅과 건물들에는 추위에도 녹지 않는 눈이불이 덮였다. 말 그대로 세상이 눈 속에 있었는데 첫눈이 이렇게 커다랗게 온건 정말 오랜만이라 보고만 있어도 참 행복했다. 

이제 슬슬 크리스마스가 다가오는 구나라는 느낌이 날 더 즐겁게 만들었다. 수정볼안에 들어있는 것처럼 어느새 밖에는 또다시 눈이 내렸다.

 

"역시 눈이 내리는 날엔 핫초코지."라는 생각으로 지난번 이마트에서 사둔 미떼를 꺼냈다. 뜨거운 물에 미떼를 녹여주고 우유를 넣고 다시 저어서 전자레인지에 살짝 돌려주면 맛 좋은 핫초코가 탄생한다. 이렇게 탄 핫초코를 들고 베란다에 나가 첫눈의 흔적과 함께 계속해서 오고 있는 눈송이들을 한동안 구경했다. 여기에 이불이라도 하나 들고 오면 딱일 텐데라는 생각을 하던 중 이전에 내가 베란다에서 자보겠다고 도전했던 날이 생각이 났다.

 

그런 날들이 있다. 계절마다 해볼 수 있는 삶의 체험 현장.

유독 겨울을 좋아해서 그런지 나는 겨울에 그런 느낌이 강하게 든다. 삶의 체험 현장이라 해서 웅장한 것이 아니다. 가령 한겨울에 황토 맨발 걷기 하기나 베란다에서 자보기 같은 1박 2일에서 나올 법한 상황을 이야기하는 것이다.

 

나는 황토 맨발걷기를 할 수 있는 공원들에서 한겨울에 사람들이 걷지 않는 공통점을 발견하고 "왤 까"란 생각으로 가족들의 동의를 얻어 걸어봤다. 우리 가족들은 그런 나를 보고 "그래, 해봐라"라는 마인드를 가지고 공원 옆에 있는 그네에 앉았다. 처음엔 양말을 벗고 얼어있는 땅에 발이 닿으니 약간의 한기가 느껴졌다. 황토가 얼어있었다. 걸어봤다. 발바닥이 아픈 것도 아픈 것이지만 순식간에 내 따뜻했던 발바닥이 냉골의 상태가 되었다. 그래도 이렇게 시작을 해봤으니 한 바퀴는 돌아봐야 지란 생각을 가지고 빠른 스피드로 빠른 걷기를 시작했다. 다 걷고 난 다음 발을 물로 씻어야 됐는데 엄두가 나지 않아서 양말을 신고 집에 와서 따뜻한 물로 발을 씻었다. 집에 돌아오던 길 내내 우리 가족들은 나를 걱정하면서도 도대체 그걸 왜 해봐야 아냐고 타박을 했지만 나름 재미있었다. 그 후로 나는 겨울이 되거나 좀 추워질 때 공원을 산책하는 순간이 오면 그때의 내가 생각난다. 사람들도 아마 어쩌면 해봤다가 이건 아니란 사실을 알게 된 것일 지도 모른다. 

 

베란다에서 자보기를 한 날은 유독 뉴스에서 한파 주의를 외치던 날이었다.

겨울중에서도 가장 추운 겨울에 해보고 싶었던 마음이 컸던지라 뉴스에서 속보같이 떠있는 한파주의 단어를 보자마자 "오늘이다!"를 생각했다. 엄마, 아빠께 오늘은 밖에서 자볼 것이라 이야기하고 설레는 마음으로 밤이 될 때까지 기다렸다. 이전에 나의 형제는 고등학교 때 자신의 선교부 선배의 수능을 응원하기 위해 선배가 수능을 보는 학교 앞에서 강제로 강 추위 속에서 노숙을 한 경험이 있다. 그는 나를 보며 왜 사서 고생을 하냐는 눈빛을 보냈다. 엄마, 아빠는 그때도 어디서 구했는지 모를 냉장고 박스와 신문지, 침낭을 챙겨 나의 형제에게 집을 만들어 주었던 경력이 있다. 그때의 그 사건은 사실 학교 선배들의 강압적 태도 안에서 이뤄진 거라 자발적인 의도가 없었기 때문에 더 추위를 느꼈던 것 같았었다. 내 일이 아닌 해프닝이었기에 나 또한 얼마나 추운지 궁금했었던 마음이 컸다.

엄마, 아빠는 그날도 여기서 침낭 깔고 자다가는 얼굴이 돌아간다며 몇 가지만 해줄 테니 그 위에서 자라하셨다. 난 뭐 얼마나 달라질까를 생각했지만 부모의 사랑은 위대했다. 어째서인지 베란다 창문과 문에 김서림이 끼기 시작했다. 난 베란다에 누워 창문 밖 별을 보고자 했던 것인데 왜 저렇게 하얀 베란다가 되어있을까 싶어 나가 봤다. 바닥엔 우리 집에 있었는지도 모를 두꺼운 돗자리부터 시작해서 이불 요 매트 + 이불 + 침낭 + 파쉬 물주머니 여러 개. 원래라면 베란다에 가면 코가 시린데 그때는 집안보다 베란다가 더 따뜻했다. 뭔가 나의 의도와는 전혀 다른 잠자리였지만 그래도 베란다에서 자보기가 주 포인트였기에 설레는 마음으로 잠을 자보려 침낭 안으로 들어갔다. 얼마나 바닥이 많이 깔려있는 것인지 내 침대만큼 폭신했다. 이게 바로 가족의 사랑인가 싶었는데 가족의 사랑은 그렇게 끝나지 않았었다.

잠을 자다 너무 더워서 뒤척이다 눈이 떠졌는데 누군가가 날 보고 있는 게 아닌가. 정말 화들짝 놀라서 일어나 보니 엄마와 아빠가 베란다 안을 쳐다보고 있었다. 밖에서 자겠다는 나를 말리기엔 내 행동이 너무 완강했고, 그렇게 내버려 두자니 내가 얼어 죽을까 봐 걱정이 되셨던 것이었다. 그 새벽에 우리는 서로 놀랐지만 그만큼 웃었다. 다음날 아침 여전히 뽀얀 창문들을 보며 잠에서 깼다. 역시나 나름 재밌었던 기억이다.

 

어제는 엄마와 밤 산책을 갔었는데, 엄마가 나무들마다 한가득 눈이 쌓여있는 걸 보고 "눈이 많이 쌓여있으면 나무들이 무거울꺼야"라는 말과 함께 어디서 찾아왔는지 모를 긴 나뭇가지로 나무들을 털어주었다. 키가 닿지 않는 곳엔 점프를 하면서 털어주었는데 그 모습이 참 귀여웠다. 슬쩍 내 뒤에 와서 나에게 눈벼락을 맞게 하기 전까지 난 엄마의 따스함에 감동하고 있었다. 그렇게 눈도 맞고, 밟으면서 뽀독 뽀독 소리도 들으며 산책을 하니 슬슬 생각나는 게 있지 뭔가. 

 

한겨울에 붕어빵은 못참지.

붕어빵 파는 아줌마를 찾아 이리저리 다니다 드디어 찾아서 팥붕어빵 4개를 샀다. 아주머니께서 갓 만든 거라 아마 한입 먹으면 잊지 못할 거라 하시길래 두근거림은 배가 되었다. 무엇보다 여기 붕어빵은 아직 2개에 1000원이었다. 감사합니다의 인사와 함께 가장 맛있게 생긴 붕어빵을 엄마한테 주고 나도 하나 꺼내 먹었다. 엄마와 동시에 눈이 마주쳤고 우린 다시 왔던 길을 돌아 붕어빵 두 개를 더 사 왔다. 그 사이 붕어빵아주머니는 줄이 길게 늘어나있었다. 모두가 한마음이었던 것이다. 사실 우리가 지나가는 사람들에게 다 들리도록 너무 크게 맛있다를 외쳤던 것 같기도 하다. 

역대급으로 만족했던 붕어빵을 먹으면서 신나게 집으로 향했다. 겨울의 시작이 아주 좋다!

붕어빵은 팥이지.

728x90
반응형
728x90
반응형
롤케이크 자르기 문제

 

내 코드
using System;
using System.Collections.Generic;
using System.Linq;
public class Solution {
    public int solution(int[] topping) {
        int answer = 0;
        Stack<int> stack = new Stack<int>(topping);
        var dic = topping.GroupBy(x => x).ToDictionary(x=>x.Key,x=>x.Count());
        HashSet<int> hashSet = new HashSet<int>();
        while(stack.Count > 0)
        {
            int n = stack.Pop();
            hashSet.Add(n);
            if (dic[n] == 1)
                dic.Remove(n);
            else
                dic[n]--;
            if(dic.Count() == hashSet.Count)
                answer++;
        }
        return answer;
    }
}

 

728x90
반응형
728x90
반응형
의상 문제

 

 

내 코드
using System;
using System.Collections.Generic;
public class Solution {
    public int solution(string[,] clothes) {
        int answer = 1;
        Dictionary<string, int> dic = new Dictionary<string, int>();
        dic.Add(clothes[0, 1], 0);
        for (int i = 0; i < clothes.GetLength(0); i++)
        {
            if(dic.ContainsKey(clothes[i, 1]))
            {
                dic[clothes[i, 1]]++;
            }
            else
            {
                dic.Add(clothes[i, 1],1);
            }
        }
        foreach(string key in dic.Keys)
        {
            // *(한옷의 종류 수 +1(안입는 경우의 수))
            answer *= dic[key] + 1;
        }
        answer -= 1; // - 아무것도 안입는 경우의 수
        return answer;
    }
}
728x90
반응형
728x90
반응형

유니티에서 스프라이트를 가져와야 할 때 스프라이트 뒤에 배경 색이 있는 경우가 있다. 주로 한 가지 색깔로 되어 있는데 그냥 사용하기엔 번거롭기 때문에 투명으로 변경해 주는 게 편하다.

 

나의 경우 Aseprite 프로그램을 구매해둔게 있어서 거기서 간단하게 수정하는 방법을 기록해 두기로 했다.

예전엔 이 쉬운걸 몰라서 지우개로 한 땀 한 땀 지웠던 적도 있었는데 지금 생각해 보면 그 당시 시간이 많았던 걸까 싶다.


 

화면에 보이는 것 처럼 마젠타 색깔로 되어 있는 스프라이트를 봐보자. 

바로 옆에 있는 팔레트에는 해당 스프라이트에서 사용한 색깔들이 구성되어 있다. 우리는 여기에 투명색을 하나 더 추가해줄 것이다. 

 

색깔 선택하는 창에서 투명쪽으로 마우스를 가져다 대고 꾹 한번 누르면 

 

이렇게 바뀌는데 여기서 저 흰 느낌표가 들어가 있는 빨간색 ui를 클릭해서 팔레트에 추가해 준다.

 

마지막으로 위 순서대로 해주면 배경은 투명으로 바뀐다.

투명색깔을 팔레트에 추가해 주고 채우기 버튼을 클릭해서 배경으로 들어가 있는 마젠타 색 어디든 클릭해주면 배경이 투명으로 채워진다.

 

안지워진 마젠타색들이 거슬리지만 저부분은 사용할 필요 없는 부분이니까 작업해줄 필요가 없다. 혹시나 필요하다면 그냥 똑같이 클릭클릭해주면서 투명으로 채워주면 해결된다.

 

지금까지 아주 간단하지만 모르면 생각보다 뻘짓을 해야 되는 작업이었다. 

 

 

++ 만약 연두색 배경이 있는 스프라이트를 들고 왔는데 Aseprite에서 투명이라고 인식할 수 있다.

그럴 때는 아래쪽에 위치해 있는 Background 레이어를 복제해 준 다음 새 레이어를 하나 만들어 복제한 레이어와 합체해 주고 원래 있던 Background레이어를 지워주면 해결된다.

728x90
반응형
728x90
반응형

어제 써둔 pseudo 3d game 유니티 공부에 대해 저장해 두려고 글을 쓴다. 

일단 내가 만든 방법은 2d 배경 1장에 도로의 가운데 라인과, 장애물이 플레이어의 반대 방향(위에서 아래 방향)으로 이동하게 해서 플레이어는 가만히 있지만 이동하는 느낌을 만들어 봤다. 장애물 만드는 방법과 도로 라인 만드는 방법을 다르게 해 봤다. 사실 두 가지 다 각자 테스트 해보다가 각자 만들어졌지만 원리는 비슷할 것이다. 

 

 


1. 우선 게임에 필요한 스프라이트들을 구글에서 찾아서 들고온다.

 

나의 경우는 아래 스프라이트 사이트에서 많이 이용한다.

https://www.spriters-resource.com/

 

The Spriters Resource

This page does not work well in portrait mode on mobile. Please rotate your device.

www.spriters-resource.com

생각보다 많은 다양한 게임들의 스프라이트들을 다운로드할 수 있다. 만약 배경이 있다면 aseprite 프로그램을 사용해서 제거해 준다. 물론 코드로 뒷배경을 삭제시켜 주는 방법도 있다. 하지만 나는 이 프로그램을 돈 주고 샀기 때문에 뽕을 뽑아야 한다는 마음으로 작은 작업이라도 사용하는 편이다.

 

ai를 사용해서 나만의 배경을 만들어 볼까도 했지만 2시간 동안 하늘의 사이즈를 줄여주지 않는 ai에게 굴복하고 그만뒀다.(회원가입도 했는데... 하늘을 줄여주는 게 그렇게 싫었던 걸까. 그렇게 소량으로만 줄여주고 싶었던 것일까. 한글도 영어도 전부 먹히지 않았던 너란 ai. 배경 오브젝트들이 점점 한자들이 나오길래 중국인가 싶었지만 난 중국어는 못하는 걸.)

언덕도, 산도, 나무도, 하늘도 더이상 나의 계획대로 변경되지 않았던 나름 예뻤던 내가 요청한 배경. 도로 안에 가운데 라인도 빼달라고 빼달라고 그렇게 말했는데 완강했던 너의 모습. 잊지 못할 거야.

 

2. 유니티에서 몇 가지 작업을 해준다.

일단 우선적으로 2d project를 만들어 준다. 그리고 Obstacle Manager(장애물 관리 매니저) 오브젝트와, RoadLineManager(도로 라인 관리 매니저)를 만들어 준다. 프리팹들은 장애물 프리팹과, 라인 프리팹만 있으면 된다. UI Canvas를 만들어 주고 필요하다면 플레이어가 이동할 수 있는 버튼을 추가로 만들어 주면 된다. 나의 경우 버튼을 눌러 좌, 우로 이동할 수 있게 작업했다. 속도를 늘리는 버튼과 속도를 줄이는 버튼 또한 만들어 두었다.

 

 

 

3. 이제 스크립트를 짜보자.

 

우선 도로 라인이 내려오게 만들어 주는 RoadLineManager를 만들어 보자. 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;

public class RoadLineManager : MonoBehaviour
{
    [Header("Line Spawn Settings")]
    public GameObject roadLinePrefab;
    public int poolSize = 10;
    public float spawnInterval = 1f;
    
    [Header("Line Position Settings")]
    public float startYPosition = 5.7f;
    public float startXPosition = 0.15f;
    public float endYPosition = -1f;
    public float fixedLineSpacing = 1.8f;
    
    [Header("Line Scale Settings")]
    public float startScaleX = 0.05f;
    public float startScaleY = 0.02f;
    public float endScaleX = 0.4f;
    public float endScaleY = 0.5f;
    
    [Header("Movement Settings")]
    public float initialMoveSpeed = 5f;
    public float currentMoveSpeed;
    public float maxMoveSpeed = 15f;
    public float accelerationRate = 2f;
    
    [Header("Line Settings")]
    public int maxVisibleLines = 6;

    private List<GameObject> linePool;
    private float[] linePositions;
    //private float[] spacings = new float[] { 1.0f, 1.2f, 1.5f, 1.8f, 2.2f, 2.7f };
    private float[] spacings = new float[] { 0.8f, 1.2f, 1.8f, 2.5f, 3.3f, 4.2f };
    private void Start()
    {
        linePool = new List<GameObject>();
        linePositions = new float[maxVisibleLines];
        InitializePool();
        currentMoveSpeed = initialMoveSpeed;
        UpdateLinePositions();
        SpawnInitialLines();
    }
    
    private void InitializePool()
    {
        for (int i = 0; i < poolSize; i++)
        {
            GameObject line = Instantiate(roadLinePrefab);
            line.SetActive(false);
            linePool.Add(line);
        }
    }

    private void UpdateLinePositions()
    {
        linePositions[0] = startYPosition;
        
        for (int i = 1; i < maxVisibleLines; i++)
        {
            float spacing = fixedLineSpacing * spacings[i-1];
            linePositions[i] = linePositions[i-1] - spacing;
        }
    }
    
    private void SpawnInitialLines()
    {
        for (int i = 0; i < maxVisibleLines; i++)
        {
            SpawnLineAtPosition(linePositions[i]);
        }
    }

    private void SpawnLineAtPosition(float yPosition)
    {
        GameObject line = GetInactiveLine();
        if (line != null)
        {
            float t = (startYPosition - yPosition) / (startYPosition - endYPosition);
            t = Mathf.Clamp01(t);
            
            float scaleX = Mathf.Lerp(startScaleX, endScaleX, t);
            float scaleY = Mathf.Lerp(startScaleY, endScaleY, t);
            
            line.transform.position = new Vector3(startXPosition, yPosition, 0);
            line.transform.localScale = new Vector3(scaleX, scaleY, 1);
            line.SetActive(true);
        }
    }
    
    private void Update()
    {
        UpdateLines();
    }

    private void UpdateLines()
    {
        int visibleCount = 0;
        float highestY = float.MinValue;

        foreach (GameObject line in linePool)
        {

            if (line.activeInHierarchy)
            {
                Vector3 pos = line.transform.position;
                float t = (startYPosition - pos.y) / (startYPosition - endYPosition);
                t = Mathf.Clamp01(t);

                float scaleX = Mathf.Lerp(startScaleX, endScaleX, t);
                float scaleY = Mathf.Lerp(startScaleY, endScaleY, t);

                // 스케일이 클수록 더 빠르게 이동하도록 수정
                float speedMultiplier = scaleY / startScaleY;
                pos.y -= (currentMoveSpeed * speedMultiplier) * Time.deltaTime;

                line.transform.localScale = new Vector3(scaleX, scaleY, 1);
                line.transform.position = pos;

                if (pos.y < endYPosition)
                {
                    line.SetActive(false);
                }
                else
                {
                    highestY = Mathf.Max(highestY, pos.y);
                    visibleCount++;
                }
            }
        }

        if (visibleCount < maxVisibleLines)
        {
            if (highestY == float.MinValue)
            {
                SpawnLineAtPosition(startYPosition);
            }
            else
            {
                int currentIndex = visibleCount - 1;
                float spacing = fixedLineSpacing * spacings[currentIndex];
                float newYPos = highestY + spacing;
                
                if (newYPos <= startYPosition)
                {
                    SpawnLineAtPosition(newYPos);
                }
            }
        }
    }
    
    private GameObject GetInactiveLine()
    {
        return linePool.Find(line => !line.activeInHierarchy);
    }

    public void IncreaseSpeed()
    {
        currentMoveSpeed = Mathf.Min(currentMoveSpeed + accelerationRate, maxMoveSpeed);
    }
    
    public void DecreaseSpeed()
    {
        currentMoveSpeed = Mathf.Max(currentMoveSpeed - accelerationRate, initialMoveSpeed);
    }
    
    public void ResetSpeed()
    {
        currentMoveSpeed = initialMoveSpeed;
    }
}

 

이 코드를 짤 때 몇 가지 골머리 써야 됐었던 부분이 있었다.

그중 가장 큰 게 라인이 프리팹으로 활성화가 될 때 아래로 내려오는 느낌으로 원근감 있게 내려오려면 처음 적용해 줬던 간격을 계속 유지하면 안 되는 문제가 있었다. 이 부분을 수정하기 위해 나에겐 여러 번의 시도가 필요했다. 그래서 UpdateLines() 함수는 정말 쉴 새 없이 수정됐었다. 그리고 마침내 해결된 방법이 바로 위에 적어둔 스케일의 사이즈에 따라 빠르게 이동시키는 것. 이 부분이 사실 그렇게 어려운 부분은 아니었다. 처음부터 수치를 디버깅해서 확인해 봤으면 문제가 되지 않았을 것이다. 변경시켜 주던 수치값이 제대로 늘어나지 않는 오류를 모르고 도대체 왜 내가 원했던 데로 나오지 않는 거지 하다가 시간을 두배로 썼다. 잊지 말자 디버깅.

 

버튼에 적용시켰던 speed up 버튼과 speed down 버튼에 사용할 함수도 여기 있는 IncreaseSpeed() 함수와 DecreaseSpeed() 함수를 연결해 주면 된다. 필요시 나머지 함수들도 연결시켜줘도 기능은 할 것이다. 

 

나의 경우는 변수들을 많이 만들어 최대한 유니티에서 배경을 바꿨을 때도 사용할 수 있게 작업했다. 지금 보이는 것과 같이 저 배경은 free이기 때문에 가져온 스프라이트지 그렇게 내 마음에 들지 않기 때문일지도 모른다. 도로의 시작과 끝을 잡아줄 위치 변수들과, 라인의 스케일을 관리해 줄 변수들, 라인과 라인 사이의 간격을 정할 변수로 되어있다. 그리고 화면에 나올 라인의 개수 변수가 있는데 이건 간격과 도로의 총길이에 따라 나오지 않을 수도 있다. 프리팹은 오브젝트 풀링을 사용해서 최적화해 두었다. 

 

 

 

장애물 매니저는 내일 다시 정리해 두겠다. 여기서는 역시나 오브젝트풀로 장애물을 관리하고, 도로의 좌, 우에 따라붙어서 원근감 있게 스케일이 커지도록 작업했다.

 

++ 나도 모르게 필요없는 파일인줄 알고 프로젝트 삭제시켜서 장애물 매니저는 그렇게 날라갔다...다시 작업해서 올리도록 하겠다.

728x90
반응형
728x90
반응형

며칠 전 공부했던 곳에서 봤던 단어 pseudo! 그때도 낯설었던 단어 슈도라는 단어를 이렇게 다시 만날 줄이야.

그때 공부했을 땐 슈도 코드에 대해서였지만 지금은 2d로 3d게임 환경을 만드는 것이다. 오래전 우리가 게임했을 때 대체로 많이 보였던 기법인데 대표적인 게임이 "Out Run" 게임이 있다. 이외에도 레이싱 게임에서는 종종 많이 발견됐었던 것 같다. 그때는 그게 3D인지 2D인지 알게 뭐였는가. 그저 게임하는 게 즐거웠을 뿐이었지. 

그렇게 즐거웠던 게임이 이제 단순히 "참 즐거웠던 게임이었지."란 과거형의 기억이 아닌 순간이 왔다. 새로 작업해 보기로 한 프로젝트 팀에서 딱 "Out Run"게임처럼 구현해 주길 바라는 말을 듣게 되었다.

 

사실 처음엔 뭐 얼마나 어렵겠어라는 생각이 전부였다. 못할 건 없지란 생각으로 일주일 동안 시도해 볼 시간을 얻어 Test를 해보기 전까지 정말 딱 그 생각이었다. 생각보다 구글에 "How do i make OutRun"라고 치는 순간 기가 막힌 정보들도 찾을 수 있어서 쉽게 접근할 수 있겠다 생각했었다. 

 

https://jakesgordon.com/writing/javascript-racer/

 

How to build a racing game

Personal Website for Jake Gordon

jakesgordon.com

https://jakesgordon.com/writing/javascript-racer-v1-straight/

 

How to build a racing game - straight roads

Personal Website for Jake Gordon

jakesgordon.com

 

정말 멋진 내용이다. 도로 기하학에서 약간 주춤거렸지만 생각보다 꽤 술술 재미있게 읽혔다. 

여기와 함께 Pseudo 3d game 부분도 필수로 읽어야 되는 내용이였다.

http://www.extentofthejam.com/pseudo/

 

Lou's Pseudo 3d Page

Lou's Pseudo 3d Page Pseudo 3d en español aqui! Gracias a Luis Peña! (C) 2013 Louis Gorenfeld, updated May 3, 2013 NEW: Important details on the segmented road system and some additional links NEW: An (optional) explanation of finding field-of-view for t

www.extentofthejam.com

 

읽으면서 드는 생각은 "생각보다 어렵겠는데"였다. 

 

위에 글을 읽어보고 하루 후에 유니티를 켜봤다가 그새를 잊은 건지 허튼짓을 했었던 순간이 있었다. 바로 2d 스프라이트들을 아래쪽에 쭉 깔아 두고 플레이어한테 다가오게 하면 안 되나 했다가 그렇게 되면 3d가 되는 거란 걸 인식하고 세상 똥멍청이인가 싶었었다.

누군가는 또 이 방법을 사용해서 작업을 했던 흔적을 발견했다. 완전히 똥멍청이 생각은 아니였던 걸로!

 

뭐 그래도 뭐든 시도는 좋은 거니까 라는 마인드로 다시 시도해 봤다. 그러다 세그먼트들 작업을 할 때 생각했다. 왜 2D환경에서 3d를 구현하고 싶은 걸까. 옛날이라면 어쩔 수 없었던 선택일지 몰라도 요즘 같은 현대 사회에 왜 3d로 만들면 한방에 해결이 되는 이 상황을 난 지금 왜 이렇게 집중하고 있을까였다.ㅎㅎ 이렇게 오늘도 난 성장했다.

 

그럼에도 불구하고 뭔가 슬슬 화면에 나오기 시작했을 때쯤 쾌감은 짜릿했다.

하지만 내 코드에서는 몇 가지 문제점들이 보였다. 속도가 붙을수록 세그먼트들의 간격이 생기는 상황. 뭔가 잘못 만든 게 분명했다. 작업하면서 스케일값도 중요했다. 일주일 동안 이 작업만 할 수 있는 스케줄은 아니었기에 다른 쉽게 가는 방법이 있을까 하고 생각하다가 도로에 있는 차선만 이동하는 것도 나쁘지 않겠다는 생각을 했다. 결과적으로는 나쁘지 않은 결괏값을 얻을 수 있었다. 위의 방식대로 하던 중 문제가 있었던 부분은 연속으로 나오는 부분에서 간격이 벌어지는 문제였다면 라인은 연속해서 나오지 않기에 그대로 작업을 해보았다. 

작업할 때 가장 중점적으로 생각한 부분이 멀리 있는 공간에서 화면 가까이로 오는 부분에서 느껴져야 할 원근감과 스케일 부분, 그리고 왼쪽 도로와 오른쪽 도로에서 오는 장애물의 경우 차선을 따라 각각의 끝에 자연스럽게 유지돼서 붙어 내려와야 하는 부분. 그리고 애증의 간격 부분. 

간격이 생각보다 어려웠던 게 차량 이동을 생각할 때 도로 위의 라인이 멀리서 가까이로 올수록 간격이 넓어져야 하는데 간격을 정해두고 스케일만 키웠던 게 문제였다. 그래서 접근한 부분이 스케일 값이 커질수록 position y의 값을 더 작아지게 해서 간격을 늘려볼까도 생각을 했는데 이렇게 구현해 보니 무슨 문제인지 전혀 원하는 방향대로 나오지 않길래 디버그를 해봤더니 알았다. 내가 원한 만큼의 수치가 계산에서 이루어지고 있지 않았다. 수치를 조절하는 부분을 다시 수정하고 봤더니 이젠 프리팹으로 만들어 둔 오브젝트가 시작 부분에서 종료되는 부분까지 나오는 거리가 문제가 돼서 활성화가 안 되는 버그를 보고 눈을 감았다. 이것저것 머릿속에서 한참 생각해 보다가(프리팹 순서를 정해서 활성화해 볼까, 리스크에 위치값을 저장시켜서 그 부분에 가면 위치를 두 배씩 늘려볼까, 등등 지금 생각해 보면 별의별 말도 안 되는 생각들이 있었다.) 속도 값을 늘려볼까 싶었는데 이게 생각보다 괜찮은 결괏값을 만들어 줬다. 그렇게 장애물과, 라인의 움직임을 구현시켜 두었더니 한결 마음이 편해졌다. 결과적으로 프리뷰느낌으로 보기에 나쁘지 않을 정도의 상태를 만들어 두고 생각해 봤는데 이 방법이 최선의 선택은 아닌 것 같다. 저 위의 내용을 토대로 다시 한번 작업해 봐야겠다.

 

사실 이제 더 이상 이런 부분의 코드 작업을 안 할 줄 알았는데 막상 해보니까 너무 재미있었다. 아무래도 이쪽 길로 가지 않는다 해도 취미로라도 계속해서 관심을 유지해야겠다.

 

 


밑에는 도로 기하학 찾아봤다가 흥미로우면서 경악했던 구글 화면.

728x90
반응형

+ Recent posts