No.4 Unity 3D Platform Tutorial "First Steps #3"
컨셉 스크립팅(scripting Concepts)
우리의 역사는 흥미를 위하여 우리의 선조들에 의해 만들어진 오토마타(Automata: Automation (자동화기계)의 복수형)라 불리는 놀랍도록 복잡한 기계들로 어질러져있습니다. 몇몇 기계들은 매우 정교하며 인형들을 이용한 간단한 연극을 공연할 수도 있었으며, 몇몇 다른 기계들은 상호작용을 하며 사용자의 입력에 따라 행동을 변화하기도 하였습니다. 하지만 이러한 기계들은 근본적으로 설계자가 에셋 -- 인형들, 소품들, 색칠된 배경막 등 -- 을 만들고 기계가 이 자산들을 원하는 방식으로 행동하게 하도록 설계하는 점에서 모두 같았습니다.
이러한 기본적인 원칙은 수년간 바뀌지 않고 유지되었습니다. 컴퓨터들은 단지 철과 스프링으로 만들어진 물리적인 기계들을 여러 명령들의 목록에 의해 제어되는 가상적 기계로 바꿔놓았을 뿐입니다. 유니티는 이러한 명령들의 목록을 스크립트라 부릅니다.
대부분의 스크립트들은 게임 개발에 있어서 대중적인 컨셉인 피니트 스테이트 머신(Finite State Machine: 유한 상태 장치)를 중심으로 작성되었습니다. 이 머신은 근본적으로 스테이트(State: 상태)라고 불리는 상호작용하는 스테이트의 시스템을 정의합니다.
스테이트는 객체가 렌더링이 되어야 할지, 물리의 법칙에 영향을 받아야할지, 객체에 빛이 날지 또는 그림자가 드리워져야 할지, 객체가 튀어오를 수 있는지, 객체의 스크린에서의 위치 등등, 어떤 것이로든 정의될 수 있습니다. Inspector 창은 이러한 많은 스테이트들이 거의 대부분의 게임들에서 흔하기 때문에 이들을 직접적으로 바꿀 수 있도록 해줍니다.
그러나 게임 자체에 좀 더 구체적인 스테이트의 종류가 있습니다. 유니티는 플레이어의 아바타가 외계인인지, 럽츠가 얼마의 데미지를 견딜 수 있는지, 또는 럽츠가 제트 팩이 있는지 등의 스테이트를 모릅니다. 유니티는 어떻게 경비 로봇이 럽츠와 상호작용을 해야 할지 또는 로봇의 필요한 행동패턴 을 인지할 수 있을까요?
이런 부분에 바로 스크립트가 작용을 합니다. 우리는 스크립트를 사용하여 상호작용성과 게임의 구체적인 스테이트 관리를 추가시킵니다.
우리의 게임은 아래와 같은 여러 가지의 스테이트들을 관리해 주어야 합니다:
• 플레이어의 체력;
• 플레이어가 모은 연료통의 수;
• 플레이어가 포스필드를 해제하기 위해 필요한 연료통의 수를 습득했는지의 여부;
• 플레이어가 점프패드를 밟았는지;
• 플레이어가 아이템을 건드렸는지;
• 플레이어가 우주선을 건드렸는지;
• 플레이어가 리스폰 지점을 건드렸는지;
• 게임오버 또는 시작 화면이 스크린에 표시되어야 할 지;
• ...등등.
이러한 많은 스테이트들은 스스로를 최신화 시키기 위해 타 객체의 스테이트와의 테스트를 필요로 합니다. 종종 전환을 위해서 우리는 즉시적인 스테이트가 필요할 때도 있습니다. 예를 들어, 연로 통을 모으는 행위는 플레이어가 포스필드를 해제키 위한 수를 모았는지를 체크하도록 강요할 것입니다.
구성과 구조
본 튜토리얼에서는 플레이어를 위한 스테이트 머신과 레벨, 그리고 적들 등이 다양한 게임 객체와 링크된 많은 스크립트들에 의해 다루어졌습니다. 이 스크립트들은 서로 대화하고 메시지를 보내며 서로의 함수를 불러옵니다.
이 링크들을 설정할 수 있는 몇 가지 방법이 있습니다:
• Inspector창에 나타난 링크를 관련된 객체에 드래그 하여 추가합니다. 이는 다른 프로젝트에 재사용 할 목적을 가진 다용도 스크립트에 이상적인 방법입니다.
이는 스크립트가 단지 관련된 변수에서 데이터를 뽑아내는 것에 불과하며 검색 자체가 필요하지 않다는 면을 고려할 때 가장 효율적인 방법입니다. 그러나 스크립트는 자신의 기능을 하기에 앞서서 사용자가 어떤 객체 또는 구성요소에 링크하는지를 알고 있다고 전제하고 있습니다.
우리는 이 방법을 사용하여 LevelStatus 스크립트의(이 스크립트는 현재 작은 코드 조각으로서 Level 게임 객체에 이미 첨부되어있습니다.) 컷씬(Cut-scene) 카메라를 구현할 것입니다. 이 스크립트로 하여금 "레벨 출구 열림" 컷씬을 위한 카메라와 "레벨 완료" 시퀀스를 위한 카메라 등 여러 개의 카메라를 설정하는 데에 유연성을 제공할 것입니다. 그러나 이를 변경하기위한 옵션이 존재합니다.
• 스크립트의 Awake() 함수 내에 링크를 설정합니다. Awake()함수는 사용자가 작성한 모든 스크립트에서 첫 Update()함수가 첨부된 게임 객체에 적용되기 전에 불립니다. 이곳에 링크를 설정하는 것은 함수의 결과를 Update() 함수에서 나중에 사용할 수 있도록 저장할 수 있도록 해줍니다. 일반적으로, 사용자는 사용자가 스크립트 내에서 접근이 필요한 다른 게임객체 또는 구성요소에 링크와 함께 개인 변수를 설정할 것입니다. 만약 사용자가 관련된 객체를 위치시키기 위해 함수를 호출하려 한다면, 상당히
느린 GameObject.Find() 함수를 호출 하는 것보다 Awake()함수 내에서 한번만 호출 하는 것이 좋습니다.
이 방법은 첫 번째 방법에서의 유연성이 필요하지 않지만 매 게임 사이클 마다 복잡한 검색을 하고 싶지 않을 때 상황에서 더 적합합니다. 그러므로 이 방법은 스크립트가 '깨어났을 때(Awake)" 만 객체를 검색하고 업데이트 섹션에서 사용하기 위한 검색결과를 저장하도록 하는 것입니다.
예를 들어, 레벨의 스테이트를 다루는 LevelStatus 스크립트는 Player 객체를 포함한 여러 다른 객체로의 링크를 저장합니다. 우리는 이들이 바뀌지 않을 것을 알기 때문에 이 또한 컴퓨터가 대신 하도록 할 수 있습니다.
• Update() 함수 내에 링크를 설정합니다. Update() 함수는 최소한 매 게임 사이클 마다 한 번씩 불러들여지기 때문에 여기서는 처리속도가 느린 함수를 피하는 것이 상책입니다. 그러나 GameObject.Find()와 GetComponent() 함수들은 약간 느릴 수도 있습니다.
이 방법은 사용자가 필요한 객체가 게임 플레이 도중 언제든지 바뀔 수 있는 상황에서 사용됩니다.
예를 들어, 본 튜토리얼의 씬에 배치된 많은 리스폰 지점 중에서 플레이어는 어디에 리스폰 되어야 할까요? 이러한 문제는 분명히 게임이 플레이 되는 도중에 바뀌므로 상황에 맞게 다루어져야 합니다.
이 방법은 단점은 너무 느리다는 것이므로 게임을 디자인할 때 이 방법을 자주 쓰지 않도록 하는 것이 좋습니다.
-------------------------------------------------
시각적 개발 환경에서의 스크립트 박스
유니티는 에셋의 링크와 연결보다 에셋 자체의 시각적인 면에 중점을 둔 색다른 도구입니다. 유니티로 개발한 큰 프로젝트는 프로젝트의 계층 내에 다양한 복잡성을 가진 수많은 스크립트를 포함하고 있을 수 있으므로 본 튜토리얼에 사용된 디자인은 이를 완화하기 위하여 약간의 객체 지향적 테크닉을 사용하였습니다.
스테이트 머신의 특정 부분을 다루는 스크립트 -- 플레이어 애니메이션과 같은 -- 또한 관련된 스테이트 변수를 파악하고 있어야합니다. 이는 스크립트가 다른 스크립트에 저장된 스테이트 변수에 접근할 때 일을 약간 복잡하게 만 들 수 있기 때문에 몇몇 스크립트들은 빠른 정보의 접근을 위해 값 몇 개를 자체적으로 저장하기도 합니다. 이 테크닉은 또한 한 스크립트의 함수가 다른 스크립트의 비슷한 함수를 부르는 명령체계에서 주로 발생합니다. 플레이어의 죽음과 체력을 다루는 것이 이것의 좋은 예입니다.
-------------------------------------------------
☆ 참고
유니티에 더 익숙해질수록, 자신의 게임에 알맞게 스테이트를 다루는 다른 방법들을 찾을 수 있을 것입니다. 본 튜토리얼에서 사용된 디자인 패턴은 절대 "만능" 해법이 아닙니다!
사용자의 기호에 따라 현재까지의 모든 변경 내용이 반영된 Player 게임 객체를 Prefab으로 변환시켜 다른 프로젝트의 시작점으로 사용할 수 있습니다:
ㅁ Project Pane의 Player 폴더를 클릭합니다.
ㅁ 새 Prefab을 생성합니다. (Player 폴더 안에 생성될 것입니다.)
ㅁ 새 Prefab에 적절한 이름을 붙입니다 -- 본 튜토리얼에서는 LerpzPrefab으로 이름짓기를 제안합니다.
ㅁ Player 게임 객체를 LerpzPrefab으로 드래그해서 이 과정을 마칩니다.
죽음과 부활
플랫폼 게임에서의 캐릭터는 자신의 목숨을 걸기 마련이며 럽츠도 그것으로부터 자유로울 수는 없습니다. 우리는 럽츠가 레벨에서 추락하면 목숨을 잃도록 해야 합니다. 우리는 또한 럽츠가 레벨의 안전한 곳 -- 주로 리스폰 지점이라고 불리는 곳-- 에 다시 나타나도록 하여 그가 모험을 계속할 수 있도록 해야 합니다.
다른 고려해야 할점은 만약 럽츠가 지면에서 떨어질 수 있다면 레벨에 상주하는 다른 캐릭터들도 그럴 수 있으므로, 이러한 부분도 적절하게 처리되어야 합니다.
이 문제에 대해서 최고의 해법은 박스 컬라이더(box collider)를 이용하여 레벨에서 떨어지는 모든 것을 잡아내는 것입니다. 우리는 이를 길고 넓게 만들어서 플레이어가 떨어질 때 제트 팩을 쓰더라도 컬라이더에 걸리도록 할 것입니다. 그러나 럽츠는 리스폰할 지점이 필요합니다. 리스폰 지점에 대해서는 곧 차후에 다루도록 하고, 먼저 박스 컬라이더를 만들도록 합시다:
ㅁ 빈 GameObject를 생성합니다.
ㅁ 새 객체를 FalloutCatcher로 이름 짓습니다.
ㅁ 객체에 박스 컬라이더(Box Collider)를 추가합니다.
ㅁ Component->Third Person Props에서 Fallout Death 스크립트를 찾아 추가합니다.
Inspector 창을 이용해서 아래의 스크린샷처럼 값을 설정하십시오:
Fallout Catcher 설정.
추락사 스크립트 (The Fallout Death script)
이 스크립트는 모든 기능을 ThirdPersonStatus 스크립트(이 스크립트는 Lerpz 객체에 첨부되어야 하지만, 아직 첨부하지는 않을 것입니다.)에 위임하기 때문에 길이가 짧습니다.
컬라이더의 트리거를 다루는 코드는 OnTriggerEnter() 함수에 있습니다. 이 함수는 박스 컬라이더가 럽츠나 적 캐릭터 같은 컬라이더 구성요소를 포함한 다른 게임 객체와 충돌할 때 유니티에 의해 불러집니다.
이 행위에는 세 가지 테스트가 있습니다: 하나는 플레이어에게, 하나는 간단한 RigidBody 객체, 그리고 마지막 세 번째 테스트는 객체가 CharacterController 구성요소가 있는지를 체크하는 테스트입니다. 이중 두 번째 테스트는 레벨에서 떨어지는 상자 같은 소품 등을 체크합니다. (이 레벨에는 그러한 아이템은 없지만, 실험하고 싶다면 추가하여도 좋습니다.) 그리고 세 번째 테스트는 평범한 물리가 적용되지 않는 적 캐릭터들에게 사용됩니다.
플레이어가 박스 컬라이더와 충돌하게 되면, 코드는 간단히 럽츠의 ThirdPersonStatus 스크립트에서 FalloutDeath() 함수를 호출합니다.
만약 다른 컬라이더 객체를 가진 객체가 충돌하게 되면, 그냥 파괴시킨 다음, 씬에서 없애버립니다. 그렇게 하지 않으면 평생 충돌하지 않고 추락하게 되기 때문입니다.
추가적으로 우리는 아래의 요소들을 가지고 있습니다:
• 유틸리티 기능 -- Reset() -- 함수는 필요한 구성요소의 현존여부를 확인합니다. 이 함수는 구성요소를 처음으로 추가할 때 유니티에 의해서 자동으로 호출됩니다. 이 함수는 또한 편집기(Editor)에서 Inspector창의 구성요소 이름 우측에 위치한 톱니바퀴 버튼을 클릭하여 호출할 수도 있습니다:
리셋 메뉴 명령모음.리셋 메뉴 명령모음.
• @script 디렉티브 또한 유니티의 구성요소 메뉴에 스크립트를 직접 추가할 수 있는 기능입니다. 이 기능은 특히 매우 복잡한 프로젝트에서 Project Pane에서 스크립트를 찾는 시간을 아껴준다는 면에서 편리합니다.
현재 시점에서 다시 게임을 실행하려고 하면, 유니티는 럽츠가 어디서 다시 나타나야 되는지 모른다는 불평을 할 것입니다. 여기에 바로 리스폰 지점이 사용됩니다.
리스폰 지점
플레이어가 사망할 때, 우리는 플레이어가 다시 나타날 안전한 곳이 필요합니다. 본 튜토리얼에서는 럽츠는 세 개의 리스폰 지점 중 하나에서 다시 나타날 것입니다. 럽츠가 세 개의 리스폰 지점 중 하나를 건드리면 지점이 활성화 되면서 사망 후 럽츠가 다시 나타나는 장소가 될 것입니다.
활성화된 리스폰 지점에 서있는 럽츠.
리스폰 지점들은 RespawnPrefab 프리팹 객체의 인스턴스들입니다. (이 Prefab은 Project Pane의 Props 폴더에서 찾을 수 있습니다.)
본 Prefab은 텔레포트 베이스의 모델로서 스포트라이트와 몇 가지 잡동사니를 포함한 세 가지의 완전한 파티클 시스템으로 연결되어있습니다. 아래는 이 Prefab의 구성입니다:
• RSBase는 빛나는 파란 디스크를 중앙에 배치한 작은 실린더형태의 베이스인 모델 그 자체를 포함합니다.
• RSSpotlight는 스포트라이트 객체로서 미묘한 파란 빛을 모델의 지면으로 부터 위로 비추어 파란 텍스쳐가 빛나는 듯한 느낌을 줍니다.
• 남은 게임 객체들은 파티클 시스템들입니다. 상위 RespawnPrefab객체에 부착된 Respawn 스크립트는 이 파티클 시스템들을 Prefab의 아래의 스테이트에 따라 서로 바꾸어줍니다:
‣ 만약 리스폰 지점이 비활성화 되어 있다면, 밝고 파란 안개처럼 보이는 작고 미묘한 파티클 효과가 나타납니다. 이 효과는
RsParticlesInactive 스크립트에 포함되어있습니다.
‣ 만약 리스폰 지점이 활성화 돼 있다면, 좀 더 크고 화려한 효과가 나타납니다. 이 효과는 RsParticles Active 스크립트에 포함
되어있습니다.
오직 하나의 리스폰 지점만이 레벨에 활성화 되어 있어야합니다. 플레이어가 리스폰 지점을 건드리면, (트리거로 설정된) 컬라
이더 객체가 이를 감지하여 리스폰 지점의 활성화를 트리거합니다.
‣ 남은 세 개의 파티클 시스템인 -- RSParticlesRespawn1, RSParticlesRespawn2, 그리고 RSParticlesRespawn3는 플레이어
가 리스폰 지점에 리스폰될 때에 동시에 사용 가능케 됩니다. 이들은 일회성 파티클 시스템들입니다. 스크립트는 이들을 재생
시켜 놓고 일회성 시퀀스가 완료되면 RsParticlesActive 파티클 시스템을 복구합니다.
RespawnPrefab은 리스폰 지점의 스테이트를 제어하는 Respawn이라는 스크립트를 포함하고 있습니다. 그러나 게임이 어떤
특정 리스폰 지점이 플레이어가 사망 후 돌아와야 할 지점인지를 알기 위해서는 이 리스폰 지점들을 Hierarchy의 master
controller 스크립트 아래 정렬해야합니다. 지금 이것을 하도록 합시다:
ㅁ RespawnPrefab을 씬 뷰(Scene View)에 드래그 해놓습니다.
ㅁ 다음 페이지의 이미지처럼 Prefab을 위치시킵니다.
ㅁ 인스턴스의 이름을 Respawn1으로 바꿉니다.
ㅁ 위의 단계들을 두 번 더 반복합니다. 사용자는 리스폰 지점들을 아무데나 위치시킬 수 있고 원한다면 더 추가할 수 있습니다! 우리는 레벨의 먼 쪽의 아레나 근처에 하나, 플랫폼 위의 위치한 정원의 나무근처에 또 하나를 위치시키기는 것을 추천합니다.
다음 단계는 이 모든 Prefab을 담아 둘 컨테이너 게임객체를 생성하는 일입니다.
ㅁ (빈 게임 객체를 생성하고) RespawnPoints로 이름을 바꿉니다.
ㅁ 모든 prefab인스턴스들이 RespawnPoints 객체의 하위 객체가 되도록 하십시오.
첫 번째 리스폰 지점 위치하기 (럽츠 캐릭터는 위치의 명확성을 위해 잠깐 화면에서 빠졌습니다.)
어떻게 동작하는가
씬이 로드되었을 때, 유니티는 Respawn 스크립트의 각 인스턴스에 있는 Start() 함수를 호출합니다. Respawn 스크립트에는 몇몇의 유용한 변수들이 초기화 되어있으며 다른 요소들을 가리키는 포인터들이 저장되어있습니다.
핵심 메커니즘은 static(고정) 변수에 중점을 두고 있습니다:
===================================================================================================================
static var currentRespawn : Respawn;
===================================================================================================================
위의 코드는 currentRespawn이라 명명된 전역 변수를 정의합니다.
키워드 static(고정)의 뜻은 변수가 스크립트의 모든 인스턴스에 걸쳐 공유됨을 의미합니다. 이는 어떠한 리스폰 지점이 현재 활성화 돼 있는 것인지를 파악할 수 있도록 해줍니다. 그러나 씬이 시작될 때에는 어느 지점도 활성화 돼 있지 않기 때문에, 기본 값을 설정해 주어야합니다. 유니티 Inspector 창은 이러한 고정 변수의 종류를 전혀 표시하지 않을 것이기 때문에, 스크립트는 대신 각 인스턴스에 설정되어야 하는 Initial Respawn 속성을 정의합니다.
ㅁ 기본 리스폰 지점으로 설정하고 싶은 리스폰 지점을 이곳(Inspector 창의 Initial Respawn 속성 의 드롭 다운박스)으로 드래그 합니다.
ㅁ 이러한 작업을 모든 리스폰 지점에 반복하여야할 것입니다. (본 튜토리얼에서는 기본 리스폰 지점은 플레이어의 시작지점 바로 밑에 감옥 근처에 위치한 Respawn1으로 설정되어있습니다.
☆ 참고
원본 Prefab을 이용하여 직접 이러한 속성들을 설정하실 수 없습니다.
플레이어가 컬라이더를 트리거하여 리스폰 지점이 활성화 되면, 그 지점의 Respawn 스크립트는 우선 이전에 활성화 돼 있던 리스폰 지점을 비활성화하고 currentRespawn가 현재 플레이어가 트리거한 리스폰 지점을 가리키도록 설정합니다. SetActive() 함수는 관련 파티클 시스템과 음향 효과를 적용하는 것을 다룹니다.
플레이어 캐릭터의 리스폰은 플레이어 대부분의 게임 스테이트를 관리하는 ThirdPersonStatus 스크립트에 의해 처리됩니다.
ㅁ ThirdPersonStatus 스크립트를 Player 게임객체에 추가시킵니다. 이 스크립트는 script->Player->ThirdPersonStatus 폴더에서 찾을 수 있습니다.
리스폰 지점의 스크립트는 음향 효과도 다룹니다. 이들은 각 Respawn Prefab에 첨부된 오디오 소스를 제외하고는 일회성 샘플로서 재생됩니다. 이 구성요소는 반복적이고 "활성화"된 소리를 포함하고 있습니다. 스크립트는 일회성 효과가 재생될 때 -- 플레이어가 실제로 리스폰 되거나 리스폰 지점 자체를 활성화 할 때처럼 -- 또는 리스폰이 비활성화 되었을 때와 같이 적절한 상황에 맞춰 간단히 소리를 켰다 끄는 역할을 합니다.
☆ 참고
유니티는 음향 효과의 추가를 매우 쉽게 할 수있도록 해놓았습니다. 사용자가 음향효과와 같은 에셋을 추가할 때, 이것이 어떻게 사용될 것인지 신중히 고려하십시오. 예를 들어, 본 튜토리얼에서는 게임 플레이 중에 절대 듣지 못할 "리스폰이 비활성화 될 때"의 음향효과를 포함하지 않았습니다; 왜냐하면 두 개의 리스폰 지점이 서로 가깝게 배치되지 않을 것이기 때문입니다.
만약 사용자가 본 프로젝트를 멀티플레이어 게임으로 전환하려 한다면 이러한 음향 효과와 이를 처리하는 스크립트 코드의 추가를 필요로 할것입니다.
스크립트는 복잡하지 않으며 코드자체도 이해하기 쉬울 것입니다. 우리는 이 리스폰 지점들 대해 차후 챕터에서 다시 다룰 것입니다.
본 튜토리얼 번역서는 (주)지피엠스튜디오가 운영하는 "유니티코리아" 회원님들을 위한 메뉴얼 번역 자료 입니다.
본 자료를 다른 곳에 개제 하실 때에는 아래의 번역과 출처를 명확히 밝혀 주시기 바랍니다.
본 자료는 제3자가 상업적인 용도로 사용 할 수 없음을 밝힙니다.
번역 : 유니티코리아 [U3K]게임인생
출처 : 유니티코리아(www.unity3dkorea.com)
이제 튜토리얼의 첫 챕터인 First Steps 챕터가 끝이 났군요. 다음 회차에서는 다음 챕터인 '씬 세팅하기(Setting the Scene)' 으로 찾아 뵙겠습니다.