3. 3DPlatform Tutorial - 3인칭 게임 튜토리얼

Unity3D 2012. 6. 19. 11:37
반응형

자료 다운

http://unity3d.com/support/resources/tutorials/3d-platform-game에서 3DPlatformTutorialStart.zip을 다운.

팁 :

  1. 유니티는 파일 경로에 한글이 포함되면 안된다.
  2. 유니티를 실행할 때마다 Open Project 대화상자가 열리도록 하려면 Edit > Preferences... 를 선택한 후 Show Project Wizard at Startup 을 체크하라.

목차

1. First Step

Lerpz 소개하기.

캐릭터 제어기와 3인칭 제어기 스크립트

Lerpz 애니메이션

ThirdPersonPlayerAnimation 스크립트

Gizmos => 기즈모(우리말로 거시기)

The Jec-Pack

파티클 시스템 추가하기

빛 추가하기.

Blob Shadows => 얼룩 그림자.

Blob 그림자 추가하기.

레이어 생성하기.

스크립팅 개념

죽음과 부활

Fallout Death 스크립트

부활 지점 (Respawn Points)

작동하는 방법

2. Setting the Scene

첫번째 단계

소품 놓기 (Placing Props)

Health Pickups => 건강 습득물 => 체력 회복제

The Force Field => 힘의 장 (힘이 작용하는 영역)

수집가능한 아이템 스크립트

점프 패드

3. The GUI

유저 인터페이스

유니티 2의 새로운 GUI 시스템

추가적인 정보

The In-game HUD - 화면위에 투명하게 제공되는 정보

GUI 스킨 오브젝트

시작 메뉴

장면 세팅하기

배경 이미지(Backdrop)

버튼 (Button)

게임 오버

4. Adversaries

레이저 트랩

레이저 트랩 만들기

LaserTrap 스크립트

로봇 가드

찾고 파괴하기

로봇 가드 추가하기.

죽음의 푸른 불꽃.

로봇 가드 스크립트.

로봇 생성과 최적화

작동 방법

5. Audio & Finishing Touches

샘플 노트

게임에 사운드 추가하기.

플레이어

로봇가드

주변(환경, ambient) 소리

점프 패드

수집품

우주선 장벽

플레이어

로봇 가드

Cut Scenes, 컷씬

장벽 해제하기

컷씬 카메라 1 만들기

컷씬 애니메이션.

컷씬 카메라 2 만들기.

6. Optimizing

렌더링 최적화 : 초당 프레임율 관찰하기.

Stats (Statistics, 통계) 화면 이해하기.

렌더링 최적화 : 2개의 카메라 시스템.

1. First Step

Lerpz 소개하기.

  1. 다운로드 받은 파일의 압축을 푼다.
  2. 유니티를 실행하고 Open Project에서 Open Other...을 선택 후 압축을 푼 폴더를 지정한다.
  3. 프로젝트 뷰에서 Scenes > TheGame 장면을 더블클릭하라.
  4. Objects 폴더를 열고 Lerpz 프리팹을 장면 뷰 또는 계층(Hierarchy) 뷰로 드래그하라.
  5. 계층 뷰에서 Lerpz의 이름을 Player로 변경하라.
  6. Player를 선택한 상태에서 장면 뷰로 마우스를 옮긴 후 F를 눌러서 포커스를 이동하라.
  7. Lerpz를 감옥 근처의 점프 패드가 있는 플랫폼으로 이동하라. (스크린 샷 참조)

Play를 해보면 저 멀리 Lerpz가 서있는 것을 볼 수 있다. 하지만 아직 움직일 수 없고, 카메라도 우리의 캐릭터를 바라볼 필요가가 있다. 이를 위한 몇가지 카메라 스크립트를 가지고 있지만 여기서는 SpringFollowCamera를 사용할 것이다.

  1. 프로젝트 뷰에서 Scripts > Camera > SpringFollowCamera 스크립트를 계층 뷰의 NearCamera 오브젝트로 드래그하라.
  2. Play 하면 에러가 날 것이다. (Shift + Ctrl + C 를 누르면 디버그 콘솔을 볼 수 있다.)

UnassignedReferenceException 에러는 스크립트 변수가 설정되지 않았을 때 나오는 메시지이고 이 메시지는 매우 자주 보게 될 것이다.

  1. NearCamera 오브젝트를 선택하고 Inspector 창에서 Spring Follow Camera 컴포넌트 속성을 보라.
  2. Target 설정에 Player 게임오브젝트를 드래그하라.

  1. Play 하면 여전히 에러가 날 것이다.
  2. 프로젝트 뷰의 Scripts > Player > ThirdPersonController 스크립트를 우리의 Player 오브젝트 위로 드래그하라. (3인칭 카메라는 플레이어의 컨트롤과 밀접한 관계가 있다.)

스크립트에서 우리의 Player를 찾을 수 있도록 하기위해  Player 게임오브젝트에 태그를 추가해야 한다.

  1. Player 오브젝트를 선택한 후 Inspector 창에서 “Player” 태그를 지정하라.

태그는 나중에 사용될 것이다. 그러므로 캐릭터 제어와 스크립트로 돌아가자.

  1. Player 오브젝트를 선택한 후 Inspector 패널의 속성을 아래와 같이 설정하라.

  1. Capsule Collider의 위치를 아래 스크린샷에 보이는 것처럼 위치시켜라. (Character Controller 컴포넌트의 Center 속성을 펼친 후 Y 값에 1.03을 준다.)

이제 실행을 해보면 Lerpz는 컨트롤 키에 의해 지형위를 움질일 수 있을 것이다.

캐릭터 제어기와 3인칭 제어기 스크립트

대부분의 게임에서 플레이어의 아바타는 순간적인 회전 및 멈춤, 불가능한 거리로의 도약 등 물리적으로 불가능한 능력을 가지고 있다. 캐릭터 제어기(Character Controller)는 물리 엔진으로부터 플레이어의 아바타를 분리하고 기본적인 움직임을 제공한다.

캐릭터 제어기는 플레이어 캐릭터 타입을 위한 움직임을 단순화한다. 그것은 움직이고 오르고 미끄러지는 기본 움직임 시스템에 밀접한 Capsule Collider로 구성되어있다. 당신은 최대 step 및 slope 사이즈를 변경할 수 있다.

캐릭터 제어기는 일반적으로 스크립트와 함께 사용된다. 우리의 프로젝트에서는 3인칭 제어기(Third Person Controller) 스크립트가 이것을 수행한다. 그것은 조이스틱, 키보드, 마우스 또는 다른 입력 장치를 읽고 플레이어의 아바타를 제어한다.

유니티의 인풋 매니저 (Edit > Project Settings > Input Manager) 는 입력장치가 플레이어를 어떻게 제어할지를 정의한다.

Lerpz 애니메이션

지금은 캐릭터 제어기가 애니메이션을 다루지 않기 때문에 Lerpz는 단순히 미끄러져 다닌다.

  1. 프로젝트 뷰의 Scripts > Player > ThirdPersonPlayerAnimation 스크립트를 Player 오브젝트에 추가하라.

이제 실행해보면 Lerpz의 정확한 애니메이션을 보게 될 것이다.

ThirdPersonPlayerAnimation 스크립트

Lerpz는 15개의 애니메이션을 가지고 있지만 이 튜토리얼에서는 11개만이 사용된다. Hierarchy에서 Player를 선택한 후 Inspector 창에서 Animation 컴포넌트의 Animations 속성을 펼치면 15개의 애니메이션 리스트를 볼 수 있다.

튜토리얼에서 사용하는 11개의 애니메이션은 다음과 같다.

  1. Walk                 - 걷기.
  2. Run                 - 달리기.
  3. Punch                - 펀치 어택.
  4. Jump                - 공중으로 도약할 때.
  5. Jump fall        - 정점에 도달한 후 떨어질 때.
  6. Idle                - 평상시 서있기.
  7. Wall jump        - 백플립하면서 벽 점프.
  8. Jet-pack Jump        - 제트팩이 추락을 느리게 할 때.
  9. Ledge fall        - 모서리에서 추락할 때.
  10. Buttstomp        - 로봇 가드에 의해 공격받았을 때.
  11. Jump land        - 점프 또는 추락 후에 착지할 때.

애니메이션의 대부분은 스크립트에 의해 다루어진다. 몇몇 애니메이션은 다른 것들이 단순히 차례대로 그려지는 동안 다른 것들의 위에 그려진다. 스크립트는 대부분이 메시지 응답 함수의 모음이다. 적절한 메시지는 입력장치를 읽고 캐릭터 상태를 업데이트 하는 ThirdPersonController 스크립트에 의해 발생된다.

Lerpz의 공격은 분리된 스크립트, ThirdPersonCharacterAttack 에서 다룬다. 이는 나중에 추가할 것이다.

Gizmos => 기즈모(우리말로 거시기)

ThirdPersonCharacterAttack 은 테스트하는데 유용한 것들을 포함하고 있다. Lerpz’s 의 펀치 동작에 영향을 받는 영역을 표현하기 위한 구를 그리는 기즈모. 기즈모들은 함수를 제어하는 2개의 기즈모 그리기 메시지중 하나안에서 그려진다. 예를 들면, 펀치 위치에 그려지는 노란색 와이어프레임과 그것에 영향을 받는 영역을 보여주는 기즈모는 OnDrawGizmosSelected() 함수에 반응한다. 이 함수는 반드시 static이어야 하고 유니티 에디터에 의해 호출될 것이다.

다른 하나는 매 업데이트 주기마다 유니티 에디터에 의해 호출되는 OnDrawGizmos()함수로 부모 게임오브젝트가 선택이 되었는지 아닌지는 관계없다.

The Jec-Pack

우리의 Lerpz는 하강 속도를 늦추기 위하여 제트팩을 사용할 것이다. 움직임은 이미 구현되었지만 애니메이션은 적용되지 않았다. 이것을 만들기 위해 2개의 파티클 시스템(Particle System)과 1개의 점광원(Point Light)을 추가할 필요가 있다.

이상적으로 각각의 제트마다 점광원이 있어야 하지만, 제트기 배출은 서로가 충분히 가깝기때문에 하나만으로 처리할 수 있다. 빛은 계산하기에는 비싸기 때문에 이것은 간편한 최적화이다.

파티클 시스템 추가하기

  1. Hierarchy 패널에 빈 게임오브젝트를 만들기 위해 GameObject > Create Empty를 선택하라.
  2. 이름을 Jet로 변경하라.

Jet 오브젝트에 Component > Particles 에서 다음을 추가하라.

  1. Ellipsoid Particle Emitter
  2. Particle Animator
  3. World Particle Collider
  4. Particle Renderer
  5. Inspector창에서 Particle Renderer의 Enable 체크박스를 해제하여 사용하지 않게하라.
  6. Jet 오브젝트를 Lerpz의 제트분사기 오른편의 바로 아래에 위치시켜라.
  7. 다시 Particle Renderer를 사용가능하게 하라.
  8. Ellipsoid Particle Emitter의 속성을 아래와 같이 적용하라.

위와 같이 설정하면 불이 분출되는 것처럼 파티클의 흐름이 변할 것이다.

여기까지 완료했을 때, 파티클 시스템은 Player의 몸통(torso)에 자식으로 첨부될 것이다. 이것은 Player의 움직임에 파티클 시스템이 따라다니도록 할 것이다.

Min Size와 Max Size는 파티클의 크기를 정의한다. Min Energy와 Max Energy는 최소/최대 수명을 정의한다. 우리의 파티클은 0.2초라는 짧은 시간동안만 살아있을 것이다.

Min Emission과 Max Emission은 매 초마다 생성되는 파티클의 최소/최대 개수이다. 우리는 이 설정을 이용하여 파티클의 수를 50개로 설정하였다.

알림

우리는 여기서 “Simulate in Worldspace”를 해제하였다. 이것은 많고 느린 불보다 뜨겁고 빠른 가스의 분출을 가진 인상을 준다. (파티클 시스템이 움직였을 때 이미 분사된 파티클도 같이 움직이게 한다.)

  1. 이제, Particle Animator 컴포넌트를 아래와 같이 설정하라. (Size Grow를 설정하는 것을 잊지마라).

다음은 Particle Renderer이다. 이 컴포넌트는 각각의 파티클들을 렌더링하는데 사용할 재질을 정의한다. 우리는 화염처럼 보이는 효과를 원하므로 “fire add” 재질을 사용할 것이다. 이것은 프로젝트 뷰의 Particles > Sources > Materials > fire add 에서 찾을 수 있다.

  1. fire add 재질을 Particle Renderer 컴포넌트의 Materials 속성을 펼친 후 Element 0 위로 드래그하라.
  2. 이제 Particle Renderer 컴포넌트를 아래와 같이 설정하라.

알림 - Cast Shadows와 Receive Shadows는 사용자 정의 shader를 사용하지 않는다면 효과가 없다.

빛 추가하기.

우리의 화염분출은 그럴듯해 보이지만, 파티클 시스템은 단지 작은 이미지들을 뱉어낼 뿐이다. 화염분출을 완성하기 위해서 우리는 분리된 점광원(Point Light) 오브젝트를 만들 것이다. 우리는 이것을 파티클 시스템과 동시에 온오프할 것이다. (우리는 제트마다 하나의 빛을 사용하지 않고 오직 하나의 빛만을 사용할 것이다.)

  1. Point Light 오브젝트를 새로 생성하라.
  2. 이름을 “Jet Light”로 변경하고 제트팩의 두 제트 사이에 위치시켜라.
  3. Jet Light를 선택하고 아래와 같이 적용하라.

왜 그림자를 사용하지 않는가? 그림자는 대부분의 하드웨어에서 비싼 연산을 필요로 하므로 가능하면 사용하지 않는다.

다음은 Player 오브젝트가 우리의 Jet와 Jet Ligtt 오브젝트를 포함하는 것이다.

이것을 위해, 먼저 우리의 Jet를 프로젝트 패널에 프리팹으로 추가할 것이다.

  1. 프로젝트 패널에서 빈 Player 폴더를 선택하고 Create > Prefab을 선택하라.
  2. 이름을 Jet으로 변경하라.
  3. Hierarchy 패널에서 Jet 오브젝트를 Jet 프리팹으로 드래그하라.

