횡스크롤 러닝 게임이 있다고 해보자. 사용자가 입력할 수 있는 버튼은 점프와 슬라이딩이 있다. 그럼 캐릭터가 가질 수 있는 상태는 아래와 같을 것이다.


달리기(평소)

점프

슬라이딩

죽음


만약 게임이 업데이트가 되어서 상태들이 늘어날 수도 있을 것이다. 기획자에 따라서 점프 도중 한 번 더 점프를 할 수 있는 이중점프, 점프시 빠르게 땅으로 내려오는 급하강, 무적아이템을 먹었을 때의 무적상태 등등을 추가할 수도 있다. 그 때마다 다른 클래스에서 이 상태들을 사용할 때 이중점프.Do() 혹은 급하강.Do() 이런 식으로 직접 써준다면, 상태가 늘어날 때마다 그 상태들을 사용하는 클래스를 수정해야 할 것이다. 그것은 객체지향의 Open-Closed Principle(확장에는 열리게 하고, 수정에는 닫히게 해야 한다는 객체지향 원칙)을 어기는 것이다. 그래서 각 상태들을 IState라는 인터페이스로 묶어주고, IState.Do()로 실행하게 한다면, 상태가 아무리 추가가 되어도 IState 변수에 들어갈 상태 객체만 바꿔주면 되기 때문에 더이상 상태를 사용해주는 클래스를 수정하지 않아도 될 것이다.


이런식으로 객체들을 인터페이스로 묶어주고, 객체들의 멤버함수를 실행할 때 직접실행하는게 아니라 인터페이스.Do() 이런식으로 실행한다. 이렇게 OCP를 지치고 좀 더 유연하게 구조를 짜는 것이 전략패턴의 핵심이다. 그리고 Set함수를 만들어서 인터페이스 변수에 내가 원하는 객체를 넣어줄 수 있게 한다.


보통 게임프로그래밍을 본격적으로 시작할 때 가장 먼저 배우는 것 중 하나가 유한상태기계(FSM, Finite State Machine)일 것이다. 키보드 마우스의 입력에 따라서 캐릭터의 상태가 바뀌게 하고, 현재 상태에 따라 다른 행동이나 반응을 하게 하는 것, 그것이 캐릭터의 유한상태기계이다. 전략 패턴을 사용하면 유한상태기계를 객체지향적으로 만들 수 있다.


위의 횡스크롤 러닝 게임의 유한상태기계를 전략 패턴을 이용해서 간단하게 만들면 아래와 같다.






클래스 구조 설명 : 

Player는 유저가 조작하는 캐릭터이고,

StateMachine은 키보드 입력에 따라 상태를 바꿔주고 각 상태의 Operate 함수를 실행시켜주는, 상태들을 총괄해주는 역할이다.

IState 및 세부 상태들은 맨 위에서 말했듯이 각 상태들을 객체로 만들어준 것이다.



게임 플레이 시에 상태끼리 연결되는 과정 : 

Run 상태일 때 : z를 누르면 점프. X를 누르면 슬라이딩. 적에게 닿으면 죽음.

Jump 상태일 때 : 적에게 닿으면 죽음

Sliding 상태일 때 : z를 누르면 점프, 적에게 닿으면 죽음.

dead 상태일 때 : 키 입력 받지 않음. 마우스로 다시하기 버튼을 클릭하면 게임 리셋(여기서는 구현하지 않겠음).



OperateEnter, OperateUpdate, OperateExit는 무엇 ? :

간략하게 말하자면, Enter 함수에는 상태가 바뀌자마자 하고 싶은 행동을 넣고, Update는 매 프레임마다 하고 싶은 행동을, Exit에는 다른 상태로 바뀌기 직전에 하고 싶은 행동을 넣으면 된다. 보통 객체의 라이프 사이클을 짜다보면 Enter, Update, Exit 구조로 짜는 경우가 많다. 유니티에도 이러한 경우가 많은데, 충돌처리의 OnCollision 함수 시리즈에도 OnCollisionEnter, OnCollisionStay, OnCollisionExit 로 이루어져있고, 유니티의 MonoBehaviour에도 Start, Update OnDestroy 등등의 함수가 존재하는 걸 알 수 있다.



왜 Enter, Update, Exit가 필요? : 

예를 들어 Dead의 경우에는 상태가 바뀌자마자 캐릭터의 모습을 Dead.Png로 바꾸고, 다른 키 입력이 안되게만 하면 되므로 Enter만 필요하게 된다.

반면 Sliding에 경우에는 상태가 바뀌자마자 엎드려있는 모습으로 바꾸고, x버튼에서 손을 떼면 슬라이딩을 끝내고 다시 일어서는 모습으로 바꿔야하므로 Enter와 Exit가 둘 다 필요하다.

이렇게 상태별로 Enter, Exit, Update가 필요한 경우가 있으므로 원활한 게임 제작을 위해서 이런식으로 구조를 짜는 것이다. 




상세한 코드는 아래와 같다.



//플레이어블 캐릭터
public class Player : MonoBehaviour
{
    private enum PlayerState
    {
        Run,
        Sliding,
        Jump,
        Dead
    }

    private StateMachine stateMachine;

    //스테이트들을 보관
    private Dictionary<PlayerState, Istate> dicState = new Dictionary<PlayerState, Istate>();

    // Start is called before the first frame update
    void Start()
    {
        //상태 생성
        IState run = new StateRun();
        IState sliding = new StateSliding();
        IState jump = new StateJump();
        IState dead = new StateDead();

        //키입력 등에 따라서 언제나 상태를 꺼내 쓸 수 있게 딕셔너리에 보관
        dicState.Add(PlayerState.Run, run);
        dicState.Add(PlayerState.Sliding, sliding);
        dicState.Add(PlayerState.Jump, jump);
        dicState.Add(PlayerState.Dead, dead);

        //기본상태는 달리기로 설정.
        stateMachine = new StateMachine(run);
    }

    // Update is called once per frame
    void Update()
    {
        //키입력 받기
        KeyboardInput();

        //매프레임 실행해야하는 동작 호출.
        stateMachine.DoOperateUpdate();
    }

    //키보드 입력
    void KeyboardInput()
    {
        if(Input.GetKeyDown(KeyCode.Z))
        {
            //달리기, 슬라이딩 중일 때만 점프 가능
            if (stateMachine.CurrentState == dicState[PlayerState.Run] || stateMachine.CurrentState == dicState[PlayerState.Sliding])
            {
                stateMachine.SetState(dicState[PlayerState.Jump]);
            }
        }

        if(Input.GetKeyDown(KeyCode.X))
        {
            //달리기 중에만 슬라이딩 가능.
            if(stateMachine.CurrentState == dicState[PlayerState.Run])
            {
                stateMachine.SetState(dicState[PlayerState.Sliding]);
            }
        }
    }

    public void OnTriggerEnter(Collider other)
    {
        if(other.tag == "Enemy")
        {
            stateMachine.SetState(dicState[PlayerState.Dead]);
        }
    }
}



public class StateMachine
{
    //현재 상태를 담는 프로퍼티.
    public IState CurrentState { get; private set; }

    //기본 상태를 생성시에 설정하게 생성자 만들기.
    public StateMachine(IState defaultState)
    {
        CurrentState = defaultState;
    }

    //외부에서 현재상태를 바꿔주는 부분.
    public void SetState(IState state)
    {
        //같은 행동을 연이어서 세팅하지 못하도록 예외처리.
        //예를 들어, 지금 점프중인데 또 점프를 하는 무한점프 버그를 예방할수도 있다.
        if(CurrentState == state)
        {
            Debug.Log("현재 이미 해당 상태입니다.");
            return;
        }

        //상태가 바뀌기 전에, 이전 상태의 Exit를 호출한다.
        CurrentState.OperateExit();

        //상태 교체.
        CurrentState = state;
        
        //새 상태의 Enter를 호출한다.
        CurrentState.OperateEnter();
    }
    
    //매프레임마다 호출되는 함수.
    public void DoOperateUpdate()
    {
        CurrentState.OperateUpdate();
    }
}



//상태들의 최상위 인터페이스.
public interface IState
{
    void OperateEnter();
    void OperateUpdate();
    void OperateExit();
}



//각 세부 상태들은 굳이 정의하지 않겠다.
public class StateSliding : IState
{
    public void OperateEnter()
    {
        
    }

