본문 바로가기
C#/UNITY_FPS 3D 서바이벌게임

Chapter 1-4. 캐릭터, 지형 : 팔 구현, 팔 애니메이션, 서브 카메라

by w1z 2024. 3. 11.

Chapter 1. 캐릭터 움직이기 & 지형 제작

팔 구현

🙋‍♀️ 팔 붙이기

  • Player
    • 캡슐 모양의 플레이어 오브젝트
    • 자식
      • Main Camera
        • 플레이어의 자식으로서 플레이어를 따라다니며 플레이어의 1인칭 카메라가 되어 줄 오브젝트
      • Hand Holder
        • 빈 게임 오브젝트로서 팔 모델의 부모 오브젝트로 설정하여 팔 모델의 트랜스폼엔 변화를 주지 않고 이 “Hand Holder” 오브젝트의 트랜스폼에만 변화를 주어 Player 오브젝트 내에서 팔 모델이 자리를 잡도록 한다.
        • 카메라에 팔이 알맞는 위치로 잡히도록 Position은 (0, 0.5, -1.3), Rotation은 (0, 90, 0)으로 하였다.
          • Y 축 중심으로 90 도 회전하여 카메라에 1인칭 시점으로 팔이 나오도록 카메라 방향과 일치시키도록 회전시켰다.
        • 자식
          • Hand
            • 임포트 받은 기본 팔 3D 모델. 이름을 “Hand”로 변경함
            • 우클 - Unpack Prefab 하여 프리팹 상태를 해제 시키고 자식으로 있는 카메라는 삭제해 주었다.

 

🙋‍♀️ 팔 애니메이션

 

팔 애니메이션 만들기

임포트 받은 팔의 3D 모델 파일(lowPoly_arms_Basic Import Settings)은 아래와 같이 이미 애니메이션이 내장되어 있어서 이 애니메이션 프레임을 쪼개어서 각 상황에 맞는 애니메이션을 만들어볼 것이다.

0 ~ 260 프레임 길이의 애니메이션이며 팔을 흔드는 모션을 취하는 애니메이션이다.

 

  • 👉 팔의 3D 모델 파일 애니메이션에서 0 ~ 24 범위의 프레임을 떼 옴
  • Loop Time 체크
    • 반복 재생
    • 정지해 있는 팔 애니메이션이 반복해서 재생되도록
  • Loop Pose 체크
    • 자연스럽고 부드럽게 반복 재생

위와 같은 방법으로 0 ~ 260 길이의 팔의 3 D 모델의 애니메이션 하나로부터 프레임 구간을 나눠 다음과 같이 8 개의 애니메이션 파일들을 만들었다. 설정한 후 Apply.

 

  • Hand_Idle : 정지해있을 때
    • 0 ~ 24 프레임
    • 반복 실행해야 하므로 Loop Time 체크, Loop Pose 체크
  • Handle_Run : 달릴 때
    • 30 ~ 40 프레임
    • 반복 실행해야 하므로 Loop Time 체크, Loop Pose 체크
  • Handle_Walk : 걸을 때
    • 60 ~ 84 프레임
    • 반복 실행해야 하므로 Loop Time 체크, Loop Pose 체크
  • Handle_Attack : 공격할 때
    • 90 ~ 115 프레임
    • 반복할 필요는 없다. 한번만 공격 하므로.
  • Handle_Work : 일할 때
    • 120 ~ 145 프레임
    • 반복할 필요는 없다. 한번만 공격 하므로.
  • Handle_Weapon_Out : 무기를 꺼내기 위해서 팔을 내릴 때
    • 210 ~ 220 프레임
    • 반복할 필요는 없다. 한번만 공격 하므로.
  • Handle_Weapon_In : 무기를 꺼냈을 때
    • 220 ~ 230 프레임
    • 반복할 필요는 없다. 한번만 공격 하므로.
  • Handle_Item : 아이템 주울 때
    • 240 ~ 255 프레임
    • 반복할 필요는 없다. 한번만 공격 하므로.

 

애니메이션 컨트롤러

 

📂Assets/Animations/Hand 폴더를 만들어서 위와 같이 팔의 애니메이션을 제어할 애니메이션 컨트롤러를 생성한다. 이름은 “Hand Animatior”

