brunch

You can make anything
by writing

C.S.Lewis

by Mobiinside Jan 19. 2018

미니 게임 개발기

BUZZVIL 블로그에 소개된 글을 편집한 뒤 모비인사이드에서 한 번 더 소개합니다.



버즈빌은 게임회사인가? “미니 게임 개발기” 라는 제목을 붙여놓으니 게임회사 같기도 하다. 오해하지 않도록 먼저, 버즈빌은 No.1 잠금화면 애드네트워크를 지향하는 애드 테크 회사라는것을 말해두고 싶다. 버즈빌 블로그에 올라온 다른 글들을 보더라도 대부분은 애드 테크 관련 글을 다루고 있다. 관련 기술을 기대하고 이 글을 보신 분들에게는 다소 의아할 수 있지만, 버즈빌에서 가끔씩은 하던 일에서 벗어나 이런 소소한 일탈이 허용된다는 것을 알리고 싶었다.


gettyimages



1. 어느 날 갑자기
 “나는 왜 개발자가 되었는가?” 라는 질문에 곰곰이 생각했던 적이 있었다. 컴퓨터에 대한 관심이 개발자가 되도록 했고, 컴퓨터에 대한 관심은 어릴 때부터 즐겨하던 게임들 때문이었다. 결국 게임 덕분에(?) 현재 개발자를 하고 있다. 대부분의 개발자들이 게임을 좋아하는 것으로 보아, 나뿐만 아니라 게임을 좋아해서 개발자의 길로 들어서게 된 사람들이 꽤나 많을 것 같다. 이러한 게임에 대한 관심 때문에, 막연히 언젠간 게임을 만들어보고 싶다고 생각만 하고 있었다. 


그러던 중 버즈빌에서 서비스하고 있는 허니스크린 안드로이드 앱의 UI를 개편할 일이 있었는데, 구현하고 보니 어디선가 본듯한 친숙한 모습을 하고 있다. 화면 하단의 슬라이더 뷰를 보고, 회사 내의 다른 개발자가 알카노이드(벽돌깨기) 게임과 비슷하다고 말해주었고 이 말 한마디에 한번 해볼까? 하고 미니 게임을 만들게 되었다.



2. 그래서 무엇을?

 어찌 보면 딴짓이라고 생각될 수 있는 일이 아닌 일을 시작하게 되었다. 그러다 보니 이 일에 시간을 많이 쓸 수는 없을뿐더러, 회사 내의 다른 리소스를 활용할 수도 없었다. 그래서 알카노이드의 벽돌 및 스테이지까지 모두 만들기는 무리였고, 우리 UI에 맞는 최대한 단순한 미니 게임을 만들기로 하였다. 그래서 다음과 같이 구현해야 할 리스트를 작성하였다. 처음으로 게임 비슷한 것을 만들려고 하니 아주아주 단순하게 만들기로 하였다. 



구현해야 할 리스트

- 사람의 손으로 하단 받침대를 좌우로 컨트롤 할 수 있어야 한다.
- 공은 화면 랜덤한 위치에서 생성되어서 시작된다.
- 받침대와 벽을 부딪힐 때마다 점수를 획득한다.
- 게임이 진행됨에 따라 공을 빨라지게 하여 난이도를 높여주도록 한다.
- 게임 종료 시에 획득한 점수를 보여준다.
- 스코어를 서버에 기록하여 랭킹을 보여준다.



3. 클라이언트 개발
 게임 개발을 시작하는 데 있어서 게임 엔진을 고려해볼 수도 있겠지만, 우리 앱은 게임 앱이 아니다. 앱의 성격과 상관없는 기능을 넣는 데 있어서 자칫 무거워질 수 있는 게임 엔진을 적용할 수는 없는 노릇이었다. 게임이란 것은 단순하게 생각하면, 사용자 인풋을 받아서 화면에 보이는 그래픽 요소들을 유저가 기대하는 대로 변경해주면 되는 것이다. 만들려고 하는 미니게임도 마찬가지로 터치 이벤트를 사용자 인풋으로 하여 받침대와 공의 위치를 변경해주면 되는 것이다. 이러한 단순한 생각으로 클라이언트 구현을 시작하자.



공 움직이기
 공은 한 방향으로 이동하다가 벽이나 받침대에 충돌하면 운동 방향이 변경된다. 이게 전부다. 이를 위해서는 공과 벽과의 충돌 로직 및 공의 위치 변화를 구현하면 된다. 여기서 공은 단순 원이기 때문에 중심좌표와 반지름으로 표현될 수 있고, 벽과 공의 충돌은 공의 중심부터 벽과의 거리가 반지름과 같을 때로 판별할 수 있다. 그리고 공의 운동 변화는 공의 중심좌표 이동이라고 할 수 있다. 게임 프레임 사이의 중심좌표의 이동은 다음과 같이 표현된다.


