Chapter 2-2. 무기 : 총 구현 2️⃣ (재장전, 정조준, 반동)
카테고리: Unity Lesson 3
태그: Unity Game Engine
인프런에 있는 케이디님의 [유니티 3D] 실전! 생존게임 만들기 - Advanced 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click
Chapter 2. 무기
총 구현
🚖 재장전
📜Gun.cs
public int reloadBulletCount; // 총의 재장전 개수. 재장전 할 때 몇 발씩 될지. 총의 종류마다 다름.
public int currentBulletCount; // 현재 총 안의 탄창에 남아있는 총알의 개수.
public int maxBulletCount; // 총알을 최대 몇 개까지 소유할 수 있는지.
public int carryBulletCount; // 현재 총 바깥에서 소유하고 있는 총알의 총 개수.
📜GunController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GunController : MonoBehaviour
{
[SerializeField]
private Gun currentGun; // 현재 들고 있는 총. 📜Gun.cs 가 할당 됨.
private float currentFireRate; // 이 값이 0 보다 큰 동안에는 총알이 발사 되지 않는다. 초기값은 연사 속도인 📜Gun.cs의 fireRate
private bool isReload = false; // 재장전 중인지. 재장전 중이 아닌 false 일 때만 발사가 이루어 져야 한다.
private AudioSource audioSource; // 발사 소리 재생기
void Start()
{
audioSource = GetComponent<AudioSource>();
}
void Update()
{
GunFireRateCalc();
TryFire();
TryReload();
}
private void GunFireRateCalc()
{
if (currentFireRate > 0)
currentFireRate -= Time.deltaTime; // 즉, 1 초에 1 씩 감소시킨다.
}
private void TryFire() // 발사 입력을 받음
{
if(Input.GetButton("Fire1") && currentFireRate <= 0 && !isReload)
{
Fire();
}
}
private void Fire() // 발사를 위한 과정
{
if (!isReload)
{
if (currentGun.currentBulletCount > 0)
Shoot();
else
StartCoroutine(ReloadCoroutine());
}
}
private void Shoot() // 실제 발사 되는 과정
{
currentGun.currentBulletCount--;
currentFireRate = currentGun.fireRate; // 연사 속도 재계산
PlaySE(currentGun.fire_Sound);
currentGun.muzzleFlash.Play();
Debug.Log("총알 발사 함");
}
private void TryReload()
{
if (Input.GetKeyDown(KeyCode.R) && !isReload && currentGun.currentBulletCount < currentGun.reloadBulletCount)
{
StartCoroutine(ReloadCoroutine());
}
}
IEnumerator ReloadCoroutine()
{
if(currentGun.carryBulletCount > 0)
{
isReload = true;
currentGun.anim.SetTrigger("Reload");
currentGun.carryBulletCount += currentGun.currentBulletCount;
currentGun.currentBulletCount = 0;
yield return new WaitForSeconds(currentGun.reloadTime); // 재장전 애니메이션이 다 재생될 동안 대기
if (currentGun.carryBulletCount >= currentGun.reloadBulletCount)
{
currentGun.currentBulletCount = currentGun.reloadBulletCount;
currentGun.carryBulletCount -= currentGun.reloadBulletCount;
}
else
{
currentGun.currentBulletCount = currentGun.carryBulletCount;
currentGun.carryBulletCount = 0;
}
isReload = false;
}
else
{
Debug.Log("소유한 총알이 없습니다.");
}
}
private void PlaySE(AudioClip _clip) // 발사 소리 재생
{
audioSource.clip = _clip;
audioSource.Play();
}
}
재장전 하고 싶을 때 수동으로 재장전
R 키를 누르면 재장전 할 수 있도록 한다.
- TryReload()
- 👉 매 프레임마다 아래와 같은 3 가지 조건을 검사하고 3 가지 조건이 모두 만족할 때만 실제 재장전 처리를 하는 ReloadCoroutine() 함수를 실행시킨다.
R
키 입력이 들어와야 하고isReload
값이 False 여야 하고 (이미 재장전 중이라면 재장전 하면 안된다.)- 현재 탄창 안에 있는 총알의 개수와 재장전 되는 총알의 개수가 같다면 재장전 되면 안된다. 현재 탄창 안에 있는 총알의 개수 보다 재장전 되는 총알의 개수가 클 때만 재장전이 되여 한다. 예를 들어 현재 탄창에 3발 있는데 한번 재장전 될 때 40발로 채워진다면 재장전이 가능하다. 근데 현재 탕창에 이미 40발 있다면 재장전 될 필요가 없다.
currentGun.currentBulletCount < currentGun.reloadBulletCount)
- 👉 매 프레임마다 아래와 같은 3 가지 조건을 검사하고 3 가지 조건이 모두 만족할 때만 실제 재장전 처리를 하는 ReloadCoroutine() 함수를 실행시킨다.
- ReloadCoroutine()
- 👉 실제 재장전을 처리하는 코루틴 함수
isReload
를 True 로 설정.- 이미 재장전 중일땐 또 재장전 하면 안되기 때문에 이런 boolean 멤버 변수가 필요 하다.
- 재장전 애니메이션을 재생한다.
- 이 함수가 코루틴인 이유! 재장전 애니메이션이 다 재생될 때까지 대기해야 하기 때문에. 대충
reloadTime
동안.yield return new WaitForSeconds(currentGun.reloadTime);
- 이 함수가 코루틴인 이유! 재장전 애니메이션이 다 재생될 때까지 대기해야 하기 때문에. 대충
- 탄창에 있는 총알 제외하고, 현재 소유하고 있는 총알의 개수에다가 현재 탄창에 있는 총알을 더해준다. 그리고 현재 탄창에 있는 총알을 0 으로 만들어 준다.
- 예륻 들어 현재 탄창에 3발 있고 현재 소유하고 있는 총알이 7발이라면, 현재 탄창을 0 발로 만들고 현재 소유하고 있는 총알을 10 발로 만든다.
- 이미 탄창안에 총알이 있는 상태에서 재장전 할 경우를 대비하여 이런 작업을 해주는 것이다. 기존에 탄창 안에 있던 총알은 총알 소유분으로 옮겨주고 재장전 할 수 있는 탄창 수만큼 더 해주면 기존에 탄창 안에 있던 총알이 날라가지 않는다. 현재 탄창 14/20 상태에서 20발 재장전 한다면 14발은 총알 소유분에 더해주고 20/20 으로 만들어 주어 14 발을 날라가지 않게 보존한다.
currentGun.carryBulletCount += currentGun.currentBulletCount; currentGun.currentBulletCount = 0;
- 현재 소유하고 있는 총알에서 재장전 할 총알들을 끌어오기 때문에
- (현재 소유하고 있는 총알 >= 재장전 총알 개수) 라면
- 탄창에 있는 총알을 재장전 총알 개수 만큼 넣어 주고
- 위의 과정 때문에 탄창에 있는 총알은 0 인 상태. 0/20 에서 20/20 이 된다.
- 현재 소유하고 있는 총알에서 재장전 한 총알 개수만큼 차감한다.
- 탄창에 있는 총알을 재장전 총알 개수 만큼 넣어 주고
- (현재 소유하고 있는 총알 < 재장전 총알 개수) 라면
- 탄창에 있는 총알을 현재 소유하고 있는 총알 개수만큼 넣어 주고
- 현재 소유하고 있는 총알 개수를 0 으로 만들어 준다.
- (현재 소유하고 있는 총알 >= 재장전 총알 개수) 라면
- 재장전 처리가 끝났으니
isReload
를 다시 False 로 만들어 준다.
- 👉 실제 재장전을 처리하는 코루틴 함수
발사를 시도 할 때 재장전
총을 발사를 하려는데 현재 탄창에 있는 총알이 하나도 없다면 자동으로 재장전을 하도록 한다.
- TryFire()
- 발사가 가능한지를 체크하는 함수
- 재장 전이 아닐 때만 발사가 가능해야 한다.
- 조건에서
!isReload
인지를 체크함.
- 조건에서
- Fire()
- 재장전 아닐 때만 발사가 이루어 져야 한다.
- 현재 탄창 안에 총알이 있을 때만 Shoot() 실제 발사 처리를 하고
- 현재 탄창 안에 총알이 아예 없을 땐 ReloadCoroutine() 실제 재장전 처리를 한다.
- 재장전 아닐 때만 발사가 이루어 져야 한다.
- Shoot()
- 실제 발사를 처리하는 함수
- 발사가 되면 현재 탄창 안에 있는 총알을 1 감소시켜야 한다.
- 재장전과 상관 없는 얘기지만 연사 속도를 리셋하는 부분을 그냥 Shoot() 안에로 옮겨 주었다.
SubMachineGun1
의 📜Gun.cs 에 재장전 관련 멤버 함수 값 할당.
🚖 정조준
정조준 애니메이션
- 파라미터로 Bool 타입의
FindSightMode
를 추가한다.Reload
처럼 Trigger가 아닌 이유는, 재장전은 연속적이 아닌 딱 한번만 이루어져야 하기 때문 Trigger로 애니메이션을 재생시키는게 적합하다.- 반면에 정조준은 한번 우클을 눌러 정조준 상태가 되면 정조준 상태를 유지해야 한다. 다시 우클을 또 눌러 정조준을 해제하지 않는 이상! 따라서 Boolean 이 적합하다.
- 따라서
Any Stage
👉Gun0_FindSightMode
또한 적합하지 않다.FindSightMode
가 Boolean 타입이기 때문에 한번 True 로 변경되면 수동적으로 바꿔주지 않는 한 True 를 유지하기 때문에 어떤 상태이든간에Any Stage
👉Gun0_FindSightMode
이 무한적으로 실행되기 때문이다.
- 따라서
Gun0_FindSightMode
👉Gun0_Idle
- 조건
FindSightMode
값이 True 가 되면 발동한다.- 즉시 재생하게 Hax Exit Time 체크 해제
- Translation Duration 은 0.1
- 조건
Gun0_Idle
👉Gun0_FindSightMode
- 조건
FindSightMode
값이 False 가 되면 발동한다.- 즉시 재생하게 Hax Exit Time 체크 해제
- Translation Duration 은 0.1
- 조건
Gun0_Walk
👉Gun0_FindSightMode
- 조건
FindSightMode
값이 True 가 되면 발동한다.- 즉시 재생하게 Hax Exit Time 체크 해제
- Translation Duration 은 0.1
Gun0_FindSightMode
👉Gun0_Walk
은 없어도 된다.
- 조건
📜GunController.cs
// 추가한 멤버 변수
private bool isFindSightMode = false; // 정조준 중인지.
[SerializeField]
private Vector3 originPos; // 원래 총의 위치(정조준 해제하면 나중에 돌아와야 하니까)
void Update()
{
GunFireRateCalc();
TryFire();
TryReload();
TryFindSight();
}
private void TryFindSight()
{
if(Input.GetButtonDown("Fire2"))
{
FindSight();
}
}
private void FindSight()
{
isFindSightMode = !isFindSightMode;
currentGun.anim.SetBool("FindSightMode", isFindSightMode);
if(isFindSightMode)
{
StopAllCoroutines();
StartCoroutine(FindSightActivateCoroutine());
}
else
{
StopAllCoroutines();
StartCoroutine(FindSightDeActivateCoroutine());
}
}
IEnumerator FindSightActivateCoroutine()
{
while(currentGun.transform.localPosition != currentGun.findSightOriginPos)
{
currentGun.transform.localPosition = Vector3.Lerp(currentGun.transform.localPosition, currentGun.findSightOriginPos, 0.2f);
yield return null;
}
}
IEnumerator FindSightDeActivateCoroutine()
{
while (currentGun.transform.localPosition != originPos)
{
currentGun.transform.localPosition = Vector3.Lerp(currentGun.transform.localPosition, originPos, 0.2f);
yield return null;
}
}
- 매 프레임마다
- 🚗 TryFindSight()
- 마우스 우클릭 입력이 들어오는지를 검사하고 마우스 우클 입력이 들어오면 FindSight() 함수를 실행시킨다.
- 🚗 FindSight()
- 정조준 중이였다면 정조준을 해제하고, 정조준 안된 상태였다면 정조준을 한다. 즉, 우클 한번 누른 후에 정조준 상태를 유지하고 우클을 다시 누르면 정조준을 해제 하게 한다.
isFindSightMode = !isFindSightMode;
FindSightMode
파라미터 값을isFindSightMode
값으로 설정한다. True면FindSightMode = True
조건에 따른 정조준 애니메이션이 재생되고FindSightMode = False
라면 정조준 애니메이션이 더 이상 재생되지 않는다.- 정조준 해야 한다면 (우클 홀수번째 클릭)
- 우선 현재 실행중인 코루틴 함수를 모두 중단한다.
- 중단 하는 이유
- 밑에 후술할 2 개의 코루틴 함수는 각각 다음과 같은 일을 한다.
- 정중앙 위치로 총(자기 자신)의 위치를 설정,
- 정조준 해제를 위해 원래의 위치로 총(자기 자신)의 위치를 설정.
- 위치를 변하게 할 때 while 문 안에서 Lerp 함수를 통해 보간하며 부드럽게 점점 변화시킬 것인데, 이전 포스트에서 배웠듯이 보간은 딱 떨어져서 되지 않는 경우가 대다수다. 그래서 변화를 마치고 원하는 위치로 딱 정확하게 일치할 때까지 반복시킨다는 그 while문 조건을 만족하기가 힘들다.
- 따라서 while문이 영원히 끝나지 않아 두 코루틴 함수가 내내 끝나지 않고 두 코루틴 함수가 병행해서 계속 while 문에 갇혀 보간을 무한 반복하고 있는 상태가 되어 두 함수가 계속 실행되기 때문에 총(자기 자신)의 위치는 계속해서 변한다.
- 이런 상황에서 또 우클 입력이 들어와 정조준을 해야 하면 변화 중인 위치에서부터 또 보간을 시작하기 때문에 정조준이 제대로 되지 않는다. 중앙이 아닌 엉뚱한 곳으로 총이 움직일 것이다.
- 따라서!!!!!!! 정조준을 위한 위치 설정 or 원래 위치로 설정 작업을 하기 전에 기존에 실행중이던 코루틴 함수들이 있다면 다 중단을 시켜야 정확하게 위치하게 할 수 있다!
- 밑에 후술할 2 개의 코루틴 함수는 각각 다음과 같은 일을 한다.
- 중단 하는 이유
- FindSightActivateCoroutine() 함수를 실행한다.
- 정조준을 하기 위해서 현재 총(자기 자신)의 위치를 정조준시 총의 위치인 📜Gun.cs의
findSightOriginPos
로 변화시키는데, 이를 부드럽게 업뎃하는 일을 수행한다.- FindSightActivateCoroutine() 함수
- 현재 위치가 정조준시 총이 위치가 될 때까지 현재 위치를 업데이트 하고 한 프레임 쉬기를 계속 반복한다.
- 부드럽게 5 번 보간되어 업뎃된다.
- FindSightActivateCoroutine() 함수
- 정조준을 하기 위해서 현재 총(자기 자신)의 위치를 정조준시 총의 위치인 📜Gun.cs의
- 우선 현재 실행중인 코루틴 함수를 모두 중단한다.
- 정조준을 해제해야 한다면 (우클 짝수번째 클릭)
- 우선 현재 실행중인 코루틴 함수를 모두 중단한다.
- 이유 위와 동일
- FindSightDeActivateCoroutine() 함수를 실행한다.
- 정조준 해제를 하기 위해서 현재 총(자기 자신)의 위치를 원래의 총의 위치인
originPos
로 변화시키는데, 이를 부드럽게 업뎃하는 일을 수행한다.
- 정조준 해제를 하기 위해서 현재 총(자기 자신)의 위치를 원래의 총의 위치인
- 우선 현재 실행중인 코루틴 함수를 모두 중단한다.
- 정조준 중이였다면 정조준을 해제하고, 정조준 안된 상태였다면 정조준을 한다. 즉, 우클 한번 누른 후에 정조준 상태를 유지하고 우클을 다시 누르면 정조준을 해제 하게 한다.
- 🚗 TryFindSight()
📜Gun.cs findSightOriginPos
정조준시 총이 있어야할 위치를 (0.55, 0.335, -0.586) 으로 설정한다. 총이 딱 정중앙으로 오는 위치다.
📜GunController.cs originPos
는 그대로 (0, 0, 0)으로 둔다. SubMachineGun1
의 원래 위치(LocalPosition)가 (0, 0, 0) 이기 때문에.
🚖 반동
📜GunController.cs
IEnumerator RetroActionCoroutine()
{
Vector3 recoilBack = new Vector3(currentGun.retroActionForce, originPos.y, originPos.z); // 정조준 안 했을 때의 최대 반동
Vector3 retroActionRecoilBack = new Vector3(currentGun.retroActionFineSightForce, currentGun.fineSightOriginPos.y, currentGun.fineSightOriginPos.z); // 정조준 했을 때의 최대 반동
if(!isFineSightMode) // 정조준이 아닌 상태
{
currentGun.transform.localPosition = originPos;
// 반동 시작
while(currentGun.transform.localPosition.x <= currentGun.retroActionForce - 0.02f)
{
currentGun.transform.localPosition = Vector3.Lerp(currentGun.transform.localPosition, recoilBack, 0.4f);
yield return null;
}
// 원위치
while (currentGun.transform.localPosition != originPos)
{
currentGun.transform.localPosition = Vector3.Lerp(currentGun.transform.localPosition, originPos, 0.1f);
yield return null;
}
}
else // 정조준 상태
{
currentGun.transform.localPosition = currentGun.fineSightOriginPos;
// 반동 시작
while(currentGun.transform.localPosition.x <= currentGun.retroActionFineSightForce - 0.02f)
{
currentGun.transform.localPosition = Vector3.Lerp(currentGun.transform.localPosition, retroActionRecoilBack, 0.4f);
yield return null;
}
// 원위치
while (currentGun.transform.localPosition != currentGun.fineSightOriginPos)
{
currentGun.transform.localPosition = Vector3.Lerp(currentGun.transform.localPosition, currentGun.fineSightOriginPos, 0.1f);
yield return null;
}
}
}
private void Shoot() // 실제 발사 되는 과정
{
currentGun.currentBulletCount--;
currentFireRate = currentGun.fireRate; // 연사 속도 재계산
PlaySE(currentGun.fire_Sound);
currentGun.muzzleFlash.Play();
// 총기 반동 코루틴 실행
StopAllCoroutines();
StartCoroutine(RetroActionCoroutine());
Debug.Log("총알 발사 함");
}
구현하려는 반동은 총이 올라가는 수직 반동이 아니라 총을 발사 할 때마다 총이 앞 뒤로 왔다 갔다 하는 반동을 구현할 것이다. GunHolder
를 Y 축으로 90 도 회전했었던 탓에, 총의 앞,뒤 이동 축은 Z 축이 아닌 X 축이 되었다. 사진에서 화살표 방향, 즉 플레이어 쪽으로 총이 당겨지는 쪽이 X 축 positive 방향이다. 발사할 때마다 총의 X 축 방향으로 이동값을 주어서 반동시키고 다시 원래 위치로 되돌아가는 식으로 반동을 표현한다. X 방향 Position 값만 변경해주면 된다. 정조준 하고 쐈을 때와 정조준 안하고 쐇을 때 이렇게 두 케이스로 나눈다.
recoilBack
- 정조준 안 했을 때의 최대 반동.
- 즉 X 방향으로 📜Gun.cs의
retroActionForce
(정조준 안 했을 때의 반동 세기) 위치로 이동시킨다. (델타값 아님. 절대적인 x 방향 좌표값임)- 현재 위치로부터 Vector3(
retroActionForce
, 원래Y위치, 원래Z위치) 로 Lerp 보간하여 부드럽게 업뎃시킬 것이다. X 위치 값만 변화를 준것을 확인할 수 있다.Vector3 recoilBack = new Vector3(currentGun.retroActionForce, originPos.y, originPos.z);
- 현재 위치로부터 Vector3(
retroActionRecoilBack
- 정조준 했을 때의 최대 반동.
- 즉 X 방향으로 📜Gun.cs의
retroActionFineSightForce
(정조준 했을 때의 반동 세기) 위치로 이동시킨다. (델타값 아님. 절대적인 x 방향 좌표값임)- 현재 위치로부터 Vector3(
retroActionFineSightForce
, 정조준시Y위치, 정조준시Z위치) 로 Lerp 보간하여 부드럽게 업뎃시킬 것이다. X 위치 값만 변화를 준것을 확인할 수 있다.Vector3 retroActionRecoilBack = new Vector3(currentGun.retroActionFineSightForce, currentGun.fineSightOriginPos.y, currentGun.fineSightOriginPos.z);
- 현재 위치로부터 Vector3(
- 정조준이 아닌 상태일 때
- 반동이 이루어지고 있는 상태에서 또 반동이 들어오면(또 발사가 들어 오면) 중복 처리가 되기 때문에(코루틴 함수라 병렬적으로 실행하는게 가능하므로) 반동을 시작하고 원 위치로 되돌리는 총체적인 반동 작업을 시작 하기에 앞서 총 위치를 정조준이 아닌 원래 위치에서 시작하게끔 한다.
currentGun.transform.localPosition = originPos;
- 1️⃣ 반동을 시작한다. 👉 총의 현재 절대 위치 값을 시작으로
recoilBack
Vector3 값을 목표로 총의 현재 위치 값을 부드럽게 업뎃 해나간다.- 부드럽게 보간하는 작업은 총의 X 방향 위치가
recoilBack
보다 커질 때까지(반복 종료조건) 반복한다. 단, 보간 때문에 영원히 이렇게 안될 수도 있어서recoilBack
에서 0.02 를 빼준값 보다 커질 때까지만 반복하도록 살짝 보정해주었다.- 현재 위치가
recoilBack
위치로 될 때까지 40 퍼센트 비율로 보정한다. - 한 프레임씩 쉰다.
- 현재 위치가
- 부드럽게 보간하는 작업은 총의 X 방향 위치가
- 2️⃣ 원 위치로 돌아간다. 👉 한번
recoilBack
Vector3 근처까지 당겨졌었으면 다시 원래 위치로 돌아 가야 한다. 총의 현재 위치 값을 시작으로 원래 위치인originPos
Vector3 값을 목표로 총의 현재 위치 값을 부드럽게 업뎃 해나간다.- 부드럽게 보간하는 작업은 총의 현재 위치가 원래 위치와 일치하게 될 때까지 ! (사실 이것도 보간 때문에 무한 루프에 빠질 수 있는 부분인데 나중에 코루틴 중지하는 작업을 해줄거라서 괜찮다.)
- 현재 위치가
originPos
위치로 될 때까지 10 퍼센트 비율로 보정한다. (반동 때보다는 좀 더 느리게 돌아오게끔) - 한 프레임씩 쉰다.
- 현재 위치가
- 부드럽게 보간하는 작업은 총의 현재 위치가 원래 위치와 일치하게 될 때까지 ! (사실 이것도 보간 때문에 무한 루프에 빠질 수 있는 부분인데 나중에 코루틴 중지하는 작업을 해줄거라서 괜찮다.)
- 반동이 이루어지고 있는 상태에서 또 반동이 들어오면(또 발사가 들어 오면) 중복 처리가 되기 때문에(코루틴 함수라 병렬적으로 실행하는게 가능하므로) 반동을 시작하고 원 위치로 되돌리는 총체적인 반동 작업을 시작 하기에 앞서 총 위치를 정조준이 아닌 원래 위치에서 시작하게끔 한다.
- 정조준 상태일 때
- 위와 방식이 동일하다. 반동 값은
retroActionRecoilBack
, 원래 위치는 총의 정조준 위치인currentGun.fineSightOriginPos
으로 쓰는 것 빼고는 같다.
- 위와 방식이 동일하다. 반동 값은
- 정조준이 아닐 때 총의 위치는 (0, 0, 0)
- 발사시 반동으로 (0.3, 0, 0) 에 근접해졌다가 다시 (0, 0, 0)으로 돌아온다.
- 정조준 중 일 때 총의 위치는 (0.55, 0.33, -0.557)
- 발사시 반동으로 (1.25, 0.33, -0.557) 에 근접해졌다가 다시 (0.55, 0.33, -0.557)으로 돌아온다. (0.55 + 0.7 = 1.250)
🚔 그 밖의 개선
재장전 하는 도중에 우클(정조준) 하고 나면 발사가 안되는 버그
이런 버그가 나타나는 이유
- 재장전 애니메이션을 실행하는 도중에, 즉, ReloadCoroutine() 코루틴 함수에서 currentGun.anim.SetTrigger(“Reload”); 재장전 애니메이션을 재생하고 yield return new WaitForSeconds(currentGun.reloadTime);
reloadTime
동안 대기 시간을 가지는 동안에 - 위 대기 시간 동안 우클 입력이 들어와 정조준을 하게 되어서 재장전 애니메이션을 재생하는 대기 시간동안 FindSight() 함수를 실행하고, StopAllcoroutines() 실행을 만나기 때문이다.
- 따라서
isReload
가 다시 False 가 되기 전에 ReloadCoroutine() 코루틴 함수가 중단이 되어 버려서isReload
는 True 인 상태로 남게 되어서 발사가 불가능해진 것이다.!isReload
재장전 중이 아닐 때만 발사가 가능하므로.
- 따라서
📜GunController.cs
따라서 ReloadCoroutine() 실행 중일 때 ReloadCoroutine() 함수는 StopAllcoroutines() 실행으로 중단되지 않도록 해야 한다. 재장전 중일 땐 정조준/정조준해제 가 이루어지면 안된다.
private void TryFindSight()
{
if(Input.GetButtonDown("Fire2") && !isReload)
{
FindSight();
}
}
정조준은 재장전 중이 아닐 때만 실행할 수 있도록 한다.
private void CancelFineSight()
{
if (isFineSightMode)
FineSight();
}
private void TryReload()
{
if (Input.GetKeyDown(KeyCode.R) && !isReload && currentGun.currentBulletCount < currentGun.reloadBulletCount)
{
CancelFineSight();
StartCoroutine(ReloadCoroutine());
}
}
private void Fire() // 발사를 위한 과정
{
if (!isReload)
{
if (currentGun.currentBulletCount > 0)
Shoot();
else
{
CancelFineSight();
StartCoroutine(ReloadCoroutine());
}
}
}
정조준 상태에서 재장전 우클 입력이 들어온다면 정조준을 취소한다.
- CancelFineSight() 함수
- 정조준 중이라면 FineSight() 한번 더 실행하여 정조준을 해제시킨다.
- 재장전 하기 전에 CancelFineSight() 함수를 한번 실행하여 정조준 상태라면 해제시킨다.
- 재장전 실행시키는 곳
- TryReload()
- Fire() 발사하려는데 탄창이 비어있을 때
- 재장전 실행시키는 곳
정조준 상태에서 뛰면 정조준 해제 시키기
📜PlayController.cs
// 추가한 멤버 변수
private GunController theGunController;
void Start()
{
...
theGunController = FindObjectOfType<GunController>();
...
}
private void Running()
{
if (isCrouch)
Crouch();
theGunController.CancelFineSight();
isRun = true;
applySpeed = runSpeed;
}
- 📜GunController.cs 의 CancelFineSight() 함수를 public으로 바꾼 후
- 📜PlayController.cs 의 Running() 함수에서 이를 실행.
- 달리는 중에 정조준 중인 상태라면 이를 해제 하도록
🚔 여기까지 스크립트 정리
📜PlayController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
// 스피드 조정 변수
[SerializeField]
private float walkSpeed;
[SerializeField]
private float runSpeed;
[SerializeField]
private float crouchSpeed;
private float applySpeed;
[SerializeField]
private float jumpForce;
// 상태 변수
private bool isRun = false;
private bool isGround = true;
private bool isCrouch = false;
// 앉았을 때 얼마나 앉을지 결정하는 변수
[SerializeField]
private float crouchPosY;
private float originPosY;
private float applyCrouchPosY;
// 민감도
[SerializeField]
private float lookSensitivity;
// 카메라 한계
[SerializeField]
private float cameraRotationLimit;
private float currentCameraRotationX;
// 필요한 컴포넌트
[SerializeField]
private Camera theCamera;
private Rigidbody myRigid;
private CapsuleCollider capsuleCollider;
private GunController theGunController;
void Start()
{
// 컴포넌트 할당
capsuleCollider = GetComponent<CapsuleCollider>();
myRigid = GetComponent<Rigidbody>();
applySpeed = walkSpeed;
theGunController = FindObjectOfType<GunController>();
originPosY = theCamera.transform.localPosition.y;
applyCrouchPosY = originPosY;
}
void Update()
{
IsGround();
TryJump();
TryRun();
TryCrouch();
Move();
CameraRotation();
CharacterRotation();
}
// 지면 체크
private void IsGround()
{
isGround = Physics.Raycast(transform.position, Vector3.down, capsuleCollider.bounds.extents.y + 0.1f);
}
// 점프 시도
private void TryJump()
{
if (Input.GetKeyDown(KeyCode.Space) && isGround)
{
Jump();
}
}
// 점프
private void Jump()
{
if (isCrouch)
Crouch();
myRigid.velocity = transform.up * jumpForce;
}
// 달리기 시도
private void TryRun()
{
if (Input.GetKey(KeyCode.LeftShift))
{
Running();
}
if (Input.GetKeyUp(KeyCode.LeftShift))
{
RunningCancel();
}
}
// 달리기
private void Running()
{
if (isCrouch)
Crouch();
theGunController.CancelFineSight();
isRun = true;
applySpeed = runSpeed;
}
// 달리기 취소
private void RunningCancel()
{
isRun = false;
applySpeed = walkSpeed;
}
// 앉기 시도
private void TryCrouch()
{
if (Input.GetKeyDown(KeyCode.LeftControl))
{
Crouch();
}
}
// 앉기 동작
private void Crouch()
{
isCrouch = !isCrouch;
if (isCrouch)
{
applySpeed = crouchSpeed;
applyCrouchPosY = crouchPosY;
}
else
{
applySpeed = walkSpeed;
applyCrouchPosY = originPosY;
}
StartCoroutine(CrouchCoroutine());
}
// 부드러운 앉기 동작
IEnumerator CrouchCoroutine()
{
float _posY = theCamera.transform.localPosition.y;
int count = 0;
while(_posY != applyCrouchPosY)
{
count++;
_posY = Mathf.Lerp(_posY, applyCrouchPosY, 0.2f);
theCamera.transform.localPosition = new Vector3(0, _posY, 0);
if(count > 15)
break;
yield return null;
}
theCamera.transform.localPosition = new Vector3(0, applyCrouchPosY, 0);
}
private void Move()
{
float _moveDirX = Input.GetAxisRaw("Horizontal");
float _moveDirZ = Input.GetAxisRaw("Vertical");
Vector3 _moveHorizontal = transform.right * _moveDirX;
Vector3 _moveVertical = transform.forward * _moveDirZ;
Vector3 _velocity = (_moveHorizontal + _moveVertical).normalized * applySpeed;
myRigid.MovePosition(transform.position + _velocity * Time.deltaTime);
}
private void CameraRotation()
{
float _xRotation = Input.GetAxisRaw("Mouse Y");
float _cameraRotationX = _xRotation * lookSensitivity;
currentCameraRotationX -= _cameraRotationX;
currentCameraRotationX = Mathf.Clamp(currentCameraRotationX, -cameraRotationLimit, cameraRotationLimit);
theCamera.transform.localEulerAngles = new Vector3(currentCameraRotationX, 0f, 0f);
}
private void CharacterRotation()
{
float _yRotation = Input.GetAxisRaw("Mouse X");
Vector3 _characterRotationY = new Vector3(0f, _yRotation, 0f) * lookSensitivity;
myRigid.MoveRotation(myRigid.rotation * Quaternion.Euler(_characterRotationY));
}
}
📜GunController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GunController : MonoBehaviour
{
[SerializeField]
private Gun currentGun; // 현재 들고 있는 총. 📜Gun.cs 가 할당 됨.
private float currentFireRate; // 이 값이 0 보다 큰 동안에는 총알이 발사 되지 않는다. 초기값은 연사 속도인 📜Gun.cs의 fireRate
private bool isReload = false; // 재장전 중인지.
private bool isFineSightMode = false; // 정조준 중인지.
[SerializeField]
private Vector3 originPos; // 원래 총의 위치(정조준 해제하면 나중에 돌아와야 하니까)
private AudioSource audioSource; // 발사 소리 재생기
void Start()
{
audioSource = GetComponent<AudioSource>();
}
void Update()
{
GunFireRateCalc();
TryFire();
TryReload();
TryFineSight();
}
private void GunFireRateCalc()
{
if (currentFireRate > 0)
currentFireRate -= Time.deltaTime; // 즉, 1 초에 1 씩 감소시킨다.
}
private void TryFire() // 발사 입력을 받음
{
if(Input.GetButton("Fire1") && currentFireRate <= 0 && !isReload)
{
Fire();
}
}
private void Fire() // 발사를 위한 과정
{
if (!isReload)
{
if (currentGun.currentBulletCount > 0)
Shoot();
else
{
CancelFineSight();
StartCoroutine(ReloadCoroutine());
}
}
}
private void Shoot() // 실제 발사 되는 과정
{
currentGun.currentBulletCount--;
currentFireRate = currentGun.fireRate; // 연사 속도 재계산
PlaySE(currentGun.fire_Sound);
currentGun.muzzleFlash.Play();
// 총기 반동 코루틴 실행
StopAllCoroutines();
StartCoroutine(RetroActionCoroutine());
Debug.Log("총알 발사 함");
}
private void TryReload()
{
if (Input.GetKeyDown(KeyCode.R) && !isReload && currentGun.currentBulletCount < currentGun.reloadBulletCount)
{
CancelFineSight();
StartCoroutine(ReloadCoroutine());
}
}
IEnumerator ReloadCoroutine()
{
if(currentGun.carryBulletCount > 0)
{
isReload = true;
currentGun.anim.SetTrigger("Reload");
currentGun.carryBulletCount += currentGun.currentBulletCount;
currentGun.currentBulletCount = 0;
yield return new WaitForSeconds(currentGun.reloadTime); // 재장전 애니메이션이 다 재생될 동안 대기
if (currentGun.carryBulletCount >= currentGun.reloadBulletCount)
{
currentGun.currentBulletCount = currentGun.reloadBulletCount;
currentGun.carryBulletCount -= currentGun.reloadBulletCount;
}
else
{
currentGun.currentBulletCount = currentGun.carryBulletCount;
currentGun.carryBulletCount = 0;
}
isReload = false;
}
else
{
Debug.Log("소유한 총알이 없습니다.");
}
}
private void TryFineSight()
{
if(Input.GetButtonDown("Fire2") && !isReload)
{
FineSight();
}
}
public void CancelFineSight()
{
if (isFineSightMode)
FineSight();
}
private void FineSight()
{
isFineSightMode = !isFineSightMode;
currentGun.anim.SetBool("FineSightMode", isFineSightMode);
if(isFineSightMode)
{
StopAllCoroutines();
StartCoroutine(FineSightActivateCoroutine());
}
else
{
StopAllCoroutines();
StartCoroutine(FineSightDeActivateCoroutine());
}
}
IEnumerator FineSightActivateCoroutine()
{
while(currentGun.transform.localPosition != currentGun.fineSightOriginPos)
{
currentGun.transform.localPosition = Vector3.Lerp(currentGun.transform.localPosition, currentGun.fineSightOriginPos, 0.2f);
yield return null;
}
}
IEnumerator FineSightDeActivateCoroutine()
{
while (currentGun.transform.localPosition != originPos)
{
currentGun.transform.localPosition = Vector3.Lerp(currentGun.transform.localPosition, originPos, 0.2f);
yield return null;
}
}
IEnumerator RetroActionCoroutine()
{
Vector3 recoilBack = new Vector3(currentGun.retroActionForce, originPos.y, originPos.z); // 정조준 안 했을 때의 최대 반동
Vector3 retroActionRecoilBack = new Vector3(currentGun.retroActionFineSightForce, currentGun.fineSightOriginPos.y, currentGun.fineSightOriginPos.z); // 정조준 했을 때의 최대 반동
if(!isFineSightMode) // 정조준이 아닌 상태
{
currentGun.transform.localPosition = originPos;
// 반동 시작
while(currentGun.transform.localPosition.x <= currentGun.retroActionForce - 0.02f)
{
currentGun.transform.localPosition = Vector3.Lerp(currentGun.transform.localPosition, recoilBack, 0.4f);
yield return null;
}
// 원위치
while (currentGun.transform.localPosition != originPos)
{
currentGun.transform.localPosition = Vector3.Lerp(currentGun.transform.localPosition, originPos, 0.1f);
yield return null;
}
}
else // 정조준 상태
{
currentGun.transform.localPosition = currentGun.fineSightOriginPos;
// 반동 시작
while(currentGun.transform.localPosition.x <= currentGun.retroActionFineSightForce - 0.02f)
{
currentGun.transform.localPosition = Vector3.Lerp(currentGun.transform.localPosition, retroActionRecoilBack, 0.4f);
yield return null;
}
// 원위치
while (currentGun.transform.localPosition != currentGun.fineSightOriginPos)
{
currentGun.transform.localPosition = Vector3.Lerp(currentGun.transform.localPosition, currentGun.fineSightOriginPos, 0.1f);
yield return null;
}
}
}
private void PlaySE(AudioClip _clip) // 발사 소리 재생
{
audioSource.clip = _clip;
audioSource.Play();
}
}
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글 남기기