[Unit] Enhance your game > [Tutorial] Add game audio

배경음악 (BGM) 만들기

  • 생성: Hierarchy 우클릭 → Audio → Audio Source 선택 (이름: BackgroundMusic)
  • 파일 넣기: 프로젝트 창의 음악 파일을 인스펙터의 AudioClip 칸으로 드래그.
  • 무한 반복: 인스펙터에서 Loop 체크박스 클릭.
  • 소리 고정: Spatial Blend 바를 왼쪽 끝(2D)으로 밀기. (멀어져도 소리 안 작아짐.

PlayerCharacter 게임 오브젝트 에 Audio Source 컴포넌트를 추가합니다 .

플레이어

using Beginner2D;
using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerController : MonoBehaviour
{
    // Variables related to player character movement
    public InputAction MoveAction;
    Rigidbody2D rigidbody2d;
    Vector2 move;
    public float speed = 3.0f;

    // Variables related to the health system
    public int maxHealth = 5;
    int currentHealth;
    // get: 데이터를 요청받았을 때 실행되는 '입구(함수)'
    // return: 그 입구로 들어온 사람에게 들려보낼 '결과물'
    public int health { get { return currentHealth; } }

    // Variables related to temporary invincibility 무적
    public float timeInvincible = 2.0f;
    bool isInvincible;
    float damageCooldown; // 무적 쿨타임

    // Variables related to animation
    Animator animator;
    Vector2 moveDirection = new Vector2(1, 0); // (X, Y)

    // Variables related to projectiles
    public GameObject projectilePrefab;
    public InputAction LaunchAction;

    // Variables related to NPC
    private NonPlayerCharacter lastNonPlayerCharacter;
    public InputAction TalkAction; // 대화 키

    // Variables related to audio
    AudioSource audioSource;

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        MoveAction.Enable();
        LaunchAction.Enable();
        TalkAction.Enable(); // 대화키 가능
        rigidbody2d = GetComponent<Rigidbody2D>();
        animator = GetComponent<Animator>();
        currentHealth = maxHealth;
        audioSource = GetComponent<AudioSource>();

    }

    // Update is called once per frame
    void Update()
    {
        move = MoveAction.ReadValue<Vector2>();

        // 플레이어가 움직이고 있다면 (0이 아니라면), 부동소수점문제 해결 위해 approximately 사용
        if (!Mathf.Approximately(move.x, 0.0f) || !Mathf.Approximately(move.y, 0.0f))
        {
            moveDirection.Set(move.x, move.y); // 현재 방향을 기억
            moveDirection.Normalize(); // 길이 1로 정규화
        }

        animator.SetFloat("Look X", moveDirection.x);
        animator.SetFloat("Look Y", moveDirection.y);
        animator.SetFloat("Speed", move.magnitude);

        if (isInvincible)
        {
            damageCooldown -= Time.deltaTime;
            if (damageCooldown < 0)
            {
                isInvincible = false;
            }
        }

        if (LaunchAction.WasPressedThisFrame()) // 발사 버튼 클릭 시
        {
            Launch();
        }

        // NPC 레이캐스트 감지 로직
        // Physics2D.Raycast(시작위치, 방향, 거리, 감지할 레이어)
        RaycastHit2D hit = Physics2D.Raycast(
            rigidbody2d.position + Vector2.up * 0.2f, // 시작점: 캐릭터 위치에서 위로 0.2 유닛 (발밑 감지 방지)
            moveDirection,                             // 방향: 현재 캐릭터가 움직이는(바라보는) 방향
            1.5f,                                      // 거리: 앞방향으로 1.5 유닛만큼만 레이저를 쏨
            LayerMask.GetMask("NPC")                   // 필터: "NPC" 레이어가 설정된 오브젝트만 충돌 처리
        );

        // 레이캐스트에 무언가 감지되었다면
        if (hit.collider != null)
        {
            // 충돌한 오브젝트에서 NonPlayerCharacter 스크립트 컴포넌트를 가져옴
            NonPlayerCharacter npc = hit.collider.GetComponent<NonPlayerCharacter>();

            npc.dialogueBubble.SetActive(true); // 해당 NPC의 대화 키 표시 말풍선을 화면에 표시
            lastNonPlayerCharacter = npc;       // 나중에 말풍선을 끄기 위해 현재 NPC 정보를 변수에 저장
            FindFriend(); // 친구를 찾는 추가 로직 실행
        }
        // 레이캐스트에 아무것도 감지되지 않았다면 (NPC 앞을 벗어났다면)
        else
        {
            // 이전에 감지했던 NPC 정보가 변수에 남아있는지 확인
            if (lastNonPlayerCharacter != null)
            {
                lastNonPlayerCharacter.dialogueBubble.SetActive(false); // 켜져 있던 대화 키 말풍선을 다시 끔
                lastNonPlayerCharacter = null; // NPC 저장 변수를 비움
            }
        }

    }

    // FixedUpdate has the same call rate as the physics system
    void FixedUpdate()
    {
        Vector2 position = (Vector2)rigidbody2d.position + move * speed * Time.deltaTime;
        rigidbody2d.MovePosition(position);
    }

    // 외부에서 데미지를 주거나(amount가 음수), 힐을 줄 때(amount가 양수) 호출하는 함수
    public void ChangeHealth(int amount)
    {
        if (amount < 0) // 데미지 줄 때
        {
            if (isInvincible)
            {
                return;
            }
            isInvincible = true;
            damageCooldown = timeInvincible;
            animator.SetTrigger("Hit"); // Hit(피격) 애니메이션을 딱 한 번만 실행
        }

        /* Mathf.Clamp 설명
           현재 체력에 받은 양을 더하되, 그 결과가 0보다 작아지거나 maxHealth보다 커지지 않게 '고정'합니다.
           예: 체력이 5인데 힐을 100 받아도 최대치인 5로 유지됨! 
        */
        currentHealth = Mathf.Clamp(currentHealth + amount, 0, maxHealth);
        UIHandler.instance.SetHealthValue(currentHealth / (float)maxHealth);

    }

    void Launch()
    {
        // projectilePrefab 복제, 현재 캐릭터 위치에서 위로 0.5만큼 살짝 위에서 총알이 나오게, Quaternion.identity는 회전 없음
        GameObject projectileObject = Instantiate(projectilePrefab, rigidbody2d.position + Vector2.up * 0.5f, Quaternion.identity);
        Projectile projectile = projectileObject.GetComponent<Projectile>();
        projectile.Launch(moveDirection, 300);
        animator.SetTrigger("Launch");
    }

    void FindFriend()
    {
        if (TalkAction.WasPressedThisFrame()) // 대화 키 누르면
        {
            UIHandler.instance.DisplayDialogue();
        }
    }

    public void PlaySound(AudioClip clip)
    {
        audioSource.PlayOneShot(clip); // 한 번만 소리 재생하는 함수
    }

}

