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

Theme: auto

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 from SpriteComponent. For now, just copy the code inside of SpriteComponent::Draw and put it inside TiledBGComponent::Draw.
  • You will need some new member variables:
    • ints 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 in CSVHelper.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 the Split 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 using std::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 sets SDL_Rect r, which is the destination rectangle, and draws just a single image by calling SDL_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: TileBGComponent rendering

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: Link spawned in

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 Colliders. 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
  • 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: Loading bush/soldier

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