3/19_Zombie
1. Lighting굽기

- New Lighting Settings 클릭 하고 이름을 Main Light Settings로 변경


- 환경광(Environment Lighting) 설정 → color로 변경


- 다시 Scene으로 돌아와서 Global Illumination(글로벌 일루미네이션) 체크



- 구워지면 이렇게 우중충

2. 캐릭터 애니메이션 구성하기
- 프리팹에서 Women 꺼내서 하이어라키에 놓고 이름 바꾸기

- 리지드바디 추가 후 수치 변경

- 캡슐 콜라이더 추가 후 수치 변경

- 오디오소스 추가

- 애니메이터 할당하기

3. 애니메이터 구경





- Idle에서 Reload는 동작 끝날때 까지 기다릴 필요없지만 장전은 기다려야해서 체크


- 아바타마스크


- Upper Body 레이어에 Upper Body Mask 적용

4. Player Input, Player Movement 스크립트 작성
- 입력을 감지하는 스크립트와 움직임을 따로 작성(입력과 액터 나누기)

4-1. Player Input 스크립트
using UnityEngine;
// 플레이어 캐릭터를 조작하기 위한 사용자 입력을 감지
// 감지된 입력값을 다른 컴포넌트들이 사용할 수 있도록 제공
public class PlayerInput : MonoBehaviour {
public string moveAxisName = "Vertical"; // 앞뒤 움직임을 위한 입력축 이름
public string rotateAxisName = "Horizontal"; // 좌우 회전을 위한 입력축 이름
public string fireButtonName = "Fire1"; // 발사를 위한 입력 버튼 이름
public string reloadButtonName = "Reload"; // 재장전을 위한 입력 버튼 이름
// 값 할당은 내부에서만 가능
public float move { get; private set; } // 감지된 움직임 입력값
public float rotate { get; private set; } // 감지된 회전 입력값
public bool fire { get; private set; } // 감지된 발사 입력값
public bool reload { get; private set; } // 감지된 재장전 입력값
// 매프레임 사용자 입력을 감지
private void Update() {
// 게임오버 상태에서는 사용자 입력을 감지하지 않는다
if (GameManager.instance != null && GameManager.instance.isGameover)
{
move = 0;
rotate = 0;
fire = false;
reload = false;
return;
}
// move에 관한 입력 감지
move = Input.GetAxis(moveAxisName);
// rotate에 관한 입력 감지
rotate = Input.GetAxis(rotateAxisName);
// fire에 관한 입력 감지
fire = Input.GetButton(fireButtonName);
// reload에 관한 입력 감지
reload = Input.GetButtonDown(reloadButtonName);
}
}
- 앞, 뒤, 좌, 우 : 축으로 감지 → 숫자로 반환
- 발사, 장전 : 버튼으로 감지 → true, false로 반환
4-2. Player Moverment 스크립트
using UnityEngine;
// 플레이어 캐릭터를 사용자 입력에 따라 움직이는 스크립트
public class PlayerMovement : MonoBehaviour
{
public float moveSpeed = 5f; // 앞뒤 움직임의 속도
public float rotateSpeed = 180f; // 좌우 회전 속도
private PlayerInput playerInput; // 플레이어 입력을 알려주는 컴포넌트
private Rigidbody playerRigidbody; // 플레이어 캐릭터의 리지드바디
private Animator playerAnimator; // 플레이어 캐릭터의 애니메이터
private void Start()
{
// 사용할 컴포넌트들의 참조를 가져오기
playerInput = GetComponent<PlayerInput>();
playerRigidbody = GetComponent<Rigidbody>();
playerAnimator = GetComponent<Animator>();
}
// FixedUpdate는 물리 갱신 주기에 맞춰 실행됨(기본값 0.02초)
private void FixedUpdate()
{
// 물리 갱신 주기마다 움직임, 회전, 애니메이션 처리 실행
// 회전 실행
Rotate();
// 움직임 실행
Move();
// 입력 값에 따라 애니메이터의 Move 파라미터 값 변경
playerAnimator.SetFloat("Move", playerInput.move); // 입력에 따라 애니메이션 변경됨 걷고 뛰고
}
// 입력값에 따라 캐릭터를 앞뒤로 움직임
private void Move()
{
// 상대적으로 이동할 거리 계산
Vector3 moveDistance = playerInput.move * transform.forward * moveSpeed * Time.deltaTime;
// 리지드바디를 이용해 게임 오브젝트 위치 변경
playerRigidbody.MovePosition(playerRigidbody.position + moveDistance);
}
// 입력값에 따라 캐릭터를 좌우로 회전
private void Rotate()
{
// 상대적으로 회전할 수치 계산
float turn = playerInput.rotate * rotateSpeed * Time.deltaTime;
// 리지드바디를 이용해 게임 오브젝트 회전 변경
playerRigidbody.rotation = playerRigidbody.rotation * Quaternion.Euler(0, turn, 0f);
}
}
- SetFloat("변수", 어떤 움직임) : 애니메이터 변수를 변경하는 내장메서드 : move 애니메이터 내의 Move라는 변수를 playerInput.move으로 변경
- 이동할 거리 : 방향 * 속력 * 시간 : 이동할 거리에 PlayerInput의 입력값이 곱해지면 움직이는 방향이 조절됨 : 입력값 1 → 앞으로 이동 / 0 → 안움직임 / -1 → 뒤로 이동
- Quaternion.Euler(x, y, z) : Quaternion.Euler(0, turn, 0f) → y축 회전을 turn수치만큼 : 1입력받으면 오른쪽 회전 -1 입력받으면 왼쪽 회전
- MovePosition( ) : 리지드바디 관련 내장 메서드 : transform.position = transform.position+ movementDistance 할 수도 있지만 이렇게 하면 물리처리를 무시하기 때문에 벽을 통과해 다닌다. 그래서 리지드바디 관련 MovePosition을 씀 : 상대위치가 아닌 절대위치를 변경하기 때문에 playerRigidbody.position + moveDistance 이렇게 현재위치 + 상대적으로 더 이동할 거리로 씀
5. 시네머신 추적 카메라 구성하기
- 카메라 움직임을 손쉽게 제어하는 유니티 공식 패키지
5-1. 메인카메라에 CinemachineBrain 컴포넌트 추가