    public void OperateExit()
    {
        
    }

    public void OperateUpdate()
    {
        
    }
}


메멘토는 라틴어로서 기억이라는 뜻을 가진다.

이전 데이터를 기억하고 때로는 복원하는 기능을 메멘토 패턴이라고 한다.


메멘토 패턴을 쓰는 경우는 많이 있지만, 몇가지 예시를 들겠다.

1. 캐릭터 장비를 새로 세팅했다가 [취소]버튼을 누르는 경우, 예전 세팅으로 돌아가기

2. 액션게임에서 캐릭터가 죽으면 이전 세이브 시점의 장소와 레벨로 돌아가기


이 외에도 많은 경우에 이 패턴을 쓸 수 있다. 요점은 데이터를 저장하고 필요시 그 데이터를 복원하는 것이다.




예제에서는 캐릭터가 죽으면 이전 세이브 시점의 데이터를 복원하는 것을 해보겠다.



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

//플레이어의 레벨과 자동저장된 곳의 이름을 담을 수 있는 데이터 클래스.
public class AutoSaveData
{
    public int level = -1;
    public string location = "시작의 마을";

    public AutoSaveData()
    {

    }

    public AutoSaveData(int level, string location)
    {
        this.level = level;
        this.location = location;
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//오토 세이브 데이터를 기억하고 복원해주는 관리 클래스
public class AutoSaveManager
{
    //AutoSaveManager는 자주 쓰이고 
    //게임 내에 하나만 존재할 것이므로 싱글톤으로 구현.
    private static AutoSaveManager instance = null;

    public static AutoSaveManager Instance
    {
        get
        {
            if(instance == null)
            {
                instance = new AutoSaveManager();
            }
            return instance;
        }
    }

    private List autoSaveDataList = new List();


    //오토 세이브 데이터를 리스트에 넣어 보존.
    public void PushAutoSaveData(AutoSaveData data)
    {
        if(autoSaveDataList == null)
        {
            autoSaveDataList = new List();
        }
        autoSaveDataList.Add(data);
    }

    //오토 세이브 데이터를 리스트에서 빼오고 리스트에서 삭제. 
    public AutoSaveData PopLastAutoSaveData()
    {
        AutoSaveData data = GetLastAutoSaveData();

        //최근 데이터가 존재한다면 list에서 삭제
        if (data != null)
        { 
            autoSaveDataList.Remove(data);
        }

        return data;
    }

    //가장 최근 오토 세이브 데이터 불러오기.
    public AutoSaveData GetLastAutoSaveData()
    {
        //최소한의 예외처리
        if(!CheckListUsable())
        {
            Debug.Log("데이터 불러오기 실패 : GetLastAutoSaveData");
            return null;
        }

        //프로그래밍 언어는 숫자를 셀 때 0부터 시작하므로, 마지막 세이브 데이터의 순번은 1을 빼준다.
        return autoSaveDataList[autoSaveDataList.Count - 1];
    }

    //특정위치의 오토 세이브 데이터 불러오기
    public AutoSaveData GetAutoSaveDataAt(int index)
    {
        //최소한의 예외처리
        if (!CheckListUsable())
        {
            Debug.Log("데이터 불러오기 실패 : GetAutoSaveDataAt.");
            return null;
        }
        else if (autoSaveDataList.Count <= index)
        {
            Debug.Log("요청한 인덱스가 리스트의 크기를 넘어섬.");
            return null;
        }

        return autoSaveDataList[index];
    }

    //autoSaveDataList가 정의되어있는지, 혹은 비어있는지 체크
    private bool CheckListUsable()
    {
        if (autoSaveDataList == null)
        {
            Debug.Log("오토 세이브 데이터 리스트가 정의되지 않음.");
        }
        else if (autoSaveDataList.Count == 0)
        {
            Debug.Log("오토 세이트 데이터 리스트에 저장된 데이터가 없음.");
        }
        else
        {
            return true;
        }

        return false;
    }

    //데이터 클리어
    public void ClearAutoSaveDataList()
    {
        autoSaveDataList.Clear();
    }  
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//실사용 예. 실제로는 자동저장 시점에 데이터를 저장해주고, 플레이어가 죽으면 복원하면 될 것이다.
public class SceneGame : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        AutoSaveData data = new AutoSaveData(1, "마을 입구");
        AutoSaveManager.Instance.PushAutoSaveData(data);

        AutoSaveData lastData = AutoSaveManager.Instance.GetLastAutoSaveData();
        Debug.Log(lastData.level + ", " + lastData.location);
    }
}




참고 - Pop과 Get

이전 데이터를 복원할 때 2가지 방법이 있다.

1. 이전 데이터를 복원하고 그 데이터를 더이상 기억하지 않는다(1회성 기억). = pop기능

2. 이전 데이터를 복원하고 그 데이터를 계속 남겨둔다. 한번 기억한 데이터는 원할 때 몇번이고 복원한다(지속성 기억). = get기능


pop과 get은 관용인 표현이며, 어느 때는 그 경계가 모호하다. 

보통 자료구조에서 stack이나 queue를 공부할 때 push와 pop이라는 이름을 사용하며, push는 데이터를 배열(또는 벡터)에 넣는 기능, pop은 데이터를 배열에서 빼내오는(그리고 배열에서 삭제함) 기능을 뜻한다.

get set은 유니티 프로그래머라면 property를 공부할 때 보게 될텐데, get은 property 들어있는 값을 가져오는 기능, set은 데이터를 property에 값을 넣어주는 기능이다.


push와 set이 비슷하고,

pop과 get이 비슷한 개념인 셈이다.


그래서 나는 pop을 사용하면 데이터를 빼오면서 배열에서 삭제해주고, get은 데이터를 배열에서 삭제하지 않는 식으로 함수의 이름을 지어준다. 이건 동료 프로그래머와의 소통을 위해 주석을 달아주어야 하는 문제라고 생각한다. 


자신의 게임에 어느 방식이 좋은지는 스스로 선택하도록 한다. 또는 pop과 get 2가지 함수를 모두 만들어줘도 상관 없을것이다.


옵저버 패턴은 실제 개발할 때 많이 쓰이는 패턴 중에 하나이다. 

 

옵저버 패턴은 하나의 서브젝트(subject)와 다수의 옵저버(observer)들 사이의 상호작용을 좀 더 결합도는 낮게, 응집도는 높게 만드려는 노력의 결과물이다.

서브젝트에서 어떤 일이 일어난다면, 다수의 옵저버들에게 그 일이 발생했다는 것을 알려주고, 옵저버들은 그 일에 반응하여 모종의 행동을 하게 되는 구조이다.

 

게임을 하나 예로 들어보자. 어떤 미로형 탈출 게임에서 맵 중앙에 있는 한 개의 스위치를 누르면 방의 여러개 있는 문들이 열리거나 닫힌다고 해보자. 어떤 문은 열리고, 어떤 문은 닫히고, 어떤 문은 특정 조건일 때만 열리거나 닫힌다. 갑자기 괴물이 튀어나와서 특정 방에 유인해서 가둬야 하기도 하는 등 스위치 컨트롤이 핵심인 게임이라고 해두자. 여기서 중요한 것은 스위치를 누르는 것은 한 번이지만, 문 별로 반응하는 것이 다르다는 게 핵심이다.

 

옵저버 패턴에서 중요한 점은 open/closed principle(OCP)이다. 문의 종류나 갯수를 추가하더라도 스위치버튼 클래스의 내부를 수정하지 않아도 되게 하는 점이 바로 포인트이다. 

 

일단 문의 종류를 정해보자.

 

연한 남색 : 스위치를 한 번 누르면 열리거나 닫히는 일반문.

보라색 : 스위치를 두 번 눌러야 열리거나 닫히는 무거운 문.

연두색 : 연두색 열쇠를 얻어야 스위치에 반응하는 문.

노란색 : 노란색 열쇠를 얻어야 스위치에 반응하는 문.

진한 남색 : 보스를 쓰러뜨려야 열리는 다음 챕터로 가는 문.

 


 

 

만약 옵저버 패턴을 쓰지 않는다면, 아래와 같이 해야할 것이다(물론 일부러 노골적으로 불편하게 만들어보았다).

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

public class SwitchButton : MonoBehaviour
{
    public SkyBlueDoor skyblue1;
    public SkyBlueDoor skyblue2;