X` = X + Dx
Y` = Y + Dy
((X, Y) : 현재 위치, (X`, Y`) : 다음 위치, (Dx, Dy) : 한 프레임에서의 위치 변화)


충돌하기 전까지는 일정한 Dx와 Dy를 통해 이동하다가 충돌했을 때 각각의 값이 변한다. 충돌했을 때의 공의 운동방향 변화는 벽면과 수직 성분에 대해서만 이루어지기 때문에, 완전탄성 충돌을 가정한다면 속도의 방향만 바꿔준다. 따라서 좌우의 벽은 위 식에서 Dx만 부호를 변경해주고, 위아래의 벽은 위 식에서 Dy의 부호만 변경하여 준다. 이를 통해 오른쪽 벽 충돌 후 운동 변화를 구현한 로직은 다음과 같다.


if (rightPositionOfWall <= centerXOfBall + radius) {
 Dx *= -1
 }
 centerXOfBall += Dx
 


터치 이벤트를 수신하여 받침대 움직이기
 안드로이드에서 터치 이벤트는 액티비티(안드로이드 앱내에서 하나의 화면에 대응)에서 발생하기 때문에 액티비티 내의 터치 이벤트를 수신하여 간단히 얻어낼 수 있다. 처음 손가락으로 화면을 눌렀을 때의 X 좌표를 저장하고, 손가락 이동에 따른 X 좌표의 변화만 안다면 받침대의 새로운 위치를 알아낼 수 있다.



공의 받침대 충돌
 여기까지 구현한 것으로 기본적인 공의 움직임은 완성되었다. 손가락으로 받침대를 이동하고, 공은 의도한 대로 벽과 받침대를 충돌하며 움직였다. 하지만 이 상태로는 공의 움직임이 너무 단조로웠다. 이 때문에 알카노이드 게임을 다시 플레이해보았고, 받침대는 공의 움직임에 변화를 주는 방식이 벽과는 조금 달랐다. 벽과의 충돌은 일반적인 충돌원리가 적용이 되었지만, 공은 받침대의 어떤 위치와 충돌하는지에 따라 다른 움직임을 만들어냈다. 


몇 번 플레이를 해보니 받침대의 중심점에서 오른쪽에서 충돌이 일어난 경우는 공이 왼쪽에서 날아오든 오른쪽에서 날아오든 상관없이 충돌 후 오른쪽으로 이동하였다. 반대로 받침대의 왼쪽에서 충돌이 일어난 경우는 무조건 공이 왼쪽으로 이동하였다. 그리고 받침대 끝에 맞을수록 X 성분으로의 이동이 더 커지는 방향으로 진행했다. 이를 통해 받침대 충돌 시 일어나는 운동 변화는 다음과 같이 수정하였다.            

 

if collision_with_bottom_slider:
Dx= (centerXOfBall-enterXOfSlider) / lengthOfSlider * D
Dy = -sqrt(D*D - Dx*Dx)
(y 좌표는 아래 방향이 양의 방향이기 때문에, 받침대를 튕기고 위로 이동하기 위해 Dy는 음의 값을 갖는다, D=sqrt(Dx^2+Dy^2))


이제 점점 공이 빨라지도록 하여 난이도를 높여 나가야 했다. 이를 위해 받침대에 공이 충돌할 때마다 D 값을 높여주면서 공의 빠르기를 높여나갔다. 점수는 공이 벽이나 받침대와 충돌할 때마다 1점씩 쌓이게 하였다. 특정 공 빠르기 이상이 될 때 무조건 게임오버가 된다고 가정하면, 높은 점수를 획득하기 위해서는 받침대와의 충돌을 최소화하면서 점수를 많이 획득해야 한다. 이렇게 플레이를 하기 위해서는 받침대의 중심과 가장 먼 위치로 충돌시켜서 다음 받침대 충돌까지 가장 많은 벽과의 충돌을 만들어 내야 한다. 



게임 객체 그리기
 안드로이드에서는 일반적인 뷰를 UI 쓰레드 내에서 그리게 된다. 사용자의 터치 이벤트를 포함한 대부분의 인풋도 UI 쓰레드 내에서 처리한다. 만약 일반적인 뷰를 통해 게임 객체들을 그리게 된다면, 그리고자 하는 뷰가 많아지면서 터치 이벤트 처리에 지연이 생기고 이 때문에 게임이 느려지거나 심지어는 ANR(안드로이드에서 응답이 없을 경우 에러)이 발생하기도 한다. 따라서 공이나 받침대와 같이 게임 플레이 시에 변화하는 객체들을 그려내기 위해서는 일반적인 뷰를 그리는 방식으로는 자연스러운 게임 움직임을 그려낼 수 없다.

 