- 메인이 브레인이 되며 이외의 가상카메라(Virtual Camera) 여럿을 관리하게 한다
- 가상카메라는 메인카메라의 분신이며, 진짜 카메라는 결국 메인카메라다
- 메인카메라 = 뇌, 머리 , 가상카메라 = 여러 몸 분신. → 분신에 뇌가 깃들 수 있다, 설정값도 가져올 수 있다
5-2. 하이어라키에서 Cinemachine → Virtual Camera 추가

- Follow 와 Look At에 플레이어 캐릭터 할당

- 데드존, 소프트존, 하드리밋 설정 - 데드존 : 화면의 하얀부분 : 카메라가 주시하는 물체가 데드존에 있으면 카메라의 에임이 맞는 것(카메라가 회전하지 않음) - 소프트존 : 화면의 하늘색 부분 : 물체가 데드존을 벗어나 소프트 존에 있다면 카메라가 부드럽게 회전해 에임에 오도록 맞춘다 - 데드존 : 화면의 빨간 부분 : 물체가 데드존에 위치하면 많이 벗어났으니 카메라가 급격히 회전해 에임에 오게 맞춘다
- 가상 카메라의 파라미터 조절

- FOV : 시야각(좁을수록 줌인)
- Body - 카메라가 Follow에 할당된 추적 대상을 어떻게 따라나닐지 결정 - 추적대상 = 카메라의 몸 : 카메라 = 머리 : 카메라는 World Space(전역공간) 을 기준으로 (-8, 16, -8) 떨어져서 몸을 따라다닌다 - Damping : 제동값, 급격한 변화를 ‘꺽어’ 이전값 이후값을 부드럽게 이어주는 비율 : 값이 커질수록 카메라 위치의 급격한 변화가 줄어들지만 신속하게 변경되지 않고 지연시간이 늘어난다.