이를 Hand의 Animator 컴포넌트의 Controller에 할당한다.

  • 파라미터 6 개를 만들어 주었다.
    • 네모는 Boolean
      • Walk, Run
      • 한번 걷거나 뛰면 조건이 바뀔 때까지 계속 재생을 유지 하므로 bool 이 적합
    • 동그라미는 Trigger
      • Item_Pickup, Weapon_Out, Attack, Work
      • 한번만 재생할 것들이기 때문에 Trigger가 적합.
  • Bool 과 Trigger 의 차이
    • True, False 두개의 값을 가진다는 면에서 비슷하다.
    • Bool은 한번 값이 바뀌면 다시 수동적으로 바꿔주지 않는 한 그 값을 유지하지만
    • Trigger는 True 가 됐다면 곧 바로 다시 False 로 돌아온다

팔 3D 모델에 내장되어 있던 애니메이션 하나를 쪼개서 만들었던 8 가지 애니메이션 파일들이 팔 3D 모델의 자식으로 들어가 있다. 팔 3D 모델의 재생 버튼을 클릭하면 위와 같이 자식 에셋들을 확인할 수 있다. 이 8 가지 애니메이션 파일들을 상태도에 드래그 앤 드롭 해주자.

  • 디폴트 애니메이션은 Handle_Weapon_In
    • 우클 - Set as Layer Default State
    • 게임이 시작될 때처럼 부모인 팔 모델이 활성화 될 때 가장 먼저 실행될 애니메이션.
      • 손을 꺼내는 동작으로 시작할거라서

  • 두 애니메이션의 순서를 나타내는 트랜지션 만들기
    • 우클 - Make Transition
  • Handle_WeaPon_In이 끝나면 Handle_Idle을 재생한다.
    • Has Exit Time 을 체크한다.
      • 이게 체크되어 있으면 종료 시점을 활성화 한다는 뜻으로, 다음 트랜지션을 발동시킬 조건이 만족하더라도 무조건 해당 애니메이션을 종료 시점까지 재생한 후 다음 트랜지션으로 넘어간다는 뜻이다.
    • Exit Time 을 1 로 설정한다.
      • 이 의미는 Handle_WeaPon_In를 끝까지 다 100퍼센트 재생한 후 Handle_Idle을 재생하겠다는 의미다. 초가 아닌 비율이다.
      • 만약 0.5 라면 Handle_WeaPon_In의 절반만 재생한 후 Handle_Idle 재생하러 감.
    • Transition Duration 을 0 으로 설정한다.
      • 전이가 일어나는데에 두는 지연시간이다.
      • 지연시간 없이 바로 Handle_WeaPon_In가 끝나면 Handle_Idle를 재생한다.

  • Hand_Idle 👉 Hand_Walk
    • 전이 조건 Condition
      • Walk 파라미터 값이 True 일 때 발동
    • Has Exit Time은 체크 해제한다.
      • 정지해있다가 걸으려면 바로 걸어야 하므로 정지 애니메이션이 끝날 때까지 기다릴 필요가 없다.
    • Transition Duration 을 0.1 으로 설정하여 곧바로 바로 걷기 애니메이션이 재생되는 것이 아닌 0.1초 텀을 두어 자연스럽게 이어지도록 했다.
  • Hand_Idle 👉 Hand_Run
    • 전이 조건 Condition
      • Run 파라미터 값이 True 일 때 발동
    • 나머지 설정 똑같이
  • Handle_Walk 👉 Handle_Idle
    • 전이 조건 Condition
      • Walk 파라미터 값이 False 일 때 발동
    • 나머지 설정 똑같이
  • Handle_Run 👉 Handle_Idle
    • 전이 조건 Condition
      • Run 파라미터 값이 False 일 때 발동
    • 나머지 설정 똑같이
  • Handle_Walk 👉 Handle_Run
    • 전이 조건 Condition
      • Run 파라미터 값이 True 일 때 발동
    • 나머지 설정 똑같이
    • Run 👉 Walk 를 만들지 않은 이유
      • 걷게 된다는건 더 이상 Left Shift를 누르고 있지 않은 채로 WASD 키만 누르게 된다는 것이다. 즉, Run도, Walk도 둘 다 True인 상태에서 Run 만 False가 되는 것이다. 자연스럽게 Idle 상태에 오게 되고, 이어서 자연스럽게 곧 바로 Idle 에서 Walk 로 전이될 수 있다. Walk 가 True 라는 조건이 만족 되므로 Idle이 재생되지 않고 바로 Walk로 전이될 수 있다. (Has Exit Time 해제해서)
    • Walk 👉 Run 트랜지션도 Idle을 중간에 거치면 되니까 만들 필요 없는것 아닌가?
      • 걷다가 뛴다는 것은 WASD 키 입력으로 Walk만 True인 상태에서 Left SHift + WASD 를 같이 누른다는거니까 Run도 Walk도 둘 다 True가 된다는 것인데, 이 과정에서 Walk 👉 Idle 이 될 수 있는 조건은 없다. 달리려면 걷는 동작인 WASD 도 같이 쉬프트키와 함께 입력 되야 하기 때문에 Walk도 True여야 하기 때문이다.
        • 직접 걷다가 WASD 키를 떼고 Idle 이 한번 된 후 달리는 수 밖엔 없다.
      • 따라서 직접 바로 Walk 상태에서 Run으로 전이될 수 있도록 트랜지션을 따로 만들어준 것이다.
      • 이게 맞는진 모르겠지만 이렇게 이해했다. 헷갈려 ㅠ ㅠ

  • AnyState 👉 Hand_Attack
    • 어느 상태를 재생중이던 간에 Attack Trigger가 발동되면 Hand_Attack을 재생한다.
      • 전이 조건 Condition
        • Attack가 발동될 때.
      • Has Exit Time은 체크 해제한다. 어떤 상태이던간에 공격을 해야 하면 바로 재생을 끊고 공격 애니메이션을 재생할 수 있도록!
      • Transition Duration 을 0.1 으로 설정한다.
  • Hand_Attack 👉 Idle
    • 전이 조건은 없다.
    • Has Exit Time은 체크 한다.
      • 그냥 별다른 전이 조건 없이 공격 애니메이션 다 끝나면 Idle 상태로 되돌아 오도록 한다.
    • Exit Time 은 1 로 설정한다.
      • 공격 애니메이션이 끝까지 재생을 완료 한 후 Idle 이 될 수 있도록
    • Transition Duration 을 0.1 으로 설정한다.

