Asteroids and Shooting
Now we’ll add asteroids moving around and the ability to shoot lasers at them.
Asteroid Basics
Create a new actor subclass called Asteroid
. In the constructor, create the following components:
- A
SpriteComponent
using the"Assets/Asteroid.png"
texture - A
MoveComponent
with a forward speed that you like - A
WrapComponent
We want to create asteroids with random positions and orientations so that they are different each time. To help with this, we have provided a basic random library in Random.h/cpp. To initialize this library, at the start of Game::Initialize
, add a call to Random::Init()
.
All the Random functions are static
, so you don’t need to create an instance of Random
to use it.
Then, back in the Asteroid
constructor, initialize the rotation and position to random values:
- Set the rotation to a
Random::GetFloatRange
between0.0f
andMath::TwoPi
. - Set the position to a
Random::GetVector
. This takes in amin
andmax
vector. In this case case, you wantmin
/max
to correspond to the coordinates for the top left corner and bottom right corners of the window, respectively.
Now in Game::LoadData
, create 10 asteroids (make a loop for this).
You should now have 10 asteroids flying around the screen, looking roughly like this:
Laser
Make a new subclass of Actor
called Laser
. It needs:
- A
SpriteComponent
using"Assets/Laser.png"
- A
MoveComponent
with a forward speed of something you like (should be faster than the ship by a decent amount) - A
WrapComponent
Creating Actors from Another Actor
We want to add code to Ship::HandleInput
that says when you press the space bar, create a Laser
with the same position/rotation as the ship.
There’s a problem here, though. In Game::ProcessInput
we are looping over the actor vector. If, while we are looping over it, we call CreateActor
we are going to add another actor to the vector. However, this could crash because it’s not safe to add to a vector while looping over it. The same thing could happen if we create an actor in HandleUpdate
.
To fix this, add a second std::vector<Actor*>
to game for the “pending create” actors. These are actors that have been newly created.
Then, change CreateActor
so it adds the new actor to “pending create” actors instead of just the regular actor vector.
Then, in Game::UpdateGame
, right before you loop over the actor vector, loop over “pending create” actors and add all of them to the regular actor vector. Then clear pending actors. This effectively will “move” all the actors from the pending actors to the regular actors.
Let’s Make Lasers
Now in Ship::HandleInput
create a Laser
actor when the spacebar is pressed. Set the position of the laser to the ship’s position, and the rotation of the laser to the ship’s rotation.
Now if you hold down the space bar, you should be able to create a ridiculous number of lasers:
Laser Limiting
We want to make it so that you can only shoot one laser per second. To do this:
- Add a
float
variable toShip
to track the “cooldown” of the laser, and initialize it to0.0f
- When you’re deciding whether to fire the laser, only fire the laser if the the cooldown is
<= 0.0f
. If you do fire a laser, set the cooldown to1.0f
- In
Ship
, add an override ofHandleUpdate
and reduce the cooldown by delta time - Thus, when you fire a laser, you won’t be able to fire another one for a second
You should only be able to fire one laser per second:
Adding Support for Destroying Actors
We don’t want every actor to live forever. For example, we want a laser to get destroyed after either it hits an asteroid or 1.5 seconds elapse.
Like with creating actors, we have to be careful not mess with the actor vector while looping over it.
First, in Game
:
- Add a
std::vector<Actor*>
toGame
for actors that are “pending destroy” - Add a public function called
AddPendingDestroy
that takes in anActor*
and adds it to the “pending destroy” vector
Then, in Actor
:
- Add a
bool
for if the actor “is active.” Initialized it totrue
and add a getter/setter for this:- The getter should be named
IsActive
- The setter should be named
SetIsActive
- The getter should be named
- Change both
Update
andInput
so they do nothing if “is active” is false - Add a public
Destroy
function which:- Sets “is active” to false
- Calls
AddPendingDestroy
function you added toGame
to add this actor to the “pending destroy” vector
Back in Game
:
- Add a private function called
DestroyActor
which takes in anActor*
and:- Removes the actor from the regular actors vector (you can use
std::erase
) - Calls
delete
on the pointer
- Removes the actor from the regular actors vector (you can use
- Then, at the end of
UpdateGame
, loop over the “pending destroy” vector, callingDestroyActor
on each actor in it. Then clear the “pending destroy vector”
Ok that seems a bit convoluted, so let’s take a step back to understand:
- When you want to destroy an actor in your logic, simply call
Destroy()
on it. That’s it. - How’s it work?
- When you call
Destroy
, it sets “is active” to false, which makes sure it won’t update/receive input anymore - It also adds it to “pending destroy” actors in game
- Then when update game reaches the end, the “pending destroy” actors are removed from the regular actor vector and deleted
- When you call
Laser Lifetime
Now make it so that lasers die after being alive for 1.5 seconds.
You’ll need to add a member variable to track the lifetime and an override of HandleUpdate
. And when it’s time to destroy itself, call Destroy
.
Now test it out! You should get lasers disappearing now.
Lasers Colliding with Asteroids
Now we’ll make it so that lasers can destroy asteroids. We haven’t talked about more complex collision yet, so we’ll keep it simple for now.
First, we need a way to track all the asteroids in the game world. So, add a private std::vector<class Asteroid*>
to Game
, and corresponding AddAsteroid
/RemoveAsteroid
functions. Then, make the Asteroid
constructor and destructor call the appropriate functions.
You also need to make a GetAsteroids()
function that returns the asteroid vector by const
reference, like:
const std::vector<class Asteroid*>& GetAsteroids() const;
It’s very important that we use the reference here, as it avoids making a very expensive copy.
Then in Laser::HandleUpdate
, you’ll need to loop over the asteroid vector. Use Vector2::Distance
to calculate the distance between the position of each asteroid and the position of the laser. If this distance is <= 50, then call Destroy
on both the itself and the asteroid, and break out of the loop.
You should now be able to shoot at and destroy asteroids, and it’ll play something like the reference version.
Once you’ve pushed your code, you should review the grading specifications to confirm you’ve satisfied them.