- Aim 파라미터: 카메라 조준점
- Tracked Object Offset : 얼마나 떨어진 곳을 조준할 지 : 캐릭터 실제위치보다 Y축으로 0.5 높은 곳
- 제동값 다 0 으로 하고 소프트존도 없애서 카메라가 지연시간 없이 플레이어 캐릭터를 조준하도록 만듦
6. 인터페이스
- 인터페이스란 외부와 통신하는 공개 통로, 통로의 구격 (마치 like USB)
- 클래스가 반드시 구현해야하는 메서드(기능)들의 틀을 정의하는 개념 → 인터페이스를 상속받은 클래스는 무조건 인터페이스의 메서드를 구현 해야함
- C#에서 interface 키워드를 사용해서 만들 수 있다
- 느슨한 커플링 : 어떤 코드가 특정 클래스의 구현에 결합되지 않아 유연하게 변경 가능한 상태
EX) IItem 스크립트( Item앞에 붙은 I가 인터페이스 스크립트인걸 암시)
using UnityEngine;
// 아이템 타입들이 반드시 구현해야하는 인터페이스
public interface IItem
{
// 입력으로 받는 target은 아이템 효과가 적용될 대상
void Use(GameObject target);
}
- 선언만 있다
EX) IItem을 상속하는 AmmoPack 스크립트
using UnityEngine;
// 총알을 충전하는 아이템
public class AmmoPack : MonoBehaviour, IItem {
public int ammo = 30; // 충전할 총알 수
public void Use(GameObject target) {
// 전달 받은 게임 오브젝트로부터 PlayerShooter 컴포넌트를 가져오기 시도
PlayerShooter playerShooter = target.GetComponent<PlayerShooter>();
// PlayerShooter 컴포넌트가 있으며, 총 오브젝트가 존재하면
if (playerShooter != null && playerShooter.gun != null)
{
// 총의 남은 탄환 수를 ammo 만큼 더한다
playerShooter.gun.ammoRemain += ammo;
}
// 사용되었으므로, 자신을 파괴
Destroy(gameObject);
}
}
- public void Use(GameObject target) 메서드를 구현한걸 볼 수 있다
Ex) IDamagerable 스크립트 (인터페이스)
using UnityEngine;
// 데미지를 입을 수 있는 타입들이 공통적으로 가져야 하는 인터페이스
public interface IDamageable
{
// 데미지를 입을 수 있는 타입들은 IDamageable을 상속하고 OnDamage 메서드를 반드시 구현해야 한다
// OnDamage 메서드는 입력으로 데미지 크기(damage), 맞은 지점(hitPoint), 맞은 표면의 방향(hitNormal)을 받는다
void OnDamage(float damage, Vector3 hitPoint, Vector3 hitNormal);
}
- 공격당할 수 있는 모든 대상을 위한 인터페이스
7. 캐릭터에 우클릭 빈 GameObject를 만들고 이름을 Gun Pivot으로 설정
- 총을 배치할 지점을 만드는 것


7-1. 프리팹에 Gun을 꺼내 Gun Pivot에 넣는다
7-2. Gun에 Line Renderer 컴포넌트 추가
- 렌더러는 일단 꺼둔다




7-3. 오디오 소스 추가

7-4. 파티클 프리팹을 Gun에 넣어주기

8. 스크립터블 오브젝트
- 여러 오브젝트가 사용할 데이터를 유니티 에셋 형태로 저장할 수 있는 타입