Jet 오브젝트는 Jet 프리팹과 연결되면서 이름이 푸른색으로 변할것이다. 우리는 제트팩을 위해 Jet 프리팹의 인스턴스 2개를 사용할 것이다. 프리팹을 사용한다는 것은 원본 프리팹을 수정하는 것만으로 2개의 제트를 한꺼번에 수정할 수 있다는 것을 의미한다.

  1. 우리의 오리지널 Jet 오브젝트를 Hierarchy 패널에서 지워라. (그러나 Jet Light는 그대로 둬라)
  2. 이제 Hierarchy 패널에서 Player 오브젝트로 간 후 torso를 찾을 때 까지 확장하라.
  3. Jet 프리팹을 torso위로 2번 드래그하라. 이것은 2개의 제트 인스턴스를 생성할 것이다.
  4. 두 제트 인스턴스를 각각 Jet L 과 Jet R로 이름을 변경하라.
  5. Jet Light를 torso오브젝트로 드래그하라.
  6. 이제 아래와 같이 보일 것이다.
  7. 유니티의 툴을 이용하여 Jet L과 Jet R을 제트팩에 맞게 위치시킨 후 방향에 맞게 회전시켜라. (오브젝트가 멀리 있을 경우 우선 오브젝트를 선택한 후  이동하고 싶은 위치로 가서 메인메뉴의 GameObject > Move To View를 선택하면 편하게 이동할 수 있다.)
  8. Jet Light를 Jet L과 Jet R 사이에 위치시켜라.

이제 Lerpz가 움직이면 제트팩이 분사하는 불길도 같이 움직일 것이다. 이제 거의 다 됐다.

마지막은 Jet 프리팹과 Jet Light를 점프중에만 활성화되게 하는 것이다. 이것은 스크립트를 통해 수행된다.

  1. Scripts > Player > JetPackParticleController 스크립트를 Hierarchy 패널의 Player 오브젝트 위로 드래그하라.

이제 우리가 기대하는대로 작동하는 제트팩을 볼 수 있을 것이다. 이 스크립트는 Lerpz의 움직임과 동기화 되도록 2개의 파티클 시스템과 빛을 제어한다.

Blob Shadows => 얼룩 그림자.

Lerpz는 언제나 쉽게 확인이 되어야 한다. 이는 대부분 아티스트와 게임 디자이너에게 달려있지만 몇가지 요소는 유니티에서 직접 제어해야 한다.

이러한 것들 중 가장 중요한 하나는 그림자와 빛이다. 성능을 위해서, 빛의 효과는 종종 미리 렌더링되어야 한다. 이런 기술(“baking”이라고 불리운다)은 정적인 오브젝트에만 잘 작동한다. 거리의 불빛 아래서 움직이는 캐릭터는 실시간으로 빛에 반응해야 한다.

해결책은 빛이 있어야 할 곳에 위치시키는 것이다. 만약 “baked” 텍스처를 사용한다면, baked 빛이 있는 곳에 빛을 추가해야 한다는 것을 기억하라. 하지만 빛이 움직이는 물체에만 영향을 주도록 만들어라. 이 튜토리얼에서 빛은 이미 장면 곳곳에 위치하고 있다.

그림자는 그래픽 엔진에 의해 실시간으로 계산되고 렌더링되는 빛을 사용하여 제공될 수 있다. 하지만, 이러한 그림자는 매우 비싸다. 게다가 모든 그래픽이 빠르고 효율적으로 계산할 수 있는 것도 아니다. 옛날 그래픽 카드는 전혀 지원하지 못할수도 있다.

이러한 이유로, 우리는 Lerpz를 위해 Blob 그림자를 사용할 것이다.

Blob 그림자 추가하기.

Blob 그림자는 일종의 편법이다. 이것은 빛이 부딪히는 영역을 검사하는 대신에 캐릭터 아래에 단순히 어두운 이미지(여기서는 단지 원형의 검은색 얼룩)를 투영한다. 이것은 거의 모든 하드웨어에서 잘 작동한다.

유니티는 Blob 그림자 프리팹을 Standard Assets 모음에 포함하고 있다. 그러므로 우리가 스스로 만들기 보다는 이것을 사용할 것이다. 이 에셋은 이미 프로젝트의 Blob-Shadow 폴더에 포함되어 있다.

  1. blob shadow projector 프리팹을 Hierarchy 패널의 Player 오브젝트 위로 드래그하라.
  2. blob shadow projector 를 Player의 머리 위로 이동한 후 서있는 바로 아래를 가리키도록 회전하라. (회전(Rotation)값은 90, 180, 0을 주면 된다.)

레이어 생성하기.

이 시점에서 알아야 할 점은 blob이 Lerpz에게도 그림자를 투영하고 있다는 것이다. 우리는 이러한 일이 발생하는 것을 원하지 않는다. 이것을 피하는 2가지 옵션이 있다. 하나는 blob shadow projector를 선택한 후 나타나는 Projector의 Near Clip Plane(가까운 평면)속성의 값을 더 멀리 설정하는 것이고 다른 하나는 단순히 특별한 레이어에 있는 오브젝트에는 투영하지 않도록 알리는 것이다. 우리는 후자를 사용할 것이다.

  1. Player 오브젝트를 선택하라.
  2. Inspector 창에서 Layer 드롭다운 버튼을 클릭하라.
  3. Add new layer...를 선택하라.
  4. 첫번째로 비어있는 User Layer의 오른쪽 빈칸을 클릭하고 noShadow라고 입력하라.
  5. 아래와 비슷하게 보일것이다.
  6. 다시 Player 오브젝트를 선택하고 Inspector창의 Layer를 noShadow로 변경하라.
  7. 유니티는 모든 자식들에게도 적용할 것이냐고 물을 것이다. “Yes, change children”을 선택하라.

다음은 Blob Shadow Projector에게 noShadow 레이어에 있는 오브젝트에는 투영하지 않도록 알려야 한다.

  1. blob shadow projector 오브젝트를 선택한 후 Projector 컴포넌트의 Ignore Layers 속성에서 noShadow를 선택하라.

이제 실행을 해보면 거의 기대한 대로 그림자가 작동할 것이다. 연료와 같은 수집가능한 아이템 주변에서 점프하는 것만 제외한다면 말이다. 이것을 시도하면, 당신은 그림자가 아이템위에 그려지는 것을 보게 될 것이다.

  1. 프로젝트 패널의 Props 폴더에서 FuelCellPrefab 과 HealthLifePickUpPrefab 프리팹(연료와라이프)을 선택한 후 레이어를 noShadow로 변경하라. (모든 자식들에게도 적용한다.)

알림 - 레이어를 변경할 때 생각없이 모든 자식들에게도 적용하는 것은 위험하다.

스크립팅 개념

링크(게임오브젝트에 연결하기)하는 방법에는 여러가지가 있다.

  1. Inspector 창에 노출된 링크(public 멤버변수) 추가하기. 이것은 적절한 변수로부터 단지 데이터를 뽑아내기만 하는데에는 가장 효율적이다. 우리는 LevelStatus에서 컷신 카메라를 위해 이 옵션을 사용하고 있다.

  1. 스크립트의 Awake() 함수 안에서 링크 설정하기. GameObject.Find() 함수같은 경우 꽤 느리기때문에 Awake() 함수 안에서 단 한번만 호출하는 것이 좋다.

  1. Update() 함수가 호출되는 동안 링크 설정하기. 이 함수는 게임 주기마다 적어도 한번은 호출되는 함수이다. 그러므로 느린 함수는 이곳에서 사용하지 않는게 가장 좋다. 이 옵션은 당신이 필요로 하는 오브젝트의 위치가 언제든지 변할 수 있을 경우에 주로 사용된다. 예를 들면, 튜토리얼에 있는 여러개의 부활장소 중에서 플레이어가 살아나야 할 곳은?? 이것은 분명히 게임 도중에 변화한다. 문제는 이것은 느리다는 것이다. 그러므로 가능한한 이 옵션을 사용하지 않도록 디자인하는게 최선이다.

만약 당신이 원한다면, 우리의 모든 변화를 포함하고 있는 Player 오브젝트를 다른 프로젝트에서 재사용하기 위해서 프리팹으로 만들 수 있다.

  1. 프로젝트 패널에서 Player 폴더를 클릭하라.
  2. 새로운 프리팹을 만들어라. (Player 폴더안에 나타날 것이다.)
  3. 프리팹을 적절한 이름으로 변경하라. (LerpzPrefab을 추천한다.)
  4. 우리의 Player 오브젝트를 LerpzPrefab으로 드래그하라.

죽음과 부활

대부분의 게임 캐릭터는 위험한 삶을 살아가는데 우리의 Lerpz 역시 예외가 아니다. 그가 죽을 수도 있고 죽은 후 “respawn point”라고 불리우는 안전한 장소에서 다시 부활하게 만들어야할 필요가 있다.

또 다른 문제는 Lerpz가 지형 외부로 떨어지는(fallout) 것이다. 이  문제는 다른 거주민들도 역시 마찬가지이며, 이것들은 적절하게 다루어져야 한다.

최적의 해법은 박스 충돌체를 사용하는 것이다. 우리는 플레이어가 제트팩을 이용할 수 있도록 하기 위하여 이것을 매우 길고 광대하게 만들 것이다.

일단은, 트리거로 사용할 박스 충돌체를 만들도록 하자.

  1. 빈 게임오브젝트를 만들어라.
  2. 이름을 FalloutCatcher로 변경하라.
  3. Component > Physics로부터 Box Collider를 추가하라.
  4. Component > Third Person Props로부터 Fallout Death 스크립트를 추가하라.
  5. Inspector 창에서 아래와 같이 설정하라.

Fallout Death 스크립트

이 스크립트는 ThirdPersonStatus 스크립트에서 모든 작업을 대신하고 있기때문에 매우 단순하다. (이것은 Lerpz에 부착되어야 하지만 아직은 아니다.)

충돌체의 트리거를 다루는 코드는 OnTriggerEnter() 이다. 이 함수는 박스 충돌체가 Lerpz 또는 적들처럼 충돌체 컴포넌트를 가지고 있는 다른 게임오브젝트와 부딪혔을 경우 유니티에 의해 호출된다.

3가지 테스트 : 하나는 Player, 또 다른 하나는 단순한 RigidBody,  세번째는 Character Controller 컴포넌트를 가지고 있는 오브젝이다. 두번째는 상자나 크레이트 같은 것들을 테스트하고(우리는 이러한 아이템들을 가지고 있지 않지만 당신이 원하면 추가할 수 있다.) 세번째는 물리가 부착되지 않은 적들을 테스트하는데 사용된다.

만약 Player가 박스 충돌체(FalloutCatcher)와 부딪히면 코드는 단순히 Lerpz의 ThirdPersonStatus 스크립트에 있는 FalloutDeath() 함수를 호출한다.

만약 충돌체를 가진 다른 오브젝트가 부딪히면 우리는 단순히 그것을 장면에서 제거한다.  그렇지 않으면 영원히 떨어질 것이다.

추가적으로 우리는,

  1. Reset() 이라는 유틸 함수를 가지고 있다. 이 함수는 컴포넌트가 추가되기 직전에 자동으로 실행된다. 여기서는 충돌체(Collider) 컴포넌트를 가지고 있지 않을 경우 자동으로 BoxCollider 컴포넌트를 추가시키고 있다. - 아래와 같이 에디터에서 기어모양 아이콘을 클릭함으로써 직접 호출할 수도 있다.

  1. @script AddComponentMenu("Third Person Props/Fallout Death") 코드는 유니티의 Component 메뉴에 Third Person Props > Fallout Death를 직접적으로 추가하여 프로젝트 패널에서 찾기 위한 시간을 줄여준다.

부활 지점 (Respawn Points)

이 강좌에서 Lerpz는 3개의 부활 지점을 가지고 있다. Lerpz가 이 지점들 중에 하나를 만지면, 그것이 활성화되고 그가 죽었을 때 여기서 이 지점에서 다시 나타날 것이다.

부활 지점은 RespawnPrefab 프리팹 오브젝트의 인스턴스들이다. RespawnPrefab의 기본 구조는 아래와 같다.

  1. RSBase - 그 자신의 모델을 포함하고 있다.
  2. RSSpotlight - 모델의 표면으로부터 푸르게 빛나는 스포트라이트.
  3. 남은 오브젝트는 파티클 시스템이다. RespawnPrefab에 부착되어 있는 Respawn 스크립트는 프리팹의 상태에 의존하는 파티클 시스템들 사이를 스위치한다.
  1. 부활 지점이 비활성화되면, 작고 미묘한 파티클 효과가 빛나는 푸른 안개처럼 보인다. 이는 RSParticlesInactive 에 포함되어 있다.
  2. 부활 지점이 활성화되면, 더 크고 과시하는 듯한 효과가 보여진다. 이는 RSParticlesActive 에 포함되어 있다.
  3. 한번에 하나의 부활지점만 활성화 될 수 있다.
  4. 남아있는 3개의 파티클 시스템 - RSParticlesRepawn1, RSParticlesRepawn2, RSParticlesRepawn3 - 은 Player가 부활 지점에서 부활할 때 함께 활성화된다. 이들은 일회성(one-shot) 파티클 시스템이다.

프리팹이 포함하고 있는 Respawn 스크립트는 부활 지점의 상태를 제어한다.하지만, 어느 지점이 부활 지점인지를 게임이 알게하기 위해서, 우리는 Hierarchy 창에서 master controller 스크립트 아래에 부활 지점들을 가지런히 정렬할 필요가 있다. 이제 이것을 수행하자.

  1. RespawnPrefab 을 장면 뷰로 드래그하라.
  2. 아래 이미지에 보이는 곳에 그것을 위치시켜라.
  3. 이 인스턴스의 이름을 Respawn1으로 변경하라.
  4. 위의 과정을 2번 반복하되 위치시킬 지점은 당신이 원하는 곳으로 하라. 하나는 아레나 근처에 또 다른 하나는 정원에 있는 나무 근처에 놓기를 제안한다.
  5. 빈 게임오브젝트를 만들고 RespawnPoints로 이름을 변경하라.
  6. 모든 respawn 프리팹 인스턴스를 RespawnPoints의 자식으로 만들어라.

