Unity Chapter 11-10. 좀비 TPS 게임 만들기 : PlayerShooter

Date:     Updated:

카테고리:

태그:

인프런에 있는 이제민님의 레트로의 유니티 C# 게임 프로그래밍 에센스 강의를 듣고 정리한 필기입니다. 😀
🌜 [레트로의 유니티 C# 게임 프로그래밍 에센스] 강의 들으러 가기!


Chapter 11. 좀비 TPS 게임 만들기

TPS 게임의 특징 (FPS와 비교)

  1. 조준점이 2 개가 필요하다.
    • 플레이어 앞에 가로막는 벽 같은게 있거나 하면 카메라 화면상의 정중앙 조준점과 실제 플레이어가 든 총에서 나가는 조준점이 일치 하지 않을 때가 있다. 일치 하지 않는 경우엔 후자로 조준점을 설정해야 한다.
    • 이와 달리 FPS 에서는 화면상의 정중앙 조준점과 플레이어가 든 총에서 나가는 조준점이 늘 일치한다.
  2. 플레이어가 조준을 하지 않는 Idle 상태에서는 플레이어 캐릭터 오브젝트가 바라보는 방향과 카메라가 바라보는 화면상 방향과 일치할 필요가 없다. 그러나 플레이어 캐릭터가 총을 쏘기 위해 조준하는 HitFire 상태가 되면 카메라가 바라보는 방향으로 플레이어를 강제로 회전시켜 카메라가 바라보는 방향과 일치시켜야 한다. 총을 쏠 때 이렇게 카메라와 플레이어 캐릭터의 방향을 일치시키지 않으면, 예를들어 카메라는 앞을 보고 있는데 총알은 오른쪽으로 날아가는 현상이 발생하니까.
    • Idle 👉 HitFire 상태 변화 과정에선 총알이 쏴지지 않아야 한다. 왜냐 하면 카메라가 바라보는 방향과 일치하도록 플레이어 캐릭터가 바라보는 방향을 회전시켜야 하는 과정이 필요하기 때문에 ! 회전 다 시키지도 전에 총알을 쏴버리면 카메라는 앞을 보는데 총알은 막 왼쪽으로 날아가버리고 이런 현상이 생기므로 총알이 쏴지기전엔 먼저 두 방향을 일치시키는게 우선적으로 이루어져야 한다.
    • HitFire 👉 Idle 상태가 변화하면 이제 카메라가 바라보는 방향과 플레이어가 바라보는 방향을 일치시킬 필요가 없다. 긴장 풀어주면 됨.
    • 카메라가 바라보는 방향으로 플레이어의 방향을 회전시키는 것은 📜PlayerMovement.cs 에서 구현했다. FixedUpdate() 함수의 Rotate() 실행 부분 if 문 참고
  • 아래와 같이 플레이어는 뒤를 바라보고 있는데 화면상에서 플레이어의 앞을 조준하고 쏘니깐 쏘기전에 플레이어 캐릭터가 미리 앞으로 회전되어 카메라 방향과 일치시킨 후 발사가 되는 것을 볼 수 있다.
    • image


📜PlayerShooter.cs

Player 오브젝트에 붙인다.

  • 역할
    1. Player Character의 입력에 따라 총을 쏘거나 재장전 한다.
    2. 알맞은 애니메이션을 재생하고 IK를 사용해 Player Character 왼손의 위치가 총의 왼손 손잡이에 위치하도록 갱신한다.
using UnityEngine;

// 주어진 Gun 오브젝트를 쏘거나 재장전
// 알맞은 애니메이션을 재생하고 IK를 사용해 캐릭터 양손이 총에 위치하도록 조정
public class PlayerShooter : MonoBehaviour
{
    public enum AimState
    {
        Idle,
        HipFire
    }

    public AimState aimState { get; private set; }

    public Gun gun; // 사용할 총
    public LayerMask excludeTarget;

    private PlayerInput playerInput;
    private Animator playerAnimator; // 애니메이터 컴포넌트
    private Camera playerCamera;

    private float waitingTimeForReleasingAim = 2.5f;
    private float lastFireInputIime;

    private Vector3 aimPoint;
    private bool linedUp => !(Mathf.Abs(playerCamera.transform.eulerAngles.y - transform.eulerAngles.y) > 1f);
    private bool hasEnoughDistance => !Physics.Linecast(transform.position + Vector3.up * gun.fireTransform.position.y, gun.fireTransform.position, ~excludeTarget);

    void Awake()
    {
        if (excludeTarget != (excludeTarget | (1 << gameObject.layer)))
        {
            excludeTarget |= 1 << gameObject.layer;
        }
    }

    private void Start()
    {
        playerCamera = Camera.main;
        playerInput = GetComponent<PlayerInput>();
        playerAnimator = GetComponent<Animator>();
    }

    private void OnEnable()
    {
        aimState = AimState.Idle;
        gun.gameObject.SetActive(true);
        gun.Setup(this);
    }

    private void OnDisable()
    {
        aimState = AimState.Idle;
        gun.gameObject.SetActive(false);
    }

    private void FixedUpdate()
    {
        if (playerInput.fire)
        {
            lastFireInputIime = Time.time;
            Shoot();
        }
        else if (playerInput.reload)
        {
            Reload();
        }
    }

    private void Update()
    {
        UpdateAimTarget();

        var angle = playerCamera.transform.eulerAngles.x;
        if (angle > 270f) angle -= 360f;

        angle = angle / 180f * -1f + 0.5f;

        playerAnimator.SetFloat("Angle", angle);

        if(!playerInput.fire && Time.time >= lastFireInputIime + waitingTimeForReleasingAim)
        {
            aimState = AimState.Idle;
        }

        UpdateUI();
    }

    public void Shoot()
    {
        if (aimState == AimState.Idle)
        {
            if (linedUp) aimState = AimState.HipFire;
        }
        else if (aimState == AimState.HipFire)
        {
            if (hasEnoughDistance)
            {
                if (gun.Fire(aimPoint)) playerAnimator.SetTrigger("Shoot");
            }
            else
            {
                aimState = AimState.Idle;
            }
        }
    }

    public void Reload()
    {
        // 재장전 입력 감지시 재장전
        if (gun.Reload()) playerAnimator.SetTrigger("Reload");
    }

    private void UpdateAimTarget()
    {
        RaycastHit hit;

        var ray = playerCamera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 1f));

        if (Physics.Raycast(ray, out hit, gun.fireDistance, ~excludeTarget))
        {
            aimPoint = hit.point;

            if (Physics.Linecast(gun.fireTransform.position, hit.point, out hit, ~excludeTarget))
            {
                aimPoint = hit.point;
            }
        }
        else
        {
            aimPoint = playerCamera.transform.position + playerCamera.transform.forward * gun.fireDistance;
        }
    }

    // 탄약 UI 갱신
    private void UpdateUI()
    {
        if (gun == null || UIManager.Instance == null) return;

        // UI 매니저의 탄약 텍스트에 탄창의 탄약과 남은 전체 탄약을 표시
        UIManager.Instance.UpdateAmmoText(gun.magAmmo, gun.ammoRemain);

        UIManager.Instance.SetActiveCrosshair(hasEnoughDistance);
        UIManager.Instance.UpdateCrossHairPosition(aimPoint);
    }

    // 애니메이터의 IK 갱신
    private void OnAnimatorIK(int layerIndex)
    {
        if (gun == null || gun.state == Gun.State.Reloading) return;

        // IK를 사용하여 왼손의 위치와 회전을 총의 오른쪽 손잡이에 맞춘다
        playerAnimator.SetIKPositionWeight(AvatarIKGoal.LeftHand, 1.0f);
        playerAnimator.SetIKRotationWeight(AvatarIKGoal.LeftHand, 1.0f);

        playerAnimator.SetIKPosition(AvatarIKGoal.LeftHand,
            gun.leftHandMount.position);
        playerAnimator.SetIKRotation(AvatarIKGoal.LeftHand,
            gun.leftHandMount.rotation);
    }
}


