Audio
게임이 모양을 갖춰 갑니다. 이제 게임을 좀 더 다듬을 차례입니다.
사운드 효과와 음악을 입혀 주려고 합니다. 유니티에는 세개의 메인 오디오 컴포넌트가 있습니다. Audio clips, audio sources, audio listener가 그것입니다. 우리는 그 중 앞의 둘에 치중하려고 합니다.
Audio clips는 오디오 데이터 혹은 사운드 파일을 가지고 있습니다. audio source는 우리의 씬에서 audio clips를 플레이 해주는 역할을 합니다.
우리 에셋의 Audio폴더에 파일이 들어 있습니다. 일단 아무거나 선택해 봅니다. 인스펙터 창에 임포트 세팅이 나타나고, 아래에 미리보기 창도 있습니다.
미리보기에서 오디오 파일의 파형 보기와 듣기가 가능합니다.
오디오 폴더 안에는 6개의 파일이 있는데, 3개의 폭발음과 한 개의 백그라운드 뮤직, 그리고 무기 소리 두개 입니다. 이 중 weapon_enemy는 옵션으로 추후 적기 추가시 사용할 것입니다. 이 음향효과들은 각각의 프리팹들과 합쳐져야 하는데, 폭발은 씬에 인스턴스화 되어 나타났을 때 바로 폭발음이 플레이되어야 하는 반면, 무기음은 무기가 발사되었을 때만 플레이 되어야 합니다.
이 audio clip은 audio source component를 통해 게임 오브젝트와 합쳐집니다. 원칙적으로는 오브젝트에 audio source component를 추가하고, 거기에 audio clip을 연결해서 참조하게 해야 하지만, 단순히 audio clip을 오브젝트에 드래그 해주기만 해도 위의 과정이 자동으로 해결됩니다.
프로젝트 창에서 직접 드래그하여 연결하기 위해서는 프로젝트 레이아웃을 투컬럼이 아닌 원컬럼으로 바꾸어야 합니다.
그리고 Prefabs>VFX>Explosions>explosion_asteroid를 선택한 상태에서 Audio>explosion_asteroid를 드래그해서 인스펙터 창에 끌어다 놓으면 Audio Source component가 생성된다고 했는데 저는 되질 않고 explosion_asteroid(1)이라는 인스턴스가 생성되어 버리는군요. 이것 저것 검색해 봤는데 도무지 이유를 모르겠습니다.
부득이하게 영상과 달리 explosion_asteroid 프리팹에 Add Component>Audio>Audio Source를 선택해 먼저 컴포넌트를 추가하고 거기에 오디오 클립을 연결하는 방법을 사용하겠습니다.
explosion_player프리팹과 사운드 클립도 같은 방법으로 연결하겠습니다.
다음은 무기 소리 차례입니다. 이번에는 좀 다르게 프로젝트 창의 Audio>weapon_player파일을 드래그하여 Hierarchy창의 Player 오브젝트에 끌어다 놓습니다.
그러면 전처럼 Audio Source가 생성되는데 폭발음과는 달리 좀 더 고려할 사항이 있습니다. 인스펙터 창의 Audio Source란을 보면 여러가지 옵션들이 있는데 그 중 Play On Awake라는 항목이 있습니다. 폭발은 그와 동시에 소리가 나므로 생성되면서 바로 소리가 나야 하지만, 총은 발사할 때만 소리가 나므로 체크 해제합니다.
이 소리는 PlayerController 스크립트에서 제어합니다. 에디터로 파일을 엽니다.
Update() 메소드 내에서 "Fire1"이 눌러졌는지 판정하니까 거기에 무기효과음을 플레이 하는 코드를 넣으면 될 것 같습니다. 그것을 위해 먼저 오디오 소스에 대한 레퍼런스를 선언하고, Start() 메소드 내에서 얻어 옵니다.
public class PlayerController : MonoBehaviour
{
public float speed;
public float tilt;
public Boundary boundary;
private Rigidbody rb;
private AudioSource audioSource;
public GameObject shot;
public Transform shotSpawn;
public float fireRate;
private float nextFire;
private void Start()
{
rb = GetComponent<Rigidbody>();
audioSource = GetComponent<AudioSource>();
}
private void Update()
{
if (Input.GetButton("Fire1") && Time.time > nextFire)
{
nextFire = Time.time + fireRate;
Instantiate(shot, shotSpawn.position, shotSpawn.rotation);
audioSource.Play();
}
}
void FixedUpdate()
{
float moveHorizontal = Input.GetAxis("Horizontal");
float moveVertical = Input.GetAxis("Vertical");
Vector3 movement = new Vector3(moveHorizontal, 0.0f, moveVertical);
rb.velocity = movement * speed;
rb.position = new Vector3
(
Mathf.Clamp(rb.position.x, boundary.xMin, boundary.xMax),
0.0f,
Mathf.Clamp(rb.position.z, boundary.zMin, boundary.zMax)
);
rb.rotation = Quaternion.Euler(0.0f, 0.0f, rb.velocity.x * -tilt);
}
}
| cs |
저장하고 유니티로 돌아갑니다. 플레이 해보면 소행성 파괴, 총 발사, 비행선 파괴 시 모두 소리가 잘 나는 것을 확인할 수 있습니다.
이제 배경음악을 넣을 차례입니다.
Audio>music_background 파일을 드래그 하여 Hierarchy창의 Game Controller에 연결합니다. 이 음악은 게임 시작과 동시에 플레이되며, 계속 이어져야 하므로 Play On Awake, Loop에 모두 체크해 줍니다.
씬을 저장하고 플레이하여 사운드를 모두 확인합니다.
음향 효과가 모두 입혀졌는데 모두 최대 볼륨으로 설정되어 있으므로 밸런스를 위해 좀 조절해야할 것 같습니다. Player 오브젝트를 선택하고 1로 되어 있는 볼륨을 0.5로 바꿉니다.
같은 방법으로 Game Controller의 Volume 프로퍼티도 0.5로 바꿔주면 폭발음은 좀 더 선명하게 들리고, 배경음악과 총 소리가 너무 지나치지 않게 설정된 듯 합니다.
Counting points and displaying the score
소행성을 파괴하면 1포인트씩 얻고 점수를 보여주게 만들려고 합니다.
실제 계산하는 코드를 만들기에 앞서 GUIText를 생성하겠습니다. 먼저 빈 게임 오브젝트를 하나 만들고 이름을 Score Text라고 해 줍니다. 그리고 그 오브젝트를 선택한 채로 Add Component>Rendering>GUI Text를 선택해서 추가합니다.
Transform position은 0.5, 0.5, 0으로 하고, 일단 눈에 보이게 하기 위해 Text 프로퍼티에는 Score Text라고 입력합니다. 그러면 아래와 같은 모습이 됩니다.
중앙에 출력된 글씨를 좌상단으로 옮기겠습니다. 이 글씨는 우리의 게임 월드에 직접 들어간 것이 아니라 그 위에 레이어로 덧입혀진 것입니다. 따라서 씬뷰에서는 보이지 않습니다. 또 한가지 알아둘 것이 GUI Text는 Screen space가 아닌 Viewport space를 사용한다는 점입니다. Screen space는 픽셀 단위를 이용하는 반면 Viewport space는 0-1의 가로세로 값을 사용하여 좌하단이 (0,0), 우상단이 (1,1)이라는 점입니다. 좌상단에 가게 하려면 transform position 수치를 0, 1, 0으로 바꿉니다.
이렇게 하고 나니 글씨가 좌상단으로 완전히 틀어박혔습니다. 조금 간격을 주는 편이 좋겠습니다만 Viewport공간이 0~1 사이의 수이다 보니 얼마나 작은 수로 간격을 주어야 하는지 알기가 어렵습니다. 이럴 때 GUI text의 Pixel Offset을 사용합니다. 이름 그대로 픽셀 단위의 오프셋입니다. 10, -10을 주니 적당히 간격이 생깁니다.
점수가 표시될 위치가 마련되었으니 이제 점수를 계산하는 방법을 강구합니다. Hierarchy 창에서 Game Controller를 선택하여 그 스크립트를 엽니다.
GUIText에 대한 레퍼런스 변수인 scoreText와 점수를 계산할 int형 score변수를 선언하고, 점수를 표시할 UpdateScore()라는 메소드를 작성합니다.
public GUIText scoreText;
public int score;
void UpdateScore()
{
scoreText.text = "Score:" + score;
}
| cs |
string과 int형 score를 더하는 것이 이상할 겁니다만, 사실 저렇게만 작성하면 유니티가 알아서 변환하여 표시한다고 하니 편리하긴 합니다.
Start() 메소드 내에서 score를 0으로 초기화하고, UpdateScore()를 호출하여 점수를 표시합니다. 그리고 소행성이 파괴될때마다 점수를 1 추가하고 업데이트 해줘야 합니다. 그럼 소행성이 파괴되었는지는 어떻게 알 수가 있을까요?
여기서는 알 수가 없습니다. 그래서 public타입의 AddScore()라는 메소드를 정의하여 소행성이 파괴되었을 때 다른 곳에서 호출하여 추가해 주려고 합니다.
public class GameController : MonoBehaviour
{
public GameObject hazard;
public Vector3 spawnValues;
public int hazardCount;
public float spawnWait;
public float startWait;
public float waveWait;
public GUIText scoreText;
public int score;
private void Start()
{
score = 0;
UpdateScore();
StartCoroutine(SpawnWaves());
}
IEnumerator SpawnWaves()
{
yield return new WaitForSeconds(startWait);
while (true)
{
for (int count = 0; count < hazardCount; count++)
{
Vector3 spawnPosition = new Vector3(Random.Range(-spawnValues.x, spawnValues.x),
spawnValues.y, spawnValues.z);
Quaternion spawnRotation = Quaternion.identity;
Instantiate(hazard, spawnPosition, spawnRotation);
yield return new WaitForSeconds(spawnWait);
}
yield return new WaitForSeconds(waveWait);
}
}
public void AddScore(int newScoreValue)
{
score += newScoreValue;
UpdateScore();
}
void UpdateScore()
{
scoreText.text = "Score:" + score;
}
}
| cs |
저장하고 유니티로 돌아갑니다. 소행성이 파괴되었을 때 AddScore()를 호출하기 위해 이번에는 Asteroid 프리팹의 DestroyByContact 스크립트를 엽니다.
소행성의 점수를 위한 int형의 scoreValue라는 변수를 선언하고, 소행성 파괴 판정문 뒤에 GameController.AddScore(scoreValue)라고 작성해 봅니다.
public class DestroyByContact : MonoBehaviour
{
public GameObject explosion;
public GameObject playerExplosion;
public int scoreValue;
void OnTriggerEnter(Collider other)
{
if (other.tag == "Boundary")
return;
Instantiate(explosion, transform.position, transform.rotation);
if (other.tag == "Player")
{
Instantiate(playerExplosion, other.transform.position, other.transform.rotation);
}
GameController.AddScore(scoreValue);
Destroy(other.gameObject);
Destroy(gameObject);
}
}
| cs |
저장하고 유니티로 돌아가 보니 에러가 발생했습니다. 콘솔에서 메세지를 확인해 봅니다.
Assets/Scripts/DestroyByContact.cs(20,24): error CS0120: An object reference is required to access non-static member `GameController.AddScore(int)'
Static도 아닌 멤버를 상대로 레퍼런스도 없이 사용할 생각은 하지 말라네요.
DestroyByContact 내에 레퍼런스를 작성합니다.
public class DestroyByContact : MonoBehaviour
{
public GameObject explosion;
public GameObject playerExplosion;
public int scoreValue;
public GameController gameController;
void OnTriggerEnter(Collider other)
{
if (other.tag == "Boundary")
return;
Instantiate(explosion, transform.position, transform.rotation);
if (other.tag == "Player")
{
Instantiate(playerExplosion, other.transform.position, other.transform.rotation);
}
gameController.AddScore(scoreValue);
Destroy(other.gameObject);
Destroy(gameObject);
}
}
| cs |
레퍼런스 변수를 선언하고, 그 변수를 통해 메소드를 사용하게 바꾸었습니다. 저장 후 유니티로 돌아가니 에러 메세지가 사라집니다.
이제 Asteroid 프리팹을 선택해 보면 인스펙터 창의 Script에 Game Controller 프로퍼티가 생겼습니다. 이것을 Hierarchy창의 Game Controller와 연결하면 될까요?
안되네요.
그것은 Hierarchy 창의 Game Controller가 인스턴스이기 때문입니다. 인스턴스는 씬 내에 복사된 오브젝트의 복사본일 뿐입니다. 여기서 프리팹의 성질에 대해 생각해 보면, 프리팹은 게임 오브젝트의 템플릿이고, 게임 내의 어떤 씬에라도 인스턴스화 시킬 수 있습니다. 게임 내의 어디에라도 추가될 수 있는 템플릿이 한 개의 씬 내에서만 인스턴스에 대해 레퍼런스를 가진다는 것이 말이 안 됩니다. 그래서 소행성을 하나 씬내에 인스턴스화 시키면 우리는 소행성 인스턴스를 가지게 되며, 그 인스턴스는 또 Game Controller의 인스턴스에 대해 레퍼런스를 가지게 됩니다. 그래서 우리는 게임이 시작된 후에 Game Controller에 대한 레퍼런스를 찾아야 합니다. 각각의 소행성은 인스턴스화 될 때마다 Game Controller에 대해 새로운 레퍼런스를 가지게 됩니다.
다시 DestroyByContact 스크립트로 돌아갑니다.
모든 소행성은 이 스크립트에 대한 인스턴스를 가지고, Game Controller라는 게임 오브젝트에 있는 Game Controller라는 콤포넌트에 대한 레퍼런스를 찾아야 합니다(말이 너무 어렵군요. 제가 번역하고도 대체 무슨 말인지 모를 지경입니다). 스크립트가 그것을 해내도록 수정해 보겠습니다.
public class DestroyByContact : MonoBehaviour
{
public GameObject explosion;
public GameObject playerExplosion;
public int scoreValue;
public GameController gameController;
private void Start()
{
GameObject gameControllerObject = GameObject.FindWithTag("GameController");
if (gameControllerObject != null)
{
gameController = gameControllerObject.GetComponent<GameController>();
}
else
{
Debug.Log("Cannot find 'GameController' script");
}
}
void OnTriggerEnter(Collider other)
{
if (other.tag == "Boundary")
return;
Instantiate(explosion, transform.position, transform.rotation);
if (other.tag == "Player")
{
Instantiate(playerExplosion, other.transform.position, other.transform.rotation);
}
gameController.AddScore(scoreValue);
Destroy(other.gameObject);
Destroy(gameObject);
}
}
| cs |
GameController라는 오브젝트를 찾아서 그 중 GameController라는 스크립트 컴포넌트를 찾아 미리 선언된 레퍼런스 변수에 넣어줍니다.
유니티로 돌아갑니다.
Asteroid 프리팹을 선택하고 Score Value 프로퍼티를 10으로 설정해줍니다. 그 아래에 Game Controller를 설정하는 란이 있지만 인스펙터창에서는 불가능 합니다. 그러니 에디터를 다시 열고 public으로 선언된 것을 private으로 바꿔줍니다.
private GameController gameController;
| cs |
유니티로 다시 돌아가면 Game Controller프로퍼티가 사라졌습니다.
다음은 Hierarchy창에서 Game Controller를 선택합니다. Score 프로퍼티가 있는데 우리는 score를 내부적으로 처리하고 직접 지정해줄 것이 아닙니다. 역시 스크립트에서 private으로 바꿉니다.
private int score;
| cs |
마지막으로 점수 표시하는 텍스트에 레퍼런스를 설정해야 합니다.
여전히 Game Controller 선택한 채로 Score Text 오브젝트를 드래그하여 Game Controller의 Score Text 프로퍼티로 끌어다 놓습니다.
저장 후 실행해 봅니다. 소행성이 맞을 때마다 점수가 올라가는 것이 보입니다.
댓글 없음:
댓글 쓰기