작동하는 방법

장면이 로드될 때, 유니티는 Respawn 스크립트의 각각의 인스턴스에서 Start() 함수를 호출한다.

주요 구조는 정적(static) 변수를 중심으로 구성된다.

static var currentRespawn : Respawn;

이것은 currentRespawn 이라는 전역변수를 정의한다.

static 키워드는 스크립트의 모든 인스턴스가 공유한다는 것을 의미한다. 이것은 부활 지점이 하나가 되도록 유지시켜준다. 하지만 장면이 시작할 때, 활성화되는 지점은 없다. 그러므로 하나를 기본값으로 지정해야 할 필요가 있다. 유니티는 static 변수를 Inspector 창에 보여주지 않으므로 각각의 인스턴스에 대하여 설정될 필요가 있는 Initial Respawn 속성을 정의한다.

  1. Respawn1을 Inspector 창에 있는 “Initial Respawn” 슬롯위로 드래그하라.
  2. 나머지 부활 지점에 대해서도 “Initial Respawn” 슬롯위로 Respawn1을 드래그하라.

알림 : 원본 프리팹에 대해서는 위의 과정을 수행할 수 없다.

부활 지점이 Player의 충돌체에 의해 활성화되면, Respawn 스크립트는 가장 먼저 이전 부활 지점을 비활성화 한다. 그리고 나서 currentRespawn 에 자기 자신을 설정한다. - 이 과정은 OnTriggerEnter() 함수에서 수행된다. SetActive() 함수는 적정한 파티클 시스템과 사운드 효과를 발생시킨다.

Player의 부활은 Player의 게임상태 대부분을 관리하는 ThirdPersonStatus 스크립트에 의해 다루어진다.

  1. Scripts > Palyer > ThirdPersonStatus 스크립트를 Player 오브젝트에 추가하라.

알림

유니티는 사운드 효과를 추가하기가 매우 쉽다. 당신이 이러한 에셋을 추가하려고 계획할 때마다 어떻게 사용할지를 신중하게 고려해야 한다. 예를 들면 당신은 2개의 부활 지점이 소리가 들릴만큼 가까이 놓기를 원하지 않을 것이다. 즉, 부활 지점이 비활성화 되는 소리를 들을 수는 없기 때문에 우리는 “respawn deactivated” 사운드를 포함하지 않는다. 만약 프로젝트를 멀티플레이어 게임으로 변환한다면 당신은 이러한 사운드를 추가하기를 원할 것이고 그에 해당하는 스크립트가 필요할 것이다.

2. Setting the Scene

첫번째 단계

이번 섹션에서는 세트(영화 용어에서)를 건축하고 소품을 놓고 우리의 영웅이 그것들과 상호작용하도록 만드는 스크립트를 작성할 것이다.

우리는 첫번째 단계는 스테이지를 준비하는 것이다. 이 튜토리얼 파일은 수집가능한 다수의 아이템과 함께 거주가능한 기본 레벨 메쉬를 이미 가지고 있다. 우리는 약간의 소품과 요소를 놓을 것이다. 그러나 대부분은 당신을 위해 이미 놓여져 있다.

Lighting (광원)

레벨(스테이지, 장면)은 이미 수만의 점광원 뿐만 아니라 환경 광원까지 제공하고 있다.

이 프로젝트의 경우, 빛들은 레벨을 모델링한 아티스트에의하여 놓여졌다.

소품 놓기 (Placing Props)

Building Your Own Levels (자신만의 레벨 건축하기)

당신이 실험하기 좋아하거나 심지어 추가적인 레벨을 만들고 싶다면, 장면에 사용된 개별적인 요소들을 프로젝트 패널의 Build Your Own! 폴더에서 찾을 수 있을 것이다.

장면에는 많은 소품들이 있는데 이들을 직접 위치시킬 경우 매우 지루한 튜토리얼이 될 수 있다. 이전 튜토리얼을 읽었다면 당신은 이미 어떻게 하는지를 알고 있다. 그러므로 우리는 체력 회복제, 점프 패드, 부활 지점들로 제한할 것이다.

Health Pickups => 건강 습득물 => 체력 회복제

Health Pickups는 이미 프로젝트 패널에 프리팹으로 정의되어 있다. Props 폴더에서 HealthLifePickUpPrefab 을 찾을 수 있을 것이다.

  1. 장면 뷰로 하나를 드래그하고 레벨의 어딘가에 위치시켜라.
  2. 이 과정을 약 6개정도까지 반복하라.
  3. 빈 게임오브젝트를 만든 후 이름을 HealthPickups로 변경하라. (폴더개념으로 사용할 것이다.)
  4. Hierarchy창의 난잡함을 피하기 위해, 위에서 만든 인스턴스를 모두 HealthPickups의 자식으로 설정하라.

The Force Field => 힘의 장 (힘이 작용하는 영역)

현재, 우리의 영웅의 우주선을 붙잡아 두고 있는 포스 필드(힘의 장)는 애니메이션 되지 않는다. 그것은 단지 정적인 메쉬 텍스처이다. 그리나쁘지 않은 비주얼 효과를 위한 몇가지 방법이 있지만 어떤 것을 선택할 것인가? 가끔은 단순한게 최고이다. 우리는 잔물결이 일어나는 포스 필드 효과를 주기 위해서 텍스처의 UV 좌표를 애니메이션할 것이다.

애니메이션은 짧은 스크립트로 수행이 될 것이며, 이는 프로젝트 패널의 Scripts > Misc 폴더의 FenceTextureOffset 에서 볼수 있을 것이다.

var scrollSpeed = 0.25;

function FixedUpdate()

{

    var offset = Time.time * scrollSpeed;

    renderer.material.mainTextureOffset = Vector2(offset,offset);

}

첫 번째 라인은 스크롤 속도를 유니티 인터페이스에서 직접 입력할 수 있도록 속성을 노출시킨다. FixedUpdate() 함수는 초당 일련의 횟수 만큼 호출된다. (고정프레임이라는 얘기) mainTextureOffset 속성은 유니티에게 텍스처의 UV 공간안에서 그려질 이미지의 offset을 전달한다.

Pretty Properties (깔끔한 속성)

유니티의 Inspector 창에서 속성이나 변수를 보여줄 때, 그 이름은 좀 더 멋지게 보이도록 적용된다. 그래서 scrollSpeed는 Inspector 창에 Scroll Speed로 나타날 것이다.

  1. Hierarchy 패널에서 LevelGeometry > impoundFence 오브젝트에 FenceTextureOffset 스크립트를 첨부하라.

수집가능한 아이템 스크립트

현재 Lerpz는 어떤 아이템도 집을 수 없다. 우리는 각각의 수집가능한 아이템에 2가지 요소를 추가할 필요가 있다.

  1. 충돌체(Collider) 컴포넌트
  2. 충돌체를 다루고 플레이어의 체력을 업데이트하는 스크립트, 등.

Hierarchy 패널에서 수집가능한 아이템은 모두 프리팹 인스턴스이고 푸른색으로 표시된다. 원본 프리팹을 수정하면 자동적으로 게임내의 모든 아이템에 적용된다.

우리의 영웅이 수집할 수 있는 2개의 프리팹은 FuelCellPrefab 과 HealthPickUpPrefab 이다. 이것은 프로젝트 패널의 Props 폴더에서 찾을 수 있다.

  1. HealthPickUpPrefab 오브젝트의 루트(최상위)를 선택하라.
  2. Component > Physics > Sphere Collider 를 추가하라.
  3. 마지막으로 Sphere Collider의 Is Trigger 속성을 체크하라.

알림

프리팹에 스핀을 주는 매우 단순한 ObjectRotater 스크립트가 이미 첨부되어 있다. 후반부에는 더욱 복잡한 애니메이션 스크립트를 보게 될 것이다.

충돌체(Collider)는 두가지 쓰임새가 있다 : 다른 오브젝트와의 충돌에 사용하거나, 트리거로 사용한다.

Triggers (방아쇠, 자동적으로 동작을 수행하도록 하는 것)

트리거는 보이지 않는 컴포넌트이며 이벤트이다. 유니티에서 트리거는 단순히 Is Trigger 속성이 설정된 충돌체(Collider)이다. 이것은 무언가가 트리거와 부딪혔을 때, 물리적인 개체가 아닌 가상 스위치처럼 작동한다는 것을 의미한다.

트리거는 OnTriggerEnter(), OntriggerStay(), OnTriggerExit() 라는 세가지 이벤트 메시지를 전송한다.

트리거 이벤트 메시지는 트리거 오브젝트에 부착된 어떤 스크립트던지 보내진다. 그러므로 우리는 적절한 스크립트가 필요하다.

  1. Component > Third Person Props > Pickup 스크립트를 선택하라.
  2. Pickup Type 속성을 Health로 설정하라.
  3. Amount 속성을 3정도로 설정하라. 이것은 플레이어에게 주는 체력 수치이다.

플레이어의 최대 체력치(health)는 6이다. 만약 체력이 가득찬 상태에서 Health Pickup을 먹으면 어떻게 되는가? 이 튜토리얼에서는 추가적인 생명력(life)를 주도록 하였다. 이 로직은 플레이어의 상태를 체크하는 ThirdPersonStatus 스크립트에서 찾을 수 있다. (실제 보면 그냥 최대치로 제한하는 내용밖에 없네요;;)

FuelCellPrefab 은 거의 같은 방법으로 설정한다. 단지 2가지만 다를 뿐이다.

  1. Pickup Type 속성을 FuelCell로 설정해야 한다.
  2. Amout 값은 주어진 값(1)을 그대로 사용한다.

점프 패드

점프 패드는 밝은 노란색과 검은색의 줄무늬로 표시되어 있다. 이것은 Lerpz를 공중으로 밀어올리기 위해 제안되었다. 우리는 이를 위해 스크립트가 부착된 충돌체를 사용할 것이다.

먼저, 빈 게임오브젝트를 생성하고 Jump Pad Triggers로 이름을 변경하라. 우리는 이를 점프 패드 트리거 오브젝트들의 폴더로 사용할 것이다.

이제 우리의 프리팹을 빌드할 것이다.

  1. 빈 게임 오브젝트를 생성하라.
  2. 이름을 JumpPad Trigger 1으로 변경하라.
  3. 박스 충돌체(Box Collider)를 추가하라. (구형 충돌체(Sphere Collider)도 이론적으로 작동하지만 박스 충돌체가 점프 패드의 모양에 더 잘 어울린다.)
  4. 박스 충돌체를 트리거로 설정하라. (Box Collider의 Is Triggers 속성을 체크하라.)
  5. Component > ThirdPersonProps > JumpPad 스크립트를 추가하라.

오브젝트가 생성되었으니, 이제 프리팹으로 변환할 필요가 있다.

  1. 프로젝트 패널에서 Props 폴더 안에 프리팹을 생성하라.
  2. JumPad Trigger 1 오브젝트를 프리팹으로 드래그하라.
  3. 프리팹의 이름을 JumPad Trigger로 변경하라.
  4. JumPad Trigger 1 오브젝트를 Hierarchy 패널에서 삭제하라.
  5. JumPad Trigger 프리팹을 장면으로 드래그하여 점프 패드가 있는 곳에 위치시켜라. (총 6개의 장소가 있다.)
  6. 모든 인스턴스를 Jump Pad Triggers 오브젝트의 하위로 이동하라.
  7. Jumppad 스크립트의 Jump Height 기본 값인 5는 정원위로 던지기에는 충분하지 않다. 이 패드에는 15~30정도의 값을 제안한다. (마지막 프로젝트에는 30을 사용한다.)
  8. 마지막으로 우리의 트리거가 잘 작동하는지 테스트하라.

하얀뱀

우리가 만든 프리팹과 동일한 기능을 하는 JumpPad 프리팹이 이미 만들어져 있습니다. 뭐야 이거..

알림

스크립트는 프리팹과 유사하게 작동한다. 프로젝트 패널에서 스크립트 파일을 수정하면 장면에 있는 모든 복사본에 적용이 된다.

좋은 조직은 당신의 작업흐름이 부드러워 지고 작은 다툼으로 부터 자유로워지게 하는데 중요하다.

  1. 가능하면 프리팹의 인스턴스를 사용하라.
  2. 타입보다는 기능위주로 조직화하라.
  3. 컨테이너(폴더)로써 빈 게임오브젝트를 사용하라.

당신은 심지어 작은 규모의 프로젝트를 위해서 얼마나 많은 에셋이 필요한지 놀랄 것이다.

3. The GUI

유저 인터페이스

게임들은 일반적으로 메뉴, 옵션 화면 등과 같은 그래픽 유저 인터페이스(GUI)를 가지고 있다. 게다가, 게임들은 종종 게임 그 자신의 최상위에 GUI를 덧 씌운다. 이것은 구석에 점수를 보여주는 것처럼 단순할 수 있거나 복잡한 아이콘을 정성스럽게 디자인하거나 인벤토리를 보여줄 수 있다.

유니티 2의 새로운 GUI 시스템

이전 시스템은 전통적인 이벤트 드리븐  GUI 모델을 사용하였다. 그러나 유니티 2에서는 즉시 모드(Immediate Mode) GUI로 알려진 새로운 GUI 시스템을  소개한다. 만약 당신이 전통적인 GUI 시스템을 사용했다면, 즉시 모드 GUI 컨셉은 충격으로 다가올 것이다.

여기 예제가 있다.

function OnGUI()

{

    If (GUI.Button (Rect(50, 50, 100, 20), "Start Game") )

        Application.LoadLevel("FirstLevel"); // load the level.

}

OnGUI() 는 게임주기마다 최소한 2번은 호출된다. 처음 호출에서는 GUI를 만들고 그것을 그린다. 이 경우, 우리는 지정한 좌표에 단순한 버튼을 그리고 그 안에 “Start Game”을 표시한다.

