Moving the Camera
지금 까지 만든 게임은 카메라가 움직이지 않으므로 많은 영역을 볼 수 없습니다. 이럴 때 카메라를 게임 오브젝트에 묶을 필요가 있습니다. 먼저 Hierachy 창의 Main Camera를 선택하고 인스펙터 창에서 카메라의 transform을 조정해 줍니다.
그리고 Hierachy창에서 Main Camera를 드래그하여 Player의 child로 만들어줍니다.
이것이 전형적인 3인칭 시점의 세팅입니다. 카메라가 오브젝트의 child이므로 카메라도 함께 움직입니다.
게임뷰에서 플레이를 누르고 오브젝트를 움직이면 카메라도 공의 움직임과 함께 인정사정없이 데굴데굴 구르는 것을 볼 수 있습니다. 이 시점은 안 되겠군요.
하나의 축일반적 3인칭과 우리 게임이 다른 것은 세 축 모두에 대해 회전한다는 점입니다.
따라서 child로 설정했던 카메라를 다시 분리하고 스크립트로 제어하도록 하겠습니다.
Main Camera를 설정한 상태에서 Add Component>New Script 하여 새 스크립트를 생성하고 CameraController라고 이름 짓습니다. 이전과 마찬가지로 루트 폴더에 생성된 스크립트를 Scripts 폴더로 옮기고 에디터로 엽니다.
여기에는 두 개의 변수가 필요합니다.
하나는 GameObject에 대한 레퍼런스이고 하나는 우리가 코드 내에서 설정할 offset이라는 Vector3 변수입니다.
public GameObject player;
private Vector3 offset;
| cs |
offset은 플레이어에 대해 카메라가 어느 위치에 떨어져 있어야 하는가에 대한 벡터값입니다. 플레이어의 위치가 변하더라도 카메라는 상대적으로 같은 위치만큼 떨어져서 뒤따라야 하므로, 게임 시작시 처음 떨어져 있던 만큼만을 유지해 주면 됩니다.
void Start () {
offset = transform.position - player.transform.position;
}
| cs |
그리고 프레임 업데이트 시 플레이어의 위치에 이 offset을 더해주면 카메라의 위치가 결정됩니다. 다만 플레이어의 위치가 결정된 후에 카메라의 위치가 변경되어야 하므로 코드를 작성할 곳은 Update()가 아닌 LateUpdate()가 적당할 것입니다. LateUpdate가 호출될 때는 플레이어 오브젝트의 위치가 업데이트 된 후일 것이기 때문입니다.
전체 코드는 다음과 같습니다.
public class CameraController : MonoBehaviour {
public GameObject player;
private Vector3 offset;
// Use this for initialization
void Start () {
offset = transform.position - player.transform.position;
}
// Update is called once per frame
void LateUpdate () {
transform.position = player.transform.position + offset;
}
}
| cs |
저장 후 다시 유니티로 돌아가서,
public 설정했던 player 프로퍼티를 연결해 주어야 합니다. Main Camera 인스펙터 창에는 Player 프로퍼티가 생성되어 있습니다.
저 빈칸에 Hierachy 창의 Player를 드래그하여 놓으면 레퍼런스가 연결됩니다.
이제 실행해보면 카메라가 회전되는 일 없이 위치 변화가 잘 이루어집니다.
Setting up the Play Area
이제 플레이 필드를 세팅해 봅시다. 사면에 플레이어가 떨어지는 것을 막는 벽을 쳐야 하겠고, 플레이어가 모을 수 있는 큐브들을 둘레에 배치해야 합니다.
먼저 Hierachy창에서 Create>Create Empty 하여 새로운 게임오브젝트를 생성하고 이름을 Walls로 바꾸어줍니다. 그리고 인스펙터의 Transform에서 톱니모양을 클릭하고 Reset을 선택하여 기준점으로 일단 옮겨놓습니다. 이 Walls는 실제 벽들의 부모로 설정하여 계층구조를 분명히 하기 위해 생성한 것입니다.
이제 실제 벽을 생성하겠습니다.
먼저 Hierachy창에서 Create>Create Empty 하여 새로운 게임오브젝트를 생성하고 이름을 Walls로 바꾸어줍니다. 그리고 인스펙터의 Transform에서 톱니모양을 클릭하고 Reset을 선택하여 기준점으로 일단 옮겨놓습니다. 이 Walls는 실제 벽들의 부모로 설정하여 계층구조를 분명히 하기 위해 생성한 것입니다.
이제 실제 벽을 생성하겠습니다.
Cube를 하나 생성하고 이름을 WestWall로 바꾸어 준 후, 드래그하여 Walls의 child로 만듭니다.
Reset하여 위치 조정하고 F키나 Edit>Frame Selected해서 보면 다음과 같은 모양이 됩니다.
이제 이 큐브의 크기를 조절해서 벽 모양으로 만듭니다.
인스펙터의 Scale 창에 x: 0.5, y: 2, z: 20.5를 설정합니다. 아, 그런데 제가 전에 Ground 크기 조절하는 것을 빼먹었네요. Ground의 scale은 (x: 2, y: 1, z:2)입니다.
이 벽을 왼쪽끝으로 옮겨줍니다. 트랜스폼 툴로 옮겨도 되지만 직접 숫자를 써 넣습니다.
x축 위치만 -10으로 옮겨주면 되겠습니다.
다음 벽을 만들기 위해 새로운 Cube를 생성해도 되겠지만 이미 WestWall의 크기가 딱 맞게 조절되어 있으므로 이 오브젝트를 복제해서 사용하도록 하겠습니다.
WestWall이 선택된 상태에서 Edit>Duplicate하여 생성된 오브젝트의 이름을 EastWall로 바꾸어주고, x 포지션을 -10에서 10으로 바꾸면 됩니다.
이제 양쪽 벽이 위 그림처럼 완성되었습니다.
이어서 EastWall을 Duplicate하고 이름을 NorthWall, x 포지션을 0으로 하여 센터로 옮깁니다. 회전시켜도 되지만 Scale을 20.5, 2, 0.5로 바꿔주면 90도 회전된 것과 같아집니다.
그리고 포지션을 0,0,10으로 이동합니다.
같은 방법으로 SouthWall도 만들고 포지션을 0,0,-10으로 해주면 벽들이 모두 완성되었습니다.
플레이해보면 벽들이 플레이어 오브젝트를 잘 막아주는 것을 확인 가능합니다.
항상 미리, 자주 테스트 하는 것을 잊지 마시기 바랍니다.
다음으로 플레이어가 움직이면서 모을 오브젝트들을 생성합니다. Cube를 생성하고 이름을 PickUp으로 바꿉니다. 위치를 Reset하고 Frame Selected해서 보면 플레이어와 모양이 겹칩니다. 이렇게 혼동될 때는 플레이어를 선택하고 인스펙터 창 옆 오브젝트 이름 옆 체크박스를 해제해주면 보이지 않게 되어 헛갈리지 않게 할 수 있습니다.
다시 PickUp을 선택해서 보면 Player를 처음 생성했을 때와 마찬가지로 플레인에 반쯤 묻혀있는 것을 볼 수 있습니다. 크기는 역시 1X1X1이므로 y 포지션을 0에서 0.5로 바꿔주면 플레인 위로 올라옵니다.
PickUp을 변경해서 좀더 플레이어로 하여금 모으고 싶어지게 만들어 봅시다.
Scale을 0.5, 0.5, 0.5로 바꿔서 공중에 떠 있게 만들고
Rotation을 45, 45, 45로 바꿔서 기울지게 만듭니다.
많이 좋아졌지만 특별하게 보이려면 아직 부족합니다. 이 큐브를 회전하게 만들어 봅시다. 그러려면 스크립트가 필요합니다.
PickUp을 선택한 상태에서 Add Component를 누르고 Rotator라는 이름의 스크립트를 생성합니다. Scripts폴더로 옮긴 후에 열어서 코드를 작성합니다.
필요없는 Start()는 지웁니다. 이 픽업은 힘을 받는 일 없이 그냥 돌고 있어야 하므로 FixedUpdate보다는 Update에 코드를 작성하는 것이 맞습니다.
우리가 할 일은 이 오브젝트의 transform 값을 변화시켜 회전하게 만드는 것입니다.
역시 API 문서로부터 transform을 검색해 보면 많은 내용이 있습니다.
그 중 transform에 영향을 주는 메소드는 두가지가 있는데 하나는 Translate이고 하나는 Rotate입니다. 그 중 우리가 필요한 것은 Rotate입니다.
클릭해보면 두 가지의 방법으로 사용이 가능하다는 것을 알 수 있습니다.
위의 것은 Vector3를 인수로 가지고 하나는 float 각도를 인수로 가집니다. 그리고 둘 다 추가적인 인수로 Space라는 것을 가지는데 여기서는 그냥 default로 둘 것입니다.
우리는 그 중 Vector3를 이용하도록 합니다.
벡터값을 정해주고, 프레임 속도와 관계없이 부드럽게 움직이도록 하기 위해 Time.deltaTime을 곱해줍니다.
파일을 저장하고 유니티로 돌아와서 테스트 해보면 큐브가 부드럽게 돌아가는 것을 볼 수 있습니다.
이제 이 오브젝트들을 게임 영역에 나열하기에 앞서 중요한 단계를 거칩니다.
이 PickUp 오브젝트들을 prefab으로 만드는 것입니다. prefab이란 오브젝트의 특성 등 모든 것을 설정한 청사진 같은 것으로서 같은 오브젝트 여러개를 간단히 인스턴스화하기 위한 것입니다. 마치 도장같은 것이라 할 수 있습니다. 이렇게 한 번 만들어 두면 오브젝트를 여러 개 복제한 것과는 달리 수정사항이 있을 때에 prefab만을 손봐도 됩니다.
먼저 프리팹을 저장하기 위한 폴더를 루트디렉토리에 만들고 Prefabs라고 이름 짓습니다.
그리고 Hierachy 창에서 PickUp을 드래그하여 Prefabs폴더에 넣어줍니다. 그러면 Prefabs폴더에 PickUp 에셋이 만들어지고 이것을 프리팹으로 사용가능합니다.
이제 이 오브젝트들을 게임 영역에 배열하기 전에 계층구조 정리용 Empty 오브젝트를 만들어 PickUps라고 이름 붙이고 Reset합니다. 그리고, PickUp을 드래그 하여 PickUps 아래로 옮겨줍니다.
이제 PickUp 여러 개를 배치할 차례입니다. PickUp을 선택하고(PickUps 아님) Gizmo의 y축을 클릭하여 위에서 내려다보는 뷰를 만듭니다.
이 상태에서 만약 토글 스위치가 Local로 되어 있다면 게임 오브젝트를 원하는 방향으로 이동시키기 어렵습니다. 이런 경우 Global로 바꾸어 주어야 월드의 축에 맞게 이동이 가능합니다.
이제 이 첫번째 픽업을 원하는 곳에 가져다 놓습니다. 여기서는 화면 상단 쪽에 위치시키도록 합니다. 그리고 Duplicate를 반복하여 시계 모양으로 배열합니다. 이때 기즈모의 xz 평면을 클릭하여 움직이면 y축 변화없이 배열하는게 가능합니다.
이제 플레이해 보면 12개의 픽업들이 모두 회전하고 있는 것을 볼 수 있습니다.
다음으로 픽업의 색을 바꾸어 좀더 구분이 잘 되도록 해보겠습니다.
이미 생성되어 있는 material을 복사하여 이름을 PickUp으로 바꾸고,
인스펙터 창에서 Albedo를 클릭하여 칼라 조정창을 띄웁니다. RGB를 255,255,0으로 조정하여 노란색으로 바꿔주면 material의 색이 결정됩니다. 이 material을 prefab에 적용하는 법은 두 가지 입니다. 하나는 material을 드래그하여 prefab중 하나에 일단 적용하고 인스펙터 창 우상단에 있는 Apply를 눌러 전체에 적용하는 법과,
다른 하나는 프로젝트 창에서 material을 prefab으로 직접 드래그하여 주는 방법입니다.
이제 Player가 PickUp과 충돌하게 되면 PickUp을 획득하게 만드는 차례입니다. 플레이어 구, Ground 플레인, Wall, PickUp 모두 충돌을 알려주는 collider를 가지고 있습니다. 어떤 오브젝트 간에 충돌한 것인지 테스트를 잘 수행하지 않으면 플레이어가 벽이나 바닥을 획득하여 게임이 엉망이 될 수 있습니다.
먼저, 앞에서 Player 체크 박스를 해제해 inactive 시켰다면 다시 체크하여 표시되게 하고 Player 오브젝트의 스크립트를 엽니다.
이제 collider를 코딩해야 하는데, API 문서에서 검색해 볼 수도 있지만 이번엔 좀 더 좋은 방법이 있습니다.
다시 유니티로 돌아가서 Player의 인스펙터 창을 보면 Sphere Collider라는 것이 있습니다.
우상단에 보면 책 모양이 있는데 그걸 누르면 브라우저로 API를 띄워줍니다.
브라우저 맨 위에 해당 Component의 이름이 있고 그 밑에 매뉴얼과 스크립팅을 오가며 볼 수 있도록 버튼이 있습니다.
현재 우리는 실제 코딩에 참고할 것을 찾고 있으므로 버튼을 누릅니다. 죽 살펴보니 OnTriggerEnter라는 것을 사용해야할 것 같습니다. 오브젝트들이 만날 때 호출됩니다.
클릭하여 내부의 샘플 코드를 보면 우리가 많이 고쳐서 써야 하겠지만 어쨌거나 아래의 부분을 복사하여 우리 에디터에 붙여 넣습니다.
인수로 다른 콜라이더가 주어지면 그것을 파괴하는 코드입니다. 그러나 우리는 오브젝트를 파괴할 것이 아니고 단순히 만난 콜라이더의 오브젝트가 PickUp이면 Deactivate만 시킬 예정이므로 Destroy는 일단 지우거나 주석처리해 놓기로 합시다.
그런데 어떤 오브젝트인지는 어떻게 알 수 있을까요? 그 단서는 Destroy의 인수에 있습니다.
other.gameObject라는 것이 내부의 Destroy 메소드의 인수로 주어졌는데, gameObject라는 것이 뭔지 API에서 찾아보도록 합시다. 문서 내에서 우리가 사용할 세 가지 항목을 찾았습니다.
하나는 tag로 태그 값으로 지정된 string을 비교하여 구분할 수 있게 해 주는 것이고,
다른 하나는 SetActive로 오브젝트를 활성화/비활성화 시켜주며,
마지막은 CompareTag로 태그 비교에 사용됩니다.
따라서, 다른 콜라이더를 만났을 때, 그 콜라이더의 오브젝트 태그가 "Pick Up"이면 비활성화 시키는 코드는 위와 같습니다. 물론 태그는 유니티 상에서 이제 설정해야 합니다.
유니티로 돌아가서 프로젝트 창의 Prefabs 폴더에 들어가 있는 PickUp prefab을 선택하고 우측 인스펙터 창을 보면 이름 아래에 Tag라는 항목이 있습니다. 그 드랍다운 메뉴를 펼쳐서 가장 아래 Add Tag라는 항목을 선택합니다.
그러면 태그 편집창이 나타나는데 +버튼을 눌러 "Pick Up"이라는 새 태그를 추가합니다.
다시 prefab 에셋으로 돌아와서 인스펙터 창의 Tag 항목을 드랍다운 해보면 아래에 우리가 입력한 Pick Up이 나타나는데, 그것을 선택하여 태그로 지정합니다.
이제 개별 PickUp들을 하나하나 선택해 보면 전부 태그가 Pick Up으로 지정된 것이 보입니다. 프리팹이기에 간단하게 한 군데서만 수정해도 전부 바뀌는 것입니다.
이제 테스트 플레이 해보면....
PickUp이 충돌해도 여전히 없어지지 않습니다. 왜일까요?
유니티의 물리 엔진은 두 개의 콜라이더가 겹쳐지는 것을 허용하지 않습니다. 두 개 이상의 콜라이더 프레임이 만나는 것을 감지하면, 유니티 엔진은 그 콜라이더의 모양, 회전, 속도 등을 분석하여 충돌을 계산합니다. 이 충돌에 있어서 중요한 요소는 그 콜라이더가 static이냐 dynamic이냐 하는 것입니다.
static 콜라이더는 벽이나 바닥, 정원의 분수처럼 움직이지 않는 것이고, dynamic은 우리의 Player나 자동차처럼 움직이는 것을 뜻합니다. 충돌이 일어날 때, static 콜라이더는 영향을 받지 않는 반면 dynamic은 영향을 받습니다. 그런데 지금 우리의 PickUp들은 static이어서 벽에 부딪히는 것과 똑같은 상태가 됩니다.
그러나 유니티 엔진이 콜라이더의 프레임간에 겹쳐지는 것을 허용하도록 할 수 있습니다. 그러는 도중에도 콜라이더의 궤도와 겹침을 계산은 하지만 상호작용은 하지 않으므로 충돌은 없습니다. 그것은 콜라이더를 trigger로 지정함으로써 가능합니다.
trigger는 어떤 때에 이용되는가 하면, 예를 들어 어드벤처 게임에서 복도를 걸어가다가 트리거를 지나게 되면 미니맵이 업데이트 되면서 "새로운 방을 발견했습니다"와 같은 메세지를 내 보낼 때라던가, 트리거를 지나게 되면 천장에서 거미가 내려오게 하는 경우에 사용됩니다.
PickUp 프리팹을 선택하고 인스펙터 창을 보면 Box Collider라는 것이 있습니다. 거기에 Is Trigger 체크박스에 체크를 하게되면 trigger 콜라이더로 작동하게 됩니다.
테스트 해봅시다.
픽업이 플레이어를 만나면 사라지는 것을 볼 수 있습니다.
아직 한가지 문제가 남아있습니다. 작은 실수인데 최적화와 관련되어 있습니다.
유니티는 씬의 모든 static 콜라이더 볼륨을 계산하여 캐쉬에 저장하는데, static 콜라이더는 변하지 않고 계속 그 자리에 있고, 다시 계산할 필요가 없기 때문입니다.
우리가 놓친 부분은 큐브가 회전하는 부분입니다.
static 콜라이더가 위치변경, 회전, 크기 변경이 있을 경우 유니티는 모든 static 콜라이더를 재계산하여 캐쉬를 업데이트 하게됩니다. dynamic 콜라이더는 변경이 있다해도 캐쉬를 업데이트하지 않습니다.
따라서 어떤 물체를 움직일 때 우리는 그것이 dynamic이라는 것을 유니티에게 알려줘야 하고, 그것에 쓰이는 것이 rigid body component입니다.
콜라이더와 rigid body인 모든 오브젝트는 dynamic으로, 콜라이더는 있으나 rigid body가 아닌 오브젝트는 static으로 간주됩니다.
현재 우리의 PickUp은 Box Collider를 가지고 있으나 rigid body는 아니기 때문에 static입니다. 따라서 유니티는 매 프레임마다 캐쉬를 재계산합니다.
PickUp 프리팹을 선택하고 Add Component>Physics>Rigid Body를 선택하면 dynamic이 됩니다. 그리고 실행해보면 큐브들이 동시에 아래로 떨어져 버립니다. static이었을 때는 움직일 수 없으므로 공중에 떠 있을 수 있었지만 dynamic이 되면 물리법칙이 적용됩니다. 이것을 피하기 위해서 Rigid Body의 Use Gravity를 체크 해제해 주면 공중에 머물러 있게 되는데, 이 상태에서는 중력의 영향은 받지 않으나 다른 힘의 영향은 계산되므로 부분적인 해결책에 불과합니다.
완전한 해결을 위해서는 Use Grivity의 체크는 그대로 두고, Is Kinematic까지 체크해 줍니다.
이렇게 하면 kinematic rigid body가 되는데 이것은 물리력은 받지 않고, animate는 가능하며, transform에 의해 움직이는 것도 가능해집니다. 엘레베이터나 움직이는 플랫폼을 표현하는데 적당한 상태입니다.
같은 방법으로 SouthWall도 만들고 포지션을 0,0,-10으로 해주면 벽들이 모두 완성되었습니다.
플레이해보면 벽들이 플레이어 오브젝트를 잘 막아주는 것을 확인 가능합니다.
항상 미리, 자주 테스트 하는 것을 잊지 마시기 바랍니다.
Creating Collectable Objects
다음으로 플레이어가 움직이면서 모을 오브젝트들을 생성합니다. Cube를 생성하고 이름을 PickUp으로 바꿉니다. 위치를 Reset하고 Frame Selected해서 보면 플레이어와 모양이 겹칩니다. 이렇게 혼동될 때는 플레이어를 선택하고 인스펙터 창 옆 오브젝트 이름 옆 체크박스를 해제해주면 보이지 않게 되어 헛갈리지 않게 할 수 있습니다.
다시 PickUp을 선택해서 보면 Player를 처음 생성했을 때와 마찬가지로 플레인에 반쯤 묻혀있는 것을 볼 수 있습니다. 크기는 역시 1X1X1이므로 y 포지션을 0에서 0.5로 바꿔주면 플레인 위로 올라옵니다.
PickUp을 변경해서 좀더 플레이어로 하여금 모으고 싶어지게 만들어 봅시다.
Scale을 0.5, 0.5, 0.5로 바꿔서 공중에 떠 있게 만들고
Rotation을 45, 45, 45로 바꿔서 기울지게 만듭니다.
많이 좋아졌지만 특별하게 보이려면 아직 부족합니다. 이 큐브를 회전하게 만들어 봅시다. 그러려면 스크립트가 필요합니다.
PickUp을 선택한 상태에서 Add Component를 누르고 Rotator라는 이름의 스크립트를 생성합니다. Scripts폴더로 옮긴 후에 열어서 코드를 작성합니다.
필요없는 Start()는 지웁니다. 이 픽업은 힘을 받는 일 없이 그냥 돌고 있어야 하므로 FixedUpdate보다는 Update에 코드를 작성하는 것이 맞습니다.
우리가 할 일은 이 오브젝트의 transform 값을 변화시켜 회전하게 만드는 것입니다.
역시 API 문서로부터 transform을 검색해 보면 많은 내용이 있습니다.
그 중 transform에 영향을 주는 메소드는 두가지가 있는데 하나는 Translate이고 하나는 Rotate입니다. 그 중 우리가 필요한 것은 Rotate입니다.
클릭해보면 두 가지의 방법으로 사용이 가능하다는 것을 알 수 있습니다.
public void Rotate(Vector3 eulerAngles, Space relativeTo = Space.Self);
public void Rotate(float xAngle, float yAngle, float zAngle, Space relativeTo = Space.Self);
| cs |
위의 것은 Vector3를 인수로 가지고 하나는 float 각도를 인수로 가집니다. 그리고 둘 다 추가적인 인수로 Space라는 것을 가지는데 여기서는 그냥 default로 둘 것입니다.
우리는 그 중 Vector3를 이용하도록 합니다.
public class Rotator : MonoBehaviour {
// Update is called once per frame
void Update () {
transform.Rotate(new Vector3(15, 30, 45) * Time.deltaTime);
}
}
| cs |
벡터값을 정해주고, 프레임 속도와 관계없이 부드럽게 움직이도록 하기 위해 Time.deltaTime을 곱해줍니다.
파일을 저장하고 유니티로 돌아와서 테스트 해보면 큐브가 부드럽게 돌아가는 것을 볼 수 있습니다.
이제 이 오브젝트들을 게임 영역에 나열하기에 앞서 중요한 단계를 거칩니다.
이 PickUp 오브젝트들을 prefab으로 만드는 것입니다. prefab이란 오브젝트의 특성 등 모든 것을 설정한 청사진 같은 것으로서 같은 오브젝트 여러개를 간단히 인스턴스화하기 위한 것입니다. 마치 도장같은 것이라 할 수 있습니다. 이렇게 한 번 만들어 두면 오브젝트를 여러 개 복제한 것과는 달리 수정사항이 있을 때에 prefab만을 손봐도 됩니다.
먼저 프리팹을 저장하기 위한 폴더를 루트디렉토리에 만들고 Prefabs라고 이름 짓습니다.
그리고 Hierachy 창에서 PickUp을 드래그하여 Prefabs폴더에 넣어줍니다. 그러면 Prefabs폴더에 PickUp 에셋이 만들어지고 이것을 프리팹으로 사용가능합니다.
이제 이 오브젝트들을 게임 영역에 배열하기 전에 계층구조 정리용 Empty 오브젝트를 만들어 PickUps라고 이름 붙이고 Reset합니다. 그리고, PickUp을 드래그 하여 PickUps 아래로 옮겨줍니다.
이제 PickUp 여러 개를 배치할 차례입니다. PickUp을 선택하고(PickUps 아님) Gizmo의 y축을 클릭하여 위에서 내려다보는 뷰를 만듭니다.
이 상태에서 만약 토글 스위치가 Local로 되어 있다면 게임 오브젝트를 원하는 방향으로 이동시키기 어렵습니다. 이런 경우 Global로 바꾸어 주어야 월드의 축에 맞게 이동이 가능합니다.
이제 이 첫번째 픽업을 원하는 곳에 가져다 놓습니다. 여기서는 화면 상단 쪽에 위치시키도록 합니다. 그리고 Duplicate를 반복하여 시계 모양으로 배열합니다. 이때 기즈모의 xz 평면을 클릭하여 움직이면 y축 변화없이 배열하는게 가능합니다.
이제 플레이해 보면 12개의 픽업들이 모두 회전하고 있는 것을 볼 수 있습니다.
다음으로 픽업의 색을 바꾸어 좀더 구분이 잘 되도록 해보겠습니다.
이미 생성되어 있는 material을 복사하여 이름을 PickUp으로 바꾸고,
인스펙터 창에서 Albedo를 클릭하여 칼라 조정창을 띄웁니다. RGB를 255,255,0으로 조정하여 노란색으로 바꿔주면 material의 색이 결정됩니다. 이 material을 prefab에 적용하는 법은 두 가지 입니다. 하나는 material을 드래그하여 prefab중 하나에 일단 적용하고 인스펙터 창 우상단에 있는 Apply를 눌러 전체에 적용하는 법과,
다른 하나는 프로젝트 창에서 material을 prefab으로 직접 드래그하여 주는 방법입니다.
Collecting the Pick Up Objects
이제 Player가 PickUp과 충돌하게 되면 PickUp을 획득하게 만드는 차례입니다. 플레이어 구, Ground 플레인, Wall, PickUp 모두 충돌을 알려주는 collider를 가지고 있습니다. 어떤 오브젝트 간에 충돌한 것인지 테스트를 잘 수행하지 않으면 플레이어가 벽이나 바닥을 획득하여 게임이 엉망이 될 수 있습니다.
먼저, 앞에서 Player 체크 박스를 해제해 inactive 시켰다면 다시 체크하여 표시되게 하고 Player 오브젝트의 스크립트를 엽니다.
이제 collider를 코딩해야 하는데, API 문서에서 검색해 볼 수도 있지만 이번엔 좀 더 좋은 방법이 있습니다.
다시 유니티로 돌아가서 Player의 인스펙터 창을 보면 Sphere Collider라는 것이 있습니다.
우상단에 보면 책 모양이 있는데 그걸 누르면 브라우저로 API를 띄워줍니다.
브라우저 맨 위에 해당 Component의 이름이 있고 그 밑에 매뉴얼과 스크립팅을 오가며 볼 수 있도록 버튼이 있습니다.
현재 우리는 실제 코딩에 참고할 것을 찾고 있으므로 버튼을 누릅니다. 죽 살펴보니 OnTriggerEnter라는 것을 사용해야할 것 같습니다. 오브젝트들이 만날 때 호출됩니다.
클릭하여 내부의 샘플 코드를 보면 우리가 많이 고쳐서 써야 하겠지만 어쨌거나 아래의 부분을 복사하여 우리 에디터에 붙여 넣습니다.
void OnTriggerEnter(Collider other)
{
Destroy(other.gameObject);
}
| cs |
인수로 다른 콜라이더가 주어지면 그것을 파괴하는 코드입니다. 그러나 우리는 오브젝트를 파괴할 것이 아니고 단순히 만난 콜라이더의 오브젝트가 PickUp이면 Deactivate만 시킬 예정이므로 Destroy는 일단 지우거나 주석처리해 놓기로 합시다.
그런데 어떤 오브젝트인지는 어떻게 알 수 있을까요? 그 단서는 Destroy의 인수에 있습니다.
other.gameObject라는 것이 내부의 Destroy 메소드의 인수로 주어졌는데, gameObject라는 것이 뭔지 API에서 찾아보도록 합시다. 문서 내에서 우리가 사용할 세 가지 항목을 찾았습니다.
하나는 tag로 태그 값으로 지정된 string을 비교하여 구분할 수 있게 해 주는 것이고,
다른 하나는 SetActive로 오브젝트를 활성화/비활성화 시켜주며,
마지막은 CompareTag로 태그 비교에 사용됩니다.
public class PlayerController : MonoBehaviour
{
public float speed;
private Rigidbody rb;
void Start()
{
rb = GetComponent<Rigidbody>();
}
void FixedUpdate()
{
float moveHorizontal = Input.GetAxis("Horizontal");
float moveVertical = Input.GetAxis("Vertical");
Vector3 movement = new Vector3(moveHorizontal, 0.0f, moveVertical);
rb.AddForce(movement*speed);
}
void OnTriggerEnter(Collider other)
{
if (other.gameObject.CompareTag("Pick Up"))
{
other.gameObject.SetActive(false);
}
}
}
| cs |
따라서, 다른 콜라이더를 만났을 때, 그 콜라이더의 오브젝트 태그가 "Pick Up"이면 비활성화 시키는 코드는 위와 같습니다. 물론 태그는 유니티 상에서 이제 설정해야 합니다.
유니티로 돌아가서 프로젝트 창의 Prefabs 폴더에 들어가 있는 PickUp prefab을 선택하고 우측 인스펙터 창을 보면 이름 아래에 Tag라는 항목이 있습니다. 그 드랍다운 메뉴를 펼쳐서 가장 아래 Add Tag라는 항목을 선택합니다.
그러면 태그 편집창이 나타나는데 +버튼을 눌러 "Pick Up"이라는 새 태그를 추가합니다.
다시 prefab 에셋으로 돌아와서 인스펙터 창의 Tag 항목을 드랍다운 해보면 아래에 우리가 입력한 Pick Up이 나타나는데, 그것을 선택하여 태그로 지정합니다.
이제 개별 PickUp들을 하나하나 선택해 보면 전부 태그가 Pick Up으로 지정된 것이 보입니다. 프리팹이기에 간단하게 한 군데서만 수정해도 전부 바뀌는 것입니다.
이제 테스트 플레이 해보면....
PickUp이 충돌해도 여전히 없어지지 않습니다. 왜일까요?
유니티의 물리 엔진은 두 개의 콜라이더가 겹쳐지는 것을 허용하지 않습니다. 두 개 이상의 콜라이더 프레임이 만나는 것을 감지하면, 유니티 엔진은 그 콜라이더의 모양, 회전, 속도 등을 분석하여 충돌을 계산합니다. 이 충돌에 있어서 중요한 요소는 그 콜라이더가 static이냐 dynamic이냐 하는 것입니다.
static 콜라이더는 벽이나 바닥, 정원의 분수처럼 움직이지 않는 것이고, dynamic은 우리의 Player나 자동차처럼 움직이는 것을 뜻합니다. 충돌이 일어날 때, static 콜라이더는 영향을 받지 않는 반면 dynamic은 영향을 받습니다. 그런데 지금 우리의 PickUp들은 static이어서 벽에 부딪히는 것과 똑같은 상태가 됩니다.
그러나 유니티 엔진이 콜라이더의 프레임간에 겹쳐지는 것을 허용하도록 할 수 있습니다. 그러는 도중에도 콜라이더의 궤도와 겹침을 계산은 하지만 상호작용은 하지 않으므로 충돌은 없습니다. 그것은 콜라이더를 trigger로 지정함으로써 가능합니다.
trigger는 어떤 때에 이용되는가 하면, 예를 들어 어드벤처 게임에서 복도를 걸어가다가 트리거를 지나게 되면 미니맵이 업데이트 되면서 "새로운 방을 발견했습니다"와 같은 메세지를 내 보낼 때라던가, 트리거를 지나게 되면 천장에서 거미가 내려오게 하는 경우에 사용됩니다.
PickUp 프리팹을 선택하고 인스펙터 창을 보면 Box Collider라는 것이 있습니다. 거기에 Is Trigger 체크박스에 체크를 하게되면 trigger 콜라이더로 작동하게 됩니다.
테스트 해봅시다.
픽업이 플레이어를 만나면 사라지는 것을 볼 수 있습니다.
아직 한가지 문제가 남아있습니다. 작은 실수인데 최적화와 관련되어 있습니다.
유니티는 씬의 모든 static 콜라이더 볼륨을 계산하여 캐쉬에 저장하는데, static 콜라이더는 변하지 않고 계속 그 자리에 있고, 다시 계산할 필요가 없기 때문입니다.
우리가 놓친 부분은 큐브가 회전하는 부분입니다.
static 콜라이더가 위치변경, 회전, 크기 변경이 있을 경우 유니티는 모든 static 콜라이더를 재계산하여 캐쉬를 업데이트 하게됩니다. dynamic 콜라이더는 변경이 있다해도 캐쉬를 업데이트하지 않습니다.
따라서 어떤 물체를 움직일 때 우리는 그것이 dynamic이라는 것을 유니티에게 알려줘야 하고, 그것에 쓰이는 것이 rigid body component입니다.
콜라이더와 rigid body인 모든 오브젝트는 dynamic으로, 콜라이더는 있으나 rigid body가 아닌 오브젝트는 static으로 간주됩니다.
현재 우리의 PickUp은 Box Collider를 가지고 있으나 rigid body는 아니기 때문에 static입니다. 따라서 유니티는 매 프레임마다 캐쉬를 재계산합니다.
PickUp 프리팹을 선택하고 Add Component>Physics>Rigid Body를 선택하면 dynamic이 됩니다. 그리고 실행해보면 큐브들이 동시에 아래로 떨어져 버립니다. static이었을 때는 움직일 수 없으므로 공중에 떠 있을 수 있었지만 dynamic이 되면 물리법칙이 적용됩니다. 이것을 피하기 위해서 Rigid Body의 Use Gravity를 체크 해제해 주면 공중에 머물러 있게 되는데, 이 상태에서는 중력의 영향은 받지 않으나 다른 힘의 영향은 계산되므로 부분적인 해결책에 불과합니다.
완전한 해결을 위해서는 Use Grivity의 체크는 그대로 두고, Is Kinematic까지 체크해 줍니다.
이렇게 하면 kinematic rigid body가 되는데 이것은 물리력은 받지 않고, animate는 가능하며, transform에 의해 움직이는 것도 가능해집니다. 엘레베이터나 움직이는 플랫폼을 표현하는데 적당한 상태입니다.
댓글 없음:
댓글 쓰기