Coding Feature.

SFML 게임 제작 공부 #4. Window 클래스. 본문

Game Development/SFML 게임 제작 공부

SFML 게임 제작 공부 #4. Window 클래스.

codingfeature 2023. 9. 5. 21:34

Huge Reference

 

SFML Game Development By Example | Packt

Create and develop exciting games from start to finish using SFML

www.packtpub.com

 

게임 창을 관리하는 Window 클래스를 만들기 이전 SFML를 이용해 창을 띄우는 소스 코드를 먼저 작성했다.

나중에 Game 클래스까지 작성하고 난 뒤의 소스 코드와 비교해보면 좋을 것 같다.

 

// Window 클래스 작성 이전 코드.
#include <SFML/Graphics.hpp>

int main() {
	sf::RenderWindow window(sf::VideoMode(800, 600), "Window"); // 윈도우 초기화.

	while (window.isOpen()) { // 창이 열려 있을 동안..
		sf::Event event;
		while (window.pollEvent(event)) {			// 큐에 저장된 이벤트를 하나씩 꺼냄.
			if (event.type == sf::Event::Closed)	// 이벤트가 창 종료일때
				window.close();						// 창 끄기.
		}
		window.clear(sf::Color::Black); // 화면을 특정 색으로 모두 채워 초기화.

		//window.draw(something)		// 특정 오브젝트를 그리기 위해 버퍼로 옮김.

		window.display();				// Draw 된 모든 오브젝트를 화면에 표시.
	}
	return 0;
}

위 코드를 보면 while loop를 통해 창이 열려 있는 동안

1. Event 관리

2. 화면에 그림.

를 반복 수행하게 된다.

 

여기서 몇 가지 중요한 포인트를 보자면

먼저 프로그램 수행하는 동안 발생하는 이벤트를 큐로 관리한다는 점과 Closed라는 이벤트가 발생했을 때 창을 종료한다는 점이다. 만약 if문을 통해 이벤트가 Closed일때의 처리문을 작성하지 않는다면 사용자는 창 종료 버튼을 눌러도 끌 수가 없다.

또한 프로그램으로 그린 오브젝트를 화면에 표시하는 방법을 살펴본다면

Clear -> Draw -> Display -> Clear -> .. 과정을 반복하게 된다.

Clear는 화면을 어떤 특정 색으로 초기화함으로써 이전 loop에서 그려진 오브젝트를 전부 지워버린다. 만약 clear를 하지 않는다면 이전에 그려졌던 모든 오브젝트들은 계속 화면에 잔상처럼 남아 있을 것이다.

그리고 Draw는 어떤 오브젝트를 화면에 그리는 게 아니라 숨겨진 버퍼(hidden buffer)에 그리는 것이다.

Display를 실행할 때에 비로소 버퍼에 저장된 내용을 화면에 표시하게 되는 것이다. 이 과정을 Double-Buffering이라고 한다.

 

아래는 SFML 공식 튜토리얼 사이트에서 발췌한 내용의 일부이다.

This clear/draw/display cycle is the only good way to draw things. Don't try other strategies, such as keeping pixels from the previous frame, "erasing" pixels, or drawing once and calling display multiple times. You'll get strange results due to double-buffering.
Modern graphics hardware and APIs are really made for repeated clear/draw/display cycles where everything is completely refreshed at each iteration of the main loop. Don't be scared to draw 1000 sprites 60 times per second, you're far below the millions of triangles that your computer can handle.
(clear/draw/display 사이클은 무언가를 그릴 때 사용하는 유일하게 좋은 방법입니다. 다른 방식을 사용하지 마세요. 현재 사용하는 Double-Buffering 방식 때문에 이상하게 결과가 나올 것입니다. 최신 그래픽 기기들, 또는 API 들은 위와 같은 사이클을 사용하는 것을 기준으로 합니다. 1000개의 스프라이트들을 초당 60번씩 그리는 것을 두려워하지 마세요. 여러분의 컴퓨터는 그것보다 훨씬 더 많은 것을 감당할 수 있습니다.)

