배경 음악, 효과음 추가 하기
[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이 이제야 온전히 발소리를 들려줄 수 있게 됩니다.