안녕하세요, 제 이름은 Mark입니다. 저는 수년동안 Unity같은 소프트웨어를 이용하여 게임을 만들고 싶었습니다. Unity는 "컵헤드" ,"네온 화이트", "튜닉"; "아우터 와일즈", "하스스톤", "파이어워치", 심지어 포켓몬 다이다몬드 리메이크에도 사용되었습니다. 그런데 저는 항상 긴 튜토리얼 영상을 볼 때마다 잠이 오더군요 전 다른 사람이 하는걸 보면서 따라하는 것보다 직접 시도해보는게 적성에 더 맞더라고요 그래서 작년에 3단계로 구성된 해결법을 만들었는데, 꽤 효과가 좋았습니다 1. Unity의 기본'만' 배우기, 2. 간단한 시험으로 기본 익히기, 3. 나머지는 게임을 만들면서 알아서 배우기. 제가 해봤더니 정말로 되더군요! 다른 모바일 게임을 배끼는 것부터 시작해서, 자석을 이용한 저만의 퍼즐 플랫포머 게임을; 만들기까지 1년정도 걸렸습니다 그런데 Unity같이 복잡한 소프트웨어의 기본을 어떻게 배워야 하는지 의문점이 들겁니다 저는 대부분의 게임의 들어가는 공통적인 요소를 리스트로 만들었습니다 캐릭터를 화면에 띄우는 방법; 캐릭터를 움직이게 하는 방법, 뭔갈 생성하거나 없애는 방법; 캐릭터의 판정, 게임 오버, 애니매이션, 사운드 등등; 전 이 방법들을 긴 튜토리얼 영상, Unity 문서; 검색, 그리고 수많은 시행착오를 겪으면서 배웠습니다 이 영상의 목적은 여러분이 그걸 겪지 않게 만드는 것입니다 이 영상은 제가 Unity를 배울 적에 봤다면 좋았을 영상입니다 앞으로 40분동안 우리는 Unity를 이용해 플래피 버드를 만들겁니다 플래피 버드를 만들고 싶어서가 아니라, 플래피 버드를 만들려면 제가 아까 말한 요소인 뭔갈 생성하는 것부터 게임 오버까지 전부 필요하기 때문입니다 이 튜토리얼에서는 Unity 설치부터 UI 이해; 프로그래밍, 그리고 당신의 지인들과 공유할 수 있도록 게임을 빌드하는 것까지 배우게 됩니다 그리고 튜토리얼이 끝난 후, 간단한 시험을 통해 기본을 익히게 되고, 그 후로는 스스로 배우는게 가능해질겁니다 괜찮은 것 같나요? 그렇다면 이제 시작하죠. Unity를 다운받는 것부터 시작합시다. 다운로드 후 Unity Hub를 설치하세요 Unity를 사용하려면 계정이 필요합니다. 계정을 만든 후, Unity 에디터 설치 창이 나올겁니다. 설치하세요 *(이 영상에선 2021.3을 사용했지만, 2022를 사용해도 상관없습니다) *(설치 시 약 15분정도 걸립니다 너무 오래 걸리면, Unity Hub를 관리자 권한으로 실행해보세요) 아직 끝난게 아닙니다. 옆에 톱니바퀴 아이콘을 누르고 모듈을 누르면 Visual Studio가 체크되어있는게 보일겁니다 저희는 이 프로그램으로 코드를 작성할 겁니다 Visual Studio를 설치한 후, 이 화면에서 아래로 스크롤하고 "Unity를 활용한 게임 개발"은 체크하고, Unity Hub는 해제합니다 왜냐면 Hub는 이미 설치 되있으니까요. Visual Studio는 계정이 필요없으니 이건 스킵하고, 어차피 나중에 열거니 이 창은 닫아도 됩니다 이제 Unity Hub에서 '새 프로젝트'를 누르고, '모든 템플릿'을 선택하고 '2D 코어'를 선택합니다 '2D 코어'는 2D 게임을 만드는데 적합한 빈 프로젝트입니다 이름을 정하고, '프로젝트 생성' 버튼을 누르세요 첫번째로, 우리는 Unity의 UI를 배울겁니다 다른 패널들을 탐방하면서, 화면에 새를 띄울겁니다 이게 Unity의 기본 레이아웃입니다 4개의 패널로 나뉘어져 있죠 아래에는 프로젝트 패널이 있습니다 여기에는 게임에 들어가는 모든 요소가 포함됩니다 예를 들면 스프라이트, 사운드, 스크립트, 타일, 폰트 등이 있습니다 이중 일부는 Unity에서 직접 만들거지만 다른 곳에서 파일을 드래그해도 됩니다 저는 포토샵에서 새와 파이프의 스프라이트를 만든 후 이걸 프로젝트에 드래그했습니다 전 여러분이 스프라이트를 직접 만들면 좋겠지만 만약 그림을 잘 못그리겠다면 설명에 에셋 다운로드 링크를 확인하세요 왼쪽에는 계층 구조 패널이 있습니다 여기에는 현재 '장면'에 있는 것들이 포함되며 '장면'은 대부분의 게임에서 레벨의 역할을 합니다. 우선 새를 만드는 것부터 시작합시다 계층 구조에서 빈 오브젝트 생성을 누르면 빈 GameObject가 생성됩니다 GameObject는 투명한 컨테이너와 같습니다 위치를 지정하거나 회전할 수 있고; 크기를 조절할수도 있습니다. 그리고 컨테이너 안에 컴포넌트를 넣어 기능을 추가할 수 있죠 예를 들어, '스프라이트 렌더러' 컴포넌트를 넣으면 GameObject에 이미지를 넣을 수 있죠 이 게임의 모든 것들은 '컴포넌트가 있는 GameObject'로 구성되어 있습니다 새, 파이프, 심지어 UI와 카메라까지 이 모든 것들은 오른쪽의 인스펙터에서 관리할 수 있습니다 빈 GameObject를 선택한 후, 위에서 이름을 정할 수 있습니다 이름은 그냥 Bird라고 하죠 그리고 Transform에서 위치, 기울기, 크기를 조절할 수 있습니다 이제 '컴포넌트 추가'를 누르고, Rendering을 누르고 Sprite Renderer를 선택하세요 이걸 작동하게 하려면, 스프라이트를 선택해야 하는데, 그냥 프로젝트 패널에서 새 이미지를 빈칸에 드래그하면, 그래픽이 생겼네요! 그래픽은 중앙에 있는 씬 패널에 나타납니다 씬에서는 현재 씬에 무엇이 있는지 볼 수 있고, 이 기능들을 이용하여 움직이거나 회전시킬 수 있습니다 이 섹션에는 '게임'이라는 탭이 있는데, 여기선 게임을 실행할때 메인 카메라에서 어떻게 보이는지 확인할 수 있습니다 그리고 여기에선 해상도나 화면비를 설정할 수 있습니다 다른 화면에서 어떻게 보이는지 알 수 있죠 - 저는 1920x1080을 선택하겠습니다 새가 너무 많은 공간을 차지하네요 새의 크기를 줄일 수도 있지만, 저희는 카메라를 축소할 겁니다. 카메라 또한 계층 구조에 있는 GameObject이기에 이를 선택하면 Camera 컴포넌트에서 수치를 조절할 수 있습니다 '크기'를 높이면 카메라가 축소됩니다 그리고 배경 색도 바꾸겠습니다. 좋네요 이제 위에 있는 Play 버튼을 누르면 세상에서 가장 지루한 게임이 시작됩니다 네. 이제 덜 지루하게 만들어 보죠 잠깐 되돌아봅시다 Unity에는 4개의 패널이 있고 그중 프로젝트에는 모든 요소가 들어가고, 계층 구조에는 현재 레벨에 있는 GameObject가 표시되고, 인스펙터에는 GameObject를 마음대로 조절할 수 있고, 씬에서는 레벨을 볼 수 있다. 그리고 GameObject는 Sprite Renderer같은 컴포넌트를 담을 수 있는 투명한 컨테이너다. 두번째로, 저희는 더 많은 컴포넌트를 이용하여 새를 중력에 영향을 받는 물리 오브젝트로 만들고 코드를 이용하여 스페이스 바를 누를 떄마다 새를 위로 날게 만들겁니다 이제 새에다가 Rigidbody2D 컴포넌트를 추가합시다. 이 컴포넌트는 새를 물리 오브젝트로 만들어 줍니다. 그래서 Play 버튼을 누르면 새가 화면 밖으로 떨어집니다. 이 새가 다른 오브젝트와 상호작용하게 만들려면 Collider가 필요합니다 Circle Collider 2D를 추가합시다 씬을 보면 초록색으로 된 선이 보입니다 중심이 약간 안맞기 때문에; 오프셋을 조절해서 움직이겠습니다 여기서 게임 디자인 팁을 주자면 Collider(판정)를 이미지보다 조금 작게 만들면 플레이어가 파이프에 아주 살짝 닿았더라도 게임 오버가 되지 않기에 빡센 느낌이 줄어들게 됩니다 마지막으로, 스크립트를 추가하겠습니다 스크립트를 활용하면 사실상 나만의 컴포넌트를 만들 수 있게 되지만, 우리가 코드를 이용해서 직접 만들어야 합니다 컴포넌트 추가에서 New Script를 선택하고, 이름은 BirdScript라고 하겠습니다. 만든 후, 스크립트를 더블클릭하면 전에 설치한 Visual Studio가 열릴겁니다 프로그래밍의 세계에 온걸 환영합니다! 너무 무섭게 생각하지 마세요; 천천히 설명해드릴 겁니다. 저희는 C#으로 코드를 작성할겁니다 먼저 Start와 Update, 이 2개에 대해 알아보겠습니다 Start는 (이 스크립트가 활성화 돼있으면) 처음으로 실행되는 코드이고, 단 한번만 실행됩니다. Update는 (이 스크립트가 활성화 돼있으면) 항상 실행되는 코드이고, 매 프레임마다 코드가 실행됩니다. 아무튼, 저희가 코드로 작성할 건 - Unity로 다시 돌아가보면 컴포넌트에 있는 숫자들이나 텍스트를 마음대로 조절할 수 있죠 저희는 코드를 이용하여 게임이 실행되는 동안 이것들을 조절할 겁니다 간단한 예시를 들자면 Start에 gameObject(오른쪽 위에 있는걸 의미함)를 치고, 점[.]을 치면 리스트가 보일겁니다 *(리스트가 안보이면, Unity 위에 '창'->'패키지 관리자'->아래로 내려서 'Visual Studio Editor' 선택 후 설치 그후 Unity 위에 '편집'->'환경 설정'->'외부 툴'->Visual Studio 선택 후 전부 체크하고 재실행) 이중 대부분은 인스펙터에 있는걸 나타냅니다 예를 들면 정적(isStatic); 태그, 레이어, 이름이 있죠 name을 선택하고, [=] 쓰고, 큰따옴표[" "]로 새의 이름을 넣으면 됩니다. 마지막으로, 명령 끝엔 항상 세미콜론[;]이 있어야 합니다 그리고 스크립트를 저장하는것도 잊지 마세요 이제 게임을 실행하면... GameObject의 이름이 바뀌였네요. 좋습니다 네, 이제 그 코드는 지우세요. 방금 쓴 코드는 우리가 코드로 게임과 대화할 수 있다는 걸 알려줍니다 저희는 코드를 작성하여 누구와 대화할지 대상을 정할 수 있고, 대화의 주제도 정하고, 그 후 명령을 정할 수 있습니다 저희는 이 짓을 많이 하게 될겁니다. 아무튼 저희가 진짜로 해야할 일은, Rigidbody2D 컴포넌트에서 Info 아래에 보면 Velocity가 회색으로 표시된게 보일겁니다 저희는 위쪽으로 Velocity를 주는 코드를 작성해서 새가 하늘로 날 수 있게 만들겁니다 문제는, 이 스크립트는 GameObject의 윗부분과 Transform끼리만 대화할 수 있고 다른 컴포넌트의 존재도 모릅니다 그래서 이 스크립트에 RigidBody2D를 위한 공간을 만들어야 합니다 그러면 대화를 할 수 있게 되고 명령을 보낼 수 있게 되며 이를 '레퍼런스'라 부릅니다 Class와 Start 사이에 레퍼런스를 작성할 겁니다: public RigidBody2D myRigidBody; 이제 RigidBody2D를 보관할 공간이 생겼고, 다른 Rigidbody2D와 구분하기 위해 공간의 이름도 정했습니다 public으로 정했기 때문에 스크립트 밖에서도 공간을 건드릴 수 있습니다 저장하고 Unity로 돌아가면, 스크립트 컴포넌트에 RigidBody2D를 위한 공간이 생긴걸 볼 수 있습니다 이제 RigidBody2D 컴포넌트를 빈칸에 드래그하면, 이제 스크립트와 RigidBody간의 통신선이 생겼습니다. 다시 Visual Studio로 돌아와서, Update에 myRigidBody 그리고 점[.]을 치면 이 많은 대화 주제들을 보세요 angularDrag, gravityScale, mass; 이것들이 전부 RigidBody2D에 있는 속성들입니다 우리가 원하는건 velocity죠 전에 했던 것처럼 [=]을 쓰고, 여기에서는 Vector라는 2D 공간에서 위치를 나타내는 값을 이용할 겁니다 여기서 Vector는 새가 나는 방향을 나타내는데 사용할겁니다 저희는 새가 위로 나는걸 원하기 때문에, (0,1)이 좋겠네요. 저는 Vector2.up을 이용할 겁니다 *Vector2.up은 Vector2(0,1)과 같은 의미입니다 그리고 힘을 주려면 Vector에 힘을 곱하면 됩니다 힘은 대충 10 정도면 충분할 겁니다 그리고, 전에 말했지만, Update 안에 있는 코드는 매 프레임마다 실행되기 때문에 저장하고 플레이 해보면... 영원히 날아갑니다 하지만 저희가 원하는건 스페이스 바를 누를 때만 새가 날아가게 하는 것이죠 이제 프로그래밍에서 가장 중요한 요소를 사용할 때입니다: 바로 if 입니다. if는 마치 관문과도 같습니다 게임이 특정 조건을 만족하지 못한다면, 매 프레임마다 그 코드는 완전히 무시됩니다 반대로, 관문에 적혀 있는 특정 조건을 만족한다면, 통과할 수 있게 되고 코드가 실행됩니다. 저희가 원하는건 "만약 플레이어가 스페이스 바를 누르면, 위쪽으로 velocity를 더한다"죠 if를 적고 괄호 안에 조건을 적으면 됩니다 여기서는 컴포넌트와 대화하는게 아니라 Unity와 대화하게 됩니다 정확히는 Unity의 인풋 시스템이죠 (Input.GetKeyDown, 그리고 괄호 안에 KeyCode.Space) 이렇게 하면 Unity에게 이 프레임에 스페이스를 눌렀는지 물어볼 수 있습니다 그리고 마지막에 ==true 로 마무리합시다 참고로 등호를 1개[=] 만 사용하면 왼쪽에 있는 걸 오른쪽과 같게 만들라는 의미이고, 2개[==]를 사용하면 왼쪽에 있는게 오른쪽에 있는것과 같은지 여부를 확인하라는 의미입니다. 괜찮죠? 아무튼, "만약 스페이스가 눌리면..." 그 후는 중괄호{}를 사용해야 합니다. 중괄호 안에 위로 velocity를 주는 코드를 쓰면 됩니다 이제 Update에서 - 매 프레임마다 게임이 관문에 가서 "스페이스가 눌렸니?"라는 질문을 받은 후, 만약 맞다면 코드가 실행되고 새는 날게 되겠죠, 아니라면 중괄호 안에 있는 코드는 무시하고, 다음 프레임에 다시 시도하게 될겁니다 이제 저장하고 Unity로 돌아와서 플레이해보면, 짜잔: 이제 스페이스를 누를 때마다 새가 위로 올라가네요 이제 저희는 Input에 반응하는 캐릭터를 만들었습니다. 이것은 게임입니다. 좋아요! 그런데, 조작감이 좀 쓰레기같네요 원작 플래피 버드와는 전혀 다른 느낌입니다 그러니 숫자를 좀 바꿔볼까요 저장, 플레이, 그래도 별로네, 정지, 숫자 번경; 저장,... 그런데 이건 너무 느리고 바보같네요 덜 바보같은 짓을 해봅시다 먼저, 함수를 만들겁니다. 스크립트의 윗부분의 RigidBody 레퍼런스 바로 밑에 public float flapStrength를 만듭시다 float란 부동소수점(floating point number)를 뜻합니다. 간단히 말하면 소수점이 있는 숫자라는 의미입니다 다시 Update로 돌아와서, Vector2.up에 10이 아니라 flapStrength를 곱할겁니다 이제 스크립트 컴포넌트를 보면 새로운 공간이 보일겁니다: Flap Strength. 그리고 이건 언제든지 번경할 수 있습니다; 심지어 플레이 중에도 번경할 수 있습니다 참고로 플레이 중 번경한 값은 저장되지 않기 때문에, 게임의 중요한 요소를 다룰 때에도 아무 숫자나 막 집어넣어도 상관없습니다 그래서, flapStrength와 RigidBody의 중력 스케일이나 질량을 적당히 조절하다 보면 언젠가는 좋은 값을 찾을 수 있을 겁니다 숫자 계속 바꾸기, 이게 게임 디자인이죠 잠깐 되돌아봅시다 코드를 활용하여 플레이 중에도 컴포넌트의 속성을 바꿀 수 있다. 스크립트는 기본적으로 gameObject에 있는 다른 컴포넌트들과 대화할 수 없고, 할려면 원하는 컴포넌트의 레퍼런스(공간)를 만들어야 한다. 레퍼런스(공간)는 코딩으로 만들고, 빈 공간은 Unity에서 드래그 앤 드롭으로 채운다 Start에 있는 코드는 (스크립트가 존재할 때) 한번만 실행되고 Update에 있는 코드는 매 프레임마다 계속 실행된다. 하지만 if를 사용하여 조건 미충족시 코드를 스킵할 수도 있다 그리고 public을 사용하면 *(팁: [SerializeField]를 사용하면 private여도 Unity에서 값 바꿀수 있음) Unity의 인스펙터에서 값을 바꿀 수 있다. *(팁: [SerializeField]를 사용하면 private여도 Unity에서 값 바꿀수 있음) 플래피 버드의 비밀을 하나 말씀드리자면 새가 파이프를 지나는 것처럼 보이지만 새는 가만히 있고 파이프가 움직이는 겁니다 그래서 이번에는 파이프 생성, 그리고 파이프를 움직이게 하고 없애는것까지 배울겁니다 우선 생성할 오브젝트를 만듭시다 오른쪽에서 왼쪽으로 가는 파이프 2개를요 빈 오브젝트를 만들고, 이름은 pipe로 합시다 일단은 파이프 크기를 맞추기 위해 새 중앙에 놓겠습니다. 그 다음 pipe 안에 빈 오브젝트를 만들고 이름은 top pipe로 하겠습니다 이런 걸 GameObject의 "자식"(child) 이라고 합니다 자식을 이용하면 GameObejct 여러 개를 한 곳에 모와, "부모"를 움직이면 자식도 함께 움직이게 할 수 있습니다 새를 만들 때 했었던 짓을 또 하겠습니다 이미지를 넣기 위해 Sprite Renderer를 추가하고 판정을 위해 Box Collider 2D를 넣겠습니다 Rigidbody는 필요 없습니다 물리의 영향을 받을 필요가 없으니까요. 파이프를 위로 올리겠습니다 - 단, X좌표는 0으로 하세요 그 후, 이걸 복사하고 이름을 bottom pipe로 바꾼 후, 180도 회전하고 아래로 내리겠습니다 여기서 부모 GameObject를 움직여 보면, 파이프 2개가 부모를 중심으로 동시에 움직이는걸 볼 수 있습니다 이제 움직이게 하기 위해 부모에 스크립트를 추가하겠습니다 우선 이동 속도를 위한 변수를 하나 만듭시다 여기에 값을 부여하면, Unity에서 그 값이 기본으로 설정됩니다 물론 Unity에서 언제든지 바꿀 수 있긴 합니다 이제 Update에 오브젝트를 움직이는 코드를 작성합시다 여기서 그냥 transform.position.x로 할 수 있다면 좋겠지만 아쉽게도 안됩니다 Vector 전체를 한번에 바꿔야 합니다 그리고 이번엔 Vector2 대신 Vector3를 쓸 겁니다 transform은 값 3개가 필요하기 때문이죠 이 게임은 2D이긴 하지만 Unity는 근본적으로 3D 엔진이기 때문에 오브젝트의 Z값도 필요합니다 아무튼, 움직이게 하려면 현재 위치에다가 다른걸 더해야 하기 떄문에 transform.position = transform.position + 그 후 괄호 안에 Vector3.left * moveSpeed; Unity로 돌아와서, 플레이를 누르면 너무 빠르네요 그냥 moveSpeed에 엄청 작은 값, 예를 들어 0.001을 넣어도 되긴 하지만 문제의 근본적인 원인은 그게 아닙니다 Update에 있는 코드는 가능한 한 많이 실행됩니다 게임 뷰에서 통계를 보면; 게임이 초당 1000프레임으로 구동되고 있는게 보이네요 플레이스테이션 5에게 미안해지네요, 120프레임? 플래피 버드에겐 잽도 안되는군요 아무튼 이게 중요한 이유는, 컴퓨터 사양에 따라 게임이 다른 속도로 구동될 가능성이 있다는 겁니다 컴퓨터 사양에 따라 파이프 속도가 달라지면 안돼죠 시장에 있는 게임에도 이런 실수가 종종 보입니다 - 다크 소울 2에선, 무기 내구도가 프레임과 연관되어 있어서 60프레임에선 30프레임보다 내구도가 2배 빨리 닳았습니다 해결법은 간단합니다 그냥 Time.deltaTime을 곱하면 됩니다 이러면 프레임에 상관 없이 움직임이 일정한 간격으로 발생합니다 Rigidbody에 이게 없어도 됐던 이유는 물리와 관련된 건 타이머가 내장되어 있기 때문이였고, 그 외 상황에는 필요합니다. 이거(혹은 다른거 뭐든지)에 대해 알고 싶다면, Unity 사용자 매뉴얼을 확인해보세요 정보와 샘플 코드도 있습니다 아무튼, 이제 파이프가 부드럽게 움직이네요 좋습니다 다음으로, 파이프가 무한히 생성되는 시스템을 만들겠습니다 우선 계층 구조에 있는 부모 GameObject를 프로젝트에 드래그하세요 이러면 프리팹이 생성됩니다 프리팹은 이 GameObject 전체의 설계도와도 같죠 그리고 프리팹을 이용하면 이 GameObject들의 바리에이션을 만들기 쉬워집니다 프리팹을 만들었으면, 계층 구조에 있는건 지워도 됩니다. 이제 새로운 빈 오브젝트를 만들고 이름을 PipeSpawner로 지읍시다 카메라 영역 오른쪽에 놓고, 이제 스크립트를 만듭시다 스크립트에는 매초마다 파이프 프리팹의 새로운 바리에이션을 생성하는 코드를 작성할겁니다 - 파이프가 왼쪽으로 움직이는건 이미 전에 했기 덕분에, 파이프가 생성되면 자동으로 왼쪽으로 움직일겁니다 방금 전에 만든 프리팹을 소환하는 코드를 작성할겁니다 일단 프리팹의 레퍼런스부터 만들겠습니다 여기에 public GameObject pipe; 를 작성합니다 그 후 Unity에서 빈칸을 드래그로 채울겁니다. 이 빈칸은 컴포넌트로 채우는게 아니라, 프리팹으로 채워야 합니다 Unity에는 새로운 GameObject를 생성하는 기능이 내장되어 있기에 그걸 써먹으려면, Instantiate, 그리고 괄호를 열면 뭔가 자세히 입력하라고 뜨네요 화살표를 눌러 이 레시피? 같은걸 넘기다 보면 - 4번이 좋아보이는군요 특정한 좌표와 기울기를 가진 GameObject를 생성한다고 하네요 생성할 GameObject는 pipe고; 좌표는 transform.position을 사용하면 이 스크립트가 달린 오브젝트의 좌표를 구할 수 있기에 그걸 쓰면 되고, 기울기는 같은 원리로 transform.rotation을 쓰면 됩니다 플레이 해보면, 맙소사 너무 많이 소환되네요 소환은 되는데 매 프레임마다 소환되고 있습니다 작당한 간격을 두고 소환되게 만듭시다 - Visual Studio로 돌아와서 타이머를 만들겁니다 타이머가 끝나면, 코드 실행, 타이머, 같은 식으로요 일단 변수 몇개를 만들어야 합니다 spawnRate는 소환 간 간격을 나타내고, timer는 말 그대로 타이머입니다 timer는 private로 작성할 겁니다 왜냐면 다른 스크립트에서도 timer를 쓰면 혼동이 오니까요