Unity 2D : 싱글톤, GameManager, SpawnManager

농담고미고미·2024년 9월 20일

Unity 개발 일지

목록 보기
24/26

[유니티 2D 게임 개발] 책을 읽고 참고하여 작성했습니다.

싱글톤

싱글톤이란 애플리케이션이 끝날 때까지 딱 하나의 인스턴스만 만들어야 할 클래스가 있을 때 사용한다.

게임 로직을 조율하는 게임 매니저 클래스처럼 여러 클래스가 사용할 기능을 하나의 클래스로 제공해야 한다면 싱글톤이 유용할 수 있다.

싱글톤을 사용하면 어디서든 싱글톤 클래스와 기능에 접근할 수 있는 경로를 하나로 통합해서 제공할 수 있다. 또 싱글톤은 인스턴스화 지연이 가능하다. 인스턴스화 지연이란 인스턴스화 하는 코드가 있어도 인스턴스를 만들지 않고 실제로 처음 사용할 때 인스턴스를 만든다는 뜻이다.

  • PRGGameManager
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    public class PRGGameManager : MonoBehavior
    {
    	public static PRGGameManager sharedInstance = null;
    	/* static 변수인 sharedInstance는 싱글톤 오브젝트에 접근할 때 사용한다. 싱글톤은 이속성을 통해서만 접근할 수 있다. */ 
    	void Awake()
    	{
    		if(sharedInstance != null && sharedInstance != this)
    		{
    			Destroy(GameObject);
    		}
    		else
    		{
    			sharedInstance = this;
    		}
    	}
    	
    	void Start()
    	{
    		SetUpScene();
    		/* 씬의 설정에 관한 모든 로직을 하나의 메서드에 넣으려 한다. 이렇게 해놓으면 나중에 Start() 메서드가 아닌 다른 코드에서 다시 호출하기 편하다. */
    	}
    	
    	public void SetUpScene()
    	{
    	}
    }

위 코드를 하나씩 살펴보자.

public static PRGGameManager sharedInstance = null; static 변수가 클래스의 특정 인스턴스가 아닌 PRGGameManager 클래스 자체에 속한다는 점이 중요하다. 클래스 자체에 속하므로 PRGGameManager.sharedInstance 의 복사본은 메모리에 단 하나만 존재한다.

if(sharedInstance != null && sharedInstance != this)
		{
			Destroy(GameObject);
		}

PRGGameManager의 인스턴스는 한번에 하나만 존재해야 하므로 sharedInstance가 이미 존재하는지 아니면 현재 인스턴스와 다른 인스턴스인지 확인한다.

만약 이미 존재하고 현재 인스턴스와 다르다면 sharedInstance를 제거한다. PRGGameManager의 인스턴스는 유일해야 하기 때문이다.

else
		{
			sharedInstance = this;
		}

현재 인스턴스가 유일한 인스턴스면 sharedInstance 변수에 현재 오브젝트를 대입한다.

이후 PRGGameManager를 프리팹으로 만든다.

스폰 위치

스폰 위치를 나타내는 SpawnPoint 프리팹을 만들고, 스폰 로직을 스크립트로 작성해서 이 프리팹에 추가하는 과정을 통해 목적을 달성하려 한다.

  • SpawnPoint Script
    using UnityEngine;
    
    public class SpawnPoint : MonoBehavior
    {
    	public GameObject prefabToSpawn;
    	//리스폰할 프리팹.
    	public float repeatInterval;
    	
    	public void Start()
    	{
    		if(repeatInterval > 0)
    		{
    			InvokeRepeating("SpawnObject", 0.0f, repeatInterval);
    			//InvokeRepeating의 파라미터는 호출한 메서드, 최초 호출까지의 대기 시간, 호출 사이의 대기시간.
    		}
    	}
    	
    	public GameObject SpawnObject()
    	{ /* SpawnObject() 는 실제로 프리팹을 인스턴스화해서 오브젝트를 스폰하는 역할을 한다. 리턴 타입은 GameObject임. */
    		if(prefabToSpawn != null)
    		{ // 에러를 방지하기 위해 인스턴스화 하기 전에 유니티 에디터에서 프리팹을 설정했는지 확인해야 한다. 
    			return Instantiate(prefabToSpawn, transform.position, Quaternion.identity);
    			/* SpawnPoint 오브젝트의 현재 위치에 프리팹을 인스턴스화 한다. Instantiate의 파라미터는 프리팹, 위치를 나타내는 Vector3, 쿼터니언이다. 쿼터니언은 회전을 표현할 때 사용하며 Quaternion.identity는 "회전이 없음"을 나타낸다. */
    		}
    		return null;
    		//null이면 SpawnPoint를 제대로 설정하지 않았다는 뜻이다. 
    	}
    }

