총을 사용하기 위해서는 당연하게도 총알이 필요합니다. 그러니 총을 사용할 수 있도록 총알을 충전하는 것은 매우 중요하다고 할 수 있습니다.
여러 슈팅게임에서 재장전은 당연하게도 존재합니다. 하지만 재장전의 방식에는 여러가지가 존재합니다. 예를 들어서 '배틀그라운드'나 '카운터 스트라이크'같은 게임에서는 소지한 총알이 부족한 수 만큼 소모되는 식으로 재장전됩니다. '이스케이프 프롬 타르코프'같은 사실적인 것에 더 중점을 둔 게임의 경우에는 총알을 저장하고 있는 탄창 자체가 교체되는 식으로 재장전됩니다. 후자의 경우에는 탄창에 들어있는 총알이 유지되었다가 게임 내내 반영됩니다. 해당 프로젝트의는 전자의 방식을 채택하였습니다.
재장전에 대해 고찰한 결과는 다음과 같습니다.
public virtual bool CommandReload() // 장전 메소드를 외부로 부터 호출
{
if (isReloading) return false; // 장전을 안하고 있을 때만 장전
if (weaponStats.currentAmmo >= weaponStats.maxAmmo) return false; // 장탄 수가 최대 장탄 수 보다 적을 때만 장전
TGCharacter itemHolderCharacter = itemHolder.GetComponent<TGCharacter>();
if (!itemHolderCharacter.inventory.TryGetValue(weaponStats.ammoType, out TGItem item)) return false; // 키가 없으면 중단
if (item == null || item.itemCount < 0) return false; // 아이템이 1개 이상 존재하면 실행
StartCoroutine(Reload());
return true;
}
protected override IEnumerator Reload() // 무기 장전 메소드
{
//장전 버튼이 눌리자 마자 실행되야 하는 명령어
Debug.Log("(TGItemWeapon:Reload) Start reload");
isReloading = true;
yield return reloadWaitForSeconds; // 장전 시간 만큼 멈춤
//장전이 종료된 후 실행되야 하는 명령어
TGCharacter itemHolderCharacter = itemHolder.GetComponent<TGCharacter>();
TGItem ammoItem = itemHolderCharacter.inventory[weaponStats.ammoType];
int ammoCount = Mathf.Clamp(ammoItem.itemCount, 0, weaponStats.maxAmmo - weaponStats.currentAmmo); // 아이템 갯수가 감소될 수 결정
ammoItem.itemCount -= ammoCount; // 최대 장탄 수 까지만 아이템 감소
weaponStats.currentAmmo += ammoCount; // 최대 장탄 수 만큼만 장전, 현재 장탄 수 보존
// UI 업데이트
TGEventManager.Instance.TriggerEvent(EEventType.UpdateItemInfo, this);
ammoItem.itemButton.SetItemName();
isReloading = false;
Debug.Log($"(TGItemWeapon:Reload) End reload, current Ammo = {weaponStats.currentAmmo}");
}
CommandReload의 반환 타입을 bool로 처리함으로써, 재장전 시도가 거절되었을 때 UI가 실행되지 않도록 구현하였습니다. 또한 필요한 갯수 만큼만 아이템이 소모되도록 적절한 Mathf 메소드를 사용하였습니다.
사용자가 재장전이 얼마나 남았는지 확인할 수 있도록 원형 타이머도 구현하였습니다.
void StartCircleTimer(object parameter) // Circle Tiemr 실행
{
transform.localPosition = Vector3.zero; // 위치를 중앙으로 이동
timeValue = (float)parameter;
currentTime = timeValue;
TimerText.text = $"{currentTime}";
StartCoroutine(TimerCoroutine()); // 타이머 코루틴 실행
}
void HideCircleTimer(object parameter) // Circle Tiemr를 종료하고 숨김
{
transform.localPosition = new Vector3(0, 1080, 0);
}
IEnumerator TimerCoroutine() // 타이머 코루틴
{
Debug.Log("(TGUICircleTimer:TimerCoroutine) Start circle timer");
while(currentTime >= 0)
{
yield return null;
currentTime -= Time.deltaTime;
// 타이머 텍스트 업데이트
TimerText.text = $"{currentTime:F1}";
// Progress circle 비율 업데이트
float fillRatio = Mathf.Clamp01(currentTime / timeValue);
ProgressCircle.fillAmount = fillRatio;
}
Debug.Log("(TGUICircleTimer:TimerCoroutine) End circle timer");
HideCircleTimer(null);
}
'Time.deltaTime'은 1 프레임을 진행하는데 몇 초가 걸렸는지 알려주기 때문에 이를 'currentTime'에 반영하고, 매 프레임마다 타이머 텍스트를 업데이트하되, 소수점 첫번째 자리만 출력되도록 하였습니다. 이렇게 구현한 이유는 0.1초마다 currentTime을 0.1씩 감소시키도록 구현하면 오차가 발생하여 정확한 시간을 측정하기 어렵기 때문입니다. 즉, 매 프레임마다 시간을 측정하는 것이 0.1초마다 갱신하는 것 보다 더 정확합니다.
(예시) 컴퓨터에서 1.1 - 0.1은 1.0이 아닐 수 있습니다.
다음은 시연 영상입니다.