회복 아이템

using UnityEngine;

// HealthCollectible 클래스: 아이템(체력 회복 아이템 등)에 부착하는 스크립트입니다.
public class HealthCollectible : MonoBehaviour
{

    public AudioClip collectedClip;

    // OnTriggerEnter2D: 이 오브젝트의 Trigger Collider에 다른 오브젝트가 들어왔을 때 호출됩니다.
    // 'other' 변수는 방금 부딪힌(겹쳐진) 상대방의 Collider 정보입니다.
    void OnTriggerEnter2D(Collider2D other)
    {
        // 1. 부딪힌 상대방(other)에게 'PlayerController'라는 스크립트가 있는지 확인합니다.
        PlayerController controller = other.GetComponent<PlayerController>();

        // 2. 만약 상대방에게 PlayerController가 있고 최대 체력이 아니면
        if (controller != null && controller.health < controller.maxHealth)
        {
            controller.PlaySound(collectedClip);
            controller.ChangeHealth(1);

            Destroy(gameObject);
        }
    }
}

🔍 아이템에 소리 데이터 연결

  • 프리팹 진입: Hierarchy에서 HealthCollectible 더블 클릭.
  • 파일 연결: 프로젝트 창의 효과음 파일을 스크립트의 Collected Clip 빈칸으로 드래그.

❓ 수집품 오브젝트의 오디오 소스컴포넌트를 사용하지 않는 이유

  • 수집품 파괴 = 스피커 파괴 = 소리 끊김
  • 플레이어 유지 = 스피커 유지 = 소리 정상 출력
  • 이런 이유 때문에 유니티에서는 아이템 획득음을 사라지지 않는 오브젝트(플레이어 또는 사운드 매니저)가 대신 연주하게 만듭니다.

적 캐릭터 3D 오디오 설정

  • 프리팹 편집 모드에서 적 게임 오브젝트를 열고 오디오 소스 컴포넌트를 추가합니다.
  • EnemyWalk 에셋을 오디오 generator에 할당하고 루프 속성을 활성화합니다.
  • 3D 활성화: Spatial Blend를 1 (3D)로 설정합니다. 이때 씬 뷰에 파란색 원(최대/최소 거리)이 나타납니다.
  • 거리 조절 (Max Distance): 기본값이 너무 크면 멀리 있는 적의 소리도 너무 크게 들립니다. 여기서는 10으로 줄여 적절한 가청 범위를 설정했습니다.
  • 감쇠 방식(Rolloff) Logarithmic으로 선택
  • Logarithmic (로그): 실제 물리 법칙과 유사하게 거리가 멀어질수록 급격히 작아짐.
  • Linear (선형): 거리에 비례해서 직선 형태로 일정하게 작아짐.

⚠️ 주의할 점: 2D 게임에서의 3D 사운드

가장 중요한 포인트는 카메라의 위치(Z축)입니다.

  • 문제: 유니티 2D 프로젝트라도 카메라는 보통 캐릭터보다 약간 뒤(Z축 -10처럼 게임 오브젝트들이 있는 기준면(Z = 0)보다 사용자 쪽으로 10만큼 튀어나와서 )에 위치합니다.
  • 현상: Max Distance를 10으로 설정했는데 카메라가 그보다 더 멀리 있다면, 적이 화면에 보여도 소리는 들리지 않을 수 있습니다.

✅ 해결

  • 자식 오브젝트 생성: 메인 카메라 밑에 ‘Listener’라는 빈 오브젝트를 만듭니다.
  • 위치 조정 (Z = 10): 부모인 카메라가 Z = -10에 있으므로, 자식의 Z를 10으로 설정하면 최종적인 절대 위치는 Z = 0이 됩니다.
  • 컴포넌트 : 카메라에 있던 Audio Listener를 지우고, 새로 만든 ‘Listener’ 오브젝트에 추가합니다.

❓왜 Z축을 10으로 설정했나요? (상대 좌표의 이해)

유니티에서 자식 오브젝트의 위치는 부모 기준입니다.

  • 리스너 (자식): 메인 카메라(부모)로부터 Z축 방향으로 +10만큼 이동함.
  • 최종 결과: -10 + 10 = 0. 즉, 리스너는 이제 적 캐릭터들과 똑같은 Z = 0 평면에 서 있게 됩니다.
  • 3D 사운드 정상 작동: 이제 적(Z=0)과 리스너(Z=0) 사이의 거리가 ‘0’에서 시작합니다. 아까 설정한 Max Distance = 10이 이제야 온전히 발소리를 들려줄 수 있게 됩니다.

📌 출처: [Unity Learn] 2D Adventure: Robot Repair