    public VioletDoor violet1;
    public VioletDoor violet2;

    public GreenDoor green1;

    public YellowDoor yellow1;

    public BlueDoor blue1;

    //스위치 눌렀을 때
    public void OnClickSwitch()
    {
        skyblue1.OepnOrClose();
        skyblue2.OepnOrClose();

        violet1.OepnOrClose();
        violet2.OepnOrClose();

        green1.OepnOrClose();

        yellow1.OepnOrClose();
        
        blue1.OepnOrClose();
    }
}

 

 

이렇게 되면 작동은 할 것이다. 하지만 굉장히 비효율 적이다. 왜냐하면 문을 더 만들거나 종류가 늘어났을 때마다 서브젝트(여기서는 SwitchButton 클래스)를 항상 수정해야 하기 때문이다. 이것은 객체지향이 원하는 바가 아니다.

 

그래서 최종적으로는 아래와 같이 수정해주도록 한다.

 

1. 문들은 Door라는 추상클래스로 묶어준다.

2. SwitchButton 클래스는 Door를 배열이나 리스트로 관리해주며, 문이 늘어나던 말던 SwitchButton 클래스는 신경 안쓴다.

3. 버튼이 눌리면 그 눌렸다는 사실을 각 문들에게 인지시켜주고, 문들은 그에 따라 열리거나 닫힌다.

 

 

 

우선 스위치 클래스는 아래와 같다.

public class SwitchButton : MonoBehaviour
{
    public List<Door> doors = new List<Door>();

    public void AddDoor(Door door)
    {
        if (doors.Contains(door))
        {
            Debug.Log("이미 해당 문이 리스트에 존재하고 있음.");
        }
        else
        {
            doors.Add(door);
        }
    }

    public void RemoveDoor(Door door)
    {
        if(doors.Contains(door))
        {
            doors.Remove(door);
        }
        else
        {
            Debug.Log("문이 리스트에 존재하지 않아서 제거하지 못함.");
        }
    }

    public void OnClickSwitchButton()
    {
        NotifyDoors();
    }

    public void NotifyDoors()
    {
        foreach(Door d in doors)
        {
            d.OpenOrClose();
        }
    }
}

 

 

아래는 Door라는 추상클래스이다.

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

public abstract class Door : MonoBehaviour
{
    public abstract void OpenOrClose();
}

 

 

그리고 Door를 상속받아서 여러가지 문을 제작하면 된다. 여기서는 인벤토리에 노란열쇠가 있으면 열리는 노란문만 구현하도록 하겠다.

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

public class DoorYellow : Door
{
    bool isClosed;
    public override void OpenOrClose()
    {
        if(Inventory.Instance.HasItem("YellowKey"))
        {
            if(isClosed)
            {
            	isClosed = false;
                Oepn();
            }
            else
            {
            	isClosed = true;
                Close();
            }
        }
    }

    void Open()
    {
		//문 열기 구현
    }

    void Close()
    {
		//문 닫기 구현
    }
}

 

 

이제 문의 갯수가 추가되면 AddDoor 함수를 사용하면 되고, 문의 종류가 늘어나면 추상클래스 Door를 상속받아서 새로 제작하면 된다. OCP도 어기지 않고, 스위치를 누르면 각 문들에게 통지를 해주게 된다.

AddDoor나 RemoveDoor는 문쪽에서 Start함수 같은데에서 직접 할 수도 있고, 아니면 스위치가 하나라는 전제 하에 맵디자인을 관장하는 클래스에서 해줘도 될 것이다.

 

 

한걸음 더 나아가서, C#의 delegate를 사용하는 방법도 있다. delegate를 사용하면 SwitchButton이 Door 객체를 참조하는게 아니고, OpenOrClose()함수를 참조하게 된다. delegate에 대해서는 여기서 다루지 않는다.

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

public class SwitchButton : MonoBehaviour
{
	//리턴타입이 void이며 매개변수가 없는 함수형을 담을 틀을 만들어준다.
    public delegate void DoorNotification();

	//OpenOrClosed 함수들을 넣어줄 delegate.
    public event DoorNotification doorNotification;

	//매개변수가 DoorNotification 타입이지만 리턴타입이 void이고 매개변수가 없는 함수는 뭐든 받을 수 있다.
    public void AddDoor(DoorNotification notification)
    {
        //중복체크
        foreach (DoorNotification n in doorNotification.GetInvocationList())
        {
            if (n == notification)
            {
                Debug.Log("이미 해당 OpenOrClose 함수가 리스트에 존재하고 있음.");
                return;
            }  
        }

        doorNotification += notification;
    }

    public void RemoveDoor(DoorNotification notification)
    {
        //있는지 체크
        foreach (DoorNotification n in doorNotification.GetInvocationList())
        {
            if (n == notification)
            {
                doorNotification -= notification;
                return;
            }
        }

        Debug.Log("해당 OpenOrClose 함수가 리스트에 존재하지 않아서 제거하지 못함.");
    }

    public void OnClickSwitchButton()
    {
        NotifyDoors();
    }

    public void NotifyDoors()
    {
        //추가되어있던 OpenOrClosed()함수들을 일제히 호출.
        doorNotification.Invoke();
    }
}

싱글톤 패턴은 초보 개발자들이 가장 많이 쓰는 디자인 패턴이 아닐까 싶다. 

 

클래스 구조를 짜다보면 다른 클래스의 함수를 사용해야 할 수도 있고, 전체 클래스들이 공유하는 전역변수가 필요할 수도 있다.

규모가 작은 게임에서는 public으로 변수를 만든 다음 유니티 Inspector에서 드래그 앤 드롭으로 의존관계를 만들수도 있지만,

게임이 점점 복잡해진다면 다른 클래스를 참조하는 변수가 너무 많이 만들어져도 문제이다.

public 변수를 만든거 자체가 메모리를 사용하게 되는 것이고, 만약 클래스를 변경하거나 삭제할 때 일일이 다 바꿔줘야 해서 개발할 때 시간낭비가 될 수도 있다.

 

클래스 구조에서 공통적으로 사용하는 전역변수나 리소스, 데이터, 아니면 게임 전체를 관장하는 매니저 클래스는 싱글톤으로 따로 빼는게 도움이 될 수도 있다.

싱글톤을 이용하면 임의의 클래스에서 내가 만든 싱글톤 인스턴스를 사용할 수 있다.

 

 

유니티에서 싱글톤을 사용하는 방법은 2가지가 있다.

1. 이 싱글톤 클래스가 여느 유니티 c#스크립트처럼 Monobehaviour를 상속받아서 Hierarchy에 존재하게 하는 것.

2. Monobehaviour을 상속받지 않고 Hierarchy에 존재하지 않게 하는 것.

 

본 예제에서는 게임시작, 일시정지 등 게임의 흐름을 관장하는 GameMgr이라는 클래스를 예시로 들겠다.

 

1번째 방법(Monobehaviour를 상속받아서 Hierarchy에 존재하게 되는 싱글톤 인스턴스)

 

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

public class GameMgr : MonoBehaviour
{
    //게임매니저의 인스턴스를 담는 전역변수(static 변수이지만 이해하기 쉽게 전역변수라고 하겠다).
    //이 게임 내에서 게임매니저 인스턴스는 이 instance에 담긴 녀석만 존재하게 할 것이다.
    //보안을 위해 private으로.
    private static GameMgr instance = null;

    void Awake()
    {
        if (null == instance)
        {
            //이 클래스 인스턴스가 탄생했을 때 전역변수 instance에 게임매니저 인스턴스가 담겨있지 않다면, 자신을 넣어준다.
            instance = this;

            //씬 전환이 되더라도 파괴되지 않게 한다.
            //gameObject만으로도 이 스크립트가 컴포넌트로서 붙어있는 Hierarchy상의 게임오브젝트라는 뜻이지만, 
            //나는 헷갈림 방지를 위해 this를 붙여주기도 한다.
            DontDestroyOnLoad(this.gameObject);
        }
        else
        {
            //만약 씬 이동이 되었는데 그 씬에도 Hierarchy에 GameMgr이 존재할 수도 있다.
            //그럴 경우엔 이전 씬에서 사용하던 인스턴스를 계속 사용해주는 경우가 많은 것 같다.
            //그래서 이미 전역변수인 instance에 인스턴스가 존재한다면 자신(새로운 씬의 GameMgr)을 삭제해준다.
            Destroy(this.gameObject);
        }
    }

    //게임 매니저 인스턴스에 접근할 수 있는 프로퍼티. static이므로 다른 클래스에서 맘껏 호출할 수 있다.
    public static GameMgr Instance
    {
        get
        {
            if (null == instance)
            {
                return null;
            }
            return instance;
        }
    }

    public void InitGame()
    {

    }

    public void PauseGame()
    {

    }

    public void ContinueGame()
    {

    }

    public void RestartGame()
    {

    }

    public void StopGame()
    {

    }
}

 

사용 예시 :

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

public class UIMenu : MonoBehaviour
{
    //pause 버튼을 누르면 게임 일시정지
    public void OnClickBtnPause()
    {
        GameMgr.Instance.PauseGame();
    }
}

 

 

2번째 방법 (Monobehaviour를 상속받지 않아서 Hierarchy에 존재하지 않게 만드는 방법)은 아래와 같다.

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

public class GameMgr
{
    //게임매니저의 인스턴스를 담는 전역변수(static 변수이지만 이해하기 쉽게 전역변수라고 하겠다).
    //이 게임 내에서 게임매니저 인스턴스는 이 instance에 담긴 녀석만 존재하게 할 것이다.
    //보안을 위해 private으로.
    private static GameMgr instance;

    //게임 매니저 인스턴스에 접근할 수 있는 프로퍼티. static이므로 다른 클래스에서 맘껏 호출할 수 있다.
    public static GameMgr Instance
    {
        get
        {
            if(null == instance)
            {
                //게임 인스턴스가 없다면 하나 생성해서 넣어준다.
                instance = new GameMgr();
            }
            return instance;
        }
    }

    //생성자를 하나 만들어줘서 원하는 세팅을 해주면 좋다.
    public GameMgr()
    {

    }

    public void InitGame()
    {

    }

    public void PauseGame()
    {
       
    }

    public void ContinueGame()
    {

    }

    public void RestartGame()
    {

    }

    public void StopGame()
    {

    }
}

 

사용법은 1번째 방법과 같다.

 

 

 

Monobehaviour를 상속받지 않는 경우의 좋은 점 :

 

1. 우선 씬 이동시의 신경을 안 써도 된다. 씬 이동을 했을 때 그 씬의 Hierarchy에 같은 싱글톤 클래스가 존재한다면, 기존씬에서 쓰던 인스턴스를 계속 쓸지, 아니면 새로운 씬의 Hierarchy에 있는 인스턴스를 쓸지를 선택해야한다(보통은 기존 씬의 것을 사용하는 것 같다). 하지만 Monobehaviour를 상속받지 않고 메모리상에만 존재하게 한다면 이런 선택의 경우를 고려하지 않아도 돼서 편하다.

2. 현재 상용버전의 유니티 오브젝트라면 모두 갖게 될 Transform 컴포넌트를 안 가져도 되니 쓸데 없는 메모리 점유를 안해도 된다는 것이다(정말 미미한 극세사 도움이겠지만..). 하지만 눈에 안 보이면 헷갈릴 수도 있으니 보통은 1번째 방법을 사용한다.

 

 

 

 

싱글톤의 문제점 :

하나의 싱글톤에 너무 많은 기능, 너무 많은 데이터를 넣으면, 훗날 프로젝트의 규모가 커졌을 때 절망을 느낄 수 있다. 우선 하나의 클래스가 하나의 일을 한다는 Single Responsibility Principle과, 수정에는 닫히고 확장에는 열려야 한다는 Open-Closed Principle 등의 원칙을 어길 수 있으며, 클래스들과 싱글톤, 그리고 싱글톤이 가지고 있는 클래스 인스턴스들간의 의존도가 복잡해져서 게임 업데이트를 하려면 게임 전체를 갈아엎어야 될 수도 있다. 또한 싱글톤은 게임이 종료되지 않는 한 계속해서 메모리를 점유하고 있으므로, 싱글톤의 남발은 메모리를 비효율적으로 사용하게 한다.

+ Recent posts