나머지 애니메이션 파일은 추후 설정

 

📜Hand.cs

Hand 오브젝트에 붙여준다.

손이 가지는 기능과 속성들

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Hand : MonoBehaviour
{
    public string handName;  // 너클이나 맨 손 구분. 이름으로 구분할 것이다.
    public float range; // 공격 범위. 팔을 뻗으면 어디까지 공격이 닿을지
    public int damage; // 공격력
    public float workSpeed; // 작업 속도
    public float attackDelay;  // 공격 딜레이. 마우스 클릭하는 순간 마다 공격할 순 없으므로.
    public float attackDelayA;  // 공격 활성화 시점. 공격 애니메이션 중에서 주먹이 다 뻗어졌을 때 부터 공격 데미지가 들어가야 한다.
    public float attackDelayB;  // 공격 비활성화 시점. 이제 다 때리고 주먹을 빼는 애니메이션이 시작되면 공격 데미지가 들어가면 안된다.

    public Animator anim;  // 애니메이터 컴포넌트
    // public BoxCollider boxCollider; 
}
  • 데미지가 입혀지는 범위인 boxCollider를 주먹에 달지 않고, 데미지를 입히는 범위를 화면 중앙 기준으로 잡을 것이다.
    • boxCollider를 주먹에 달면 1인칭 시점과 주먹에 달린 boxCollider의 위치가 서로 다르게 보일 수 있어서 문제가 생길 수 있다. 안 맞았는데 맞았다고 처리되는 등등.. 3인칭이였다면 주먹에 boxCollider 달아도 됐었다.
    • 따라서 boxCollider은 사용하지 않을거라 주석 처리 함.

공격이 시작되고 0.5 초 후에 데미지가 들어가며 그로부터 0.1 초 후부턴 데미지가 들어가지 않음

 

📜HandController.cs

Hand Holder 오브젝트에 붙여준다.

모든 무기들의 공통된 부분

