Skip to content

Getting started

Rafa edited this page May 16, 2018 · 18 revisions

Introduction

I started writing ZeltaLib back in 2014 to handle scenes and resources in my SFML projects more easily. I've progressively improved the library and I want to share it with the community.

In this "Getting started" I want to show you some of the features of the library. I assume that you know some of SFML and C++ and you are familiar with some game development concepts.

ZeltaLib is consists of some modules (Core, IA, Math, TileEngine). Most of them are very basic. In this guide I'll cover the Core module.

Creating a scene

Scene class

Scenes are the most basic feature of ZeltaLib. Games can be composed of several scenes, e.g.: the main menu scene, the credits scene, the actual game scene, etc.

You can define your own scenes by inheriting from zt::Scene and you'll use the zt::SceneManager to manage them.

This is the basic structure of a scene (you can see it in the examples/core/ex1-basic-scene folder):

class GameScene : public zt::Scene {
    public:
    // Note that zt::Scene constructor needs a scene name and a reference to
    // the scene manager.
    GameScene(zt::SceneManager& sceneManager) : 
	zt::Scene("GameScene", sceneManager) {
    }
    void manageEvents(float deltaTime, std::queue<sf::Event>& events) {
    	// EVENT HANDLING METHOD //
    
    	sf::Event event;
    	while (!events.empty()) { // While there are events to handle...
    	    // We get the  first event in the queue.
    	    event = events.front();
    
    	    // This is a helper function that will check if the
    	    // user has closed the window. If so, the game
    	    // will close.
    	    checkIfWindowClosed(event);
    
 	    // HERE YOU CAN HANDLE MORE EVENTS //
    	    events.pop();
        }
    }
    
    void logic(float deltaTime) {
    	// GAME LOGIC //
    }
    
    void render() {
    	// DRAW HERE //
    }
    private:
};

Scenes contains, among others, three important methods that you'll want to override:

  • manageEvents(float deltaTime, std::queue<sf::Event>& events): you should put you event handling code here.
  • logic(): you should put your logic here (e.g.: moving the enemies)
  • render(): here you draw thing into the screen.

Note: ZeltaLib does not prohibit putting logic into manageEvents() nor handling the events in the logic() method. You can do it but you shouldn't.

Note that manageEvents() and logic() receives the delta time as parameter. The delta time is the time it took the system to run the last iteration of the game loop. That number allows you to move things accordingly with the speed of the computer where you're running the game. It's measured in seconds.

manageEvents() also receives a std::queue<sf::Event> which is a copy of the event queue given by SFML. Why don't we call window.pollEvent() to get them? The problem with pollEvent() is that if you read the event queue from one scene then it will be empty when you read it from other scene (assuming that you have two scenes active at the same time). The solution to this problem is creating one event queue for each scene so that each one can read the elements without worrying about other scenes.

SceneManager class

We can have several scenes in our game. The SceneManager class will allow us to manage them. That class manages the game loop that will indirectly call our manageEvents(), logic() and render() methods.

Scenes have a state. There are three posible states: inactive, active and paused.

  • Inactive: this is the default state. When a scene is inactive its manageEvents(), logic() and draw() methods won't be called from the game loop.
  • Active: the three methods are called.
  • Paused: is halfway between inactive and active. When the scene is paused its manageEvents() and logic() methods won't be called but the render() method will. This may help you to implement an paused in-game menu: you could have two scenes, one for the game and other for the in-game menu. When the player pauses the game you just set the game scene to paused and the menu to active.

This is the default behaviour of the states. If you need a different one you can simply override the advanceTime(float) method on your scene. Look at the current implementation in Core/Scene.cpp to see how.

If you want to change the state of a scene you have to do it through the SceneManager. The most useful method is sceneManager.switchTo("sceneName") which will deactivate all active scenes and will activate "sceneName".

Note: as you can guess, the advanceTime() method in the scene is called from the game loop which is in the scene manager.

Basic game

Let's create a very basic game to show how these two classes, zt::Scene and zt::SceneManager, fit together. In our game we'll have a basket that we'll horizontally move using the left and right arrow keys. Apples will fall from the top of the window and we will have to catch them with the basket:

