Chapter 2-5. 무기 : 크로스헤어
카테고리: Unity Lesson 3
태그: Unity Game Engine
인프런에 있는 케이디님의 [유니티 3D] 실전! 생존게임 만들기 - Advanced 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click
Chapter 2. 무기
크로스헤어
🚖 크로스헤어 UI 만들기
CrossHair
빈 게임 오브젝트. 크로스 헤어를 나타내는 아래 5 가지 UI 요소들을 부모로서 묶는다.- 크로스 헤어를 나타내는 5 가지의 이미지 UI 요소. 위치를 크로스헤어에 맞게 각각 설정해주었다.
Dot
크로스 헤어 가운데 점. 크기 3 X 3.Line Right
크로스 헤어 오른쪽 선. 크기 20 X 3.Line Left
크로스 헤어 왼쪽 선. 크기 20 X 3.Line Up
크로스 헤어 위쪽 선. 크기 3 X 20.Line Down
크로스 헤어 아래쪽 선. 크기 3 X 20.
- 크로스 헤어를 나타내는 5 가지의 이미지 UI 요소. 위치를 크로스헤어에 맞게 각각 설정해주었다.
🚖 크로스헤어 애니메이션
- 동작에 따라 크로스헤어가 벌어지거나 투명해지는 등등 변화를 줄 것이라 애니메이션이 필요하다.
- 발사할 때 크로스 헤어가 벌어짐.
- 앉으면 크로스 헤어가 좁아짐. (정확도 Up)
- 걸으면 크로스 헤어가 넓어짐. (정확도 Down)
- 뛰면 크로스 헤어가 사라짐. (어차피 뛸 땐 발사 못 하니까)
- 정조준 할 땐 크로스 헤어 사라짐.
- 크로스 헤어 벌어지는 정도
- 발사 중이 아닐 때 < 발사 중일 때
- 발사 중일 때 👉 앉을 때 < 정지했을 때 < 걸을 때
- 발사 중이 아닐 때 👉 앉을 때 < 정지했을 때 < 걸을 때
- 발사 중이 아닐 때 < 발사 중일 때
- 뛸 때, 정조준 할 때는 크로스헤어를 사라지게 한다.
CrossHair
에 Animator 컴포넌트 추가
Animation : 애니메이션 클립 직접 만들기
크로스 헤어와 관련된 애니메이션을 만드려는거니까 ✨CrossHair
를 선택한 상태에서✨ Window - Animation - Animation 애니메이션 탭을 클릭하면 위와 같이 CrossHair
의 애니메이션을 클립을 만들 수 있는 버튼이 뜬다!
녹화 하는 방법
- Preview 옆에 있는 빨간색 녹화 버튼 🔴 을 누른 상태로 이 애니메이션이 적용되 있는 오브젝트의 여러 컴포넌트, 혹은 자식 오브젝트들의 설정 값에 변화를 주게 되면 그 변화 값이 애니메이션 클립으로서 녹화가 된다.
- 녹화만 할 뿐 현재의 실제 오브젝트의 변경 사항으로 적용 되지는 않는다. (추후에 지금 생성한 이 애니메이션 클립이 재생될 때 적용 되겠지!)
- 위 사진은 딱 0 프레임에만 Key 프레임이 있으므로 딱 1 프레임 짜리의 애니메이션 클립을 만드든 것과 같다. 딱 0 프레임 그 순간을 녹화는 것과 같다.
녹화 버튼을 누른 후 설정 값을 변경하면 빨간색으로 변한다. 예를 들어 위와 같이 Pos Y 값을 -20 으로 입력 한 상태라면, 작업 중인 이 애니메이션 클립을 재생하면 해당 프레임 때 Pos Y 값이 -20 으로 변화 할거라는 뜻이다.
녹화 버튼을 해제하면 애니메이션 클립으로서 동작할 때 변경될 값이 파란색으로 표시된다.
생성할 클립 추가하는 방법
Create New Clip 으로 또 다른 새로운 클립을 생성할 수 있다.
1️⃣ CrossHair_Idle 클립
정지 상태일 때 크로스 헤어.
- 크로스 헤어를 이루는 5 가지의 이미지들의 원래 설정했던값 그대로를 녹화했다.
- 1 프레임 길이의, 딱 0 프레임 때의 그 순간만 녹화함.
2️⃣ CrossHair_Idle_Fire 클립
정지 상태에서 발사했을 때 크로스 헤어.
Line Left
,Line Right
의 경우 Pos X 값이 정지 상태에 비해 -35, 35 로 늘었다.Line Down
,Line Up
의 경우 Pos Y 값이 정지 상태에 비해 -35, 35 로 늘었다.- 1 프레임 길이의, 딱 0 프레임 때의 그 순간만 녹화함.
3️⃣ CrossHair_Crouch 클립
앉은 상태일 때 크로스 헤어.
Line Left
,Line Right
의 경우 Pos X 값이 정지 상태에 비해 -15, 15 로 줄었다.Line Down
,Line Up
의 경우 Pos Y 값이 정지 상태에 비해 -15, 15 로 줄었다.- 1 프레임 길이의, 딱 0 프레임 때의 그 순간만 녹화함.
4️⃣ CrossHair_Crouch_Fire 클립
앉은 상태에서 발사했을 때 크로스 헤어.
Line Left
,Line Right
의 경우 Pos X 값이 발사 + 정지 상태에 비해 -30, 30 로 줄었다.Line Down
,Line Up
의 경우 Pos Y 값이 발사 + 정지 상태에 비해 -30, 30 로 줄었다.- 1 프레임 길이의, 딱 0 프레임 때의 그 순간만 녹화함.
5️⃣ CrossHair_Walk 클립
걷는 상태일 때 크로스 헤어.
Line Left
,Line Right
의 경우 Pos X 값이 정지 상태에 비해 -25, 25 로 늘었다.Line Down
,Line Up
의 경우 Pos Y 값이 정지 상태에 비해 -25, 25 로 늘었다.- 1 프레임 길이의, 딱 0 프레임 때의 그 순간만 녹화함.
6️⃣ CrossHair_Walk_Fire 클립
걷는 상태에서 발사 할 때 크로스 헤어.
Line Left
,Line Right
의 경우 Pos X 값이 발사 + 정지 상태에 비해 -45, 45 로 늘었다.Line Down
,Line Up
의 경우 Pos Y 값이 발사 + 정지 상태에 비해 -45, 45 로 늘었다.- 1 프레임 길이의, 딱 0 프레임 때의 그 순간만 녹화함.
7️⃣ CrossHair_Run 클립
뛰는 상태일 때 크로스 헤어.
- 색상을 투명하게 바꿔주었다. 뛸 땐 가운데 점을 제외한 4 개의 선이 보이지 않도록 4 개의 이미지 UI의 색상이 투명해지도록 녹화했다.
- 투명해져서 안보이게 되긴 하겠지만 일단
Line Left
,Line Right
의 경우 Pos X 값을 100까지 늘려주었다. 뛰면 크로스헤어가 늘려졌다가 투명해지며 사라지도록! - 투명해져서 안보이게 되긴 하겠지만 일단
Line Down
,Line Up
의 경우 Pos Y 값을 100까지 늘려주었다. 뛰면 크로스헤어가 늘려졌다가 투명해지며 사라지도록! - 1 프레임 길이의, 딱 0 프레임 때의 그 순간만 녹화함.
7️⃣ CrossHair_FineSight 클립
정조준 상태일 때 크로스 헤어.
- 색상을 투명하게 바꿔주었다. 정조준할 땐 가운데 점을 제외한 4 개의 선이 보이지 않도록 4 개의 이미지 UI의 색상이 투명해지도록 녹화했다.
- 투명해져서 안보이게 되긴 하겠지만 일단
Line Left
,Line Right
의 경우 Pos X 값을 10까지 좁혀주었다. 정조준 하면 크로스헤어가 좁아졌다가 투명해지며 사라지도록! - 투명해져서 안보이게 되긴 하겠지만 일단
Line Down
,Line Up
의 경우 Pos Y 값을 10까지 좁혀주었다. 정조준 하면 크로스헤어가 좁아졌다가 투명해지며 사라지도록! - 1 프레임 길이의, 딱 0 프레임 때의 그 순간만 녹화함.
애니메이션 컨트롤러
“CrossHairController” 이름의 애니메이션 컨트롤러를 CrossHair
에 Animator 컴포넌트의 컨트롤러로서 할당해준다.
파라미터는 위와 같이 추가해주었다.
Idle
👉Walk
,Run
,Crouch
,FineSight
/Crouch
,Walk
👉FineSight
/Walk
,Crouch
👉Run
/Walk
👉Crouch
- 조건
- 각각의 Bool 파라미터 True
- Has Exit Time 해제 (파라미터가 True 되면 바로 실행 되야 함)
- Translation Duration 0.2
- 조건
Walk
,Run
,Crouch
,FineSight
👉Idle
- 조건
- 각각의 Bool 파라미터 False
- Has Exit Time 해제 (파라미터가 False 되면 바로 실행 되야 함)
- Translation Duration 0.2
- 조건
Idle
,Walk
,Crouch
👉Idle_Fire
,Walk_Fire
,Crouch_Fire
- 조건
- 각각의 Trigger 발동
- Has Exit Time 해제 (Trigger가 발동되면 바로 실행 되야 함)
- Translation Duration 0.2
- 조건
Idle_Fire
,Walk_Fire
,Crouch_Fire
👉Idle
,Walk
,Crouch
- Has Exit Time 체크
- 별다른 조건이 없이 트리거로 인해 한번 발동했으면, 그냥 해당 애니메이션 끝까지 재생하고 전이
- Exit TIme 은 0.01 로 설정
- 1 % 만 재생하고 크로스헤어가 한 순간에 원래대로 돌아오게끔 !!
- Translation Duration 0.1
- Has Exit Time 체크
애니메이션 재생
📜Crosshair.cs
Canvas
에 붙인다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Crosshair : MonoBehaviour
{
[SerializeField]
private Animator animator;
private float gunAccuracy; // 크로스 헤어의 상태에 따른 총의 정확도. 작으면 작을 수록 좋다. 총알이 뻗치는 정도!
[SerializeField]
private GameObject go_CrosshairHUD; // 크로스 헤어 활성화/비활성화를 위한 부모 객체
public void WalkingAnimation(bool _flag)
{
animator.SetBool("Walking", _flag);
}
public void RunningAnimation(bool _flag)
{
animator.SetBool("Running", _flag);
}
public void CrouchingAnimation(bool _flag)
{
animator.SetBool("Crouching", _flag);
}
public void FineSightAnimation(bool _flag)
{
animator.SetBool("FineSight", _flag);
}
public void FireAnimaion()
{
if(animator.GetBool("Walking"))
{
animator.SetTrigger("Walk_Fire");
}
else if(animator.GetBool("Crouching"))
{
animator.SetTrigger("Crouch_Fire");
}
else
{
animator.SetTrigger("Idle_Fire");
}
}
}
gunAccuracy
- 크로스헤어에 따른 정확도. 총이 튀는 정도.
- 📜Gun.cs 의
accruracy
총마다 가지고 있는 총의 정확도에서 이 값을 뺀 값에서 부터 총의 정확도에서 이 값을 더해준 값 범위의 사이에서 랜덤한 방향으로 Raycast를 쏠 것이다.- 📜GunController.cs 의 Hit() 에서 !
go_CrosshairHUD
- 크로스 헤어 활성화/비활성화를 위한 부모 객체
- 나중에 다른 총 종류 많아지고 총에서 손으로 바꾸는 등등 무기 바꿀 때 필요함! 이 크로스 헤어를 내리거나 다시 올려야 하므로.
- WalkingAnimation
- 인수에 따라 “Walking” 애니메이션 파라미터 값을 바꿔주는 함수
- 걸을 때 크로스헤어 애니메이션을 실행하거나 해제한다.
- RunningAnimation
- 인수에 따라 “Running” 애니메이션 파라미터 값을 바꿔주는 함수
- 달릴 때 크로스헤어 애니메이션을 실행하거나 해제한다.
- CrouchingAnimation
- 인수에 따라 “Crouching” 애니메이션 파라미터 값을 바꿔주는 함수
- 앉았을 때 크로스헤어 애니메이션을 실행하거나 해제한다.
- FineSightAnimation
- 인수에 따라 “FineSight” 애니메이션 파라미터 값을 바꿔주는 함수
- 정조준 크로스헤어 애니메이션을 실행하거나 해제한다.
- FireAnimaion()
- 발사를 해야할 때 걷는지 정조준중인지 등등의 상태에 따른 크로스헤어 애니메이션을 재생하도록 한다.
- 발사를 처리하는 📜GunController.cs 의 Shoot() 에서 실행할 것이다.
- 발사하려는데 현재 “Walking” 파라미터 값이 True 라면 “Walk_Fire” 파라미터를 발동 시킨다.
- 걷기 + 발사시 크로스헤어 애니메이션 재생
- 발사하려는데 현재 “Crouching” 파라미터 값이 True 라면 “Crouch_Fire” 파라미터를 발동 시킨다.
- 앉기 + 발사시 크로스헤어 애니메이션 재생
- 발사하려는데 둘 다 아니라면 “Idle” 상태인 것이다. “Idle” 파라미터를 발동 시킨다.
- 정지 + 발사시 크로스헤어 애니메이션 재생
- 발사를 해야할 때 걷는지 정조준중인지 등등의 상태에 따른 크로스헤어 애니메이션을 재생하도록 한다.
📜PlayerController.cs
// 추가된 멤버 변수
private bool isWalk = false;
private Vector3 lastPos; // 전 프레임의 위치. 현재 프레임의 위치와 비교했을 때 달라지면 움직임이 있었던 것이다.
private Crosshair theCrosshair;
void Start()
{
// 컴포넌트 할당
capsuleCollider = GetComponent<CapsuleCollider>();
myRigid = GetComponent<Rigidbody>();
theGunController = FindObjectOfType<GunController>();
theCrosshair = FindObjectOfType<CrossHair>();
applySpeed = walkSpeed;
originPosY = theCamera.transform.localPosition.y;
applyCrouchPosY = originPosY;
}
void Update()
{
IsGround();
TryJump();
TryRun();
TryCrouch();
Move();
MoveCheck();
CameraRotation();
CharacterRotation();
}
private void MoveCheck()
{
if (!isRun && !isCrouch && isGround)
{
if (Vector3.Distance(lastPos, transform.position) >= 0.01f)
isWalk = true;
else
isWalk = false;
theCrosshair.WalkingAnimation(isWalk);
lastPos = transform.position;
}
}
// 달리기
private void Running()
{
if (isCrouch)
Crouch();
theGunController.CancelFineSight();
isRun = true;
theCrosshair.RunningAnimation(isRun);
applySpeed = runSpeed;
}
// 달리기 취소
private void RunningCancel()
{
isRun = false;
theCrosshair.RunningAnimation(isRun);
applySpeed = walkSpeed;
}
// 앉기 동작
private void Crouch()
{
isCrouch = !isCrouch;
theCrosshair.CrouchingAnimation(isCrouch);
if (isCrouch)
{
applySpeed = crouchSpeed;
applyCrouchPosY = crouchPosY;
}
else
{
applySpeed = walkSpeed;
applyCrouchPosY = originPosY;
}
StartCoroutine(CrouchCoroutine());
}
// 지면 체크
private void IsGround()
{
isGround = Physics.Raycast(transform.position, Vector3.down, capsuleCollider.bounds.extents.y + 0.1f);
theCrosshair.RunningAnimation(!isGround);
}
- MoveCheck()
- 크로스헤어 걷기 애니메이션 재생 하려면 지금 플레이어가 걷는 중인지 상태를 알아야 한다.
- 매 프레임마다 실행될 수 있도록 Update() 내에 추가
- 플레이어가 걷는 중인지를 검사하고
isWalk
를 업데이트 한다.- 단, 걷는 중, 뛰는 중, 앉는 중이라면
isWalk
를 업데이트 할 필요가 없다. - 플레이어가 걷는 중인지를 검사하는 방법
- 이전 프레임에서 업데이트 되었던
lastPos
는 이전 프레임에서의 즉 과거의 현재 위치 - 현재 프레임에서의 현재 위치와
lastPos
가 같은지 비교해 보면 된다.- 같다면 움직임이 없었던 것이므로
isWalk
는 False다.- 완전히 같다고 보기 보단 0.01 정도의 오차 이내면 같다고 보았다.
- Vector3.Distance 는 두 Vector3의 차이, 즉 거리를 리턴한다.
- 다르다면 움직임이 있었던 것이므로
isWalk
는 True다.
- 같다면 움직임이 없었던 것이므로
- 이전 프레임에서 업데이트 되었던
- 단, 걷는 중, 뛰는 중, 앉는 중이라면
isWalk
업데이트가 완료되면 이 값에 따른 크로스헤어 걷기 애니메이션을 재생한다.- 이 값에 따라 “Walking” 파라미터 값을 설정한다.
- 다음 프레임때의 검사를 위해
lastPos
도 현재 위치로 업데이트 해준다.
- 크로스헤어 걷기 애니메이션 재생 하려면 지금 플레이어가 걷는 중인지 상태를 알아야 한다.
- Running()
isRun
값에 따라 “Running” 파라미터 값을 설정한다. 그에 따른 크로스헤어 달리기 애니메이션 재생.isRun
은 True
- RunningCancel()
isRun
값에 따라 “Running” 파라미터 값을 설정한다. 그에 따른 크로스헤어 달리기 애니메이션 재생.isRun
은 False
- Crouch()
isCrouch
값에 따라 “Crouching” 파라미터 값을 설정한다. 그에 따른 크로스헤어 앉기 애니메이션 재생.
- IsGround()
isGround
가 False면, 즉 점프 중이면 크로스헤어 앉기 애니메이션 재생시킨다.- 점프 중일때도 크로스 헤어가 벌어지면서 투명해지도록 하기 위해서
📜GunController.cs
// 추가된 멤버 변수
private Crosshair theCrosshair;
void Start()
{
originPos = Vector3.zero;
audioSource = GetComponent<AudioSource>();
theCrosshair = FindObjectOfType<Crosshair>();
}
private void Shoot() // 실제 발사 되는 과정
{
theCrosshair.FireAnimaion();
// 발사 처리
currentGun.currentBulletCount--;
currentFireRate = currentGun.fireRate; // 연사 속도 재계산
PlaySE(currentGun.fire_Sound);
currentGun.muzzleFlash.Play();
// 피격 처리
Hit();
// 총기 반동 코루틴 실행
StopAllCoroutines();
StartCoroutine(RetroActionCoroutine());
}
private void FineSight()
{
isFineSightMode = !isFineSightMode;
currentGun.anim.SetBool("FineSightMode", isFineSightMode);
theCrosshair.FineSightAnimation(isFineSightMode);
if (isFineSightMode)
{
StopAllCoroutines();
StartCoroutine(FineSightActivateCoroutine());
}
else
{
StopAllCoroutines();
StartCoroutine(FineSightDeActivateCoroutine());
}
}
- Shoot()
- 발사를 했으니 현재 상태에 맞춰 크로스헤어 애니메이션을 재생시켜주는 📜Crosshair.cs의 FireAnimaion() 함수를 실행시킨다.
- FineSight()
isFineSightModeh
값에 따라 “FineSight” 파라미터 값을 설정한다. 그에 따른 크로스헤어 정조준 애니메이션 재생.
🚖 크로스헤어에 따른 정확도 수정
📜Crosshair.cs
Canvas
에 붙인다.
// 추가된 멤버 변수
[SerializeField]
private GunController theGunController;
public float GetAccuracy()
{
if (animator.GetBool("Walking"))
{
gunAccuracy = 0.06f;
}
else if (animator.GetBool("Crouching"))
{
gunAccuracy = 0.015f;
}
else if (theGunController.GetFineSightMode())
{
gunAccuracy = 0.001f;
}
else
{
gunAccuracy = 0.035f;
}
return gunAccuracy;
}
- GetAccuracy()
- 각기 상태에 따라총의 튀는 정도인
gunAccuracy
값을 설정한 후 이를 리턴한다.
- 각기 상태에 따라총의 튀는 정도인
📜GunController.cs
public bool GetFineSightMode()
{
return isFineSightMode;
}
private void Hit()
{
// 카메라 월드 좌표!! (localPosition이 아님)
if (Physics.Raycast(theCam.transform.position,
theCam.transform.forward +
new Vector3(Random.Range(-theCrosshair.GetAccuracy() - currentGun.accuracy, -theCrosshair.GetAccuracy() + currentGun.accuracy),
Random.Range(-theCrosshair.GetAccuracy() - currentGun.accuracy, -theCrosshair.GetAccuracy() + currentGun.accuracy),
0),
out hitInfo,
currentGun.range))
{
GameObject clone = Instantiate(hitEffectPrefab, hitInfo.point, Quaternion.LookRotation(hitInfo.normal));
Destroy(clone, 2f);
}
}
- Hit()
- 발사시 카메라의 위치에서 Raycast를 쏴서 충돌되는게 있다면 그 위치가 바로 피격 위치다.
- Raycast를 날리는 방향
- 카메라의 앞 방향(theCam.transform.forward)으로 부터 정확도에 따라 랜덤한 오차를 주어 랜덤한 방향으로 총이 튀게끔 Raycast를 쏜다.
- 총은 좌우(X축), 수직위아래(Y축)으로 튀므로 X, Y 값만 더해준다.
- -theCrosshair.GetAccuracy() - currentGun.accuracy 부터
- -theCrosshair.GetAccuracy() + currentGun.accuracy 범위까지의
- 랜덤한 값
총의 정확도 Accuracy
를 0.02로 주었다.
🚔 여기까지 스크립트 정리
📜Crosshair.cs
Canvas
에 붙인다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Crosshair : MonoBehaviour
{
[SerializeField]
private Animator animator;
private float gunAccuracy; // 크로스 헤어의 상태에 따른 총의 정확도. 작으면 작을 수록 좋다. 총알이 뻗치는 정도!
[SerializeField]
private GameObject go_CrosshairHUD; // 크로스 헤어 활성화/비활성화를 위한 부모 객체
[SerializeField]
private GunController theGunController;
public void WalkingAnimation(bool _flag)
{
animator.SetBool("Walking", _flag);
}
public void RunningAnimation(bool _flag)
{
animator.SetBool("Running", _flag);
}
public void CrouchingAnimation(bool _flag)
{
animator.SetBool("Crouching", _flag);
}
public void FineSightAnimation(bool _flag)
{
animator.SetBool("FineSight", _flag);
}
public void FireAnimaion()
{
if(animator.GetBool("Walking"))
{
animator.SetTrigger("Walk_Fire");
}
else if(animator.GetBool("Crouching"))
{
animator.SetTrigger("Crouch_Fire");
}
else
{
animator.SetTrigger("Idle_Fire");
}
}
public float GetAccuracy()
{
if (animator.GetBool("Walking"))
{
gunAccuracy = 0.06f;
}
else if (animator.GetBool("Crouching"))
{
gunAccuracy = 0.015f;
}
else if (theGunController.GetFineSightMode())
{
gunAccuracy = 0.001f;
}
else
{
gunAccuracy = 0.035f;
}
return gunAccuracy;
}
}
📜PlayerController.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 isWalk = false;
private bool isRun = false;
private bool isGround = true;
private bool isCrouch = false;
// 움직임 체크 변수
private Vector3 lastPos; // 전 프레임의 위치. 현재 프레임의 위치와 비교했을 때 달라지면 움직임이 있었던 것이다.
// 앉았을 때 얼마나 앉을지 결정하는 변수
[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;
private Crosshair theCrosshair;
void Start()
{
// 컴포넌트 할당
capsuleCollider = GetComponent<CapsuleCollider>();
myRigid = GetComponent<Rigidbody>();
theGunController = FindObjectOfType<GunController>();
theCrosshair = FindObjectOfType<Crosshair>();
applySpeed = walkSpeed;
originPosY = theCamera.transform.localPosition.y;
applyCrouchPosY = originPosY;
}
void Update()
{
IsGround();
TryJump();
TryRun();
TryCrouch();
Move();
MoveCheck();
CameraRotation();
CharacterRotation();
}
// 지면 체크
private void IsGround()
{
isGround = Physics.Raycast(transform.position, Vector3.down, capsuleCollider.bounds.extents.y + 0.1f);
theCrosshair.RunningAnimation(!isGround);
}
// 점프 시도
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;
theCrosshair.RunningAnimation(isRun);
applySpeed = runSpeed;
}
// 달리기 취소
private void RunningCancel()
{
isRun = false;
theCrosshair.RunningAnimation(isRun);
applySpeed = walkSpeed;
}
// 앉기 시도
private void TryCrouch()
{
if (Input.GetKeyDown(KeyCode.LeftControl))
{
Crouch();
}
}
// 앉기 동작
private void Crouch()
{
isCrouch = !isCrouch;
theCrosshair.CrouchingAnimation(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 MoveCheck()
{
if (!isRun && !isCrouch && isGround)
{
if (Vector3.Distance(lastPos, transform.position) >= 0.01f)
isWalk = true;
else
isWalk = false;
theCrosshair.WalkingAnimation(isWalk);
lastPos = transform.position;
}
}
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; // 재장전 중인지.
[HideInInspector]
public bool isFineSightMode = false; // 정조준 중인지.
[SerializeField]
private Vector3 originPos; // 원래 총의 위치(정조준 해제하면 나중에 돌아와야 하니까)
private AudioSource audioSource; // 발사 소리 재생기
private RaycastHit hitInfo; // 총알의 충돌 정보
// 필요한 컴포넌트
[SerializeField]
private Camera theCam; // 카메라 시점에서 정 중앙에 발사할 거라서
private Crosshair theCrosshair;
[SerializeField]
private GameObject hitEffectPrefab; // 피격 이펙트
void Start()
{
originPos = Vector3.zero;
audioSource = GetComponent<AudioSource>();
theCrosshair = FindObjectOfType<Crosshair>();
}
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() // 실제 발사 되는 과정
{
theCrosshair.FireAnimaion();
// 발사 처리
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 +
new Vector3(Random.Range(-theCrosshair.GetAccuracy() - currentGun.accuracy, -theCrosshair.GetAccuracy() + currentGun.accuracy),
Random.Range(-theCrosshair.GetAccuracy() - currentGun.accuracy, -theCrosshair.GetAccuracy() + currentGun.accuracy),
0),
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);
theCrosshair.FineSightAnimation(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();
}
public Gun GetGun()
{
return currentGun;
}
public bool GetFineSightMode()
{
return isFineSightMode;
}
}
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글 남기기