Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
SFML Game Development.pdf
Скачиваний:
194
Добавлен:
28.03.2016
Размер:
4.19 Mб
Скачать

Chapter 3

If you are interested in a more detailed explanation on the topic of component-based game design, we recommend reading the article series on the following website:

http://stefan.boxbox.org/game-development-design.

Rendering the scene

At one point, we have to reflect about how the game is rendered on the screen.

How do we draw all the entities, the scenery, and interface elements (such as a health bar)? A simple option is to have different sequential containers through which we iterate. For each element, we call a possible Entity::draw() function to draw the corresponding entity on the screen. We only have to make sure that objects that appear behind others (such as the scenery background) are drawn first.

Relative coordinates

The sequential rendering approach works well for many cases, but makes it difficult to handle an entity relative to another one. Imagine we have a formation of airplanes, where one is the leader and the rest follows it. It would be nice if we could set the position of the following airplanes dependent on the leader, in the sense of "plane

A is located 300 units behind (below on the screen) and 100 units right of the leader" instead of "plane A is located at position (x, y) in the world". We call (300, 100) a relative position, expressing where plane A is located with respect to the leader.

We can apply the principle of relative coordinates not only to position, but also to rotation and scale. In the mentioned example of a plane formation, we also want the planes to be headed towards the same direction as the leader. In other words, we want their rotation to be the same. If the followers' relative rotation is zero, all planes will face the same direction, namely the leader's one.

The advantage of relative coordinates consists of avoiding the need for manual synchronization of different entities. Using absolute coordinates, we have to recognize the leader and its followers, copy the leader's position to every follower, and add it to the follower's own position. This has to be done every time the position of the leader changes, which is cumbersome and error-prone (one can easily forget to update a single plane). Now compare that with the relative coordinate approach: we set the position of the leader, done. All followers adapt automatically.

This relationship is not limited to a single layer. An airplane that is following the leader can in turn have other followers that move relatively to it. Eventually, we build up a hierarchy of objects with relative transforms.

[ 53 ]

www.it-ebooks.info

Forge of the Gods – Shaping Our World

SFML and transforms

Now, the question is how we achieve the semantics of relative transforms in our game. Fortunately, SFML provides already the basic framework, on top of which we can build our own abstractions.

A geometrical transform specifies the way an object is represented on the screen.

In mathematical terms, a transform maps a coordinate system onto another. Translation affects the position of an object, rotation affects its orientation, and scale affects its size. Although transforms are not limited to these three operations, we will focus on them, because they are the ones mostly used.

SFML provides an API to work with position, rotation, and scale in the class sf::Transformable. The class stores the three transforms separately and provides useful accessor functions such as setPosition(), move(), rotate(), getScale(), and many more. It can be used as a base class, such that the derived class automatically inherits those functions. The following diagram visualizes the three transforms you will encounter again and again:

Rotation

Translation

Scale

 

sf::Transformable also contains the methods setOrigin() and getOrigin(), which give access to the coordinate system's origin. The origin is the reference point for the three transforms—it determines, which point in the object is looked at to set/get the position, or around which point the object rotates, or which point the object uses as the center for scaling. The origin is specified in local coordinates (relative to the object).

By default, it has the value (0, 0) and resides in the object's upper-left corner. Calling setOrigin() with the half object size places the origin to the object's center.

SFML provides another useful base class for graphical entities: the class sf::Drawable is a stateless interface and provides only a pure virtual function with the following signature:

virtual void Drawable::draw(sf::RenderTarget& target, sf::RenderStates states) const = 0

[ 54 ]

www.it-ebooks.info

Chapter 3

The first parameter specifies, where the drawable object is drawn to. Mostly, this will be a sf::RenderWindow. The second parameter contains additional information for the rendering process, such as blend mode, transform, the used texture, or shader. SFML's high-level classes Sprite, Text, and Shape are all derived from

Transformable and Drawable.

Scene graphs

In order to manage transform hierarchies in a user-friendly way, we develop a scene graph—a tree data structure consisting of multiple nodes, called scene nodes. Each scene node can store an object that is drawn on the screen, most often this is an entity.

Each node may have an arbitrary amount of child nodes, which adapt to the transform of their parent node when rendering. Children only store the position, rotation, and scale relative to their parent.

A scene graph contains a root scene node, which exists only once in a world. It resides above all other scene nodes in the hierarchy, thus it has no parent node.

Scene nodes

We represent the nodes in the scene graph with a class called SceneNode. Before we can implement it, we need to think about ownership semantics. The scene graph owns the scene nodes; therefore it is responsible for their lifetime and destruction. We want each node to store all its child nodes. If a node is destroyed, its children are destroyed with it. If the root is destroyed, the whole scene graph is torn down.

To store the children, we make use of the STL container std::vector. We cannot use std::vector<SceneNode>, since element types must be complete types (which they are only at the end of the SceneNode class definition, but not at the time of declaring the container as a member variable) and since our class is polymorphic (there will be derived classes overriding virtual functions of SceneNode). We could use std::vector<SceneNode*>, but then we would have to manage memory ourselves, so we take std::vector<std::unique_ptr<SceneNode>> instead. Since we are going to use the type std::unique_ptr<SceneNode> really often, we create a typedef for it, as a member inside the SceneNode class.

[ 55 ]

www.it-ebooks.info

Forge of the Gods – Shaping Our World

In addition to the children container, a pointer to the parent node is stored, so our class looks as follows:

class SceneNode

{

public:

typedef std::unique_ptr<SceneNode> Ptr;

public:

 

SceneNode();

private:

 

std::vector<Ptr>

mChildren;

SceneNode*

mParent;

};

The constructor initializes the parent pointer to nullptr and leaves the container empty.

nullptr is a keyword introduced with C++11 and represents the value of a null pointer. The formerly used NULL is a macro for the integer value 0, which implies many problems. A function overloaded for int and char*, that is passed NULL as argument, would unexpectedly choose the int overload.

For null pointer literals, nullptr should always be preferred over 0 or NULL. It is also convertible to the standard library smart pointers std::unique_ptr and std::shared_ptr.

Node insertion and removal

Now we write an interface to insert or remove child nodes into or from a scene node. We do this by adding the following two functions:

void

attachChild(Ptr child);

Ptr

detachChild(const SceneNode& node);

The first method takes a unique_ptr<SceneNode> by value, taking ownership of the scene node. The second method searches for an occurrence of the specified

node, releases it from the container, and returns it to the caller, again wrapped in a unique_ptr. If the return value is ignored, the node will be destroyed.

[ 56 ]

www.it-ebooks.info

Chapter 3

The function to attach a child node is simple to implement. We set its parent pointer to the current node, and add the smart pointer to the container by moving its contents in the following code snippet:

void SceneNode::attachChild(Ptr child)

{

child->mParent = this; mChildren.push_back(std::move(child));

}

The second method is slightly more complex. First, we search for the specified node in the container with the help of a lambda expression.

Lambda expressions are a C++11 language feature. They allow the definition of local functions inside other functions, and the direct usage in surrounding code.

A lambda expression creates an anonymous function object, similar to a named functor class with overloaded operator(). Its declaration consists of the following parts:

The capture list specifies to which variables in the surrounding

scope the lambda expression has access. [] captures no variables, [&] captures all variables by reference, [=] by value, [&node] and [node] capture only the listed variables by reference or value, respectively. The capture list is comparable to the arguments passed to a functor's constructor.

The parameter list contains the parameters passed to the function. These correspond to the parameters of the functor's operator().

The return type has the syntax -> type. If the function body consists of a single return statement, the return type can be omitted.

The function body is enclosed in {} and contains the actual statements.

The lambda expression is invoked with each container element and returns true if the element's pointer p.get() is equal to the address of the wanted node. The STL algorithm std::find_if() returns an iterator to the found element, which we check for validity using assert.

SceneNode::Ptr SceneNode::detachChild(const SceneNode& node)

{

auto found = std::find_if(mChildren.begin(), mChildren.end(), [&] (Ptr& p) -> bool { return p.get() == &node; });

assert(found != mChildren.end());

[ 57 ]

www.it-ebooks.info

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]