내일배움캠프 38일차 TIL <Unity 게임개발 숙련 프로젝트 5일차> 05/30

정광훈(Unity_9기)·2025년 5월 30일

TIL (Today I Learned)

목록 보기
48/97
post-thumbnail

오늘 있었던 일

장애물이 자연스럽게 회전하면서 날아가게 하고 싶었다.
이렇게 세팅하고 실행한 결과

    void SpawnNextObstacle()
    {
        // 풀에서 비활성 장애물 꺼내기
        GameObject obstacle = obstaclePool.Dequeue();

        // ================================================== 추가 부분
        obstacle.transform.rotation = Quaternion.identity;
        
        Rigidbody rb = obstacle.GetComponent<Rigidbody>();
        if (rb != null)
        {
            rb. velocity = Vector3.zero;
            rb.angularVelocity = Vector3.zero;
        }
    
        ObstacleBehavior obstacleBehavior = obstacle.GetComponent<ObstacleBehavior>();
        if (obstacleBehavior != null)
        {
            obstacleBehavior.InitObstacle(); // 커스텀 초기화 함수 호출
        }
        
        // +============================================
        
        ... 아래 생략
    private bool isKnockback = false; // 넉백 중인가

    void FixedUpdate()
    {
        if (!isKnockback) // 넉백 중이 아닐 때
        {
            _rigidbody.velocity = Vector3.back * obstacleSpeed; // 장애물 이동방향과 속도 
        }
    }
    
     private void OnCollisionEnter(Collision other)
    {
        if (other.gameObject.CompareTag("Player")) // 플레이어와 충돌하면
        {
            // 충돌한 상대방 오브젝트에서 PlayerCondition이라는 스크립트 컴포넌트를 가져와라
            PlayerCondition playerCondition = other.gameObject.GetComponent<PlayerCondition>();
            if (playerCondition.IsInvincible == false) // 무적이 아닐 때
            {
                StartCoroutine(nameof(GameOver)); // 게임 오버
            }
            else // 무적일 때
            {
                isKnockback = true; // 넉백 중이다
                
                int direction = Random.Range(0, 2); // 장애물 날아가는 방향 정하기
                Vector3 selectedDirection;
                
                if (direction == 0)
                {
                    selectedDirection = Vector3.right; // 오른쪽 방향으로 날림
                }
                else 
                {
                    selectedDirection = Vector3.left; // 왼쪽 방향으로 날림
                }
                
                // 플레이어와 충돌하면 장애물이 대각선 방향으로 날아감
                Vector3 knockbackDirection = (selectedDirection + Vector3.up * 5f).normalized; 
                // Impulse 모드는 즉각적인 힘을 가함
               _rigidbody.AddForce(knockbackDirection * knockbackForce, ForceMode.Impulse);

               // -- 이 코드가 문제 / 장애물이 회전하면서 날아가도록 하는 로직-----
               Vector3 randomTorqueDirection = Random.insideUnitSphere;
               float torqueForce = 100f; // 힘이 너무 강했던 것 같다
               _rigidbody.AddTorque(randomTorqueDirection * torqueForce, ForceMode.Impulse);
               // ---------------------------------------------------
            }
        }
    }   

<문제>

장애물이 빠른 속도로 하늘로 솟구치고 재생성되지 않는 문제가 발생했다.

<원인>

장애물이 플레이어 뒤로 갔을 때 비활성화되어야 하는데
장애물이 앞과 위의 방향으로 날아가다보니
재생성되지 않는 것으로 파악하고 있다.

<해결>

이라기 보다는 타협
일단 기존대로하고
Vector3 knockbackDirection = (selectedDirection 5f + Vector3.up 5f + Vector3.back).normalized;
이 부분의 수치를 조절하는 것으로 함

   public void InitObstacle()
    {
        transform.rotation = Quaternion.Euler(0, 90f, 0);
        _rigidbody.velocity = Vector3.back * obstacleSpeed; // 장애물 이동방향과 속도
        _rigidbody.angularVelocity = Vector3.zero; // 회전각도 초기화
        // 장애물 모델링이 기본적으로 90도 틀어져있기 때문에 90도를 더 돌려 180도(정면)로 맞춰준다. 
    }

    // 플레이어와 장애물이 충돌하면 게임 정지
    private void OnCollisionEnter(Collision other)
    {
        if (other.gameObject.CompareTag("Player")) // 플레이어와 충돌하면
        {
            // 충돌한 상대방 오브젝트에서 PlayerCondition이라는 스크립트 컴포넌트를 가져와라
            PlayerCondition playerCondition = other.gameObject.GetComponent<PlayerCondition>();
            if (playerCondition.IsInvincible == false) // 무적이 아닐 때
            {
                StartCoroutine(nameof(GameOver)); // 게임 오버
            }
            else // 무적일 때
            {
                int direction = Random.Range(0, 2); // 장애물 날아가는 방향 정하기
                // 왼쪽 방향과 오른쪽 방향중 하나 랜덤으로 선택됨
                Vector3 selectedDirection = (direction == 0 ? Vector3.right : Vector3.left);

                // 플레이어와 충돌하면 장애물이 대각선 방향으로 날아감
                Vector3 knockbackDirection = (selectedDirection * 5f + Vector3.up * 5f + Vector3.back).normalized;
                // Impulse 모드는 즉각적인 힘을 가함
                _rigidbody.AddForce(knockbackDirection * knockbackForce, ForceMode.Impulse);
            }
        }
    }


<작업 코드>

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

// 플레이어 모델링의 색상을 바꾸는 스크립트 (무적 상태일 때)
public class InvincibleEffect : MonoBehaviour
{
    [Header("무적 시각 효과 설정")]
    [SerializeField] private Renderer characterRenderer; // 캐릭터 모델의 Renderer 컴포넌트를 연결
    public float intensity = 1f; // 발광 강도 (인스펙터에서 조절 가능)
    public float changeDuration = 1f; // 색상 / 강도가 변하는 시간

    private bool _isInvincible = false; // 무적 상태 인지 확인
    private Material[] _materials; // 여러 개의 material로 모델링이 구성되어 있어서 배열로 함

    // 각 material별 원래 Emission 색상 및 키워드 상태를 저장할 Dictionary
    private Dictionary<Material, Color> _initEmsissions;

    // 원래의 발광 색상 저장 및 초기화
    void Start()
    {
        // 이 스크립트에서 캐릭터에 적용된 모든 material을 가져와 materials배열에 저장
        _materials = characterRenderer.materials;
        _initEmsissions = new Dictionary<Material, Color>(); // Dictionary 초기화

        // material을 하나씩 가져옴
        for (int i = 0; i < _materials.Length; i++)
        {
            Material mat = _materials[i]; // 현재 [i]번째의 material을 mat에 할당

            // 현재 material이 "_EmissionColor"속성을 가지고 있는다면
            if (mat.HasProperty("_EmissionColor"))
            {
                // 원래 Emission 색상을 저장
                _initEmsissions[mat] = mat.GetColor("_EmissionColor");
                
                mat.SetColor("_EmissionColor", Color.black); // Emission 색상을 검은색(기본값)으로 설정
                mat.DisableKeyword("_EMISSION"); // Emission 키워드(발광 기능) 비활성화

                // _EMISSION: 유니티 Standard 쉐이더에서 발광 기능을 활성화/비활성화하는 데 사용되는 쉐이더 키워드
                // 인스펙터에서 Emission 체크박스를 켜고 끄는 것과 동일한 역할을 코드로 수행
            }
        }
    }

    // 캐릭터가 현재 무적 상태가 아니라면, 무적 상태로 전환하고
    // material에 발광 효과를 부드럽게 페이드 인(서서히 밝아지는)시키는 코루틴을 시작
    public void StartInvincible() // 무적 상태 시작
    {
        // 현재 무적 상태인지 확인 (중복 실행 방지)
        // 무적이면 메서드 종료
        if (_isInvincible) return;
        _isInvincible = true; // 무적으로 전환

        // materials 배열이 유효한지 확인 (null이거나 비어있는 경우를 대비한 안전 장치)
        if (_materials == null || _materials.Length == 0) return;

        // materials 배열에 있는 모든 재질을 하나씩 순회
        foreach (Material mat in _materials)
        {
            // 현재 material이 쉐이더가 발광(Emission) 속성(_EmissionColor)을 지원하는지 다시 확인
            if (mat.HasProperty("_EmissionColor"))
            {
                Color selectedEmissionColor = Color.black; // 기본값은 검은색

                // 각 material의 원래(인스펙터에서 설정된) Emission 색상을 가져옴
                if (_initEmsissions.ContainsKey(mat))
                {
                    selectedEmissionColor = _initEmsissions[mat];
                }

                // 발광 효과를 부드럽게 페이드 인(서서히 밝아지는) 시키는 코루틴 시작
                StartCoroutine(FadeEmission(mat, mat.GetColor("_EmissionColor"), selectedEmissionColor * intensity,
                    changeDuration, true));
            }
        }
    }

    // 캐릭터가 현재 무적 상태라면, 무적 상태를 해제하고
    // material의 발광 효과를 원래 색상으로 부드럽게 페이드 아웃(서서히 어두워지는)시키는 코루틴을 시작
    public void EndInvincible() // 무적 상태 종료
    {
        // 현재 무적 상태인지 확인 (중복 실행 방지)
        // 무적이 아니면 메서드 종료
        if (!_isInvincible) return;
        _isInvincible = false; // 무적 해제

        // materials 배열이 유효한지 확인 (null이거나 비어있는 경우를 대비한 안전 장치)
        if (_materials == null || _materials.Length == 0) return;

        // 'materials' 배열에 있는 모든 재질을 하나씩 순회
        foreach (Material mat in _materials)
        {
            // 현재 material이 쉐이더가 발광(Emission) 속성(_EmissionColor)을 지원하는지 다시 확인
            if (mat.HasProperty("_EmissionColor"))
            {
                Color originalColor = Color.black; // 기본값

                // 각 material의 원래 Emission 색상 (Start에서 저장된 것)을 가져옴
                if (_initEmsissions.ContainsKey(mat))
                {
                    originalColor = _initEmsissions[mat];
                }

                // 발광 효과를 부드럽게 페이드 아웃(서서히 어두워지는) 시키는 코루틴 시작
                StartCoroutine(FadeEmission(mat, mat.GetColor("_EmissionColor"), Color.black, changeDuration, false,originalColor));
            }
        }
    }

    // 발광이 부드럽게 켜지고 꺼지는(페이드 인/아웃) 시각적 변화
    IEnumerator FadeEmission(Material mat, Color startColor, Color endColor, float duration,
        bool finalKeywordState = true,  Color? restoreColor = null)
    {
        float timer = 0f; // 페이드(서서히 색이 바뀌는) 타이머
        while (timer < duration)
        {
            // 현재 시간에 따른 페이드 진행률을 계산하여 색상 보간
            Color currentColor = Color.Lerp(startColor, endColor, timer / duration);
            // material의 Emission 색상을 currentColor으로 설정
            mat.SetColor("_EmissionColor", currentColor);
            // material의 Emission 키워드 활성화 (계속 발광 상태)
            mat.EnableKeyword("_EMISSION");

            timer += Time.deltaTime; // 프레임 시간만큼 타이머 증가
            yield return null; // 끝나면 다음 프레임까지 대기
        }

        // 무적 활성화 루프가 끝나면 endColor 색상으로 설정
        mat.SetColor("_EmissionColor", endColor);

        // finalKeywordState 적용 (페이드가 끝난 후에만 적용)
        // StartInvincible 호출 시 (true)
        if (finalKeywordState)
        {
            mat.EnableKeyword("_EMISSION"); // 키워드 활성화
        }
        else
        {
            mat.DisableKeyword("_EMISSION"); // 키워드 비활성화
            
            if (restoreColor.HasValue)
            {
                mat.SetColor("_EmissionColor", restoreColor.Value);
            }
            // 만약 원래 Emission이 없었거나, 효과를 끄는 것이라면 검은색으로 설정
           else if (endColor.r + endColor.g + endColor.b == 0) // 색상값이 0이면 (검은색이면)
            {
                mat.SetColor("_EmissionColor", Color.black);
            }
        }
    }
}

====

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

public class ObstacleBehavior : MonoBehaviour
{
    Rigidbody _rigidbody;
    public float obstacleSpeed = 25f; // 장애물 이동속도
    public float knockbackForce = 500f; // 넉백 강도

    void Awake()
    {
        _rigidbody = GetComponent<Rigidbody>();
    }

    void FixedUpdate()
    {
        _rigidbody.velocity = Vector3.back * obstacleSpeed; // 장애물 이동방향과 속도 
    }

    /// <summary>
    /// 무적으로 상태에서 충돌되어 날아가는 장애물의 transform이 기본값과 다름.
    /// 재소환 시에 기본값으로 초기화
    /// </summary>
    public void InitObstacle()
    {
        transform.rotation = Quaternion.Euler(0, 90f, 0);
        _rigidbody.velocity = Vector3.back * obstacleSpeed; // 장애물 이동방향과 속도
        _rigidbody.angularVelocity = Vector3.zero; // 회전각도 초기화
        // 장애물 모델링이 기본적으로 90도 틀어져있기 때문에 90도를 더 돌려 180도(정면)로 맞춰준다. 
    }

    // 플레이어와 장애물이 충돌하면 게임 정지
    private void OnCollisionEnter(Collision other)
    {
        if (other.gameObject.CompareTag("Player")) // 플레이어와 충돌하면
        {
            // 충돌한 상대방 오브젝트에서 PlayerCondition이라는 스크립트 컴포넌트를 가져와라
            PlayerCondition playerCondition = other.gameObject.GetComponent<PlayerCondition>();
            if (playerCondition.IsInvincible == false) // 무적이 아닐 때
            {
                StartCoroutine(nameof(GameOver)); // 게임 오버
            }
            else // 무적일 때
            {
                int direction = Random.Range(0, 2); // 장애물 날아가는 방향 정하기
                // 왼쪽 방향과 오른쪽 방향중 하나 랜덤으로 선택됨
                Vector3 selectedDirection = (direction == 0 ? Vector3.right : Vector3.left);

                // 플레이어와 충돌하면 장애물이 대각선 방향으로 날아감
                Vector3 knockbackDirection = (selectedDirection * 5f + Vector3.up * 5f + Vector3.back).normalized;
                // Impulse 모드는 즉각적인 힘을 가함
                _rigidbody.AddForce(knockbackDirection * knockbackForce, ForceMode.Impulse);
            }
        }
    }

    IEnumerator GameOver()
    {
        CharacterManager.Instance.Player.controller.Dead();
        Time.timeScale = 0;
        yield return new WaitForSecondsRealtime(2.28f);

        Debug.Log("장애물과 충돌로 인한 게임정지");
        UIController.Instance.ShowGameOverUI();
    }
}

===

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

public class PlayerCondition : MonoBehaviour
{
    [SerializeField] private InvincibleEffect _invincibleEffect; // InvincibleEffect스크립트 인스펙터에서 할당
    private bool isInvincible = false; // 무적상태 확인
    private Item _item;
    
    public bool IsInvincible // 외부에서 무적 상태인지 확인할 수 있도록 함
    {
        get { return isInvincible; }
        private set { isInvincible = value; }
    }

    // 무적 메서드
    public void ActivateInvincibility(float duration)
    {
        if (!isInvincible) // 무적일 때
        {
            IsInvincible = true; // 무적상태
            // 무적 코루틴 시작
            StartCoroutine(InvincibilityCoroutine(duration));
        }
    }

    /// <summary>
    /// 무적활성화 코루틴, durataion에 따라 효과가 지속됨
    /// </summary>
    /// <param name="duration"></param>
    /// <returns></returns>
    IEnumerator InvincibilityCoroutine(float duration)
    {

        if (_invincibleEffect != null)
        {
            _invincibleEffect.StartInvincible(); // 무적 효과 시작
        }

        // 실제 무적 시간보다 2초 먼저 종료 시키고 무적 이펙트를 먼저 끔
        // 무적이 끝나고 2초 후에 이펙트가 꺼지기 때문에 무적 해제를 알아보기가 힘들음
        yield return new WaitForSeconds(duration - 2);

        if (_invincibleEffect != null)
        {
            _invincibleEffect.EndInvincible(); // 무적 효과 종료
        }

        // 2초 후 무적 종료
        yield return new WaitForSeconds(2);
        
        IsInvincible = false; // 무적 해제
    }
}

0개의 댓글