━━━━ ◇ ━━━━
유니티(Unity)/콘텐츠 개발

[Unity 기능] 2D Endless Platform 구현 (1편)

✍️ 개요

 

 

 

 

최근에 끝이 없는 2D Platform을 제작하고 있습니다.

 

하나의 Layer만 사용하는 Serial Endless Platform, 그리고 여러 개의 Multi Layer을 사용하는 Parallel Endless Platform으로 구성하려고 합니다.

 

하지만 모두 다루기엔 글이 길어질 것 같아 2편으로 나눠 정리하도록 하겠습니다.

 

 

 

 

 


📌 시스템 설계

 

 

위 사진을 예시로 설계해보도록 하겠습니다.

 

하나의 Camera와 자동차, 4개의 숲 풍경 조각이 있습니다.

 

저는 이제 자동차를 앞으로 움직이고 무한한 풍경을 구현할 예정입니다.

 

 

 

 

가장 먼저 Camera와 풍경(Map)이 움직이도록 합니다.

 

Q. Camera만 움직여도 Map이 뒤로가는 느낌을 줄 수 있지 않나요?

 

A. 맞습니다. 다만, 저는 추후에 Layer 별로 서로 다른 속도를 주기 위해 Map도 움직이도록 하였습니다.

 

위 예시는 4개의 Node가 연결되어 하나의 Map을 나타냅니다.

 

 

 

 

만약 마지막 Node가 Camera Right 안쪽으로 들어온다면 새로운 Node을 생성합니다.

 

이때, 새로운 노드는 ① ~ ④ 노드 중 랜덤 노드를 추가하여 자연스럽게 연출합니다.

 

 

 

 

또한, 첫번째 Node가 Camera Left 바깥쪽을 넘어간다면 첫번째 Node을 삭제합니다.

 

위 사진을 보면 ① 노드가 카메라를 벗어나며 삭제된 것을 확인할 수 있어요.

 

자, 지금까지 설명한 부분들을 구현하면 끝입니다. 의외로 간단하죠?

 

바로 코드를 살펴보도록 하겠습니다.

 

 

 

 

 


📌 코드 분석

 

    public class EndlessPlatformNode : MonoBehaviour
    {
        [SerializeField] private SpriteRenderer spriteRenderer;
        public float Width => spriteRenderer ? spriteRenderer.bounds.size.x / 2 : 0.0f;
        public float Height => spriteRenderer ? spriteRenderer.bounds.size.y / 2 : 0.0f;

        public void Destroy() => Object.Destroy(this.gameObject);
    }

 

EndlessPlatformNode : Map을 구성하는 조각

 

SpriteRenderer을 기반으로 View을 나타냅니다.

 

 

 

 

 

    public class EndlessPlatformNodeContainer
    {
        private readonly List<EndlessPlatformNode> _nodes;
        public List<EndlessPlatformNode> Nodes => _nodes;
        private readonly float _moveSpeed;

        public EndlessPlatformNodeContainer(float moveSpeed)
        {
            _nodes = new List<EndlessPlatformNode>();
            _moveSpeed = moveSpeed;
        }

        public void AddFirst(EndlessPlatformNode node) => _nodes.Insert(0, node);
        public void AddLast(EndlessPlatformNode node) => _nodes.Add(node);
        public void Remove(EndlessPlatformNode node) => _nodes.Remove(node);
        
        public void Move(Vector3 moveVec)
        {
            foreach (var node in _nodes)
            {
                node.transform.position += moveVec * _moveSpeed;
            }
        }
    }

 

EndlessPlatformNodeContainer : 여러 개의 Node을 담는 컨테이너

 

Node을 추가하거나 삭제, 그리고 이동시키는 역할을 합니다.

 

 

 

 

 

 

EndlessPlatformSpawner : 끝이 없는 2D Platform 기능을 수행하는 Controller

 

EndlessPlatformNodeContainer 자료구조를 기반으로 로직을 처리합니다.

 

 

 

[ 소스 코드 펼치기 ]

더보기
using System.Collections.Generic;
using UnityEngine;

namespace DevelopKit.EndlessPlatform
{
    public class EndlessPlatformSpawner : MonoBehaviour
    {
        [SerializeField] private Transform platform;
        [SerializeField] private float moveSpeed = 1.0f;
        [SerializeField] private bool pause = false;

        private EndlessPlatformNodeContainer _container;
        private List<EndlessPlatformNode> _nodePrefabs;

        private void Start()
        {
            _container = new EndlessPlatformNodeContainer(moveSpeed);
            _nodePrefabs = new List<EndlessPlatformNode>();
            var platformNodes = platform.GetComponentsInChildren<EndlessPlatformNode>();

            foreach (var node in platformNodes)
            {
                _nodePrefabs.Add(node);
                node.gameObject.SetActive(false);
                
                var newNode = Instantiate(node.gameObject, node.transform.position, node.transform.rotation)
                    .GetComponent<EndlessPlatformNode>();
                _container.AddLast(newNode);
                newNode.gameObject.SetActive(true);
            }
        }

        private void Update()
        {
            if (pause) return;
            
            SpawnPlatformNode();
            ClearPlatformNode();
            MovePlatformNode();
        }
        