Ex) GunData 스크립트 (총의 수치에 대해 따로 작성한 스크립트)
using UnityEngine;
[CreateAssetMenu(menuName = "Scriptable/GunData", fileName = "Gun Data")]
public class GunData : ScriptableObject
{
public AudioClip shotClip; // 발사 소리
public AudioClip reloadClip; // 재장전 소리
public float damage = 25; // 공격력
public int startAmmoRemain = 100; // 처음에 주어질 전체 탄약
public int magCapacity = 25; // 탄창 용량
public float timeBetFire = 0.12f; // 총알 발사 간격
public float reloadTime = 1.8f; // 재장전 소요 시간
}
- [CreateAssetMenu]를 선언 해둬야 스크립터블 오브젝트 사용 가능
- 총의 업그레이드나 다른 종류의 총이 등장해도 수정하기 편하고 관리하기 쉬워진다
- 게임오브젝트가 아닌 형태로 존재 해야한다 → 게임오브젝트는 씬에서만 수정 가능하기에
9. Gun 스크립트 작성
using System.Collections;
using UnityEngine;
// 총을 구현
public class Gun : MonoBehaviour
{
// 총의 상태를 표현하는 데 사용할 타입을 선언
public enum State
{
Ready, // 발사 준비됨
Empty, // 탄알집이 빔
Reloading // 재장전 중
}
public State state { get; private set; } // 현재 총의 상태
public Transform fireTransform; // 탄알이 발사될 위치
public ParticleSystem muzzleFlashEffect; // 총구 화염 효과
public ParticleSystem shellEjectEffect; // 탄피 배출 효과
private LineRenderer bulletLineRenderer; // 탄알 궤적을 그리기 위한 렌더러
private AudioSource gunAudioPlayer; // 총 소리 재생기
public GunData gunData; // 총의 현재 데이터
private float fireDistance = 50f; // 사정거리
public int ammoRemain = 100; // 남은 전체 탄알
public int magAmmo; // 현재 탄알집에 남아 있는 탄알
private float lastFireTime; // 총을 마지막으로 발사한 시점
- enum 열거형으로 총이 어떤 상태인지 저장하는 * 상태(State)*를 만듦
private void Awake()
{
// 사용할 컴포넌트의 참조 가져오기
gunAudioPlayer = GetComponent<AudioSource>();
bulletLineRenderer = GetComponent<LineRenderer>();
// 사용할 점을 두개로 변경
bulletLineRenderer.positionCount = 2;
// 라인 렌더러를 비활성화
bulletLineRenderer.enabled = false;
}
private void OnEnable()
{
// 총 상태 초기화
// 전체 예비 탄알 양을 초기화
ammoRemain = gunData.startAmmoRemain;
// 현재 탄창을 가득 채우기
magAmmo = gunData.magCapacity;
// 총의 현재 상태를 총을 쏠 준비가 된 상태로 변경
state = State.Ready;
// 마지막으로 총을 쏜 시점을 초기화
lastFireTime = 0;
}
// 발사 시도
public void Fire()
{
// 현재 상태가 발사 가능한 상태
// && 마지막 총 발사 시점에서 gunData.timeBetFire 이상의 시간이 지남
if(state == State.Ready && Time.time >= lastFireTime + gunData.timeBetFire)
{
// 마지막 총 발사 시점 갱신
lastFireTime = Time.time;
// 실제 발사 처리 실행
Shot();
}
}
// 실제 발사 처리
private void Shot()
{
// 레이캐스트에 의한 충돌 정보를 저장하는 컨테이너
RaycastHit hit;
// 탄알이 맞은 곳을 저장할 변수
Vector3 hitPosition = Vector3.zero;
// 레이캐스트(시작 지점, 방향, 충돌정보 컨테이너, 사정거리)
if (Physics.Raycast(fireTransform.position, fireTransform.forward, out hit, fireDistance))
{
// 레이가 어떤 물체와 충돌한 경우
// 충돌한 상대방으로부터 IDamageable 오브젝트 가져오기 시도
IDamageable target = hit.collider.GetComponent<IDamageable>();
// 상대방으로부터 IDamageable 오브젝트를 가져오는 데 성공했다면
if (target != null)
{
// 상대방의 OnDamage 함수를 실행시켜 상대방에 데미지 주기
target.OnDamage(gunData.damage, hit.point, hit.normal);
}
// 레이가 충돌한 위치 저장
hitPosition = hit.point;
}
else
{
// 레이가 다른 물체와 충돌하지 않았다면
// 탄알이 최대 사정거리까지 날아갔을 때의 위치를 충돌 위치로 사용
hitPosition = fireTransform.position + fireTransform.forward * fireDistance;
}
- State.Ready : 준비 된 상태
- 현재 시간 > = 마지막 발사시점(최근 발사시점) + 발사 간격
- Raycast
- IDamageable 타입의 타겟이 존재한다면 → if (target != null)
- hitPoint : 맞은 위치, hit.Normal : 탄알이 충돌한 표면의 방향
// 발사 이펙트 재생 시작(코루틴)
StartCoroutine(ShotEffect(hitPosition));
// 남은 탄알 수를 -1
magAmmo--;
if (magAmmo <= 0)
{
// 탄창에 남은 탄알이 없다면 총의 현재 상태를 Empty로 갱신
state = State.Empty;
}
}
// 발사 이펙트와 소리를 재생하고 탄알 궤적을 그림
private IEnumerator ShotEffect(Vector3 hitPosition)
{
// 총구 화염 효과 재생
muzzleFlashEffect.Play();
// 탄피 배출 효과 재생
shellEjectEffect.Play();
// 총격 소리 재생
gunAudioPlayer.PlayOneShot(gunData.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 >= gunData.magCapacity)
{
// 재장전 할 수 없음
return false;
}
// 재장전 처리 시작(코루틴)
StartCoroutine(ReloadRoutine());
return false;
}
// 실제 재장전 처리를 진행
private IEnumerator ReloadRoutine()
{
// 현재 상태를 재장전 중 상태로 전환
state = State.Reloading;
// 재장전 소리 재생
gunAudioPlayer.PlayOneShot(gunData.reloadClip);
// 재장전 소요 시간 만큼 처리 쉬기
yield return new WaitForSeconds(gunData.reloadTime);
// 탄창에 채울 탄알 계산
int ammoToFill = gunData.magCapacity - magAmmo;
// 탄창에 채워야 할 탄알이 남은 탄알보다 많다면
if(ammoRemain < ammoToFill)
{
// 채워야 할 탄알 수를 남은 탄알 수에 맞춰 줄임
ammoToFill = ammoRemain;
}
// 탄창을 채움
magAmmo += ammoToFill;
// 남은 탄알에서 탄창에 채운만큼 탄알을 뺌
ammoRemain -= ammoToFill;
// 총의 현재 상태를 발사 준비된 상태로 변경
state = State.Ready;
}
}
- bulletRenderer 점 2개 선언한걸 지금 하나는 총구에, 하나는 힛포지션(맞는곳)에 배치.
- 코루틴 시작(발사이펙트)
- ammoToFill : 채워넣어야 할 탄알 수 = 탄창최대용량 - 현재 탄알 수
10. 스크립트 할당하기