무기들 마다 각각 Controller 스크립트를 만드는건 귀찮은 일. 모든 Hand형 무기들은 애니메이션과 공격 범위 같은 것이 같으므로 그와 관련된 것들을 이 스크립트에 작성한다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class HandController : MonoBehaviour
{
    [SerializeField]
    private Hand currentHand; // 현재 장착된 Hand 형 타입 무기

    private bool isAttack = false;  // 현재 공격 중인지 
    private bool isSwing = false;  // 팔을 휘두르는 중인지. isSwing = True 일 때만 데미지를 적용할 것이다.

    private RaycastHit hitInfo;  // 현재 무기(Hand)에 닿은 것들의 정보.
    
    void Update()
    {
        TryAttack();
    }

    private void TryAttack()
    {
        if(Input.GetButton("Fire1"))
        {
            if(!isAttack)
            {
                StartCoroutine(AttackCoroutine());
            }
        }
    }

    IEnumerator AttackCoroutine()
    {
        isAttack = true;
        currentHand.anim.SetTrigger("Attack");

        yield return new WaitForSeconds(currentHand.attackDelayA);
        isSwing = true;

        StartCoroutine(HitCoroutine());

        yield return new WaitForSeconds(currentHand.attackDelayB);
        isSwing = false;

        yield return new WaitForSeconds(currentHand.attackDelay - currentHand.attackDelayA - currentHand.attackDelayB);
        isAttack = false;
    }

    IEnumerator HitCoroutine()
    {
        while (isSwing)
        {
            if(CheckObject())
            {
                isSwing = false;
                Debug.Log(hitInfo.transform.name);
            }
            yield return null;
        }
    }

    private bool CheckObject()
    {
        if(Physics.Raycast(transform.position, transform.forward, out hitInfo, currentHand.range))
        {
            return true;
        }
       
        return false;
    }
}

  • Edit - Project Setting - Input
    • “Fire1”에 마우스 좌클릭(Mouse 0) 뿐만 아니라 Left Ctrl 도 할당이 되어 있어서 이를 지워 주었다. 이 게임에선 Left Ctrl 키를 앉은 키로 쓰기 때문에
  • TryAttack()
    • 매 프레임마다 좌클릭 입력이 들어오는지를 검사하고 좌클릭 입력시 공격의 과정을 처리하는 코루틴 함수인 AttackCoroutine() 함수를 실행한다.
  • AttackCoroutine()
    • 좌클릭 입력시 실행된다.
    • isAttack을 True 로 설정하고 공격 처리 과정을 시작한다.
      • “Attack” 트리거를 발동시켜 공격 애니메이션을 재생하게 한다. Any State 👉 Hand_Attack 전이 됨.
        • 📜Hand.cs 의 attackDelayA 시간 동안 대기 한다. (대략 공격 애니메이션에서 팔을 뻗는 데가 재생되는데까지 걸리는 시간)
        • isSwing을 True 로 설정한다. 👉 데미지가 들어가기 시작
          • HitCoroutine() 코루틴 함수를 실행하여 Raycast에 충돌 된 오브젝트가 있는지 while문으로 계속 검사하는 일을 하자.
          • 충돌한 오브젝트를 찾아 HitCoroutine() 코루틴 함수가 끝나면 isSwing이 false가 된 상태일 것이다.
        • 📜Hand.cs 의 attackDelayB 시간 동안 추가로 대기 한다. (대략 공격 애니메이션에서 팔을 다 뻗고 이제 집어넣는데 걸리는 시간)
        • isSwing을 False 로 설정한다. 👉 이제 데미지가 더 이상 들어가지 않음
        • 📜Hand.cs 의 attackDelay -attackDelayA - attackDelayB 즉, 남은 시간동안 추가로 대기 한다.
    • isAttack을 False 로 설정하고 공격 처리 과정을 마친다.
  • HitCoroutine()
    • 한프레임씩 쉬면서 CheckObject() 함수를 실행하여 이 스크립트가 붙을 Hand Holder의 위치로부터 쏜 Raycast에 충돌한 오브젝트가 있는지 무한 반복하여 검사한다.
    • 충돌한 오브젝트를 찾았다면 isSwing을 False로 만들고 while문을 마치고 함수도 끝낸다.
    • 일단 추후 여기에 데미지 처리를 할 것이다. 일단 Debug.Log(hitInfo.transform.name) 충돌한 물체의 이름을 출력하기로.
  • CheckObject()
    • 이 스크립트가 붙을 Hand Holder의 위치로부터 앞 방향으로 📜Hand.cs 의 range 길이로 쏜 Raycast에 충돌한 오브젝트가 있다면 그 정보를 hitInfo에 담고 True를 리턴한다.
    • 충돌한 오브젝트가 없다면 False를 리턴한다.

Hand를 CurrentHand에 할당.

 

🙋‍♀️ 그 밖의 개선

팔이 카메라를 따라오게끔

위아래로 고개를 흔들 때 팔은 따라오지 않아 부자연스럽다. 팔도 카메라를 따라 회전해야 한다. 따라서 Hand Holder를 Main Camera의 자식으로 넣어 카메라를 따라다니게 해주었다.

 

팔을 뻗으면 팔 밑부분이 잘리고, 나무에 가까이가면 팔이 파묻히는 현상Permalink