멤버 변수 & 프로퍼티

    public enum AimState
    {
        Idle,
        HipFire
    }

    public AimState aimState { get; private set; }

    public Gun gun; // 사용할 총
    public LayerMask excludeTarget;

    private PlayerInput playerInput;
    private Animator playerAnimator; // 애니메이터 컴포넌트
    private Camera playerCamera;

    private float waitingTimeForReleasingAim = 2.5f;
    private float lastFireInputIime;

    private Vector3 aimPoint;
    private bool linedUp => !(Mathf.Abs(playerCamera.transform.eulerAngles.y - transform.eulerAngles.y) > 1f);
    private bool hasEnoughDistance => !Physics.Linecast(transform.position + Vector3.up * gun.fireTransform.position.y, gun.fireTransform.position, ~excludeTarget);
  • aimState
    • 2 가지 상태
      1. Idle : 무기 사용 X
      2. HipFire : 발사 (조준 없이 견착 발사)
    • 프로퍼티를 통하여 get은 public, set은 private으로 해두었다.
      • 📜PlayerShooter 내부에서만 상태 값을 변경할 수 있고 외부에서만 변경 불가능 하고 값을 불러오는 것만 가능하게끔.
    • 보통 TPS 게임에선 3 가지 에임 상태를 가지는데 여기선 2 가지만 다룸
      • 👉 가만히 있기, 조준 없이 발사(견착), 정조준 발사
    • 따로 에임 상태 aimState를 구분 하는 이유
      • 조준 상태일 때는 무조건 플레이어가 조준하고 있는 방향과 카메라가 바라보는 방향이 일치해야 하기 때문에 플레이어의 방향을 카메라의 방향과 일치시켜 주어야 한다.
        • 총을 사용하는 상태가 아닐 때는 플레이어가 카메라의 방향과 일치할 필요가 없다.
  • gun
    • 📜Gun 타입
  • excludeTarget
    • LayerMask 타입으로 조준에서 제외할 레이어들을 할당할 레이어 마스크
  • 플레이어
    • playerInput
      • 📜PlayerInput 타입으로 플레이어 입력 정보를 전달
    • playerAnimator
      • Animator로, 왼손 IK를 통해 총의 왼손잡이에 두어야 하기 때문에 필요한 Player Character의 애니메이터 컴포넌트
  • 카메라
    • playerCamera
      • 메인 카메라가 할당 될 곳
  • waitingTimeForReleasingAim
    • 마지막 발사 입력 시점으로부터 얼마의 기간동안 발사 입력이 없어야지 견착 조준 상태 HitFire 상태에서 Idle상태로 돌아가는지 그 텀이 되는 시간.
    • 2.5초로 초기화 되어 있으니까 마지막 조준 시점으로부터 2.5초동안 발사 입력이 또 없다면 Idle상태로 돌아가며 카메라와 플레이어의 방향을 일치시킬 필요가 없어진다.
  • lastFireInputIime
    • 마지막 발사 입력 시점
  • aimPoint
    • 실제로 조준하고 있는 대상의 위치
      • TPS가 아닌 1인칭인 FPS를 만들 때는 이게 필요 없다. FPS 에서는 카메라 화면 정중앙을 기준으로 총을 발사하면 되기 때문에!
      • FPS와 달리 TPS에서는 실제로 조준한 경우로 날아가지 않는 경우가 발생한다. 경우에 따라서는 카메라 화면 정중앙으로 발사할 수 없다
        • image
          • 위 그림으로 봤을 때 실제로 플레이어의 Gun의 위치로 봤을 때 플레이어 바로 앞에 있는 빨간 벽에 총알이 맞아야 하는데 카메라의 정중앙에서 쏘는 레이캐스트로는 뒤에 있는 벽에 맞게 된다.
          • 따라서 FPS와 달리 TPS 에서는 항상 카메라 중앙 기준으로 쏘아지는 조준점이 정답이 아닌 경우가 있기 때문에 실제로 총알이 맞아야 하는 위치를 따로 저장해두어야 한다.
    • 따라서 TPS에서는 조준점을 2 개 사용해야 한다.
      1. 실제로 Player Character 입장에서 조준한, 총알이 맞아야할 조준점 aimPoint
      2. 카메라(화면) 정중앙을 기준으로 한 조준점
  • linedUp
    • Player Character가 바라보는 방향과 playerCamera 카메라가 바라보는 방향 사이의 y 축 회전값 사이의 각도가 너무 벌어졌는지 벌어지지 않았는지를 bool 타입으로 리턴해주는 프로퍼티
      • y축 각도 차이가 1도 이상이면 false리턴
      • y축 각도 차이가 1도 이하면 true 리턴
      • 람다 함수로 정의된 프로퍼티다.
    • Player Character는 카메라 입장에서 왼쪽을 바라보고 있다면 총을 쏘면 안됨.
      • 총을 쏘는 대신에, 발사 버튼을 누르고 있는 동안 Player Character가 카메라 방향으로 정렬 되게 해야 한다.
  • hasEnoughDistance
    • Player Character가 정면에 총을 발사할 수 있을 정도의 넉넉한 공간을 확보하고 있는지를 리턴하는 프로퍼티
      • Player Character가 너무 벽에 가까이 붙어있으면 총구가 벽에 파묻히게 되니까 이런 상황에선 총이 발사되지 않도록 할 것.
    • Physics Linecast
      • 라인캐스트. 눈에 보이지 않는 광선을 쏴서 물리적 충돌을 감지한다.
        • Raycast 와의 차이점
          • 👉 Linecast는 정확한 종료 지점을 알 때, Raycast는 방향만 알 때 사용!
      • Linecast 시작 지점
        • transform.position + Vector3.up * gun.fireTransform.position.y
          • 현재 위치(= Player Character의 발🦶)로부터 총의 발사구 위치의 y 값에 위쪽 방향 벡터를 곱해준 만큼을 더해준다.
            • 즉 플레이어의 발 위치에서 총의 발사구의 y방향의 거리벡터만큼을 위로 더해서 Linecast 시작 위치를 높혀준 것!
              • 즉, Linecast의 시작 지점은 개머리 판이 있는 플레이어의 가슴 정도 위치에 해당되게 된다.
      • Linecast 종료 지점
        • gun.fireTransform.position
          • 총의 발사구 위치
      • ~excludeTarget
        • 마찬가지로 조준 대상이 아닌 레이어들은 처리 대상에서 제외했다.
    • image
      • 라인 캐스트 범위는 대충 플레이어의 가슴으로부터 총의 발사구 까지가 된다.
      • 이 부분에 어떤 충돌이 일어나게 된다면, 즉 이 부분이 벽 같은 곳에 파묻히게 되어 false가 hasEnoughDistance에 리턴된다면 총이 파묻혔다는 뜻이므로 발사가 되지 않게끔 할 것이다.
        • !가 붙어있어 bool 타입 반대된 것에 주의하기