이제 Window 클래스를 만들어서  SFML/C++의 객체 지향적 특성을 최대한으로 활용해보자.

 

먼저 헤더 파일을 작성해보았다.

//Window.hpp
#pragma once
#include <SFML/Graphics.hpp>

class Window {
	sf::RenderWindow _window;
	sf::Vector2u _windowSize;
	std::string _windowTitle;

	bool _isDone;
	bool _isFullScreen;

	void Setup(const std::string& l_title, const sf::Vector2u& l_size);
	void Destroy();
	void Create();

public:
	Window();
	Window(const std::string& l_title, const sf::Vector2u& l_size);
	~Window();

	void BeginDraw();
	void EndDraw();

	void Update();

	bool IsDone();
	bool IsFullScreen();
	sf::Vector2u GetWindowSize();

	void ToggleFullScreen();

	void Draw(sf::Drawable& l_drawable);
};

위 클래스는 말 그대로 Window, 창에 대한 관리를 위한 클래스이며 창 크기, 이름, 전체화면 토글, Clear/Draw/Display 사이클 등을 관리할 수 있게 된다.

 

그리고 클래스 메서드에 대해서 각각 코드를 완성해보았다.

Window::Window() { Setup("Window", sf::Vector2u(640, 480)); }

Window::Window(const std::string& l_title, const sf::Vector2u& l_size){
	Setup(l_title, l_size);
}

Window::~Window() { Destroy(); }

위 세 개의 메서드는 Window Class의 생성자, 소멸자이다.

 

생성자는 Setup이라는 나중에 구현할 또 다른 메서드를 호출하여 윈도우를 생성하며 창의 이름, 크기에 대한 인자를 전달하지 않는 경우, "Window" 라는 이름의, 크기가 (640, 480)인 창을 기본으로 생성하도록 한다. 그리고 인자를 전달한 경우에도 전달한 정보에 맞게 창을 Setup하게 될 것이다.

 

소멸자는 Destroy 라는 또 다른 메서드를 호출함으로써 창을 종료할 것이다.

 

void Window::Setup(const std::string& l_title, const sf::Vector2u& l_size) {
	_windowTitle = l_title;
	_windowSize = l_size;
	_isFullScreen = false;
	_isDone = false;
	Create();
}

void Window::Destroy() {
	_window.close();
}

그 다음에는 생성자, 소멸자에서 사용하는 Setup과 Destroy 메서드를 작성했다.

Setup은 클래스의 private 변수에 각각 창 이름, 사이즈를 입력하고 전체화면인지를 나타내는 _isFullScreen을 false, 창이 종료되었는지를 나타내는 _isDone 을 false로 설정한다. 이후 또 다른 메서드인 Create를 호출함으로써 창을 생성한다.

 

Destroy 메서드는 close()라는 윈도우의 메서드를 호출함으로써 창을 닫는다.

 

void Window::Create() {
	auto style = (_isFullScreen ? sf::Style::Fullscreen : sf::Style::Default);
	_window.create({ _windowSize.x, _windowSize.y, 32 }, _windowTitle, style);
}

그 다음에는 이전 Setup 메서드에서 사용되는 Create 메서드를 작성했다.

Create는 이전 메서드에서 설정된 _isFullScreen 의 값에 따라서 스타일을 FullScreen으로 할지 아니면 Default로 할지 정하게 된다. 창에 대한 스타일 속성은 위 두 가지 외에도 다른 것들이 있다.

출처 -&nbsp;SFML Game Development By Example By Raimondas Pupius

보통은 Default 또는 Fullscreen을 사용한다. 

뒤에 Mutually Exclusive는 서로 혼용이 가능한지의 여부에 대한 것이며, 만약 둘 다 No인 경우 혼용이 가능하다.

그 다음 window 개체의 create() 함수를 사용해서 private 변수에 담겨진 화면의 크기, 이름을 받아 화면을 생성하게 된다. 

 

