//구글콘솔 광고 추가가
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