멤버 함수

void Awake()

excludeTargetPlayer Character의 레이어가 포함되어 있지 않다면 Player Character의 레이어를 excludeTarget에 추가하는 연산을 수행한다.

    void Awake()
    {
        if (excludeTarget != (excludeTarget | (1 << gameObject.layer)))
        {
            excludeTarget |= 1 << gameObject.layer;
        }
    }
  • *if (excludeTarget != (excludeTarget (1 « gameObject.layer)))*
    • 예를 들어 레이어가 8가지라면 32bit중 8bit의 각각의 자리는 각 layer에 대응한다.
    • (excludeTarget | (1 << gameObject.layer))
      • gameObject, 즉 Player Character의 layer 와 excludeTarget합친 것 👉 OR 연산
        • (1 << gameObject.layer)
          • 00000000000000000000000000000001 을 gameObject.layer 만큼, 즉 해당 레이어의 인덱스만큼 left-shift 연산을 한다.
            • 비트 내에서 1 을 해당 레이어에 대응 하는 위치만큼 옮김.
        • 조준 대상이 아닌 레이어들의 비트는 1로 모아둔 excludeTarget비트와 (1 « gameObject.layer) 를 합쳤을 때 excludeTarget비트와 다르다면 중복되지 않은 레이어다.
          • 즉, gameObject.layerexcludeTarget에 포함되지 않은 레이어다.
            • 그렇다면 포함 해서 excludeTarget을 갱신해야 한다.
              • *excludeTarget = 1 « gameObject.layer;*
  • 이같이 excludeTarget을 두는 이유
    • 플레이어는 플레이어 스스로를 쏘면 안되기 때문.
      • 나중에 Player 를 레이어로 설정할 것이다.


private void Start()

📜PlayerShooter 에서 사용할 외부 컴포넌트들을 가져옴

    private void Start()
    {
        playerCamera = Camera.main;  // 메인 카메라 가져옴
        playerInput = GetComponent<PlayerInput>(); 
        playerAnimator = GetComponent<Animator>();
    }


private void OnEnable()

📜PlayerShooter스크립트(컴포넌트) 혹은 오브젝트가 활성화되는 순간마다 실행되는 이벤트 함수

    private void OnEnable()
    {
        aimState = AimState.Idle;
        gun.gameObject.SetActive(true);
        gun.Setup(this);
    }
  • 에임 상태를 Idle로 초기화
  • Gun 오브젝트를 활성화 시키고
  • 📜Gun.cs의 Setup 함수 실행
    • 📜Gun.cs의 gunHolderexcludeTarget를 초기화
    • Gun을 쥐고있는 Shooter가 나 자신(Player Character)임을 인수(this)로 넘겨 알려준다.


private void OnDisable()

📜PlayerShooter 스크립트(컴포넌트) 혹은 오브젝트가 비활성화되는 순간마다 실행되는 이벤트 함수

    private void OnDisable()
    {
        aimState = AimState.Idle;
        gun.gameObject.SetActive(false);
    }
  • 에임 상태를 Idle로 초기화
  • Gun 오브젝트도 같이 비활성화 한다.