두번째는 유저 입력이 진행될 때 호출된다. 만약 유저가 버튼을 클릭하면, If(...)문의 조건이 true를 리턴하고 Application.LoadLevel() 이 호출될 것이다.

다른 GUI 요소들 - 레이블, 그룹, 체크박스 등등 - 모두 비슷하게 작동한다.

여기서 분명한 이점은 당신이 GUI를 위한 이벤트 핸들러가 필요하지 않다는 것이다. 모든 것이 OnGUI() 함수에 포함되어 있다.

유니티 2는 두 세트의 즉시 모드 GUI 함수들을 제공한다 : 위의 예제와 같은 기본 GUI 클래스와 이와 유사하지만 시간을 절약하려는 당신을 위해 GUI의 레이아아웃을 다루는 GUILayout 클래스이다.

추가적인 정보

유니티의 새로운 GUI 시스템에 대해서 더 많은 정보는 아래 링크에서 찾을 수 있다.

  1. http://unity3d.com/support/documentation/Components/GUI%20Scripting%20Guide.html
  1. http://unity3d.com/support/documentation/ScriptReference/GUI.html

The In-game HUD - 화면위에 투명하게 제공되는 정보

우리의 게임은 플레이어의 체력, 생명력, 수집해야할 연료의 숫자를 표시하는 GUI가 필요하다. 그래픽적인 요소는 이미 프로젝트에 포함되어 있다.

우리의 GUI는 다양한 요소를 배치하기 위한 새로운 GUI 컴포넌트를 사용하는 GameHUD 스크립트안에서 다루어진다. 이 스크립트는 Level 게임오브젝트에 첨부될 필요가 있다. (우리는 쉽게 NearCamera 오브젝트나 자신의 고유한 ‘GUI’ 게임오브젝트에 추가할 수도 있다. 이것은 게임의 디자인을 결정하는 핵심이라기 보다는 개인적인 취향이다.)

우리는 Level 게임오브젝트를 레벨한정 상태와 다른 스크립트를 다루기 위해 사용할 것이다.

GUI 스킨 오브젝트

유니티 2의 새로운 GUI 시스템은 스킨 시스템을 제공한다. 이것은 룩앤필을 완전히 제어할 수 있게 해준다.

GUISkin 에셋은 웹사이트의 CSS 파일처럼 유니티의 GUI의 모양을 정의하며 기본 모양을 변경할 필요가 있을때 요구된다. 우리는 폰트를 변경할 것이므로 장면에 GUISkin 을 포함할 필요가 있다.

  1. 새로운 GUI Skin 오브젝트를 생성하기 위해 Assets 메뉴를 사용하라. 이것은 프로젝트 패널에 나타날 것이고 기본적인 유니티 GUI 스킨 데이터를 포함하고 있을 것이다.
  2. GUI 폴더 안으로 이동한 후 이름을 LerpzTutorialSkin 으로 변경하라.
  3. GUI 폴더안의 ‘FLOURIDE” 폰트를 스킨 에셋의 Font 속성위로 드래그하라.

GUI Skin 오브젝트는 Hierarchy 패널에는 추가되지 않는다. 대신에 우리는 우리의 GameHUD 스크립트에서 직접적으로 참조한다.

알림

이미지의 크기를 2의 승수로 만들지 않을 경우 유니티에서 많은 경고를 보게 될 것이다. 이는 성능을 위해 빠른 계산을 하기 위함이다.

이제 플레이어의 체력 정보와 연료 상태를 표시해보자.

  1. Component > Scripts > GameHUD 스크립트를 Hierarchy 패널의 Level 오브젝트에 추가하라.
  2. 프로젝트 패널의 GUI 폴더에 있는 GUIHealthRing, GUIFuelCell 이미지를 각각 GameHUD 스크립트의 Health Image, Fuel Cells Image 에 지정하라.
  3. 스크립트에서 Health Pie Images 속성을 클릭하여 확장하라.

Health Pie Images 는 배열이다. 유니티는 배열의 크기를 알지 못하므로 0으로 설정된다. 우리는 6개의 파이 이미지를 이 배열에 추가할 것이므로 값을 변경해야 한다.

  1. Size 값을 6으로 변경하라.
  2. 프로젝트 패널의 GUI 폴더에 있는 healthPie1~6 이미지들을 Health Pie Images 배열의 0~5에 순서대로 드래그하라.
  3. Gui Skin 속성에 우리가 만들었던 LerpzTutorialSkin 오브젝트를 드래그하라.

이제 게임을 시작하면 아래와 같이 HUD를 볼 수 있을 것이다.

해상도 독립

GUI.matrix = Matrix4x4.TRS (Vector3.zero, Quaternion.identity, Vector3

 (Screen.width / 1920.0, Screen.height / 1200.0, 1));

or

GUI.matrix = Matrix4x4.TRS (Vector3(0, 0, 0), Quaternion.identity, Vector3 (Screen.height / nativeVerticalResolution, Screen.height / nativeVerticalResolution, 1));

GUI와 관련된 하나의 문제점은 그것의 크기이다. 우리의 HUD는 화면 해상도에 따라서 동적으로 크기가 변화해야 한다. 이것을 어떻게 처리할 것인가?

유니티 2의 새로운 GUI 시스템은 변환행렬(transform matrix)을 지원한다. 이 행렬은 렌더링 보다 우선하여 모든 GUI 요소에 적용되므로 그들은 동적으로 변환(회전, 확대/축소)될 수 있다.

게임 뷰에서 Maximize on Play 옵션을 활성화 하고 플레이해보면 그에 맞게 변화된 GUI를 보게 될 것이다.

시작 메뉴

모든 게임은 시작 메뉴를 필요로 한다.

알림

스플래시 스크린, 메뉴와 같은 모든 것들은 유니티의 장면(Scene)이다. 하지만 게임 레벨은 아니다. 게임 레벨(일종의 스테이지)은 일반적으로 장면이지만, 장면이 언제나 게임 레벨인것은 아니다.

시작 메뉴를 위해 우리에게 필요한 것들

  1. 두 개의 GUI 텍스트 버튼 : “Play” 와 “Quit”.
  2. 게임의 이름. 이것은 사용자 폰트를 이용하여 렌더링 될 것이다.
  3. 몇개의 적절한 음악.
  4. 몇 종류의 배경.

즉, 아래와 같은 형식이다.

장면 세팅하기

우선은 새로운 (비어있는) 장면을 만들어야 한다.

  1. File > New Scene 를 선택하거나 Ctrl + N을 누른다.
  2. Ctrl + S를 누른 후 Scenes 폴더에 StartMenu로 저장한다.

이제 우리는 메뉴를 만들기 위해 GUI 시스템을 이용할 것이다.

  1. 프로젝트 패널에 빈 스크립트(C Sharp) 파일을 생성하라.
  2. 이름을 StartMenuGUI로 변경하고 에디터로 열어라.

우리는 우리의 스크립트가 실행중이 아니더라도, 즉 에디트 상태에서도 실행되도록 할 것이다. 클래스 상단에 아래 코드를 작성하라. (전체 코드는 아래부분에서 제공할 것이다.)

[ExecuteInEditMode]

우리는 LerpzTutorialSkin 에셋을 링크할 필요가 있으므로 아래 코드를 추가한다.

public GUISkin gSkin;

우리는 배경을 위한 Texture2D 오브젝트가 필요하다. (Inspector 패널에서 이미지를 지정할 것이다.)

public Texture2D backdrop; // 우리의 배경 이미지는 여기에 지정된다.

우리는 플레이어가 “Play” 버튼을 클릭했을 때 “Loading...” 메시지를 표시하기를 원하므로 이를 다루기 위한 플래그가 필요할 것이다.

private bool isLoading = false; // 참이면, "Loading..." 메시지를 표시할 것이다.

마지막으로 우리는 OnGUI 함수가 필요하다.

void OnGUI()

{

        if (gSkin)

                GUI.skin = gSkin;

        else

                Debug.Log("StartMenuGUI : GUI Skin object missing!");

위 코드는 GUI Skin 오브젝트의 유효성을 체크한다.

배경 이미지(Backdrop)

배경 이미지는 배경으로 우리의 이미지를 사용하도록 설정한 GUI.Label 요소이다. 이것은 텍스트가 없고 언제나 우리의 디스플레이의 크기에 자동으로 맞춰진다.

GUIStyle backgroundStyle = new GUIStyle();

backgroundStyle.normal.background = backdrop;

GUI.Label(new Rect((Screen.width - (Screen.height * 2)) * 0.75f, 0, Screen.height * 2, Screen.height), "", backgroundStyle);

먼저, 우리는 새로운 GUIStyle() 오브젝트를 정의했다. 이는 기본 GUI 스킨 스타일을 덮어쓰기 위해 사용할 것이다. 이 경우, 우리는 단지 기본 배경(normal.background) 스타일을 우리의 배경 이미지로 변경하였다.

GUI.Label() 함수에 주어진 Rect 오브젝트는 이미지가 언제나 화면을 가득 채우도록 하기 위한 화면 해상도에 따른 사각형의 크기이다. 위의 코드에 의해 이미지의 가로세로 비율은 일그러지지 않고 유지되며, 화면에 맞게 짤리거나 적절하게 확대/축소 될 것이다.

다음은 타이틀 텍스트이다. 이를 위한 스크립트 코드를 작성하기 전에 먼저 해야할 일이 있다.

우리가 만들었던 LerpzTutorialSkin 에셋에 정의되어 있는 폰트는 거대한 게임 타이틀로는 적절하지 않다. 그러므로 우리는 스킨에 mainMenuTitle이라는 이름의 새로운 GUI Style 을 추가할 것이다.

  1. LerpzTutorialSkin 에셋의 Custom Sytle 속성을 확장한 후 Size 를 1로 설정하라.
  2. Element 0을 확장하고 위와 같이 설정하라. (색상을 설정할 때 스포이드 툴을 이용하면 편하다.)

마지막으로 남은 코드를 아래와 같이 작성한다.

GUI.Label(new Rect((Screen.width / 2) - 197, 50, 400, 100), "Lerpz Escapes", "mainMenuTitle");

알림

GUI Style의 이름은 대소문자를 구분하지 않는다. 그러므로 “mainMenuTitle”과 “mainmenutitle”은 서로 같다.

버튼 (Button)

우리는 좀 더 흥미로운 버튼을 만들기 위해 LerpzTutorialSkin 에셋의 GUI Button 속성을 수정할 필요가 있다.

우리는 시작 메뉴와 HUD에서 같은 GUI 스킨을 사용할 것이다. HUD에서는 기본 폰트를 변화하는 걸로 충분했지만, 시작 메뉴에서는 버튼 텍스트 뒤에 그래픽적 이미지를 가질 필요가 있다. 기본 버튼은 게임의 비주얼 스타일에 적합하지는 않으므로 우리는 그것을 변경할 것이다.

당신의 요소들이 Hover(마우스가 올려진 상태), Focus(선택한 상태) 그리고 Active(클릭된 상태) 이벤트에 반응하기를 원한다면, 당신은 반드시 배경 이미지를 설정해야 한다.

  1. 프로젝트 패널에서 LerpzTutorialSkin 에셋을 선택하고 Button 스타일을 아래와 같이 설정하라.

이제 “Play” 버튼을 추가하자.

if (GUI.Button(new Rect((Screen.width / 2) - 70, Screen.height - 160, 140, 70), "Play"))

{

        isLoading = true;

        Application.LoadLevel("TheGame"); // TheGame 레벨을 로드하라.

}

위의 코드가 “Play” 버튼을 화면에 그리고 클릭 이벤트를 다루는 모든 것이다. 우리는 “Loading...” 메시지를 보여주기 위해 isLoading 값을 참으로 설정했다.

“Quit” 버튼은 위와 유사하게 다루어진다. 하지만 독립실행형( 또는 에디터에서 실행중)인지 확인을 해야 한다. 즉, 웹 스트리밍이 아니라는 것을 보장해야 한다.

bool isWebPlayer = (Application.platform == RuntimePlatform.OSXWebPlayer || Application.platform == RuntimePlatform.WindowsWebPlayer);

if (!isWebPlayer)

{

        if (GUI.Button(new Rect((Screen.width / 2) - 70, Screen.height - 80, 140, 70), "Quit"))

                Application.Quit();

}

마지막 단계는 “Loading...” 메시지를 출력하는 것이다.

if (isLoading)

        GUI.Label(new Rect((Screen.width / 2) - 110, (Screen.height / 2) - 60, 400, 70), "Loading...", "mainMenuTitle");

}

다음 단계는 몇가지 음악을 추가하는 것이다.

  1. 프로젝트 패널의 Sounds 폴더를 열고 StartMenu 오디오 파일을 Hierarchy 패널의 Main Camera 위로 드래그하라. 이는 메인 카메라에 Audio Source 컴포넌트를 추가할 것이다.
  2. 추가된 Audio Source 컴포넌트의 Play On Awake 속성과 Loop 속성을 체크하라.
  3. 지금까지 작성했던 StartMenuGUI 스크립트를 Main Camera 위로 드래그하여 추가하라.
  4. 마지막으로, 추가된 StartMenuGUI 컴포넌트의 GSkin 속성에 LerpzTutorialSkin을 적용하고 Backdrop 속성에 StartSplashScreen 을 적용하라.

드디어 우리의 시작 메뉴가 완성이 되었다.

하얀뱀

튜토리얼에는 타이틀 텍스트의 크기를 조절하는 내용이 빠져있습니다. 빠진 부분에 대해서는 제가 추가하였습니다.

우선 타이틀 텍스트의 크기를 Inspector 패널에서 조절할 수 있도록 아래와 같이 멤버변수를 선언합니다.

public float gameTitleScale = 2.0f;

다음으로 타이틀 텍스트를 그리기 전에 GUI 시스템의 변환행렬을 이용하여 크기를 조절합니다.

GUI.matrix = Matrix4x4.TRS(new Vector3(0, 0, 0), Quaternion.identity, Vector3.one * gameTitleScale);