        private void SpawnPlatformNode()
        {
            var node = _container.Nodes[^1];
            if (CameraUtils.InRightEdge(node))
            {
                var newNode = SpawnRandomNode(node, 1);
                _container.AddLast(newNode);
                newNode.gameObject.SetActive(true);
            }
            
            node = _container.Nodes[0];
            if (CameraUtils.InLeftEdge(node))
            {
                var newNode = SpawnRandomNode(node, -1);
                _container.AddFirst(newNode);
                newNode.gameObject.SetActive(true);
            }
        }
        
        private void ClearPlatformNode()
        {
            for (int i = _container.Nodes.Count - 1; i >= 0; i--)
            {
                var node = _container.Nodes[i];
                if (CameraUtils.OutOfView(node))
                {
                    _container.Remove(node);
                    node.Destroy();
                }
            }
        }
        
        private void MovePlatformNode()
        {
            var moveVec = Vector3.left * CameraUtils.CamPosDelta;
            _container.Move(moveVec);
        }
        
        private EndlessPlatformNode SpawnRandomNode(EndlessPlatformNode current, float shift)
        {
            var randomNode = _nodePrefabs[Random.Range(0, _nodePrefabs.Count)];
            var destination = current.transform.position + Vector3.right * (shift * (current.Width + randomNode.Width));
            var rotation = randomNode.transform.rotation;
            destination.y = randomNode.transform.position.y;
            return Instantiate(randomNode.gameObject, destination, rotation).GetComponent<EndlessPlatformNode>();
        }
    }
}

 

public static class CameraUtils
{
    private static Camera _main;
    public static Camera Main
    {
        get
        {
            if (_main == null)
            {
                _main = Camera.main;
            }

            return _main;
        }
    }
    
    public static float GetCamWidth() => Main.orthographicSize * Screen.width / Screen.height;
    
    public static float GetCamHeight() => Main.orthographicSize;
    
    public static bool OutOfView(EndlessPlatformNode node, float threshold = 0.5f) 
        => node.transform.position.x - node.Width - threshold > Main.transform.position.x + GetCamWidth()
           || node.transform.position.x + node.Width + threshold <= Main.transform.position.x - GetCamWidth();
    
    public static bool InLeftEdge(EndlessPlatformNode node, float threshold = 0.5f) 
        => node.transform.position.x - node.Width - threshold > Main.transform.position.x - GetCamWidth();
    
    public static bool InRightEdge(EndlessPlatformNode node, float threshold = 0.5f) 
        => node.transform.position.x + node.Width - threshold <= Main.transform.position.x + GetCamWidth();
}

 

 

 

 

 


📌 최적화 적용

 

 

 

위 코드를 그대로 사용하면 반복적인 Instantiate와 Destory로 성능에 문제가 발생할 수 있습니다.

 

그래서 ObjectPool을 사용하여 성능을 최적화하면 좋습니다.

 

아래 코드는 제가 주로 사용하는 ObjectPool 방식을 적용시킨 코드입니다.

 

 

 

[ 소스 코드 펼치기]

더보기
public sealed class EndlessPlatformPool : Singleton<EndlessPlatformPool>
{
    private static List<IObjectPool<EndlessPlatformNode>> _pools = new();
    private static List<EndlessPlatformNode> _nodePrefabs = new();
    private static Transform _parent;
    private static int _currentCapacity;
    
    public static void Bind(List<EndlessPlatformNode> prefabs, Transform parent = null)
    {
        _nodePrefabs = prefabs;
        _parent = parent;
        _currentCapacity = 0;

        for (int i = 0; i < prefabs.Count; i++)
        {
            var nodeIndex = i;
            _pools.Add(new ObjectPool<EndlessPlatformNode>(() => CreatePooledItem(nodeIndex), GetPooledItem, ReleasePooledItem));
        }
    }
    
    public static EndlessPlatformNode Get()
    {
        var randomNodeIndex = GetRandomNodeIndex();
        return Get(randomNodeIndex);
    }
    
    public static EndlessPlatformNode Get(int nodeIndex)
    {
        if (_currentCapacity <= 0)
        {
            return CreatePooledItem(nodeIndex);
        }
        
        _currentCapacity--;
        return _pools[nodeIndex].Get();
    }
    
    public static void Release(EndlessPlatformNode item)
    {
        _currentCapacity++;
        var randomNodeIndex = item.NodeIndex;
        _pools[randomNodeIndex].Release(item);
    }
    
    private static EndlessPlatformNode CreatePooledItem(int nodeIndex)
    {
        var prefab = _nodePrefabs[nodeIndex];
        var newNode = Object.Instantiate(prefab, prefab.transform.position, prefab.transform.rotation, _parent)
            .GetComponent<EndlessPlatformNode>();
        newNode.NodeIndex = nodeIndex;
        newNode.gameObject.SetActive(true);
        return newNode;
    }
    
    private static void GetPooledItem(EndlessPlatformNode item)
    {
        item.gameObject.SetActive(true);
    }
    
    private static void ReleasePooledItem(EndlessPlatformNode item)
    {
        item.gameObject.SetActive(false);
    }
    
    private static int GetRandomNodeIndex() => Random.Range(0, _nodePrefabs.Count);
}

 

 

 

 

 

COMMENT