private void FixedUpdate()

고정된 간격으로 실행되는 Update 이벤트 함수인만큼 물리처리 기능을 담당한다.

    private void FixedUpdate()
    {
        if (playerInput.fire)
        {
            lastFireInputIime = Time.time;
            Shoot();
        }
        else if (playerInput.reload)
        {
            Reload();
        }
    }
  • 매 순간 고정된 간격으로 실행된다.
    • 입력에 따라 fire 상태라면 Shoot() 함수 실행
      • 발사를 했다면 마지막 발사 시점을 발사한 당시의 현재 시간으로 갱신해주어야 한다.
      • lastFireInputTime 갱신
    • 입력에 따라 reload 상태라면 Reload() 함수 실행


private void Update()

    private void Update()
    {
        UpdateAimTarget();

        var angle = playerCamera.transform.eulerAngles.x;  // x 축 회전값
        if (angle > 270f) angle -= 360f;  // -90 ~ +90 범위로 만들기. 270도는 -90도나 마찬가지니까 270도 넘는건 360을 빼주어야 함.
        angle = angle / -180f + 0.5f; // 1 ~ 0 범위로 변환
        playerAnimator.SetFloat("Angle", angle);

        if(!playerInput.fire && Time.time >= lastFireInputIime + waitingTimeForReleasingAim)
        {
            aimState = AimState.Idle;
        }

        UpdateUI();
    }
  • UpdateAimTarget() 함수를 매 프레임 실행
    • aimPoint 계속해서 갱신
  • Player Charater 애니메이션의 “angle” 파라미터 매 프레임 갱신
    • “angle” 값이 0 이면 땅을 바라보는 애니메이션, 0.5 면 정면을 바라보는 애니메이션, 1 이면 하늘을 바라보는 애니메이션을 취함
      • var angle = playerCamera.transform.eulerAngles.x;
      • x 방향의 회전이 고개를 숙이거나 높이는 것과 관련된 회전축이다.
        • 바닥을 보고 있을 땐 + 90 도에 가까워지고 하늘을 바라볼 땐 - 90 도에 가까워 진다.
        • 바닥을 보고 있을 땐 0에 가까워 지게, 하늘을 바라볼 땐 1에 가까워지게 변환 해야 함!
          • -90 ~ +90 범위를 1 ~ 0 범위로 변환해야 한다.
  • 발사 입력이 없고 현재 시간이 (lastFireInputIime + waitingTimeForReleasingAim) 을 초과했다면
    • Hitfire 👉 Idle 상태로 바꾸어야 한다.
      • Idle 상태가 되면 카메라의 방향에 플레이어의 방향을 더 이상 일치시킬 필요가 없어짐.
  • UpdateUI() 함수를 매 프레임 실행


public void Shoot()

aimState에 따른 발사 처리

  • Gun을 사용하는 코드만 넣으면 된다.
    • 발사 처리의 세부 구현은 📜Gun.cs 에서 구혆놓음.
    public void Shoot()
    {
        if (aimState == AimState.Idle)
        {
            if (linedUp) aimState = AimState.HipFire;
        }
        else if (aimState == AimState.HipFire)
        {
            if (hasEnoughDistance)
            {
                if (gun.Fire(aimPoint)) playerAnimator.SetTrigger("Shoot");
            }
            else
            {
                aimState = AimState.Idle;
            }
        }
    }

  • 에임 상태가 무기를 사용하지 않는 Idle 상태에서 Shoot 입력이 들어온거라면
    • 에임 상태를 견착 조준 상태 HipFire로 바꾸어 주어야 한다.
      • 단, lineUp이 true일때. 즉, Player Character가 바라보는 방향과 playerCamera 카메라가 바라보는 방향 사이의 y 축 회전값 사이의 각도가 1 도를 넘기지 않을 때만.
      • 카메라와 플레이어가 같은 방향으로 정렬이 되어 있을 때만
  • 이미 견착 조준 상태였던 HipFire 에서 Shoot 입력이 들어온거라면
    • hasEnoughDistance이 true일 때 즉, 총이 어디 벽같은데 파묻혀 있는게 아닐 때만
      • 그리고 발사가 가능할 때만 즉, gun.Fire(aimPoint) ture 일 때만
        • 플레이어 애니메이션의 Trigger를 “Shoot” 상태로 설정.
    • hasEnoughDistance이 false일 때 즉, 총이 어디 벽같은데 파묻혀 있는거라면
      • 에임 상태를 Idle 상태로 되돌리기.


public void Reload()

    public void Reload()
    {
        // 재장전 입력 감지시 재장전
        if (gun.Reload()) playerAnimator.SetTrigger("Reload");
    }
  • 재장전이 가능한 상태라면, 즉 gun.Reload()가 true 리턴
    • 플레이어 애니메이션의 Trigger를 “Reload” 상태로 설정.


private void UpdateAimTarget()

aimPoint을 매 프레임마다 갱신

    private void UpdateAimTarget()
    {
        RaycastHit hit;

        var ray = playerCamera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0f));

        if (Physics.Raycast(ray, out hit, gun.fireDistance, ~excludeTarget))
        {
            aimPoint = hit.point;

            if (Physics.Linecast(gun.fireTransform.position, hit.point, out hit, ~excludeTarget))
            {
                aimPoint = hit.point;
            }
        }
        else
        {
            aimPoint = playerCamera.transform.position + playerCamera.transform.forward * gun.fireDistance; // 앞쪽 방향으로 최대 사정 거리
        }
    }