GUI.Label(new Rect((Screen.width / (2 * gameTitleScale)) - 197, 50, 400, 100), "Lerpz Escapes", "mainMenuTitle");

마지막으로 위에서 설정한 변환행렬이 다른 요소들에게 적용이 되지 않도록 하기위해 행렬을 다시 초기화 합니다.

GUI.matrix = Matrix4x4.identity;

이제 Insepctor 패널에 보면 Game Title Scale 라는 속성이 추가되었고 이 값을 조절하여 타이틀 텍스트의 크기를 조절하시면 됩니다.

하얀뱀

한번 Insepctor 패널에 값이 노출되면 소스코드에서 값을 바꿔도 Inspector 패널의 값은 이전 값으로 계속 유지됩니다. 그러므로 소스코드를 변경하였는데 적용되지 않는다고 고민하지 마시고 Insepctor 패널의 값을 변경하세요.

아래는 StartMenuGUI.cs 파일의 전체 소스입니다.

using UnityEngine;

using System.Collections;

[ExecuteInEditMode]

public class StartMenuGUI : MonoBehaviour

{

        // Use this for initialization

        void Start()

        {

        }

        // Update is called once per frame

        void Update()

        {      

        }

        void OnGUI()

        {

                if (gSkin)

                        GUI.skin = gSkin;

                else

                        Debug.Log("StartMenuGUI : GUI Skin object missing!");

                // 배경 이미지.

                GUIStyle backgroundStyle = new GUIStyle();

                backgroundStyle.normal.background = backdrop;

                GUI.Label(new Rect((Screen.width - (Screen.height * 2)) * 0.75f, 0, Screen.height * 2, Screen.height), "", backgroundStyle);

                // 타이틀.

                GUI.matrix = Matrix4x4.TRS(new Vector3(0, 0, 0), Quaternion.identity, Vector3.one * gameTitleScale);

                GUI.Label(new Rect((Screen.width / (2 * gameTitleScale)) - 197, 50, 400, 100), "Lerpz Escapes", "mainMenuTitle");

                // 행렬 초기화.

                GUI.matrix = Matrix4x4.identity;

                // Play 버튼.

                if (GUI.Button(new Rect((Screen.width / 2) - 70, Screen.height - 160, 140, 70), "Play"))

                {

                        isLoading = true;

                        Application.LoadLevel("TheGame"); // TheGame 레벨을 로드하라.

                }

                // Quit 버튼.

                bool isWebPlayer = (Application.platform == RuntimePlatform.OSXWebPlayer || Application.platform == RuntimePlatform.WindowsWebPlayer);

                if (!isWebPlayer)

                {

                        if (GUI.Button(new Rect((Screen.width / 2) - 70, Screen.height - 80, 140, 70), "Quit"))

                                Application.Quit();

                }

                // 로딩 메시지.

                if (isLoading)

                        GUI.Label(new Rect((Screen.width / 2) - 110, (Screen.height / 2) - 60, 400, 70), "Loading...", "mainMenuTitle");

        }

        public float gameTitleScale = 2.0f;

        public GUISkin gSkin;

        public Texture2D backdrop; // 우리의 배경 이미지는 여기에 지정된다.

        private bool isLoading = false; // 참이면, "Loading..." 메시지를 표시할 것이다.

}

게임 오버

게임 오버 화면은 플레이어가 도전을 완수하거나 실패했을 때 보여진다. 시작 메뉴와 달리 이 장면은 버튼이라던지 유저와 상호작용하는 그 무엇도 없다. 단지 음악이 재생되는 동안 배경이미지 위에 “Game Over” 메시지만 보여준다. 일단 음악 재생이 완료되거나 유저가 마우스를 클릭하면 자동적으로 Start Menu 장면이 로드된다.

  1. 새로운 장면을 만들고 이름을 “GameOver”로 저장하라.
  2. GameOverJingle 오디오 파일을 Main Camera 오브젝트 위로 드래그한 후 아래와 같이 설정하라.

우리는 유니티 에디터를 이용해 더이상 추가할 것이 없다. 메인 카메라 하나면 충분하다.

다음 단계는 스크립트를 만드는 것이다.

  1. GameOverGUI 라는 이름의 새로운 스크립트를 만든 후 아래 코드를 작성하라.

[ExecuteInEditMode]

시작 메뉴에서도 사용했듯이 에디터에서 스크립트가 수행되도록 하는 코드이다.

시작 메뉴에서는 LerpzTutorialSkin GUI Skin 에셋을 이용하여 전체적으로 스타일을 적용했었다.

또 다른 방법으로는 개별적인 GUI 스타일을 직접적으로 정의하는 방법이 있다. GameOver 스크립트에서는 이 방법을 사용할 것이고 GUI 스타일 변수를 노출하여 Inspector 패널에서 설정할 수 있도록 할 것이다.

아래 변수를 선언하라.

public GUIStyle background;

public GUIStyle gameOverText;

public GUIStyle gameOverShadow;

우리는 “Game Over” 메시지의 크기 값을 기본 폰트보다 더 크게 정의할 필요가 있다. 우리는 이 메시지를 그림자 효과를 주기 위해 2번 렌더링할 것이다. 그러므로 아래와 같이 변수를 선언하라.

public float gameOverScale = 1.5f;

public float gameOverShadowScale = 1.5f;

이제 OnGUI() 함수를 작성한다.

우선, 시작 메뉴에서 사용했던 방법과 유사하게 배경 이미지를 출력한다.

void OnGUI()

{

        GUI.Label(new Rect((Screen.width - (Screen.height * 2)) * 0.75f, 0, Screen.height * 2, Screen.height), "", background);

다음 작업은 “Game Over” 메시지의 그림자 버전을 그리는 것이다. 우리는 텍스트의 크기를 변경하고 화면의 중앙에 출력할 필요가 있다. 다행히도, 우리는 GUI 시스템에 내장된 변환행렬(transform matrix)을 이용하여 크기를 조절할 수 있다.

GUI 변환행렬은 자유로운 이동, 회전, 크기 등을 수행하기 위해서 사용될 수 있다.

GUI.Label() 함수에 전달되는 gameOverShadow 스타일의 텍스트가 어두운 그림자 색상이 되도록 하라.

GUI.matrix = Matrix4x4.TRS(new Vector3(0, 0, 0), Quaternion.identity, Vector3.one * gameOverShadowScale);

GUI.Label(new Rect((Screen.width / (2 * gameOverShadowScale)) - 150, (Screen.height / (2 * gameOverShadowScale)) - 40, 300, 100), "Game Over", gameOverShadow);

마지막으로, 우리는 좀 더 밝은 색상으로 같은 텍스트를 그린다. 유니티의 GUI 시스템은 언제나 코드에 작성된 순서대로 요소들을 렌더링한다. 그러므로 이 텍스트는 그림자의 위에 나타날 것이다.

GUI.matrix = Matrix4x4.TRS(new Vector3(0, 0, 0), Quaternion.identity, Vector3.one * gameOverScale);

GUI.Label ( new Rect( (Screen.width / (2 * gameOverScale)) - 150, (Screen.height / (2 * gameOverScale)) - 40, 300, 100), "Game Over", gameOverText);

}

전체 소스코드는 아래와 같다.

using UnityEngine;

using System.Collections;

[ExecuteInEditMode]

public class GameOverGUI : MonoBehaviour

{

        // Use this for initialization

        void Start()

        {

        }

        // Update is called once per frame

        void Update()

        {      

        }

        void OnGUI()

        {

                // 배경 이미지.

                GUI.Label(new Rect((Screen.width - (Screen.height * 2)) * 0.75f, 0, Screen.height * 2, Screen.height), "", background);

                // Game Over 텍스트 그림자.

                GUI.matrix = Matrix4x4.TRS(new Vector3(0, 0, 0), Quaternion.identity, Vector3.one * gameOverShadowScale);

                GUI.Label(new Rect((Screen.width / (2 * gameOverShadowScale)) - 150, (Screen.height / (2 * gameOverShadowScale)) - 40, 300, 100), "Game Over", gameOverShadow);

                // Game Over 텍스트.

                GUI.matrix = Matrix4x4.TRS(new Vector3(0, 0, 0), Quaternion.identity, Vector3.one * gameOverScale);

                GUI.Label ( new Rect( (Screen.width / (2 * gameOverScale)) - 150, (Screen.height / (2 * gameOverScale)) - 40, 300, 100), "Game Over", gameOverText);

                // 행렬 초기화.

                //GUI.matrix = Matrix4x4.identity;

        }

        public GUIStyle background;

        public GUIStyle gameOverText;

        public GUIStyle gameOverShadow;

        public float gameOverScale = 1.5f;

        public float gameOverShadowScale = 1.5f;

}

이제 스크립트를 적용할 단계이다.

  1. 스크립트를 저장하고 Main Camera에 추가하라.
  2. 이제 Main Camera를 선택하고 Inspector 패널에서 속성들을 수정할 차례이다.
  3. 우선, Background 스타일의 Normal.Background 속성에 아래와 같이 GameOverSplashScreen 이미지를 지정하라.
  4. 다음으로, Game Over Text 스타일의 색상과 폰트를 아래와 같이 설정하라.
  5. Game Over Scale 를 1.69로 설정하라.
  6. 이제 Game Over Shadow 스타일의 색상고 폰트를 아래와 같이 설정하라.
  7. Game Over Shadow Scale 을 1.61로 설정하라.

이제 아래와 같은 화면을 보게 될 것이다.

마지막 작업은 Main Camera 에 두번째 스크립트를 추가하는 것이다. 이 스크립트는 음악 재생이 종료되었는지를 체크한 후 Start Menu 장면을 로드한다.

  1. 새로운 스크립트를 만들고 GameOverScript로 명명하라.
  2. 아래와 같이 코드를 작성하라.

using UnityEngine;

using System.Collections;

public class GameOverScript : MonoBehaviour

{

        // Use this for initialization

        void Start()

        {

        }

        // Update is called once per frame

        void Update()

        {      

        }

        void LateUpdate()

        {

                if (!audio.isPlaying || Input.anyKeyDown)

                        Application.LoadLevel("StartMenu");

        }

}

이 코드는 오디오 재생이 끝나거나 플레이어가 키를 눌렀는지를 체크한다. LateUpdate() 함수는 매 프레임마다 호출된다. 하지만 모든 Update() 함수의 호출이 끝난 다음에 호출이 되므로 캐릭터가 움직인 후 쫓아가야 하는 카메라 움직임 같은 경우에 주로 사용된다.

  1. GameOverScript를 Main Camera에 추가하라.

이제 GUI를 완료하였다.

4. Adversaries

Lerpz 는 두 종류의 장애물을 만난다. 하나는 로봇 가드이고 다른 하나는 레이저 방벽이다.

레이저 트랩

레이저 함정은 레이저 통로에 위치한다. 그리고 우리의 플레이어어가 그 빔을 만질경우 피해를 준다.

통로에 2개의 레이저를 설치할 것이다. 레이저는 위아래로 움직인다.

레이저 트랩 만들기

각각의 레이저 트랩은 같은 수직 평면에서 위아래로 움직이는 빔이다. 플레이어나 적들이 빔에 맞는다면, 대미지를 입을 것이다.

  1. 빈 게임오브젝트를 생성하고 이름을 “Laser”로 변경하라.
  2. Component > Miscellaneous > Line Renderer 컴포넌트를 추가하라.

아직은 레이저를 장면에 위치시키려고 하지마라. 당신은 우선 “Use World Space”의 체크박스를 해제할 필요가 있다.

  1. LaserTrap 스크립트를 추가하라.
  2. Lin Renderer 를 아래와 같이 설정하라. (laser 재질은 Particles > Sources > Materials 에 있다.)
  3. 레이저 터널(감옥으로 부터 멀리 떨어진 아레나로 가는 통로)에 오브젝트를 위치시켜라.
  4. 점광원(Point Light) 오브젝트를 만들고 Laser 의 자식으로 만들어라.
  5. 점광원을 아래와 같이 설정하라.

점광원은 레이저의 빛으로 사용되고 레이저와 함께 위아래로 움직일 것이다. 이것은 레이저빔과 같은 착각을 준다.

Line Renderer 컴포넌트

라인 렌더러는 3D 공간상에 선을 그린다. 이것은 선이 그려질 점의 배열을 가지고 있다. 라인렌더러는 Trail Renderer 컴포넌트와 같은 기술을 이용하여 그려진다.

  1. LaserTrap 스크립트의 속성을 아래와 같이 설정하라. (LaserHit 에셋은 프로젝트 패널의 Props 폴더에서 찾을 수 있다.)
  2. 이제 레이저 트랩을 복사하여 다른 곳에도 위치시켜라. 터널마다 2개씩 놓는 것이 좋다.

LaserTrap 스크립트

레이저 트랩의 핵심은 LaserTrap 스크립트이다. 그러므로 자세히 살펴보자.

레이저 트랩은 스크립트에 의해 위아래로 움직이는 라인 렌더러 컴포넌트다. 스크립트는 또한 충돌을 체크하고 충돌시 발생하는 비주얼 이펙트를 발생시킨다.

이제 스크립트의 몇가지 기본 속성에 대해서 알아보자.

  1. height - 위 아래로 진동하는 폭을 정의.
  2. speed - 빔이 움직이는 속도.
  3. timingOffset - 각각의 레이저 트랩이 서로 다른 위치에서 시작하도록 한다.
  4. laserWidth - 레이저 빔의 가로 크기.
  5. damage - 플레이어가 받는 대미지.
  6. hitEffect - 무언가가 레이저 트랩에 부딪혔을 때 인스턴스화 될 수 있는 임의의 게임오브젝트를 링크할 수 있다. 이것은 레이저트랩 내부에서 이펙트를 하드코딩하는 것보다 훨씬 유연하다. 그리고 다른 프로젝트에서 이 에셋을 재사용하기 쉽게 만들어준다.

스크립트에는 2개의 함수를 정의했다.

Start() 함수에서는 나중을 위해 라인 렌더러 컴포넌트의 초기 위치값을 저장한다. 그리고 라인 렌더러의 두번째 정점(vertex)를 laserWidth에 의해 정의되는 위치와 매칭되도록 설정한다. 이는 복도의 넓이와 매칭되는 빔의 길이를 쉽게 조절할 수 있게 한다.

