오늘은 아무래도 한 업무가 적은 게, 삽질을 너무 많이 해서 결국 작업 내용에 있어서는 부실할 수밖에 없었다.
맵에 적용할 셰이더를 만들기 위해 셰이더 그래프에 대한 공부를 했다, 그리고 무한 삽질 후 만든 쉐이더 그래프는 너무 못생겨서 사용 못함...
맵 랜덤 생성 시스템 구현, 카메라워크 구현
셰이더 그래프란 것을 처음 사용해 보았다. 하지만, 기능을 이해한 것과 디자인적 감각을 살리는 거는 별개라는 걸 알게 되었다...
간략하게 만들어 본 셰이더는 위와 같다. 셰이더는 그 효과를 주기 위해 다양한 기능을 제공한다.
위의 경우를 상세한 정보로 나누어 사용 방법에 대해 설명하자면 다음과 같이 사용할 수 있다.
각각의 변수를 기준으로 아래와 같이 연결했다고 말할 수 있다.
사실은 이렇게 쓴 게 정확하지 않을 수도 있기는 하지만, 셰이더 그래프 관련으로 제공되는 공식 문서는 하나같이 내용이 이해하기 쉽지 않았다.
https://docs.unity3d.com/kr/Packages/com.unity.shadergraph@10.8/manual/index.html
셰이더에 대한 부분은 당장 우선순위가 아니기 때문에 우선은 공부해보는 걸 뒤로 미루기로 했다. 그리고 만든 결과물도 처참했으니, 디자인이 구리다고 결국 사용하지 못했다.
수많은 삽질의 흔적, 거의 4시간을 붙잡고 있다가 결국엔 포기 선언을 했다...
맵 랜덤 생성 시스템을 만들기 위해서 우선은 맵의 프리팹을 정리했다. 영점이 안 맞춰져 있는 경우 전부 영점을 맞춰주었고 맵 배치 위치를 대략 파악했다. 대략 x축 기준으로 35 정도면 충분히 맵 사이의 공간이 확보가 되는 것을 확인했다.
여기서 랜덤 생성을 해야 하니까, 기왕 하는 거 저번 프로젝트에서 만들었던 가중치 랜덤 시스템을 사용하기로 했다.
저번에 가중치 랜덤 시스템을 사용했을 때, 가중치를 변경하는 랜덤 뽑기는 사용하지 않았었는데 이번 기회에 사용할 수 있을 것으로 보였다.
우선 맵 생성 매커니즘 자체는 아래와 같이 작성했다.
using System.Security.Cryptography;
using UnityEngine;
public class RandomMapPresetCreator : MonoBehaviour
{
// 맵을 Resources로 저장할 거면 해당 방식으로 변경 필요
[SerializeField] GameObject[] mapResources;
// 맵 위치 오프셋
[SerializeField] float mapTransformOffset = 35;
// 단일 라운드 수
[SerializeField] int gameCycleNum = 3;
private WeightedRandom<GameObject> mapWeightedRandom = new WeightedRandom<GameObject>();
private void OnEnable()
{
RandomInit();
RandomMapSelect();
}
private void RandomInit()
{
for(int i = 0; i < mapResources.Length; i++)
{
mapWeightedRandom.Add(mapResources[i], 1);
}
}
private void RandomMapSelect()
{
for(int i = 0; i < gameCycleNum; i++)
{
GameObject selectedMap = mapWeightedRandom.GetRandomItemBySub();
Vector3 selectedMapPosition = new Vector3(i * mapTransformOffset, 0, 5);
Instantiate(selectedMap, selectedMapPosition, Quaternion.identity);
}
}
public float GetTransformOffset()
{
return mapTransformOffset;
}
}
씬이 로딩될 때마다 맵을 랜덤 생성기에 넣을 것이다. 여기서, 모든 맵이 딱 한 번 씩만 나오는 조건이 필요한데, 이전에 만들어놨던 가중치 랜덤 시스템에는 특정 아이템을 뽑았을 시에 그 아이템의 개수를 차감하는 시스템이 있었다.
이걸 응용하면 모든 맵을 한 개씩만 넣고 그 하나를 제하면 다음에는 그 아이템이 나올 확률을 0퍼센트로 만들 수 있기 때문에, 별도로 아이템의 중복 여부를 검사할 필요가 없어진다.
다음으로 맵을 생성할 좌표에 관한 부분으로, 처음 생성될 맵의 위치는 (0, 0, 5)가 되어야 하고 그 다음 맵은 (35, 0, 5)가 되어야 한다. for문의 첫 번째 i = 0이란 점을 이용해 좌표를 계산하는 방식으로 맵을 Instantiate하는 방식으로 구성했다.
랜덤 생성 자체는 잘 되었는데, 분명 이론적으로 생각했을 때 맵의 중복이 발생하지 않을 터였다. 하지만 여러 번 재생해보았을 때 알게 된 것이, 맵 중복이 발생한다는 것이었다.
이 시스템을 저번에 사용했을 때에는 뭔가 결함이 있다고는 생각지도 못했는데 뭐가 원인일까? 디버깅을 해 보면서 찾아본 결과 문제가 있는 부분을 찾아냈다.
/// <summary>
/// Get random item and substrate.
/// (if you picked certain item, then the item percentage of list decreases.)
/// </summary>
/// <returns></returns>
public T GetRandomItemBySub()
{
if (_dic.Count <= 0)
{
Debug.LogError("There's no item in list.");
return default;
}
int weight = 0;
int totalWeight = GetTotalWeight();
int pivot = Mathf.RoundToInt(totalWeight * Random.Range(0.0f, 1.0f));
foreach (var item in _dic)
{
weight += item.Value;
if (pivot <= weight)
{
_dic[item.Key] -= 1;
return item.Key;
}
}
return default;
}
해당 방법을 보면 특정 아이템을 뽑았을 경우, 해당 아이템의 value를 1만큼 빼는 것으로 확률을 줄이는 것이다. 다만 이와 같이 처리할 경우 문제가 발생한다.
해당 방식은 Value가 0 이하가 되었을 때 키값을 제거해주지 못하기 때문에, 뽑을 수 있는 횟수가 0이 되었어도 계속 뽑을 수 있는 상태로 남게 된다. Value가 -1이 되었을 때에는 걸러지지만 0일 때 다시 뽑히는 현상이 발생해서 문제가 발생한 것이다.
따라서 해당 함수를 다음과 같이 Sub 함수를 이용하여 고쳤다.
/// <summary>
/// Substract item from list due to weight.
/// </summary>
/// <param name="item"></param>
/// <param name="value"></param>
public void Sub(T item, int value)
{
if (value < 0)
{
Debug.LogError("Value under 0 can't be substracted");
return;
}
if (_dic.ContainsKey(item))
{
if (_dic[item] > value)
{
_dic[item] -= value;
}
else
{
Remove(item);
}
}
}
/// <summary>
/// Remove the item in the list.
/// </summary>
/// <param name="item"></param>
public void Remove(T item)
{
if (_dic.ContainsKey(item))
{
_dic.Remove(item);
}
else
{
Debug.LogError($"{item} is not exist");
}
}
...
/// <summary>
/// Get random item and substrate.
/// (if you picked certain item, then the item percentage of list decreases.)
/// </summary>
/// <returns></returns>
public T GetRandomItemBySub()
{
if (_dic.Count <= 0)
{
Debug.LogError("There's no item in list.");
return default;
}
int weight = 0;
int totalWeight = GetTotalWeight();
int pivot = Mathf.RoundToInt(totalWeight * Random.Range(0.0f, 1.0f));
foreach (var item in _dic)
{
weight += item.Value;
if (pivot <= weight)
{
Sub(item.Key, 1);
return item.Key;
}
}
return default;
}
저번에는 이 함수를 사용하지 않았지만 이번에 사용하면서 문제점을 알아내고 디버깅했다.
이제 랜덤 맵 생성까지는 완료했고, 다음은 카메라워킹을 할 차례이다.
카메라워킹의 경우, 방식을 고민해보다가 맵 랜덤생성 시스템에서의 오프셋을 가져다 쓰는 것이 좋겠다는 판단 하에 이와 같이 인게임 매니저를 구성했다.(매니저이긴 하지만, 이것들은 Static이나 싱글톤은 아니다.)
using ExitGames.Client.Photon.StructWrapping;
using System.Collections;
using System.Collections.Generic;
using System.Timers;
using UnityEngine;
public class IngameCameraMovement : MonoBehaviour
{
[SerializeField] private bool isRoundOver = false;
[SerializeField] private bool isRoundSetOver = false;
[SerializeField] private float moveDuration = 1f;
// 게임매니저가 없어서 일단 Update로 처리 후 테스트
// 후에 이벤트로 라운드 및 라운드셋 종료 여부를 받아오는 방법 고려중
private RandomMapPresetCreator creator;
private Camera mainCamera;
private Vector2 startPosition;
private Vector2 targetPosition;
private Coroutine cameraCoroutine;
private void OnEnable()
{
creator = GetComponent<RandomMapPresetCreator>();
mainCamera = Camera.main;
startPosition = Camera.main.transform.position;
}
private void Update()
{
if(isRoundSetOver)
{
SceneChange();
}
else if (isRoundOver)
{
IngameCameraMove();
}
}
private void IngameCameraMove()
{
float offset = creator.GetTransformOffset();
targetPosition = startPosition + new Vector2(offset, 0);
cameraCoroutine = StartCoroutine(MoveCamera());
isRoundOver = false;
}
IEnumerator MoveCamera()
{
float elaspedTime = 0f;
while(elaspedTime < moveDuration)
{
elaspedTime += Time.deltaTime;
float t = elaspedTime/moveDuration;
mainCamera.transform.position = Vector2.Lerp(startPosition, targetPosition, Mathf.SmoothStep(0, 1, t));
yield return null;
}
mainCamera.transform.position = targetPosition;
startPosition = Camera.main.transform.position;
cameraCoroutine = null;
}
private void SceneChange()
{
// 씬 로드 - **님 비동기 로드 씬이 어느거지?
}
}
방식은 다음과 같다.
카메라는 메인카메라를 가져오고, 랜덤 맵 생성 시스템에서 설정된 오프셋을 가져온다.
최초의 시작 위치는 카메라의 위치이고, 라운드가 종료되었을 때 카메라 움직임이 시작된다. 카메라의 움직임은 코루틴으로 진행되며, 설정된 시간안에 targetPosition으로 설정된 위치까지 Lerp로 이동한다. 마지막에 Lerp로 완전히 도달하지 못한 지점까지 최종적으로 위치를 확정하고, startPosition을 지금의 위치에 있는 카메라까지로 설정한다.
이와 같은 방식으로 연출을 넣을 수 있었다.
카메라 연출 부분은 의도한 대로 만들기는 했지만, 팀장님이 기왕이면 왼쪽으로 살짝 움직였다가 다시 오른쪽으로 가는 방식을 원한다고 했다. 디테일적인 요소로 넣고 싶다는 모양인데 해당 부분을 추가해보자.
현재 맵과 네트워크가 제대로 동기화되지 않는 문제점이 발생한다. 이는 맵 오브젝트들이 PhotonView로 설정되어 있지 않음으로 인해 발생하는 문제이며, 팀장님이 동기화 방법에 대한 힌트로 PhotonView + PhotonTransformView 를 사용해 보라고 조언했다.
네트워크로 작업물을 병합할 때 동기화가 잘 되는지 확인해보자.
셰이더를 직접 만드는 것은 보류가 되었다 보니, 일단은 에셋을 찾아서 적용하는 방향으로 맵 효과를 줄 예정이다. 적절한 에셋은 아직 찾지 못해 서치를 해 봐야 할 것 같다.