image

  • ViewportPointToRay(Vecotr3)
    • 카메라 오브젝트에 내장되어 있는 함수로서
    • 카메라가 찍고 있는 화면(뷰포트)상의 위치를 Vector3로 넘기면 그 화면 상 위치를 가로지르는 Ray를 리턴한다.
    • position.z 는 무시 된다.
  • ray = playerCamera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0f));
    • 카메라 화면의 정중앙을 가로지르는 광선 ray
  • 카메라 화면의 정중앙을 가로지르는 광선의 충돌이 감지 된다면
    • 1 차로 aimPoint 업데이트. Raycast 충돌 지점인 hit.point로 업데이트
    • 만약 이런 상황에서 총의 발사구(gun.fireTransform.position)로부터 카메라 화면의 정중앙을 가로지르는 광선의 충돌지점(hit.point)까지의 Linecast 에서 뭔가 충돌되는게 있다면!
      • 실제로 카메라 화면의 정중앙을 가로지르는 광선의 충돌지점을 조준점으로 삼아야 하는게 아닌 그 Linecast 충돌 지점을 aimPoint로 잡아야 한다.
        • 2 차로 aimPoint 업데이트. Linecast 충돌 지점인 hit.point로 업데이트
  • 카메라 화면의 정중앙을 가로지르는 광선에 아무것도 충돌되는게 없다면
    • 조준점 aimPointplayerCamera의 위치로부터 (playerCamera 앞쪽 방향 X 총알의 최대 사정 거리) 만큼 더해준 곳(벡터)로 잡는다.


private void UpdateUI()

남은 탄창 UI 갱신, 조준점 크로스헤어 갱신

    private void UpdateUI()
    {
        if (gun == null || UIManager.Instance == null) return;

        // 탄약 UI 갱신
        UIManager.Instance.UpdateAmmoText(gun.magAmmo, gun.ammoRemain);

        // 크로스 헤어 UI 갱신
        UIManager.Instance.SetActiveCrosshair(hasEnoughDistance);  // 충분한 공간이 있을 때만 크로스 헤어를 활성화함
        UIManager.Instance.UpdateCrossHairPosition(aimPoint); // 크로스 헤어를 조준점 위치로 업데이트
    }
  • 추후에 작성할 📜UIManager.cs 를 사용한다.
    • 싱글톤으로 각종 게임 UI에 즉시 접근해서 UI 갱신의 통로를 제공하는 스크립트다.
  • 총이 없거나 📜UIManager.cs 인스턴스가 없다면 그냥 return


private void OnAnimatorIK(int layerIndex)

IK 가 갱신될 때마다 매번 자동으로 실행되는 이벤트 함수

  • 왼손의 위치를 IK로 오버라이딩 한다.
    private void OnAnimatorIK(int layerIndex)
    {
        if (gun == null || gun.state == Gun.State.Reloading) return;

        // IK를 사용하여 왼손의 위치와 회전을 총의 오른쪽 손잡이에 맞춘다
        playerAnimator.SetIKPositionWeight(AvatarIKGoal.LeftHand, 1.0f);  // 왼손의 위치 weight을 100퍼센트로
        playerAnimator.SetIKRotationWeight(AvatarIKGoal.LeftHand, 1.0f); // 왼손의 회전 weight을 100퍼센트로

        playerAnimator.SetIKPosition(AvatarIKGoal.LeftHand, gun.leftHandMount.position);  // 왼손을 총의 왼손 손잡이 부분을 중심으로 꺾어서 위치시킴 
        playerAnimator.SetIKRotation(AvatarIKGoal.LeftHand, gun.leftHandMount.rotation); // 왼손을 총의 왼손 손잡이 부분을 중심으로 꺾어서 회전시킴
    }
  • 총이 없거나 재장전 중인 상태일 때는 왼손의 위치를 IK로 오버라이딩 하지 않을 것.


컴포넌트 설정

image

  • 레이어로 Player를 할당해주는데, 사실 Awake() 함수에서 자동으로 Player를 레이어로 할당해주기 때문에 여기서 Player로 안해주어도 상관은 없다.


🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기


Unity Lesson 1 카테고리 내 다른 글 보러가기

댓글 남기기