C#/수업 내용

Unity) [유니티 3D] 게임 진입까지 구조

HSH12345 2023. 2. 27. 17:52
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneArgs { }
public class GameSceneArgs : SceneArgs
{
    public int selectedCharacterId;
}

public class App : MonoBehaviour
{
    public enum eSceneType
    {
        App, Title, Loading, Lobby, Game
    }

    private TitleMain titleMain;

    private void Awake()
    {
        DontDestroyOnLoad(this.gameObject);
    }

    private void Start()
    {
        DataManager.instance.LoadCharacterData();
        DataManager.instance.LoadAssetData();

        this.ChangeScene(eSceneType.Title);
    }

    public void ChangeScene(eSceneType sceneType, SceneArgs args = null)
    {

        Debug.LogFormat("ChangeScene: {0}", sceneType);

        var oper = SceneManager.LoadSceneAsync(sceneType.ToString());

        switch (sceneType)
        {
            case eSceneType.Title:
                //비동기 로드 
                oper.completed += (obj) => {
                    //씬로드가 완료됨 (메모리에 다 올라감)
                    //인스턴스에 접근 가능 
                    this.titleMain = GameObject.FindObjectOfType<TitleMain>();
                    this.titleMain.uiDirector.onClick = () => {
                        this.ChangeScene(eSceneType.Loading);
                    };
                    this.titleMain.Init();
                };
                break;

            case eSceneType.Loading:
                oper.completed += (obj) => {
                    var loadingMain = GameObject.FindObjectOfType<LoadingMain>();
                    loadingMain.onComplete = () =>
                    {
                        this.ChangeScene(eSceneType.Lobby);
                    };
                    loadingMain.Init();
                };
                break;

            case eSceneType.Lobby:
                oper.completed += (obj) =>
                {
                    var lobbyMain = GameObject.FindObjectOfType<LobbyMain>();
                    lobbyMain.onClickStartGame = (selectedId) =>
                    {
                        var args = new GameSceneArgs() { selectedCharacterId = selectedId };
                        this.ChangeScene(eSceneType.Game, args);
                    };
                    lobbyMain.Init();
                };
                break;

            case eSceneType.Game:
                oper.completed += (obj) =>
                {
                    var gameSceneArgs = args as GameSceneArgs;
                    var gameMain = GameObject.FindObjectOfType<GameMain>();
                    gameMain.Init(gameSceneArgs.selectedCharacterId);
                };
                break;
        }
    }
}

 - as 연산자 : 형변환이 가능하면 변환 값을 반환하고 그렇지 않으면 null 값을 반환

 - is 연산자 : 형변환 가능 여부를 boolean 타입으로 반환

 -> 실제 하향캐스팅에서 사용된 예제를 확인할 필요가 있음

 

 

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

public class TitleMain : MonoBehaviour
{
    public UITitleDirector uiDirector;

    void Start()
    {
        
    }