이를 해결하기 위해서는 안드로이드에서 제공하는 Surface View를 사용하면 된다.Surface View를 사용하게 되면 화면 그리기를 UI 쓰레드가 아닌 다른 쓰레드에서 처리하도록 만들 수가 있다. 따라서 터치 이벤트만 Surface View에 지속적으로 전달하고, 받침대 및 이에 따른 공의 움직임을 Surface View를 통해 그려내면 된다.



4. 서버 개발
 원래는 클라이언트만 구현하고 적당히 마무리하려고 하였다. 그리고 허니스크린의 UI가 변경되면서 UI에서 확장된 미니게임의 의미가 없어진 것 같아 미니 게임을 제거하려고 하였다. 하지만 그동안의 게임 플레이 현황을 보니 매일 몇만 번의 플레이가 일어나고 있었고 허니스크린 네이버 연관검색어 상위권에 랭크되어 있었다. 제거한다면 많은 유저들의 악플에 시달릴 것 같아 계속 가져가기로 하고, 이참에 이벤트성으로 서버에서 랭킹 페이지까지 만들어 보기로 하였다. 


서버에서 점수를 기록하여 1~30위까지의 점수 및 본인의 랭킹을 보여주기로 하였다. 구현 방법으로는 DB 사용, 로그 분석 등 다양한 방법을 생각할 수 있다. DB는 구현이 단순하지만 데이터 양이 많아질수록 부담을 주게 될 테고, 로그 분석은 실시간성이 떨어진다. 물론 이를 해결할 수 있는 다양한 방법이 있겠으나 들어가는 리소스를 생각해야만 했다. 최소한의 리소스로 구현 방법을 찾던 중 다행히 우리가 사용하는 Redis에서 아주 쉽게 랭킹을 구현할 수 있는 기능을 제공하고 있었다. 


Redis는 인 메모리 기반 데이터베이스로 다양한 자료구조와 커맨드를 지원하였다. ZADD 커맨드를 사용하여 스코어를 기록하면 언제든지 ZRANGE 커맨드를 통해 높은 점수순으로 원하는 순위까지의 데이터를 얻을 수 있다. 여기서 주의할 점은 ZADD는 한 유저에 대해 이미 스코어가 기록된 경우 기존 스코어를 업데이트 하고 새로운 랭킹이 계산된다. 따라서 점수를 기록할 때 매번 ZADD를 호출한다면 유저의 최근 점수 기준으로 랭킹이 계산된다. 


현재 점수 기준이 아닌 각 유저 별 최고기록으로 랭킹을 보여줘야 하기 때문에 ZSCORE를 통해 먼저 유저의 점수를 가져오고 더 높은 점수를 획득한 경우만 ZADD를 호출하도록 하였다. 열심히 ZADD로 점수를 기록하고 랭킹은 ZRANGE를 통해 가져오면 끝이다. Redis 덕분에 스코어 랭킹 시스템은 쉽게 구현이 되었다. 



5. 결과
 위에서는 주요 로직들에 대해서 다루었지만, 실제로는 게임 어뷰징을 막기 위한 안전장치들과 많은 예외상황 처리들도 포함되어 있기에 생각보다 많은 노력이 들어갔다. 랭킹페이지를 구현하고 3일간의 랭킹 이벤트를 진행하였는데, 이 기간 동안 한국, 대만, 일본 합쳐서 60만 번의 점수가 기록되었고, 유저별 최고득점은 327점까지 나왔다. 아직은 게임이라고 하기엔 자연스럽지 못한 부분도 많고, 최적화할 부분도 많지만 다행히 별 사고 없이 미니게임 이벤트가 종료되었다. 


처음에는 이스터 에그로 시작되었지만 이벤트에도 활용되면서 하나의 기능이 되었기에 더 이상 이스터 에그라고 하기는 힘들 것 같다. 그렇다면 다음에는 어떤 이스터 에그를???..  



[버즈빌의 누구나 궁금해하는 개발 이야기] 시리즈  

아마존 에코 음성인식 에어컨 제어

안드로이드 앱에 MVP 패턴 적용하기

How to use Django rest framework

안드로이드 파편화(Fragmentation)에 대하여

오픈소스를 쇼핑하는 엔지니어


원문 바로가기: 미니 게임 개발기  




브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari