Skip to main content Link Search Menu Expand Document (external link)

Theme: auto

Actors, Components, and Sprites

The starting code includes a declaration of Actor and Component. We’re using the hybrid component model where an Actor has-a std::vector of component pointers but also is inheritable.

Component

Look at the declaration in Component.h, and the implementation in Component.cpp. Notice how the Component constructor automatically adds itself to the owning Actor via AddComponent. So, when you construct a component, you do not need to manually call AddComponent (in fact, you won’t be able to manually call this as it’s private and we use friendship to allow Actor to call it).

Actor

Now look at Actor.h. Notice the ActorState enum can be (Active, Paused, or Destroy). It also has variables for scale, rotation, and position. Note that the position uses a Vector2. Actor also has a std::vector of component pointers.

Next, implement Actor::Update. If the Actor’s mState is ActorState::Active:

  1. Loop over all its components and call Update on each of them
  2. After the loop, call the OnUpdate member function. The OnUpdate member function is overridable by subclasses, so it’s where custom update logic can get added for a child class of Actor.

Similarly, in Actor::ProcessInput, if the Actor’s state is ActorState::Active, call ProcessInput on all components and then call OnProcessInput.

Game

Next, we need to hook up actors to the game. Add a std::vector of Actor* to Game. Hint: Make sure you use a forward declaration for all class pointers in header files, as discussed in lecture. This avoids extra includes and circular includes.

Game::AddActor and Game::RemoveActor

Create these two public functions in Game:

  • AddActor – Takes in an Actor* and adds it to the vector in Game
  • RemoveActor – Takes in an Actor*, removes the actor from the vector. You can use std::find (in <algorithm>) to get an iterator the Actor* and then erase to remove the element the iterator points to.

Actor Constructor and Destructor

Now implement Actor’s constructor and destructor. In the constructor, the Actor needs to add itself to the game via AddActor (remember Actor has an mGame member variable).

Similarly, the Actor destructor should call RemoveActor. This means Actors will automatically add/remove themselves from the Game’s Actor vector when they are constructed/destructed.

The Actor destructor should also loop over all its components and call delete on each, then finally clear the vector of components.

Game::UpdateGame

Updating the actors is somewhat complex. Add this code in your “update game” function, after all the code for computing and frame limiting the delta time:

  1. Make a copy (just use =) of the actor vector
  2. Loop over the copy and call Update on each actor
  3. Make a temporary Actor* vector for actors to destroy
  4. Loop over the actor vector, and any actors which are in state ActorState::Destroy should be added to the temporary vector from step 3
  5. Loop over the vector from step 3 and delete each actor in it (this will automatically call the Actor destructor, which then calls RemoveActor, which removes the actor from the Game’s vector).

The reason we make a copy and iterate over the copy is to account for the case where an actor’s update creates additional actors. This is inefficient for a bigger game. A better approach is to make a separate “pending” actor list and add actors to that, then move then from pending to the real one.

Remember that creating a new Actor automatically adds it to the Actor* vector. Modifying a vector while we’re iterating over it invalidates the iterator, and that can lead to bad results like crashes.

Game::ProcessInput

After the SDL_GetKeyboardState call, make a copy of the actor vector. Next, loop over the copy of all actors and call ProcessInput on each.

LoadData and UnloadData

Create two new private functions in Game (both take no parameters and return void):

  • LoadData – Doesn’t do anything right now
  • UnloadData – Call delete on all the actors in the vector. Since deleting an actor will remove it from the vector of actors, you have to be careful how you write this so that you don’t accidentally miss elements or invalidate the iterator. One way to do this is to continue to call delete on the back() element of the vector as long as the vector is not empty.

Again, the reason the UnloadData has to delete the actors in this way is because the destructor calls RemoveActor which will change the contents of the actor vector. You DO NOT need to do this for the components when deleting them in the Actor destructor, because the component destructor does not remove itself from the component vector.

Call LoadData in Game::Initialize, after you initialize all the systems.

Call UnloadData at the beginning of Game::Shutdown.

Unfortunately, there isn’t really anything to test just yet. However, make sure your code compiles and runs after all these changes. When you quit the game, it should not crash.

Sprites

Now we’ll add support for images and then integrate the SpriteComponent class. (A sprite is an image or sequence of images used for an object in a 2D game).

Loading Images

We’ll use the SDL Image library to load image files. In Game.cpp, add an include for <SDL2/SDL_image.h>.

In Game::Initialize, after creating the other SDL systems, but before you call LoadData, initialize SDL Image with IMG_Init: https://wiki.libsdl.org/SDL_image/IMG_Init. We will only support PNG files, so only pass in the IMG_INIT_PNG flag.

In Game::Shutdown, call IMG_Quit: https://wiki.libsdl.org/SDL_image/IMG_Quit.

Caching Loaded Textures

Suppose you need to draw twenty sprites that use the same texture (image). You clearly don’t want to load the same texture twenty times. You want to load the texture once, and then reuse it.

Add a hash map (use std::unordered_map) to your Game class. The key is a std::string and the value is an SDL_Texture*.

Now add a GetTexture function to Game that takes in a file name and returns an SDL_Texture*. This function should be public as many different actors will need to ask Game for specific textures. GetTexture should first check if a file by that name is in the hash map. If it is, just return that SDL_Texture* (remember that maps have a find() function).

If that texture does not already exist in the map, then you first need to load the texture with that file name. Loading an image file with SDL requires the following steps:

  1. Call IMG_Load: https://wiki.libsdl.org/SDL_image/IMG_Load
  2. Use SDL_CreateTextureFromSurface to convert the SDL_Surface* to an SDL_Texture*: https://wiki.libsdl.org/SDL_CreateTextureFromSurface
  3. Free the SDL_Surface* using SDL_FreeSurface: https://wiki.libsdl.org/SDL_FreeSurface

Because SDL is designed for C and not C++, IMG_Load takes in a C-style string pointer, not a std::string. You can use the c_str() member function to get the C-style string representation from a std::string.

Once you have the SDL_Texture* pointer, you should add an entry to the map for that file, so subsequent calls to GetTexture will find the file in the map. Then just return the SDL_Texture* since the image is now available.

If the IMG_Load function returns a nullptr, it means that the image failed to load (and you should not add it to the map). For debugging purposes, we recommend that if you fail to load a texture, you add an SDL_Log message that says which texture file it tried to load, and that it failed to load it. (A common reason this might fail is just because you have the wrong file name.)

Keep in mind when using the %s specifier for SDL_Log that it also expects a C-style string so you similarly have to use c_str().

In UnloadData, call SDL_DestroyTexture (https://wiki.libsdl.org/SDL_DestroyTexture) on every texture in the textures map, and then clear the map.

Now that we are loading textures, we can add a SpriteComponent that knows how to draw textures.

SpriteComponent

We’ve given you the implementation of SpriteComponent. Your job is to integrate it into Actor and Game.

Supporting SpriteComponents in Actor

Since SpriteComponent inherits from Component, it automatically gets added to the owning actor’s component vector when you create it! So you don’t need to do anything to Actor to add “support” for SpriteComponents.

Integrating in Game

We need to track sprite components in Game so we can draw them in the “draw scene” step of generating graphics output. Add a std::vector of SpriteComponent* to Game.

Create two public functions – AddSprite and RemoveSprite. AddSprite adds the sprite to the vector and then sorts the vector by draw order. This way, we can make sure to draw the sprites with a lower draw order first. To sort, use the std::sort (include <algorithm>) like this:

// mSprites is std::vector<SpriteComponent*>
std::sort(mSprites.begin(), mSprites.end(),
	[](SpriteComponent* a, SpriteComponent* b) {
		return a->GetDrawOrder() < b->GetDrawOrder();
});

In RemoveSprite, remove the sprite from the vector. You can use std::find and erase as you did for actors.

Now, in SpriteComponent.cpp, uncomment out lines 12 and 17 which call the AddSprite and RemoveSprite functions you just created.

Now in GenerateOutput, after the “clear” step and before the “present” step, you need to loop over the sprite component vector. For each sprite component, make sure it’s visible (use IsVisible()), and if it is, call Draw on it.

Test Actors

To test your sprite components, create a multiple actors with corresponding SpriteComponents in Game::LoadData. (Remember, you need to dynamically allocate them!)

  • One actor using the "Assets/Ship.png" texture
  • One actor with position (200.0f, 100.0f) using the "Assets/Laser.png" texture
  • One actor with position (200.0f, 200.0f), scale 0.75f, and rotation Math::PiOver2 using the "Assets/ShipThrust.png" texture
  • One actor with position (512.0f, 384.0f) using the "Assets/Stars.png" texture, setting the draw order of this sprite component to a value lower than the default of 100 (so it’s behind the other sprites)

Try each of these actors incrementally. For example, the code for the first actor is:

Actor* test = new Actor(this);
SpriteComponent* sc = new SpriteComponent(test);
sc->SetTexture(GetTexture("Assets/Ship.png"));

Once your test actors are all in, you should now see different actors with different textures: Test actors with sprites

Once you’e pushed this code, you’re ready to move on to part 2.