게임 메이커: 아두이노 게임기 만들기 Part2 – 게임 프로그래밍 준비
강좌 시리즈:
.
제가 처음 PC를 만져본게 중 1때 였던걸로 기억합니다. 당시에는 XT 또는 16비트 컴퓨터로 불리곤 했었죠. 학원을 다니면서 GW 베이직을 배우긴 했습니다만 당시 제 수준에서 할 수 있는 프로그래밍이란게 별거 없었죠. 그래서 대부분의 시간은 (다른 대부분의 아이들처럼) 게임을 하는데 보냈습니다. 처음 테트리스를 접한것도 이때네요. 지금은 SW관련된 일을 하다보니 어느정도 원하는 프로그램은 직접 짤 수 있는 수준이 되었지만 어릴적만큼 PC를 가지고 노는 일이 신기하진 않습니다.
아두이노가 가진 잠재력이 풍부하다는 점에는 이견이 없지만 하드웨어적인 성능의 한계를 고려하면 딱 제가 처음 PC를 배우던 시절과 컴퓨팅 파워가 유사하다고 느낍니다. 그래서 그 때 제가 가장 즐겨 했던 일, 게임을 아두이노로 구현하게 되었습니다.
이번 파트에서는 아두이노로 게임을 구현하기 위해 필요한 사전지식을 다룰려고 합니다. 그리고 다음 파트부터 본격적으로 아두이노 게임 프로그래밍을 시작하도록 하겠습니다.
.
게임을 구성하는요소들
PC에서 접하는 게임들은 다양한 구성요소들이 전문적으로 개발되고 상호 결합합니다. 디스플레이, 3D 그래픽과 게임엔진, 사운드, 유저 인터페이스, 입력장치 컨트롤, 네트워크, 결재, 게임 개발 툴 등등… 그래서 이런 게임들은 사실 개인 수준에서 구현해보기가 어렵습니다. 안드로이드나 아이폰용 간단한 게임을 만드는 것도 실은 꽤나 복잡한 개발 환경을 익히고 해당 플랫폼의 특성을 익혀야 가능합니다.
그런점에서 아두이노를 이용한 게임 개발은 많은 문제들을 단순화 해줍니다. 단순한 아두이노 스케치 구조를 익히고 프로그래밍 언어에 대한 기본 지식만 있으면 상대적으로 쉽게 도전이 가능합니다. 그리고 게임기를 직접 만들어서 사용할 수도 있구요. 대신 아두이노가 가진 성능의 한계 때문에 8비트 게임 수준의 구현만 가능합니다.
먼저 아두이노 게임기를 만들기 위해 필요한 최소한의 하드웨어를 보겠습니다.
- 아두이노 : PC의 메인보드, CPU 역할
- 버튼, 조이스틱 : 사용자 입력을 받기 위한 장치들
- OLED디스플레이 : 게임 화면
- 사운드 : 버저 혹은 MP3 모듈을 활용한 게임 사운드
이 중 사운드는 필수가 아니므로 실질적으로는 아두이노, 버튼-조이스틱, 디스플레이만 있으면 됩니다. 이 하드웨어들을 적절히 컨트롤 하면서 게임이 진행될 수 있도록 SW 구조를 잡아 게임을 만들면 됩니다.
.
게임 화면과 OLED
게임에서 가장 중요한 요소는 바로 게임 화면입니다. 특히 아두이노에서는 어떤 디스플레이 장치를 연결해서 사용하느냐에 따라 게임 프로그래밍에서 가장 중요한 drawing 방법이 결정됩니다. 아두이노에 사용할 수 있는 디스플레이는 여러 종류가 있지만 아두이노의 성능을 고려해보면 선택할 수 있는 디스플레이는 많지 않습니다.
기존에 있던 아두이노 게임기 프로젝트를 살펴보면 2가지 종류의 디스플레이를 주로 사용합니다. 하나는 128×64해상도의 OLED 이고 다른 하나는 88×64(또는 99×64) 해상도의 Nokia LCD 디스플레이입니다. 디스플레이 종류에 따라 사용되는 라이브러리가 바뀌게되고, 라이브러리에 따라 지원되는 drawing 함수나 폰트, 메모리 사용량 등 여러가지 차이가 발생하게 됩니다.
개인적으로는 128×64해상도의 OLED 디스플레이가 아두이노 게임기에는 적절해 보입니다. 아두이노 입장에서는 꽤나 고해상도이기 때문에 램 소비량이 높다는 점이 단점이지만 라이브러리도 잘 갖춰져 있고 근 2년새 많이 대중화 되어서 디스플레이 모듈 구하기도 쉬운 장점이 있습니다. 그리고 게임보이와 같이 상업적으로 판매하기 시작하는 아두이노 게임기가 있기 때문에 앞으로 개발되는 게임 구하기도 쉬운 장점이 있습니다. 그래서 여기서는 128×64 OLED 디스플레이를 기준으로 설명하겠습니다.
.
그래픽 라이브러리와 그래픽 버퍼
게임 화면을 위한 하드웨어가 결정되면 이걸 제어하기 위한 라이브러리가 필요합니다. 그래픽 라이브러리라고 하는데 OLED 디스플레이용으로는 Adafruit 라이브러리와 u8glib 라이브러리를 사용할 수 있습니다. 이 두 가지 라이브러리는 특징이 정 반대입니다.
라이브러리의 특징을 결정짓는 가장 중요한 요소는 그래픽 버퍼입니다. 아두이노에서는 현재 디스플레이 화면의 상태가 어떤지 디스플레이에 물어볼 수가 없습니다. 그리고 디스플레이에 점 하나를 찍더라도 특정 픽셀만 바꾸는 것이 아니라 인접 픽셀들의 값을 같이 업데이트 해줘야 하는 제약이 있습니다. 그래서 아두이노는 화면의 크기만큼 메모리를 할당해서 일단 메모리에 그리는 작업을 한 뒤, 이 값들을 OLED로 한번에 전송해서 디스플레이를 제어합니다. 여기서 아두이노가 잡아둔 메모리를 그래픽 버퍼라고 합니다. 그래픽 라이브러리의 핵심은 이 버퍼를 어떻게 관리하느냐 입니다.
Adafruit 라이브러리는 그래픽 버퍼를 관리하기 위해 단순하고 상식적인 방법을 사용합니다. 바로 OLED 디스플레이 크기만큼 그래픽 버퍼를 잡아버리는 겁니다. 이 강좌에서 사용하는 128×64 OLED의 경우 화면 크기만큼 그래픽 버퍼를 잡을 경우 1KB의 메모리가 필요합니다. 그런데 아두이노 UNO 보드는 단 2KB의 메모리만을 가지고 있습니다. 그만큼 게임 엔진에 사용할 수 있는 메모리가 줄어드는 것이 단점입니다. 대신에 화면 일부만 업데이트가 가능하고 그리는 속도가 상대적으로 빠릅니다.
U8glib 라이브러리는 그래픽 버퍼를 약간 다른 방식으로 관리합니다. 화면을 2의 배수(2, 4, 8, 16)로 분할한 만큼만 그래픽 버퍼를 잡아둡니다. 그리고 화면을 그릴 때는 분할한 만큼 화면 전체를 그리는 작업을 반복합니다. 즉, 라이브러리가 4분할 한다고 가정하면 화면 전체 그리는 코드가 4번 반복되면서 분할한 영역을 하나씩 채우게 됩니다. 아두이노 보드에서는 u8glib 라이브러리가 8분할을 하기 때문에 화면을 그리는 루틴이 8번 반복됩니다. 따라서 메모리는 1/8 만 사용하지만 그리는 속도가 느려지고 화면 일부만 업데이트 할 수가 없습니다.
일반적으로는 u8glib의 방식이 더 유용합니다. 아두이노와 같은 마이크로 컨트롤러는 대부분 속도는 빠르지만 램 크기가 매우 작기 때문입니다. 하지만 액션, 슈팅 게임처럼 화면이 복잡하고 업데이트가 빈번한 경우는 화면을 그리는 속도가 주요한 이슈이기 때문에 Adafruit 라이브러리가 더 좋습니다. 그래서 이 강좌에서는 Adafruit 라이브러리를 사용합니다.
.
게임방식과 화면 업데이트
화면을 업데이트 할 수 있는 하드웨어, 라이브러리의 준비가 완료되면 게임엔진에서 어떻게 화면을 그릴지를 결정해야 합니다. 이때 FPS(Frame Per Second)라는 단어가 사용되는데 초 당 몇 번 프레임(화면)을 그릴 것인지를 나타내는 단어입니다. 단순히 아두이노의 loop() 반복문을 돌면서 쉼없이 화면을 그린다면 화면은 1초에 몇 번 그려질지 장담할 수 없습니다. 게임의 로직을 처리하고 화면 그리는 작업에 걸리는 시간은 매 프레임마다 다를 수 있기 때문에 화면 그리는 간격도, 게임상의 시간도 들쭉날쭉해집니다. 따라서 1초에 몇 번 화면을 그릴지 개발자가 미리 정하거나 상황에 따라 가변적으로 조절될 수 있도록 만듭니다..
int FPS = 1000/10; unsigned long cTime = 0; void loop() { if(millis() > cTime + FPS) { // draw screen … cTime = millis(); } }
위 소스에서 FPS가 초당 화면을 그리는 횟수를 결정합니다. 1000/10 이므로 1초를 10분할 한다는 뜻입니다. 즉 100ms 에 한 번, 1초에 10번 화면을 그리도록 간격을 잡아둔겁니다. 초당 5프레임만 그리고 싶다면 1000/5 를 사용하면 되겠지요. Loop() 함수가 무한반복 되면서 계속 현재 경과시간과 FPS 간격을 체크합니다. 그래서 100ms 가 지났을 경우만 화면을 그리도록 코드가 작성되어 있습니다. 이 방법으로 FPS 를 구현하면 됩니다.
FPS 간격에 따라 게임화면을 그릴 때 화면 전체를 다시 그릴지, 화면 일부만 업데이트 할지도 중요한 요소입니다. 가장 확실하고 간단한 방법은 화면 전체를 다시 그리는 것입니다. 게임 화면에 있는 오브젝트들을 조금씩 위치를 이동시키고 화면 전체를 그리는 작업을 반복하면 마치 애니메이션처럼 움직이는 게임화면이 될 것입니다. 하지만 이런 작업은 각각의 프레임을 그리는데 많은 연산을 필요로합니다. 배경부터 맵, 캐릭터, 오브젝트, 각종 정보창 내용을 모두 그리는데 필요한 시간이 FPS 간격보다 길어지면 우리가 굳이 FPS 간격을 설정한 이유가 없어져 버리기도 합니다. 화면에서 변화되는 요소들이 많지 않다면 화면 일부만 지우고 다시 그리는 것이 좋겠지요.
FPS 개념을 구현하는 것이 반드시 필수는 아닙니다. 사용자 입력이 그리 빈번하지도 않고 화면도 정적인 경우, 예를 들어 보드게임이나 퍼즐게임, 텍스트 기반 게임의 경우는 사용자의 입력이 있을 때나 이벤트가 있을 때 화면이 업데이트 되도록 만들 수도 있습니다. 따라서 게임의 성격, 화면 구성에 따라 어떻게 할지 선택하면 됩니다.
이 강좌에서는 게임 진행 화면을 FPS 개념을 이용해서 구현했습니다.
.
사용자 입력 처리
여기서는 사용자의 입력을 받기 위해 조이스틱과 버튼을 사용합니다. 조이스틱 모듈은 XY축 방향의 움직임(상하좌우 4방향 움직임)을 analog input 으로 전달해줍니다. 따라서 아두이노의 analog pin 2개를 사용해서 조이스틱 움직임을 감지하고 0~1023 범위 중 어디에 해당하는 값인지를 보고 상하좌우를 판단합니다. 조이스틱 모듈의 핸들을 누르면 버튼 입력도 가능한데 여기서는 사용하지 않습니다.
그리고 2개의 버튼이 별도로 배치되어 있습니다. 이 두 개의 버튼은 아두이노의 digital pin 2개로 감지합니다. 아두이노의 무한 반복함수인 loop() 함수 안에서 조이스틱과 버튼의 입력을 빠르게, 계속 체크합니다. 그리고 FPS 간격에 따라 다음 장면을 처리하기 전까지 어떤 버튼이 눌러졌는지 어떤 움직임이 감지되었는지 누적해둡니다. 다음 장면이 처리될 때 누적해둔 사용자 입력을 적용합니다.
.
사운드
이번 강좌에서 사운드는 고려하지 않습니다. 사운드 기능까지 더하고 싶은 경우는 버저, 피에조 스피커를 연결해서 사용하거나 별도의 MP3 모듈을 더해서 사용할 수 있을 것입니다.
다음 포스트부터는 본격적으로 게임을 설계하고 단계별로 구현해 나가도록 하겠습니다.
.
참고자료 :