본문 바로가기
자료

Mini Kingdom 자료 - 2021 홍익대학교 졸업 프로젝트

by 소리쿤 2021. 12. 1.

 

소스 코드 GitHub Repository ( 전체 시스템 구조도 포함 )

https://github.com/CodeMonkeys-Graduation/Graduation-Project

 

 

GitHub - CodeMonkeys-Graduation/Graduation-Project: [✔Complete] Graduation-Project

[✔Complete] Graduation-Project. Contribute to CodeMonkeys-Graduation/Graduation-Project development by creating an account on GitHub.

github.com


GameInstance (매니저 생성기)

 

  1. 자신이 존재할 Scene에 대한 정보를 가진 매니저 프리팹을 Dictionary 형태로 가집니다.
  2. Awake()에서는 우선 모든 매니저들을 생성합니다.
  3. Scene이 새로 로드되면, OnEnable()에서 매니저 생성 코루틴을 수행합니다. (Awake 후속이며 매 씬마다 불림)
  4. 생성과 삭제가 같은 프레임에서 수행되면 안되므로, 코루틴을 사용했습니다.
  5. 아래 코드에선 우선 모든 매니저를 파괴하며, 씬에 맞는 매니저만 생성합니다. (경우에 따라 매니저 유지가 가능)
using ObserverPattern;
using RotaryHeart.Lib.SerializableDictionary;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

/// <summary>
/// 게임의 시작부터 끝까지 존재하는 게임오브젝트입니다.
/// Manager들의 Lifecycle을 관리합니다.
/// </summary>

public class GameInstance : SingletonBehaviour<GameInstance>
{
    [System.Serializable]
    public class ManagerDictionary : SerializableDictionaryBase<ManagerType, ManagerBehaviour> { }

    public enum ManagerType
    {
        NONE,

        ActionPlanner,
        BattleMgr,
        CameraMgr,
        EventMgr,
        MapMgr,
        Pathfinder,
        TurnMgr,
        UIMgr,
        SceneMgr,
        AudioMgr,
        CinematicDialogMgr
    }

    [SerializeField] public ManagerDictionary ManagerPrefabs;

    protected override void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
        }
        else
        {
            Instance = this;
        }

        DontDestroyOnLoad(gameObject);

        foreach (var Mgr in ManagerPrefabs)
            Instantiate(Mgr.Value);
    }

    private void OnEnable()
    {
        SceneManager.sceneLoaded += OnSceneLoaded;
    }

    private void OnDisable()    
    {
        SceneManager.sceneLoaded -= OnSceneLoaded;
    }

    private void OnSceneLoaded(Scene scene, LoadSceneMode loadSceneMode)
    {
        Debug.Log($"OnSceneLoaded");
        Debug.Assert(SceneMgr.sceneMap.TryGetValue(scene.name, out _));

        // Coroutine으로 하는 이유는 모든 매니저를 파괴하고 
        // 라이프 사이클인 매니저만 다시 스폰해야 하는데,
        // 이를 한 프레임에 하면 모든 매니저를 파괴하는 것으로 마킹하면 Instaniate이 안먹힌다.
        // 그래서 모든 매니저를 파괴하고 한 프레임을 쉬어줘야하여 코루틴으로 한다.
        StartCoroutine(UpdateAllManagersInTwoFrames(scene));
    }

    private IEnumerator UpdateAllManagersInTwoFrames(Scene scene)
    {
        foreach (var mgr in ManagerPrefabs)
        {
            if (mgr.Value.GetInstance() != null)
            {
                Destroy(mgr.Value.GetInstance().gameObject);
            }
        }

        // 한 프레임을 쉬어줘야 매니저들이 파괴됩니다.
        yield return null;

        foreach (var mgr in ManagerPrefabs)
        {
            // 이 Manager의 LifeCycle에 해당하는 Scene임.
            if (mgr.Value.LifeCycles.Contains(SceneMgr.sceneMap[scene.name]))
            {
                if (mgr.Value.GetInstance() == null)
                {
                    Instantiate(mgr.Value);
                }
            }
        }
    }

}

턴 매니징 등에 사용할 StateMachine 구현

 

  1. StateMachine은 Input에 따라 상태가 변하고, 상태에 따라 다른 역할을 수행해야할 때 사용하는 패턴입니다.
  2. Generic하게 사용할 수 있도록 제작하였으며, State를 담을 자료 구조는 스택으로 하였습니다. (PrevState로 돌아가기 위함)
  3. 제작한 StateMachine은 스택의 top에 해당하는 State의 함수만을 수행합니다.
  4. StateMachine의 Update문의 첫 프레임에 첫 State의 Enter()가 호출되며, 그 다음부턴 Execute()가 호출됩니다.
  5. State가 변화된 후, 이전 State의 Exit()가 수행되고, 현재 State의 Enter()가 호출됩니다.
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;

public class StateMachine<T>
{
    public T owner;

    public enum StateTransitionMethod { PopNPush, JustPush, ReturnToPrev, ClearNPush }

    public Stack<State<T>> stateStack = new Stack<State<T>>();
    public int StackCount { get => stateStack.Count; }
    Type defaultState;

    private bool isStarted = false;

    private bool isActive = true;

    public StateMachine(State<T> initialState)
    {
        stateStack.Push(initialState);
        defaultState = initialState.GetType();
    }

    public void Run()
    {
        if (!isActive) return;

        if (!isStarted)
        {
            stateStack.Peek().Enter();
            isStarted = true;
        }
        else
        {
            stateStack.Peek().Execute();
        }
        
    }

    public void SetActive(bool active) => isActive = active;

    public bool IsStateType(System.Type type) => stateStack.Peek().GetType() == type;

    public void ChangeState(State<T> nextState, StateTransitionMethod method)
    {
        if (!isActive) return;

        switch (method)
        {
            case StateTransitionMethod.PopNPush:
                {
                    State<T> prevState = stateStack.Peek();
                    prevState.Exit();
                    stateStack.Pop();
                    stateStack.Push(nextState);
                    nextState.Enter();
                    break;
                }
                
            case StateTransitionMethod.JustPush:
                {
                    State<T> prevState = stateStack.Peek();
                    prevState.Exit();
                    stateStack.Push(nextState);
                    nextState.Enter();
                    break;
                }

            case StateTransitionMethod.ReturnToPrev:
                {
                    State<T> currState = stateStack.Peek();
                    currState.Exit();
                    stateStack.Pop();
                    State<T> prevState = stateStack.Peek();
                    prevState.Enter();
                    break;
                }

            case StateTransitionMethod.ClearNPush:
                {
                    State<T> currState = stateStack.Peek();
                    currState.Exit();
                    stateStack.Clear();
                    stateStack.Push(nextState);
                    nextState.Enter();
                    break;
                }
        }

    }

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

public abstract class State<T>
{
    public T owner;
    public State(T owner)
    {
        this.owner = owner;
    }
    /// <summary>
    /// State에 Enter할때 한번 호출합니다.
    /// </summary>
    public abstract void Enter();

    /// <summary>
    /// owner의 StateMachine에서 프레임마다 호출합니다.
    /// </summary>
    public abstract void Execute();

    /// <summary>
    /// State에서 Exit할때 한번 호출합니다.
    /// </summary>
    public abstract void Exit();
}

옵저버 패턴을 위한 EventListener 래핑 및 Event 구현



1. 원하는 Event를 추상화하여 Scritable Object로 만듭니다. (원활한 관리를 위함)
2. Event는 Event Listener(Unity Event 래퍼)를 받아 대신 콜백을 등록해주거나, 해제해줍니다.

3. 그러므로 Event 구독을 원하는 객체는 반드시 EventListener을 갖고, 콜백과 함께 Event에 제공합니다.
4. 싱글톤 형태의 EventMgr을 통해 Event를 Invoke()하여 발생시켜 사용합니다.

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

public interface EventParam {  }

[CreateAssetMenu(order = 0, fileName = "E_OnXXX", menuName = "New Event")]
public class Event : ScriptableObject
{
    List<EventListener> eListeners = new List<EventListener>();
    [SerializeField] public int listenerCount = 0;
    
    public void Register(EventListener l, UnityAction<EventParam> action)
    {
        l.OnNotify.AddListener(action);
        eListeners.Add(l);

        listenerCount = eListeners.Count;
    }


    public void Unregister(EventListener l)
    {
        eListeners.Remove(l);

        listenerCount = eListeners.Count;

    }

    public void Invoke(EventParam param = null)
    {
        //eListeners.ForEach(l => l.OnNotify.Invoke());
        foreach (var l in eListeners.ToArray())
            l.OnNotify.Invoke(param);

        listenerCount = eListeners.Count;
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class CustomNotify : UnityEvent<EventParam> { }

public class EventListener
{
    public CustomNotify OnNotify = new CustomNotify();
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class EventMgr : SingletonBehaviour<EventMgr> // 등록할 수 있는 이벤트를 싱글톤으로 관리하는 매니저
{
    [Header("SceneChanged")]
    [SerializeField] public ObserverEvent OnSceneChanged;

    [Header("PathFinding Event")]
    [SerializeField] public ObserverEvent onPathfindRequesterCountZero;
    [SerializeField] public ObserverEvent onPathUpdatingStart;

    [Header("Positioning Event")]
    [SerializeField] public ObserverEvent onUnitInitEnd;

    [Header("Unit Event")]
    [SerializeField] public ObserverEvent onUnitCommandResult;
    [SerializeField] public ObserverEvent onUnitAttackExit;
    [SerializeField] public ObserverEvent onUnitDeadEnter;
    [SerializeField] public ObserverEvent onUnitDeadExit;
    [SerializeField] public ObserverEvent onUnitIdleEnter;
    [SerializeField] public ObserverEvent onUnitItemExit;
    [SerializeField] public ObserverEvent onUnitRunEnter;
    [SerializeField] public ObserverEvent onUnitRunExit;
    [SerializeField] public ObserverEvent onUnitSkillExit;
    [SerializeField] public ObserverEvent onUnitDeadCountZero;

    [Header("UI Event")]
    [SerializeField] public ObserverEvent onUICreated;
}