Post

유니티3D - 3

유니티3D - 3

이번 목표

총 히트스캔 구현 , 히트스캔과 비슷한 목적지에 총알발사 , 줌자세 추가 (줌상태에서만 총알 발사)

줌자세(Aiming) 애니메이션

줌 애니메이션은 Idle 형태만 있어 상체 분리 블렌딩이 필요했다. 언리얼의 Layered Blend Per Bone 에 대응해 유니티에서는 Avatar Mask + Animation Layer 조합을 사용했다. Animator에 Upper 레이어를 추가하고, Upper Mask를 적용해 상체만 가중치로 제어한다.

일단 WeaponComponent에 L_Attack() 함수를 실행할 때 이벤트를 호출하고 Animcontroller에서 바인딩하여 사용하려고 한다.
이 방법도 유지보수를 생각해 델리게이트를 활용했다.

1
2
3
4
5
6
7
8
9
10
public class WeaponComponent : MonoBehaviour
{
    public event Action OnFireEvent;

    public void L_Attack()
    {
        CurrentWeapon.fire();
        OnFireEvent.Invoke();
    }
}

이것을 구현하다가 내 Player구조가 헷갈려서 정리해 보았다.

에이밍을 할 때 두번째 UpperLayer에 가중치를 1 안할 때 0으로 애니메이션을 조절을 했다. 그리고 에이밍 파라미터로 BaseLayer에서 또 앞을 응시하는 애니메이션트리로 재생하고 이동방향으로 캐릭터 회전을 꺼야하는 기능을 구현할 것이다.

  • 가중치 조절 코드
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
     // if(animator.GetCurrentAnimatorStateInfo(1).normalizedTime > 0.5f) 이것은 2번째레이어의 애니메이션 절반 진행이다.
    
      void OnAiming()
      {
          animator.SetLayerWeight(1, 1);
      }
    
          void OffAiming()
      {
          animator.SetLayerWeight(1, 0);
      }
    

    바로 가중치를 0↔1로 바꾸면 끊김이 발생해, 목표치로 보간하는 방식으로 전환해 자연스럽게 만들었다.

1
2
  currentWeight = Mathf.MoveTowards(currentWeight, targetWeight, Time.deltaTime * 5);
  animator.SetLayerWeight(1, currentWeight);

에이밍중에는 캐릭터에 방향을 받고 그 이동방향을 Direction 변수에 넣어 애님컨트롤러에 넘겨주어 애니메이션에 반영할 것이고 카메라를 회전할 때 전방 방향으로 캐릭터도 회전시켜주는 코드를 추가해주었다.
캐릭터 이동코드에 좀 추가를 하고 바꾸었는데 코드가 한눈에 안들어와 정리를 했다 주석 설명으로 함수화 하는게 더 좋긴할 것이다.

  • 구현 코드
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    
     private void Update()
     {
         maxSpeed = run ? 10f : 5f;
    
         // 에이밍 중 카메라 포워드 벡터 방향으로 캐릭터를 회전 시킨다.
         if (weapon.IsAiming())
         {
             Quaternion targetRotation = Quaternion.Euler(0, Camera.transform.eulerAngles.y, 0);
             transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, turnSpeed * Time.deltaTime);
         }
           
           
         // 카메라 기준으로 x, z 축 이동 방향을 정한다.
         Vector3 moveDir = Camera.transform.right * moveInput.x + Camera.transform.forward * moveInput.y;
         moveDir.y = 0f;
         moveDir.Normalize();
    
         // 키입력을 했을경우
         if (moveDir.magnitude > 0.1f)
         {
             // 캐릭터의 이동속도에 따라 방향벡터를 짧고 길게 만들어줌
             horizontalVelocity = Vector3.MoveTowards(horizontalVelocity, moveDir * maxSpeed, acceleration * Time.deltaTime);
    
             // 에이밍을 하지 않을 때 캐릭터를 이동방향으로 회전시켜줌
             if (!weapon.IsAiming())
             {
                 Quaternion toRotation = Quaternion.LookRotation(moveDir, Vector3.up);
                 transform.rotation = Quaternion.Lerp(transform.rotation, toRotation, turnSpeed * Time.deltaTime);
             }
         }
         else
         {
             horizontalVelocity = Vector3.MoveTowards(horizontalVelocity, Vector3.zero, deceleration * Time.deltaTime);
         }
           
    
         // 중력 및 점프 처리
         if (controller.isGrounded)
         {
             if (verticalVelocity < 0f)
                 verticalVelocity = -1f; 
         }
         else
         {
             verticalVelocity -= gravity * Time.deltaTime;
         }
    
         // 현재 스피드를 애니메이터에 반영할 변수에 전달
         animSpeed = horizontalVelocity.magnitude;
    
         // 현재 이동 방향을 전달할 변수를 구하는 과정
         {
             moveDir = horizontalVelocity.normalized;
             Vector3 cameraForward = Camera.transform.forward;
             cameraForward.y = 0f;
             cameraForward.Normalize();
    
             direction = Vector3.SignedAngle(cameraForward, moveDir, Vector3.up);
         }
    
         // 최종 이동 방향 Y축 포함한 벡터를 만든다.
         Vector3 totalVelocity = horizontalVelocity;
         totalVelocity.y = verticalVelocity;
    
         // 실제 캐릭터를 벡터의 방향과 크기만큼 이동시키는 코드
         controller.Move(totalVelocity * Time.deltaTime);
     }
    

    Animator 베이스 레이어는 2D 블렌드 트리로 속도/방향을 받아 상하좌우 이동을 구성했다. 2d 블렌더 설정

상하좌우이동

음 구동은 잘된다 하지만 역시 삐걱거리는게 있다 저건아마 180도 방향회전일 때 좀더 자연스럽게 넘어가야하는 부분이 있어야할 거 같다. 아마 감속과 관련이 있어 보인다. 그리고 캐릭터 애니메이션이 각도가 정면이 아닌데 저것도 수정해야 좀 이쁠거 같다.

총알 방향과 히스트캔

일단 GunWeapon의 Fire 함수를 수정하여 Ray를 카메라의 전방방향으로 쏘고 히트 포인트 지점을 가져왔다.
그 후에 히트포인트 지점에 이펙트를 생성했다. ( 이상하긴한데 이펙트는 내가 그냥 대충 만들었다 )
히트포인트 - 총구포지션을 해서 총알이 날라갈 방향을 지정해줘 우리의 에이밍한 목표지점에 가깝게 총알이 날라가 보이게끔 만들었다.
그리고 프로젝타일에서 포워드벡터로 쭉 날라가게끔 되어있기 때문에 날라가는 방향으로 또 회전을 시켜주었다.
하지만 총알이 세워져서 나가는데 회전값을 바꾸어도 스폰할 때 바뀌기 때문에 메쉬용으로 빈오브젝트를 하나 만들어 자식오브젝트로 처리했다.

  • 구현 코드 ```c# using UnityEngine;

[System.Serializable] public struct GunData { public float delayTime; public int maxBullet; public bool auto; public GameObject bullet; public Transform muzzlePoint; public ParticleSystem effect; }

public abstract class GunWeapon : MonoBehaviour { public GunData data;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
float MaxDistance = 100f;

public abstract void InitSetting();

public virtual void fire()
{
    // 카메라 기준 Ray 쏘기
    Camera cam = Camera.main;
    Vector3 origin = cam.transform.position;
    Vector3 direction = cam.transform.forward;
    Vector3 hitPoint = origin + direction * MaxDistance;

    if (Physics.Raycast(origin, direction, out RaycastHit hit, MaxDistance))
    {
        hitPoint = hit.point;
        Debug.DrawRay(origin, direction * hit.distance, Color.green, 1f);
        Instantiate(data.effect, hitPoint, Quaternion.identity);
        // TODO: hit.collider.SendMessage("TakeDamage", damage)
    }
    else
    {
        Debug.DrawRay(origin, direction * 100f, Color.red, 1f);
    }

    // 총구 기준 방향 계산
    Vector3 muzzlePos = data.muzzlePoint.position;
    Vector3 fireDir = (hitPoint - muzzlePos).normalized;

    // 총알 생성 및 방향 세팅
    GameObject bulletObj = Instantiate(data.bullet, muzzlePos, Quaternion.LookRotation(fireDir));
} } ```

시현 영상

동작은 정상이나 일부 각도에서 회전 감속과 캐릭터 정면 정합이 어색하다. TODO로 감속/회전 튜닝을 남겨 후속에서 다룬다.

TODO

감속/회전 보간 값 튜닝, 정면 정렬 정밀도 보강

This post is licensed under CC BY 4.0 by the author.