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

Theme: auto

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

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 a protected 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 of Component:

    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

  1. In the Settings menu, go to Editor>File and Code Templates

  2. In here, under the “Files” section click the + sign for “Create Template”: Create Template

  3. 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;
      };
           
      
  4. Click Apply

  5. This should look like: Component Class template

  6. While “Component Class” is still selected, click the “Create Child Template File” button immediately to the right of the + button: Create Child Template File

  7. 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)
      {
      }
           
      
  8. Click Apply

  9. It should look like this now: CPP File settings

  10. 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:

New Component Class

In the dialog box that pops up, it’ll ask you for the file name. Put SpriteComponent:

Call it 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 SpriteComponents 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:

  1. 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)
  2. 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 to get an SDL_Surface* pointer to the image data: https://wiki.libsdl.org/SDL3_image/IMG_Load
    2. If step (a) fails (it will return nullptr) you should use an SDL_Log message to say which texture image file failed to load (see the note below), and GetTexture should return nullptr
    3. Use SDL_CreateTextureFromSurface to convert the SDL_Surface* to an SDL_Texture*: https://wiki.libsdl.org/SDL3/SDL_CreateTextureFromSurface
    4. Free the SDL_Surface* using SDL_DestroySurface: https://wiki.libsdl.org/SDL3/SDL_DestroySurface
    5. Add the SDL_Texture* to the map
    6. Return the SDL_Texture*

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…

  1. Create an SDL_FRect on the stack

  2. Use SDL_GetTextureSize to get the width and height of the texture: https://wiki.libsdl.org/SDL3/SDL_GetTextureSize, saving them in the rect’s w/h components

  3. Get the scale of the owning actor, which should be something like this:
    float scale = GetOwner()->GetTransform().GetScale();
    
  4. *= the rect’s w/h by this scale

  5. Using the owner position and w/h, calculate the top left corner of the rectangle into x/y, pretty much like how you did in Lab 1

  6. 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) where angle 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 SpriteComponents 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: First ship drawingIf 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

  1. Put a breakpoint on the line that calls SDL_RenderTextureRotated in SpriteComponent. 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?).
  2. 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
  3. 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:A wild asteroid appears!

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 to 0
  • Add int GetDrawOrder() const and SetDrawOrder functions

Then, in Game.cpp:

  • Include <algorithm>

  • If your GenerateOutput is marked as a const member function, you will have to remove the const-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 in LoadData, call SetDrawOrder, passing in -42

You should now see the starry background with the ship and asteroid in front:Draw order works

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