Level and Basic Movement
In this part, you’re going to implement loading of the level (including the tiled background) as well as the basic movement of Link.
TiledBGComponent
First, you will implement a new component that given a tile map texture and a CSV file will draw the tiles as specified. This will use the tile mapping technique discussed in lecture. Since this is drawing images on screen, you want this new component to inherit from SpriteComponent
.
The basic setup of TiledBGComponent
is:
- The constructor should take in the exact same parameters as
SpriteComponent
does (owning actor and draw order). - Set the default draw order to 50 instead of 100. You can set this in the declaration of the constructor
- You will need to override the
Draw
function fromSpriteComponent
. For now, just copy the code inside ofSpriteComponent::Draw
and put it insideTiledBGComponent::Draw
. - You will need some new member variables:
int
s for the tile width and tile height- A
std::vector<std::vector<int>>
to store the integers from the CSV file
Now add a new public function to TiledBGComponent
:
void LoadTileCSV(const std::string& fileName, int tileWidth, int tileHeight);
- This function should open the CSV file as specified by the
fileName
parameter, and then populate your vector of vectors correctly - To help parsing the CSV, we’ve given you a
CSVHelper::Split
function declared inCSVHelper.h
. This function takes in comma-separated string and returns a vector of strings. So, you could read in a line from the CSV, and then call theSplit
function on it to get a vector of the separate strings from that line. Note that since your member data contains integers, you will need to convert them usingstd::stoi
- The very last line of the file is empty, so don’t try to split an empty line
Now in Game::LoadData
, create an Actor
that has a TiledBGComponent
. Call LoadTileCSV
on it, passing in the "Assets/Map/Tiles.csv"
file and a tile width/height of 32
.
Verify that after you call LoadTileCSV
, the TiledBGComponent
’s member data matches what’s in the CSV file you loaded. You should have 64 rows with each row having 96 columns.
Now you need to make the TiledBGComponent
draw the tiles. You may find the lecture slides on tile mapping useful for this part.
- The code you copied from
SpriteComponent::Draw
just setsSDL_Rect r
, which is the destination rectangle, and draws just a single image by callingSDL_RenderCopyEx
- Remember for tile mapping, you will need to draw an image per square in the grid. For each square you draw in the grid, you’ll need to calculate both the correct destination rectangle as well as the correct source rectangle
- If the CSV data has a -1 at a specific (row, col), that means there’s no image to draw at that location. Otherwise, the number corresponds to the tile number from the source texture
- For the x/y of the source rectangle, you need to figure out which (x,y) from the source texture corresponds to the top left corner of the requested tile using the equations discussed in class
- You can get the overall tile’s texture width with
GetTexWidth()
- For the destination rectangle
r
:- Don’t worry about the owner position for the destination rectangle
- Don’t subtract half the width/height like
SpriteComponent
did, because we want the x/y coordinates here to correspond to the top left of the tile - Don’t forget to subtract the camera position
- For both the source and destination rectangles, the width/height is just the tile width/height (don’t worry about the owner scale)
- Be sure to change
SDL_RenderCopyEx
call to use the source rectangle and a rotation of 0, as in the sample code in the slides
Now in Game::LoadData
, set the texture of the TiledBGComponent
you created to "Assets/Map/Tiles.png"
.
If your tile map drawing works properly, you will see the top left corner of the overworld map, which will look like this:
If your tiles look all jumbled up, the most common cause of this is that you swapped which component corresponds to the row and which corresponds to the column. Remember the x-value increases with each column while the y-value increases with each row.
Loading the Player
In this game, we also use CSV files to specify the positions and properties of objects in each room of the dungeon. Open "Assets/Map/ObjectsOneSoldier.csv"
to understand the format of the CSV file. The first row is just a header. Each subsequent row corresponds to a specific object. Every row minimally has an x
, y
, width
, and height
. There are additional columns used by the Soldier
, but don’t worry about those for now.
The position of actors in the level is NOT not simply the x
and y
values from the CSV. This is because in Tiled, the positions are the top left corner of the object, but our game code assumes that Actor
’s positions are in the center of the object. To fix this, you need to add half of the width
and height
to the x
/y
values, and use that position for any actor you create.
You need to add code to game that can read in the objects CSV file, we’d suggest making a separate function in Game
and call this function in LoadData
, loading in the "Assets/Map/ObjectsOneSoldier.csv"
level. For now, just worry about the type "Player"
. If you see that type, then create an instance of the Player
. Don’t forget to set Game’s member variable for the player when you create it.
Loading the Animations
Since there are a lot of different animations for Link/other characters in this game, manually loading them all would be very annoying. We’re going to add a function that can load all the animations for a character given a root path where the animations exist. The code assumes that each animation is in a subdirectory from this root path.
First, in AnimatedSprite.cpp add #include
directives for <filesystem>
and <algorithm>
.
Next, add the following member function to your AnimatedSprite
implementation:
void AnimatedSprite::LoadAnimations(const std::string& rootPath)
{
#ifndef __clang_analyzer__
Game* game = mOwner->GetGame();
for (const auto& rootDirEntry : std::filesystem::directory_iterator{rootPath})
{
// If this is a directory, it's a multi-frame animation so need to load all the frames
if (rootDirEntry.is_directory())
{
// Load the file names into the vector
std::vector<std::string> fileNames;
for (const auto& animDirEntry : std::filesystem::directory_iterator{rootDirEntry})
{
if (animDirEntry.path().extension().string() == ".png")
{
fileNames.emplace_back(animDirEntry.path().string());
}
}
// Technically the order is undefined, so sort just in case
std::sort(fileNames.begin(), fileNames.end());
// Now load all the textures
std::vector<SDL_Texture*> images;
for (const auto& file : fileNames)
{
images.emplace_back(game->GetTexture(file));
}
// Now add the animation using the directory name as the animation name
AddAnimation(rootDirEntry.path().filename().string(), images);
}
// Non-directory means single-frame animation
else if (rootDirEntry.path().extension().string() == ".png")
{
std::vector<SDL_Texture*> images;
images.emplace_back(game->GetTexture(rootDirEntry.path().string()));
AddAnimation(rootDirEntry.path().stem().string(), images);
}
}
#endif
}
Next, in the Player constructor call LoadAnimations
on the AnimatedSprite
you create, passing in the "Assets/Link"
directory into it. Set the animation to "StandDown"
initially.
Fixing the Camera
For the camera position, add code to end of PlayerMove::Update
that sets the camera position to the player’s position plus an offset of (-256.0f, -224.0f)
. You don’t need any code that restricts the position of the camera. (This does mean that sometimes you’ll see empty space on the edge of the screen, but that’s expected).
Link should now spawn at the starting position and face down, with the camera centered on him, which looks like this:
Player Movement
In this game, the player can walk up, down, left, or right on screen by holding down the corresponding WASD key. The player cannot move diagonally or in more than one direction at once.
- Add code to
PlayerMove
so that the player can move as described Hint: We recommend making a direction vector as well as an enum to track which direction the player is facing - If multiple WASD keys are pressed, you can just move in any one of the pressed directions (you don’t need to “cancel out” the movement as in prior games)
- Regardless of the direction, the player should move 150 pixels/second
Next, make it so the player plays the correct animation based on both the direction the player is facing and whether the player is actively moving or not:
- When moving, the animation should be one of
"WalkUp"
,"WalkDown"
,"WalkLeft"
, or"WalkRight"
- When the player stops moving, they should switch to one of the animations based on the last direction they were walking in:
"StandUp"
,"StandDown"
,"StandLeft"
, or"StandRight"
. For example, if Link was walking towards the left, when Link stops moving, the animation should switch to"StandLeft"
Link should now be able to walk around and play the correct animations based on the direction of travel:
Colliders
First, in Player.cpp, change the size of the Player
’s collision component to 20x20. This is because if you leave it at 32x32, it will be too difficult to walk through paths areas that are one tile wide.
Now make a new Actor
subclass called Collider
. It needs:
- A
CollisionComponent
- Make the constructor take in a width/height as additional parameters, and set the
CollisionComponent
’s width/height to these parameters. This is needed because different colliders will have different dimensions.
Back in Game
, make a private std::vector
of Collider*
and a public getter function that returns this vector by const
reference.
Now in the level loading code, you need to handle type "Collider"
. Unsurprisingly, you need to make a Collider
object for each entry of that type.
- You’ll want to pass in the width/height from the CSV into the
Collider
constructor - When you set the position of the
Collider
, make sure you’re using the adjusted position and not just the x/y components directly from the CSV file - Add it to your vector of
Collider*
Now in PlayerMove::Update
, use the GetMinOverlap
function to make sure that the player can’t go through any Collider
s. You will need to use the offset regardless of the side the player overlaps with.
Link should no longer be able to walk through the solid objects in the level:
Loading Other Objects
Last, we have two more object types to load. The player won’t interact with these objects yet, but we’ll at least load them in.
First, make a new Actor
subclass called Bush
. It needs:
- A
SpriteComponent
using the texture"Assets/Bush.png"
- A
CollisionComponent
with a size of 32x32
Next, make a new Actor
subclass called Soldier
. It needs:
- An
AnimatedSprite
- Call
LoadAnimations
with the path"Assets/Soldier"
- Set the initial animation to
"WalkDown"
- Call
SetAnimFPS(5.0f)
, because we’ll want the soldier animations to play more slowly
- Call
- A
CollisionComponent
with a size of 32x32
Finally, update your level loading code so it correctly creates these two classes based on type "Bush"
or "Soldier"
, respectively.
Confirm that you see a single soldier near the start and also see the bushes. For now, you will be able to walk through them. It will look like this:
Once you’ve pushed this code, you’re ready to move on to part 2.