Chapter 2-3. 무기 : 총 피격 구현
카테고리: Unity Lesson 3
태그: Unity Game Engine
인프런에 있는 케이디님의 [유니티 3D] 실전! 생존게임 만들기 - Advanced 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click
Chapter 2. 무기
총 피격 구현
총알이 맞는 곳의 피격 효과
🚖 피격 위치 확인
📜GunController.cs
// 추가한 멤버 변수
private RaycastHit hitInfo; // 총알의 충돌 정보
[SerializeField]
private Camera theCam; // 카메라 시점에서 정 중앙에 발사할 거라서
private void Shoot() // 실제 발사 되는 과정
{
// 발사 처리
currentGun.currentBulletCount--;
currentFireRate = currentGun.fireRate; // 연사 속도 재계산
PlaySE(currentGun.fire_Sound);
currentGun.muzzleFlash.Play();
// 피격 처리
Hit();
// 총기 반동 코루틴 실행
StopAllCoroutines();
StartCoroutine(RetroActionCoroutine());
}
private void Hit()
{
// 카메라 월드 좌표!! (localPosition이 아님)
if (Physics.Raycast(theCam.transform.position, theCam.transform.forward, out hitInfo, currentGun.range))
{
Debug.Log(hitInfo.transform.name);
}
}
플레이어 머리 위에 달려 있는 카메라로부터 RayCast를 쏴서 충돌 되는 지점을 총알이 맞은 곳이라고 할 것이다. 1 인칭 카메라이기 때문에 사실상 화면 정중앙에 총을 쏘게 되기 때문이다.
theCam
Main Camera
를 이 변수에 할당해주자.
- Shoot()
- 발사 처리 후, 반동 처리 전, Hit() 함수 실행
- Hit()
- 👉 총알이 맞은 곳을 인지한다.
- 카메라 위치로부터 카메라의 앞 방향으로 Raycast를 공격 사정 거리인
range
거리만큼 쐈을 때 충돌 되는 것이 있다면 그 정보를hitInfo
에 담고 충돌 된 오브젝트의 이름을 출력! - theCam.transform.localPosition이 아닌, theCam.transform.position 을 기준으로 한다. 즉, 카메라의 로컬 위치가 아닌 카메라의 월드 좌표 기준에서의 위치로부터 Raycast를 쏜다.
- 카메라 위치로부터 카메라의 앞 방향으로 Raycast를 공격 사정 거리인
- 👉 총알이 맞은 곳을 인지한다.
🚖 피격 이펙트 파티클 시스템
파티클 시스템 만들기
피격 효과가 될 “Hit Effect” 이름의 파티클 시스템을 만든다.
- 메인 모듈
- Duration - 1.0
- Start Lifetime - 0.5
- Start Speed - 15
- Start Color - 색깔 지정
- Loop 체크 해제
- Play On Awake 체크 해둔다!!! 👉 이 파티클이 생성되자마자 바로 재생.
- Gravity Modifier - 0.4
- 중력의 영향을 받아 파티클 입자가 생성 후 자연스레 떨어지게 된다.
- Emission 모듈
- Rate over Time - 0
- Bursts 추가 - Count는 7로
- Shape 모듈 - 입자들이 방출되는 모양을 결정한다.
- Angle - 70
- 파티클 입자가 더 넓게 튀도록
- Cone 원뿔 꼭지점의 각도로, 각도가 0 이면 원기둥이 되고 90 이면 평면 원반이 된다.
- Radius - 0.0001 최소로 해줌. (원 뿔의 반지름)
- 가장 작은 거의 점에 가까운 반경 내에서 파티클들이 생성 되도록
- Angle - 70
- Color Over Lifetime
- 자연스럽게 사라지도록 수명 주기의 중간 즈음부터는 투명해지게 하였다.
- Size Over Lifetime
- 자연스럽게 작아지면서 사라지도록
- Trails 모듈
- 잔상 남기기
- Ratio 는 1 로 설정
- 잔상을 남길 입자 비율
- 파티클 모든 입자에게 100 퍼 잔상을 생성한다.
- 만약 0.5 라면 입자 개수의 절반에 해당하는 랜덤한 입자들만 잔상을 생성한다.
- Lifetime 은 0.2 로 설정
- 잔상의 수명
- Minimum Vertex Distance는 기본값이 0.2
- 잔상이 어느 거리만큼 가야 Vertex 가 업데이트 될지를 나타내는 설정 값이다.
- 이 값이 작으면 작을 수록, 0 에 가까울 수록 자주 업데이트 되므로 잔상이 자연스럽다.
- 크면 클수록 잔상이 업데이트 되는 거리가 길어져 뚝뚝 끊길 것이다.
- Color Over LifeTime
- 잔상의 알파값을 0.5 로 해주어 점점 투명해지게 하였다.
- Ratio 는 1 로 설정
- 잔상 남기기
- Renderer 모듈
- Trails 모듈을 사용하기 위해 Trail Maerial을 설정해준다. - “Sprited Default”로 설정.
이제 이
Hit Effect
파티클 시스템을 📂Assets/Prefabs 로 옮겨 프리팹화 시킨다.
프리팹으로 만든 후 Hierarchy 에서는 지워준다. 총에게 피격 당한 위치에서 Hit Effect
프리팹을 오브젝트로 생성시킬 것이다.
📜GunController.cs
피격시 프리팹화 해 두었던
Hit Effect
피격 파티클 시스템 생성하기
// 추가한 멤버 변수
[SerializeField]
private GameObject hitEffectPrefab;
private void Hit()
{
// 카메라 월드 좌표!! (localPosition이 아님)
if (Physics.Raycast(theCam.transform.position, theCam.transform.forward, out hitInfo, currentGun.range))
{
GameObject clone = Instantiate(hitEffectPrefab, hitInfo.point, Quaternion.LookRotation(hitInfo.normal));
Destroy(clone, 2f);
}
}
- 카메라로부터 Raycast를 쐈을 때 충돌된게 있으면
Hit Effect
피격 파티클 시스템 프리팹을 생성한다.- 피격된 위치에 (hitInfo.point)
- 피격된 위치의 표면의 방향을 쳐다보는 방향으로 (Quaternion.LookRotation(hitInfo.normal))
hitEffectPrefab
을 생성하여clone
변수에 할당 한다.
- 게임 플레이 중 무한정 생성하다보면 메모리 부족 문제가 생길 수 있으므로 이 파티클 시스템은 2 초 뒤에 사라지게 한다.
- Destroy(clone, 2f)
- 파티클을 생성해주기만 하면 된다. Play On Awake에 체크 해놨기 때문에 파티클이 생성되어 활성화되자마자 자동으로 재생될 것이다. 따라서 Play()를 직접 실행시킬 필요는 없다.
Hit Effect
프리팹을 hitEffectPrefab
에 할당.
🚖 여기까지 스크립트 정리
📜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; // 재장전 중인지.
[HideInInspector]
public bool isFineSightMode = false; // 정조준 중인지.
[SerializeField]
private Vector3 originPos; // 원래 총의 위치(정조준 해제하면 나중에 돌아와야 하니까)
private AudioSource audioSource; // 발사 소리 재생기
private RaycastHit hitInfo; // 총알의 충돌 정보
[SerializeField]
private Camera theCam; // 카메라 시점에서 정 중앙에 발사할 거라서
[SerializeField]
private GameObject hitEffectPrefab;
void Start()
{
originPos = Vector3.zero;
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();
// 피격 처리
Hit();
// 총기 반동 코루틴 실행
StopAllCoroutines();
StartCoroutine(RetroActionCoroutine());
}
private void Hit()
{
// 카메라 월드 좌표!! (localPosition이 아님)
if (Physics.Raycast(theCam.transform.position, theCam.transform.forward, out hitInfo, currentGun.range))
{
GameObject clone = Instantiate(hitEffectPrefab, hitInfo.point, Quaternion.LookRotation(hitInfo.normal));
Destroy(clone, 2f);
}
}
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();
}
}
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글남기기