✍️ 개요
최근에 끝이 없는 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);
}
'유니티(Unity) > 기능 구현' 카테고리의 다른 글
[Unity 기능] 2D Endless Platform 구현 (2편) (0) | 2025.01.02 |
---|---|
[Unity 기능] 컴파일 시점 제어하기 (1) | 2024.12.25 |
[Unity 기능] 커스텀 패키지 제작하기 (0) | 2024.12.20 |
[Unity 기능] Sprite Atlas에 대하여 (1) | 2024.12.15 |
[Unity 기능] 9-Slicing Sprite 제작 (1) | 2024.12.07 |