게임 메이커: 아두이노 게임기 만들기 PART3 – 기획 및 기본 구조
강좌 시리즈:
.
본격적으로 게임 프로그래밍을 시작해 볼 차례입니다. 지난 강좌에서 만들었던 하드웨어를 그대로 사용할 예정이므로 여기서는 SW 부분만을 다룹니다.
강좌를 통해 테스트로 만들어볼 게임은 횡 스크롤 액션 게임 러닝맨(Running Man)입니다. 점프와 사격, 두 개의 버튼만을 사용해서 다가오는 장애물을 뛰어넘고 전방에 나타나는 해골을 없애면서 점수를 얻어나가는 게임입니다. 아래 사진처럼요…
.
게임 프로그래밍에 사용되는 파일들
먼저 게임 프로그래밍에 사용될 예제 소스코드를 다운로드 받으세요. 아래 GitHub 링크에서 다운로드 받을 수 있습니다.
https://github.com/godstale/game-maker
[Template/Template] 폴더를 열어보면 파일들이 여러 개 있습니다. 미리 게임 프로그래밍에 필요한 파일들을 기능별로 나눠둔 것입니다. 각 파일들이 담당하는 역할은 다음과 같습니다.
- Adafruit_GFX.cpp, Adafruit_GFX.h
Adafruit에서 만든 그래픽 라이브러리 파일입니다. 화면에 글자를 쓰거나 도형, 이미지를 그리는데 필요한 함수들이 정의되어 있습니다. 특별히 손 댈 필요가 없는 파일입니다. - Adafruit_SSD1306.cpp, Adafruit_SSD1306.h
Adafruit에서 만든 그래픽 칩 제어용 라이브러리입니다. I2C 통신(혹은 SPI)을 이용해 그래픽 드라이버 칩셋(SSD1306)을 제어합니다. 마찬가지로 손 댈 필요가 없는 파일입니다. - InputController.cpp, InputController.h
조이스틱의 상하좌우 움직임과 두 개의 버튼 입력 여부를 감지합니다. 본인의 조이스틱 종류와 연결 방법에 따라 이 부분을 살짝 수정해줘야 정상작동이 가능합니다. - Template.ino
실제 게임을 진행하기위한 코드들이 모두 들어있는 메인 파일입니다. 아두이노의 기본 구조인 setup(), loop() 함수를 중심으로 동작합니다. - bitmaps.cpp, bitmaps.h
이미지(bitmap)를 코드형태로 담고 있는 파일입니다. 게임 화면을 그리기 위해 사용되는 이미지들은 모두 여기에 담아두고 메인 파일인 Template.ino 파일에서 호출해 사용합니다. - glcdfont.c
화면에 ASCII 문자를 표시할 때 사용할 폰트 이미지를 담고 있습니다. 손댈 필요 없는 파일입니다.
모든 내용을 Template.ino 파일에만 담으면 파일이 너무 커지므로 기능에 따라 적당히 분리시켜 둔거라고 생각하시면 됩니다. 게임을 만들다가 파일 덩치가 너무 커지는 것 같으면 위와 같이 기능별로 분리해서 관리하는 것이 좋습니다.
.
게임 기획 (동작 시나리오)
무작정 게임 프로그래밍을 시작하기 전에 간단한 기획부터 해야 합니다. 아두이노에 전원이 들어올 때부터 실제 게임 진행과 종료까지 어떤 시나리오로 진행될지를 결정하는 겁니다. 런닝맨 게임은 보통 게임에 많이 사용되는 화면들을 모두 포함해서 아래와 같이 진행되도록 기획했습니다.
- 시작 애니메이션 화면
부팅과 동시에 아두이노 로고 이미지를 움직이는 애니메이션을 잠시 보여주는 화면. 사실 없어도 되지만 애니메이션 효과를 구현하는 기본 방법을 보여주려고 넣은 화면입니다.
- 게임 로고 화면
게임의 로고 이미지가 표시되고 사용자 입력을 기다리는 화면. 사용자가 버튼을 누르면 다음 화면으로 전환됩니다.
- 메뉴 화면
사용자가 선택 가능한 메뉴 항목들을 보여주고 사용자의 선택에 따라 해당 화면으로 전환될 수 있도록 해줍니다. 예를 들어 게임 상태를 저장(Save)-불러오기(Load)가 가능하도록 할 경우 게임을 시작하기 전에 이런 메뉴 화면을 통해 몇 가지 선택지를 제시할 수 있습니다. 러닝맨 게임에서는 저장-불러오기를 사용하지 않으므로 ‘게임시작’, ‘제작자 정보’(Credit) 두 가지 메뉴를 선택할 수 있도록 하겠습니다. 메뉴 화면에서 다른 화면으로 전환되더라도 다시 메뉴 화면으로 돌아오도록 만들어야 합니다.
- 제작자 정보(Credit) 화면
제작자 정보를 보여주는 화면입니다. 아무키나 누르면 다시 메뉴 화면으로 전환됩니다.
- 게임 화면
실제 게임을 진행하는 화면입니다. 게임 엔진이 본격 가동되는 가장 중요한 화면임은 두 말할 필요가 없습니다. 특정한 종료 조건이 되기 전까지는 이 화면이 계속 유지됩니다.
- 결과 화면
게임 종료 조건이 되면 실행되는 화면. 게임 엔진의 동작이 멈추고 결과(점수)를 보여줍니다. 아두이노의 전원이 꺼지더라도 최고 점수가 유실되지 않도록 EEPROM을 사용합니다. 아무 버튼이나 누르면 다시 메뉴 화면으로 돌아갑니다.
.
게임 기획 (세부 내용 결정)
게임 구조에 대한 큰 틀이 정해지면 이제 보다 세부적인 요소들을 결정하고 준비해야 합니다. 로고 이미지, 조이스틱-버튼의 사용법, 게임 화면의 세부구성, 캐릭터 이미지의 크기와 배치 위치, 장애물-적 크기 및 이미지와 움직임, 점수 계산법과 게임 종료 조건 등… 실제 코딩에 앞서 세부적인 그림을 그려두는 것이 좋습니다. 그리고 게임에 필요한 이미지 등을 미리 준비해 두는 것이 좋습니다. 그래야 게임이 일관성 있게 원래 기획의도에 크게 벗어나지 않은 상태로 제작됩니다. 간단한 게임이야 생각만 미리 해두고 만들어가도 상관없겠지만 게임 덩치가 조금만 커져도 점점 임기응변식으로 프로그래밍을 하게 되고, 게임이 본래 의도와 멀어져 버립니다.
일단은 강좌를 따라서 해보는 형태이므로 이런 세부 기획을 직접 하기보다는 ‘게임을 진행하기 위해 대충 어떻게 프로그래밍 하면 되겠다‘ 정도를 미리 생각두세요. 그리고 예제 파일들고 생각했던 것이 어떻게 차이가 나는지 한번 확인해보세요. 본인의 스타일대로 수정하셔도 좋습니다.
.
게임 소스 기본 구조 잡기
Template.ino 파일을 열어보세요. 런닝맨 게임을 구현하기 위한 기본 구조가 잡혀져 있을겁니다. 코드를 세부적으로 살펴보도록 하겠습니다.
소스코드는 크게 ‘전역 변수’, ‘setup()’, ‘loop()’ 세 부분으로 나뉘어져 있습니다.
전역 변수 부분은 setup() 함수가 시작하기 전까지의 코드입니다. 게임 제작에 필요한 라이브러리 및 외부 파일 include를 하고 필요한 변수들을 선언하는 부분입니다.
아래 부분은 화면의 크기와 화면 갱신 시간을 결정하는 변수들입니다.
// Display variables int HEIGHT = 64; int WIDTH = 128; int gameFPS = 1000/10; // Time variables unsigned long lTime;
lTime 변수는 gameFPS(프레임 업데이트 간격)가 지났는지 확인하는 용도로 사용하기 위해 지난번 업데이트 시간을 저장하는 변수입니다.
아래 변수들은 화면 간 전환을 구현하기 위해 선언했습니다.
// Game status #define STATUS_MENU 0 #define STATUS_PLAYING 1 #define STATUS_PAUSED 2 #define STATUS_RESULT 3 #define STATUS_CREDIT 4 int gameState = STATUS_MENU; // Pause or not
게임 로고 화면까지는 아두이노 부팅시 한번만 실행되므로 setup() 함수 안에서 처리해주면 됩니다. 하지만 이후 menu 화면에 진입하고 부터는 게임화면과 메뉴화면, 기타 화면간 계속 전환을 해줘야 합니다. 그래서 현재 화면이 무엇인지, 바뀔 화면이 무엇인지 gameState 변수로 관리합니다.
메뉴화면-게임화면-결과화면-게임정보화면이 정해진 숫자로 표현되도록 #define 구문을 사용했습니다.
사용자 입력은 InputController.cpp에서 처리합니다. InputController 클래스의 getInput() 메서드를 호출하면 현재 조이스틱-버튼 입력 상태를 체크해서 결과를 넘겨줍니다. 결과값을 보고 현재 어떤 버튼들이 눌러져 있는지 알 수 있습니다. 이 결과를 저장하는 변수들을 아래와 같이 선언했습니다.
// Input InputController inputController; bool up, down, left, right, aBut, bBut;
이제 아두이노에 전원이 들어가면 가장 먼저 실행되는 setup() 함수를 통해 ‘시작 애니메이션 화면’과 ‘로고 화면’을 만들어 보겠습니다.
setup() 함수가 실행되면 조이스틱-버튼, 디스플레이와 같이 물리적인 장치들이 연결된 핀과 라이브러리를 초기화 해줍니다.
// Initialize display #ifndef SSD1306_I2C SPI.begin(); display.begin(SSD1306_SWITCHCAPVCC); #else display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // initialize with the I2C addr 0x3D (for the 128x64) #endif display.setTextSize(1); display.setTextColor(WHITE); //digitalWrite( 17, LOW); // what is this?? // set button pins as INPUT_PULLUP pinMode(6,INPUT_PULLUP); pinMode(5,INPUT_PULLUP);
그리고 이어서 시작 애니메이션을 그려줍니다. for 반복문을 수차례 실행하면서 로고 이미지를 최상단에서 조금씩 아래로 이동해서 그려줍니다. 그러면 로고 이미지가 아래로 떨어지는 것처럼 애니메이션 효과가 납니다. 애니메이션이 끝나면 잠시 쉬었다 다음 화면으로 넘어갑니다.
아래 애니메이션 만드는 코드를 유심히 보세요. display.clearDisplay() 함수는 화면을 깨끗이 지워줍니다. display.drawBitmap() 함수는 특정 위치 x, y 에 이미지 arduino를 가로x세로=32×8 pixel 크기로 그려줍니다. 마지막 파라미터는 0과 1을 사용할 수 있고 BLACK, WHITE 색상을 의미합니다. 여기서 이미지 arduino 는 어디에 정의되어 있을까요? 네. bitmaps.cpp, bitmaps.h 파일에 정의되어 있습니다. 이번 예제를 위해 필요한 시작 애니메이션 이미지와 로고 이미지를 arduino[], nightrun[] 으로 미리 넣어뒀기 때문에 아래처럼 사용만 하면 됩니다.
// display startup animation for(int i=-8; i<28; i=i+2) { display.clearDisplay(); display.drawBitmap(46,i, arduino, 32,8,1); display.display(); } delay(2000);
이제 로고 화면을 그려줍니다. display.clearDisplay()를 실행해서 화면을 지우고 로고 이미지(128×64)로 화면을 꽉 채웁니다. 그리고 while 반복문을 계속 반복시켜버립니다. while 반복문은 버튼을 눌러야만 빠져나올 수 있습니다. 버튼을 누르면 메뉴 화면으로 들어가는데 여기서부터는 아두이노가 살아있는 동안 무한 반복되는 loop() 함수 안에서 그려집니다.
// display logo display.clearDisplay(); display.drawBitmap(0,0,nightrun,128,64,1); display.display(); // Waiting for user input bool waitingForInput = true; while (waitingForInput) { if (digitalRead(6)==0) { waitingForInput = false; } if (digitalRead(5)==0) { waitingForInput = false; } } delay(500);
loop() 함수 안에서 해주는 작업은 크게 두 가지입니다. 먼저 inputController.getInput()을 통해 사용자 입력이 있는지를 체크하고 어떤 입력을 받았는지 변수에 저장해둡니다.
uint8_t input = inputController.getInput(); // Get input status if (input & (1<<5)) left = true; if (input & (1<<4)) up = true; if (input & (1<<3)) right = true; if (input & (1<<2)) down = true; if (input & (1<<1)) aBut = true; // a button if (input & (1<<0)) bBut = true; // b button
버튼 체크가 끝나면 현재 모드(gameState)에 따라 화면을 그려주도록 해야합니다. 그리고 화면 갱신은 gameFPS(프레임 업데이트 간격)에 맞게 해야합니다. 이걸 구현한 코드가 아래와 같습니다.
if (millis() > lTime + gameFPS) { lTime = millis(); if (gameState == STATUS_MENU) { // Main menu // TODO: } else if (gameState == STATUS_PAUSED) { // If the game is paused // TODO: } else if (gameState == STATUS_PLAYING) { // If the game is playing // Calc game turn // Draw game screen } else if (gameState == STATUS_RESULT) { // Draw a Game Over screen w/ score if (gameScore > gameHighScore) { gameHighScore = gameScore; } // Update game score display.display(); // Make sure final frame is drawn } else if (gameState == STATUS_CREDIT) { // Draw a Game Over screen w/ score display.display(); // Make sure final frame is drawn } }
현재 시간인 millis() 값이 앞선 업데이트 시간(lTime)에 gameFPS(프레임 업데이트 간격)를 더한 값보다 클 경우 화면을 업데이트해야 할 시간으로 판단해서 화면 그리는 루틴을 실행합니다.
그리고 그릴 화면은 현재 gameState 변수 값이 어떻게 설정되어 있냐에 따라 달라집니다. 즉, 화면을 전환하고 싶으면 gameState 변수 값을 바꿔주면 되는 것입니다.
자, 여기까지 작성한 소스코드를 아두이노에 올리면 시작 애니메이션 – 로고 화면이 차례대로 나오는 모습을 보실 수 있을 겁니다. 아직 loop() 함수안에서 각 화면을 그리는 코드를 넣지는 않았기 때문에 더 이상 화면이 바뀌지는 않습니다. 하지만 화면을 구성하기 위한 기본 소스코드 구조는 갖추었습니다.
다음 강좌에서는 메뉴화면과 게임화면, 결과화면, Credit 화면 그리는 루틴을 더해서 조이스틱과 버튼으로 화면 간 전환이 가능하도록 만들어 보겠습니다. 본격적인 게임 화면을 그리기에 앞서 부가적인 화면 제작을 미리 해두면서 연습하는 과정입니다.
.
참고자료 :