Unity Chapter 11-9. 좀비 TPS 게임 만들기 : Gun
카테고리: Unity Lesson 1
태그: Unity Game Engine
인프런에 있는 이제민님의 레트로의 유니티 C# 게임 프로그래밍 에센스 강의를 듣고 정리한 필기입니다. 😀
🌜 [레트로의 유니티 C# 게임 프로그래밍 에센스] 강의 들으러 가기!
Chapter 11. 좀비 TPS 게임 만들기
📜Gun.cs
Gun
오브젝트에 붙인다.
using System;
using System.Collections;
using UnityEngine;
// 총을 구현한다
public class Gun : MonoBehaviour
{
// 총의 상태를 표현하는데 사용할 타입을 선언한다
public enum State
{
Ready, // 발사 준비됨
Empty, // 탄창이 빔
Reloading // 재장전 중
}
public State state { get; private set; } // 현재 총의 상태
private PlayerShooter gunHolder;
private LineRenderer bulletLineRenderer; // 총알 궤적을 그리기 위한 렌더러
private AudioSource gunAudioPlayer; // 총 소리 재생기
public AudioClip shotClip; // 발사 소리
public AudioClip reloadClip; // 재장전 소리
public ParticleSystem muzzleFlashEffect; // 총구 화염 효과
public ParticleSystem shellEjectEffect; // 탄피 배출 효과
public Transform fireTransform; // 총알이 발사될 위치
public Transform leftHandMount;
public float damage = 25; // 공격력
public float fireDistance = 100f; // 사정거리
public int ammoRemain = 100; // 남은 전체 탄약
public int magAmmo; // 현재 탄창에 남아있는 탄약
public int magCapacity = 30; // 탄창 용량
public float timeBetFire = 0.12f; // 총알 발사 간격
public float reloadTime = 1.8f; // 재장전 소요 시간
[Range(0f, 10f)] public float maxSpread = 3f;
[Range(1f, 10f)] public float stability = 1f;
[Range(0.01f, 3f)] public float restoreFromRecoilSpeed = 2f;
private float currentSpread;
private float currentSpreadVelocity;
private float lastFireTime; // 총을 마지막으로 발사한 시점
private LayerMask excludeTarget;
private void Awake()
{
// 사용할 컴포넌트들의 참조를 가져오기
gunAudioPlayer = GetComponent<AudioSource>();
bulletLineRenderer = GetComponent<LineRenderer>();
// 사용할 점을 두개로 변경
bulletLineRenderer.positionCount = 2;
// 라인 렌더러를 비활성화
bulletLineRenderer.enabled = false;
}
public void Setup(PlayerShooter gunHolder)
{
this.gunHolder = gunHolder;
excludeTarget = gunHolder.excludeTarget;
}
private void OnEnable()
{
currentSpread = 0;
// 현재 탄창을 가득채우기
magAmmo = magCapacity;
// 총의 현재 상태를 총을 쏠 준비가 된 상태로 변경
state = State.Ready;
// 마지막으로 총을 쏜 시점을 초기화
lastFireTime = 0;
}
private void OnDisable()
{
StopAllCoroutines();
}
public bool Fire(Vector3 aimTarget)
{
// 현재 상태가 발사 가능한 상태
// && 마지막 총 발사 시점에서 timeBetFire 이상의 시간이 지남
if (state == State.Ready
&& Time.time >= lastFireTime + timeBetFire)
{
var xError = Utility.GetRandomNormalDistribution(0f, currentSpread);
var yError = Utility.GetRandomNormalDistribution(0f, currentSpread);
var fireDirection = aimTarget - fireTransform.position;
fireDirection = Quaternion.AngleAxis(yError, Vector3.up) * fireDirection;
fireDirection = Quaternion.AngleAxis(xError, Vector3.right) * fireDirection;
currentSpread += 1f / stability;
// 마지막 총 발사 시점을 갱신
lastFireTime = Time.time;
// 실제 발사 처리 실행
Shot(fireTransform.position, fireDirection);
return true;
}
return false;
}
// 실제 발사 처리
private void Shot(Vector3 startPoint, Vector3 direction)
{
// 레이캐스트에 의한 충돌 정보를 저장하는 컨테이너
RaycastHit hit;
// 총알이 맞은 곳을 저장할 변수
var hitPosition = Vector3.zero;
// 레이캐스트(시작지점, 방향, 충돌 정보 컨테이너, 사정거리)
if (Physics.Raycast(startPoint, direction, out hit, fireDistance, ~excludeTarget))
{
// 레이가 어떤 물체와 충돌한 경우
// 충돌한 상대방으로부터 IDamageable 오브젝트를 가져오기 시도
var target =
hit.collider.GetComponent<IDamageable>();
// 상대방으로 부터 IDamageable 오브젝트를 가져오는데 성공했다면
if (target != null)
{
DamageMessage damageMessage;
damageMessage.damager = gunHolder.gameObject;
damageMessage.amount = damage;
damageMessage.hitPoint = hit.point;
damageMessage.hitNormal = hit.normal;
// 상대방의 OnDamage 함수를 실행시켜서 상대방에게 데미지 주기
target.ApplyDamage(damageMessage);
}
else
{
EffectManager.Instance.PlayHitEffect(hit.point, hit.normal, hit.transform);
}
// 레이가 충돌한 위치 저장
hitPosition = hit.point;
}
else
{
// 레이가 다른 물체와 충돌하지 않았다면
// 총알이 최대 사정거리까지 날아갔을때의 위치를 충돌 위치로 사용
hitPosition = startPoint + direction * fireDistance;
}
// 발사 이펙트 재생 시작
StartCoroutine(ShotEffect(hitPosition));
// 남은 탄환의 수를 -1
magAmmo--;
if (magAmmo <= 0)
// 탄창에 남은 탄약이 없다면, 총의 현재 상태를 Empty으로 갱신
state = State.Empty;
}
// 발사 이펙트와 소리를 재생하고 총알 궤적을 그린다
private IEnumerator ShotEffect(Vector3 hitPosition)
{
// 총구 화염 효과 재생
muzzleFlashEffect.Play();
// 탄피 배출 효과 재생
shellEjectEffect.Play();
// 총격 소리 재생
gunAudioPlayer.PlayOneShot(shotClip);
// 선의 시작점은 총구의 위치
bulletLineRenderer.SetPosition(0, fireTransform.position);
// 선의 끝점은 입력으로 들어온 충돌 위치
bulletLineRenderer.SetPosition(1, hitPosition);
// 라인 렌더러를 활성화하여 총알 궤적을 그린다
bulletLineRenderer.enabled = true;
// 0.03초 동안 잠시 처리를 대기
yield return new WaitForSeconds(0.03f);
// 라인 렌더러를 비활성화하여 총알 궤적을 지운다
bulletLineRenderer.enabled = false;
}
// 재장전 시도
public bool Reload()
{
if (state == State.Reloading ||
ammoRemain <= 0 || magAmmo >= magCapacity)
// 이미 재장전 중이거나, 남은 총알이 없거나
// 탄창에 총알이 이미 가득한 경우 재장전 할수 없다
return false;
// 재장전 처리 시작
StartCoroutine(ReloadRoutine());
return true;
}
// 실제 재장전 처리를 진행
private IEnumerator ReloadRoutine()
{
// 현재 상태를 재장전 중 상태로 전환
state = State.Reloading;
// 재장전 소리 재생
gunAudioPlayer.PlayOneShot(reloadClip);
// 재장전 소요 시간 만큼 처리를 쉬기
yield return new WaitForSeconds(reloadTime);
// 탄창에 채울 탄약을 계산한다
var ammoToFill = magCapacity - magAmmo;
// 탄창에 채워야할 탄약이 남은 탄약보다 많다면,
// 채워야할 탄약 수를 남은 탄약 수에 맞춰 줄인다
if (ammoRemain < ammoToFill) ammoToFill = ammoRemain;
// 탄창을 채운다
magAmmo += ammoToFill;
// 남은 탄약에서, 탄창에 채운만큼 탄약을 뺸다
ammoRemain -= ammoToFill;
// 총의 현재 상태를 발사 준비된 상태로 변경
state = State.Ready;
}
private void Update()
{
currentSpread = Mathf.SmoothDamp(currentSpread, 0f, ref currentSpreadVelocity, 1f / restoreFromRecoilSpeed);
currentSpread = Mathf.Clamp(currentSpread, 0f, maxSpread);
}
}
멤버 변수
public enum State
{
Ready, // 발사 준비됨
Empty, // 탄창이 빔
Reloading // 재장전 중
}
public State state { get; private set; } // 현재 총의 상태
private PlayerShooter gunHolder;
private LineRenderer bulletLineRenderer; // 총알 궤적을 그리기 위한 렌더러
private AudioSource gunAudioPlayer; // 총 소리 재생기
public AudioClip shotClip; // 발사 소리
public AudioClip reloadClip; // 재장전 소리
public ParticleSystem muzzleFlashEffect; // 총구 화염 효과
public ParticleSystem shellEjectEffect; // 탄피 배출 효과
public Transform fireTransform; // 총알이 발사될 위치
public Transform leftHandMount;
public float damage = 25; // 공격력
public float fireDistance = 100f; // 사정거리
public int ammoRemain = 100; // 남은 전체 탄약
public int magAmmo; // 현재 탄창에 남아있는 탄약
public int magCapacity = 30; // 탄창 용량
public float timeBetFire = 0.12f; // 총알 발사 간격
public float reloadTime = 1.8f; // 재장전 소요 시간
[Range(0f, 10f)] public float maxSpread = 3f;
[Range(1f, 10f)] public float stability = 1f;
[Range(0.01f, 3f)] public float restoreFromRecoilSpeed = 2f;
private float currentSpread;
private float currentSpreadVelocity;
private float lastFireTime; // 총을 마지막으로 발사한 시점
private LayerMask excludeTarget;
state
- 3 가지 상태
Ready
: 발사 준비 상태Empty
: 탄창이 빈 상태Reloading
: 재장전 중인 상태
- 프로퍼티를 통하여 get은 public, set은 private으로 해두었다.
- 📜Gun 내부에서만 총의 상태 값을 변경할 수 있고 외부에서만 변경 불가능 하고 값을 불러오는 것만 가능하게끔.
- 3 가지 상태
gunHolder
- 총을 쏘는 주체인 Plyaer Shooer 타입
- 총의 주인이 누구인지 알려주는 역할을 한다.
bulletLineRenderer
- LineRenderer 타입으로 총알 궤적을 그리기 위한 렌더러
- 오디오 소스
gunAudioPlayer
- AudioSource 타입으로 총 소리 재생기
shotClip
- AudioClip 타입으로 발사 소리.
reloadClip
- AudioClip 타입으로 재장전 소리
- 파티클
muzzleFlashEffect
- ParticleSystem 타입으로 총구 화염 파티클 효과
shellEjectEffect
- ParticleSystem 타입으로 탄피 배출 효과 파티클 효과
- Transform
fireTransform
- Transform 타입으로 총알이 발사될 위치
leftHandMount
- Transform 타입으로 왼손의 위치
- 총 데이터
damage
👉 총의 데미지fireDistance
👉 사정 거리. 총알 발사 체크를 할 거리.
- 탄창 데이터
ammoRemain
👉 남은 전체! 총 탄약magAmmo
👉 현재 탄창에 남아있는 탄약magCapacity
👉 탄창 용량.
- 총알 데이터
timeBetFire
👉 총알 발사 사이의 간격. 적으면 적을 수록 빠르게 연사가 가능하다.reloadTime
👉 재장전 소요 시간
- 탄 퍼짐
maxSpread
👉 0 ~ 10 범위에서 결정되며 총알이 퍼지는 최대 범위를 결정. 이 값이 크면 클 수록 총알이 넓게 흩어진다.stability
👉 1 ~ 10 범위에서 결정되며 안정성, 즉 반동이 증가하는 속도. 높을 수록 반동이 증가하는 속도가 낮아져 안정성이 높아진다. 낮으면 낮을 수록 탄 퍼짐의 속도가 높아진다.restoreFromRecoilSpeed
👉 0.01 ~ 3 범위에서 결정되며 연사를 멈춘 후 탄 퍼짐이 0 이 되기까지 걸리는 시간. 회복 속도.currentSpread
👉 현재 탄 퍼짐의 정도값currentSpreadVelocity
👉 현재 탄 퍼짐의 반경이 실시간 변화량. smoothDamp 연산에 사용할 것.
lastFireTime
- 가장 최근에 발사가 이루어진 시점을 기록
excludeTarget
- LayerMask 타입으로 총알을 쏴서는 안되는 대상을 거르기 위한 Layer
- 총알을 맞으면 안되는 대상들이 여기에 할당될 것.
- LayerMask 타입으로 총알을 쏴서는 안되는 대상을 거르기 위한 Layer
멤버 함수
private void Awake()
가장 먼저 실행되는 함수인만큼
Gun
게임 오브젝트로부터 필요한 컴포넌트들을 가져올 것
private void Awake()
{
// 사용할 컴포넌트들의 참조를 가져오기
gunAudioPlayer = GetComponent<AudioSource>();
bulletLineRenderer = GetComponent<LineRenderer>();
// 사용할 점을 두개로 변경
bulletLineRenderer.positionCount = 2;
// 라인 렌더러를 비활성화
bulletLineRenderer.enabled = false;
}
- 사용할 컴포넌트들의 참조를 가져오기
- 총알의 궤적을 그릴 선에 사용할 정점을 2 개로 한다.
- LineRenderer 의
positionCount
- 선에 위치한 정점의 수를 Set 하고 Get 할 수 있는 프로퍼티.
- 정점 1️⃣ 총구의 위치
- 정점 2️⃣ 총알이 닿은 위치
- LineRenderer 의
- 총알의 궤적 그리는 것을 비활성화 해두고 시작
public void Setup(PlayerShooter gunHolder)
총 주인인
PlayerShooter
측에서 실행할 함수라 public.Gun
게임 오브젝트의 컴포넌트들을 대상으로 총에 대해 초기화를 실행하는 메서드.
- 현재 총(
Gun
입장에서 나 자신)을 쥐고있는 shooter가 누구인지 알 수 있도록 초기화를 하는 처리가 들어감.
public void Setup(PlayerShooter gunHolder)
{
this.gunHolder = gunHolder;
excludeTarget = gunHolder.excludeTarget;
}
- 📜PlayerShooter 스크립트는 추후에 작성할 것!
- 인수로 📜PlayerShooter 를 받는다.
this.gunHolder = gunHolder;
- 인수로 입력 받은 gunHolder를 자기 자신의 gunHolder로 지정.
excludeTarget = gunHolder.excludeTarget;
- 📜PlayerShooter 스크립트에도
Player
가 총을 쏘지 않을 대상을 할당 할 LayerMask 타입의excludeTarget
멤버 변수가 따로 들어가 있음.- 이를 총의(📜Gun)
excludeTarget
에 할당.- 📜PlayerShooter 에서 쏘지 않기로 결정된 대상들을 가져와서 할당
- 이를 총의(📜Gun)
- 📜PlayerShooter 스크립트에도
private void OnEnable()
📜Gun스크립트(컴포넌트)가 활성화되는 순간마다 실행되는 이벤트 함수
private void OnEnable()
{
currentSpread = 0;
magAmmo = magCapacity; // 현재 탄창을 가득채우기
state = State.Ready; // 총의 현재 상태를 총을 쏠 준비가 된 상태로 변경
lastFireTime = 0; // 마지막으로 총을 쏜 시점을 초기화
}
- 총의 상태 초기화
- 탄창 가득 채우기
- 상태는
Ready
- 마지막으로 총을 쏜 시점을 초기화
- 탄퍼짐 0
private void OnDisable()
📜Gun스크립트(컴포넌트)가 비활성화되는 순간마다 실행되는 이벤트 함수
- 현재 실행되고 있는 코루틴 함수들이 있다면 모조리 종료. StopAllCoroutines()
private void OnDisable()
{
StopAllCoroutines();
}
public bool Fire(Vector3 aimTarget)
Gun
클래스 외부에서 총을 사용해서 발사를 시도하는 메소드.
- Shot 함수를 안전하게 감싸는 역할
- Shot 함수 는 private으로서 실제로 발사를 처리하는 함수
-
발사 성공하면 true 리턴 실패하면 false 리턴
- 발사 하려면 현재 상태가 발사 가능한 상태인지를 체크 해야 함
- 조건 1️⃣
Ready
상태여야 함 - 조건 2️⃣ 현재 시간이 마지막 총 발사 시점에서 발사 간격을 더한 시간보다 많이 지나 있어야 함
- 조건 1️⃣
- 실제로 발사를 처리하는 함수인 Shot 을 실행시키려면 1️⃣발사 위치, 2️⃣발사 방향을 넘겨야 함
- 발사 방향
fireDirection
구하기- 발사 방향 = 목표 지점 - 시작 지점
var fireDirection = aimTarget - fireTransform.position;
- 발사 방향 = 목표 지점 - 시작 지점
- 발사 방향
- 마지막으로 총을 발사한 시점을 현재 시간으로 갱신
- 실제 발사 처리 실행 👉 Shot 함수 실행
if (state == State.Ready
&& Time.time >= lastFireTime + timeBetFire)
{
var fireDirection = aimTarget - fireTransform.position;
lastFireTime = Time.time; // 마지막 총 발사 시점을 갱신
Shot(fireTransform.position, fireDirection); // 실제 발사 처리 실행
return true;
}
- 위와 같이만 구현하면
- 총알은 무조건 조준한 방향으로만 날아가게 된다.
- 총 연사와 그로 인한 반동에 의한 정확도 하락 & 탄퍼짐이 구현되지 않음
- 👉
fireDirection
에 적절한 오차 값을 더해서 탄 퍼짐을 구현해야 한다.
📜Utility.cs
using UnityEngine;
using UnityEngine.AI;
public static class Utility
{
public static Vector3 GetRandomPointOnNavMesh(Vector3 center, float distance, int areaMask)
{
var randomPos = Random.insideUnitSphere * distance + center;
NavMeshHit hit;
NavMesh.SamplePosition(randomPos, out hit, distance, areaMask);
return hit.position;
}
public static float GetRandomNormalDistribution(float mean, float standard) // 정규 분포로 부터 랜덤값을 가져오는 함수
{
var x1 = Random.Range(0f, 1f);
var x2 = Random.Range(0f, 1f);
return mean + standard * (Mathf.Sqrt(-2.0f * Mathf.Log(x1)) * Mathf.Sin(2.0f * Mathf.PI * x2));
}
}
- 표준 정규 분포도
- 평균에 가까울 수록 출현 확률이 높아지고
- 평균에서 멀어질 수록 출현 확률이 낮아진다.
- 표준 편차가 클 수록 곡선이 완만해지며 평균에서 멀어져도 출현 확률이 높아진다.
탄퍼짐 구현하기 👉 정규분포도 사용
정규 분포도로부터 원래 조준했던 위치가 출현할 확률이 가장 높고, 원래 조준했던 위치에서 먼 위치일 수록 출현 확률이 낮은 랜덤 값을 생성한다.
- 원래 조준했던 위치에 가깝게 나올 확률이 높은 랜덤값들을 생성할 수 있다.
- 📜Utility.cs 에 정규분포로부터 랜덤값을 가져오는 함수 GetRandomNormalDistribution를 구현해놨고 이 함수를 사용하면 된다.
- 인수 👉 평균
mean
, 표준편차standard
- 정규 분포 공식을 사용함
- 인수로 넘기는
mean
이 0 이면 ‘표준 정규 분포’- 0 에 가까운 값이 가장 많이 출현하게 된다.
- 인수로 넘기는
- 인수 👉 평균
Utility.GetRandomNormalDistribution(0f, currentSpread);
- 표준편차로
currentSpread
를 넣어서 탄퍼짐이 심하면 심할 수록mean
인 0 에서 먼 값이 출현할 확률도 높아지게끔 한다.
- 표준편차로
- 📜Utility.cs 에 정규분포로부터 랜덤값을 가져오는 함수 GetRandomNormalDistribution를 구현해놨고 이 함수를 사용하면 된다.
public bool Fire(Vector3 aimTarget)
{
// 현재 상태가 발사 가능한 상태
// && 마지막 총 발사 시점에서 timeBetFire 이상의 시간이 지남
if (state == State.Ready
&& Time.time >= lastFireTime + timeBetFire)
{
var xError = Utility.GetRandomNormalDistribution(0f, currentSpread);
var yError = Utility.GetRandomNormalDistribution(0f, currentSpread);
var fireDirection = aimTarget - fireTransform.position;
fireDirection = Quaternion.AngleAxis(yError, Vector3.up) * fireDirection;
fireDirection = Quaternion.AngleAxis(xError, Vector3.right) * fireDirection;
currentSpread += 1f / stability;
// 마지막 총 발사 시점을 갱신
lastFireTime = Time.time;
// 실제 발사 처리 실행
Shot(fireTransform.position, fireDirection);
return true;
}
return false;
}
정규 분포에 의한 오차 생성. 탄퍼짐이 심하면 심할 수록 0 에서 부터 먼 값의 난수가 발생할 확률이 올라간다.
xError
👉 X 방향쪽의 랜덤 오차yError
👉 Y 방향쪽의 랜덤 오차- 위에서 구한 랜덤 오차에 따라서
fireDirection
방향 회전시켜 움직여주기- 원래 총알이 향하던 방향인
fireDirection
에서 오차만큼 회전시킨다.- 쿼터니언 X 벡터 : 벡터를 해당 쿼터니언값만큼 회전한 결과가 나온다.
- y 방향 회전 👉 위쪽으로
yError
각도만큼 회전한 결과인 쿼터니언에fireDirection
을 곱한다.fireDirection = Quaternion.AngleAxis(yError, Vector3.up) * fireDirection;
- x 방향 회전 👉 오른쪽으로
xError
각도만큼 회전한 결과인 쿼터니언에fireDirection
을 곱한다.fireDirection = Quaternion.AngleAxis(xError, Vector3.right) * fireDirection;
- 원래 총알이 향하던 방향인
currentSpread += 1f / stability;
- 한번 발사하고나면 탄퍼짐이 더 심해지도록
1/안정성
만큼 증가시킨다.- 안정성이 낮을 수록 탄퍼짐 정도가 빠르게 증가할 것이고
- 안정성이 높을 수록 탄퍼짐 정도가 느리게 증가할 것이다.
- 한번 발사하고나면 탄퍼짐이 더 심해지도록
private void Shot(Vector3 startPoint, Vector3 direction)
총 발사를 실제로 처리.
- 1️⃣ 발사 시작 지점, 2️⃣ 총이 날아가는 방향을 인수로 받음
// 실제 발사 처리
private void Shot(Vector3 startPoint, Vector3 direction)
{
// 레이캐스트에 의한 충돌 정보를 저장하는 컨테이너
RaycastHit hit;
// 총알이 맞은 곳을 저장할 변수
var hitPosition = Vector3.zero;
// 레이캐스트(시작지점, 방향, 충돌 정보 컨테이너, 사정거리)
if (Physics.Raycast(startPoint, direction, out hit, fireDistance, ~excludeTarget))
{
// 레이가 어떤 물체와 충돌한 경우
// 충돌한 상대방으로부터 IDamageable 오브젝트를 가져오기 시도
var target =
hit.collider.GetComponent<IDamageable>();
// 상대방으로 부터 IDamageable 오브젝트를 가져오는데 성공했다면
if (target != null)
{
DamageMessage damageMessage;
damageMessage.damager = gunHolder.gameObject;
damageMessage.amount = damage;
damageMessage.hitPoint = hit.point;
damageMessage.hitNormal = hit.normal;
// 상대방의 OnDamage 함수를 실행시켜서 상대방에게 데미지 주기
target.ApplyDamage(damageMessage);
}
else
{
EffectManager.Instance.PlayHitEffect(hit.point, hit.normal, hit.transform);
}
// 레이가 충돌한 위치 저장
hitPosition = hit.point;
}
else
{
// 레이가 다른 물체와 충돌하지 않았다면
// 총알이 최대 사정거리까지 날아갔을때의 위치를 충돌 위치로 사용
hitPosition = startPoint + direction * fireDistance;
}
// 발사 이펙트 재생 시작
StartCoroutine(ShotEffect(hitPosition));
// 남은 탄환의 수를 -1
magAmmo--;
if (magAmmo <= 0)
// 탄창에 남은 탄약이 없다면, 총의 현재 상태를 Empty으로 갱신
state = State.Empty;
}
- RaycastHit
hit
- 레이캐스트에 의한 충돌 정보를 저장하는 컨테이너
- 레이캐스트로 총알이 물체에 맞았는지를 체크할 것
hitPosition
- 총알이 맞은 위치 벡터
- if (Physics.Raycast(startPoint, direction, out hit, fireDistance, ~excludeTarget))
- 인수 👉 시작점, 방향, 충돌 정보를 저장할
hit
, 레이 캐스트 사정거리,~
을 붙여 특정 대상의 레이어 마스크는 제외- 감지할 레이어에
~
을 붙이면 이 레이어가 붙은 것들은 레이 캐스트 처리에서 제외하라는 뜻
- 감지할 레이어에
- 1️⃣ 레이 캐스트 성공했다면 `hit`에 그 충돌 정보들을 저장한 후 true 리턴
- 총의 데미지를 받을 수 있는 오브젝트 인지를 검사해야 함
- var
target
= hit.collider.GetComponent<IDamageable>();IDamageable
타입의 오브젝트이라면 데미지를 받을 수 있는 타입이라는뜻.if (target != null)
즉 데미지를 받을 수 있는 타입이라면- 데미지 메세지 생성
- DamageMessage
damageMessage
- 클래스가 아닌 구조체라서 new 사용할 필요 없음
- DamageMessage
- 데미지를 주는 사람은
gunHolder.gameObject
- 데미지 양은
damage
- 타격이 가해진 위치는
hit.point
- 타격이 가해진 방향은
hit.normal
target
에 데미지 주기target.ApplyDamage(damageMessage);
- 이 ApplyDamage 함수에서 파티클 효과를 재생할 것이다
- 데미지 메세지 생성
- 데미지를 받을 수 없는 타입이라면
- 직접 여기서 파티클 프리팹을 생성 하고 재생하는 📜EffectManager.cs 스크립트(싱글톤이라 클래스 이름으로 바로 접근 가능)의 함수 실행
- EffectManager.Instance.PlayHitEffect(hit.point, hit.normal, hit.transform);
- 📜EffectManager.cs 포스트 참고
- 직접 여기서 파티클 프리팹을 생성 하고 재생하는 📜EffectManager.cs 스크립트(싱글톤이라 클래스 이름으로 바로 접근 가능)의 함수 실행
- var
- 레이가 충돌한 위치 저장
hitPosition
= hit.point;
- 총의 데미지를 받을 수 있는 오브젝트 인지를 검사해야 함
- 2️⃣
else
👉 레이 캐스트 충돌이 일어나지 않았다면- 총알이 최대 사정거리까지 날아갔을때의 위치를 충돌 위치로 사용.
- 즉 뭐 별다른 충돌이 일어나지 않았다면 총알은 발사 지점으로부터 날아간 방향으로의 최대 사정 거리 위치에서 파티클 효과를 재생할 것이다.
hitPosition
= startPoint + direction * fireDistance
- 총알이 최대 사정거리까지 날아갔을때의 위치를 충돌 위치로 사용.
- 인수 👉 시작점, 방향, 충돌 정보를 저장할
- 발사 이펙트 재생 시작
hitPosition
을 StartCoroutine 함수의 인수로 넘겨 실행- 파티클 효과 + 총격 소리 재생 + 총알 궤적 그리기
- 발사했으니 탄환 수 업뎃
- 탄환 개수 1 감소
- 탄환 개수가 0 에 도달했다면 총의 현재 상태를
Empty
으로 갱신
private IEnumerator ShotEffect(Vector3 hitPosition)
총알이 맞은 지점을 인수로 받아 총알과 관련된 Effect 재생하는 코루틴 함수(지연시간을 들여서 이루어짐)
파티클 효과 + 총격 소리 재생 + 총알 궤적 그리기
private IEnumerator ShotEffect(Vector3 hitPosition)
{
// 총구 화염 효과 재생
muzzleFlashEffect.Play();
// 탄피 배출 효과 재생
shellEjectEffect.Play();
// 총격 소리 재생
gunAudioPlayer.PlayOneShot(shotClip);
// 선의 시작점은 총구의 위치
bulletLineRenderer.SetPosition(0, fireTransform.position);
// 선의 끝점은 입력으로 들어온 충돌 위치
bulletLineRenderer.SetPosition(1, hitPosition);
// 라인 렌더러를 활성화하여 총알 궤적을 그린다
bulletLineRenderer.enabled = true;
// 0.03초 동안 잠시 처리를 대기
yield return new WaitForSeconds(0.03f);
// 라인 렌더러를 비활성화하여 총알 궤적을 지운다
bulletLineRenderer.enabled = false;
}
- 발사시 두 파티클 효과를 재생시킨다.
- 총격 소리를 1회 재생한다.
- 발사 소리인
shotClip
클립 할당- AudioSource의 PLayOneShot 함수
- 그냥 Play()함수와의 차이점
- 미리
clip
이 오디오 소스에 할당이 되어 있어 이를 재생만 하면 되는 Plqy()와는 달리 PlayOneShot은 재생할 클립을 직접 인수로 넣어주어야 한다. - Plqy()는 소리 중첩이 되지 않기 때문에 직전 소리를 중지한 후 재생한다. 반면에 PlayOneShot은 소리 중첩이 된다. 발사 소리같이 연달아 재생되는 소리 같은 경우에는 Play() 함수는 부적합.
- 미리
- 그냥 Play()함수와의 차이점
- AudioSource의 PLayOneShot 함수
- 발사 소리인
- 선의 정점은 현재 2 개 (위에서
positionCount
로 설정 했었음- 인덱스 0, 1 인 정점이 2 개
- 첫번째 정점, 즉 선의 시작점이 되는 총구의 위치인
fireTransform.position
로 설정 - 두번째 정점, 즉 인수로 받은 총알의 충돌 위치인
hitPosition
로 설정
- 첫번째 정점, 즉 선의 시작점이 되는 총구의 위치인
- 인덱스 0, 1 인 정점이 2 개
- 짧은 시간동안 탄알 궤도 그림 👉 총알궤도가 번쩍이는 효과
- 잠시동안 총알 궤적을 그린 채로 다시 대기하고 다시 총알 궤적을 지운다.
- 1️⃣총알 궤적 활성화 해서 그리고 2️⃣0.03초 대기 하고 3️⃣비활성화
- 대기를 안해주면 활성화되자마자 바로 비활성화 되기 때문에 총알이 그려졌는지도 알 수가 없다. 그려지기도 전에 비활성화 되서..
- 1️⃣총알 궤적 활성화 해서 그리고 2️⃣0.03초 대기 하고 3️⃣비활성화
- 잠시동안 총알 궤적을 그린 채로 다시 대기하고 다시 총알 궤적을 지운다.
public bool Reload()
Gun
클래스 외부에서 재장전을 시도하는 public 메소드
- 재장전 성공시 true, 실패시 false 리턴
- 실제 재장전 처리는 private한 코루틴 함수인 ReloadRoutine() 에서 할 것.
public bool Reload()
{
if (state == State.Reloading ||
ammoRemain <= 0 || magAmmo >= magCapacity)
// 이미 재장전 중이거나, 남은 총알이 없거나
// 탄창에 총알이 이미 가득한 경우 재장전 할수 없다
return false;
// 재장전 처리 시작
StartCoroutine(ReloadRoutine());
return true;
}
- 재장전이 가능한 경우를 검사해야 함
- 재장전을 할 수 없는 경우
- 이미 재장전 중이거나
- 남은 총알이 없거나
- 탄창에 총알이 이미 가득한 경우
- 재장전을 할 수 있는 상태일 때만 실제 재장전 처리를 하는 ReloadRoutine() 함수 실행
- 재장전을 할 수 없는 경우
private IEnumerator ReloadRoutine()
실제 재장전 처리.
private IEnumerator ReloadRoutine()
{
// 현재 상태를 재장전 중 상태로 전환
state = State.Reloading;
// 재장전 소리 재생
gunAudioPlayer.PlayOneShot(reloadClip);
// 재장전 소요 시간 만큼 처리를 쉬기
yield return new WaitForSeconds(reloadTime);
// 탄창에 채울 탄약을 계산한다
var ammoToFill = magCapacity - magAmmo;
// 탄창에 채워야할 탄약이 남은 탄약보다 많다면,
// 채워야할 탄약 수를 남은 탄약 수에 맞춰 줄인다
if (ammoRemain < ammoToFill) ammoToFill = ammoRemain;
// 탄창을 채운다
magAmmo += ammoToFill;
// 남은 탄약에서, 탄창에 채운만큼 탄약을 뺸다
ammoRemain -= ammoToFill;
// 총의 현재 상태를 발사 준비된 상태로 변경
state = State.Ready;
}
- 현재 상태를 재장전 중 상태로 전환
Reloading
- 재장전 도중에 발사하거나 재장전 도중에 또 재장전 하는 것을 막기 위하여 상태 변경
- 재장전 소리 재생
- 재장전 소요 시간 만큼 대기 시간 가지기
- 탄창에 채울 탄약을 계산한다
- 탄창에 채워야할 탄약이 남은 탄약보다 많다면, 채워야할 탄약 수를 남은 탄약 수에 맞춰 줄인다
- 탄창을 채운다.
- 남은 탄약에서, 탄창에 채운만큼 탄약을 뺸다
- 총의 현재 상태를 발사 준비된 상태로 변경
Ready
private void Update()
현재 탄 퍼짐(반동) 값을 상태에 따라 매 프레임마다 갱신
private void Update()
{
currentSpread = Mathf.SmoothDamp(currentSpread, 0f, ref currentSpreadVelocity, 1f / restoreFromRecoilSpeed);
currentSpread = Mathf.Clamp(currentSpread, 0f, maxSpread);
}
currentSpread
증가는 Fire 함수에서 이루어 진다.- 가만히 있는 상태라면 `currentSpread`를 0 으로 서서히 스무스하게 초기화 해야 한다.
- Mathf.Clamp 를 통하여
currentSpread
가maxSpread
를 넘기지 못하도록 한다.- 아무리 반동이 누적이 되더라도 매 프레임마다
currentSpread
가maxSpread
를 넘기지 못하도록 막아 준다.
- 아무리 반동이 누적이 되더라도 매 프레임마다
- Mathf.SmoothDamp 를 통하여
currentSpread
가 ✨증가하는 것과 별개로 매 프레임마다 스무스 하게 0 에 가까워 지게 만들어야 한다.- 지연 시간으로는
1 / restoreFromRecoilSpeed
restoreFromRecoilSpeed
가 높으면 높을 수록 지연 시간이 줄어들어서 빠르게 탄퍼짐이 0 에 도달 함.
- 지연 시간으로는
- Mathf.Clamp 를 통하여
Gun 컴포넌트 설정
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글 남기기