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
:
- Loop over all its components and call
Update
on each of them - After the loop, call the
OnUpdate
member function. TheOnUpdate
member function is overridable by subclasses, so it’s where custom update logic can get added for a child class ofActor
.
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 anActor*
and adds it to the vector inGame
RemoveActor
– Takes in anActor*
, removes the actor from the vector. You can usestd::find
(in<algorithm>
) to get an iterator theActor*
and thenerase
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:
- Make a copy (just use
=
) of the actor vector - Loop over the copy and call
Update
on each actor - Make a temporary
Actor*
vector for actors to destroy - Loop over the actor vector, and any actors which are in state
ActorState::Destroy
should be added to the temporary vector from step 3 - Loop over the vector from step 3 and
delete
each actor in it (this will automatically call theActor
destructor, which then callsRemoveActor
, 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 nowUnloadData
– Calldelete
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 calldelete
on theback()
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:
- Call
IMG_Load
: https://wiki.libsdl.org/SDL_image/IMG_Load - Use
SDL_CreateTextureFromSurface
to convert theSDL_Surface*
to anSDL_Texture*
: https://wiki.libsdl.org/SDL_CreateTextureFromSurface - Free the
SDL_Surface*
usingSDL_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 SpriteComponent
s.
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 SpriteComponent
s 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)
, scale0.75f
, and rotationMath::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:
Once you’e pushed this code, you’re ready to move on to part 2.