설 연휴 혼자하는 해커톤

2015년 설 연휴에는 차량 행렬을 피해 미리서 부터 내려가고 뒤늦게 올라오기로 했다. 그래서 1주일 동안 고향집에서 지내게 되었다. 긴 설연휴지만 아무것도 안하고 지낼수는 없기에 설 연휴동안 혼자서 해커톤을 해보기로 했다. 보통 해커톤은 1박 2일이나 2박 3일 동안 밤을 새가며 하는 것이 보통이지만, 혼자 하는것이기도 하고 중간중간에 친척분들과 인사를 나누어야 했기에 일주일동안 쉬엄 쉬엄 진행하기로 했다. 집으로 향하는 버스안에서 머릿속으로 무엇을 할까 고민하다가 확장가능한 “다중접속비행기게임”을 만들어 보기로 하였다. 평소에 WebGL로 뭔가를 만들어 보고 싶기도 했고, 웹으로 어느정도의 구현이 가능한지도 궁금했다. 설연휴가 짧지는 않으니까 조금 어렵더라도 도전해 볼만 했다.

첫째날

월요일 저녁늦게 내려오는 바람에 화요일이 개발의 첫째날이 되었다.

제일 처음 할일은 어느 정도까지 구현할것이고 어떤 라이브러리를 쓸것인가를 정하는 것이다. WebGL은 OpenGL ES 2.0 Spec의 구현체이기 때문에 WebGL을 그냥 그대로 사용하기에는 구현 시간이 너무 오래 걸릴것이 분명했다. Three.js를 쓰면 간편하게 구현할수있는 장점이 있지만 직접 WebGL을 쓰는 것은 아니기 때문에 Threejs의 객체와 구조를 익혀야 한다는 단점이 있다. 이번 hackaton에서는 WebGL의 모든것을 처음부터 만들만큼 시간이 넉넉치는 않고 어차피 Three.js에서 구현해둔 부분의 상당수를 다시 구현해야 하기 때문에 Threejs를 그냥 사용하기로 했다. 다만 Threejs의 객체들을 내가 만들 객체들에서 가지고 있는(composite) 형태로 구현하기로 하였는데 추후에 Threejs 객체부분을 교체해서 다른 라이브러리나 자동화 bot들(아마도 인공지능)을 만들기 쉽게 하려는 것이 목적이였다.

서버측 라이브러리와 플랫폼은 별다른 고민없이 선택했다. 처음에 go lang과 martini를 사용할려고 하였다가 node.js + express + socket.io를 선택하게 되었는데 socket.io의 강력한 failback, room과 namespace 기능은 다중 접속을 위해서는 반드시 필요했기 때문이다. 거기다 socket.io는 redis를 거희 즉각적으로 사용할수 있어서 scale out에도 유리했다. 웹소켓 파트만 따로 떼어서 사용해도 됬지만 웹 로그인 후 socket.io인증을 위해서는 세션정보를 얻어오기 쉬운 nodejs을 사용하는 것이 유리했다.

javascript callback hell을 벗어나기 위한 라이브러리도 선택해야 했는데, 일단 양이 별로 많지 않을 서버에서는 그냥 적절한 구조화로 해결하고 client는 모듈화를 위해서는 requirejs를 async 흐름제어를 위해 q.js를 쓰기로 했다. requirejs는 AMD 방식을 채택하고 있어 기존의 nodejs의 모듈화 방식과는 조금 다르지만 클라이언트의 비동기 스크립트 요청과 하나의 글로벌 스페이스에 더 잘 들어 맞기 때문에1 사용하기로 했고, q.js는 최근 JavaScript비동기 callback 표준이라 할수 있는 Promise의 구현체이기 때문에 사용하기로 했다.

