0. 들어가기에 앞서
오늘은 전반적인 게임의 틀을 다듬었다. 커밋 수는 적지만 전반적인 게임 자체 로직을 설계하는 데 힘을 쏟았다.
그럴싸한 사격장 시뮬레이터가 완성된 것 같다.
원래라면 어제 작업해보려던 내용이었지만 어제는 시간도 별로 없었고 뚜렷한 설계 방안도 떠오르지 않아서 작업하지 못했다.
오늘 이 문제에 대해 골똘히 생각해보면서 그래도 확장성을 고려한 방식을 생각해 보았는데, 이와 같은 방식을 떠올렸다.
과녁의 개수가 일정하지 않을 것을 고려해서, 인스펙터로 참조하는 방식보다는 그냥 있는 과녁을 다 가져올 수 있는 방식으로 만들어 보자.
=> 과녁들의 집합 위에 부모 오브젝트를 두고 GetComponentsInChildern으로 참조한다면?
몇 개의 과녁을 활성화할 것인지에 대한 아이디어 => 참조한 과녁들의 배열 길이 / 2 + 1개
이렇게 하면 과녁 9개 기준 9 / 2 + 1 = 5개의 과녁의 활성화될 것이다.
과녁의 개수가 달라져도 대략 전체 중 반 + 1개가 활성화되는 게 시각적으로도 잘 보이지 않을까?
랜덤한 과녁을 활성화하는 방법
3.1 TargetController 자체는 과녁을 올리고 내리는 기능 및 스코어 상승 기능만 넣어두고, 부모 오브젝트에 TargetManager라는 과녁 관리 컴포넌트를 만든다.
3.2 TargetManager에서는 랜덤한 숫자를 중복되지 않게 5개 뽑아 배열로 For문을 돌려서 과녁을 활성화시킨다. 일정 시간 뒤 활성화된 과녁이 변경될 수 있도록 숫자를 다시 뽑아서 활성화된 과녁을 변경한다.
여기서 3.2에 대한 과정을 상세 설명하면 다음과 같다.
3.2.1 우선은 해당 부분을 코루틴으로 처리하며, 게임이 종료되지 않은 시점에서 계속 반복될 수 있도록 While(true)로 처리한다.
3.2.2 랜덤한 숫자를 저장할 List를 초기화한다. (List.Clear())
3.2.3 활성화된 과녁을 전부 비활성화한다(초기화)
3.2.4 과녁의 개수/2+1개만큼 반복문을 진행하며, List에 들어있는 숫자가 중복되지 않도록 5개를 Add로 저장한다.
3.2.5 해당 과녁을 활성화한다
3.2.6 WaitForSeconds(정해진 시간) - 일정 시간 이후로 다시 반복문 실행
이와 같은 과정으로 최종 완성된 TargetManager의 코드는 아래와 같다.
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
public class TargetManager : MonoBehaviour
{
// 시간제한 설정(밸런스)
[SerializeField] private float m_setTimelimit;
private float m_timeLimit;
// 과녁 변화 속도 설정(밸런스)
[SerializeField] private float m_targetRoutineTime;
// 참조 컴포넌트
private TargetController[] m_targetControllers;
// 변수 저장 및 코루틴
private List<int> m_targets = new List<int>(5);
Coroutine m_coroutine;
// 인스펙터 참조
[SerializeField] private GameObject m_timeUI;
[SerializeField] TMP_Text m_timeText;
private void Awake() => Init();
private void Init()
{
m_targetControllers = GetComponentsInChildren<TargetController>();
m_timeUI.SetActive(false);
}
private void Update()
{
// 게임이 종료된 상태가 아닐 시
if(!GameManager.Instance.IsGameOver())
{
m_timeLimit -= Time.deltaTime;
m_timeText.text = $"{m_timeLimit.ToString("00.0")}";
if (m_coroutine == null)
{
m_coroutine = StartCoroutine(TargetCoroutine());
}
// 시간 오버 시 게임 종료
if(m_timeLimit <= 0)
{
GameManager.Instance.GameOver();
if(m_coroutine != null)
{
InactivateTarget();
StopCoroutine(m_coroutine);
m_coroutine = null;
}
}
}
}
// 게임 시작 존에 들어왔을 시 게임 시작
private void OnTriggerEnter(Collider other)
{
if(other.CompareTag("Player"))
{
GameManager.Instance.GameStart();
m_timeLimit = m_setTimelimit;
m_timeUI.SetActive(true);
}
}
// 게임 시작 존에서 벗어났을 시 강제로 게임 종료
private void OnTriggerExit(Collider other)
{
if(other.CompareTag("Player"))
{
GameManager.Instance.GameOver();
m_timeUI.SetActive(false);
if (m_coroutine != null)
{
InactivateTarget();
StopCoroutine(m_coroutine);
m_coroutine = null;
}
}
}
// 과녁 랜덤 활성화 코루틴
private IEnumerator TargetCoroutine()
{
WaitForSeconds m_targetCooltime = new WaitForSeconds(m_targetRoutineTime);
// 코루틴 반복 진행
while (true)
{
// 리스트에 저장된 변수 제거
m_targets.Clear();
// 현재 활성화된 과녁 전부 비활성화
InactivateTarget();
// 과녁의 개수의 반 + 1개만큼 랜덤 숫자를 뽑는다(중복X)
while (m_targets.Count < m_targetControllers.Length / 2 + 1)
{
int num = Random.Range(0, m_targetControllers.Length);
if (!m_targets.Contains(num))
{
m_targets.Add(num);
}
}
// 해당 숫자의 과녁을 활성화함
foreach (int num in m_targets)
{
if (m_targets.Contains(num))
{
m_targetControllers[num].ActiveTarget();
}
}
yield return m_targetCooltime;
}
}
// 모든 과녁 비활성화 함수
private void InactivateTarget()
{
for (int i = 0; i < m_targetControllers.Length; i++)
{
if (m_targetControllers[i].IsActiveTarget())
{
m_targetControllers[i].InactiveTarget();
}
}
}
}
실제로 최종 코드를 완성할 때까지 상당한 시행착오를 겪었다. 특히 해결하기 어려웠던 게 코루틴이 이상하게 한 번만 실행되고 실행이 안 되는 문제였는데, 이거는 While(true)로 처리하지 않았기 때문에 생긴 문제였다.
결과적으로 해당 방법으로 일정 시간마다 활성화된 과녁이 변경되는 기능이 구현되었다.
총을 양 손으로 드는 방식으로 해야 보다 현실성 있는 VR 사격장 시뮬레이터가 될 것 같아서, 이 부분에 대한 구현을 시도했다.
변경점은 크게 아래와 같다.
public class Shooter : MonoBehaviour
{
...
// 참조 컴포넌트
private XRGrabInteractable m_interactable;
...
private void Awake() => Init();
private void Init()
{
m_audioSource = GetComponent<AudioSource>();
m_muzzle = GetComponent<Transform>();
m_interactable = GetComponentInParent<XRGrabInteractable>();
m_bulletPool = new ObjectPool(transform, m_bulletPrefab);
}
...
// 총을 발사할 수 있는지 확인용
// 총을 양 손으로 잡고 있는지와, 발사 쿨타임이 지났는지 확인 후 bool 반환
private bool TryFire()
{
// 잡고 있는 컨트롤러가 두 개 이상이면 & 총 발사 쿨타임이 지났으면
if(m_interactable.interactorsSelecting.Count >= 2 && m_shootCooltime > m_cooltime)
{
return true;
}
return false;
}
...
}
이와 같은 방식으로 만들었을 때 실제로 총을 잡는 듯한 자세를 취했을 때 양 손으로 잡아야지 총알이 나가도록 할 수 있었다.
다만 문제점이라기엔 애매하지만, 실제로 총을 잡듯이 왼 손을 앞으로 쭉 뻗고, 오른쪽 컨트롤러를 왼쪽 컨트롤러와 거리를 두고 들고 있어야 정확히 에임을 할 수 있어서, 이 부분에 대해 튜토리얼을 따로 준비해야 겠다는 생각이 들었다.
콜백 함수와 유니티 액션/이벤트와 관련된 부분이 나에게 있어서는 다소 적용하기 어려운 부분이었다.
하지만 이번 프로젝트에서만큼은 그 부분을 어떻게든 적용해서 최적화를 해 봐야겠다는 생각에 여러모로 공부를 해 봤다.
예시 코드를 찾아보고, 실제로 테스트를 진행해 본 결과 점수 표시 UI에 관해 아래와 같이 유니티액션을 추가할 수 있었다.
using DesignPattern;
using UnityEngine.Events;
public class GameManager : Singleton<GameManager>
{
private static int m_score;
private static bool m_isGameOver;
public UnityAction<int> OnScoreChanged;
private void Awake() => Init();
private void Init()
{
m_score = 0;
m_isGameOver = true;
}
public void GameStart()
{
m_score = 0;
OnScoreChanged?.Invoke(m_score);
m_isGameOver = false;
}
public void GameOver()
{
m_isGameOver = true;
}
public bool IsGameOver()
{
return m_isGameOver;
}
public void AddScore(int score)
{
m_score += score;
OnScoreChanged?.Invoke(m_score);
}
public int GetScore() => m_score;
}
using TMPro;
using UnityEngine;
/// <summary>
/// 스코어를 표사히는 UI에 현재 점수를 반영하는 스크립트
/// </summary>
public class ScoreUI : MonoBehaviour
{
private TMP_Text m_text;
private void Awake() => Init();
private void Init()
{
m_text = GetComponentInChildren<TMP_Text>();
}
private void OnEnable() => GameManager.Instance.OnScoreChanged += ScoreUpdate;
private void OnDisable() => GameManager.Instance.OnScoreChanged -= ScoreUpdate;
// 점수 변화시 Text에 반영 -> 이벤트로 처리
private void ScoreUpdate(int score)
{
m_text.text = $"{score.ToString("000")}";
}
}
이와 같이 코드를 변경했고 최종적으로 정상 적용되는 것을 확인했다.
사실 이번에 유니티액션을 활용하는데 성공했다고 해도, 아직까지는 방법이 익숙하지 않았다.
이 부분은 앞으로도 유니티액션을 계속 사용해보면서 숙련도를 높이는 작업이 필요해 보인다.
이 부분에 대한 건 확실히 수정이 필요해 보이지만, 아직 뚜렷한 해결방법이 보이지 않는 상태이다. 좀 더 고민해보자.
게임에 튜토리얼 기능이 필요해 보인다는 생각이 들었다. 총을 잡는 방법이나 쏘는 방법 등의 안내를 할 수 있는 튜토리얼 내용을 추가해보자.
지금의 맵은 다소 밋밋하고, 심지어 총도 재질이 깨져서 하얀색 총이 되었다. 좀 더 디자인적으로 괜찮은 맵을 구해보거나 에셋 적용을 해 보자.
손 관련 에셋을 구했고 컨트롤러를 손으로 바꿔보려는 시도를 했는데, 이게 생각보다 어려웠다. 실제로 반영할 수 있을지는 모르겠지만 다시 시도를 해 보자.
최고기록을 기록할 수 있는 랭킹 시스템을 구현해보고자 한다.
다만 최고기록을 저장하는 건 그렇다 쳐도, 키보드를 구현할 수 있을지가 난감하다.
지금의 게임 시스템 특성상 총 장전 시스템은 구현하지 않는 편이 좋겠다는 판단이 생겼다.