오랜만에 개발 상황을 정리해본다.
인턴이 종료되고, 포트폴리오 겸 출시 경험을 쌓으려 시작한 프로젝트.
초반에 열심히 개발일지를 작성하며 해보려 했지만,,ㅋㅋ 쉽지 않았고!
중간정리를 해보자!
먼저
데이터 관리
데이터는 이전 블로그에서 말한대로 json을 이용한 방식을 채택하여 구글 시트에 구글 시트로부터 유닛,몬스터,보스,합성 레시피 데이터를 불러와 캐시하도록 구성하였다.
// 보스 데이터 등록
foreach (var boss in allData.Boss)
{
BossDataMap[boss.BossId] = boss;
}
// 합성 레시피 등록
foreach (var recipe in allData.MergeRecipe)
{
foreach (var rawId in recipe.SourceUnitId.Split('/'))
{
var id = rawId.Trim();
string key = $"{id}_{recipe.RequireCount}";
if(!MergeRecipeDataMap.TryGetValue(key, out var list))
{
list = new List<MergeRecipeData>();
MergeRecipeDataMap[key] = list;
}
list.Add(recipe);
}
}

위와 같이 유닛, 몬스터, 보스 등은 id값으로 딕셔너리에 데이터를 넣어주었고,
합성 레시피는 "/"으로 시트에서 구분할 수 있고, 또 여러 행을 안쓸수 있도록 작성하여, Split함수를 이용해 다시 나누어 데이터를 넣어주었다.
싱글톤(모노)
전역 매니저들을 MonoSingleton로 구현해 중복을 최소화 해주었다.(게임매니저, 풀 매니저, 사운드 매니저 등)
예외처리나 확장할 부분은 아직 많은 것 같다.
using System;
using UnityEngine;
public class MonoSingleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T _instance;
private static bool _applicationIsQuitting;
public static T Instance
{
get
{
if (_applicationIsQuitting) return null;
if (_instance == null)
{
_instance = FindAnyObjectByType<T>();
if (_instance == null)
{
var go = new GameObject(typeof(T).Name);
_instance = go.AddComponent<T>();
}
}
return _instance;
}
}
protected virtual void OnApplicationQuit()
{
_applicationIsQuitting = true;
}
protected void OnDestroy()
{
if(_instance == this) _instance = null;
}
}
웨이브 관리
먼저 GameManager -> MonsterSpawner -> PlayerField -> 몬스터 생성 및 이동
위와 같은 흐름으로 진행되도록 하였다.
private void Start()
{
if(PhotonNetwork.CurrentRoom != null && PhotonNetwork.CurrentRoom.PlayerCount == 1)
{
_isSoloMode = true;
}
// 1인 모드
if (_isSoloMode)
{
Debug.Log("1인 모드로 시작합니다.");
if (playerFields.Count >= 2)
{
// 두 번째 필드를 게임 오브젝트 수준에서 비활성화
playerFields[1].gameObject.SetActive(false);
// 리스트에서 제거
playerFields.RemoveAt(1);
}
}
// 마스터만 웨이브 루틴 돌리기
if(!PhotonNetwork.IsMasterClient)
{
foreach ( var player in playerFields)
{
player.ResetField();
player.OnPlayerDeath += OnPlayerDefeated;
}
return;
}
// 2인 모드
foreach (var player in playerFields)
{
player.ResetField();
player.OnPlayerDeath += OnPlayerDefeated;
}
StartCoroutine(WaveRoutine());
}
기획 단계에서 포톤을 활용한 멀티플레이를 구상했기에 멀티기능만 넣었다. 하지만 유저가 없는 관계로 1인 모드를 추가해주었다. (일단은 대기 시간 > 10 이면 솔로 플레이)
어쨋든 코루틴을 통해 웨이브 시간, 생성 간격을 주어 생성되도록 한다.(뒤에서 말하겠지만 풀링도 적용하였다.)
몬스터/보스 생성 및 풀링
PoolManager, PhotonPool, BasePool로 구성하였고, 내가 이용하고 있는 Photon PUN2의 IPunPrefabPool인터페이스를 상속받아 생성 및 파괴 시 각각의 Id를 받아 풀에서 생성 및 리턴 되도록 구현하였다.
public class PhotonPool : IPunPrefabPool
{
public GameObject Instantiate(string prefabId, Vector3 position, Quaternion rotation)
{
GameObject go = PoolManager.Instance.Get(prefabId);
go.name = prefabId;
go.transform.position = position;
go.transform.rotation = rotation;
return go;
}
// MEMO : PhotonNetwork.Destroy가 호출되면 이 Destroy가 실행됨.
public void Destroy(GameObject gameObject)
{
Debug.Log($"{gameObject.name}");
PoolManager.Instance.Return(gameObject.name, gameObject);
}
}
using System;
using System.Collections.Generic;
using UnityEngine;
public class PoolManager : MonoSingleton<PoolManager>
{
private Dictionary<string, object> _pools = new();
public void CreatePool(string key, GameObject prefab, int size, Transform parent = null)
{
if (_pools.ContainsKey(key)) return;
BasePool<GameObject> pool = new(prefab, size, parent);
_pools[key] = pool;
}
public GameObject Get(string key)
{
if (_pools.TryGetValue(key, out var pool))
{
return ((BasePool<GameObject>)pool).Get();
}
Debug.LogError($"Pool with key {key} not found.");
return null;
}
public void Return(string key, GameObject obj)
{
if (_pools.TryGetValue(key, out var pool))
{
((BasePool<GameObject>)pool).Return(obj);
}
else
{
Debug.LogError($"Pool with key {key} not found.");
}
}
}
using System.Collections.Generic;
using UnityEngine;
public class BasePool<T> where T : UnityEngine.Object
{
private readonly Queue<T> _pool = new();
private readonly T _prefab;
private readonly Transform _parent;
public BasePool(T prefab, int initialSize, Transform parent = null)
{
_prefab = prefab;
_parent = parent;
for (int i = 0; i < initialSize; i++)
{
T obj = Object.Instantiate(_prefab, _parent);
((GameObject)(object)obj).SetActive(false);
_pool.Enqueue(obj);
}
}
public T Get()
{
if (_pool.Count == 0)
{
Add(1);
}
T obj = _pool.Dequeue();
((GameObject)(object)obj).SetActive(true);
return obj;
}
public void Return(T obj)
{
((GameObject)(object)obj).SetActive(false);
_pool.Enqueue(obj);
}
public void Add(int count)
{
for (int i = 0; i < count; i++)
{
T obj = Object.Instantiate(_prefab, _parent);
((GameObject)(object)obj).SetActive(false);
_pool.Enqueue(obj);
}
}
}
몬스터와 보스
생성 시 불러온 데이터로 스텟을 초기화 해주고, 나(유저A),상대(유저B) 각각의 화면에 본인은 하단, 상대방은 상단을 자신의 필드로 둘 수 있도록 위치를 반전 시켜주었다.(생각보다 헷갈려서 상당히 애를 먹었다.) 유닛도 마찬가지!
그러고는 waypoint에 따라 이동하고, 애니메이션, hit이펙트 등을 넣어주었고, 추후 리펙토링이 필요해 보이지만 일단은 진행시켰다..
강화 및 합성
일단 유닛 클릭 팝업으로 버튼이 생성되고, 강화 시 구글시트에서 작성한 추가 데미지를 넣어주도록 하였고, 합성은 마찬가지로 시트에 레시피를 만들어 합성유닛 id로 생성되도록 구현하였다.
정리하려니 생각 보다 많..
사운드 관리
사실 신경을 못쓰고 있다가 프로토 타입을 완성하고픈 마음에 일단 직접 녹음을...일단 진행시켜
간단히 매니저를 만들어 딕셔너리로 키값을 받아 해당 id의 사운드를 출력하도록 구현하였다.
BGM은 일단 Loop로 인트로, 로비에서 자동 재생되로록 하였고, SFX만 유닛, 몬스터 등에 Hit, Dead 상태일 때, 같이 재생되도록 하였다.
간단하게 여기까지만 정리해보도록 하고,
한 번 보자.

음하하 드디어 유니티6부터는 유니티 로고대신 내 로고를 사용할 수 있다지(무료로!)
기분좀 내봤다.

인트로씬!
![]
로비씬!(흠,,아직 컨텐츠 기획, 개발 속도 이슈..)

인게임 씬!(왁자지껄 잔잔바리 오류 존재..)
이제 마지막으로 영상!
용량 이슈로 화질구지.양해바랍니다..