설계원칙을 정하면서 약간은 모험을 하기로 했는데 “지금 당장 필요하지 않으면 따로 별도의 객체나 구조를 만들어 빼지않는다”는 것이다. 원래 여려명이서 하는 프로젝트의 경우에는 코드간의 디펜던시를 줄이고 협업을 쉽게 하기 위해서 되도록 모듈화를 하기 위해 노력하는데, 이번 설동안에는 혼자서 할것이고 나중에 그부분의 분리가 필요하면 반드시 스스로 분리할것이라는 믿음 하에 그렇게 하기로 정했다. 평소에는 내가 작성한 인터페이스가 어디에서 사용될지 모르기에 “잘 쓰기에 쉽고, 잘 못 쓰기에 어려운 구조”를 만드는데 공을 들였었고, 그 때문에 구조를 고민하다 많은 시간을 흘려보내기도 했다. 하지만 이러한 시간은 빠른 속도로 코딩하는 데에는 방해가 될수 있고 새로운 라이브러리를 익혀가며 쓰는 만큼 적절한 구조가 어떤 구조인지 쉽게 알수도 없어 오히려 평소와는 반대로 대부분을 분리가 필요하기 전까지 분리하지 않겠다는 원칙을 세웠다.

이 프로젝트를 하면서 사용할 툴들도 정해야 할 것 중에 하나인데, 여지껏 별로 안 쓰던 툴들을 잔뜩 적용했다. 다른 프로젝트에서는 디펜던시 관리나 배포의 편리성 혹은 다른 이유(핑계?)로 잘 안썼던 bower와 grunt를 적극 도입했다. grunt는 개발시 less 컴파일과 파일변경시 server restart를 위해 사용하였고 bower는 개발과정에서 필요한 client측 라이브러리를 잘 정리하기 위해 사용했다.

또한 몇가지 중요한 라이브러리를 정해야 했다. 게임을 만들 것이므로 키입력이 중요했다. 게임은 주로 매프레임마다 키의 상태를 확인한 후 게임의 상태를 업데이트 하는 방식으로 만들어 지므로 반드시 현재 키상태를 알수 있어야 한다. 하지만 안타깝게도 바닐라2 JavaScript는 현재 눌러진 키를 확인할수 있는 방법은 없다. 다만 keydown과 keyup이벤트를 지원하고 있으므로 이를 이용해서 키의 현재 상태를 기록할수 있다. 특히 게임에서는 Number pad의 숫자를 일반 숫자로 취급 하지 않는 것이 필요한데 매번 키코드를 찾아 내어 가면서 직접 만들기에는 시간이 많이 걸린다3. 그래서 이를 처리해줄수 있는 라이브러리는 찾아봤더니 세네개의 라이브러리가 나왔다.

키입력 라이브러리는 1. 현재 키상태를 얻어올수 있어야 하고, 2. number pad가 별도로 인식/매핑이 가능해야 하며 3. 조합키를 지원해야 게임용으로 사용하기 적합했다. 그런데 이 라이브러리들을 대부분 현재 키상태를 얻어올수 없거나 얻어오기 어려운 방식으로 되어 있었다. 유일하게 key master만이 현재 key 상태를 얻어올수 있게끔 되어 있었는데 아쉽게도 number pad를 별도로 인식하지 않게 끔 되어있었다. 다행히도 keymaster의 pull request에 해당 하는 기능의 patch가 있어서 우선 임의적으로 패치하여 사용하기로 했다.

또다른 중요한 라이브러리는 이벤트 처리기 였다. JavaScript는 이벤트 기반으로 동작하게끔 되어있지만 막상 Event를 trigger하고 broadcast할수 있는 객체가 없다. node.js 에서는 이 때문에 별도의 객체를 제공하고 있으며 그것이 바로 EventEmitter이다. 클라이언트 측에서도 이와 같은 객체가 필요하며 이를 매 프레임마다 onFrame Event를 broadcast하거나 지역 이동, 게임내 캐릭터 상태변화와 같은 다양한 곳에서 사용 될수 있다. 이를 client측에서 구현한 구현체도 어렵지 않게 찾을수 있었다.

