31 views
Routa Platformer Tutorial (Part 2/2) === [TOC] So, in the previous part we got our player moving per our input and got it to be able to take and deal damage. There's still a lot to do before we can call this project a platformer I'd say.. Let's continue: What does a platforming game still need in the game mechanics side of things? * Levels * Enemies * Animations * Shooting * Win condition * Lose condition Where to start.. Let's just go through these in order: ### Levels (tilemaps, Tiled Editor) How can one make levels for Routa-powered games? There's a couple of ways you could accomplish this: * Create every platform within code * Results in possibly thousands of lines of codes for a single level, not the best idea * Using an image and only attaching colliders by hand * This still results in a whole lot of work and code so not the best idea again * Using ```re::TiledComponent``` to draw tilemaps created in [Tiled Editor](https://www.mapeditor.org/) * The fastest way to get nice results and somewhat trivial to do all the colliders automatically So it's a no-brainer. We are going with the ```re::TiledComponent``` -way of doing things. There's a couple of things to note about ```re::TiledComponent``` though: #### Using ```re::TiledComponent``` What is the component doing: * Reads the ```.json``` file that Tiled Editor outputs * The file contains every tiles size, position and image to use for said tile What the component is lacking in functionality: * Doesn't support layers so you can only use a single layer (no background/foreground separation) * Always draws on the bottom-most layer (no background images) * Doesn't expose single tiles or give access to modifying them dynamically (level is always static) What you should know about using it: * The tile sheet used needs to be: * Power of two (2x2, 4x4, 8x8, 16x16, 32x32, 64x64 and so on in dimensions) * Square, like noted above. (2x4, 16x32, 128x32 *not supported*) * Tiled Editor settings need to be as follows: * Fixed Size: power of two as noted above * Tile render order: Right-Up * I am actually not 100% sure about this. This is stated in the component documentation but my levels work just fine with Left-Down * Tileset: Embedded in map * Tile width/height settings according to your sprite/tilesheet * 256x256 sheet with 4 different tiles = 128 width, 128 height * New json.dll plugin needs to changed to deprecated json1.dll * The component only supports the old json format from json1.dll * You need to change the exported level jsons line endings from normal Windows CR+LF to LF * Easiest way to do this is opening the .json after exporting in Notepad++, going to the bottom right and right-clicking the line ending indicator to change them to LF: ![](https://gitlab.dclabra.fi/wiki/uploads/upload_ca7bdd235a776c76902e076cb717073f.png) * Remember to save the file after this! That out of the way I'll show the workflow for creating a level: #### Creating a level in Tiled Everything starts with a sprite sheet to make a tileset out of: ##### Spritesheet/Tileset We need a sprite sheet to use in our level. You can find some in the Routa examples but I created my own Inca-tileset from [this spritesheet](https://opengameart.org/content/inca-tileset) by cutting it up in [GIMP](https://www.gimp.org/) and patching it back together in square dimensions: ![](https://gitlab.dclabra.fi/wiki/uploads/upload_4114b7f6ea6ebd3894a06f01c52aa582.png) inca_tileset.png The resulting size became 176 x 176 and it has got 11 rows and 11 sprites per row so a single tile is 176/11 x 176/11 = 16 x 16 pixels. Keep this in mind. Notice those four empty tiles in the first row? That's what I'm using as spawn points in my level. The player is spawned on the tile where the first empty tile is used, the goal is spawned where the second empty tile is used and I have two types of enemies that are spawned where the third and fourth empty tiles are placed. To make them visible in Tiled I also created this helper image: ![](https://gitlab.dclabra.fi/wiki/uploads/upload_926b6501d87292b46f979be18653449b.png) inca_tileset_helpers.png Now I can use this image in the editor and then just manually switch to the one where the helper tiles are empty to make them invisible in-game. Now let's boot up Tiled and create a simple level. ##### Mapping in Tiled Opening Tiled we are greeted with the choice to create a New Map, a New Tileset or opening existing ones. First of all, we are going to the preferences to set the json.dll plugin to the deprecated json1.dll plugin. Click on Edit on the top-left, click on preferences, go to Plugins-tab and uncheck json.dll and check json1.dll: ![](https://gitlab.dclabra.fi/wiki/uploads/upload_1332be04c12cabfda39fcc59d88ecb27.png) Click on New Map and insert your values there. Remember, power of two, square and the tiles need to be set to size in the sprite sheet: ![](https://gitlab.dclabra.fi/wiki/uploads/upload_be0f958c6515dbe530160542966a87d3.png) I'm using Left-Down contrary to the component documentation as it works fine for me. I set the map size to be 64 tiles x 64 tiles (square) and set the tile size to 16 x 16 pixels like we calculated beforehand. Then click Save As... and give your map file a name and change the file format to JSON map files \[Tiled 1.1] (\*.json): ![](https://gitlab.dclabra.fi/wiki/uploads/upload_8f67dd21c41986be7eb646cc6a6e5ed9.png) I saved the level under ```/levels```. I already have some other levels there, you will be creating a completely new folder. Click Save and you are greeted with an empty map grid. Find the "New Tileset" button on the bottom right, change the dropdown to "Based on Tileset image", make sure "Embed in map" is checked, browse to your sprite sheet with the helpers and make sure the tile dimensions are still correct: ![](https://gitlab.dclabra.fi/wiki/uploads/upload_14e799c98095b03423058917107b18cd.png) Click OK and your tileset will appear where the "New Tileset" button was: ![](https://gitlab.dclabra.fi/wiki/uploads/upload_2ecd074b742d5a1e88b237d7732dee10.png) Now you can start painting the tiles in your map grid by selecting it in this view and then just clicking. You can also drag-select multiple tiles at once to paint bigger objects in a single click. I created a simple level: ![](https://gitlab.dclabra.fi/wiki/uploads/upload_fedc2081dd9e52201966729941e044c0.png) Next place the player spawn tile, the goal spawn tile and enemy spawn tiles: ![](https://gitlab.dclabra.fi/wiki/uploads/upload_064894dd140897e4f6eba777c251ef23.png) Next click "File" from the top left, click Export As.. and overwrite your previous json (Mine was ```levels/my_level.json```). Make sure the format is still \[Tiled 1.1] (\*.json) as we set before. Okay, mapping out of the way we can close Tiled now. ##### Modifying the exported level for ```re::TiledComponent``` Now we find the level in our folder and open it in Notepad++. We have to find the sprite sheet variable there and set it to the one without helpers visible and change the line endings to Unix (LF) from Windows (CR+LF): ![](https://gitlab.dclabra.fi/wiki/uploads/upload_c96baf35fd29d52239a3dadfaef3edb5.png) Here I changed the image variable from ```"image":"..\/..\/platformer_tutorial\/images\/inca_tileset_helpers.png",``` to ```"image":"..\/..\/platformer_tutorial\/images\/inca_tileset.png",``` as those were my filenames. Next we have to load the level in our game: #### Loading created levels Now we have to use ```re::TiledComponent``` to read the json file and draw our level in the scene. To use ```re::TiledComponent``` we need to attach it to an entity like any other component and then call it's Init-method that expects two parameters: * filepath * pointer to graphics system This is just to draw the level. We are still going to need colliders for the tiles to make the level actually playable. The component only cares about drawing the level. It knows the position and layer of the tiles and which sprite to use on each tile so we can leverage this information to create the colliders ourselves. We'll start with creating a tile data container data structure: ```cpp /* MapTile (Class) * Holds a single tiles information. Used to access singular tiles in the map. * This is only used by the Level class and can be used in your scene to access tile information in a neat package. * NOTE: Routas TiledReader doesn't support moving individual tiles so your tiles will be static. */ #ifndef MAPTILE_H #define MAPTILE_H #include <core/Routa.h> #include <core/ECS_collection.h> class MapTile { public: MapTile(re::Vector3 _position, int _layer, int _tileID) { m_position = _position; m_layer = _layer; m_tileID = _tileID; m_collider = nullptr; } MapTile(re::Vector3 _position, int _layer, int _tileID, int _row, int _column) { m_position = _position; m_layer = _layer; m_tileID = _tileID; m_collider = nullptr; m_row = _row; m_column = _column; } ~MapTile() {}; // re::Vector3 m_position; int m_layer; int m_tileID; re::BoxColliderComponent* m_collider = nullptr; int m_row; int m_column; private: }; #endif ``` MapTile.h Here we have simple class that holds said data: tile position, tile layer, tile ID (which sprite to use). We also give it data on which row and column it belongs to to help optimizing the colliders later on. Then there's the BoxCollider pointer as to assign a collider to *this* tile. When we are optimizing colliders, we are actually using the same collider for multiple tiles. There's no .cpp implementation for this class since we are just using the constructor to create a tile and all the members are public so we don't need methods to modify them. This class is quite simply just a data container without any functionality. Next we are going to create a level loader. The level loader will initialize the ```re::TiledComponent```, create and hold ```MapTile``` data and create colliders based on this data. It also makes sense to keep references to the enemies, player and goal here to make coding easier. What will our level loader do then? * Draw the level using ```re::TiledComponent``` * Construct ```MapTile```s from level data * Create and attach colliders to ```MapTile```s * Spawn the player * Spawn the enemies * Spawn the goal * Detach old components and destroy old entities when loading a new level This is what I came up with, it can be quite a bit to ingest: ```cpp /* Level Loader (Class) * Provides functionality to load a level from a tilemap created using Tiled Editor with the help of * Routas TiledReader. TiledReader only loads the sprites and this class attaches colliders to them * and enables special functionality to specific tiles (eg. goal or spawn points set in the tilemap). * Set the index variables to match your tilemap configuration, create an instance of this class in * your scene and call LoadLevel(filepath) from there. * You can use the different MapTile getters to search for specific types of tiles outside of this class. */ #ifndef LEVEL_H #define LEVEL_H #include <core/Routa.h> #include <core/ECS_collection.h> #include "MapTile.h" #include "Enemy.h" class Player; class Level { public: struct LevelProps { int width; // columns int height; // rows bool isCaveLevel; // used for choosing background re::Vector2 backgroundOffset; // offset background position by this }; Level(re::GraphicsSystem* graphics, re::Scene* scene) { m_graphicsSystem = graphics; m_scene = scene; m_levelEntity = nullptr; m_goal = nullptr; //m_backgroundImage = nullptr; } ~Level() { } bool LoadLevel(std::string filepath, re::Scene* scene, float positionOffset); bool LoadLevel(std::string filepath, re::Scene* scene, float positionOffset, LevelProps props); /** Return tiles that have something in them (tileID != 0) */ std::vector<MapTile*> GetFilledTilesOnly(); std::vector<MapTile*> GetPlayerSpawnTileOnly(); std::vector<MapTile*> GetEnemySpawnTilesOnly(); /** Spawns goal to spawnpoint (tileID == goalSpawnTileIndex) and offset position. Returns spawned goals collider. */ re::CircleTriggerComponent* SpawnGoal(re::Vector2 offset); /** Spawn player to spawnpoint (tileID == playerSpawnTileIndex) and offset position. Returns spawned Player*. */ Player* SpawnPlayer(re::Vector3 offset); /** Spawns type of typeToSpawn to spawnpoints (tileID == enemySpawnTileIndex[typeToSpawn]) and offset position. Returns a vector with all spawned Entities. */ std::vector<re::Entity*> SpawnEnemies(re::Vector2 offset, Enemy::EnemyType typeToSpawn); std::vector<re::Entity*> GetEnemies() { return m_enemies; } private: enum TILETYPE{ PLAYER, ENEMY, PLATFORM, ALL }; re::Scene* m_scene; float m_positionOffset; re::Entity* m_levelEntity = nullptr; std::vector<MapTile*> m_mapTiles; /// Every tile in map std::vector<MapTile*> m_platformTiles; /// Every tile that is not empty or spawn (tileID > enemySpawnTileIndex) std::vector<MapTile*> m_enemyTiles; /// Every tile that has an enemy spawn std::vector<MapTile*> m_playerTiles; /// Every tile that has an player spawn //re::Entity* m_backgroundImage; re::Entity* m_goal = nullptr; std::vector<re::Entity*> m_enemies; float m_enemy_one_health = 20.0f; float m_enemy_two_health = 50.0f; Player* m_player = nullptr; re::Entity* m_player_entity = nullptr; //Player* m_player = nullptr; re::BoxColliderComponent* m_player_collider; re::TiledComponent* m_mapComponent; /// TiledReader, draws the Tilemap. re::GraphicsSystem* m_graphicsSystem; /// Reference to enable drawing (sprites) int m_playerSpawnTileIndex = 1; /// Tile index for player spawn, tile indexes start from 1 (0 = empty tile) int m_goalSpawnTileIndex = 2; /// Tile index for goal spawn /** Tile index for enemy spawns, tile indexes start from 1. MUST BE IN ORDER where last element here is last special tile index (populated tile search starts from last index in list) */ std::vector<int> m_enemySpawnTileIndexes = { 3, 4 }; LevelProps m_currentProperties; //void //_CreateBackground(bool isCaveLevel); /** Without optimized colliders */ void _ConstructTiles(re::Entity* sceneRoot); /** With optimized colliders */ void _ConstructTiles(); void _ConstructOptimizedColliders(); std::vector<MapTile*> _GetTilesByType(TILETYPE typeToConstruct); /** Init map component and loads the actual tilemap */ void _LoadTiles(std::string filepath); /** Detaches components from all entities spawned from this class and despawns said entities, clears data containers. Called on level load */ void _Cleanup(); }; #endif ``` Level.h I am [forward declaring](https://stackoverflow.com/a/4757718) ```Player```. This class will be created in the following chapter where we also create the enemies. I created a ```LevelProps``` data structure so we can have different sized maps with relative ease. The constructor is giving the class a pointer to the scene and graphics system and setting entities as ```nullptr```. Then there's the public ```LoadLevel(...)``` method to load a level expecting the filepath and scene to load in. I also gave it the possibility to offset the whole level by a vector. Next comes some public helper methods to get specific ```std::vector```s of ```MapTile```s: * Tiles that are not empty * Used for creating colliders * Tiles that contain the player spawn * Used for spawning the player * Tiles that contain the enemy spawns * Used for spawning the enemies These are made public so they can be used elsewhere in the code if needed. ```SpawnGoal(...)``` spawns the goal and returns the goal trigger pointer to easily store it for later use. I also gave it the possibility of offsetting the spawn position since every character has it's own size, it could spawn inside another collider otherwise. ```SpawnPlayer(...)``` is the same thing but returning a pointer to the ```Player``` class instead. ```SpawnEnemies(...)``` is again straight-forward but it has a parameter for choosing which type of enemy to spawn. We currently have those two types of enemies supported (enemy spawn 1 and enemy spawn 2 in our map). It returns a list of all the enemies spawned as pointers. The last public method ```GetEnemies()``` is just for getting all the enemies in the level. On the private side on the other hand: * ```m_scene```, ```m_graphicsSystem``` * Pointers grabbed from the constructor * ```m_positionOffset``` * Grabbed from the constructor * ```m_mapTiles```, ```m_platformTiles```, ```m_enemyTiles```, ```m_playerTiles``` * ```std::vector``` collections of ```MapTile*```s with every tile, every filled tile, every enemy spawn tile and every player spawn tile, respectively * ```m_level_entity``` * ```re::TiledComponent``` container with its position offset by ```m_positionOffset``` * ```m_currentProperties``` * ```LevelProps``` used for the current level * ```m_playerSpawnTileIndex```, ```m_goalSpawnTileIndex```, ```m_enemySpawnTileIndexes``` * ```MapTile``` ```tileID``` specifiers. 0 is an empty tile, 1 is the first tile (player spawn) in the tilesheet and so on. Determines which tile(s) to spawn the player, the goal and the enemies to * ```ConstructTiles(...)``` * Constructs ```MapTile```s from ```re::TiledComponent```s map data * Creates colliders for the tiles * Two versions * Creates colliders for each individual tile or * Creates horizontally connected colliders for connected tiles (optimization) * ```_GetTilesByType(...)``` * Basically an another way of ```Get"X"TilesOnly()``` * Why not use the previous ones? That's a great question. * ```_LoadTiles(...)``` * Attaches ```re::TiledComponent``` to the level entity and loads the tilemap * ```_Cleanup()``` * Detaches components and despawns entities created by the level loader to get ready for the next level So for you visual learners, the procedure is as follows: ![](https://gitlab.dclabra.fi/wiki/uploads/upload_4ab656cf202623539991a3ef2d5edcda.png) The implementation is as follows: ```cpp #include "Level.h" #include "utility/tiledreader/TiledReader.h" #include "gameDefs.h" #include <algorithm> #include <iterator> #include "EnemyController.h" #include "Player.h" #include "HealthComponent.h" #include "graphics/SpriteAnimationComponent.h" #include "Enemy.h" bool Level::LoadLevel(std::string filepath, re::Scene* scene, float positionOffset) { _Cleanup(); re::Entity* root = m_scene->GetRoot(); m_levelEntity = root->SpawnChild(); m_positionOffset = positionOffset; m_levelEntity->SetPosition({ m_positionOffset,m_positionOffset, 0.0f }); _LoadTiles(filepath); _ConstructTiles(root); m_platformTiles = _GetTilesByType(TILETYPE::PLATFORM); m_playerTiles = _GetTilesByType(TILETYPE::PLAYER); m_enemyTiles = _GetTilesByType(TILETYPE::ENEMY); return true; } bool Level::LoadLevel(std::string filepath, re::Scene* scene, float positionOffset, LevelProps props) { _Cleanup(); m_currentProperties = props; re::Entity* root = m_scene->GetRoot(); m_levelEntity = root->SpawnChild(); m_positionOffset = positionOffset; m_levelEntity->SetPosition({ m_positionOffset,m_positionOffset, 10.0f }); _LoadTiles(filepath); _ConstructTiles(); _ConstructOptimizedColliders(); m_platformTiles = _GetTilesByType(TILETYPE::PLATFORM); m_playerTiles = _GetTilesByType(TILETYPE::PLAYER); m_enemyTiles = _GetTilesByType(TILETYPE::ENEMY); return true; } void Level::_LoadTiles(std::string filepath) { m_mapComponent = static_cast<re::TiledComponent*>(m_levelEntity->AttachComponent("TiledComponent")); m_mapComponent->Init(filepath.c_str(), m_graphicsSystem); } std::vector<MapTile*> Level::GetFilledTilesOnly() { std::vector<MapTile*> populatedTiles; int minTileIndex = m_enemySpawnTileIndexes.back(); // Get last index of enemy spawns to compare to for (int i = 0; i < m_mapTiles.size(); i++) { if (m_mapTiles[i]->m_tileID > minTileIndex) { populatedTiles.emplace_back(m_mapTiles[i]); } } return populatedTiles; } std::vector<MapTile*> Level::GetPlayerSpawnTileOnly() { std::vector<MapTile*> playerTile; for (int i = 0; i < m_mapTiles.size(); i++) { if (m_mapTiles[i]->m_tileID == m_playerSpawnTileIndex) { playerTile.emplace_back(m_mapTiles[i]); } } return playerTile; } std::vector<MapTile*> Level::GetEnemySpawnTilesOnly() { std::vector<MapTile*> enemyTiles; for (int i = 0; i < m_mapTiles.size(); i++) { bool exists = std::any_of(std::begin(m_enemySpawnTileIndexes), std::end(m_enemySpawnTileIndexes), [&](int x) { return x == m_mapTiles[i]->m_tileID; } ); // if tileindex is found in enemyindexes array if (exists) { enemyTiles.emplace_back(m_mapTiles[i]); } } return enemyTiles; } re::CircleTriggerComponent* Level::SpawnGoal(re::Vector2 offset) { re::Vector3 spawnPosition; bool exists = false; for (int i = 0; i < m_mapTiles.size(); i++) { if (m_mapTiles[i]->m_tileID == m_goalSpawnTileIndex){ exists = true; spawnPosition = m_mapTiles[i]->m_position; break; } } if (!exists) { return NULL; //TODO: Proper error checking (ex. what to return..?) } m_goal = m_scene->GetRoot()->SpawnChild(); // Create child entity of scene root entity assert(m_goal); spawnPosition.x += m_positionOffset; spawnPosition.y += m_positionOffset; spawnPosition.x += offset.x; spawnPosition.y += offset.y; m_goal->SetPosition(spawnPosition); auto goal_sprite = static_cast<re::SpriteComponent*>( m_goal->AttachComponent("SpriteComponent")); goal_sprite->Init( re::Vector3(0.0f, 0.0f, 1.0f), //Offset, z-value is for layering re::Vector2(4.0f, 4.0f), //Width and height re::Vector4(0.0f, 0.0f, 1.0f, 1.0f),//Texture coords re::ColourRGBA8(255, 255, 255, 255),//Colour m_graphicsSystem->LoadImage("images/goal_portal.png") ); auto goal_animation = static_cast<re::SpriteAnimationComponent*>( m_goal->AttachComponent("SpriteAnimationComponent")); // Create/Add animation clips to animation component goal_animation->Init(3, 3, goal_sprite->GetTexture().width, goal_sprite->GetTexture().height); goal_animation->AddClip("idle", 0, 9, 0.1f); goal_animation->Play("idle"); auto goal_coll = static_cast<re::CircleTriggerComponent*>( m_goal->AttachComponent("CircleTriggerComponent")); goal_coll->SetType(re::RigidBody::BodyType::StaticBody); goal_coll->SetColliderFilter(ColliderCategories::COLLIDER_GOAL, ColliderCategories::COLLIDER_PLAYER); return goal_coll; } Player * Level::SpawnPlayer(re::Vector3 offset) { re::Vector3 spawnPosition = { m_playerTiles[0]->m_position.x, m_playerTiles[0]->m_position.y, 0.0f }; spawnPosition += offset; //m_player = static_cast<Player*>(m_scene->GetRoot()->AttachComponent("Player")); //m_player->Init(spawnPosition, "images/duke_nukem2.png"); //m_player_entity = m_player->GetParentEntity(); Player* m_player = static_cast<Player*>(m_scene->GetRoot()->SpawnChild()->AttachComponent("Player")); m_player->Init(spawnPosition, "images/duke_nukem2.png"); m_player_entity = m_player->GetParentEntity(); return m_player; } std::vector<re::Entity*> Level::SpawnEnemies(re::Vector2 offset, Enemy::EnemyType typeToSpawn) { //Find where to spawn to and how many std::vector<MapTile*> tilesToSpawnTo; // Check the type to spawn is valid (starts from 1, ends to amount of enemy types) if (typeToSpawn <= m_enemySpawnTileIndexes.size() && typeToSpawn > 0) { for (MapTile* e : m_enemyTiles) { if (e->m_tileID == m_enemySpawnTileIndexes.at(typeToSpawn - 1)) { // If enemyTile ID matches type to spawn (-1 since type 1 would be at index 0), increment amount tilesToSpawnTo.emplace_back(e); } } } // If valid spawn tiles were found from tilemap, continue with spawning if (tilesToSpawnTo.size() > 0) { //Spawn enemies and store to m_enemies for (int i = 0; i < tilesToSpawnTo.size(); i++) { switch (typeToSpawn) { case Enemy::EnemyType::BLUE_GUARD: { Enemy* enemy = static_cast<Enemy*>(m_scene->GetRoot()->SpawnChild()->AttachComponent("Enemy")); enemy->Init({ tilesToSpawnTo[i]->m_position.x + 1.0f, tilesToSpawnTo[i]->m_position.y + 1.0f, 0.0f }, Enemy::EnemyType::BLUE_GUARD); m_enemies.emplace_back(enemy->GetParentEntity()); break; } case Enemy::EnemyType::TURRET: { Enemy* enemy = static_cast<Enemy*>(m_scene->GetRoot()->SpawnChild()->AttachComponent("Enemy")); enemy->Init({ tilesToSpawnTo[i]->m_position.x + 1.0f, tilesToSpawnTo[i]->m_position.y + 1.0f, 0.0f }, Enemy::EnemyType::TURRET); m_enemies.emplace_back(enemy->GetParentEntity()); break; } } //!switch } //!for } //!if amountToSpawn.. //All enemies of type spawned (or none spawned), return reference to m_enemies return m_enemies; } void Level::_Cleanup() { if (m_levelEntity != NULL && m_levelEntity != nullptr) { if (m_levelEntity->IsSpawned()) { if (m_levelEntity->HasComponent("TiledComponent")) { m_levelEntity->DetachComponent("TiledComponent"); } m_levelEntity->Despawn(); } } if (m_platformTiles.size() > 0) { for (auto tile : m_platformTiles) { if (tile->m_collider != nullptr) { if (tile->m_collider->GetEntity()->HasComponent("BoxColliderComponent")) { tile->m_collider->GetEntity()->DetachComponent("BoxColliderComponent"); } } } m_platformTiles.clear(); m_platformTiles.shrink_to_fit(); // Memory leak debugging, probably unnecessary } if (m_enemyTiles.size() > 0) { m_enemyTiles.clear(); m_enemyTiles.shrink_to_fit(); // Memory leak debugging, probably unnecessary } if (m_playerTiles.size() > 0) { m_playerTiles.clear(); m_playerTiles.shrink_to_fit(); // Memory leak debugging, probably unnecessary } if (m_enemies.size() > 0) { for (auto enemy : m_enemies) { if (enemy != nullptr && enemy != NULL && !enemy->IsDeleted()) { enemy->DetachComponent("SpriteComponent"); // RoutaSpace detaches components before despawning enemy->DetachComponent("BoxColliderComponent"); enemy->DetachComponent("EnemyController"); enemy->DetachComponent("MovementComponent"); enemy->Despawn(); } } m_enemies.clear(); m_enemies.shrink_to_fit(); // Memory leak debugging, probably unnecessary } if (m_mapTiles.size() > 0) { // "Actual tiles" for (auto tile : m_mapTiles) { delete tile; } m_mapTiles.clear(); m_mapTiles.shrink_to_fit(); // Memory leak debugging, probably unnecessary } if (m_player_entity != nullptr && m_player_entity != NULL) { m_player_entity->DetachComponent("SpriteComponent"); // RoutaSpace detaches components before despawning m_player_entity->DetachComponent("BoxColliderComponent"); m_player_entity->DetachComponent("Camera2D"); m_player_entity->DetachComponent("PlayerController"); m_player_entity->DetachComponent("MovementComponent"); m_player_entity->DetachComponent("SpriteAnimationComponent"); m_player_entity->DetachComponent("HealthComponent"); m_player_entity->DetachComponent("ShootingComponent"); m_player_entity->Despawn(); } if (m_goal != nullptr && m_goal != NULL) { m_goal->DetachComponent("SpriteComponent"); // RoutaSpace detaches components before despawning m_goal->DetachComponent("SpriteAnimationComponent"); m_goal->DetachComponent("CircleColliderComponent"); m_goal->Despawn(); } } void Level::_ConstructTiles(re::Entity* sceneRoot) { std::vector<re::Vector4> mapData = m_mapComponent->GetMapData(false); //vector4 is for (position x, position y, layer, tileID) false = get all empty tiles too for (int i = 0; i < mapData.size(); i++) { re::Vector3 tilePosition = re::Vector3(mapData[i].x, mapData[i].y, 0.0f); int tileLayer = mapData[i].z; int tileID = mapData[i].w; MapTile* tile = new MapTile(tilePosition, tileLayer, tileID); m_mapTiles.emplace_back(tile); if (tileID > m_enemySpawnTileIndexes.back()) { // if "platform"-tile auto collider = static_cast<re::BoxColliderComponent*>( sceneRoot->SpawnChild()->AttachComponent("BoxColliderComponent")); collider->GetEntity()->SetPosition( re::Vector3( tilePosition.x + m_positionOffset + 0.5f, tilePosition.y + m_positionOffset + 0.5f, 0 ) ); collider->SetType(re::RigidBody::BodyType::StaticBody); collider->SetRestitution(0.f); collider->SetFriction(0.f); tile->m_collider = collider; } } } void Level::_ConstructOptimizedColliders() { // MapTiles are created, loop over them again to find connected tiles and encapsulate them with a BoxCollider int rows = m_currentProperties.height; int columns = m_currentProperties.width; //"Connect" colliders horizontally int indexStart = m_enemySpawnTileIndexes.back(); // Every tile ID after this is a platform tile std::vector<std::vector<MapTile*>> setsToMerge; std::vector<MapTile*> setOfTiles; bool listIsDone = false; int currentRow = 1; for (int j = 0; j < m_mapTiles.size(); j++) { MapTile* firstTileToMerge = nullptr; if (listIsDone) { // Okay, got all tiles for this set to merge if (setOfTiles.size() > 0) { // Don't add empty lists setsToMerge.emplace_back(setOfTiles); } setOfTiles.clear(); // Clear vector state for the next set listIsDone = false; } // Is it on the same row if (m_mapTiles[j]->m_row == currentRow) { // It is on the same row, is it a platform if (m_mapTiles[j]->m_tileID > indexStart) { // It is a platform, add to list setOfTiles.emplace_back(m_mapTiles[j]); } else { // It's not a platform, list is done listIsDone = true; } } else { // Not on the same row, increment row currentRow++; j--; // We must check the first tile of the next row again! listIsDone = true; } if (j == m_mapTiles.size() - 1) { // If we reached the end of the tilemap if (setOfTiles.size() > 0) { // And there were tiles on the last row setsToMerge.emplace_back(setOfTiles); } } } // All sets to merge have been found, create the actual colliders for (int set = 0; set < setsToMerge.size(); set++) { // Go through all sets to merge (in current row) int colliderWidth = setsToMerge[set].size(); auto collider = static_cast<re::BoxColliderComponent*>(m_scene->GetRoot()->SpawnChild()->AttachComponent("BoxColliderComponent")); collider->SetType(re::RigidBody::BodyType::StaticBody); collider->SetRestitution(0.f); collider->SetFriction(0.f); re::Vector3 centerPosition = setsToMerge[set][0]->m_position + re::Vector3( (setsToMerge[set].size() / 2.0f) - 0.5f, 0.0f, 0.0f ); collider->GetEntity()->SetPosition({ centerPosition.x + 0.5f, centerPosition.y + 0.5f, centerPosition.z }); collider->SetSize({ colliderWidth, 1.0f, 0.0f }); collider->SetColliderFilter(ColliderCategories::COLLIDER_TILE, 0xFFFF); for (auto tile : setsToMerge[set]) { tile->m_collider = collider; } } } std::vector<MapTile*> Level::_GetTilesByType(TILETYPE typeToConstruct) { std::vector<MapTile*> tiles; switch (typeToConstruct) { case TILETYPE::ALL: { tiles = m_mapTiles; break; } case TILETYPE::PLAYER: { for (int i = 0; i < m_mapTiles.size(); i++) { if (m_mapTiles[i]->m_tileID == m_playerSpawnTileIndex) { // if index is player spawn id tiles.emplace_back(m_mapTiles[i]); } } break; } case TILETYPE::ENEMY: { for (int i = 0; i < m_mapTiles.size(); i++) { bool exists = std::any_of(std::begin(m_enemySpawnTileIndexes), std::end(m_enemySpawnTileIndexes), [&](int spawnIndex) { return spawnIndex == m_mapTiles[i]->m_tileID; } ); if (exists) { // if id exists in enemy id array tiles.emplace_back(m_mapTiles[i]); } } break; } case TILETYPE::PLATFORM: { for (int i = 0; i < m_mapTiles.size(); i++) { if (m_mapTiles[i]->m_tileID > m_enemySpawnTileIndexes.back()) { // if id is larger than last enemy id tiles.emplace_back(m_mapTiles[i]); } } break; } } // end switch return tiles; } void Level::_ConstructTiles() { re::Entity* sceneRoot = m_scene->GetRoot(); std::vector<re::Vector4> mapData = m_mapComponent->GetMapData(false); //vector4 is for (position x, position y, layer, tileID) false = get all empty tiles too int i = 0; for (int row = 1; row <= m_currentProperties.height; row++) { for (int column = 1; column <= m_currentProperties.width; column++) { re::Vector3 tilePosition = re::Vector3(mapData[i].x, mapData[i].y, 0.0f); int tileLayer = mapData[i].z; int tileID = mapData[i].w; MapTile* tile = new MapTile(tilePosition, tileLayer, tileID,row,column); m_mapTiles.emplace_back(tile); i++; } } } ``` That's quite a bit to ingest. Feel free to debug it and see what is happening under the hood. You can see that I've included some new things: * ```#include "utility/tiledreader/TiledReader.h"``` * ```re::TiledComponent``` is here * ```#include "gameDefs.h"``` * This file contains collider categories used to filter collisions which we'll get into a bit later * It also contains enemy properties which we'll get into later also * ```#include <algorithm>``` and ```#include <iterator>``` * These are used for collider optimization helpers like ```std::any_of``` * ```#include "EnemyController.h"``` * This contains the enemy AI logic which we'll get into in the next chapter * ```#include "Player.h"``` and ```#include "Enemy.h``` * These are the ```Player``` and ```Enemy``` components we'll get into in the next chapter * ```#include "graphics/SpriteAnimationComponent.h"``` * This is used for animations which we'll get into later * It's used for animating the goal spawned in the level loader. It's also used in the ```Player``` and ```Enemy``` components Now we have to create those missing headers and functionality I've introduced. Let's start with enemies: ### Enemies (AI, character abstraction) Enemies can be created in many ways. We can use our ```Health```, ```Damage``` and ```MovementComponent``` components we have created before with our enemies. How modular! We can't use ```PlayerController``` though, obviously. So how do we make the enemies move? We create our own ```EnemyController``` of course! #### AI (```EnemyController```) What does our ```EnemyController``` need? Let's think for a second: * Comparing to ```PlayerController``` we won't be using keyboard input * We create a simple AI logic pattern instead * The AI logic pattern will need some AI states * ```IDLE```, ```ACTION```, ```ALERT``` are probably enough * Pretty self explanatory, ```IDLE``` when doing nothing and ```ACTION``` when doing something * I'm trying to clone [Duke Nukem II](https://en.wikipedia.org/wiki/Duke_Nukem_II) AI for example purposes so I'm going to use ```ALERT``` also. Duke Nukems blue guards get alerted when the player is in range before they start shooting (```ACTION```) * We will be using this same component for different types of enemies so we are going to need AI types * ```BLUEGUARD``` and ```TURRET``` from Duke Nukem are our two different types (we are currently supporting two enemy types) * I'll also include some types I created when testing: * ```LEFTRIGHT``` that moves left and right only * ```LEFTRIGHTJUMPING``` that moves left and right while jumping (think [angry Spelunky Shopkeeper](https://spelunky.fandom.com/wiki/Shopkeeper/HD)) * ```ONLYJUMPING``` that you guessed it, only keeps jumping So this is what I came up with: ```cpp #ifndef ENEMY_CONTROLLER_H #define ENEMY_CONTROLLER_H #include <core/Routa.h> #include <core/ECS_collection.h> #include "MovementComponent.h" #include "ShootingComponent.h" #include "PlayerController.h" // Change to player soon class EnemyController : public re::Component { enum AIState { IDLE, ACTION, ALERT }; public: EnemyController() {}; virtual ~EnemyController() {}; void Init( int type, float actionTime, float idleTime = 0.f, float minIdleTime = -1.f, float alertDistance = 10.f, float ticksBtwnShots = 60 ); virtual void Tick(double dt) override; bool IsLookingLeft(); enum AIType { LEFTRIGHT, LEFTRIGHTJUMPING, ONLYJUMPING, BLUEGUARD, TURRET }; private: int m_type = 0; int m_action = 1; int m_actionCount; float m_actionTime; float m_idleTime; float m_minIdleTime; bool m_randomIdle = false; double m_curTimer; int m_state = IDLE; int m_stateBefore; float m_alertDistance; int m_tickCount = 0; int m_shootingTickCount = 0; int m_tickLimitAction; int m_tickLimitIdle; int m_ticksBtwnShots; int m_tickLimitMinIdle; MovementComponent* movementComponent; ShootingComponent* shootingComponent; bool m_facingLeft = false; re::Vector2 m_lastDirection = { 0.f,0.f }; }; #endif ``` EnemyController.h I'm also holding references to the entities ```MovementComponent``` and ```ShootingComponent``` for a bit more readable code. I also included the same ```IsLookingLeft()``` method we saw before in the ```PlayerController```. We are going to use this to flip the sprite to face the correct way depending on facing direction. The implementation looks like this: ```cpp #include "EnemyController.h" #include "Enemy.h" // if minIdleTime is set to > 0 idle time is random void EnemyController::Init( int type, float actionTime, float idleTime, float minIdleTime, float alertDistance, float ticksBtwnShots ) { this->m_type = type; this->m_actionTime = actionTime; this->m_idleTime = idleTime; this->m_minIdleTime = minIdleTime; if (type == ONLYJUMPING) m_actionCount = 1; else m_actionCount = 2; this->m_tickLimitAction = actionTime * 60; this->m_tickLimitIdle = idleTime * 60; this->m_tickLimitMinIdle = minIdleTime * 60; this->m_alertDistance = alertDistance; this->m_ticksBtwnShots = ticksBtwnShots; if (minIdleTime >= 0) m_randomIdle = true; movementComponent = static_cast<MovementComponent*>(this->GetEntity()->GetComponent("MovementComponent")); shootingComponent = static_cast<ShootingComponent*>(this->GetEntity()->GetComponent("ShootingComponent")); } void EnemyController::Tick(double dt) { m_curTimer -= dt; if(m_shootingTickCount > 0) m_shootingTickCount--; re::ArrayPool<PlayerController>* playerControllers = this->GetSystem()->GetComponents<PlayerController>("PlayerController"); for (PlayerController& player : (*playerControllers)) { re::Vector3 playerpos = player.GetEntity()->GetPosition(); re::Vector3 enemypos = this->GetEntity()->GetPosition(); float distance = re::Distance(enemypos, playerpos); if (distance < m_alertDistance && (m_type == BLUEGUARD || m_type == TURRET)) { if(m_state != ALERT) m_stateBefore = m_state; m_state = ALERT; re::Vector3 direc = playerpos - enemypos; re::Vector2 lookDir(0); if (abs(direc.y) > 2.f && m_type != TURRET) { lookDir.y = direc.y / abs(direc.y); // 1 or -1 } if (lookDir.y != 0 && abs(direc.x) < 1.f) { lookDir.x = 0; } else { lookDir.x = direc.x / abs(direc.x); } if (shootingComponent && m_shootingTickCount <= 0) { // Change animation state if (GetEntity()->HasComponent("Enemy")) { auto enemy = static_cast<Enemy*>(GetEntity()->GetComponent("Enemy")); enemy->SetState(PlatformerCharacter::MovementState::SHOOT); } shootingComponent->Shoot(lookDir); m_shootingTickCount = m_ticksBtwnShots; } } // Continue with previous state after player leaves the alert distance else if (m_state == ALERT) { m_state = m_stateBefore; } } //std::cout << "CURRENT TIMER: " << curTimer << std::endl; if (m_state == ACTION && m_tickCount >= m_tickLimitAction) { m_state = IDLE; m_curTimer = m_idleTime; m_tickCount = 0; // Random between min and max if (m_randomIdle) { m_tickLimitIdle = rand() % (int)(m_idleTime * 60) + m_tickLimitMinIdle; } } if (m_state == IDLE && m_tickCount >= m_tickLimitIdle) { m_state = ACTION; m_action++; if (m_action >= m_actionCount) m_action = 0; m_tickCount = 0; m_curTimer = m_actionTime; } if(m_state != ALERT) m_tickCount++; re::Vector2 direction = re::Vector2(0, 0); if (m_state == ACTION) { switch (m_type) { case LEFTRIGHTJUMPING: { direction.y = 1; } case BLUEGUARD: case LEFTRIGHT: { if (m_action == 1) direction.x = -1; else direction.x = 1; break; } case ONLYJUMPING: { direction.y = 1; break; } }; } if (direction.x == -1 || m_lastDirection.x == -1 && direction.x != 1) { m_facingLeft = true; } else { m_facingLeft = false; } if (direction.x != 0) { m_lastDirection = direction; } movementComponent->Move(direction); } bool EnemyController::IsLookingLeft() { return m_facingLeft; } ``` EnemyController.cpp Again, feel free to debug if you want to know what's happening under the hood. It's quite a bit to ingest and explain but it's one way to do this. You can do whatever kind of AI you'd like of course :) The important part is that I have created some logic which determines the movement direction we are sending to the ```MovementComponent``` instead of reading keyboard input. You can also see that I've used that ```Enemy.h``` here again. Next let's see what it actually is: #### Character abstraction (```PlatformerCharacter```, ```Player```, ```Enemy```) We have no ```Player``` or ```Enemy``` classes yet and have just been testing our creations straight in the scene code. When you stop to think about our enemies and players and what components they (will) use, you realize that we are using mostly all the same components and functionality for both types of characters. So let's abstract them. Let's gather all the components shared by both and give them to a ```PlatformerCharacter``` base class and derive our ```Player``` and ```Enemy``` classes from that which will then have their specific components. Both types of game characters use these components: * ```re::SpriteComponent``` * ```re::SpriteAnimationComponent``` * ```re::BoxColliderComponent``` * ```MovementComponent``` * ```HealthComponent``` Then the player also needs: * ```PlayerController``` While the enemies also need: * ```EnemyController``` So if we make a ```PlatformerCharacter``` have the topmost components and then a derived ```Player``` have only the ```PlayerController``` and a derived ```Enemy``` have only the ```EnemyController``` we can cut down quite a bit on duplicate code. So let's do that next! For you visual learners, this is what our class hierarchy will look like: ![](https://gitlab.dclabra.fi/wiki/uploads/upload_86d90ff0f95f3f8e0c5c9a6ef14f7ff5.png) ##### ```PlatformerCharacter``` Let's start with our base class, ```PlatformerCharacter```: ```cpp #ifndef PLATFORMERCHARACTER_H #define PLATFORMERCHARACTER_H #include <core/Routa.h> #include <core/ECS_collection.h> class MovementComponent; class HealthComponent; namespace re { class SpriteAnimationComponent; } class PlatformerCharacter : public re::Component { public: enum CharacterType { PLAYER, BLUE_GUARD, TURRET }; enum MovementState { STAND, WALK, JUMP, FALL, SHOOT }; PlatformerCharacter(); virtual ~PlatformerCharacter() {}; void Init(re::Vector3 spawnPosition, std::string spriteFilepath); virtual void Tick(double dt) override; re::Entity* GetParentEntity(); re::SpriteComponent* GetSpriteComponent(); re::BoxColliderComponent* GetCollider(); re::SpriteAnimationComponent* GetSpriteAnimation(); MovementComponent* GetMovement(); HealthComponent* GetHealth(); MovementState GetState(); void SetState(MovementState newState); PlatformerCharacter::CharacterType GetCharacterType(); void SetCharacterType(PlatformerCharacter::CharacterType newType); private: re::Entity* m_my_entity; /// Reference to parent entity (spawned character) re::SpriteComponent* m_sprite; re::BoxColliderComponent* m_collider; re::SpriteAnimationComponent* m_animation; MovementComponent* m_movement; HealthComponent* m_health; virtual void _SetupAnimations() = 0; MovementState m_state; CharacterType m_type; }; #endif // !CHARACTER_H ``` PlatformerCharacter.h Here we have all the shared components and getters for them. I made them private so they cannot accidentally be set to something else. I also introduced a ```MovementState``` which is used for determining which animation to play and a ```CharacterType``` to determine what the character actually is. There's also a pure virtual method: ```virtual void _SetupAnimations() = 0```. This needs to be implemented in the deriving classes (```Player``` and ```Enemy```) and is used in this base classes constructor. So in essence I'm forcing animations to be set up in the deriving classes to make sure everything displays correctly. > Also note that the class derived publicly from ```re::Component``` so we can attach the derived classes to entities (you cannot derive from ```re::Entity``` to create custom entities, only custom components or systems) The implementation is as follows: ```cpp #include "PlatformerCharacter.h" #include "gameDefs.h" #include "MovementComponent.h" #include <graphics/SpriteAnimationComponent.h> #include "HealthComponent.h" PlatformerCharacter::PlatformerCharacter() : m_my_entity(nullptr), m_sprite(nullptr), m_collider(nullptr), m_animation(nullptr), m_movement(nullptr), m_health(nullptr) { } void PlatformerCharacter::Init(re::Vector3 spawnPosition, std::string spriteFilepath) { /** Attach and initialize components here, this gets called on player spawn */ // Create the player entity for functionality as a child of the scene entity //m_my_entity = GetScene()->GetRoot()->SpawnChild(); m_my_entity = GetEntity(); assert(m_my_entity); // Set player position to desired spawn position and set its scale m_my_entity->SetPosition(spawnPosition); // Attach a sprite component for visuals m_sprite = static_cast<re::SpriteComponent*>( m_my_entity->AttachComponent("SpriteComponent")); m_sprite->Init( re::Vector3(0.0f, 0.0f, 0.0f), //Offset, z-value is for layering re::Vector2(1.0f, 1.0f), //Width and height re::Vector4(0.0f, 0.0f, 1.0f, 1.0f),//Texture coords re::ColourRGBA8(255, 255, 255, 255),//Colour static_cast<re::GraphicsSystem*>(GetScene()->GetSystem("GraphicsSystem")) ->LoadImage(spriteFilepath.c_str()) ); // Attach a box collider component for enabling physics and collisions and set collider parameters m_collider = static_cast<re::BoxColliderComponent*>( m_my_entity->AttachComponent("BoxColliderComponent")); // Attach a movement component for movement. Set movement parameters m_movement = static_cast<MovementComponent*>( m_my_entity->AttachComponent("MovementComponent")); // Attach health component for taking damage and dying m_health = static_cast<HealthComponent*>(m_my_entity->AttachComponent("HealthComponent")); // Attach a sprite animation component for sprite sheet animation m_animation = static_cast<re::SpriteAnimationComponent*>( m_my_entity->AttachComponent("SpriteAnimationComponent")); assert(m_animation != nullptr); // Set up animations _SetupAnimations(); } void PlatformerCharacter::Tick(double dt) { } re::Entity * PlatformerCharacter::GetParentEntity() { return m_my_entity; } re::SpriteComponent * PlatformerCharacter::GetSpriteComponent() { return m_sprite; } re::BoxColliderComponent * PlatformerCharacter::GetCollider() { return m_collider; } re::SpriteAnimationComponent * PlatformerCharacter::GetSpriteAnimation() { return m_animation; } MovementComponent * PlatformerCharacter::GetMovement() { return m_movement; } HealthComponent * PlatformerCharacter::GetHealth() { return m_health; } PlatformerCharacter::MovementState PlatformerCharacter::GetState() { return m_state; } void PlatformerCharacter::SetState(MovementState newState) { m_state = newState; } PlatformerCharacter::CharacterType PlatformerCharacter::GetCharacterType() { return m_type; } void PlatformerCharacter::SetCharacterType(PlatformerCharacter::CharacterType newType) { m_type = newType; } ``` PlatformerCharacter.cpp Here we are simply attaching all the components in the header file to the spawned entity and then setting up animations, which will be done in the deriving class(es). Always remember to set uninitialized members as ```nullptr``` to keep Routa from panicing! Now that we have a base class to derive from. Let's design the deriving classes: ##### ```Player``` We only need to add ```PlayerController``` to make our derived class as desired but I decided to also include ```re::Camera2D``` in the ```Player```-component since the camera will always be focusing to the player. My header looks like this: ```cpp #ifndef PLAYER_H #define PLAYER_H #include "PlatformerCharacter.h" #include <core/Routa.h> #include <core/ECS_collection.h> class PlayerController; class ShootingComponent; class Player : public PlatformerCharacter { public: Player(); virtual ~Player() {}; void Init(re::Vector3 spawnPosition, std::string spriteFilepath); virtual void Tick(double dt) override; re::Camera2D* GetCamera(); PlayerController* GetController(); ShootingComponent* GetShooting(); private: re::Camera2D* m_camera; PlayerController* m_controller; ShootingComponent* m_shooting; float m_startingHealth = 100.0f; void _SetupAnimations(); void _CheckPlayerState(); // for animations void _AnimateByPlayerState(); // play animation according to current m_state }; #endif // !PLAYER_H ``` Player.h I'm introducing ```ShootingComponent``` here which we'll get into later. I'm also introducing two private methods: ```_CheckPlayerState()``` and ```_AnimateByPlayerState()``` which will be used for animations. The former will deduce if the player is moving and if they are, which way, setting the ```m_state``` in base ```PlatformerCharacter```. The latter will use this information to play the correct animation depending on the current state. Those ```Init(...)``` parameters will be used to initialize the base class with a spawn position and a sprite. To the implementation: ```cpp #include "Player.h" #include "PlayerController.h" #include "MovementComponent.h" #include "HealthComponent.h" #include "ShootingComponent.h" #include "gameDefs.h" #include <graphics/SpriteAnimationComponent.h> Player::Player() : m_camera(nullptr), m_controller(nullptr), m_shooting(nullptr) { } void Player::Init(re::Vector3 spawnPosition, std::string spriteFilepath) { PlatformerCharacter::Init(spawnPosition, spriteFilepath); SetCharacterType(PlatformerCharacter::CharacterType::PLAYER); GetParentEntity()->SetScale({ 2.0f,2.0f,2.0f }); GetParentEntity()->SetPosition(spawnPosition); float colliderHorizontalScaleMultiplier = 0.8f; GetCollider()->SetSize( { GetParentEntity()->GetScale().x * colliderHorizontalScaleMultiplier, GetParentEntity()->GetScale().y, GetParentEntity()->GetScale().z } ); GetCollider()->SetMass(200); GetCollider()->SetRestitution(0.f); // how bouncy the player is GetCollider()->SetColliderFilter(ColliderCategories::COLLIDER_PLAYER, 0xFFFF); // Attach a player controller component for reading input m_controller = static_cast<PlayerController*>( GetParentEntity()->AttachComponent("PlayerController")); GetMovement()->Init(13, 40, 13, 1000, false, true); // Attach a camera component for rendering. Set scale and rotation TODO: SHOULD THIS BE IN SCENE/LEVEL ? m_camera = static_cast<re::Camera2D*>( GetParentEntity()->AttachComponent("Camera2D")); m_camera->Init( 8, // ?? 8 // ?? ); m_camera->SetScale(0.6f); m_camera->SetRotation(0.0f); // Attach a shooting component for shooting mechanics and set shooting parameters m_shooting = static_cast<ShootingComponent*>( GetParentEntity()->AttachComponent("ShootingComponent") ); m_shooting->Init(1.f, 1.5f); GetHealth()->Init(m_startingHealth); } void Player::Tick(double dt) { m_camera->SetPos(re::Vector3( GetParentEntity()->GetPosition().x, GetParentEntity()->GetPosition().y, 1 ) ); _CheckPlayerState(); // Change animations _AnimateByPlayerState(); // Animate } re::Camera2D * Player::GetCamera() { return m_camera; } PlayerController * Player::GetController() { return m_controller; } ShootingComponent * Player::GetShooting() { return m_shooting; } void Player::_SetupAnimations() { // Create/Add animation clips to animation component GetSpriteAnimation()->Init(3, 3, GetSpriteComponent()->GetTexture().width, GetSpriteComponent()->GetTexture().height); GetSpriteAnimation()->AddClip("walk", 0, 5, 0.1f); GetSpriteAnimation()->AddClip("jump", 6, 1, 0.3f); GetSpriteAnimation()->AddClip("fall", 7, 1, 0.3f); GetSpriteAnimation()->AddClip("stand", 0, 1, 0.1f); GetSpriteAnimation()->SetPlayType(re::PlayType::NORMAL); } void Player::_CheckPlayerState() { // Get current velocities and change player state accordingly // Jumping checks first since they should override walking animations (no "airwalk") if (GetCollider()->GetLinearVelocity().y > 1.f) // Jumping { SetState(JUMP); } else if (GetCollider()->GetLinearVelocity().y < -1.f) // Falling { SetState(FALL); } else if (abs(GetCollider()->GetLinearVelocity().x) > 1.f) // Moving { SetState(WALK); } else // Not moving { SetState(STAND); } } void Player::_AnimateByPlayerState() { switch (GetState()) { case STAND: GetSpriteAnimation()->Play("stand"); break; case WALK: GetSpriteAnimation()->Play("walk"); break; case JUMP: GetSpriteAnimation()->Play("jump"); break; case FALL: GetSpriteAnimation()->Play("fall"); break; default: break; } /* Flip player-sprites UV-coordinates based on the direction the player is facing */ auto texture = GetSpriteComponent()->GetUvs(); if (m_controller->IsLookingLeft()) // left { GetSpriteComponent()->SetUvs({ texture.z, texture.y, texture.x, texture.w }); } else // right { GetSpriteComponent()->SetUvs({ texture.x, texture.y, texture.z, texture.w }); } } ``` Player.cpp In the constructor, I'm setting the additional components to ```nullptr```. In the ```Init(...)``` **I'm calling the base classes** ```Init(...)``` **first** to set up the base components. Then I'm setting the character type (```m_type``` of ```PlatformerCharacter```) and initializing all the uninitialized base class components and this classes components to desired values for a player. I'm also attaching the ```Player```-specific components ```PlayerController``` and ```re::Camera2D``` to the parent entity (the entity in which the ```Player``` component is attached to). In the ```Tick(...)``` method I'm setting the camera position to the player position to have the camera follow the player. Here I'm also changing the animation state and animating. Don't worry, we'll get into animations after implementing ```Enemy```: ##### ```Enemy``` The ```Enemy``` component follows much the same procedure as the ```Player```. This time just without a camera and a ```EnemyController``` instead of a ```PlayerController```: ```cpp #ifndef ENEMY_H #define ENEMY_H #include "PlatformerCharacter.h" #include <core/Routa.h> #include <core/ECS_collection.h> class ShootingComponent; class EnemyController; class Enemy : public PlatformerCharacter { public: enum EnemyType { BORING, MANIAC, BLUE_GUARD, TURRET }; Enemy(); virtual ~Enemy() {}; void Init(re::Vector3 spawnPosition, EnemyType type); virtual void Tick(double dt) override; ShootingComponent* GetShooting(); EnemyController* GetController(); private: ShootingComponent* m_shooting; EnemyController* m_controller; void _SetupAnimations(); void _CheckEnemyState(); // for animations void _AnimateByEnemyState(); // play animation according to current m_state and flags }; #endif // !ENEMY_H ``` Enemy.h As you can see, very similar but a bit simpler. Now instead of a sprite path in the ```Init(...)``` I'm supplying the desired ```EnemyType```. These ```EnemyType```s are used to decide which sprite and other variables will be set to the components. These preferences are found in the ```gameDefs.h``` header: ```cpp #ifndef GAMEDEFS_H #define GAMEDEFS_H #include "EnemyController.h" enum ColliderCategories { COLLIDER_PLAYER = 0x02, COLLIDER_GOAL = 0x04, COLLIDER_BULLET = 0x08, COLLIDER_ENEMY = 0x10, COLLIDER_TILE = 0x20 }; namespace EnemyPrefs { struct EnemyOne { // Physics properties re::Vector3 colliderSize = { 1.f, 3.f, 0.f }; float friction = 0.f; float restitution = 0.f; float mass = 200.f; // Collision filter ColliderCategories colliderCategory = ColliderCategories::COLLIDER_ENEMY; // Movement properties float maxSpeed = 15.f; float acceleration = 200.f; float maxJumpHeight = 10.f; float jumpForce = 100.f; bool autoJump = true; bool glide = false; // AI properties EnemyController::AIType AItype = EnemyController::AIType::ONLYJUMPING; float actionTime = 0.5f; float idleTime = 0.f; // Health properties float health = 100.f; // Visuals std::string spritePath = "images/enemy1.png"; // Shooting float shootingForce = 30.0f; float shootingOffset = 1.5f; }; struct EnemyTwo { // Physics properties re::Vector3 colliderSize = { 1.f, 3.f, 0.f }; float friction = 0.f; float restitution = 0.f; float mass = 200.f; // Collision filter ColliderCategories colliderCategory = ColliderCategories::COLLIDER_ENEMY; // Movement properties float maxSpeed = 15.f; float acceleration = 200.f; float maxJumpHeight = 10.f; float jumpForce = 100.f; bool autoJump = true; bool glide = false; // AI properties EnemyController::AIType AItype = EnemyController::AIType::LEFTRIGHTJUMPING; float actionTime = 1.0f; float idleTime = 0.5f; // Health properties float health = 100.f; // Visuals std::string spritePath = "images/enemy1.png"; // Shooting float shootingForce = 30.0f; float shootingOffset = 1.5f; }; struct Blue_Guard { // Physics properties re::Vector3 colliderSize = { 1.f, 3.f, 0.f }; float colliderHorizontalScale = 0.8f; float friction = 0.f; float restitution = 0.f; float mass = 200.f; // Collision filter ColliderCategories colliderCategory = ColliderCategories::COLLIDER_ENEMY; // Movement properties float maxSpeed = 15.f; float acceleration = 200.f; float maxJumpHeight = 30.f; float jumpForce = 400.f; bool autoJump = true; bool glide = false; // AI properties EnemyController::AIType AItype = EnemyController::AIType::BLUEGUARD; float actionTime = 1.0f; float idleTime = 0.5f; // Health properties float health = 100.f; // Visuals std::string spritePath = "images/enemy1.png"; // Shooting float shootingForce = 30.0f; float shootingOffset = 1.5f; }; struct Turret { // Physics properties re::Vector3 colliderSize = { 1.f, 3.f, 0.f }; float colliderHorizontalScale = 0.8f; float friction = 0.f; float restitution = 0.f; float mass = 200.f; // Collision filter ColliderCategories colliderCategory = ColliderCategories::COLLIDER_ENEMY; // Movement properties float maxSpeed = 15.f; float acceleration = 200.f; float maxJumpHeight = 30.f; float jumpForce = 400.f; bool autoJump = true; bool glide = false; // AI properties EnemyController::AIType AItype = EnemyController::AIType::TURRET; float actionTime = 1.0f; float idleTime = 0.5f; // Health properties float health = 100.f; // Visuals std::string spritePath = "images/enemy1.png"; // Shooting float shootingForce = 30.0f; float shootingOffset = 1.5f; }; } //!namespace EnemyPrefs namespace BulletPrefs { struct DukeNukem { float damage = 50.0f; re::Vector3 colliderSize = { 0.4f, 0.1f, 0.0f }; ColliderCategories colliderCategory = ColliderCategories::COLLIDER_BULLET; std::string spritePath = "images/bullet.png"; float spriteWidth = 0.5f; float spriteHeight = 0.25f; // Shooting bool lockRotation = true; }; struct Blue_Guard { float damage = 1.0f; re::Vector3 colliderSize = { 0.4f, 0.1f, 0.0f }; ColliderCategories colliderCategory = ColliderCategories::COLLIDER_BULLET; std::string spritePath = "images/bullet.png"; float spriteWidth = 0.5f; float spriteHeight = 0.25f; // Shooting bool lockRotation = true; }; struct Turret { float damage = 1.0f; re::Vector3 colliderSize = { 0.4f, 0.1f, 0.0f }; ColliderCategories colliderCategory = ColliderCategories::COLLIDER_BULLET; std::string spritePath = "images/bullet.png"; float spriteWidth = 0.5f; float spriteHeight = 0.25f; // Shooting bool lockRotation = true; }; } //!namespace BulletPrefs #endif ``` gameDefs.h > Note: The ```ColliderCategories``` use hex values as per Box2Ds documentation All the relevant information is grabbed from that header. This is how it is grabbed: ```cpp #include "Enemy.h" #include "EnemyController.h" #include "MovementComponent.h" #include "HealthComponent.h" #include "ShootingComponent.h" #include "gameDefs.h" #include <graphics/SpriteAnimationComponent.h> Enemy::Enemy() : m_shooting(nullptr) { } void Enemy::Init(re::Vector3 spawnPosition, EnemyType type) { switch (type) { case EnemyType::BORING: { EnemyPrefs::EnemyOne prefs; PlatformerCharacter::Init(spawnPosition, prefs.spritePath); GetParentEntity()->SetScale({ 2.0f,2.0f,2.0f }); //GetParentEntity()->SetPosition(spawnPosition); auto coll = GetCollider(); coll->SetSize(prefs.colliderSize); coll->SetFriction(prefs.friction); coll->SetRestitution(prefs.restitution); coll->SetColliderFilter(prefs.colliderCategory, 0xFFFF); auto movement = GetMovement(); movement->Init(prefs.maxSpeed, prefs.acceleration, prefs.maxJumpHeight, prefs.jumpForce, prefs.autoJump, prefs.glide); m_controller = static_cast<EnemyController*>(GetParentEntity()->AttachComponent("EnemyController")); m_controller->Init(prefs.AItype, prefs.actionTime, prefs.idleTime); auto health = GetHealth(); health->Init(prefs.health); break; } case EnemyType::MANIAC: { EnemyPrefs::EnemyTwo prefs; PlatformerCharacter::Init(spawnPosition, prefs.spritePath); GetParentEntity()->SetScale({ 2.0f,2.0f,2.0f }); //GetParentEntity()->SetPosition(spawnPosition); auto coll = GetCollider(); coll->SetSize(prefs.colliderSize); coll->SetFriction(prefs.friction); coll->SetRestitution(prefs.restitution); coll->SetColliderFilter(prefs.colliderCategory, 0xFFFF); auto movement = GetMovement(); movement->Init(prefs.maxSpeed, prefs.acceleration, prefs.maxJumpHeight, prefs.jumpForce, prefs.autoJump, prefs.glide); m_controller = static_cast<EnemyController*>(GetParentEntity()->AttachComponent("EnemyController")); m_controller->Init(prefs.AItype, prefs.actionTime, prefs.idleTime); auto health = GetHealth(); health->Init(prefs.health); break; } case EnemyType::BLUE_GUARD: { EnemyPrefs::Blue_Guard prefs; PlatformerCharacter::Init(spawnPosition, prefs.spritePath); SetCharacterType(PlatformerCharacter::CharacterType::BLUE_GUARD); GetParentEntity()->SetScale({ 2.0f,2.0f,2.0f }); //GetParentEntity()->SetPosition(spawnPosition); auto coll = GetCollider(); //coll->SetSize(prefs.colliderSize); coll->SetSize( { GetParentEntity()->GetScale().x * prefs.colliderHorizontalScale, GetParentEntity()->GetScale().y, GetParentEntity()->GetScale().z } ); coll->SetFriction(prefs.friction); coll->SetRestitution(prefs.restitution); coll->SetColliderFilter(prefs.colliderCategory, 0xFFFF); // Stop enemies from falling over etc. coll->LockRotation(true); // Attach movement and shooting components before attaching EnemyController as it grabs references on it's Init() auto movement = GetMovement(); movement->Init(prefs.maxSpeed, prefs.acceleration, prefs.maxJumpHeight, prefs.jumpForce, prefs.autoJump, prefs.glide); auto shooting = static_cast<ShootingComponent*>(GetParentEntity()->AttachComponent("ShootingComponent")); shooting->Init(prefs.shootingForce, prefs.shootingOffset); m_controller = static_cast<EnemyController*>(GetParentEntity()->AttachComponent("EnemyController")); m_controller->Init(prefs.AItype, prefs.actionTime, prefs.idleTime); auto health = GetHealth(); health->Init(prefs.health); break; } case EnemyType::TURRET: { EnemyPrefs::Turret prefs; PlatformerCharacter::Init(spawnPosition, prefs.spritePath); SetCharacterType(PlatformerCharacter::CharacterType::TURRET); GetParentEntity()->SetScale({ 2.0f,2.0f,2.0f }); //GetParentEntity()->SetPosition(spawnPosition); auto coll = GetCollider(); //coll->SetSize(prefs.colliderSize); coll->SetSize( { GetParentEntity()->GetScale().x * prefs.colliderHorizontalScale, GetParentEntity()->GetScale().y, GetParentEntity()->GetScale().z } ); coll->SetFriction(prefs.friction); coll->SetRestitution(prefs.restitution); coll->SetColliderFilter(prefs.colliderCategory, 0xFFFF); // Stop enemies from falling over etc. coll->LockRotation(true); // Attach movement and shooting components before attaching EnemyController as it grabs references on it's Init() auto movement = GetMovement(); movement->Init(prefs.maxSpeed, prefs.acceleration, prefs.maxJumpHeight, prefs.jumpForce, prefs.autoJump, prefs.glide); auto shooting = static_cast<ShootingComponent*>(GetParentEntity()->AttachComponent("ShootingComponent")); shooting->Init(prefs.shootingForce, prefs.shootingOffset); m_controller = static_cast<EnemyController*>(GetParentEntity()->AttachComponent("EnemyController")); m_controller->Init(prefs.AItype, prefs.actionTime, prefs.idleTime); auto health = GetHealth(); health->Init(prefs.health); break; } } } void Enemy::Tick(double dt) { _CheckEnemyState(); // Change animations _AnimateByEnemyState(); // Animate } ShootingComponent * Enemy::GetShooting() { return m_shooting; } EnemyController * Enemy::GetController() { return m_controller; } void Enemy::_SetupAnimations() { // Create/Add animation clips to animation component GetSpriteAnimation()->Init(3, 3, GetSpriteComponent()->GetTexture().width, GetSpriteComponent()->GetTexture().height); GetSpriteAnimation()->AddClip("walk", 1, 4, 0.1f); GetSpriteAnimation()->AddClip("stand", 0, 1, 0.1f); GetSpriteAnimation()->AddClip("shoot", 0, 1, 0.1f); GetSpriteAnimation()->SetPlayType(re::PlayType::NORMAL); } void Enemy::_CheckEnemyState() { if (abs(GetCollider()->GetLinearVelocity().x) > 1.f) // Moving { SetState(WALK); } else // Not moving { SetState(STAND); } } void Enemy::_AnimateByEnemyState() { switch (GetState()) { case STAND: GetSpriteAnimation()->Play("stand"); break; case WALK: GetSpriteAnimation()->Play("walk"); break; case SHOOT: GetSpriteAnimation()->Play("shoot"); break; default: break; } auto texture = GetSpriteComponent()->GetUvs(); if (m_controller->IsLookingLeft()) // left { GetSpriteComponent()->SetUvs({ texture.z, texture.y, texture.x, texture.w }); } else // right { GetSpriteComponent()->SetUvs({ texture.x, texture.y, texture.z, texture.w }); } } ``` Enemy.cpp I'm creating an instance of the desired ```EnemyPrefs``` (from ```gameDefs.h```) and use this instance to set the parameters to all the components. Now it's quite easy to change the enemy parameters by just changing that one header file instead of going through piles of code :) > Note: Animations use set values, these could also be included in the ```EnemyPrefs``` to create different animation setups Otherwise, the implementation is very similar to the ```Player``` components implementation. Now finally we are ready to go into animations and how to set them up: ### Animations (```re::SpriteAnimationComponent```, sprite sheets) #### Animation in Routa Animations are done using sprite sheets instead of single sprites. The ```re::SpriteAnimationComponent``` will cut your sprite sheet to be used as animation frames and you can create animation clips that the component then plays. Animating sprites in Routa has a few steps: * Create/download a sprite sheet with animation frames * **The sprite sheet needs to be square and power of two** * Initialize ```re::SpriteComponent``` with this sprite sheet * This creates a texture of your whole sprite sheet but the ```re::SpriteAnimationComponent``` will decide which part of the texture to show * Attach and initialize ```re::SpriteAnimationComponent``` with the correct texture width, height (grabbed from ```re::SpriteComponent```) and number of rows/columns in the sprite sheet * Create and add animation clips with desired time per frame and starting and stopping points * Set animation playtype (```NORMAL```, ```REVERSE```, ```PINGPONG```) * Normal plays normally * Reverse plays in reverse * Pingpong plays start to end to start to end to start... * Play created animation clips when desired #### Animation implementation Let's look at ```Player.cpp``` for example: ```cpp void Player::_SetupAnimations() { // Create/Add animation clips to animation component GetSpriteAnimation()->Init(3, 3, GetSpriteComponent()->GetTexture().width, GetSpriteComponent()->GetTexture().height); GetSpriteAnimation()->AddClip("walk", 0, 5, 0.1f); GetSpriteAnimation()->AddClip("jump", 6, 1, 0.3f); GetSpriteAnimation()->AddClip("fall", 7, 1, 0.3f); GetSpriteAnimation()->AddClip("stand", 0, 1, 0.1f); GetSpriteAnimation()->SetPlayType(re::PlayType::NORMAL); } ``` This is the sprite sheet used: ![](https://gitlab.dclabra.fi/wiki/uploads/upload_ad3545e04cb1e5200669b60418df6fb3.png) >I only needed 8 sprites in this sheet so I left one "slot" empty. Nothing wrong with that! We have attached a ```re::SpriteAnimationComponent``` in the base class already so here we are: * Initializing the animation component with the following parameters: * Rows: 3, like in the sprite sheet above * Columns: 3, like in the sprite sheet above * Texture width: ```GetSpriteComponent()->GetTexture().width``` * Texture height: ```GetSpriteComponent()->GetTexture().height``` > Get the texture width and height from the sprite component to make sure they are accurate and you won't get graphical artifacts > All this information is used by ```re::SpriteAnimationComponent``` to determine how to cut up the sprite sheet * Adding a few animation clips to be played with ```AddClip(...)```: * Walk * Clip name: walk * Clip starting image index (inclusive): 0 * Frame amount: 5 * Frametime: 0.1 seconds * Jump * Clip name: jump * Clip starting image index (inclusive): 6 * Frame amount: 1 * Frametime: 0.3 seconds * Fall * Clip name: fall * Clip starting image index (inclusive): 7 * Frame amount: 1 * Frametime: 0.3 seconds * Stand * Clip name: stand * Clip starting image index (inclusive): 0 * Frame amount: 1 * Frametime: 0.1 seconds * This is the idle animation so there's only 1 frame > Clip name is used when you want to play an animation, starting image is the first frame of animation, frame amount is the amount of frames to play (frames 0, 1, 2, 3 and 4 in this case) and frametime is the amount of time one frame takes to play. * Setting the ```PlayType``` to ```NORMAL``` Now that the animations are set up, we need to determine when to play them and actually play them: ```cpp void Player::_CheckPlayerState() { // Get current velocities and change player state accordingly // Jumping checks first since they should override walking animations (no "airwalk") if (GetCollider()->GetLinearVelocity().y > 1.f) // Jumping { SetState(JUMP); } else if (GetCollider()->GetLinearVelocity().y < -1.f) // Falling { SetState(FALL); } else if (abs(GetCollider()->GetLinearVelocity().x) > 1.f) // Moving { SetState(WALK); } else // Not moving { SetState(STAND); } } ``` Here I'm simply asking the attached ```RigidBody``` what it's current linear velocity is and setting ```m_state``` of the base class to the desired value with ```SetState(...)``` Then I'm playing the animations accordingly: ```cpp void Player::_AnimateByPlayerState() { switch (GetState()) { case STAND: GetSpriteAnimation()->Play("stand"); break; case WALK: GetSpriteAnimation()->Play("walk"); break; case JUMP: GetSpriteAnimation()->Play("jump"); break; case FALL: GetSpriteAnimation()->Play("fall"); break; default: break; } /* Flip player-sprites UV-coordinates based on the direction the player is facing */ auto texture = GetSpriteComponent()->GetUvs(); if (m_controller->IsLookingLeft()) // left { GetSpriteComponent()->SetUvs({ texture.z, texture.y, texture.x, texture.w }); } else // right { GetSpriteComponent()->SetUvs({ texture.x, texture.y, texture.z, texture.w }); } } ``` So depending on the current ```m_state``` grabbed with ```GetState()``` I'm playing the correct animation clip created before with it's specific name. Here's where that ```bool IsLookingLeft()``` method comes to play: If the player is looking left we use the ```re::SpriteComponent```s method ```SetUvs(...)``` method to flip the sprites texture coordinates (UVs) so the shown sprite is flipped to face the correct way! If you look back at the ```Enemy``` component, you'll notice it's set up in a similar way with the exception of having their own shooting animation clip. The player sprite sheet doesn't need a separate shooting animation clip. Looking back, I've also introduced ```ShootingComponent``` so next I'll show you what's going on there: ### Shooting (```ShootingComponent```, ```Bullet```) #### ```ShootingComponent``` ```ShootingComponent``` as you guessed it, handles shooting bullets. There's some things to keep in mind to keep the component modular: * Give it: * a changeable firing point (where the fired bullet will be spawned) * a changeable shooting power (how fast the fired bullet travels) The header for my ```ShootingComponent``` is quite simple: ```cpp #ifndef SHOOTING_COMPONENT_H #define SHOOTING_COMPONENT_H #include <core/ECS_collection.h> #include <core/Routa.h> #include <vector> #include "DamageComponent.h" // Create a new component, which inherits the base Component class class ShootingComponent : public re::Component { public: ShootingComponent(); // Default constructor virtual ~ShootingComponent(); // ALWAYS virtual destructor, to make sure that the base class's constructor calls it virtual void Tick(double) override; // Override the tick-function void Init(float shootingPower, float shootingOffset); // If your component needs further initializing, do it here void Shoot(re::Vector2 direction); private: std::vector<re::Entity*> m_bullets; float m_shootingOffset; float m_shootingPower; }; #endif ``` ShootingComponent.h Notice I'm keeping track of ```m_bullets```. This is to make sure they are removed from our scene. It's a good idea anyways if you want to make some cool iterative feature with the bullets, like a force field of bullets or something who knows? Anyways, quite simple. Let's check the implementation for getting our bullets flying: ```cpp #include "ShootingComponent.h" #include "Bullet.h" ShootingComponent::ShootingComponent() { } ShootingComponent::~ShootingComponent() { for (auto bullet : m_bullets) { if (bullet->IsDeleted()) continue; bullet->DetachComponent("Bullet"); // Detaching bullet component calls bullets destructor which removes other components and deletes it's entity //bullet->Despawn(); } m_bullets.clear(); } void ShootingComponent::Tick(double) { } void ShootingComponent::Init(float shootingPower, float shootingOffset) { this->m_shootingPower = shootingPower; this->m_shootingOffset = shootingOffset; } void ShootingComponent::Shoot(re::Vector2 direction) { re::Entity* shooter = this->GetEntity(); re::Entity* bullet = this->GetScene()->GetRoot()->SpawnChild(); Bullet* bulletComponent = static_cast<Bullet*>(bullet->AttachComponent("Bullet")); bulletComponent->Init(shooter, direction, m_shootingPower, m_shootingOffset); m_bullets.emplace_back(bullet); } ``` ShootingComponent.cpp Oh, something in the *destructor*? There I iterate through ```m_bullets``` and detach the ```Bullet``` component from them to despawn them from the scene if they have not been deleted yet. >The component destructors are called whenever a component is *detached* In ```Init(...)``` I'm just supplying the component the desired values. In ```Shoot(...)``` is where the magic happens: * Get the entity that shot the bullet * Spawn a new child entity to the scene root entity * Attach a ```Bullet``` component to the spawned entity * Initialize the component with supplied values (who shot, which way to shoot, where to shoot from and how fast) * Place the spawned entity pointer in ```m_bullets``` to keep track of them We are just creating an entity here. The actual ```Bullet``` creates the physics impulse to propel it towards ```direction```. But how? Let's see how I implemented this functionality: #### ```Bullet``` ```cpp #ifndef BULLET_H #define BULLET_H #include <core/ECS_collection.h> #include <core/Routa.h> #include "PlatformerCharacter.h" class DamageComponent; namespace re { class BoxColliderComponent; class SpriteComponent; } // Create a new component, which inherits the base Component class class Bullet : public re::Component { public: Bullet(); // Default constructor virtual ~Bullet(); // ALWAYS virtual destructor, to make sure that the base class's constructor calls it virtual void Tick(double) override; // Override the tick-function void Init(re::Entity* owner, re::Vector2 direction, float power, float offset); // If your component needs further initializing, do it here private: re::Entity* m_my_shooter; PlatformerCharacter::CharacterType m_shooter_type; re::Entity* m_my_entity; re::BoxColliderComponent* m_coll; re::SpriteComponent* m_sprite; DamageComponent* m_damage; void _CheckCollisions(); void _DestroyBullet(); }; #endif ``` Bullet.h Here in addition to the ```Init(...)``` parameters I gave the bullet a ```re:SpriteComponent``` and a ```DamageComponent```. There's also a private method for checking the collisions and destroying the shot bullet. Implementing this is actually quite somewhat simple: ```cpp #include "Bullet.h" #include "Player.h" #include "Enemy.h" #include "gameDefs.h" #include "DamageComponent.h" Bullet::Bullet() : m_my_entity(nullptr), m_my_shooter(nullptr), m_damage(nullptr), m_coll(nullptr), m_sprite(nullptr) { } Bullet::~Bullet() { // Bullet component was Detached, detach components added by Bullet and despawn entity if (m_my_entity->IsDeleted()) return; if (m_my_entity->HasComponent("BoxColliderComponent")) m_my_entity->DetachComponent("BoxColliderComponent"); if (m_my_entity->HasComponent("SpriteComponent")) m_my_entity->DetachComponent("SpriteComponent"); if (m_my_entity->HasComponent("DamageComponent")) m_my_entity->DetachComponent("DamageComponent"); m_my_entity->Despawn(); } void Bullet::Tick(double) { _CheckCollisions(); } void Bullet::Init(re::Entity * owner, re::Vector2 direction, float power, float offset) { m_my_shooter = owner; m_my_entity = GetEntity(); // Returns NULL pointer if casting component to base class.., charactertype cannot be used from polymorphic base class with components without doing the checking here so it's useless //PlatformerCharacter* shooterCharacter = static_cast<PlatformerCharacter*>(m_my_shooter->GetComponent("PlatformerCharacter")); //PlatformerCharacter::CharacterType shooterType = shooterCharacter->GetCharacterType(); if (m_my_shooter->HasComponent("Player")) { m_shooter_type = PlatformerCharacter::CharacterType::PLAYER; } else if (m_my_shooter->HasComponent("Enemy")) { m_shooter_type = static_cast<Enemy*>(m_my_shooter->GetComponent("Enemy"))->GetCharacterType(); } auto graphics = static_cast<re::GraphicsSystem*>(GetScene()->GetSystem("GraphicsSystem")); m_coll = static_cast<re::BoxColliderComponent*>( m_my_entity->AttachComponent("BoxColliderComponent")); m_damage = static_cast<DamageComponent*>(m_my_entity->AttachComponent("DamageComponent")); // Change bullet params depending on shooter switch (m_shooter_type) { case PlatformerCharacter::CharacterType::PLAYER: { BulletPrefs::DukeNukem bulletPrefs; // Decides what bullet params to use m_my_entity->SetPosition( re::Vector3( m_my_shooter->GetPosition().x + direction.x * offset, m_my_shooter->GetPosition().y + direction.y * offset, m_my_shooter->GetPosition().z ) ); m_coll->SetSize(bulletPrefs.colliderSize); m_coll->LockRotation(bulletPrefs.lockRotation); m_coll->SetColliderFilter(bulletPrefs.colliderCategory, 0xFFFF); // Correct..? m_damage->Init(bulletPrefs.damage); m_sprite = static_cast<re::SpriteComponent*>( m_my_entity->AttachComponent("SpriteComponent")); m_sprite->Init( re::Vector3(0.0f, 0.0f, 0.0f), re::Vector2(bulletPrefs.spriteWidth, bulletPrefs.spriteHeight), re::Vector4(0.0f, 0.0f, 1.0f, 1.0f), re::ColourRGBA8(255, 255, 255, 255), graphics->LoadImage(bulletPrefs.spritePath.c_str()) ); /* Calculate and apply impulse direction */ m_coll->ApplyLinearImpulseToCenter( re::Vector3( direction.x * power, direction.y * power, 0.0f) ); break; } case PlatformerCharacter::CharacterType::BLUE_GUARD: { BulletPrefs::Blue_Guard bulletPrefs; // Different params than player m_my_entity->SetPosition( re::Vector3( m_my_shooter->GetPosition().x + direction.x * offset, m_my_shooter->GetPosition().y + direction.y * offset, m_my_shooter->GetPosition().z ) ); m_coll->SetSize(bulletPrefs.colliderSize); m_coll->LockRotation(bulletPrefs.lockRotation); m_coll->SetColliderFilter(bulletPrefs.colliderCategory, 0xFFFF); // Correct..? m_damage->Init(bulletPrefs.damage); m_sprite = static_cast<re::SpriteComponent*>( m_my_entity->AttachComponent("SpriteComponent")); m_sprite->Init( re::Vector3(0.0f, 0.0f, 0.0f), re::Vector2(bulletPrefs.spriteWidth, bulletPrefs.spriteHeight), re::Vector4(0.0f, 0.0f, 1.0f, 1.0f), re::ColourRGBA8(255, 255, 255, 255), graphics->LoadImage(bulletPrefs.spritePath.c_str()) ); /* Calculate and apply impulse direction */ m_coll->ApplyLinearImpulseToCenter( re::Vector3( direction.x * power, direction.y * power, 0.0f) ); break; } case PlatformerCharacter::CharacterType::TURRET: { BulletPrefs::Turret bulletPrefs; m_my_entity->SetPosition( re::Vector3( m_my_shooter->GetPosition().x + direction.x * offset, m_my_shooter->GetPosition().y + direction.y * offset, m_my_shooter->GetPosition().z ) ); m_coll->SetSize(bulletPrefs.colliderSize); m_coll->LockRotation(bulletPrefs.lockRotation); m_coll->SetColliderFilter(bulletPrefs.colliderCategory, 0xFFFF); // Correct..? m_damage->Init(bulletPrefs.damage); m_sprite = static_cast<re::SpriteComponent*>( m_my_entity->AttachComponent("SpriteComponent")); m_sprite->Init( re::Vector3(0.0f, 0.0f, 0.0f), re::Vector2(bulletPrefs.spriteWidth, bulletPrefs.spriteHeight), re::Vector4(0.0f, 0.0f, 1.0f, 1.0f), re::ColourRGBA8(255, 255, 255, 255), graphics->LoadImage(bulletPrefs.spritePath.c_str()) ); /* Calculate and apply impulse direction */ m_coll->ApplyLinearImpulseToCenter( re::Vector3( direction.x * power, direction.y * power, 0.0f) ); break; } } // Rotate bullet if needed if (direction.y) { m_my_entity->SetAngle2D(re::DegToRad(direction.y * 90.f + -direction.y * direction.x * 45.f)); } } void Bullet::_CheckCollisions() { if (m_my_entity->IsDeleted()) return; if (m_my_shooter->IsDeleted()) return; b2ContactEdge* contact = m_coll->GetBodyPtr()->GetContactList(); while (contact) { // If player shot this bullet if (m_my_shooter->HasComponent("Player")) { // If player hit the enemy if (contact->other->GetFixtureList()->GetFilterData().categoryBits == ColliderCategories::COLLIDER_ENEMY) { // Colliding with enemy, check for their health component and DealDamage() to it auto enemyColl = static_cast<re::BoxColliderComponent*>(contact->other->GetUserData()); re::Entity* enemy = static_cast<re::Entity*>(enemyColl->GetEntity()); m_damage->DealDamage(enemy); // 3. Damage dealt, destroy bullet _DestroyBullet(); break; // exit while to work around "contact was 0xFDFDFD" crash } } else // If enemy shot this bullet if (m_my_shooter->HasComponent("Enemy")) { // If enemy hit the player if (contact->other->GetFixtureList()->GetFilterData().categoryBits == ColliderCategories::COLLIDER_PLAYER) { auto playerColl = static_cast<re::BoxColliderComponent*>(contact->other->GetUserData()); re::Entity* player = static_cast<re::Entity*>(playerColl->GetEntity()); m_damage->DealDamage(player); _DestroyBullet(); break; // exit while to work around "contact was 0xFDFDFD" crash } } // Regardless of shooter, if target is a tile if (contact->other->GetFixtureList()->GetFilterData().categoryBits == ColliderCategories::COLLIDER_TILE) { _DestroyBullet(); break; // exit while to work around "contact was 0xFDFDFD" crash } contact = contact->next; } //!while } void Bullet::_DestroyBullet() { // Detach components and despawn entity if (m_my_entity != nullptr && m_my_entity != NULL) { m_my_entity->DetachComponent("Bullet"); // Remove this component and call this components destructor } } ``` Bullet.cpp * In the constructor: * We again only need to set the uninitialized members to ```nullptr```. * In the destructor: * It's the same pattern as in the ```ShootingComponent```: * If entity has already been deleted, return * Or otherwise detach the components attached by this component and despawn the entity. * In ```Tick(...)```: * Just keep checking collisions * In ```Init(...)```: * Deduce if the shooter is a player or an enemy by querying their respective components * Attach a ```re::BoxColliderComponent``` to the entity that ```ShootingComponent``` spawned (the bullet) * Attach a ```DamageComponent``` to said entity * Depending on the shooter type, set bullet parameters accordingly * These preferences are grabbed from ```gameDefs.h``` > All the character type specific code is separated/duplicated so that different character types can have different ways to shoot * The bullet is offset to the shootingOffset (firing point) * The bullet rotation is locked to keep the sprite and collider from spinning around wildly * The bullet is propelled forward with ```re::RigidBody```s ```ApplyLinearImpulseToCenter```, multiplied with shooting power * Lastly, the bullet is rotated 45 to 90 degrees if the shooting direction was vertical (or diagonal). * In ```_CheckCollisions(...)```: * First check if the shooter is dead or if the bullet has already been despawned and return * If the first check failed, check the shooters type (```Player``` or ```Enemy```) * Then check if their ```ColliderCategory``` was of the other character type * If it was, we get their ```re:BoxColliderComponent``` from their ```re::RigidBody```s ```userdata``` > ```userdata``` in Box2D contains some data that the developer decided to include in ```RigidBody```s. Routa exposes it's collider component of the entity in question * Then we get the entity the collider was attached to with ```GetEntity()``` * Now we have the entity, just ```DealDamage(...)``` to them! * Damage was dealt, now we can destroy the bullet entity by calling ```_DestroyBullet()``` * In ```_DestroyBullet()```: * We are simply detaching this ```Bullet``` component which in turn calls it's destructor explained above, effectively despawning the bullet from the scene! Now that we have all the more interesting mechanics explained and out of the way. We still have to somehow use them. There's hasn't been much testing in between which you should do when getting accustomed to the engine but the code I have provided works. Now we just have to load the level we created and test the mechanics out! We will be loading and changing the levels in the scene code. Let's start implementing that then!: ### Winning (changing levels) So, level loader is armed and ready and we just need to create an instance of it in our scene and call ```LoadLevel(...)``` But before we do that, we also have to keep a list of levels our game has and which level we are currently on. I decided to create a ```std::vector``` of ```std::string```s that contain all my level names in my scene header (```myscene.h```): ```cpp Level* m_level; // Level loader std::vector<std::string> m_levels = // Level names { "levels/test_01.json", "levels/test_02.json", "levels/test_03.json", "levels/tutorial_01.json", "levels/tutorial_02.json" }; ``` If you only have that single level, yours will probably look like something like this: ```cpp std::vector<std::string> m_levels = { "levels/my_level.json" }; ``` Do you remember those ```LevelProps``` introduced in the [level loader script](#Loading-created-levels)? Let's also write those for our levels: ```cpp std::vector<Level::LevelProps> m_levelProperties = { {64,64, false, {0.0f,0.0f}}, {64,64, false, {0.0f,0.0f}}, {64,64, false, {0.0f,0.0f}}, {128,64, false, {0.0f,-16.0f}}, {256,48, true, {0.0f,0.0f}} }; ``` The format was ```float width, float height, bool isCaveLevel, re::Vector2 backgroundOffset```. I used isCaveLevel to determine which background image to use and backgroundOffset to position the background image correctly, but as I stated before, ```re::TiledComponent``` doesn't support background but I left the code in to give you an idea of a way to accomplish this. Then we also need the information on which level to load. I am saving the next index to the next level name string in ```m_levels``` above: ```cpp int m_nextLevelToLoad = 3; ``` As I had some test levels in my ```std::vector``` of levels, I decide to load index 3 straight away (which would point to ```"levels/tutorial_01.json"```). Then, we need some methods to change our level: ```cpp bool IsLevelCompleted(); void LoadNextLevel(); ``` The former checks if we have collided with the goal: ```cpp // Collision check for goal, loads next level bool MyScene::IsLevelCompleted() { auto player_coll = m_player->GetCollider(); b2ContactEdge* contact = player_coll->GetBodyPtr()->GetContactList(); while (contact) { if (contact->other->GetFixtureList()->GetFilterData().categoryBits == ColliderCategories::COLLIDER_GOAL) return true; contact = contact->next; } return false; } ``` And the latter calls ```LoadLevel()``` from our level loader, spawns entities (player, goal and enemies) and increments ```m_nextLevelToLoad```: ```cpp void MyScene::LoadNextLevel() { if (m_nextLevelToLoad >= m_levels.size()) { //return; // Last level -> can't change further m_nextLevelToLoad = 0; // Last level -> loop back around to first } m_level->LoadLevel(m_levels[m_nextLevelToLoad], this, 0.f,m_levelProperties[m_nextLevelToLoad]); m_goal_coll = m_level->SpawnGoal({ 0.5f,0.5f }); m_player = m_level->SpawnPlayer({ 0.5f, 0.5f, 0.0f }); m_camera = m_player->GetCamera(); m_level->SpawnEnemies({ 0.5f, 1.5f }, Enemy::EnemyType::BLUE_GUARD); m_level->SpawnEnemies({ 0.5f, 1.5f }, Enemy::EnemyType::TURRET); m_nextLevelToLoad++; } ``` You can see I also introduced ```m_goal_coll```. This just keeps a reference on the spawned goal for my previous version of checking level completion and is unneeded but was left in to show you a simple way of grabbing the reference straight from the spawn call. Now that we can change levels and get to the next level by going in the goal we can also implement our losing condition. If the player dies or jumps to a pit, the level should maybe be restarted or the player set back to a checkpoint. ### Losing (restarting levels) **But**, this is in exercise for the reader. I'll give some tips and ideas to go off of: * Decrement ```m_nextLevelToLoad``` and call ```LoadNextLevel()``` after that, effectively restarting the level * If you have pits or traps in your level, those should probably trigger beforementioned behaviour * Create a new empty tile in your tilemap and create a death trigger index in the level loader and use this new tile type to differentiate a death trigger similar to the spawn position tiles * If you want checkpoints, use the same way to implement a checkpoint trigger tile and create checkpoint definitions where you set their size and important spawn points and afterwards just spawn a ```re::*TriggerComponent``` in their position, scaled to the definitions. Then if the player dies, reload the level but use ```SpawnPlayerToCheckpoint(int index)``` or such for a custom spawn position * You could also create a level width wide trigger below the level or manually at each pit location and insert these in the ```LevelProps``` struct I hope these ideas give you a direction on implementing your losing condition :sunglasses: ### Stitching it all together (using all these new components) Now that we have all components armed and ready (and quite untested in your case) we still have to register them to our ```PlatformerSystem``` and load a level in our scene constructor: ```cpp #include "DamageComponent.h" #include "HealthComponent.h" #include "PlayerController.h" #include "EnemyController.h" #include "MovementComponent.h" #include "ShootingComponent.h" #include "Player.h" #include "Enemy.h" #include "Bullet.h" #include "PlatformerSystem.h" #include <iostream> PlatformerSystem::PlatformerSystem() { // Register the components that belong to this system this->CreateComponentType<HealthComponent>("HealthComponent"); this->CreateComponentType<DamageComponent>("DamageComponent"); this->CreateComponentType<PlayerController>("PlayerController"); this->CreateComponentType<MovementComponent>("MovementComponent"); this->CreateComponentType<EnemyController>("EnemyController"); this->CreateComponentType<ShootingComponent>("ShootingComponent"); this->CreateComponentType<Player>("Player"); this->CreateComponentType<Enemy>("Enemy"); this->CreateComponentType<Bullet>("Bullet"); } PlatformerSystem::~PlatformerSystem() { // If needed to cleanup memory, do it here } void PlatformerSystem::Tick(double dt) { /* Update registered components here First, find all the components of that type */ re::ArrayPool<HealthComponent>* healthComponents = GetComponents<HealthComponent>("HealthComponent"); re::ArrayPool<DamageComponent>* damageComponents = GetComponents<DamageComponent>("DamageComponent"); re::ArrayPool<PlayerController>* playerControllers = GetComponents<PlayerController>("PlayerController"); re::ArrayPool<EnemyController>* enemyControllers = GetComponents<EnemyController>("EnemyController"); re::ArrayPool<MovementComponent>* movementComponents = GetComponents<MovementComponent>("MovementComponent"); re::ArrayPool<ShootingComponent>* shootingComponents = GetComponents<ShootingComponent>("ShootingComponent"); re::ArrayPool<Bullet>* bullets = GetComponents<Bullet>("Bullet"); re::ArrayPool<Player>* player = GetComponents<Player>("Player"); re::ArrayPool<Enemy>* enemies = GetComponents<Enemy>("Enemy"); // Iterate through the found components, ORDER MATTERS! for (auto& c : (*player)) { c.Tick(dt); } for (auto& c : (*enemies)) { c.Tick(dt); } for (auto& c : (*damageComponents)) { c.Tick(dt); } for (auto& c : (*playerControllers)) { c.Tick(dt); } for (auto& c : (*enemyControllers)) { c.Tick(dt); } for (auto& c : (*movementComponents)) { c.Tick(dt); } for (auto& c : (*shootingComponents)) { c.Tick(dt); } for (auto& c : (*bullets)) { c.Tick(dt); } for (auto& c : (*healthComponents)) { c.Tick(dt); } } void PlatformerSystem::Init() { // If the system needs further initializing, do it here } ``` PlatformerSystem.cpp As you can see the same procedure as before, creating the component types (registering the components) and then just ticking them categorically. Remember that the order of ```Tick(...)``` matters. I kept the order as executionally logical as possible: Check player, check enemies, deal damages, control entities, move according to control, shoot according to control, spawn shot bullets and lastly destroy entities if needed. And to load the initial starting level in our scene constructor: ```cpp MyScene::MyScene(re::Engine* engine) : re::Scene(engine) /* Engine pointer passed to re::Scene */ , m_engine_ptr{ engine } #if !defined(__ANDROID__) , m_audio_system{ nullptr } #endif , m_graphics_system{ nullptr } , m_physics_system{ nullptr } , m_animation_system{ nullptr } , m_player{ nullptr } , m_camera{ nullptr } , m_platformer_system{ nullptr } { /* Just to be sure */ assert(m_engine_ptr); /* Systems that this scene uses are added here. * Names given as strings must match the type name without the scopea * operator 're::'. */ #if !defined(__ANDROID__) m_audio_system = this->CreateSystem<re::AudioSystem>("AudioSystem"); assert(m_audio_system); #endif m_graphics_system = this->CreateSystem<re::GraphicsSystem>("GraphicsSystem"); re::ColourRGBA8 col; //col.setHSV(172, 73, 55, 100); // Default teal col.setHSV(36, 29, 87, 255); m_graphics_system->Init(re::GL,col); assert(m_graphics_system); m_physics_system = this->CreateSystem<re::PhysicsSystem>("PhysicsSystem"); assert(m_physics_system); m_animation_system = this->CreateSystem<re::AnimationSystem>("AnimationSystem"); assert(m_animation_system); m_platformer_system = this->CreateSystem<PlatformerSystem>("PlatformerSystem"); assert(m_platformer_system); m_graphics_system->ToggleDebugDraw(); /* Set world gravity */ m_physics_system->SetGravity(re::Vector2(0.0f, -25.0f)); m_level = new Level(m_graphics_system, this); // create level loader object LoadNextLevel(); } ``` There in the end a level loader instance is created and then a level loaded. I've also removed all those player and platform creation calls since our level loader and ```LoadNextLevel()``` now handles those. I've also changed the background color to something easier to look at and in the end, the screen after pressing play looks like this: ![](https://gitlab.dclabra.fi/wiki/uploads/upload_074eeb8def7857dbd229290d177c1a55.png) And the enemies spawn in their spawn positions and the animations play: ![](https://gitlab.dclabra.fi/wiki/uploads/upload_0bfca9beaf769bd134c08817e2daba12.png) ### Full project source code You can find the full source code in the Routa repository [inside this path](https://repo.kamit.fi/routa/routaengine/-/tree/master/examples/platformer_tutorial): ```routaengine\examples\platformer_tutorial``` Good luck with Routa! :smile: