Actors, Components, and Sprites
Right now Actors are just a basic container for their Transform. However, we’re going to implement the hybrid component model discussed in lecture, an Actor
has-a std::vector
of component pointers but also is inheritable.
As in Lab 1, there is a lot of specific setup to do, so the instructions for this first part especially are quite detailed. However, about half way through this assignment the instructions will become less detailed.
Changes to Transform
While the size and GetRect()
functions were useful for Pong, moving forward it’ll be a bit constraining as the size of many actors will be based on the image used for them, and we may also want to track collision size separately from visual size.
Go ahead and remove the size variable, getter/setters, and GetRect()
function.
Instead, add two new float
member variables to Transform
:
- One for the rotation of the actor (set it to 0 by default)
- One for the scale of the actor (set it to 1 by default)
Then, add getters/setters for both new varaibles.
Since they’re floats, you do not need to pass them or return them by reference, e.g. the declarations for the getter/setter for rotation would be:
float GetRotation() const;
void SetRotation(float rotation);
Make sure your code still builds.
Creating the Component Class
In Component.h, declare a new class called Component
.
When implementing the header for Component
, you MUST use a forward declaration of Actor
(as discussed in lecture), or you will very quickly introduce a circular include that will not compile.
- In the protected member data, add an
Actor*
for the owner - In the public section, add:
- A constructor that takes an
Actor*
owner and sets the member variable - A
virtual
destructor, e.g.virtual ~Component();
. The implementation can be empty - An
Actor* GetOwner() const
function that returns the member variable - You DO NOT want to add a setter for the owner because we won’t support dynamically changing the owner of the component once it’s created
- A constructor that takes an
You need the virtual destructor here because we will be inheriting from Component
. In general, classes that support inheritance should have a virtual destructor.
Make sure your code still compiles.
Adding Components to Actor
The Actor
class needs a private std::vector
of Component*
pointers called mComponents
to track the components that it has.
Just like how we had a helper function in Game
to create actors, we need a helper function in Actor
to create components.
However, there’s an added complication here, which will soon come up for CreateActor
, too. We are going to inherit from both Component
and Actor
. When we create one of them, we need to be able to specify which type to create. For example, we aren’t going to ever create just a Component
but maybe a SpriteComponent
or CollisionComponent
. In order for us to tell CreateComponent
what type we want to create, we need a template argument for the function.
CreateComponent
Add the following templated CreateComponent
function in the public section of Actor
. You need to do this in the header because it’s a template function.
template <typename T>
T* CreateComponent()
{
// Create a new instance of type T, which should inherit from Component
// It takes in "this" because every component takes an Actor* in its constructor
T* component = new T(this);
// Add this component to our component vector
mComponents.emplace_back(component);
return component;
}
CreateActor Changes
Similarly, change the CreateActor
function in Game.h to the following:
template <typename T>
T* CreateActor()
{
T* actor = new T();
mActors.emplace_back(actor);
return actor;
}
Deleting Components
Now you also need to add a virtual
destructor for Actor
.
In here, you should loop over all the components and call delete
on each. After the loop, clear out the vector.
Preventing Creation from Outside CreateActor/CreateComponent
Well-designed code should prevent you from doing things that are incorrect. In our case, we really only want to be able to create actors through the CreateActor
function. We don’t want someone accidentally adding their own new Actor()
code somewhere, because that won’t work properly.
Similarly, we don’t want some unauthorized code calling delete
on actors or components. We want to protect against that.
To fix this, we can use C++ friendship. The idea here is we can make the constructors and destructors of Actor and Component protected instead of public. Then we make the class that’s allowed to create/destroy the class a friend
.
So, in the declaration of Actor
:
-
If you don’t already have a default constructor declared for
Actor
, declare one in aprotected
section -
Move the destructor of
Actor
protected, also -
Add the following line of code:
friend class Game;
By making Game
a friend of Actor
, it means that Game
is allowed to access protected/private members of Actor
. It’s not that commonly used in C++, but sometimes it’s useful.
Next, in the declaration of Component
:
-
Make sure the constructor and destructor in a
protected
section -
Add the following line of code to make
Actor
a friend ofComponent
:friend class Actor;
Creating Your First Component Subclass
When we create a subclass of Component
, there are some minimal things you’d need to do. Hypothetically, say you want to create a SpriteComponent
. You’d have to do a lot of boilerplate code like adding includes, declaring the inheritance, declaring a constructor in the header file, and the adding basic implementations in the cpp.
All that would just be to get it setup. Then you’d go ahead and add any additional custom stuff you need specific to that component. Manually creating the new files once or twice would be fine, but doing it over and over is tedious. So, we will work a bit smarter by leveraging CLion’s support for File and Code Templates.
Setting up the Component Class File Template
-
In the Settings menu, go to Editor>File and Code Templates
-
In here, under the “Files” section click the + sign for “Create Template”:
-
Set the following properties:
-
For the Name, call it
Component Class
-
For the Extension, just put in
h
-
For the File Name, put in
${NAME}
-
In the big box for the template, copy/paste the following:
// // Created by $USER_NAME on ${DATE}. // #[[#pragma]]# once #[[#include]]# "Component.h" class ${NAME} : public Component { protected: ${NAME}(class Actor* owner); friend class Actor; };
-
-
Click Apply
-
This should look like:
-
While “Component Class” is still selected, click the “Create Child Template File” button immediately to the right of the + button:
-
Set the following properties:
-
For the File Name, put in
${NAME}
-
For the Extension, put in
cpp
-
In the big box for the template, copy/paste the following:
// // Created by $USER_NAME on ${DATE}. // #[[#include]]# "${NAME}.h" #[[#include]]# "Actor.h" ${NAME}::${NAME}(class Actor* owner) : Component(owner) { }
-
-
Click Apply
-
It should look like this now:
-
Click “OK”
We are setting this up just for your convenience. We do not expect you to understand the CLion file template syntax (which is not C++) and won’t test it or anything like that.
Creating SpriteComponent
Now that you’ve setup the file template, let’s test it out.
In the File menu, select New>Component Class:
In the dialog box that pops up, it’ll ask you for the file name. Put SpriteComponent
:
After you click OK, it’ll ask you if you want to add the files to Git, which you can just say yes.
You should now have basic SpriteComponent.h/cpp files setup for you.
Make sure your project still builds with these new files.
Loading Images
Before we can worry about SpriteComponent
drawing something, we need a way to load in images from files. Since we may have hundreds of SpriteComponent
s using the same image, we don’t want to just load it repeatedly. Instead, we will save any loaded images in the Game class so it can be loaded once and reused as needed.
Loading Images
We’ll use the SDL Image library to load image files. In Game.cpp, add an include for "SDL3_image/SDL_image.h"
.
Caching Loaded Textures
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*
.
Recall that you need to add includes for STL classes. In this case, since you’re using both std::unordered_map
and std::string
, it means you need to include both <unordered_map>
and <string>
.
Now add a public 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:
- Check if a file by that name is in the hash map. If it is, just return that
SDL_Texture*
(remember that maps have afind()
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
to get anSDL_Surface*
pointer to the image data: https://wiki.libsdl.org/SDL3_image/IMG_Load - If step (a) fails (it will return
nullptr
) you should use anSDL_Log
message to say which texture image file failed to load (see the note below), andGetTexture
should return nullptr - Use
SDL_CreateTextureFromSurface
to convert theSDL_Surface*
to anSDL_Texture*
: https://wiki.libsdl.org/SDL3/SDL_CreateTextureFromSurface - Free the
SDL_Surface*
usingSDL_DestroySurface
: https://wiki.libsdl.org/SDL3/SDL_DestroySurface - Add the
SDL_Texture*
to the map - Return the
SDL_Texture*
- Call
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
.
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()
.
Deleting All Textures in Unload Data
In UnloadData
, call SDL_DestroyTexture
(https://wiki.libsdl.org/SDL3/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
Now in SpriteComponent.h, add an include for "SDL3/SDL.h"
since we need to use SDL types (that we can’t forward declare).
In the protected section, add a SDL_Texture*
for the texture associated with the SpriteComponent
.
In the public section, add the following two virtual function declarations:
virtual void Draw(SDL_Renderer* renderer);
virtual void SetTexture(SDL_Texture* texture);
(They need to be virtual because in future labs, we’ll subclass SpriteComponent
).
SpriteComponent::SetTexture
The implementation of SetTexture
can just set the member variable.
SpriteComponent::Draw
If mTexture
is nullptr
, this function should do nothing.
Otherwise…
-
Create an
SDL_FRect
on the stack -
Use
SDL_GetTextureSize
to get the width and height of the texture: https://wiki.libsdl.org/SDL3/SDL_GetTextureSize, saving them in the rect’sw
/h
components - Get the scale of the owning actor, which should be something like this:
float scale = GetOwner()->GetTransform().GetScale();
-
*=
the rect’sw
/h
by this scale -
Using the owner position and
w
/h
, calculate the top left corner of the rectangle intox
/y
, pretty much like how you did in Lab 1 -
Call
SDL_RenderTextureRotated
: https://wiki.libsdl.org/SDL3/SDL_RenderTextureRotated. The parameters are a little complicated, so this is what you should pass in:- renderer - The renderer function parameter
- texture - Your texture member variable
- srcrect -
nullptr
(because we want the whole texture) - dstrect - The pointer to the local
SDL_FRect
you just made - angle - This should be
-Math::ToDegrees(angle)
whereangle
is the owner’s rotation - center -
nullptr
(because we just want to rotate around the center) - flip -
SDL_FLIP_NONE
Tracking SpriteComponents
We have a draw function now, but we need some way to actually track what SpriteComponent
s exist and then draw them in GenerateOutput
.
Add a private std::vector<class SpriteComponent*>
to Game
.
Then in the public section, create an AddSprite
function that takes in the class SpriteComponent*
and adds it to the vector.
You also need to create a RemoveSprite
function that takes in a class SpriteComponent*
and removes it from the vector. You can use the std::erase
function to remove it.
In order for this to work, we need every SpriteComponent
to tell gGame
it exists when it is created, and tell gGame
it no longer exists when it’s destroyed. It makes the most sense to do this in the constructor and destructor of SpriteComponent
, so add that code!
Drawing SpriteComponents
Now in GenerateOutput
, replace the commented-out code that you used to draw rectangles in Lab 1 to instead loop over the sprites and call Draw
on each.
Testing Sprites
We added lots of code, now let’s see if we actually can see stuff. Add the following code to Game::LoadData
:
Actor* actor = CreateActor<Actor>();
actor->GetTransform().SetPosition({200, 100});
SpriteComponent* sc = actor->CreateComponent<SpriteComponent>();
sc->SetTexture(GetTexture("Assets/Ship.png"));
If everything works, you should see a ship pointed to the right, like this: If you don’t see the ship, try moving onto the “practice debugging” section as maybe the steps there will help figure it out.
Practicing Debugging, Part 2
- Put a breakpoint on the line that calls
SDL_RenderTextureRotated
inSpriteComponent
. Take a screenshot of the source code view showing the debugger paused on this, and save it in Lab02/Screenshots/1.png. (If you don’t hit this at all, that’s a strong clue as to what’s wrong. Trace back in your logic to try to figure out why you aren’t getting to this breakpoint?). - Take a screenshot of the bottom debug pane showing the call stack and the values inside your local
SDL_FRect
. Save this in Lab02/Screenshots/2.png - Click the “Step Out” button. Take a screenshot of the source code view, showing where the debugger is paused at now. Save this in Lab02/Screenshots/3.png
Testing More Sprites
At the bottom of LoadData
, add another test actor, this with its scale/rotation changed, as well:
actor = CreateActor<Actor>();
actor->GetTransform().SetPosition({400, 300});
actor->GetTransform().SetScale(0.5f);
actor->GetTransform().SetRotation(Math::PiOver2);
sc = actor->CreateComponent<SpriteComponent>();
sc->SetTexture(GetTexture("Assets/Asteroid.png"));
You should see an asteroid now:
Next, add another actor for the background:
Actor* bgActor = CreateActor<Actor>();
bgActor->GetTransform().SetPosition({400, 300});
SpriteComponent* bgSprite = bgActor->CreateComponent<SpriteComponent>();
bgSprite->SetTexture(GetTexture("Assets/Stars.png"));
You should now see stars, but what’s this? It’s drawing over the ship and asteroid, which you won’t see anymore. Let’s fix that.
Adding a Draw Order
The problem is that we just draw the sprites in the order they are in the vector. That means the last one added will always be on top. Rather than having to worry about that, we need to add a member variable to SpriteComponent
for the draw order, and then we can sort the sprites by that before we draw them.
So, in SpriteComponent
:
- Add a protected
int
member variable for the draw order. It should default to0
- Add
int GetDrawOrder() const
andSetDrawOrder
functions
Then, in Game.cpp:
-
Include
<algorithm>
-
If your
GenerateOutput
is marked as aconst
member function, you will have to remove theconst
-ness, or you won’t be able to sort - Inside
GenerateOutput
, right before you loop over the sprites to draw, add the following code to sort by the draw order:// Sort by draw order (this assumes mSprites is the vector) // Don't worry about the []... syntax (it tells it how to sort them) std::ranges::stable_sort(mSprites, [](const SpriteComponent* a, const SpriteComponent* b) { return a->GetDrawOrder() < b->GetDrawOrder(); });
- After you create the
bgSprite
inLoadData
, callSetDrawOrder
, passing in-42
You should now see the starry background with the ship and asteroid in front:
Once you’ve pushed this code, you’re ready to move on to part 2.