2020. 6. 11. 15:11ㆍToy Project/Programming
** 모든 소스코드는 저의 Github에 공개되어 있습니다!
SFML(Simple and Fast Multimedia Library)은 이름에서부터 알 수 있듯이 가볍고 쉬운 멀티미디어 라이브러리이다. OpenGL에 기반으로 두고있으며, 크로스 플랫폼을 지원하여 '게임'이나 '멀티미디어 응용 프로그램' 제작에 주로 사용된다. 따라서 본 포스트에서는 SFML을 활용하여 C++로 나만의 게임을 개발한 과정을 설명하고자 한다.
1. 게임 소개
1 ) 개발 동기
고전 게임인 Snake Game은 1970년대 게임으로, 플레이어가 뱀을 상하좌우로 조작하며 아이템(사과)을 획득하고 점점 성장하는 게임이다. 이때, 벽이나 자신의 몸통에 부딪히면 게임은 종료된다. 1
이처럼 게임 규칙은 단순하지만 중독성이 강해 아직까지도 수많은 변형 게임이 나오고 있고, 구글에 'Snake Game'이라고만 검색해도 바로 게임을 해볼 수 있다.
하지만... 변형 게임이라 한들 다 Snake Game에서 살짝만 변형시킨 것 뿐이어서 기존 게임과는 큰 차별성이 없다. 따라서 본 작성자는 Snake Game의 아이디어를 차용해서 "Snake Game과 비슷하지만 완전히 다른, 차별성 있는 게임"을 개발하고자 하였다.
2 ) 시나리오
"Snake Game과 비슷하지만 완전히 다른, 차별성 있는 게임"
참 거창한 목표이다. 어떻게 하면 목표에 부합하는 시나리오를 구성할 수 있을까? 우선 기존의 Snake Game을 분석해보자.
Q1. Snake Game에서 찾을 수 있는 좋은 아이디어는 무엇이 있을까?
첫 번째로 게임이 진행될수록 '공간적 제약'이 발생한다는 점이다. 뱀이 사과를 먹으면 먹을수록 몸통의 길이가 길어져 움직일 수 있는 공간이 점차 줄어들며, 이에 따라 게임도 어려워진다.
그리고 어떤 길을 선택해야 할지 계속해서 고민해야 한다. 뱀의 방향을 잘 못 선택하면 움직일 수 있는 공간이 순식간에 좁아져 죽을수도 있기 때문이다. 따라서 짧은 시간내에 최적의 길을 생각해내는 것이 중요하다.
☞ 본 작성자는 이 두 가지 요소가 Snake Game의 중독성을 책임지고 있는게 아닐까 생각했다. 따라서 이러한 강점을 나의 게임에도 잘 적용시켜 중독성이 강한 게임을 개발하고자 하였다.
Q2. Snake Game에서 보완해야 할 단점은 무엇이 있을까?
우선 뱀에게 너무 많은 역할이 부여되었다. 뱀이 사과를 먹어야 게임이 진행되며, 자신의 몸과 부딪히면 죽으므로 뱀은 '플레이어'이면서 동시에 '장애물' 역할을 한다. 이 두 가지를 한 객체가 소화함으로써 게임의 난이도가 다소 급격하게 변하는 느낌이었다.
그리고 이는 매우 주관적인 생각이긴 하지만, 매번 게임을 할 때 마다 자비가 없다는 느낌을 받았다. 부딪히면 바로 죽기 때문에 단 한번의 실수도 용납되지 않는다.
☞ 따라서 나의 게임에는 생명력을 길게 유지시킬 수 있는 장치를 포함시키고, 뱀의 역할을 분리하고자 하였다.
이러한 생각들을 바탕으로 새롭게 디자인한 나만의 게임은 다음과 같다.
우선 뱀의 역할을 분리하여 플레이어와 장애물을 다르게 두었다. 또한, '라운드(round) 제도'를 도입하여 게임이 진행될수록 장애물의 개수를 늘리며 플레이어의 활동 범위에 제약을 주었다. 더불어 아이템을 추가하여 게임을 오랫동안 지속할 수 있도록 하였다.
게임의 컨셉은 식상한 '뱀'과 '사과'에서 벗어나 한국적이고 산뜻한 분위기를 주고자 하였다. 따라서 한국의 국화인 무궁화를 주제로 삼았고 게임 제목 또한 '무궁'으로 정하였다. 여기서 '무궁'에는 두 가지 의미가 있는데 첫 번째는 무궁화의 무궁이고, 두 번째는 벌에 부딪혀 죽지 않는 이상 무궁무진하게 진행될 수 있는 게임의 특징을 살린 이름이다. 뱀은 소녀로 대체하여 꽃과 함께 어우러져 산뜻한 분위기를 내고자 하였고, 장애물에는 벌과 2벌집, 아이템에는 안전망(벌망)을 차용하였다. 세부적인 게임 규칙은 다음과 같다.
1. 상하좌우 키를 활용해 벌을 피하면서 무궁화 꽃을 수집한다.
2. 모든 무궁화 꽃을 수집하면 다음 라운드로 넘어간다.
3. 라운드가 진행될 때 마다 무궁화 꽃과 벌의 수는 많아진다.
* 이때, 날아다니는 벌에 세 번 부딪히면 게임은 종료된다.
* 벌집을 건드리면 더 많은 벌이 생성된다.
* 무궁화 꽃을 5개 수집할 때 마다 안전망 아이템이 생겨나며, Tab키를 활용해 이를 사용하면 벌에 부딪혀도 다치지 않는다.
결과적으로, Snake Game과 무궁 게임을 비교하여 정리하면 다음과 같다.
Snake Game | 무궁 게임 3 | |
플레이어 | 뱀 | 소녀 |
장애물 | 벌, 벌집 | |
점수 제도 | 사과 | 무궁화 꽃 |
아이템 | X | 안전망 |
생명 | 1번 | 3번 |
라운드 | X | O |
2. 개발 과정
1 ) 객체 분류
이번 게임은 C++을 활용하여 개발하는 만큼 "객체 지향적 프로그래밍"에 집중하였다. 따라서 위의 게임 규칙을 바탕으로 player, bee, flower, honeycomb, game 총 5가지 객체를 구성하였다. 각 객체의 역할과 관계를 간단히 정리해보면 다음과 같다(자세한 내용은 소스 코드와 함께 보도록 하자).
player 객체 | - 플레이어 역할을 하는 소녀를 생성한다. - 사용자의 Keyboard 입력을 통해 상하좌우로 움직인다. |
bee 객체 | - 장애물 역할을 하는 벌을 생성한다. - player 객체와 충돌하면 생명을 하나 잃는다. |
honeycomb 객체 | - 장애물 역할을 하는 벌집을 생성한다. - player 객체와 충돌하면 bee 객체가 하나 더 생성된다. |
flower 객체 |
- 점수 획득을 위한 무궁화 꽃을 생성한다. - player 객체와 충돌하면 점수를 얻는다. |
game 객체 | - 게임의 전반적인 구동을 담당한다. |
2 ) Asset
본 작성자는 뼛속까지 공대생인지라 미적 감각이 0에 수렴하여 일단 인터넷 서치부터 시작했다... 그렇게 잔디 배경, 무궁화 꽃, 벌, 벌집 사진 그리고 각종 음원들은 저작권 무료 사이트에서 구할 수 있었다. 4
하지만... 플레이어와 아이템(안전망)은 게임 컨셉과 어우러지는 사진을 찾지 못해 베이스만 잡아두고 직접 손(=발) 그림을 그렸다.
3 ) 소스 코드 작성
위 2-1)에서 분류한 객체 기준에 따라 '*.cpp' 파일과 '*.h' 파일을 생성하여 각 객체를 선언하고 정의하였다. 본 포스트에서 모든 내용을 다루기에는 어려움이 있으므로 '*.h' 파일만 살펴보면서 프로그램 동작 방식이나 객체를 어떻게 선언하였는지에 대해서만 알아보자. 5
(1) player 객체 (player.cpp , player.h)
첫 번째로 player 객체를 정의한 'player.h' 파일을 살펴보자.
우선 본 작성자가 그린 소녀의 얼굴이 원형이기 때문에 CircleShape 클래스를 상속하도록 하였다. 멤버 변수로는 소녀의 위치를 기록하는 playerX, playerY를 선언하였고, private으로 보호하였다. 이때, 다른 객체(bee, honeycomb, flower)와의 충돌 검사를 하기 위해서는 소녀의 위치 정보가 필요하므로 playerX, playerY 멤버 변수를 반환하는 getPlayerX(), getPlayerY() 멤버 함수를 public으로 따로 선언하였다.
추가적으로 키보드의 입력값을 받아 소녀의 위치를 계속해서 업데이트 해주는 update() 함수를 선언하였다. 특히 벌과 충돌했을때는 좀 더 사실적으로 벌에 물린 얼굴로 바꾸기 위해서 isBite() 함수를 선언해 벌과 부딪혔을 때 마다 Texture(얼굴 사진)를 바꾸도록 하였다. setPositionCenter() 멤버함수는 새 라운드가 시작되거나, 벌에 물렸을 때 화면 중앙으로 소녀를 위치시키기 위해 선언하였다.
/*
* Copyright (C) 2020 by Hannah Noh
*
* 본 헤더파일은 player 객체를 선언한다.
*
* Last Updated On 2020.05.29. FRI
* @author : Hannah Noh
*/
#ifndef GAME_PLAYER__H__
#define GAME_PLAYER__H__
#include <SFML/Graphics.hpp>
using namespace sf;
class Player : public CircleShape {
private:
float playerX, playerY;
public:
Player();
Player(Texture* tPlayer, int& WINDOW_WIDTH, int& WINDOW_HEIGHT);
void update(float x, float y);
float getPlayerX();
float getPlayerY();
void setPositionCenter(int& WINDOW_WIDTH, int& WINDOW_HEIGHT);
void isBite(Texture* tPlayer, int& numofBite);
};
#endif // !GAME_PLAYER__H__
(2) bee 객체 (bee.cpp , beer.h)
두 번째로 bee 객체를 정의한 'bee.h' 파일을 살펴보자.
player 객체와 마찬가지로 CircleShpae 클래스를 상속하여 원형으로 선언하였다. 멤버 변수로는 BEE_SPEED를 상수로 설정하여 5.0의 속도로 벌이 움직이도록 작성하였고, 이는 private으로 선언하여 타 클래스로부터 보호하였다.
public으로는 생성자를 선언하고, 벌의 위치를 지속적으로 업데이트 해주는 update() 멤버 함수를 선언하였다. 이때, 매개변수에 윈도우 창 크기가 들어간 이유는 벌을 윈도우 안에서만 움직이게 하기 위해 Boundary를 설정하였기 때문이다. 추가적으로 isIntersecting()과 handleCollision()을 통해 bee객체와 player 객체의 충돌 여부를 판단하였다.
/*
* Copyright (C) 2020 by Hannah Noh
*
* 본 헤더파일은 bee 객체를 선언한다.
*
* Last Updated On 2020.05.29. FRI
* @author : Hannah Noh
*/
#ifndef GAME_BEE__H__
#define GAME_BEE__H__
#include <SFML/Graphics.hpp>
#include "player.h"
using namespace sf;
class Bee : public CircleShape {
private:
const float BEE_SPEED = 5.0;
float m_speedX, m_speedY;
public:
Bee(Texture* tBee);
void update(int& WINDOW_WIDTH, int& WINDOW_HEIGHT);
bool isIntersecting(Player& player);
void handleCollision(Player& player, bool& g_isBite);
};
#endif // !GAME_BEE__H__
(3) honeycomb 객체 (honeycomb.cpp, honeycomb.h)
세 번째로 honeycomb 객체를 정의한 'honeycomb.h' 파일을 살펴보자.
이 또한 CircleShape 클래스를 상속하여 원형으로 선언하였다. 멤버 변수로는 isFirstCollision을 boolean 형으로 선언하여 해당 라운드에서 honeycomb-player간의 충돌이 이미 발생했는지 여부를 확인하였다. 이 변수가 도입된 이유는, 충돌했을 때 honeycomb 객체를 파괴하고 다음 라운드에서 다시 생성해도 되지만, 살짝 트릭을 써서 객체 자체를 지우지 않고 화면에서만 보이지 않도록 프로그램을 작성했기 때문이다. isFirstCollsion는 private으로 작성되었기 때문에 이 값을 변경하거나 불러오는 setIsFirstCollision(), getIsFirstCollision() 멤버 함수를 public으로 선언해주었다.
추가적으로 public에는 생성자와 honeycomb-player간의 충돌을 검사하는 isIntersecting() 멤버함수, 그리고 매 라운드마다 랜덤하게 honeycomb의 위치를 설정하기 위해 setNewPosition() 멤버함수를 선언하였다.
/*
* Copyright (C) 2020 by Hannah Noh
*
* 본 헤더파일은 honeycomb 객체를 선언한다.
*
* Last Updated On 2020.05.29. FRI
* @author : Hannah Noh
*/
#ifndef GAME_HONEYCOMB__H__
#define GAME_HONEYCOMB__H__
#include <SFML/Graphics.hpp>
#include "player.h"
using namespace sf;
class Honeycomb : public CircleShape {
private:
bool m_isFirstCollision;
public:
Honeycomb();
Honeycomb(Texture* tHoneycomb, float x, float y);
bool isIntersecting(Player& player);
void setNewPosition(float x, float y);
void setIsFirstCollision(bool tf);
bool getIsFirstCollision();
};
#endif // !GAME_HONEYCOMB__H__
(4) flower 객체 (flower.cpp, flower.h)
네 번째로 flower 객체를 정의한 'flower.h' 파일을 살펴보자.
flower 객체도 앞의 세 가지 객체와 매우 유사하다. CircleShpae 클래스를 상속하여 원형으로 선언하였으며, 멤버 변수로는 m_deleted를 private으로 선언하여 flower-player간의 충돌 여부를 기록하였다. 게임 루프에서 이 값을 바탕으로 충돌 여부를 판단하고 충돌한 flower 객체는 파괴했다. 하지만 이는 private으로 선언되었기 때문에 외부에서 접근할 수 있도록 getDeleted() 멤버 함수를 선언하였다.
더불어 public으로 생성자와 flower-player간의 충돌을 검사하는 isIntersecting(), handleCollision() 멤버 함수를 선언하였다.
#ifndef GAME_FLOWER__H__
#define GAME_FLOWER__H__
#include <SFML/Graphics.hpp>
#include "player.h"
using namespace sf;
class Flower : public CircleShape {
private:
bool m_deleted;
public:
Flower(Texture* tFlower, float x, float y);
bool isIntersecting(Player& player);
bool handleCollision(Player& player, int& g_score);
bool getDeleted();
};
#endif // !GAME_FLOWER__H__
(5) game 객체 (game.cpp, game.h)
다섯 번째로 game 객체를 정의한 'game.h' 파일을 살펴보자.
이는 게임의 전반적인 구동을 담당하는 객체이므로 게임에서 사용되는 모든 객체를 private으로 선언하였다.
public으로는 게임 시작 함수 startGame()과 키보드 이벤트에 따라 실행하는 함수를 결정하는 startPage(), 게임 설명 화면을 띄우는 showRule()을 선언하였다. 또한, 본격적인 게임을 담당하는 runGame() 함수를 선언하였으며, 이와 관련된 setText() 6, roundUpdate() 7, handleKeyboardEvents() 8, collisionEvent() 9, drawGame() 10를 선언하였다. 마지막으로 Game Over를 알리는 endPage() 멤버 함수를 선언하였다(*각 함수의 세부 역할은 각주 11-글씨 옆 숫자-를 눌러보세요!).
/*
* Copyright (C) 2020 by Hannah Noh
*
* 본 헤더파일은 game 객체를 선언한다.
*
* Last Updated On 2020.05.29. FRI
* @author : Hannah Noh
*/
#ifndef GAME_H_
#define GAME_H_
#include <SFML/Graphics.hpp>
#include <SFML/Audio.hpp>
#include <Windows.h>
#include <ctime>
#include <cstdlib>
#include <iostream>
#include <string>
#include "honeycomb.h"
#include "player.h"
#include "flower.h"
#include "bee.h"
using namespace std;
using namespace sf;
class Game {
private:
/* 맴버 변수 */
int WINDOW_WIDTH, WINDOW_HEIGHT;
int g_score, g_round, g_numofBite, g_numofItem;
bool g_isBite, g_isItemplus, g_isUsedItem, g_clockRestart, g_isStartPageOpen;
// flower
float flowerX, flowerY;
vector<Flower> flowers;
Texture tFlower;
SoundBuffer bufferFlower;
Sound soundFlower;
// bee
vector<Bee> bees;
Texture tBee;
SoundBuffer bufferBee;
Sound soundBee;
// player
Player player;
Texture tPlayer;
// honeycomb
float honeycombX, honeycombY;
Honeycomb honeycomb;
Texture tHoneycomb;
SoundBuffer bufferHoneycomb;
Sound soundHoneycomb;
// item
CircleShape item;
Texture tItem;
SoundBuffer bufferItem;
Sound soundItem;
// text
string strText;
Font font;
Text text;
// background image
Texture tBackground;
Sprite sBackground;
// event
Event event;
// window
RenderWindow window;
// clock
Clock clock;
// start page
Texture tStartpage;
Sprite sStartpage;
// bgm
Music bgm;
public:
Game(int width, int height);
/* 맴버 함수 */
// 첫 페이지
void startGame();
void startPage();
void showRule();
// Game Loop
void runGame();
void setText(int textSize, float positionX, float positionY);
void roundUpdate();
void handleKeyboardEvents();
bool collisionEvent();
void drawGame();
// Game Over 페이지
void endPage();
};
#endif
(6) main.cpp
마지막으로 user가 접근하는 공간인 메인 함수를 살펴보자.
game 객체를 별도로 선언함으로써 실제 구현 내용을 외부로부터 감추었기 때문에 main 함수는 매우 간단하게 작성되었다.
/*
* Copyright (C) 2020 by Hannah Noh
*
* main.cpp
*
* Last Updated On 2020.05.29. FRI
* @author : Hannah Noh
*/
#include "game/game.h"
int main() {
int WINDOW_WIDTH = 1200;
int WINDOW_HEIGHT = 800;
Game game(WINDOW_WIDTH, WINDOW_HEIGHT);
game.startGame();
return 0;
}
즉, 각 파일간의 관계를 정리해보면 다음과 같다.
3. 결과
게임을 실행하게 되면 화면에 첫 페이지가 출력된다. 이때 '1'키를 누르면 바로 게임이 시작되며, '2'키를 누르면 게임 방법 화면을 출력하게 된다. 게임 방법 화면으로 넘어갔을 때는 'Enter'키를 눌러 게임을 시작할 수 있다. 게임을 하던 중 세 번 벌에 부딪히게 되면 'Game Over!' 페이지를 띄우면서 게임은 종료된다.
실제 시연 영상은 다음과 같다.
4. 결론
본 포스트에서는 SFML을 활용하여 나만의 게임을 제작하는 과정을 보여주었다. 이번에 개발한 '무궁' 게임은 Snake Game의 장점을 본받고 단점을 보완하여 "Snake Game과 비슷하지만 완전히 다른 차별성이 있는 게임"을 목표로 제작하였다.
우선 기존 Snake Game과의 가장 큰 차이점은, 1) 뱀의 역할을 '소녀'와 '벌'에게 분배하였으며, 2) 라운드 제도를 도입입하였다는 점이다. 3) 또한, 세 번의 회생 기회와 아이템을 제공함으로써 게임의 난이도를 조절하고, 사용자를 게임 속에 더 오래 머무를 수 있도록 하였다.
공통점으로는 라운드가 진행될수록 벌의 개수를 증가시킴으로써 1) 소녀가 움직일 수 있는 공간에 제약을 두었다는 점이다. 2) 또한, 벌의 위치에 따라 소녀의 이동 방향을 미리 계산해야 한다는 점도 있다 12. 13
위의 시연 영상을 살펴보면 벌이 생성되는 위치가 왼쪽 상단으로 고정되어있음을 발견할 수 있는데, 그 이유는 벌이 렌덤한 위치에서 발생할 경우 사용자가 짧은 시간 내에 모든 벌의 움직임을 파악할 수 없기 때문이다. 만약 벌이 렌덤한 방향에서 생성되어 움직인다면, 사용자는 수많은 벌의 움직임을 제각각 파악해야 하므로 게임의 난이도가 급격하게 어려워진다. 따라서 벌의 시작 위치를 고정함으로써 움직이는 패턴에 규칙성을 두어 게임의 난이도를 조절하였다. (Snake Game에서도 보면, 뱀 머리의 방향에 따라서 나머지 몸통의 방향도 미리 추측할 수 있어 사용자가 짧은 시간 내에 뱀의 움직임을 파악할 수 있다.)
여기서 '게임을 할 때마다 벌이 똑같이 움직이면 게임이 너무 단순하고 쉬운 것이 아닌가?'라는 의문이 들 수도 있는데, 라운드마다 무궁화 꽃과 벌집이 생성되는 위치가 달라지고 사용자마다 라운드를 깨는 시간이 제각각이기 때문에 수학적으로 매 라운드마다 벌이 완전히 똑같이 움직일 확률은 매우 적다.
따라서 결과적으로 Snake Game의 게임 아이디어를 따르면서 완전히 다른 새로운 규칙과 컨셉을 적용시킴으로써 나만의 무궁 게임을 제작하였다.
Asset 저작권 출처
[1] 잔디밭 : Charles Hasse, <br.pinterest.com/pin/530791506060179284/>
[2] 무궁화 : Littel Deep - 무궁화 일러스트, <littledeep.com/hibiscus-illustration-free-download/>
[3] 꿀벌 : illustAC - 캐릭터 꿀벌, <ac-illust.com/ko/clip-art/434798/%EC%BA%90%EB%A6%AD%ED%84%B0-%EA%BF%80%EB%B2%8C>
[4] 소녀 : 588ku(pngtree.com) - 큰 눈 소녀 창조적 인 패턴 계속 웃고 평평한 얼굴, <kor.pngtree.com/freepng/green-avatar-female-illustration_4657030.html>
[5] 이외 : 저작권 미표시 허용(CC0)
참고 문헌
[1] 천인국(저). (2019). 어서와 C++는 처음이지!. 인피니티북스
각주
- 밑에 gif 파일은 이상적인 시나리오라 한다. [본문으로]
- 꽃하면 벌이니까... [본문으로]
- 한국어를 사랑하자 [본문으로]
- 저작권을 지킵시다! (FROM. 2019 한국저작권위원회 저작권서포터즈) [본문으로]
- 코드가 매우 길다... [본문으로]
- 1을 누르면 즉시 게임 시작, 2를 누르면 게임 방법 설명 페이지 출력 [본문으로]
- 화면에 나타나는 텍스트의 위치와 크기, 내용을 정의한다. [본문으로]
- 라운드를 업데이트 한다. 벌과 무궁화 꽃의 개수를 늘리고, honeycomb과 소녀의 위치는 새롭게 초기화 한다. [본문으로]
- 사용자의 키보드 이벤트를 받아 소녀를 움직인다. [본문으로]
- 객체들간의 충돌 여부를 검사한다. [본문으로]
- 모든 객체들을 화면에 그린다. [본문으로]
- Snake Game에서는 뱀이 자신의 몸통에 의해 공간에 제약을 받는다. [본문으로]
- Snake Game에서는 뱀의 몸 길이를 생각하여 방향을 결정해야한다. [본문으로]
'Toy Project > Programming' 카테고리의 다른 글
[Python, Processing] Data visualization - 조망권 (0) | 2021.05.23 |
---|---|
[HTML+CSS+JS] TENET 홍보 홈페이지 제작 (0) | 2020.07.27 |
[Processing] Data Visualization - Kpop & Fashion (0) | 2020.05.08 |
[Processing] Media Art (0) | 2020.05.08 |
[Processing] Mosaic (0) | 2020.05.08 |