오브젝트에 팔이 닿으면, 팔이 파묻히는 현상이 일어난다.

마우스 좌클로 공격 애니메이션이 재생되면 위와 같이 팔 아랫부분이 잘리는 현상이 일어난다.

 

이를 해결하기 위해 서브 카메라 하나 더 생성 !

Weapon Camera라는 이름의 카메라를 만든다. 서브 카메라인 셈이다. 이 카메라를 Main Camera의 자식으로 넣어어 Main Camera를 따라다니게 한다.

  • 카메라에 기본으로 달려있는 Audio Listner 컴포넌트는 한 Scene에 하나만 존재해야 하기 때문에, Main Camera 하나에만 있으면 된다 따라서 Weapon Camera의 Audio Listner 컴포넌트는 지워주도록 하자.

해결 방법

  • 서브 카메라는 오직 팔만 찍고, 메인 카메라는 팔을 제외한 모든 것들을 찍게 한다.
    • 이 상태에서 서브 카메라의 depth 값을 메인 카메라 depth 값보다 높게 지정하여 서브 카메라가 메인 카메라보다 우선하게 한다. 즉, 서브카메라의 렌더링을 메인 카메라보다 나중에 한다.
      • depth 값이 낮은 카메라일 수록 먼저 렌더링 된다. depth 값이 높은 카메라일 수록 제일 나중에 렌더링 되므로 가장 나중에 덮어 씌우는 격이 된다. 더 우선시 되는 꼴.
      • 팔을 찍는 서브 카메라를 렌더링시 더 우선하게 되므로 팔이 오브젝트에 파묻히는 현상 해결!
    • 팔을 휘두를때 팔 밑부분이 잘리는 현상은 카메라가 렌더링을 시작하는 지점이 카메라로부터 멀기 때문에 나타나는 현상이다. 따라서 이 지점을 카메라 위치와 최대한 가깝게 해주어 팔의 밑 부분까지 다 렌더링 되도록 한다.
      • 팔 아랫부분이 잘리는 현상 해결!

 

우선 “Weapon” 이라는 이름의 Layer를 추가한 후, Hand의 Layer를 “Weapon”으로 지정한다. 서브 카메라인 Weapon Camera가 오직 Hand만 찍게 할 것이기 때문에 Layer를 달아 구분해주는 것이 필요하기 때문이다.

 

  • Main Camera 설정
    • Culling Mask 👉 카메라로 찍을, 즉 렌더링 할 레이어들을 선택한다.
      • 렌더링 할 레이어들에서 “Weapon”이 체크 해제 되어 있는 것을 알 수 있다.
      • 즉, 팔은 찍지 않고 팔을 제외한 모든 것을 찍는다.
        • 아래 사진은 Main Camera가 찍는 화면이다. 팔을 렌더링에서 제외하는 것을 볼 수 있다.
    • Depth 👉 해당 카메라가 그려지는 순서.
      • -1 값인 것을 확인할 수 있다.
      • depth 값이 낮은 카메라일 수록 먼저 렌더링 된다. depth 값이 높은 카메라일 수록 제일 나중에 렌더링 되므로 가장 나중에 덮어 씌우는 격이 된다. 더 우선시 되는 꼴.
        • 메인 카메라는 기본 디폴트 Depth 값이 -1로서 주로 가장 먼저, 또 가장 밑에서 렌더링 된다는 것을 의미한다.

  • Weapon Camera 설정
    • Clear Flags 👉 Depth Only 로 설정했다.
      • 즉, 찍는 대상(레이어)를 제외한 나머지 빈 공간은 투명 처리한다.
      • 종류
        • SkyBox
          • 스카이 박스라 불리는 배경으로 빈 공간을 채운다.
        • Solid Color
          • 단색으로 빈 공간을 채운다.
        • Depth Only
          • 빈 공간을 투명 처리한다.
            • 이와 같은 특성 때문에 지금과 같이 카메라 2 대 쓸 때와 같이 여러 대의 카메라를 한 화면에 합성할 때 주로 사용한다.
            • 각 여러 카메라들의 영상들을 합성하기 위해선 빈 공간은 투명해야 하기 때문이다.
            • 유니티는 여러대의 카메라가 있으면 depth 값이 낮은 카메라부터 먼저 렌더링 하여 차곡 차곡 쌓아서 그린다. 즉, depth 값이 낮은 카메라가 가장 먼저 그려지고 그 위에 depth 값이 높은 카메라가 덮어 씌워져 그려지게 된다.
        • Don’t Clear
          • 화면을 새로 갱신하지 않고 이전의 화면 위에다가 다시 그린다. 때문에 오브젝트가 움직이면 잔상이 화면에 남는다.
    • Culling Mask 👉 “Weapon”
      • 즉, 오로지 “Weapon” Layer가 설정되어 있는 오브젝트만 찍는다.
      • 따라서 팔만 찍게 된다.
        • 아래 사진은 Weapon Camera가 찍는 화면이다. 팔만 찍고 나머지 빈 공간은 투명 처리 된 것을 볼 수 있다.
    • Clipping Planes 👉 Near 의 값을 최소 값이 0.01로 해주었다.
      • 카메라는 모든 공간을 다 렌더링 하지 않는다. Near 평면과 Far 평면 사이에 해당하는 3 차원 공간만 렌더링 하여 카메라 화면에 담는다.
      • Near 값을 최소값인 0.01로 설정하여 최대한 카메라와 가까운 거리도 다 렌더링 될 수 있도록 하였다.
        • 팔을 뻗을 때 팔 아랫부분이 렌더링 되지 않고 잘렸던 이유는 Near 값이 높아 카메라가 렌더링 하는, 즉 찍는 시작점이 카메라로부터 멀었기 때문이다. Near 값을 줄여 카메라 바로 앞 거리도 렌더링 하게 됨으로써 팔 아랫부분이 렌더링 되지 않는 현상을 해결하였다.
    • Depth 👉 0
    • -1 인 Main Camera 보다 depth 값이 높아 더 나중에 렌더링 된다. 즉, 제일 상위에서 덮어 씌워지게 되므로 팔이 더 이상 파묻히지 않게 된다.
      • Weapon Camera가 찍는 화면인 아래 사진이 Main Camera가 찍는 화면 위에 그려지게 된다. Depth 값이 0 으로 더 높기 때문에 더 나중에 그려지기 때문이다!