첫날의 마지막은 Threejs와 내가 직접만든 객체간의 동기화를 어떻게 할것인지 정하는 것이었다. Threejs는 각각의 Object3D객체, 그러니까 각 렌더링 객체마다 position과 rotation등 위치정보를 담고 있었는데 내가 만들어야 하는 객체도 position과 rotation 같은 위치정보를 담고 있을 것이 확실했다. 이는 데이터의 중복이기도 하고 하나의 정보를 서로 다른 곳에서 관리하게 되면 동기화 Timing문제도 반드시 생길것이기 때문에 이를 하나로 만드는것이 반드시 필요했다. C++이라면 당연히 참조자나 포인터를 사용했겠지만, JavaScript였기 때문에 그럴수없었다. 이를 해결하기 위해서 Threejs의 Object3D 위치 정보를 defineProperty를 활용해서 서로간에 reference하기로 했다. 이 메소드는 객체의 property를 getter와 setter를 이용해 접근하기 때문에 reference 처럼 사용할수도 있고 읽기 전용 property를 만들수도 있지만 매번 호출 때마다 function call을 할것이기 때문에 성능 문제가 걱정되기는 했다. 다만 단순한 setter getter이므로 JavaScript Engine의 JIT compiler의 성능을 믿고 그냥 사용하기로 했다.

둘째날

본격적으로 비행 시뮬레이터를 만들기에 앞서서, 지형 지물을 Random Generate 하기로 했다. 대충이라도 지형 지물이 있어야 회전축과 좌표계를 인지할수 있고, 전체 세계의 모습을 둘러볼수 있기 때문이다. 어차피 추후에는 제대로 된 맵을 로딩후 사용할것이었기 때문에 임시로 사용될 맵을 만드는 것이라는 측면이 강했다. 따라서 가장 간단하게 적용할수 있는 알고리즘으로 만들기로 했는데 구글 검색으로 찾은 블로그의 맵 생성 알고리즘을 이용하기로 하였다. 다만 이 알고리즘과 구현은 이전 Three.js버전을 기반으로 만들어져서 일부 함수명이 다르고, 실제 생성시에 사용할 객체의 구조도 달라서 소스코드를 참고 하여 직접 작성하기로 하였다. 내용 자체는 어려운 내용이 아니였기 때문에 어렵지 않게 구현할수 있었다. 맵은 추후에 바뀔가능성이 매우 크기 때문에 별도의 모듈로 만들어졌고 해당 모듈에서는 generate가 호출되면 단순히 여러개의 Box를 생성하고 해당 Box를 Sence에 추가하도록 만들어졌다. Sence는 Singleton 패턴으로 구현되어 있었기 때문에 어느 모듈에서나 Add를 하면 바로 화면에 추가 될수 있도록 작성되었다.

일단 랜덤맵을 만들기는 했으나 제대로 만들어 졌는지 둘러볼수가 없었다. 따라서 마우스 움직임이나 키보드 움직임에 따라서 반응하는 컨트롤러를 만들어야 했다. 컨트롤러는 별도의 설정을 가지거나 전혀 다은 인터페이스로 동작할수도 있고 유저의 입력이 아닌 컨트롤러도 작성될수 있었고 컨트롤러가 카메라의 움직임 또한 제어 해야 했기에 User객체를 별도로 만들어 해당 객체가 Camera객체와 Drone객체를 다룰수 있는 인터페이스가 되도록 하였다. 추후에 자동 비행을 구현하거나 각종 조정키가 옵션에 의해서 변경될때는 이 User객체에서 모든것을 컨트롤 할수 있도록 하여 추후 변경범위를 좁히고자 했다. 최초에 컨트롤러를 제작할때는 기존의 Three.js의 예제 컨트롤러를 참조할까 생각하였는데, 예제 컨트롤러는 비행시뮬레이터에는 적합하지 않은 구조라서 새로 구현하기로 하였다. 회전을 하기 위해서 Threejs의 회전 함수와 더불어 여러가지 생각을 해보았는데 Threejs의 RotateX, RotateY, RotateZ와 같은 함수들은 기준 축에 대해서만 회전을 할수 있었다. 물론 기존축의 회전을 조합하면 다양한 회전을 할수 있지만, 비행체는 기준 축이 아니라 다른 축으로도 회전을 해야 하기 때문에(예를 들어 비행체가 앞으로 기울어 진경우에 roll기동을 하면 이때의 roll의 축은 좌표축이 아니라 비행기의 기수가 향하고 있는 방향의 기준 벡터이다) RotateOnAxis함수를 사용해야 한다. 하지만 왠일인지 계속해서 버그가 발생하였고 해당 버그가 잘 고쳐지지 않았다. 비행기가 90도쯤 회전하면 회전축이 이상하게 되어 버리는 버그가 있었는데 해당 버그는 잘 고쳐지지않았고 결국 다음날까지 버그를 고칠수 없었다.