이후 게임창의 원하는 곳에 SpawnPoint 를 프리팹으로 만들어서, 스폰되길 원하는 위치에 놔둔다.

플레이어 스폰

이제 PRGGameManager가 SpawnObject()를 호출하게 만들자.

  • PRGGameManager (수정)
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class MovementController : MonoBehavior
    {
    	public SpawnPoint playerSpawnPoint;
    	/* PlayerSpawnPoint 속성에 플레이어 스폰 위치의 참조를 저장하려고 한다. 플레이어가 죽으면 다시 스폰할 수 있게 플레이어용 스폰 위치의 참조는 계속 저장해두려 한다. */
    	 
    	public float movementSpeed = 3.0f;
    	Vector2 movement = new Vector2();
    	
    	Animator animator;
    	
    	string animationState = "AnimationState";
    	/* 하드코딩을 피하기 위해서. 문자열을 변수에 한번 대입해놓고 문자열을 참조해야 할 떼마다 이 변수를 사용해서 버그가 일어날 가능성을 원천차단해야 한다. */
    	
    	Rigidbody2D rb2D;
    	//Rigidbody2D의 참조를 저장할 변수를 선언한다.
    	
    	enum CharStates
    	{ /* "enum"데이터 형식을 사용하면 열거형 상수를 선언할 수 있다. 각 열거형 상수는 뒤에 적은 int 등의 값과 일치하므로 열거형을 참조하면 해당 값을 얻을 수 있다. */
    		walkEast = 1,
    		walkSouth = 2,
    		walkWest = 3,
    		walkNorth = 4,
    		idleSouth = 5
    	}
    	
    	private void Start()
    	{
    		rb2D = GetComponent<Rigidboidy 2D>();
    		/* GetComponent 메서드는 Type 형식의 파라미터를 입력 받아서 현재 게임 오브젝트가 지닌 컴포넌트 중에서 Type이 동일한 컴포넌트가 있으면 반환하는 메서드다. */
    		animator = GetComponent<Animator>();
    	}
    	
    	private void Update()
    	{
    		**UpDateState();
    		/* 애니메이션 상태 머신을 업데이트하려고 작성한 함수를 호출한다. 코드를 깔끔하게 유지하고 가독성을 높이고자 업데이트 로직을 별도의 메서드로 옮겼다. */** 
    	}
    	
    	void FixedUpdate()
    	{
    		**MoveCharacter();**
    	}
    	
    	private void MoveCharacter()
    	{
    		**movement.x = Input.GetAxisRaw("Horizontal");
    		movement.y = Input.GetAxisRaw("Vertical");**
    		/* Input 클래스는 사용자의 입력을 획득할 수 있는 방법을 제공한다. 이 코드는 GetAxisRaw() 메서드를 사용해서 사용자 입력을 획득하고 획득한 값을 Vector2 구조체의 x와 y 변수에 대입한다. GetAxisRaw() 메서드는 입력을 받을 2D 축을 지정하는 Horizontal 이나 Vertica(수직)을 파라미터로 받고, 유니티 입력 매니저가 전달하는 -1, 0, 1 을 반환한다. */
    		
    		movement.Normalize();
    		// 벡터를 정규화(normalize)해서 플레이어가 대각선, 수직, 수평, 어느 방향으로 움직이든 일정한 속력을 유지하게 한다.
    		
    		rd2D.velocity = movement * movementSpeed;
    		// movement벡터와 movementSpeed을 곱한 결과를 velocity에 대입하면 플레이어가 움직인다.
    	}
    	
    	**private void UpdateState()
    	{
    		if(movement.x > 0)
    		{
    			animator.SetInteger(animationState, (int)CharStates.walkEast);
    		}**
    		else if (movement.x < 0)
    		{
    			animator.SetInteger(animationState, (int)CharStates.walkWest);
    		}
    		else if(movement.y > 0)
    		{
    			animator.SetInteger(animationState, (int)CharStates.walkNorth);
    		}
    		else if(movement.y < 0)
    		{
    			animator.SetInteger(animationState, (int)CharStates.idleSouth);
    		}
    	}
    	
    	public void SpawnPlayer()
    	{
    		if (PlayerSpawnPoint != null)
    		{
    			GameObject player = playerSpawnPoint.SpawnObject();
    			//playerSpawnPoint의 SpawnObject() 메서드를 호출해서 플레이어를 스폰하고, 만들어진 플레이어 인스턴스의 로컬 참조를 저장한다. 
    		}
    	}
    	
    	public void SetupScene()
    	{
    		SpawnPlayer();
    	}
    }

요약

  1. Spawn Point 스크립트를 통해 스폰할 오브젝트의 종류와 스폰할 위치를 정한다. PlayerSpawnPoint 인스턴스에 PlayerObject 프리팹의 참조를 설정했다.
  2. PRGGameManager 인스턴스의 PlayerSpawnPoint 속성을 설정한다.
  3. PRGGameManager의 SetupScene() 메서드에서 플레이어용 SpawnPoint 클래스의 SpawnObject()를 호출한다.

적의 스폰 위치

  1. SpawnPoint 프리팹을 씬으로 끌어다 놓는다.
  2. 이름을 EnemySpawnPoint로 변경한다.
  3. “Prefab To Spawn”속성에 EnemyObject 프리팹을 설정한다.
  4. 적을 10초마다 스폰하게 Repeat Interval 속성을 10으로 설정한다.

새로운 문제 발생

이러면 두둥. 문제 발생.

어떤 문제냐? 카메라가 플레이어를 따라다니지 않는다! 콰광…

왜냐하면 이제 시네머신 가상 카메라의 Follow 속성에 플레이어 프리팹 인스턴스를 설정하지 않고 플레이어를 동적으로 스폰하기 때문이다. 가상 카메라는 따라다닐 대상이 없어서 한 자리에 머문다.

카메라 매니저

다시 카메라가 플레이어를 따라다니게 하려면 카메라 매니저 클래스를 만들고 게임 매니저를 사용해서 가상 카메라를 적절하게 설정해야한다.

하이라이키 창에서 빈 게임 오브젝트를 새로 만드고 이름을 “PRGCameraManager”로 바꾼다. 프로젝트 창의 Scripts/Managers 폴더에 PRGCameraManager라는 스크립트를 생성하자. (얘도 싱글톤으로 만들거임.)

  • PRGCameraManager Script
    using UnityEngine;
    
    using Cinemachine;
    //Cinemachine 네임스페이스를 임포트하면 PRGCameraManager에서 시네머신 클래스와 데이터 형식을 사용할 수 있다.
    
    public class PRGCameraManager : MonoBehavior {
    	public static PRGCameraManager sharedInstance = null;
    	
    	[HideInInspector]
    	public CinemachineVirtualCamera virtualCamera;
    	// [HideInInspector]특성을 사용해서 유니티 에디터에 나타나지 않게 한다.
    	
    	void Awake()
    	{ // 싱글톤 패턴 구현.
    		if(sharedInstance != null && sharedInstance != this)
    		{
    			Destroy(gameObject);
    		}
    		else
    		{
    			sharedInstance = this;
    		}
    		
    		GameObject vCamGameObject = GameObject.FindWithTag("VirtualCamera");
    		/* 현재 씬에서 VirtualCamera라는 태그가 있는 게임 오브젝트를 찾는다. 하나의 게임 오브젝트에 각각 다른 기능을 제공하는 컴포넌트 여러개를 추가할 수 있다. 이런 방식을 "컴포지션 디자인 패턴"이라고 한다. */
    		
    		virtualCamera = v.CamGameObject.GetComponent<CinemachineVirtualCamera>();
    		/* 직교 크기, Follow 등 가상 카메라의 모든 속성은 유니티 에디터뿐만 아니라 스크립트를 통해서도 설정할 수 있다. 가상 카메라 컴포넌트를 저장해두면 코드를 통해 가상 카메라의 속성을 제어할 수 있다. */
    	}
    }

이제 PRGCameraManager를 Prefabs 폴더로 끌어다 놓아 프리팹을 만든다. 하이라이키 창의 인스턴스는 그대로 둔다! 아직 삭제 안함.