Let's see the GameScene class:

#include <Zelta/Core/Scene.hpp>
#include <Zelta/Core/SceneManager.hpp>
#include <SFML/Graphics.hpp>

class GameScene : public zt::Scene {
	public:
	// Note that zt::Scene constructor needs a scene name and a reference to
	// the scene manager.
	GameScene(zt::SceneManager& sceneManager) : zt::Scene("GameScene", sceneManager) {
		// Resource loading //
		appleTexture.loadFromFile("assets/apple.png");
		basketTexture.loadFromFile("assets/basket.png");

		basket.setTexture(basketTexture);
		basket.setScale(0.3, 0.3);
		basket.setPosition(0, 800 - 100/* - apple.getLocalBounds().height*/);
	}

	void manageEvents(float deltaTime, std::queue<sf::Event>& events) {
		// EVENT HANDLING METHOD //

		sf::Event event;
		while (!events.empty()) { // While there are events to handle...
			// We get the  first event in the queue.
			event = events.front();

			// This is a helper function that will check if the
			// user has closed the window. If so, the game
			// will close.
			checkIfWindowClosed(event);

			// HERE YOU CAN HANDLE MORE EVENTS //

			events.pop();
		}

		// REAL TIME EVENT HANDLING //
		if (sf::Keyboard::isKeyPressed(sf::Keyboard::Right)) {
			if (basket.getPosition().x < getScreenSize().x - basket.getGlobalBounds().width) {
				basket.move(500 * deltaTime, 0);
			}
		}

		if (sf::Keyboard::isKeyPressed(sf::Keyboard::Left)) {
			if (basket.getPosition().x > 0) {
				basket.move(-500 * deltaTime, 0);
			}
		}
	}

	void logic(float deltaTime) {
		// GAME LOGIC //

		// In the game logic we handle the generation of new
		// apples.
		if (generationClock.getElapsedTime().asSeconds() > 1.0f) {
			sf::Sprite newApple = sf::Sprite(appleTexture);
			float randX = rand() % (int)(getScreenSize().x - newApple.getGlobalBounds().width);
			newApple.setPosition(randX, -newApple.getGlobalBounds().height);
			newApple.setScale(0.2, 0.2);
			apples.push_back(newApple);

			generationClock.restart();
		}

		// We iterate through the apples...
		for (auto it = apples.begin(); it != apples.end();) {
			// ... and move it down.
			(*it).move(0, 400 * deltaTime);

			bool removeApple = false;

			// If the apple is outside de window we can destroy it.
			if ((*it).getPosition().y > getScreenSize().y) {
				removeApple = true;
			}

			// We should also check if the apple is touching the basket.
			// If so, we remove it.
			if (!removeApple && (*it).getGlobalBounds().intersects(basket.getGlobalBounds())) {
				removeApple = true;
			}

			if (removeApple) {
				it = apples.erase(it);
			}
			else {
				it++;
			}
		}

	}

	void render() {
		// DRAW HERE //

		// clear() is a helper method which calls renderWindow.clear(...)
		clear(sf::Color(80, 120, 220));

		// draw() is a helper method which calls renderWindow.draw(...)
		for (sf::Sprite& apple : apples) {
			draw(apple);
		}
		draw(basket);
	}

	private:
	sf::Texture appleTexture,basketTexture;
	sf::Sprite basket;
	sf::Clock generationClock;
	std::list<sf::Sprite> apples;
};

From the main.cpp we'll instanciate the zt::SceneManager and our GameScene. Then we'll add the scene to the scene manager and run the game loop:

int main() {
	sf::RenderWindow window;

	window.create(sf::VideoMode(600, 800), "ex1-game-v1");

	zt::SceneManager sceneManager(window);
	GameScene gameScene(sceneManager);
	
	sceneManager.addScene(gameScene);
	// switchTo() activates the scene especified
	// and deactivates the others.
	sceneManager.switchTo("GameScene");
	
	// The manage method contains the gameloop.
	sceneManager.manage();
	return 0;
}

Loading resources

The ResourceManager

A resource manager stores certain type of resource (e.g.: sf::Texture, sf::SoundBuffer). Each resource in the manager must have a unique name within it that will allow you to manage it easily.

ZeltaLib defines three resource managers by default:

Class name Name Contains
zt::TextureManager texture sf::Texture
zt::SoundBufferManager sound sf::SoundBuffer
zt::FontManager font sf::Font

All these resource managers inherit from zt::ResourceManager, so they have a common interface:

  • resourceManager.get("name") or resourceManager["name"] returns a resource. If it does not exist, a zt::ResourceNotFoundException is thrown.
  • loadFromFile("name", "filename"): loads a resource from a file. The resource will be called name.
  • loadFromMemory("name", pointerToData, sizeOfData): similar to loadFromFile() but it loads from the memory.

Under the hood, loadFromFile() and loadFromMemory() just call their SFML's homonym methods and adds the loaded resource to a std::map.

In the previous section we loaded our resources in the SFML way. We are going to modify that code to use zt::ResourceManager.

We just neet to do some little changes over the previous example.

First, we'll be using the zt::TextureManager to store our textures, so let's include the header:

#include <Zelta/Core/TextureManager.hpp>

Now, instead of having a sf::Texture for the apple and for the basket we instantiate the resource manager in our scene:

...
private:
zt::TextureManager textureManager; // Put this instead of sf::Texture.
sf::Sprite basket;
sf::Clock generationClock;

std::list<sf::Sprite> apples;

Then, in the constructor we loat into the resource manager and we get the textures from it:

GameScene(zt::SceneManager& sceneManager) : zt::Scene("GameScene", sceneManager) {
	textureManager.loadFromFile("apple", "assets/apple.png");
	textureManager.loadFromFile("basket", "assets/basket.png");

	basket.setTexture(textureManager["basket"]); // Note this.

	basket.setScale(0.3, 0.3);
	basket.setPosition(0, 800 - 100/* - apple.getLocalBounds().height*/);
}

You can see the whole code in the examples/core/ex3-game-v2-basic-resource-management folder.

Important: you must guarantee that your zt::ResourceManager doesn't get destroyed as long as its resources are beeing used. For example, if your zt::TextureManager contains a texture that is being used by a sf::Sprite, the manager should be alive as long as the sprite needs. That's because SFML sf::Sprite keeps a pointer to the resource.

At this point you may be asking if using the resource manager does really help. Effectively, using it in such a simple scenario does not make much sense but in the next examples we will see its power.

Loading from a resource file

Loading files from the source code works but doing it from a resource file is more convenient. The resource file defines the name of the resource and its path. Then, from our code, we use the zt::ResourceProvider to load that file.

So let's improve our previous example. Instead of loading from the code we are going to write this resources file:

resources.res:

load texture apple  assets/apple.png
load texture basket assets/basket.png

Note: we must write the name of the manager (texture, sound or font) just after the load keyword so that ZeltaLib can know to which manager should load the resource.

In our code we have to include the new header:

#include <Zelta/Core/ResourceProvider.hpp>

Finally, we are going create a zt::ResourceProvider and load resources.res:

zt::ResourceProvider loader;
loader.load("resources.res").into(textureManager);

Note that we no longer need to call the textureManager.loadFromFile(...) method. That is done by the zt::ResourceProvider.

The into() method is the one which actually performs the load, it also defines to which resource managers the resources will be loaded. The method gets a variable number of resource managers. In the example we pass our three default managers. If you are not using all of them you don't have to pass it.

With those few lines of code we can have all our resources loaded.

You can see the whole code in the examples/core/ex4-game-v3-resource-file folder.

On-demand resource loading

When we execute loader.load("resources.res").into(textureManager, soundManager, fotManager), all the resources defined in the file are loaded. For small projects this can be fine but sometimes we want our files to be loaded only when they are requested. That is the on-demand loading.

Enabling it is pretty straighforward: just call the onDemand() method in your resource provider:

ResourceProvider loader;
loader.load("resources.res").onDemand().into(textureManager, soundManager, fontManager);

Now, the resources.res file is read and interpreted but its files are not loaded. Each of its files will be independently loaded when we ask the resource manager for them. For example:

doSomething(textureManager["someResource"]); // someResource is loaded now.

Important: ResourceManager just contains resources and knows how to load them but it does not know where to load them from. That is the ResourceProvider's task. That means that when we use the on-demand loading we must keep the ResourceProvider object alive because the resource managers will ask it to load files.

Packaging you assets

The assets packaging tool allows you to put all your game assets in a file so they will be a little bit more safe from users modifications. Think of a package like a ZIP file. ZeltaLib packages does not support compression yet, though. It uses a simple custom format called ZeltaLib Package (ZPKG).

Lets store our assets directory in a package. We could do it programtically with the Package class but it's easier with the command line interface (I assume that you've compiled the CLI and its in your projects folder):

  1. Create an empty package:
./zelta package:create assets.zpkg
  1. Add the assets directory to it:
./zelta package:add-directory assets.zpkg assets
  1. Add the resources file to the package.
./zelta package:add resource.res

Now that we have our ZPKG file let's see how to load our resources from there using the ResourceProvider:

zt::ResourceProvider loader;
loader.load("resources.res").onDemand().fromPackage("assets.zpkg").into(textureManager);

Pretty easy. You have the code in the examples/core/ex5-game-v4-packaging folder.

Note: note that the resources file is loaded from the package. If, for some reason, you want the resources file to be outside the package but you still want to load the resources from it you can pass false as the second parameter of .fromPackge():

zt::ResourceProvider loader;
loader.load("resources.res").onDemand().fromPackage("assets.zpkg", false).into(textureManager);

Note: the opposite thing to loading from a package is loading from the file system which is the default behaviour. You can explicitly set it by calling .fromFileSystem():

loader.load("resources.res").onDemand().fromFileSystem().into(textureManager, soundManager, fontManager);

Releasing a resource

If you are no longer using a resource, you can explicitly release them using the release("resourceName") method of your resource manager.

A note about ResourceProvider

This features are not suported by zt::ResourceProvider:

  • loading from more than one resource file.
  • loading from more than one package.
  • loading from the filesystem and from a package.
  • loading on-demand and instantly.
  • reading the resources file from a package.

So, for example, avoid doing things like this:

zt::ResourceProvider loader;
loader.load("resources.res").onDemand().fromFileSystem().load("resource2.res").now().fromPackage("package.zpkg").into(textureManager);

Instead, you can use more than one provider:

zt::ResourceProvider loader1;
loader1.load("resources.res").onDemand().fromFileSystem().into(textureManager);

zt::ResourceProvider loader2;
loader2.load("resource2.res").now().fromPackage("package.zpkg").into(textureManager);

Other features

The Application class

Some developers prefer using an Application class to start writing their code instead of putting it in the main function. Then they just create an instance of the Application class in the main function.

If you like that, consider inheriting from zt::Application. It does not do anything special. It has two constructors: the default constructor and another one which receives the command line parameters (int argc, char** argv). You can get those parameters using the getArguments() method.

You'll have to implement the run() method.

This is a possible implementation:

class App : public zt::Application {
    public:
    App(int argc, char** argv) : zt::Application(argc, argv) {
        // This may be a good place to initialize your
        // scenes or even your resources.

        sceneManager.add(gameScene);
        sceneManager.switchTo("GameScene");
    }

    int run() {
        sceneManager.manage();
        return 0;
    }

    private:
    zt::SceneManager sceneManager;
    zt::GameScene gameScene;
};

int main(int argc, char** argv) {
    App app(argc, argv);
    return app.run();
}

Time handling

ZeltaLib defines the zt::Clock. It inherits from sf::Clock but adds the pause() and resume() methods.

You have algo a zt::NestableClock which is, essentially, a zt::Clock that can have child clocks. When a parent clock is paused, all its children will be paused, when it's resumed, all its children will be resumed. All scenes have a zt::NestableClock which is called master clock and you can get it by calling zt::Scene::getMasterClock().

Logs

ZeltaLib can help you to create logs. You can log to a file or to the console through zt::FileLog and zt::ConsoleLog. Both classes share the same interface.