알림

우리는 라인 렌더러의 배열에서 마지막 정점만을 수동으로 설정했다. 그러나 이를 스크립트에서 다룬다면 더욱 유연하게 레이저 빔을 애니메이션 시킬 수 있다.

Update() 함수를 살펴보자.

처음에는 레이저 빔의 움직임을 계산한다. 우리는 이를 위해 Mathf.Sin() 함수를 사용했다. 우리는 현재시간을 Time.time(게임이 시작한 이후의 초 단위 시간)을 이용해 알아내고 그것을 우리의 애니메이션 속도와 곱한 후 timingOffset 값을 더한다. 이것은 우리에게 싸인 곡선에 따른 위치를 알려준다. 마지막으로, 그것을 우리가 원하는 높이(height)로 조절하고 그 결과를 오프셋(offset)으로 사용한다.

다음은 충돌 체크이다. 스크립트에서는 빔의 궤도에 따라 광선을 쏘고 충돌체를 가진 어떤 게임오브젝트가 이에 부딪히는지를 체크하는 것으로 충돌체크를 수행한다.

(시간에 기반한 테스트는 반사작용할 시간을 허용한다. 그렇지 않으면 플레이어는 레이캐스팅(광선을 쏘아 부딪히는 것을 검사하는 것)에 의해 매 프레임마다 체력을 잃어버릴 것이다.)

만약 무언가가 부딪힌다면, 우선 그것이 Player 또는 Enemy 인지를 체크한다. 만약 그렇다면, “Apply Damage”를 그것에게 전달한다. 동시에 우리는 이펙트 효과를 발생시키기 위해 hitEffect 게임오브젝트를 인스턴스화 한다.

만약, 플레이를 한 후 레이저 빔에 부딪히면 체력이 소모되는 것을 볼 수 있다.

로봇 가드

움직이는 적대자들은 로봇 가드이다. 이들은 레벨 곳곳에 전략적으로 위치한다. Lerpz가 그들의 범위안에 들어서면 그들은 Lerpz에게 접근하여 해를 가한다.

로봇에 대한 사항

  1. 로봇은 상당히 기본적인 인공지능을 가진다.
  2. 로봇은 넉다운 당했을 때 수집가능 아이템을 분출한다.
  3. 로봇은 플레이어가 그들을 볼 수 없을 때 리스폰된다.

찾고 파괴하기

대부분의 게임 인공지능은 지능과 같은 것보다는 무엇보다 먼저 행동에 집중된다. 우리의 로봇은 작은 인공지능을 보여준다. 하지만 그저 플레이어가 가는 길을 예측하여 반응할 뿐이다. 이것은 반드시 나쁜 것은 아니다. 많은 플레이어들은 이와 같은 행동을 좋아한다. 이것은 그들이 로봇을 파괴하는 방법을 쉽게 익힐 수 있도록 만든다.

로봇 가드는 매우 단순한 행동 패턴을 가지고 있고 이는 EnemyPoliceGuy 스크립트에 작성되어 있다.

  1. Idle - 이 모드에서는 그저 서 있기만 하고 범위 안에 침입자가 들어오기를 기다린다.
  2. Threaten - 침입자가 범위안에 들어오면, 로봇 가드는 대기한 후 지팡이를 회전하면서 그것의 목적을 알린다.
  3. Seek & Destroy - 일단 활성화되면, 로봇 가드는 침입자에게 접근하고 그에게 공격을 시도한다.
  4. Struck - 만약 플레이어가 로봇 가드를 고격하면, 이 애니메이션이 재생된다.

만약 플레이어가 로봇의 탐색 제한 범위를 벗어나면, 로봇은 다시 Idle 모드로 돌아간다.

로봇 가드 추가하기.

  1. 프로젝트 패널의 Enemies 폴더에서 Copper 프리팹을 장면의 적절한 곳에 가져다 놓아라. (이 프리팹은 Character Controller 를 포함하고 있다. 캡슐 충돌체의 하단부가 지형에 닿거나 약간 위에 위치하도록 하라.)

울타리 또는 둘러싸인 지역은 로봇이 배경으로 떨어지지 않도록 하는데 가장 좋다.

이제 플레이를 하면 로봇이 Idle 애니메이션을 보이면서 서있을 것이다. 이제 스크립트를 추가해야 한다.

로봇을 위한 2개의 스크립트가 있는데 이를 원본 프리팹에 직접 추가할 것이다.

  1. 프로젝트 패널의 Scripts > Enemies 폴더에 있는 EnemyDamage, EnemyPoliceGuy 스크립트를 Copper 원본 프리팹위로 드래그하라.
  2. Player 오브젝트에 ThirdPersonCharacterAttack 스크립트를 추가하라. (이 스크립트가 있어야 플레이어가 공격할 수 있다.)

이제 플레이를 해보면, 당신이 가까이 갔을 때 로봇이 반응할 것이다.

죽음의 푸른 불꽃.

플레이어가 로봇을 넉다운 시키면, 죽음의 시퀀스가 시작된다. 우리는 플레이어가 로봇의 범위밖으로 벗어나 로봇이 리셋되기 전까지 로봇이 넘어져서 푸른 불꽃을 쏟아내기를 원한다.

추가적으로, 로봇이 죽을 때, 우리는 로봇이 수집가능한 아이템을 뱉어내길 원한다.

더 많은 스크립트와, 애니메이션 데이터 그리고 다른 요소들을 현재 프리팹(Copper)에 추가하는 것 보다, 우리는 단지 새로운 것을  만들것이다.

로봇 가드 스크립트.

플레이어가 로봇을 공격하면, ApplyDamage 메시지가 로봇에게 전달되고, 이는 로봇의 ApplyDamage() 함수를 호출한다. 로봇이 너무 많은 공격을 받으면 -- 우리의 예제에서 HP는 3으로 설정했다. -- 죽게 되고 이때 Die() 함수를 호출하게 된다.

Die() 함수는 Copper 게임 오브젝트를 파괴한다. (오브젝트 파괴는 Update() 함수가 완료되기 전까지는 수행되지 않는다. 그러므로 함수의 시작에 오브젝트를 파괴하는 코드를 사용해도 상관없다.)

다음으로, 로봇의 현재 위치에 대체 프리팹을 인스턴스화(복사) 한다. - CopyTransformsRecurse() 함수에서 자식들의 모든 변환을 복사한다.

불꽃 폭발은 단지 또 다른 파티클 시스템 에셋이다. 이것은 정확한 위치에 나타나기 위해 죽어가는 로봇 오브젝트의 자식으로 인스턴스화 된다.

다음 단계는, 우리의 인스턴스를 넘어지고 구르게 하도록 플레이어로부터 멀리 밀어낸다.

마지막으로, 우리는 최대 2개의 픽업 아이템을 인스턴스화 한다. 체력과 연료셀은 50대 50의 확률로 만들어지고 그것들은 랜덤한 방향의 공중으로 튀어오른다.

하얀뱀

로봇가드(Copper) 프리팹에 추가한 스크립트의 속성에 노출된 값들을 지정하는 부분이 튜토리얼에는 빠져있습니다. 그 부분을 추가해보겠습니다.

우선 프로젝트 패널에서 Props > FuelCellPrefab, HealthLifePickUpPrefab 프리팹에 Component > Scripts > Droppable Mover 스크립트를 추가합니다.

다음으로, 추가된 Droppable Mover 스크립트의 Collision Mask 속성을 Everything로 변경합니다.

EnemyDamage 스크립트

Explosion Prefab - Enemies > RobotExplode 지정.

Dead Model Prefab - Enemies > CopperDead 지정.

Health Prefab - Props > HealthLifePickUpPrefab 지정.

Fuel Prefab - Props > FuelCellPrefab 지정.

Drop Max - 2로 변경.

Struck Sound - Sounds > CopperNewHitSFX 지정.  <- 다음 챕터에서 다루고 있음.

Enemy Police Guy 스크립트

Idle Sound - Sounds > CopperIdleLoop 지정.  <- 다음 챕터에서 다루고 있음.

Attack Sound - Sounds > CopperNewAttackSFX 지정.  <- 다음 챕터에서 다루고 있음.

Droppable Pickups & Physics => 떨어뜨릴 수 있는 물체와 물리

체력과 연료 프리팹은 이전에 만들었지만 여기에 DroppableMover 스크립트를 추가해야 한다. 왜냐하면 이들 프리팹에 있는 충돌체(Collider)는 트리거로 설정이 되었고 이는 물리엔진이 무시하기 때문이다.

로봇 생성과 최적화

플레이어와의 지정된 거리내에서 적들을 생성하는 것은 오래된 기법이며 이는 각각의 적들의 상태를 저장할 필요가 없게 만듦으로써 프로세서의 부담을 줄여줄 수 있다. 단순하게 플레이어에게 보이지 않는 로봇들을 삭제함으로써, 우리는 불필요한 AI와 스크립트의 실행을 피할 수 있다.

시작해보자

  1. 장면에 생성되어 있는 Copper 오브젝트를 제거하라.
  2. Hierarchy 패널에 빈 게임 오브젝트를 생성하고 이름을 CopperSpawn으로 변경하라.
  3. CopperSpawn에 EnemyRespawn 스크립트를 추가하라.
  4. 로봇이 나타나길 원하는 곳에 CopperSpawn 오브젝트를 위치시켜라. (당신은 장면 뷰에서 생성범위를 보여주는 구로써 작은 로봇 아이콘을 보게 될 것이다. 이는 EnemyRespawn 스크립트에 의해 그려진다.)
  5. 오브젝트를 아래와 같이 설정하라.

우리는 이 게임 오브젝트가 좀 더 필요할 것이다. 그러므로 우리의 CopperSpawn 오브젝트들을 관리할 부모 오브젝트를 만들자.

  1. 빈 게임오브젝트를 만들고 이름을 Enemies로 변경하라.
  2. CopperSpawn 오브젝트를 Enemies로 드래그하여 하위 오브젝트로 만들어라.
  3. 이제 CopperSpawn 오브젝트를 이용해 프리팹을 만들고 지형 곳곳에 이프리팹의 인스턴스들을 만들어라.

알림

이 기술의 부작용 하나는 당신이 로봇을 죽인 후 범위 밖으로 이동하면, 당신이 다시 돌아왔을 때 새것처럼 다시 나타난다는 것이다.

작동 방법

CopperSpawn 오브젝트는 플레이어가 범위 내로 접근하면 Copper 프리팹의 인스턴스를 생성하고 다시 범위밖으로 나가면 자동으로 생성했던 인스턴스를 파괴한다.

이는 EnemyRespawn 스크립트에 작성되어 있다. 이 스크립트의 두 가지 주요 함수는,

Start() -- 두고두고 사용할 플레이어의 위치(Transform)를 저장해 둔다.

Update() -- 먼저 플레이어가 범위 내에 있는지 체크하여 참이면 로봇을 인스턴스화하고 그렇지 않고 범위 밖에 있으면 로봇 인스턴스를 파괴한다.

또한 편집기에서 사용되는 2개의 기즈모 함수가 있다.

기즈모는 일반적으로 비주얼적인 목적으로 장면 뷰에서만 표시된다.

이 스크립트에서는, 일단 로봇 아이콘을 그리고 오브젝트가 선택되었을 때 생성(spawn) 범위를 보여주는 구체를 그리고 있다.

매번 유니티 편집기의 GUI가 업데이트 되면 OnDrawGizmos() 함수가 호출되어 언제나 아이콘이 보이도록 한다.

반대로 OnDrawGizmosSelected() 함수는 유니티 편집기 GUI에 의해 오브젝트가 선택되어 있는 동안에만 호출된다.

하얀뱀

  1. 아놔~ 튜토리얼씨!! 왜케 빼먹은게 많습니까?? 객체가 죽었을 때 CopperDead모델을 새로 생성시키고 나서 다시 제거하는 내용이 없습니다. 이 부분을 추가해보죠.
  2. 우선 스크립트 파일을 하나 만들고 이름을 DestroyDeadModel로 변경합니다.
  3. 스크립트의 내용을 아래와 같이 작성합니다. (저는 C#을 사용합니다.)

using UnityEngine;

using System.Collections;

public class DestroyDeadModel : MonoBehaviour

{

        // Use this for initialization

        void Start()

        {

                player = GameObject.FindWithTag("Player").transform;

        }

        // Update is called once per frame

        void Update()

        {

                float distanceToPlayer = Vector3.Distance(transform.position, player.position);

                if (distanceToPlayer > destroyRange)

                        Destroy(gameObject);

        }

        private Transform player;

        public float destroyRange = 50.0f;

}

  1. 이 스크립트를 프로젝트 패널의 CopperDead 프리팹에 추가합니다.
  2. 이제 플레이어와 일정거리(50)이상 멀어지면 CopperDead 인스턴스는 자동으로 파괴가 됩니다.
  3. destroyRange의 값은 원하시는 값으로 수정하세요.

다른 최적화 방법

위에서 사용한 방법외에, 유니티는 OnBecameVisible() 함수와 OnBecameInvisible() 함수를 제공한다. 이는 화면에 보였을 때와 보이지 않았을 때 호출되는 함수이다. 이를 이용하여 로봇 오브젝트를 장면에서 제거하면 단지 카메라 방향을 돌리는 것만으로도 로봇이 생성/파괴될 수 있다. 이는 우리가 원하는 상황은 아니다.

또 다른 방법으로는, 우리의 것보다 더울 최적화된 것으로, 플레이어의 위치를 확인하기 위해 스크립트 코드를 사용하는 대신 일정 범위에 충돌체(Collider)를 만들고 이를 트리거로 사용하는 것이다. 유니티는 이를 위해 OnTriggerEnter()와 OnTriggerExit() 함수를 제공한다. 하지만 이는 충돌체를 다른 용도로 사용할 오브젝트에 추가되는 스크립트에서는 불가능한 방법이다.

5. Audio & Finishing Touches

이 챕터에서 우리는 사운드 효과와 2개의 컷씬을 추가할 것이다.

샘플 노트

유니티는 오디오에 대하여 상당히 단순한 인터페이스를 가지고 있지만, 고려해야할 몇가지 중요한 사항이 있다.

  1. 모든 음악샘플이 비슷한 볼륨 레벨을 가지도록 하라. 또한, 사운드가 서로 믹스되었을 때를 고려해야 한다.
  2. 3D 공간상에서 현실적으로 배치되기를 원한다면 모노 사운드를 사용하라.
  3. Ogg Vorbis 압축 시스템을 사용하라. (유니티가 대신 해 줄 수 있다.)
  4. 만약 웹기반으로 개발 중이라면, 언제나 Ogg Vorbis로 압축해야 한다.
  5. 재생 시간이 짧고 자주 사용되는 사운드 효과는 “Decompress on load”(불러올 때 압축해제) 를 체크하라.
  6. 장면에 공간적으로 배치될 필요성이 없는 재생시간이 긴 음악 작품에 대해서는 스테레오 사운드를 사용하라. 이러한 샘플 파일들은 언제나 기본 오디오 리스터 볼륨(default Audio Listner Volume)에서 재생될 것이다.

게임에 사운드 추가하기.

게임에 필요한 사운드 효과 리스트.

플레이어

  1. 걷는 / 뛰는 소리
  2. 공격하는 소리
  3. 공격받는 소리
  4. 죽는 소리
  5. 제트 팩 추진 소리

로봇가드

  1. 평상시 소리
  2. 공격하는 소리
  3. 공격받는 소리
  4. 죽는 / 폭박하는 소리

아이템

  1. 연료셀을 수집하는 소리
  2. 체력을 수집하는 소리

주위 환경 소리

  1. 분위기를 조성하기 위한 길고 반복되는 소리
  2. 점프패드를 위한 ‘휙’하는 소리

감금된 우주선 장벽

  1. 활성화 사운드
  2. 운전 정지 사운드

우주선

  1. 컷씬 애니메이션과 함께 재생될 이륙 소리

우리는 이미 약간의 사운드 효과를 추가하였지만, 아직 15개 이상을 추가해야 할 필요가 있다.

주변(환경, ambient) 소리

이 강좌에서는 sceneAtmosphere 라는 이름의 반복되는 주변(환경) 소리 파일을 포함하고 있다. 이 파일은 Ogg Vorbis 샘플이다.

  1. 이 샘플을 Hierarachy 패널에서 Near Camera 오브젝트에 드래그하라.
  2. 아래와 같이 설정하라.

우리는 사운드가 자동으로 재생되도록 하기 위해 Play On Awake 을 체크하였다. 이 사운드가 배경음으로써 게임 내의 다른 사운드들을 방해하지 않게 하기 위해 의도적으로 볼륨 레벨을 낮추었다. Pitch 옵션은 재생 속도를 정의하는 것으로 1은 보통 속도를 의미한다. (2는 2배속을 의미한다.) Rolloff 는 사운드의 볼륨이 청취자의 거리에 따라 얼마나 빨리 변화하는지를 정의한다.

점프 패드

우리가 점프 패드 트리거 프리팹을 만들 때 사운드 효과를 추가하지 않았다. 하지만 Jumppad 스크립트에는 이미 사운드를 재생하는 코드가 작성되어 있다.

...

                if (audio)

                {

                        audio.Play();

                }

...

스크립트 내에는 audio 변수가 정의되어 있지 않다. 이는 유니티 자체에 정의되어 있는 다수의 편의 변수 (convenience variable) 중 하나이며 게임 오브젝트에 첨부된 Audio Source 컴포넌트를 가리킨다. 우리는 이 컴포넌트를 추가하지 않았으므로, audio 변수는 null 이 될 것이고, if() 문은 거짓이 될 것이다.

  1. Hierarchy 패널에 있는 JumpPad Trigger 오브젝트 하나를 선택한 후 프로젝트 패널에서 jumpPad 사운드를 Inspector 패널위로 드래그하라.
  2. Play On Awake 를 체크해제 하라.
  3. 원본 프리팹에 적용하여 모든 점프 패드가 같은 사운드 효과를 공유할 수 있도록 Inspector 패널의 상단에 있는 Apply 버튼을 클릭하라.
  4. Audio Source의 설정을 원하는 형태로 설정하라.

수집품

수집가능한 아이템은 다루기 가장 쉽다. Pickup 스크립트에는 이미 사운드를 재생하는 코드가 작성되어 있으므로 sound 변수의 값만 지정해주면 된다.

  1. 게임 레벨상에 존재하는 FuelCells 오브젝트 중 하나를 선택하고 Pickup 스크립트의 Sound 속성에 pickupFuel 사운드를 지정하라.
  2. 사운드 효과가 잘 들리도록 사운드 볼륨을 2로 설정하라.
  3. Apply 버튼을 눌러 모든 FuelCells 오브젝트에 적용하라.
  4. 위의 과정과 마찬가지로 HealthPickups 오브젝트에 pickupHealth 사운드를 지정하라.

프리팹의 인스턴스에 적용 후 Apply 버튼으로 적용하는 것보다 프리팹 원본에 직접 추가하여 시간을 절약할 수 있다.

우주선 장벽

  1. Hierarchy 패널에서 levelGeometry > impundFence 를 찾아라.
  2. 이 오브젝트에 activeFence 사운드를 추가하라.
  3. 아래와 같이 설정하라.

플레이어

이번에 구현할 사운드 효과는 아래와 같다.

  1. 펀치 소리
  2. 공격받는 소리
  3. 제트팩 추진 소리
  4. 플레이어가 죽은 후 되살아날 때 소리
  5. (발자국 소리는 연습용으로 놔두겠습니다.)

펀치

펀치 동작과 애니메이션은 ThirdPersonCharacterAttack 스크립트에 의해 다루어진다. 해당 스크립트의 속성을 보면 Punch Sound 속성이 노출되어 있다.

  1. Punch Sound 속성에 lerpzPunch 사운드를 지정하라.

알림

게임 실행중에 이런한 일회성 사운드가 Hierarchy 패널에 잠깐 나타나는 것을 볼 것이다. 이는 정상이다.

맞는 소리(Struch Sound)와 죽는 소리(Death Sound)

ThirdPersonStatus 스크립트는 2개의 사운드 효과를 다룬다. 하나는 플레이어가 적에 의해 공격당했을 때 그리고 다른 하나는 플레이어가 죽었을 때 나는 소리이다.

  1. Struck Sound 에 lerpzStruck 사운드를 지정하라.
  2. Death Sound 에 LerpzScreamSFX 사운드를 지정하라.

제트 팩

제트 팩은 일회성이 아닌 반복되는 사운드 효과이므로 플레이어 오브젝트에 직접 추가하여 Audio Source 컴포넌트로 만들 것이다. 이 사운드는 JetPackParticleController 스크립트에 의해서 재생이 되며 또한 이 스크립트에 의해 이미 비어있는 Audio Source 컴포넌트가 추가되어 있을 것이다.

  1. thrusterSound 사운드를 Player 오브젝트 위로 또는 비어있는 Audio Source 컴포넌트 위로 드래그 하라.
  2. 아래와 같이 설정하라.

위 사운드는 점프 중에만 재생을 한다. 그리고 사운드를 반복하기 위해 Loop 속성을 사용하지 않고 스크립트에서 사운드가 종료될 때 마다 다시 재생시키고 있다.

음악이 재생되는 도중에 다시 Play()함수를 호출하면 언제나 처음부터 다시 재생된다.

로봇 가드

자신의 Audio Source 컴포넌트를 오직 제트팩 소리에만 사용하는 플레이어와는 달리 EnemyPoliceGuy 스크립트는 Copper 프리팹의 Audio Source 컴포넌트를 여러가지 사운드로 바꿔가면서 재생하는데 사용한다. 예제 코드는 아래와 같다.

        if (idleSound)

        {

                if (audio.clip != idleSound)

                {

                        audio.Stop();

                        audio.clip = idleSound;

                        audio.loop = true;

                        audio.Play();        // play the idle sound.

                }

        }

위의 예제는 Idle() 함수에서 볼 수 있다. 위 코드에서 audio.Stop() 함수의 호출은 매우 중요하다. 정지하지 않고 재생중에 사운드를 교체하는 것은 알 수 없는 결과를 가져오기 때문이다.

  1. 프로젝트 패널에서 Copper 프리팹을 선택하라.
  2. CopperIdleLoop 사운드를 Audio Source 컴포넌트로 드래그하라.
  3. 같은 사운드를 EnemyPoliceGuy 스크립트의 Idle Sound 속성에도 드래그하라.
  4. CopperActiveLoop 사운드를 EnemyPoliceGuy 스크립트의 Attack Sound 속성에 드래그하라.
  5. 마지막으로, metalHit 사운드를 EnemyDamage 스크립트의 Struck Sound 속성에 드래그하라.

metalHit 사운드는 EnemyDamage 스크립트의 ApplyDamage() 함수에서 재생된다.

사운드 파일을 추가한 후 Copper 프리팹의 Inspector 패널 모습은 아래와 같다.

하얀뱀

위의 스크린샷을 보면 Audio Source 컴포넌트의 Play On Awake 속성이 체크되어 있는데 이럴 경우, 로봇 가드의 근처에 가지 않아도 소리가 나게 된다. 위의 속성을 체크 해제하자.

하지만 여전히 한번 소리가 나면 죽이기 전에는 소리가 사라지지 않는다. ㅡㅡㅋ 요 처리는 나중에...

Cut Scenes, 컷씬

여기서 우리는 두 개의 컷씬을 사용한다.

첫번째는 플레이어가 모든 연료셀을 모으는 데 성공하여 우주선의 락이 풀렸을 때 발생한다. 이 컷씬은 그림안의 그림(picture-in-picture) 기술을 이용하여 나타나므로, 플레이어는 계속 움직일 수 있다.

알림

여전히 장벽 근처의 상자에 올라 우주선에 접근하는 것이 가능하지만, 우주선은 잠겨서 이륙하지 않을 것이다. (장벽이 풀리기 전까지는 메쉬 충돌체가 트리거 타입으로 변화되지 않는다.)

두번째는 장벽이 비활성화 된 후 플레이어가 우주선을 만졌을 때 발생한다. 이 장면에서는 전체화면으로 플레이가 되며, 우리는 자유와 새로운 모험을 날아가는 것을 보게 된다. 그 후에는 게임이 종료될 것이다.

이제 첫번째 컷씬을 자세히 알아보자.

장벽 해제하기

우리는 장벽에 애니메이션과 사운드 효과를 추가하였다. 이제는 이것을 해제하여보자.

하지만 우선, 우리가 모든 연료셀을 모았는지를 확인하자.

  1. ThirdPersonStatus 스크립트를 열고 FoundItem() 함수를 찾아라.
  2. 주석 아래쪽에 다음 코드를 작성하라.

        if (remainingItems == 0)

        {

                levelStateMachine.UnlockLevelExit(); // ...and let our player out of the level.

        }

  1. 스크립트를 저장하라.
  2. LevelStatus 스크립트를 열어라. 이 스크립트에서 두 개의 컷씬이 다루어진다.
  3. 스크립트 최상단에 아래 코드를 추가하라.

var exitGateway: GameObject;

var levelGoal: GameObject;

var unlockedSound: AudioClip;

var levelCompleteSound: AudioClip;

var mainCamera: GameObject;  

var unlockedCamera: GameObject;

var levelCompletedCamera: GameObject;

위 코드에는 두번째 컷씬에 필요한 변수도 포함되어 있다.

  1. 다음에는 Awake() 함수에 아래 코드를 추가하라.

        levelGoal.GetComponent(MeshCollider).isTrigger = false;

이 코드는 두번째 컷씬이 너무 이르게 트리거 되는 것을 막아주는 중요한 코드이다.

이제는 장벽을 해제하여 보자.

  1. LevelStatus 스크립트에 다음 함수를 추가하라.

function UnlockLevelExit()

{

        mainCamera.GetComponent(AudioListener).enabled = false;

유니티는 하나의 장면에 오직 하나의 Audio Listener 컴포넌트만을 지원한다. 일반적으로 이 컴포넌트는 메인 카메라에 부착이 되어 있다. 그러나 우리는 여러개의 카메라를 사용하고 있으므로 장벽이 비활성화되는 소리는 듣기 위해 잠시 컷씬 카메라의 Audio Listener 컴포넌트를 활성화해야 한다.

다음이 우리의 컷씬 카메라를 작동시키고 Audio Listener 컴포넌트를 활성화하는 코드이다.

        unlockedCamera.active = true;

        unlockedCamera.GetComponent(AudioListener).enabled = true;

장벽에 있는 사운드 효과를 정지한다.

        exitGateway.GetComponent(AudioSource).Stop();

이제 우리는 장벽이 해제되는 사운드 효과를 컷씬 카메라의 위치에 재생해야 한다.

        if (unlockedSound)

        {

                AudioSource.PlayClipAtPoint(unlockedSound, unlockedCamera.GetComponent(Transform).position, 2.0);

        }

우리의 사운드 효과가 시작되는 것과 함께 우리는 컷씬 애니메이션 절차를 시작할 수 있다. 우리는 이 절차를 스크립트에서 수행할 것이다. 첫번째 줄은 애니메이션이 시작하기 전에 시간을 지연시켜 플레이어가 의식할 수 있도록 한다.

        yield WaitForSeconds(1);

 

        exitGateway.active = false; // ... the fence goes down briefly...

 

        yield WaitForSeconds(0.2); //... pause for a fraction of a second...

        exitGateway.active = true; //... now the fence flashes back on again...

        yield WaitForSeconds(0.2); //... another brief pause before...

        exitGateway.active = false; //... the fence finally goes down forever!

이제 우리는 배에 접근할 수 있다. 이제 우리는 우주선의 메쉬 충돌체에 트리거 속성을 부여해야 한다.

        levelGoal.GetComponent(MeshCollider).isTrigger = true;

마지막으로 플레이어가 이 결과를 볼 수 있도록 몇 초간 일시정지한다. 그 후 컷씬 카메라를 끄고 다시 NearCamera 를 활성화 한다. 그리고 Audio Listener를 NearCamera로 돌려보낸다.

        yield WaitForSeconds(4); // give the player time to see the result.

 

        // swap the cameras back.

        unlockedCamera.active = false; // this lets the NearCamera get the screen all to itself.

        unlockedCamera.GetComponent(AudioListener).enabled = false;

        mainCamera.GetComponent(AudioListener).enabled = true;

컷씬 카메라 1 만들기

이제 우리가 해야할 일은 컷씬 카메라를 만드는 것이다.

  1. 장면에 새 카메라를 추가하라.
  2. 이름을 CutSceneCamera1으로 변경하라.
  3. 카메라에 Scripts > Camera > SmoothLookAt 스크립트를 추가하라.
  4. 카메라가 바라볼 방향을 계산하기위해 스크립트의 Target 속성에 spaceShip 오브젝트를 드래그 하라.
  5. 나머지 설정은 아래와 같이 하라.

하얀뱀

카메라에 의해 출력되는 내용이 화면 전체가 아닌 우측 상단 일부분이 되도록 하는 설정이 바로 Normalized View Port Rect 부분입니다. 위에서 설정한 값은 화면의 (69%, 69%) 위치에 가로/세로 30%의 크기로 나타나게 하고 있습니다.

이 컷씬 카메라는 스크립트에 의해 활성화될 것이므로 기본적으로 비활성화 시키도록 한다. 비활성화 하는 방법은 Inspector 패널 상단에 있는 체크박스를 해제하기만 하면 된다.

이제 테스트하기 위해 Level오브젝트에 첨부된 LevelStatus 스크립트의 속성을 아래와 같이 설정하라.

위에서 Items Needes 를 2로 설정하여 연료셀을 2개만 먹어도 컷씬이 테스트할 수 있게 하였다. 테스트 후 다시 20으로 돌려놓는 것을 잊지 말아라.

알림

초당 프레임 수 카운터는 최적화 챕터에서 다루고 있다.

이제 플레이를 하고, 연료셀을 모두 모으면 컷씬을 볼 수 있다.

컷씬 애니메이션.

마지막 컷씬은 약간 더 복잡하다. 우주선이 날아가아가도록 해야 하는데 이를 스크립트로 할 수도 있지만 애니메이션 클립을 이용하는게 훨씬 더 쉽다.

  1. 프로젝트 패널의 Animations > shipAnimation 에셋을 Hierarchy 패널의 spaceShip 오브젝트로 드래그 하라.

게임을 실행하면 우주선이 이륙하는 것을 볼 수 있다. 하지만 우리는 레벨이 완료(게임을 클리어) 되었을 때에만 이륙하기를 원한다. 이것은 스크립트에서 할 것이다. 그러나 유니티는 기본적으로 애니메이션이 자동으로 실행되도록 되어있다. 이것을 멈추어보자.

  1. spaceShip의 Animation 컴포넌트에서 Play Automatically 속성을 체크 해제하라.

컷씬 카메라 2 만들기.

다음은 2번째 컷씬 카메라를 만든다.

  1. 카메라 오브젝트를 새로 만들고 이름을 CutSceneCamera2로 변경하라.
  2. 아래와 같이 우주선 장벽을 관리하는 사무실 건물의 위쪽으로 위치시켜라.

카메라가 바라보는 방향은 상관없다. 위치가 중요하다. 나머지는 스크립트에서 처리할 것이다.

  1. SmoothLookAt 스크립트를 카메라에 추가하라.
  2. 스크립트의 Target 속성에 spaceShipt 오브젝트를 드래그하라.

컷씬 카메라 1 과 마찬가지로 우리는 이것을 비활성화 시켜야 한다. 하지만 나머지 설정은 약간 다르다. 주요 차이점은 이 카메라에 의한 컷씬이 구석에 일부분만 나타나는게 아니라 전체화면으로 보여져야 하기때문에 Normalized View Port Rect 의 값을 아래와 같이 지정해야 한다는 점이다.

이 컷씬은 연속된 트리거 메시지를 다뤄야 하므로 첫번재 컷씬보다는 약간 다루기 힘들다.

초기 트리거는 플레이어가 우주선을 터치했을 때 발생한다. 그러므로 우주선 모델은 트리거 이벤트를 다루기 위한 스크립트가 필요하다.

  1. 새로운 자바스크립트 파일을 생성하고 이름을 HandleSpaceshipCollision 으로 변경하라.
  2. 아래 코드를 작성하라.

private var playerLink : ThirdPersonStatus;

function OnTriggerEnter (col : Collider)

{

        playerLink=col.GetComponent(ThirdPersonStatus);

 

        if (!playerLink) // not the player.

        {

                return;

        }

        else

        {

                playerLink.LevelCompleted();

        }

}

하얀뱀

위 코드에는 ThirdPersonStatus 스크립트를 불러오는 부분이 있습니다. 문제는 이 스크립트가 자바스크립트로 작성이 되어있기 때문에 이번 스크립트를 C#으로 만들게 되면 서로 호환이 되지 않습니다. 그러므로 어쩔 수 없이 이번 스크립트 파일은 자바스크립트로 작성해야 합니다.

위의 코드는 플레이어가 우주선을 터치할 경우, ThirdPersonStatus 스크립트의 LevelCompleted() 함수를 호출한다.

  1. spaceShip 오브젝트에 방금 만든 스크립트를 추가하라.

ThirdPersonStatus 스크립트의 LevelCompleted() 함수는 훨씬 짧다.

  1. 다음 함수를 ThirdPersonStatus 스크립트에 추가하라.

function LevelCompleted ()

{

        levelStateMachine.LevelCompleted();

}

levelStateMachine은 LevelStatus 컴포넌트(스크립트)를 불러온 것이다. LevelStatus는 레벨완료 애니메이션이 발생하는 곳이다.

  1. LevelStatus 스크립트에 LevelCompleted() 함수를 추가하라.

function LevelCompleted()

{

먼저, 첫번째 컷씬에서 했던 것처럼 Audio Listener 를 전환해야 한다.

        mainCamera.GetComponent(AudioListener).enabled = false;

        levelCompletedCamera.active = true;

        levelCompletedCamera.GetComponent(AudioListener).enabled = true;

다음으로, 우리는 플레이어가 우주선 안에 있는 것처럼 보이게 해야 하므로 그를 안보이게 할 것이다. 이를 위해, 플레이어의 ThirdPersonController 스크립트에 “HidePlayer” 메시지를 보낼것이다. 이 함수는 플레이어를 렌더링하는 것을 멈추게 하여 보이지 않게 한다.

        playerLink.GetComponent(ThirdPersonController).SendMessage("HidePlayer");

로봇은 플레이어가 보이는지를 체크하는게 아니라 위치를 체크하므로 안전하게 플레이어의 위치를 500 units 위로 이동할 것이다.

        playerLink.transform.position+=Vector3.up*500.0; // just move him 500 units

다음은 레벨이 완료되었음을 알리는 사운드 효과를 발생시킬 것이다. 이 경우에는, 우주선이 이륙하는 소리이다.

        if (levelCompleteSound)

        {

                AudioSource.PlayClipAtPoint(levelCompleteSound, levelGoal.transform.position, 2.0);

        }

이제 우리는 이전에 작성한 타임라인 기반 애니메이션을 시작하고 끝나기를 기다린다.

        levelGoal.animation.Play();

 

        yield WaitForSeconds (levelGoal.animation.clip.length);

마지막으로, 우리는 “Game Over” 장면을 로드한다.

        Application.LoadLevel("GameOver"); //...just show the Game Over sequence.  

이제 우리는 spaceShip 오브젝트가 레벨이 시작할 때(게임이 시작할 때), 트리거가 아니도록 설정이 되었는지 확인해야 한다. 그리고 playerLink 가 Player 오브젝트를 가리키도록 해야 한다. 우리는 스크립트가 로드될 때 유니티에서 자동으로 이 변수를 초기화 하도록 Awake() 함수 안에 작성할 것이다.

  1. Awake() 함수를 아래와 같이 수정하라.

private var playerLink: GameObject; // player object.

function Awake()

{

        playerLink = GameObject.Find("Player");

        if (!playerLink)

                Debug.Log("Could not get link to Lerpz");

        levelGoal.GetComponent(MeshCollider).isTrigger = false; // spaceShip 오브젝트 트리거 해제.

}        

마지막으로 우리는 Level 오브젝트의 LevelStatus 스크립트의 속성을 아래와 같이 설정해야 한다.

하얀뱀

전체 흐름은 다음과 같다.

1. 우주선과 플레이어가 부딪힌다.

2. 우주선의 OnTriggerEnter() 함수가 플레이어의 LevelComplete() 함수를 호출.

3. 플레이어의 LevelComplete() 함수는 단순히 Level의 LevelComplete() 함수 호출.

4. GameOver 장면 로드.

이제 모든 연료를 모으고 우주선을 터치했을 때 아래와 비슷하게 보여야 한다.

6. Optimizing

렌더링 최적화 : 초당 프레임율 관찰하기.

게임에 최적화가 필요한지 알아보는 최상의 방법은 초당 프레임률(fps)을 알아보는 것이다. 우리는 이를 Scripts > GUI 폴더에 있는 FPS 스크립트에서 구현하였다. 이 스크립트는 소스코드에 자세한 설명까지 포함되었으므로 따로 설명을 하지는 않지만, GUIText 컴포넌트가 필요하다는 것을 알아둬야 한다.

Stats (Statistics, 통계) 화면 이해하기.

게임 뷰의 상단에는 “Stats”라는 버튼이 있다. 이를 활성화하면, 추가적인 정보를 얻을 수 있다.

이러한 통계들은 카메라가 렌더링하고 있는 것들에 기반한다. 그러므로 장면 주변을 돌아다니면 통계의 많은 값들이 변할 것이다.

중요한 요소는 다음과 같다.

  1. Draw Calls -- 렌더 패스(Render Passes)의 수.  

장면에 있는 요소들은 여러번 렌더링 될 수 있다 : 그림자, 다수의 카메라, 텍스처에 렌더링하기, 픽셀 광원 등등. 반사 또는 굴절 같은 복잡한 셰이더 역시 추가적인 렌더링을 발생시킬 수 있다.

  1. Tris -- 그려지고 있는 삼각형의 수.

모든 3D 모델들은 삼각형으로 만들어진다. 삼각형이 적을 수록 더 빨리 렌더링이 된다. 둥글고 곡선형의 오브젝트들은 일반적으로 기본적인 정육면체나 평면처럼 직선형의 오브젝트보다 많은 삼각형을 사용한다.

  1. Verts -- 그래픽 칩으로 보내지고 있는 (삼각형을 이루는) 꼭지점의 수.

삼각형들끼리 공유하는 꼭지점이 많을수록  더 복잡한 모델을 렌더링할 수 있다.

  1. Used Textures -- 렌더링하는데 사용되는 텍스처의 수.

재질은 하나 이상의 텍스처를 사용할 수 있다.

파티클 시스템은 파티클마다 2개의 삼각형을 사용한다. 그리고 파티클 시스템은 최소한 하나의 텍스처를 사용한다. (텍스처는 일반적으로 모든 파티클들이 공유한다.)

  1. Render Textures -- 화면이 아닌 텍스처에 출력하는 카메라의 수. 이는 생각보다 명확하지 않다.

렌더 텍스처(Render Textures)는 후처리 이펙트, CCTV, 물의 반사, 거울 또는 유리의 굴절 효과와 같은 다양한 효과를 위해 사용된다. 대부분의 그림자는 이 기술을 사용하여 표현된다.

렌더링 최적화 : 2개의 카메라 시스템.

우리의 완성된 게임을 플레이해보면, 2개의 카메라(Near Camera, Far Camera)가 있음을 알 수 있다.

Near Camera는 가까운 거리(0.4유닛에서 50유닛까지)의 모든 것을 렌더링한다.

유닛은 정하기 나름이지만 일반적으로 “1유닛 = 1미터”를 많이 사용한다.

Far Camera는 50유닛부터 500유닛까지 렌더링한다. 그러나 장면의 서브셋(일부분)만 렌더링한다. 서브셋은 레이어에 의해 정의된다. 장면에 있는 각각의 오브젝트들에게는 레이어가 주어진다. Near Camera는 레이어와 상관없이 모든 요소를 렌더링하지만 Far Camera는 “cameraTwo” 또는 “cameraTwoIgnoreLights” 레이어에 있는 아이템들을 무시한다. 우리는 또한 연료캔과 체력 아이템도 무시하기를 원한다. 이 아이템들은 “noShadow” 레이어에 있다. 그러므로 이것도 체크해제해야 한다.

알림

Far Camera는 스카이박스가 렌더링되는 곳이기도 하다. (위의 설정에서 Clear Flags(배경을 지우는 방법)를 보라.) Near Camera의 Clear Flags 설정은 “Depth Only”이다. 이 때문에 Far Camera에 의해 렌더링된 화면 위에 겹쳐서 그려진다.

하얀뱀

여러개의 카메라가 있을 경우, 렌더링 되는 순서는 Depth(깊이) 설정에 의하여 정해집니다. 숫자가 낮을수록 먼저 그려지는데 여기서는 Far Camera의 깊이 값이 0이고 Near Camera의 깊이 값이 1이므로 Far Camera가 먼저 그려지고 나중에 Near Camera가 그려집니다. 그리고 Far Camera에서 스카이박스(배경)을 그리고 난 후 그 위에 Near Camear가 이미 그려진 내용을 지우지 않도록 근처의 오브젝트들을 그리기 위해 Clear Flags 의 값이 “Depth Only”로 설정되어진 것입니다.

Culling Mask에서 체크된 레이어는 렌더링이 되며, 체크되지 않은 레이어는 렌더링 되지 않는다. 이와 같은 최적화는 경비로봇과 아이템에서 작동하는 것을 발견할 수 있다. 아이템 주위의 배경은 언제나 렌더링 되지만 아이템은 플레이어와 가까워져야 렌더링된다.

반응형
: