The Last Signal. Post #3: sprite management considerations

The projects (games) which we are working on in the course 5SD033 – Introduction to Game Development may not be big, but they are considerably bigger in scope than the simple games in 5SD022 – Game Programming I course.

In these games, a lot of sprites are required. A pressing need for a flexible sprite manager arises.

I spent a lot of time simply thinking about the right strategy to handle sprites. I also had my awful GameState::Enter() function in mind; it was doing most of initial sprite handling, so the sprite manager had to relieve the GameState::Enter() function of sprite handling burden as well as be reusable elsewhere in the game. Here is an excerpt of the code in GameState::Enter() implementation which is relevant to sprite management:

bool GameState::Enter()
{

//load textures

	m_transfer.tplayer.loadFromFile("../Assets/Astronaut_Idle.png");
	
// and so for every texture in the game, currently seven such lines ...

//create sprites. Apply loaded textures and set sprite properties, such as scale, origin and position

	m_transfer.sprite.setTexture(m_transfer.tplayer);
	m_transfer.sprite.setScale(0.5f, 0.5f);
	m_transfer.sprite.setPosition(400, 850);
	m_transfer.sprite.setOrigin(67, 81);
	
//and so for every sprite in the game, currently nine such four-line blocks ...
	
//create in-game objects with specified coords. Apply sprites to them


    m_player = new Player(&m_transfer.sprite, 400.0f, 850.0f);
	m_entities.push_back(m_player);

	m_enemy1 = new Enemy1(&m_transfer.enemy, m_room3, 750.0f, 250.0f);
	m_entities.push_back(m_enemy1);

	m_enemy2 = new Enemy2(&m_transfer.enemy2, 750.0f, 250.0f);
	m_entities.push_back(m_enemy2);

	m_pwr = new Overcharge(&m_transfer.pwr, 700, 850);
	m_entities.push_back(m_pwr);

	for (int i = 0; i < 10; i++)
	{
		m_bullet = new Bullet(&m_transfer.bullet, 0, 0);
		m_entities.push_back(m_bullet);
	}

	return true;
};

I removed repeated blocks of code and replaced them by the comments seen above. You should get the impression – the GameState::Enter() function is cluttered with the same code repeating again and again which does the following for every in-game object:

1. loads a texture from an external file
2. creates a sprite with that texture; applies required scale, centers sprite origin and sets position
(in this game development stage the textures drawn by the artists have varying dimensions and do not look good together without manually rescaling every one of them individually; this will be fixed soon when they rescale everything according to one standard but it is easier to work with assets if they are scaled correctly from the beginning)
3. creates objects using the previously created sprites

Some more sprite management is done in GameState::Update() function.

bool GameState::Update(float deltatime)
{
//run update functions for every object
     m_player->UpdateTemp(deltatime, curr.x, curr.y, Transfer::GetMoving(), m_room_active);
     m_enemy1->Update(deltatime);
     m_enemy2->Update(m_player);

     for (int i = 0; i < m_entities.size(); i++)
		if (m_entities[i]->GetType() == ENTITY_BULLET)
		{
		Bullet* bullet = static_cast<Bullet*>(m_entities[i]);
		if (m_room_active->EvaluateCoords(bullet->GetX(), bullet->GetY()) != CONFINED)
			bullet->SetState(false);
		bullet->Update(deltatime);
		}

//update sprites using corresponding object properties

	m_transfer.enemy2.setPosition(m_enemy2->GetX(), m_enemy2->GetY());
	//m_transfer.enemy2.setRotation(m_enemy2->GetAngle());

	for (int i = 0; i < m_entities.size(); i++)
		if (m_entities[i]->GetType() == ENTITY_BULLET)
		{
		Bullet* bullet = static_cast<Bullet*>(m_entities[i]);
		sf::Sprite temp = *bullet->GetSprite();
		temp.setPosition(bullet->GetX(), bullet->GetY());
		temp.setRotation(bullet->GetAngle());
		bullet->SetSprite(temp);
		}
}

What happens here is that update functions are run for all objects (this does belong to GameState::Update() function) and then sprites are updated to calculated object values.

Final point of contact is in the GameState::Draw() function:

void GameState::Draw()
{
	m_window->draw(m_transfer.floor1);
	m_window->draw(m_transfer.floor2);
	m_window->draw(m_transfer.floor3);
	m_window->draw(m_transfer.floor_variation);
	m_window->draw(m_transfer.line1);
	m_window->draw(m_transfer.line2);
	m_draw_manager->Draw(m_transfer.sprite, sf::RenderStates::Default);
	m_draw_manager->Draw(m_transfer.enemy, sf::RenderStates::Default);
	m_draw_manager->Draw(m_transfer.enemy2, sf::RenderStates::Default);

	for (int i = 0; i < m_entities.size(); i++)
		if (m_entities[i]->GetType() == ENTITY_BULLET)
		{
		Bullet* bullet = static_cast<Bullet*>(m_entities[i]);
		sf::Sprite* temp = bullet->GetSpriteFull();
		if (bullet->IsActive())
			m_draw_manager->Draw(*temp, sf::RenderStates::Default);
		}
	m_draw_manager->Draw(m_transfer.pwr, sf::RenderStates::Default);
	m_draw_manager->Draw(m_firebar, sf::RenderStates::Default);
};

Here we draw the sprites either by accessing them directly (since they are defined as GameState class member variables) or by extracting them from corresponding in-game objects.

To sum up:

GameState::Enter():

1. Load external textures
2. Create sprites and apply required sprite properties
3. Create objects

GameState::Update():

4. Update objects
5. Update sprites to reflect object data

GameState::Draw():

6. Draw sprites

NB: some sprites have parent objects, e.g. player, enemies, bullets, doors etc. Some others don’t, e.g. floor tiles, non-interactable decorations etc. The latter ones are handled differently (more simply) than the former ones since they don’t have to be updated to the values of their parent objects which are recalculated every frame.


I had devised the following system to handle sprites:

Ritning1My intention was to encapsulate sprite in its parent object just like colliders are part of their parent objects. In this way sprite handling would be transferred from GameState function to the object itself.

Looking at the diagram above, loaded texture would be passed to oject constructor at the time of its creation. The object class would create a sprite for itself. GameState::Update() function calls every object’s update function. In this case, the object would first update its member variable values and then internally update its sprite properties, such as position and rotation. The sprite can then be accessed via the object’s GetSprite() method and can be used by, e.g. CollisionManager.


Object implementations have to be modified something like this:

Player::Player(sf::Texture* texture, float x, float y)
{
	m_x = x;
	m_y = y;

	m_sprite = new sf::Sprite;
	m_sprite->setTexture(*texture);

	//hardcoding player sprite scale and origin. Scale will eventually go away hen graphics artists adjust the assets but origin will most probably remain.
	m_sprite->setOrigin(m_sprite->getGlobalBounds().width * 0.5f, m_sprite->getGlobalBounds().height * 0.5f);
	m_sprite->setScale(0.5f, 0.5f);

	//...
};
void Player::Update(float deltatime)
{
	//update object properties, such as position and rotation ...

	UpdateSprite();
};
void Player::UpdateSprite()
{
	m_sprite->setPosition(m_x, m_y);
	m_sprite->setRotation(m_angle);
}

This makes the code much more concise. Steps 2-3 in the summary (see the “To sum up” part) are united to one single step – object creation. It takes the texture and internally creates the sprite. Steps 4-5 are united to one single step as well, object update, since it internally updates its sprite. GameState function only contains the code to create objects (GameState::Enter()), update them (GameState::Update()) and draw their sprites (GameState::Draw()).

However, it introduces two new problems:

1. while GameState function is relieved of the code that sets sprite scale and origin at its creation, now object constructor becomes littered with this code
2. “free” sprites that don’t have parent objects (such as decorations) need to be treated differently, so this system does not apply to them


It seems I will have to devise a different system to the one described in this post. A failed attempt is still part of the learning process, which warrants this post.
A question to anyone reading this post: what do you think? Is this sprite management system reasonable? You’re welcome to leave a comment in the comments section below.

This entry was posted in Studies at Campus Gotland and tagged . Bookmark the permalink.

1 Response to The Last Signal. Post #3: sprite management considerations

  1. Hi Rokas.

    After reading your post I must say that it does seem reasonable, but it is not the way I would have solved the problem. I am the lead code for the other Last Signal project so there are bound to be similarities between our code, even if we take the concept in different directions.

    You have the creation of sprites in the Enter() method of your GameState, whereas I would have put them in the constructor of the individual objects so that the sprites can be easily changed and manipulated as the need for that occurs. You have presented a similar solution to the problem in the later examples but I would perhaps take it further and create the Texture pointers in the relevant objects instead of relaying them to the objects as parameters. The need for a sprite manager is not as great as the need for a texture manager in that case, but it does create a more readable code instead of having code for handling textures in the Enter() method.

    Another idea that we have implemented in our game is that we split up the drawing of sprites into the code for the separate objects in order to reduce clutter and make the code more readable. This may work for your code as well if you find that there is a lot of repeating code.

    // Anton Classon

Leave a comment