Depth 값이 가장 낮은 Main Camera 가 촬영하는 화면이 먼저 렌더링 되고, 그 위에 Weapon Camera 가 촬영 하는 화면인 팔이 덮어 씌워져 렌더링 되어 최종적인 카메라 화면이 완성된다.

 

충돌 처리가 제대로 안되는 이유

    private bool CheckObject()
    {
        if(Physics.Raycast(transform.position, transform.forward, out hitInfo, currentHand.range))
        {
            return true;
        }
       
        return false;
    }

📜HandController.cs 에서 이 스크립트가 붙여진 HandHolder로부터 앞 쪽 방향 (Z축 positive 방향)으로 Raycast를 쏘기로 했었다. 그러나 게임을 실행해서 Collider가 이미 기본으로 붙어있는 Terrain 지형을 때려도 아무 반응이 일어나지 않는다.

그 이유는, 이전에 HandHolder을 Y 축으로 90 도 회전했던 탓에 HandHolder의 Z 축도 같이 우측으로 90 도 회전해버렸기 때문이다. 앞 방향(forward)는 Z 축의 Positive 방향을 뜻하는데, 서로 Z 축이 달라서 아무리 Player로서 앞을 보고 땅을 때려도 HandHolder 입장에선 그 방향은 오른쪽이였기 때문에 그 방향으로는 Raycast가 나가지 않아 충돌처리가 일어나지 않았기 때문에 발생한 문제다.

빈 오브젝트 Holder를 하나 더 추가하여 이의 자식으로 HandHolder를 넣어준다. Holder는 트랜스폼 Position, Rotation 모두 원점을 유지하고 트랜스폼 값 변경은 Handle Holder에서 담당하게 한다. 그리고 📜HandControllder.cs를 Handle Holder가 아닌 Holder에 붙여 주면 문제가 해결 된다. Holder는 90도로 회전하지 않으며 90도로 회전하는 것은 자식인 Handle Holder가 대신하기 때문이다. 이제 📜HandControllder.cs가 붙은 Holder의 Z 축과 Player의 Z 축이 일치하게 되므로 문제가 해결 된다.

Player 자신 또한 캡슐 콜라이더가 있기 때문에 자기 자신과의 충돌 처리가 가능하다. 따라서 Player의 Layer를 “Ignore Raycast” 로 설정하여 Raycast 에 의한 충돌처리가 되지 않도록 해준다. 자식들까지 모두 적용한다. 단, 자식들까지 모두 적용하니 Hand의 Layer를 다시금 “Weapon”으로 변경해준다.