If you look in the resources folder you’ll find a new sprite sheet for the character called Skeleton.png.
The class inherits from Component but it also is derived from C_Collidable, which is the system we were working on last week. We override OnCollisionEnter as we’ll use that function to remove the projectile at the beginning of a collision.
- auto: when we want to make a copy of the original.
- auto*: when we want to read and modify the original and be able to change what the pointer references.
- const auto&: when we want to read and modify a property of the original but do not want to be able to change what the pointer references.
- const shared_ptr&: when we want a read-only copy of the original.
First I want to make a quick change to the C_InstanceID class, whose job it is to provide a unique id number to every object. Currently we are storing the ids as int, however, I want to change this to unsigned int for reasons that will become apparent shortly.
“When x and y are non-negative integers, elegantPair(x, y) function outputs single non-negative integer that is uniquely associated with that pair.”
- ‘We need some way to let our engine know which objects we want to collide”. We accomplished this by creating the C_BoxCollider component, which we can attach to any object we want to collide.
- “The objects will be passed to a structure called a quadtree”. Yep done that as well.
- “Each frame our collision system will check if any objects with a collider component are colliding. If two are found to be colliding then the collision will need to be resolved so that they are no longer colliding”. Our collision system takes care of that.
- “We should have the ability to mark an object as static, which means that we will not resolve its collisions”. This has also been implemented. We use it to reduce the number of collision checks.
- “Every collidable will be on a specific layer. We will be able to set which layers collide”. We have our layer system setup. Another useful tool used to reduce the number of collision checks we need to perform.
- “When two objects collide we want to be able to inform any interested components”. This is yet to be completed and is what I want to work on this week.
So how are we going to accomplish this? As with any programming task, there are loads of ways we can go about it. However, I want to take advantage of our component system. First, we’ll create a new component that will be the base class for any component that wants to be notified of a collision change. This component will have three virtual functions: OnCollisionEnter, OnCollisionStay, and OnCollisionExit. The names of the functions may give you a clue on when the functions will be called:
- OnCollisionEnter will be called when two objects first collide.
- OnCollisionStay will be called every frame that the objects stay colliding.
- OnCollisionExit is called when two objects stop colliding by moving apart or when either of the objects has been queued for removal.
When a new component is added to an object, the object will check if the component derives from this base class and if it does it stores it in a separate collection. The object class will also have an OnCollisionEnter, Stay, and Exit functions. These will call the corresponding functions in any collidable components added to the object. We will call these functions on the objects rather than interacting with the components directly.
We recently wrote a feature that enables us to call a function on a specific animation frame. This week we’ll take advantage of this system for our projectile attack. At the moment we are spawning projectiles as soon as we press the projectile attack key, which doesn’t look quite right. What we really want is for the projectile to spawn at the correct moment in the animation which is when the player’s character releases the bowstring.
Before we write the animation action I want to make one small change to C_Animation. As I was writing the code for this tutorial I noticed that I’ve used a map to store the animations. A map will store the entries in a specific order (sorted using caparison function ‘Compare’). As we don’t need the entries sorted we can swap to an unordered_map as they are generally faster for insertions and retrievals.
C_ProjectileAttack still needs access to this class so include our new file.
Include the new file in C_Animation and then use it as the third parameter in the AnimationList (which I’ve also changed to use an unordered_map instead of a map) and the animations collection.
With that done we can start on the goal for this week: spawning the projectiles as an animation action. The Animation class already has the functionality for adding frame actions but we don’t interact directly with it, instead we’ll write a function in the animation component that will. This way any component can retrieve the animation component and add an action.
I’ve numbered the first call to the AddAnimationAction so we can discuss it in more detail.
- We want to add an action to the projectile animation for this entity so we pass that as the first argument.
- In this instance, we are adding an action for the Up direction (we add actions for the other 3 directions directly after).
- The animation has 10 frames and we want to want the action to be run on the last frame so, and remembering that the count starts at 0, we pass 9 in as the third parameter.
- The fourth argument is the AnimationAction. It’s the function we want to run on the specified frame. If we look at the definition of AnimationAction:
using AnimationAction = std::function<void(void)>;
This week we’ll continue working on our projectile system. By the end of this tutorial we will have:
- Changed the arrows initial position so that it aligns with the player’s bow.
- Made the projectiles move through the environment.
- Made the projectiles collide with the environment.
Similarly to the textureDirectionBindings I’ve made this map static for the same reasons I outlined in the previous tutorial: it reduces memory usage.
If you don’t recognise the EnumClassHash then you probably don’t need it to compile, see the previous tutorial where I created and discussed the need for the hash class. If you didn’t need it last week then you don’t need it now and you can just remove it so the declaration looks like this:
We’ll insert the four offsets in the Start function. I’ve chosen the following offsets:
- Up = 0, 0
- Left = -8, 3
- Down = -3, 15
- Right = 8, 3
We’ll populate the map in the Start function (again). Each direction will be stored along with the normalised velocity for that direction:
- Up = 0, -1
- Left = -1, 0
- Down = 0, 1
- Right = 1, 0
You’ll notice that the arrows do not collide with the environment and will fly off into the abyss. To fix this we’ll move onto task number three: enabling the projectiles to collide with the environment. Luckily we have already written a collision system that we can use by adding a C_BoxCollider component to each new arrow we spawn.
Before we add the component we need to add a new collision layer for the projectiles. Collision layers are used to control which objects collide. For example, we can add an object to the collision layer of ‘Projectile’ and set that to collide with another collision layer called ‘Tile’. Using layers we can minimise the number of collision checks we perform. For more information check out the previous tutorial where we wrote the layer system. To add a new collision layer we need to first add a new entry to the CollisionLayer enum in C_Collider.hpp
We assign the Projectile layer the number 4. For each layer, we create a bitmask (link) that sets bits at specific locations to determine whether a layer collides with another. For example, if we assign a layer a bitmask with the bits at position 1 and 3 set then we know that layer collides with the ‘Default’ and ‘Tile’ layer. For more information on why we assign the entries in the enum specific numbers have a look at the tutorial where we created the Collision system. We initialise the layer with it’s associated bitmask in the constructor of S_Collidable.
After the C_BoxCollider is added to the projectile, the colliders size is set to 32 * 32 pixels, which is larger than the arrow image but will help show the collision using the debug drawing system. As the name suggests a C_BoxCollider uses a box to check for collisions. We’re using a box collider for the arrow because it is relatively quick to check a collision between two squares (i.e. a tile and the box around the projectile).
The component stores a reference to the objects velocity component, we’ll use this to calculate the movement direction. It also stores the current FacingDirection in a variable called currentDir, which, unsurprisingly, stores the entities current direction. We do this because if the entity is not moving that frame and we cannot calculate its direction based on its velocity, we can pass this as its last known direction (this will become more clear shortly).
The component has an Awake function that we won’t be calling directly (for more information on for the Awake and Start functions work see here) but will be used to retrieve the velocity component from the object. So the only public function that we will call is ‘Get’ which returns a FacingDirection.
The hash object is a functor object, which is an object that defines the () operator and can be treated as if they are a function. If we use them in the core code for the game I’ll go through it in more detail.
Using the image above we know that four directions and there corresponding texture locations are:
- Up = 0, 0, 64, 64
- Left = 64, 0, 64, 64
- Down = 128, 0, 64, 64
- Right = 192, 0, 64, 64
And now we can use the rectangle returned from the unordered_map to specify which sprite we want to use.
Creating a shared context involves a number of steps:
- Writing the shared context and adding references to the classes we want shared.
- Ensuring every object has access to the shared context.
- Removing any local references that components currently store and instead using the references in the shared context.
- Removing any setters/mutators from components for classes that are now included in the shared context.
- Input: we want any component to be able to query the keyboard state.
- ObjectCollection: we want any component to be able to add other objects to the game, such as our projectile attack component creating new arrow objects.
- WorkingDirectory: to retrieve anything from the disk we first need to retrieve the location of the asset.
- TextureAllocator: some of our components (like our projectile attack) can add textures to objects.
- Window: any component that acts as a camera or interacts with the player’s view will require access to the Window class.
Now that we’ve decided which classes we want to include we can write the SharedContext class.
Because we’ve made numerous small changes to the SceneGame::OnCreate function over the course of this tutorial you may want to refer to the implementation in the Github repo to make sure that your code matches mine.
I’ve created a sprite sheet consisting of four images. Each image shows an arrow pointing in a different direction. In future, we can use the one image and rotate it to suit our needs but this will do for now. The sprites I used are included in this week’s resources/LPC/Weapons folder in the Github repo.
- Creates a new object.
- Sets it position equal to the player’s position using the transform component that is automatically added to all new objects.
- Adds a sprite component and loads the arrow texture.
- Adds the newly created object to the object collection so it is updated and drawn the window.
Instead of just checking if the ‘e’ key is pressed we now perform two checks:
- When it is first pressed we start the projectile animation.
- When the key is released we transition to the idle state.
You may have noticed that the player can still move while ‘shooting’ a projectile. Well, they can move while playing the projectile animation as we are yet to create the actual projectiles. For now, we want the player to be stationary while shooting, although this design decision may change as we playtest the game. To stop movement while shooting we’ll update C_KeyboardMovement by re-adding a reference to the animation component that we removed when we created the velocity component.