카메라 매니저 사용

  • PRGGameManager(수정)
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class MovementController : MonoBehavior
    {
    	public PRGCameraManager cameraManager;
    	public SpawnPoint playerSpawnPoint;
    	/* PlayerSpawnPoint 속성에 플레이어 스폰 위치의 참조를 저장하려고 한다. 플레이어가 죽으면 다시 스폰할 수 있게 플레이어용 스폰 위치의 참조는 계속 저장해두려 한다. */
    	 
    	public float movementSpeed = 3.0f;
    	Vector2 movement = new Vector2();
    	
    	Animator animator;
    	
    	string animationState = "AnimationState";
    	/* 하드코딩을 피하기 위해서. 문자열을 변수에 한번 대입해놓고 문자열을 참조해야 할 떼마다 이 변수를 사용해서 버그가 일어날 가능성을 원천차단해야 한다. */
    	
    	Rigidbody2D rb2D;
    	//Rigidbody2D의 참조를 저장할 변수를 선언한다.
    	
    	enum CharStates
    	{ /* "enum"데이터 형식을 사용하면 열거형 상수를 선언할 수 있다. 각 열거형 상수는 뒤에 적은 int 등의 값과 일치하므로 열거형을 참조하면 해당 값을 얻을 수 있다. */
    		walkEast = 1,
    		walkSouth = 2,
    		walkWest = 3,
    		walkNorth = 4,
    		idleSouth = 5
    	}
    	
    	private void Start()
    	{
    		rb2D = GetComponent<Rigidboidy 2D>();
    		/* GetComponent 메서드는 Type 형식의 파라미터를 입력 받아서 현재 게임 오브젝트가 지닌 컴포넌트 중에서 Type이 동일한 컴포넌트가 있으면 반환하는 메서드다. */
    		animator = GetComponent<Animator>();
    	}
    	
    	private void Update()
    	{
    		**UpDateState();
    		/* 애니메이션 상태 머신을 업데이트하려고 작성한 함수를 호출한다. 코드를 깔끔하게 유지하고 가독성을 높이고자 업데이트 로직을 별도의 메서드로 옮겼다. */** 
    	}
    	
    	void FixedUpdate()
    	{
    		**MoveCharacter();**
    	}
    	
    	private void MoveCharacter()
    	{
    		**movement.x = Input.GetAxisRaw("Horizontal");
    		movement.y = Input.GetAxisRaw("Vertical");**
    		/* Input 클래스는 사용자의 입력을 획득할 수 있는 방법을 제공한다. 이 코드는 GetAxisRaw() 메서드를 사용해서 사용자 입력을 획득하고 획득한 값을 Vector2 구조체의 x와 y 변수에 대입한다. GetAxisRaw() 메서드는 입력을 받을 2D 축을 지정하는 Horizontal 이나 Vertica(수직)을 파라미터로 받고, 유니티 입력 매니저가 전달하는 -1, 0, 1 을 반환한다. */
    		
    		movement.Normalize();
    		// 벡터를 정규화(normalize)해서 플레이어가 대각선, 수직, 수평, 어느 방향으로 움직이든 일정한 속력을 유지하게 한다.
    		
    		rd2D.velocity = movement * movementSpeed;
    		// movement벡터와 movementSpeed을 곱한 결과를 velocity에 대입하면 플레이어가 움직인다.
    	}
    	
    	**private void UpdateState()
    	{
    		if(movement.x > 0)
    		{
    			animator.SetInteger(animationState, (int)CharStates.walkEast);
    		}**
    		else if (movement.x < 0)
    		{
    			animator.SetInteger(animationState, (int)CharStates.walkWest);
    		}
    		else if(movement.y > 0)
    		{
    			animator.SetInteger(animationState, (int)CharStates.walkNorth);
    		}
    		else if(movement.y < 0)
    		{
    			animator.SetInteger(animationState, (int)CharStates.idleSouth);
    		}
    	}
    	
    	public void SpawnPlayer()
    	{
    		if (PlayerSpawnPoint != null)
    		{
    			GameObject player = playerSpawnPoint.SpawnObject();
    			//playerSpawnPoint의 SpawnObject() 메서드를 호출해서 플레이어를 스폰하고, 만들어진 플레이어 인스턴스의 로컬 참조를 저장한다. 
    			cameraManager.virtualCamera.Follow = player.transform;
    			/* virtualCamera의 Folloew 속성에 플레이어 오브젝트의 트랜스폼을 설정한다. 이러면 시네머신 가상 카메라가 다시 맵을 돌아다니는 플레이어를 따라다닌다. */
    		}
    	}
    	
    	public void SetupScene()
    	{
    		SpawnPlayer();
    	}
    }

PRG Game Manager의 인스턴스 창에서 Camera Manager에 PRGCameraManager 인스턴스를 드래그 앤 드롯한다. 그리고 PRGCameraManager 스크립트가 가상 카메라를 찾을 수 있게 가상 카메라에 “VirtualCamera”태그를 설정한다.

profile
농담곰을 좋아해요 말랑곰탱이

0개의 댓글