11. 플레이어 슈터 만들기
- 플레이어 입력에 따라 총을 쏘거나 재장전
- 플레이어의 손이 항상 총의 손잡이에 있게 하기
using UnityEngine;
// 주어진 Gun 오브젝트를 쏘거나 재장전
// 알맞은 애니메이션을 재생하고 IK를 사용해 캐릭터 양손이 총에 위치하도록 조정
public class PlayerShooter : MonoBehaviour
{
public Gun gun; // 사용할 총
public Transform gunPivot; // 총 배치의 기준점
public Transform leftHandMount; // 총의 왼쪽 손잡이, 왼손이 위치할 지점
public Transform rightHandMount; // 총의 오른쪽 손잡이, 오른손이 위치할 지점
private PlayerInput playerInput; // 플레이어의 입력
private Animator playerAnimator; // 애니메이터 컴포넌트
private void Start()
{
// 사용할 컴포넌트들을 가져오기
playerInput = GetComponent<PlayerInput>();
playerAnimator = GetComponent<Animator>();
}
private void OnEnable()
{
// 슈터가 활성화될 때 총도 함께 활성화
gun.gameObject.SetActive(true);
}
private void OnDisable()
{
// 슈터가 비활성화될 때 총도 함께 비활성화
gun.gameObject.SetActive(false);
}
- OnEnable( ) : 게임 오브젝트가 활성화 될 때 자동으로 호출 되는 메서드 , 총도 함께 활성화!
- OnDisable( ) : 게임 오브젝트가 비활성화 될 때 자동으로 호출
private void Update()
{
// 입력을 감지하고 총 발사하거나 재장전
// 발사 입력 감지 시
if (playerInput.fire)
{
// 총 발사
gun.Fire();
}
// 재장전 입력 감지 시
else if(playerInput.reload)
{
// 재장전
if(gun.Reload())
{
// 재장전 성공 시에만 재장전 애니메이션 재생
playerAnimator.SetTrigger("Reload");
}
}
// 남은 탄알 UI 갱신
UpdateUI();
}
// 탄약 UI 갱신
private void UpdateUI()
{
if (gun != null && UIManager.instance != null)
{
// UI 매니저의 탄약 텍스트에 탄창의 탄약과 남은 전체 탄약을 표시
UIManager.instance.UpdateAmmoText(gun.magAmmo, gun.ammoRemain);
}
}
// 애니메이터의 IK 갱신
private void OnAnimatorIK(int layerIndex)
{
// 총의 기준점 gunPivot을 3D 모델의 오른쪽 팔꿈치 위치로 이동
gunPivot.position = playerAnimator.GetIKHintPosition(AvatarIKHint.RightElbow);
// GetIKHintPosition : 캐릭터의 특정 부위 위치를 알려주는 기능
// IK를 사용하여 왼손의 "위치"와 "회전"을 총의 왼쪽 손잡이에 맞춤
playerAnimator.SetIKPositionWeight(AvatarIKGoal.LeftHand, 1.0f);
playerAnimator.SetIKRotationWeight(AvatarIKGoal.LeftHand, 1.0f);
playerAnimator.SetIKPosition(AvatarIKGoal.LeftHand, leftHandMount.position);
playerAnimator.SetIKRotation(AvatarIKGoal.LeftHand, leftHandMount.rotation);
// SetIKPosition : IK 목표의 위치를 설정하는 역할
// SetIKPositionWeight : IK를 제어하는데, 0과 1 사이의 가중치 값을 설정하여 영향력을 조절
// IK를 사용하여 오른손의 위치와 회전을 총의 오른손 손잡이에 맞춤
playerAnimator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1.0f);
playerAnimator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1.0f);
playerAnimator.SetIKPosition(AvatarIKGoal.RightHand, rightHandMount.position);
playerAnimator.SetIKRotation(AvatarIKGoal.RightHand, rightHandMount.rotation);
}
}
- GetIKHintPosition : 캐릭터의 특정 부위 위치를 알려주는 기능
- SetIKPositionWeight( ) : 얼마나 IK의 영향을 받을지 정하는 메서드
- SetIKPosition : IK 목표의 위치를 설정하는 역할
12. 스크립트에 해당 항목 할당