void Window::Update() {
	sf::Event event;
	while (_window.pollEvent(event)) {
		if (event.type == sf::Event::Closed) {
			_isDone = true;
		}
		else if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::F5) {
			ToggleFullScreen();
		}
	}
}

void Window::ToggleFullScreen() {
	_isFullScreen = !_isFullScreen;
	Destroy();
	Create();
}

Update 메서드에서는 윈도우에 대한 이벤트 처리를 관리한다. 현재는 창 끄기 또는 전체 화면 토글에 대한 이벤트를 처리하게 된다. KeyCode가 F5, 즉 키보드 상의 F5 버튼을 누를 때 ToggleFullScreen 메서드를 호출함으로써 전체화면을 토글하게 된다.

 

ToggleFullScreen 메서드에서는 _isFullScreen을 flip하고 Destroy와 Create 메서드를 차례대로 호출함으로써 토글 기능을 수행하도록 되어있다.

 

void Window::BeginDraw() { _window.clear(sf::Color::Black); }

void Window::Draw(sf::Drawable& l_drawable) {
	_window.draw(l_drawable);
}

void Window::EndDraw() { _window.display(); }

앞서 보았던 clear->draw->display 사이클을 각각 BeginDraw, Draw, EndDraw 메서드로 처리한다. 좀 더 직관적인 메서드 명으로 Wrapping 함으로써 위 사이클을 헷갈리지 않게 처리할 수 있을 것이다.

윈도우의 draw() 함수에서 처리할 수 있는 Drawable의 구조는 다음과 같다.

위 구조에서 보이듯, Shape, Sprite, Text 등의 오브젝트에 대해서 처리를 하게 된다.

 

bool Window::IsDone() { return _isDone; }

bool Window::IsFullScreen() { return _isFullScreen; }

sf::Vector2u Window::GetWindowSize() { return _windowSize; }

나머지는 클래스의 private 변수에 대해 접근할 수 있게 해주는 메서드를 작성했다.

 

Window.cpp의 전체 소스 코드는 다음과 같다.

// Window.cpp
#include "Window.hpp"

Window::Window() { Setup("Window", sf::Vector2u(640, 480)); }

Window::Window(const std::string& l_title, const sf::Vector2u& l_size){
	Setup(l_title, l_size);
}

Window::~Window() { Destroy(); }

void Window::Setup(const std::string& l_title, const sf::Vector2u& l_size) {
	_windowTitle = l_title;
	_windowSize = l_size;
	_isFullScreen = false;
	_isDone = false;
	Create();
}

void Window::Destroy() {
	_window.close();
}

void Window::Create() {
	auto style = (_isFullScreen ? sf::Style::Fullscreen : sf::Style::Default);
	_window.create({ _windowSize.x, _windowSize.y, 32 }, _windowTitle, style);
}

void Window::Update() {
	sf::Event event;
	while (_window.pollEvent(event)) {
		if (event.type == sf::Event::Closed) {
			_isDone = true;
		}
		else if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::F5) {
			ToggleFullScreen();
		}
	}
}

void Window::ToggleFullScreen() {
	_isFullScreen = !_isFullScreen;
	Destroy();
	Create();
}

void Window::BeginDraw() { _window.clear(sf::Color::Black); }

void Window::EndDraw() { _window.display(); }

bool Window::IsDone() { return _isDone; }

bool Window::IsFullScreen() { return _isFullScreen; }

sf::Vector2u Window::GetWindowSize() { return _windowSize; }

void Window::Draw(sf::Drawable& l_drawable) {
	_window.draw(l_drawable);
}

 

이로써 Window 클래스를 다 작성해보았다.

 

다음에는 Game 클래스에 대해서 공부해보겠다.

Game 클래스는 이전 Game loop에서 살펴본, Input 처리와 게임 요소 업데이트, 그리고 화면에 렌더링하는 기능을 수행하도록 할 것이다. 위에서 만든 Window 클래스 역시 Game 클래스에서 관리를 하도록 함으로써 좀 더 깔끔하고 체계적인 개발 환경을 구축하도록 할 것이다.