셋째날

이날은 본격 설날이었기 때문에 많은 량의 작업은 하지 못했다. 그래도 간간히 버그 수정과 간단한 수정은 할수 있었다.

첫번쨰로 한일은 Camera가 Object3D라는 것을 발견한 것이었다. 이것이 중요한 이유는 Object3d인 객체는 Sence에 추가가 가능하고 다른 Object3d에 add가 가능하다. 즉 Drone Module에 add가 가능하기 때문에 비행체의 움직임과 시점의 움직임을 따로 구현할필요가 없다는 것을 의미했다. Threejs의 Object3D는 부모의 변환이 자식에게 그대로 전달되기 때문에 (traselate stack을 공부한적이 있다면 기억날것이다) 카메라가 매 프레임마다 위치를 바꾸는 코드 없이도 특정 객체를 따라다닐수 있게 된다. 즉 특정 객체와의 상대 좌표로 표현이 가능하다는 의미이다. 그렇다면 각 객체에 미리 카메라를 부착(?) 해두고 필요에 따라 다른 카메라를 선택하는 것이 가능하다. 즉 여러시점으로 보여주는 것이 상대적으로 간편해진다는 의미를 갖는다. 물론 User컨트롤러의 구현 내용이 줄어드는 것도 장점이다.

두번째로 발견한것이 Object3d의 upVector는 Object3d의 rotate property의 영향을 받지 않는 다는 것이다. 이는 upvector가 모델링의 upvector를 나타내기 때문인데 이를 착각하면 잘못된 upVector를 쓸 대가 종종 생긴다. 회전축 버그의 일부분이 바로 이 upvector의 오해 때문에 생긴 일인데 upVector가 yaw의 기준 축으로 쓰일수 있을줄 알았지만 ratate property의 영향을 받지 않아서 기준축으로 쓸수 없다.

또한 rotateOnAxis의 기준 Axis는 Global한 축을 말하는 것이 아니라 해당 Object의 Local에서의 축이라는 것을 알수 있었다.

넷째날

이날은 설날의 휴유증으로 인해 많은 것을 진행하지는 못했다. 대부분의 시간을 무료 Blender 모델을 찾는것과 Blender Export를 살펴보는데에 보냈다. 처음에는 Scene 전체를 Exports하는 실수를 해서 제대로 되지 않았지만 추후에 고쳐서 가능하게 되었다. ModelLoader는 따로 두고 모두 로딩되기 전까지는 시작되지 않도록 하였다.

결론

3~4일이라는 짧은 기간동안 간단한 비행 시뮬레이션을 만드는 데에는 성공했지만 그 이상을 작성할수가 없었다. 너무나 큰 목표이기도 했고 처음 만들어 보는 것이라 이곳 저곳에서 문제점이 발견되기도 했다. 추후에 이슈하나하나를 별도의 문서로 만들어 가며 작업하는것도 나쁘지 않을것 같다.

See Also


  1. 그래서 client측 프레임 워크인 angular는 AMD의 방식을 따르고 있다.
  2. 아무런 라이브러리도 붙이지 않은 상태를 일컫는다.
  3. 물론 이것에 대한 문서는 있다. w3c # 그리 어렵지도 않다.