    public void Init()
    {
        this.uiDirector.Init();
        this.uiDirector.FadeIn();
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using DG.Tweening;

public class UITitleDirector : MonoBehaviour
{
    public Image dim;
    public Text txtVersion; //0.0.1
    public Button btn;

    public System.Action onClick;

    public void Init()
    {
        this.txtVersion.text = string.Format("v{0}", Application.version);
        var color = this.dim.color;
        color.a = 1;
        this.dim.color = color;
        this.dim.raycastTarget = true;
        this.btn.onClick.AddListener(() =>
        {
            this.btn.interactable = false;
            this.dim.raycastTarget = false;
            this.onClick();
        });
        this.btn.gameObject.SetActive(false);               
    }

    public void FadeIn()
    {
        DOTween.ToAlpha(() => this.dim.color, x => this.dim.color = x, 0, 1f).SetEase(Ease.OutQuad)
            .onComplete = () =>
            {
                Debug.Log("FadeIn Complete!");
                this.btn.gameObject.SetActive(true);
                this.dim.raycastTarget = false;
            };
    }

    public void FadeOut()
    {
        DOTween.ToAlpha(() => this.dim.color, x => this.dim.color = x, 1, 1f).SetEase(Ease.OutQuad)
    .onComplete = () =>
    {
        Debug.Log("FadeOut Complete!");
    };
    }
}

 - interactable : 버튼의 상호작용 가능여부를 boolean타입으로 지정한다.

 - DoTween 네임스페이스를 참조하여 메서드를 호출할 수 있는데 순차적으로 변화하는 값을 구현할 수 있다. 현재 코드에서는 Color의 알파값을 Ease그래프에 따라 순차적으로 변화시킨다.

 

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

public class LoadingMain : MonoBehaviour
{
    public UILoadingDirector director;
    public System.Action onComplete;

    public void Init()
    {
        AssetManager.instance.onProgress = (per) =>
        {
            if (per >= 1)
            {
                this.onComplete();
            }
            this.director.UpdateUI(per);
        };

        AssetManager.instance.LoadAllAssets();
    }
}

- AssetManager의 onProgrss 대리자를 정의하고 있는데 해당 대리자는 AssetManager 클래스에서 IEnumerator 인터페이스를 통해 비동기적으로 실행되고 있으며 해당 클래스의 메서드 내부에서 데이터의 갯수만큼 실행되어 전부 로딩이 될 때까지 비동기적으로 데이터를 불러오는 기능을 수행한다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;

public class UILoadingDirector : MonoBehaviour
{
    public Slider slider;
    public TMP_Text txtPer;

    public void UpdateUI(float per)
    {
        this.slider.value = per;
        this.txtPer.text = string.Format("{0:0.##}%", per * 100f);   //98.32, 100%
    }
}

 

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

public class LobbyMain : MonoBehaviour
{
    public UILobbyDirector director;

    public System.Action<int> onClickStartGame;
    private int selectedCharacterId;

    public void Init()
    {
        this.director.Init();

        this.director.btn.onClick.AddListener(() => {
            this.onClickStartGame(this.director.selectedId);
        });
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UILobbyDirector : MonoBehaviour
{
    public Button btn;
    public UICharacterSlot[] uiCharacterSlots;
    private int selectedIdx = 0;
    public int selectedId = -1;

    public void Init()
    {

        var list = DataManager.instance.GetCharacterDatas();
        for (int i = 0; i < list.Count; i++)
        {
            //프리팹(clone) 인스턴화 
            //슬롯에 넣기 
            var data = list[i];
            var prefabName = string.Format("UI{0}", data.prefab_name);
            var prefab = AssetManager.instance.GetPrefab(prefabName);
            var go = Instantiate(prefab);
            this.uiCharacterSlots[i].Init(data.id, go);
        }

        //다선택 취소 하기 
        this.UnSelectAllSlots();

        //이벤트 붙이기 
        for (int i = 0; i < this.uiCharacterSlots.Length; i++)
        {
            var idx = i;
            var slot = uiCharacterSlots[idx];
            slot.btn.onClick.AddListener(() => {
                Debug.LogFormat("selected slot id :{0}, idx:{1}", slot.id, idx);
                this.selectedId = slot.id;
                this.selectedIdx = idx;
                this.UnSelectAllSlots();
                this.SelectSlot(this.selectedIdx);
            });
        }

        this.selectedId = list[0].id;
        this.selectedIdx = 0;
        this.SelectSlot(this.selectedIdx);
    }

    private void SelectSlot(int idx)
    {
        this.uiCharacterSlots[this.selectedIdx].Select();
    }

    private void UnSelectAllSlots()
    {
        foreach (var slot in this.uiCharacterSlots)
        {
            slot.UnSelect();
        }
    }
}

 

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

public class UICharacterSlot : MonoBehaviour
{
    public Transform pivot; //character가 부착될 부모 
    public int id;
    public GameObject checkGo;
    public Button btn;

    private void Awake()
    {
            
    }

    public void Init(int id, GameObject go)
    {
        this.id = id;
        go.transform.SetParent(this.pivot);
        go.transform.localPosition = Vector3.zero;
        go.transform.localScale = Vector3.one;
    }

    public void Select()
    {
        this.checkGo.SetActive(true);
    }
    public void UnSelect()
    {
        this.checkGo.SetActive(false);
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameMain : MonoBehaviour
{
    public GameObject heroShellPrefab;    //prefab

    public void Init(int selectedCharacterId)
    {
        Debug.LogFormat("[GameMain] selectedCharacterId: {0}", selectedCharacterId);
        this.CreateHero(selectedCharacterId);
    }

    private void CreateHero(int id)
    {
        //껍데기 만들기 
        var shellGo = Instantiate(this.heroShellPrefab);
        var hero = shellGo.GetComponent<Hero>();

        //모델 
        var data = DataManager.instance.GetCharacterData(id);
        var prefab = AssetManager.instance.GetPrefab(data.prefab_name);
        var modelGo = Instantiate(prefab);

        hero.Init(modelGo);
    }
}

 - 프리팹을 통해 바로 원하는 오브젝트를 인스턴스화 하는 것이 아닌 빈 오브젝트를 먼저 인스턴스화 하고 해당 오브젝트에 스크립트를 붙여 위치, 크기조절을 효율적으로 한다.

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

public class Hero : MonoBehaviour
{
    private GameObject modelGo;
    private Animator anim;

    public void Init(GameObject modelGo)
    {
        this.modelGo = modelGo;
        this.modelGo.name = "model";
        this.modelGo.transform.SetParent(this.transform);
        this.anim = this.modelGo.GetComponent<Animator>();
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AssetData : MonoBehaviour
{
    public int id;
    public string path;
    public string prefab_name;
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AssetManager : MonoBehaviour
{
    public static AssetManager instance;
    public System.Action<float> onProgress;
    private Dictionary<string, GameObject> dicPrefabs = new Dictionary<string, GameObject>();

    private void Awake()
    {
        AssetManager.instance = this;
    }

    public void LoadAllAssets()
    {
        List<AssetData> assetData = DataManager.instance.GetAssetDatas();
        for (int i = 0; i < assetData.Count; i++)
        {
            AssetData data = assetData[i];
            var fullPath = string.Format("{0}/{1}", data.path, data.prefab_name);
            Debug.Log(fullPath);
            this.StartCoroutine(this.LoadAsync(fullPath));
        }
    }

    private int loadedAssetCount = 0;
    public IEnumerator LoadAsync(string path)
    {
        var req = Resources.LoadAsync<GameObject>(path);
        yield return req;
        //어셋로드 완료 

        var arr = path.Split('/');
        var key = arr[arr.Length - 1];
        this.dicPrefabs.Add(key, (GameObject)req.asset);

        ++this.loadedAssetCount;
        Debug.LogFormat("{0}/{1}\t{2}", this.loadedAssetCount, DataManager.instance.GetAssetDatas().Count, path);

        var per = (float)this.loadedAssetCount / DataManager.instance.GetAssetDatas().Count;
        this.onProgress(per);
    }

    /// <summary>
    /// get prefab by prefab_name
    /// </summary>
    /// <param name="key">prefab name</param>
    /// <returns></returns>
    public GameObject GetPrefab(string key)
    {
        return this.dicPrefabs[key];
    }
}

 - App 오브젝트의 자식 오브젝트로서 존재한다.

 - LoadAsync 메서드를 통해 ResouceRequest 타입의 값을 반환하고 해당 값을 비동기로 로드했다면 return 아래 코드를 실행한다.

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

public class CharacterData : MonoBehaviour
{
    public int id;
    public string name;
    public string prefab_name;
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Newtonsoft.Json;
using System.Linq;
public class DataManager
{
    public static readonly DataManager instance = new DataManager();
    private Dictionary<int, CharacterData> dicCharacterDatas;
    private Dictionary<int, AssetData> dicAssetDatas;
    private const string CHARACTER_DATA_PATH = "Data/character_data";
    private const string ASSET_DATA_PATH = "Data/asset_data";

    private DataManager()
    {
    }

    public void LoadAssetData()
    {
        var asset = Resources.Load<TextAsset>(ASSET_DATA_PATH);
        var json = asset.text;
        this.dicAssetDatas = JsonConvert.DeserializeObject<AssetData[]>(json).ToDictionary(x => x.id);
    }

    public void LoadCharacterData()
    {
        var asset = Resources.Load<TextAsset>(CHARACTER_DATA_PATH);
        var json = asset.text;
        this.dicCharacterDatas = JsonConvert.DeserializeObject<CharacterData[]>(json).ToDictionary(x => x.id);
    }

    public List<AssetData> GetAssetDatas()
    {
        return this.dicAssetDatas.Values.ToList();
    }

    public List<CharacterData> GetCharacterDatas()
    {
        return this.dicCharacterDatas.Values.ToList();
    }

    public CharacterData GetCharacterData(int id)
    {
        return this.dicCharacterDatas[id];
    }
}

 - 데이터테이블을 역질렬화하여 링큐를 통해 딕셔너리에 값을 입력하는 기능을 수행하는 메서드들과 해당 딕셔너리의 값들을 리스트, 데이터타입형태로 반환하는 기능을 수행하는 메서드들로 이루어져있다.

[
  {
    "id": "100",
    "path": "Prefabs",
    "prefab_name": "DeerStandard"
  },
  {
    "id": "101",
    "path": "Prefabs",
    "prefab_name": "BadgerStandard"
  },
  {
    "id": "102",
    "path": "Prefabs",
    "prefab_name": "LionStandard"
  },
  {
    "id": "103",
    "path": "Prefabs/UI",
    "prefab_name": "UIDeerStandard"
  },
  {
    "id": "104",
    "path": "Prefabs/UI",
    "prefab_name": "UIBadgerStandard"
  },
  {
    "id": "105",
    "path": "Prefabs/UI",
    "prefab_name": "UILionStandard"
  }
]
[
  {
    "id": "100",
    "name": "Deer",
    "prefab_name": "DeerStandard"
  },
  {
    "id": "101",
    "name": "Badger",
    "prefab_name": "BadgerStandard"
  },
  {
    "id": "102",
    "name": "Lion",
    "prefab_name": "LionStandard"
  }
]