Carrot
본문 바로가기
Unity/멋쟁이사자처럼 부트캠프

[멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(97일차) - [3D 게임] Enemy 사망 효과 / Character Joint, Regdoll 활용

by 독기품은토끼 2025. 10. 17.
✅ 오늘의 학습 목표
1. Enemy 사망 효과 처리
- Character Joint
- Ragdoll
- MaterialPropertyBlock
2. Hp bar 수정

1. Character Joint

Character Joint는 Unity의 물리 기반 조인트 중 하나로

두 개의 Rigidbody를 관절처럼 연결해서 회전 운동을 제약하는 데 사용한다.

주로 팔·다리 같은 뼈대 구조, 물리 인형(Ragdoll), 물리적으로 움직이는 체인이나 로프, 기계적인 arm 구조 등에 쓰인다.

 

Connected Body

  • 연결할 다른 Rigidbody
  • 팔에 Character Joint를 붙이고 Connected Body에 몸통의 Rigidbody를 넣으면 팔이 몸에 붙은 관절처럼 움직임

Root 오브젝트에 Rigidbody 컴포넌트 추가

 

좌우앞뒤 발에 동일한 작업 진행

 

1. Ragdoll

 

랙돌은 게임에서 캐릭터가 죽거나 힘을 잃었을 때

애니메이션 대신 물리 법칙에 따라 몸이 쓰러지고 흔들리는 효과를 구현한 것이다.

캐릭터 본(Bone) 구조에 Rigidbody, Collider, Character Joint 등을 조합해서 캐릭터가 죽었을 때 자연스럽게 동작할 수 있도록 도와준다.

 

🚨 랙돌 구현 시 주의할 점

항목 주의 사항
Collider 겹침 처음 세팅할 때 Collider가 서로 겹쳐 있으면 폭발하듯 튕겨나감
질량 밸런스 몸통을 가장 무겁게, 팔다리는 가볍게 설정해야 자연스러움
Joint 제한값 너무 빡빡하면 부자연스럽고, 너무 느슨하면 팔·다리가 이상하게 꺾임
Animator 충돌 Ragdoll 상태로 전환할 때 Animator를 꺼주지 않으면 물리와 애니메이션이 충돌함 (→ 흔들림, 떨림 발생)

 

public class EnemyController : MonoBehaviour
{
    [Header("Ragdoll")]
    [SerializeField] private Collider[] ragdollColliders;
    [SerializeField] private Rigidbody[] ragdollRigidbodies;
    [SerializeField] private CharacterJoint[] ragdollJoints;

    private void Awake()
    {
        // Ragdoll 비활성화
        SetRagdollEnabled(false);

        // ...
    }

    private void OnCollisionEnter(Collision other)
    {
        if (other.gameObject.CompareTag("Ground"))
        {
            Debug.Log("## Ground");
            SetRagdollEnabled(true);
        }
    }

    #region Ragdoll 관련 함수
    private void SetRagdollEnabled(bool isEnabled)
    {
        foreach (var ragdollCollider in ragdollColliders)
        {
            ragdollCollider.enabled = isEnabled;
        }

        foreach (var ragdollRigidbody in ragdollRigidbodies)
        {
            ragdollRigidbody.isKinematic = !isEnabled;
            ragdollRigidbody.detectCollisions = isEnabled;
        }

        _animator.enabled = !isEnabled;
        _collider.enabled = !isEnabled;
        _rigidbody.detectCollisions = !isEnabled;

        _animator.Rebind();
        _animator.Update(0f);
    }
    #endregion
}
옵션 설명
ragdollCollider.enabled 랙돌 충돌 부위의 Collider를 On/Off
ragdollRigidbody.isKinematic 물리 연산을 적용할지 여부 전환
_animator.enabled Animator를 꺼서 물리 상태와 애니메이션 충돌 방지
_collider.enabled / _rigidbody.detectCollisions 본체의 충돌 기능을 꺼서 래그돌 부위만 충돌 처리
_animator.Rebind() / Update(0) 랙돌에서 애니메이션 상태로 돌아올 때 본 위치를 리셋

 

1. 랙돌 콜라이더 On/Off

foreach (var ragdollCollider in ragdollColliders)
{
    ragdollCollider.enabled = isEnabled;
}

랙돌용으로 부착한 콜라이더들을 활성/비활성 처리하여 충돌 처리를 전환

 

2. 랙돌 Rigidbody 설정

foreach (var ragdollRigidbody in ragdollRigidbodies)
{
    ragdollRigidbody.isKinematic = !isEnabled;
    ragdollRigidbody.detectCollisions = isEnabled;
}

랙돌을 켜면 → kinematic을 끄고, 물리 연산을 활성화해서 중력/충돌에 반응
랙돌을 끄면 → kinematic을 켜고, 충돌 감지를 끄며 애니메이션으로만 움직이게

 

3. 기본 Animator / Collider / Rigidbody 제어

_animator.enabled = !isEnabled;       // Animator 끄기 (래그돌 상태일 때 애니메이션과 물리 충돌 방지)
_collider.enabled = !isEnabled;       // 본체 Collider 비활성화 (중복 충돌 방지)
_rigidbody.detectCollisions = !isEnabled; // 본체 Rigidbody 충돌 감지 끄기 (래그돌이 대신 충돌 처리)

랙돌이 켜질 때는 Animator를 끄고, 본체의 Collider와 Rigidbody 충돌도 비활성화해서
랙돌 부위끼리만 물리 연산이 적용되게 함

 

4. Animator 상태 초기화

_animator.Rebind();  // 본/포즈를 초기 상태로 되돌림
_animator.Update(0f); // 0초짜리 프레임 강제 갱신

랙돌을 다시 끌 때 Animator의 본(Transform)들이 물리 연산으로 흐트러져 있으므로

Rebind()로 본래 포즈를 다시 불러오고 Update(0)으로 반영

 

콜라이더, 리지드바디, 조인트가 붙어있는 오브젝트들을 모두 배열에 담아주기

 

 

Enemy가 사망했을 때 물리적인 힘을 모두 빼서 바닥에 쓰러지기 위해서는 콜라이더가 정확히 몸에 맞아야한다.

그래서 Root에 캡슐 콜라이더를 몸에 맞게 추가해주었다.

 

2. 사망 효과

Enemy가 죽었을 때 Destory() 함수를 써서 오브젝트를 바로 사라지게 하는 방법이 있지만

좀 더 시각적인 연출을 활용해주기 위해 쉐이더 값을 조절해주려고 한다.

 

Chomper는 커스텀된 쉐이더를 사용하고 있는데 해당 쉐이더의 스크립트를 살펴보면 여러 속성들이 정의되어 있는 것을 살펴볼 수 있다.

 

우리는 이 중에서 _Cutoff 속성을 활용해서

Enemy가 죽었을 때 서서히 사라지는 효과를 주려고 한다.

 

❓ Cutoff가 무엇인가?

Standard/URP Lit 등에서 Alpha Clipping (컷아웃) 임계값으로 흔히 _Cutoff를 사용한다.

1 : 투명(알파 낮은) 픽셀 부터 잘라내기(clip/discard)가 많이 일어남

0 : 잘라내기가 적음

즉 Cutoff 값을 0에서 1로 바꾸어주면 오브젝트가 점점 사라지는 효과가 나타난다.

 

1. MaterialPropertyBlock

MaterialPropertyBlock은 Renderer 하나에만 적용되는 임시 머테리얼 속성 덮어쓰기 기능이다.

 

renderer.material로 값을 바꾸면 머테리얼 인스턴스가 복제되어 메모리 관련해서 비효율적이다. (개체수가 많을 수록 더)

MaterialPropertyBlock는 원본 머테리얼은 손대지 않고 특정 Renderer 하나에만 값(색, float, 텍스처 등)을 임시로 적용시킬 수 있다.

 

MaterialPropertyBlock을 활용하면

Enemy 개체가 많을 때 해당 Enemy들이 같은 머테리얼을 공유한다해도 각자 다른 값을 넣어줄 수도 있다.

public class EnemyController : MonoBehaviour
{
    [Header("Renderer")]
    [SerializeField] private Renderer enemyRenderer;
    
    private void OnCollisionEnter(Collision other)
    {
        if (other.gameObject.CompareTag("Ground"))
        {
            Debug.Log("## Ground");
            SetRagdollEnabled(true);
            StartCoroutine(Disolve());
        }
    }
    
    IEnumerator Disolve()
    {
        var propertyBlock = new MaterialPropertyBlock();
        enemyRenderer.GetPropertyBlock(propertyBlock);
        var value = 0f;

        while (value < 1f)
        {
            value += Time.deltaTime;
            propertyBlock.SetFloat("_Cutoff", value);
            enemyRenderer.SetPropertyBlock(propertyBlock);
            yield return null;
        }
    }
}

 

Cutoff의 값을 0에서 Time값에 따라 점점 1에 가까워지도록 While문을 작성해주었다.

따라서 Enemy가 사망했을때 서서히 사라지는 것을 확인할 수 있다.

 

3. HP Bar 수정

Enemy가 공격 받았을 때만 Hp바가 나타나도록 변경해주려고 한다.

public class HPBarController : MonoBehaviour
{
    private Coroutine _hideHPBarCoroutine;

    void Start()
    {
        // ...
        SetActiveHPBar(false);
    }

    public void SetHp(float hp)
    {
        _hpBar.SetHPGauge(hp);
        SetActiveHPBar(true);

        if (_hideHPBarCoroutine != null )
        {
            StopCoroutine(_hideHPBarCoroutine );
        }
        _hideHPBarCoroutine = StartCoroutine(HideHPBarAfterDelay(1f));
    }

    IEnumerator HideHPBarAfterDelay(float delay)
    {
        yield return new WaitForSeconds(delay);
        SetActiveHPBar(false);

        _hideHPBarCoroutine = null;
    }

    public void SetActiveHPBar(bool isActive)
    {
        _hpBar.gameObject.SetActive(isActive);
    }

    // Enemy의 움직임에 따라 캔버스도 위치 변경되도록 LateUpdate 활용
    void LateUpdate()
    {
        var screenPosition = _camera.WorldToScreenPoint(transform.position + _offset);

        // 카메라가 바라보는 시야에 있는 경우에만 HP Bar 보이도록 설정
        bool isVisible = screenPosition.z > 0
            && screenPosition.x > 0 && screenPosition.x < Screen.width
            && screenPosition.y > 0 && screenPosition.y < Screen.height;

        if (isVisible)
            _hpBarRectTransform.position = screenPosition;
        else
            SetActiveHPBar(false);
    }
}

 

1초동안만 Hp 바가 보이도록 코루틴을 생성해 주었고

HP바가 활성화/비활성화 되는 부분이 많기 때문에 SetActiveHPBar() 메서드를 생성해주었다.

 

 

촘퍼.. 마음 아파서 못 때리겠다.. ㅠ