Game development in sge/C++: Part I (window creation)

Introduction

Raptor: Call of the shadows

Raptor: Call of the shadows

This is part 1 of the series I called “Game development in sge/C++”. There are two reasons for this tutorial:

  1. There’s no real documentation for the game engine sge and no tutorial which shows how to use sge’s components in combination.
  2. Most game developers using C++ have no idea of it and don’t use any “modern” programming patterns, just C with classes – and maybe a templated vector math class.

So I’m going to address both these issues in writing a game which uses sge and C++, explaining the language concepts as well as the game and engine concepts. But what kind of game? 2D/3D? Something extremely simple or something to build upon? After some thinking I’ve decided to design a 2D top-down shooter which should behave somewhat like the popular Linux game chromium B.S.U. or the classic Raptor: Call of the shadows. You fly a jet fighter close to a planet’s surface and shoot at other fighters and buildings on the ground (see the screenshot).

The game’s development process should cover 2D sprites for the objects, point sprites for the particle effects, sounds, multiple game states and maybe some texture hackery to get the background to pan (I’m not sure about that, yet). I’m probably going to set up a github repository for the project so you can browse the source there, but large portions (if not all of the code) will be posted here on the blog. Also, I’m not going to describe how to set up the project (downloading/installing sge and its dependencies, writing a makefile etc.), just the code. The articles make heavy use of fcppt, but I will explain everything that’s used from it in detail, so you don’t need to read up on it.

Framework

This first article might be a bit dry – not a lot of code – but I have to explain some basics first, so bear with me.

But enough of the introductory crap, let’s cut to the chase. Where to begin? Well, we have to…

  1. create a window to draw onto
  2. load the renderer
  3. set up a main loop which runs until the user either presses escape or closes the program using some window button

sge supports multiple platforms (Windows and Linux, currently), so everything platform-dependant has to be abstracted. If you want to write a DirectX renderer backend, for example, you have to implement the interface sge::renderer::device which contains functions like create_texture or create_vertex_buffer. When you’re finished writing the DirectX renderer implementation, you have to compile it to a dynamic link library (a dll in windows, an .so in Linux). Then the user can load your plugin, instantiate it and pass to it a window (which was created using another plugin, responsible for window creation).

But loading the dlls by hand would be really tedious. So sge provides an initialization class called sge::systems::instance which takes care of loading the dlls and resolving the plugin dependencies (stuff like: pass the window to the renderer). The head of our main file (call it main.cpp for now) looks like this:

#include <sge/systems/systems.hpp>
#include <sge/viewport/viewport.hpp>
#include <sge/renderer/renderer.hpp>
#include <sge/input/keyboard/keyboard.hpp>
#include <sge/window/window.hpp>
#include <fcppt/math/dim/dim.hpp>
#include <fcppt/container/bitfield/bitfield.hpp>
#include <fcppt/text.hpp>
#include <fcppt/exception.hpp>
#include <fcppt/io/cerr.hpp>
#include <iostream>
#include <ostream>
#include <exception>
#include <cstdlib>

int main()
try
{
	sge::systems::instance sys(
	sge::systems::list()
		(sge::systems::window(
			sge::window::simple_parameters(
				FCPPT_TEXT("the_game"),
			sge::window::dim(
				1024,768))))
		(sge::systems::renderer(
			sge::renderer::parameters(
				sge::renderer::visual_depth::depth32,
				sge::renderer::depth_stencil_buffer::off,
				sge::renderer::vsync::on,
				sge::renderer::no_multi_sampling),
			sge::viewport::center_on_resize(
				sge::window::dim(1024,768))))
		(sge::systems::input(
			sge::systems::input_helper::keyboard_collector,
			sge::systems::cursor_option_field::null())));
	return EXIT_SUCCESS;
}
catch (fcppt::exception const &e)
{
	fcppt::io::cerr << FCPPT_TEXT("Exception caught: ") << e.string() << FCPPT_TEXT("\n");
	return EXIT_FAILURE;
}
catch (std::exception const &e)
{
	std::cerr << "Exception caught: " << e.what() << "\n";
	return EXIT_FAILURE;
}

You might already have some “wtf?!” moments when you read this code. Let’s first examine the structure and then the systems statement.

Structure

Notation

Then there’s this strange notation:

int main()
try
{
}
catch (...)
{
}
...

But that’s easily explained, it’s just a shorthand for

int main() 
{ 
	try 
	{ 
		// ... 
	} 
	catch 
	{ 
		// ... 
	} 
}

so you save a level of indentation.

namespaces

You might also have noticed that there are a lot of ‘::’ in the code. That’s because sge relies heavily on namespaces. For every subdirectory in the sge/include directory, a new namespace is created, resulting in long names like sge::input::keyboard::key_code, as seen below. At first sight, this might look “abnormal” and tedious to write, but the fact of the matter is: you cannot have too many (nested) namespaces!

Most C++ libraries do not respect that and squeeze everything into one base namespace – even boost does it! There’s boost::source and boost::target which make no sense on their own – until you realize that they actually belong to the boost graph library and receive a graph edge as the parameter. But other boost sub-libraries might like to define a “source” and a “target” function, too. Alas, without breaking old code, you cannot correct that mistake by moving source and target to boost::graph. The other way, lifting the functions to the base namespace, would be possible.

That’s because C++ has lots of syntax to make working with namespaces easier. There’s using, using namespace, typedef, namespaces can be renamed and so on. So if you feel tired of repeating some long namespace prefix, consider a local using namespace, for example.

fcppt strings

Everything you see in the code above which comes from fcppt somehow relates to fcppt::string. Let me explain why. In C++, there are two character types: char and wchar_t. char is always 1 byte long, the size of wchar_t is implementation defined. In Windows it’s 2, in Linux it’s often 4 (which corresponds to the encodings used: UTF-16 and UTF-32). Consequently, there are two types of character literals: narrow and wide literals, as such:

char const [] chars = "foobar";
wchar_t const [] wchars = L"foobar";

System functions like CreateFile in Windows accept wchar_t. In Linux, calls like open accept char. The two systems use a different string type as the default. So the idea of fcppt::string is simple: Define its base character type as char or wchar_t depending on the operating system. But if you do that, you have to be consistent: You have to define a macro FCPPT_TEXT which wraps its argument in either "" or L"". You also have to define functions to convert from std::string and std::wstring to fcppt::string and so on. That’s why we have fcppt::io::cerr and fcppt::exception which operate on fcppt::char_type.

systems

So next up: The statement which uses sge::systems::instance. Almost all the time, you want to initialize more than one sge subsystem, so sge::systems::instance receives a sge::systems::list of plugins. The list has an overloaded operator(), in case you’re wondering about the notation.

The first item in the list is the window, which gets the window title as parameter, as well as the window’s desired size (more on that in a later article). Then there’s the renderer which gets a lot more parameters, but most of them should be self-explanatory. You pass it bit depth, and some other rather uninteresting parameters.

The last part is the input system. This is a bit non-straightforward. For now, let’s assume we don’t want to concern ourselves with mouse input. In this case, we only need to acquire a “keyboard collector” object, which we’ll explain shortly. We can ignore the rest of the input initialization for now.

Enter “main loop”

So that’s it. If you run the program, it should do absolutely nothing except display a window for a very short period of time. Not very exciting. We need at least a game loop which runs forever or until the user cancels it. We add the following code:

// <add other includes here>
#include <fcppt/signal/scoped_connection.hpp>

namespace
{
bool running;

void
exit_program(
	sge::input::keyboard::key_event const &e)
{
	if (e.pressed() && e.key_code() == sge::input::keyboard::key_code::escape)
		running = false;
}
}

int main()
try
{
	// <add systems code here>
	
	running = true;

	fcppt::signal::scoped_connection const cb(
		sys.keyboard_collector().key_callback(
			&exit_program));

	while(running)
	{
		sys.window().dispatch();
	}
}
// <add exception handling here>

So we’ve got a boolean called “running” which obviously decides if the program has come to an end. We want this boolean to be false when the user has pressed the “escape” button on the keyboard. So we ask the input system to call the function exit_program whenever there was a key event, meaning a key up or key down (as you can see, key_event contains the function pressed to decide the key’s state). Inside this callback, we test if the key was pressed and if the key’s code was “escape”. Simple enough. But why is there no input “system”, only a “keyboard collector”?

The reason that it’s a “collector” and not just a “keyboard” is that you might have more than one keyboard attached to your computer. And you might even take advantage of that – think of a game with a split-screen mode where two players can compete using two keyboards on the same computer. Most times, though, you don’t want to treat the keyboards separately, so sge::input offers the collector which – well – collects all events from all keyboards and forwards them to the user, ignoring where they came from.

A quick word on the fcppt::signal::scoped_connection type: We’ll cover signals and binding when we get the jet fighter to move via the keyboard, so let’s just ignore that topic completely, at least for now.

Finally, the main loop consists of a lonely dispatch call which collects all window events – like a mouse move, a keyboard press etc. – from the main window and calls the registered callback functions (like our exit_program).

So folks, that’s it for now! I hope it wasn’t too boring. Next time we’ll be adding a jet fighter to our application using the sge::sprite subsystem.

Advertisements
This entry was posted in Uncategorized and tagged , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s