Grid system
Collisions
Enemy AI
Player upgrades
Checkpoints and camera
Using a grid for traversal creates simple and cheap collision checks for the player and enemies that dictates where objects can move and be interacted with.
The grid is a simple 2D grid with height data inside used for the y placement of objects aswell as pathfinding. This meant that while moving we'd check your cells height data and compare it to neighbouring cells to determine if you are able to walk there or not. For stairs we simply added a step height and check how far along you are in the stairs direction to decide your height. Elevator worked by increasing the cell's height data over time making objects in the cell move up or down. For obstacles it was a simple on or off switch that says if a cell is traversable or not.
To make it easier for level designers height data is calculated when a scene is loaded and grids are generated so level designers didn't have to be super thoughtful when placing floors, stairs and other traversable tiles. They only had to make sure it's placed on coordinates with whole numbers, but setdressing and obstacles could be placed anyway they wanted and during scene loading everything will be applied and work correctly.
When accurate collision detection was needed such as during attacks and projectiles AABB collision detection was used. Objects only had to be checked against others within the same cells as objects could be within several cells at the same time depending on their collision boxes.
Enemy AI is split between the two enemy types, The melee enemy uses simple seek behaviour with A* pathfinding to chase and attack the player. The ranged enemy uses seek to get closer and attack but if the player gets to close it runs a simple flee behaviour
class Grid
{
public:
void Init(GridValues aGridValues);
void AddToGrid(GridObjPtr aObj);
void DrawGrid();
GridCell* operator()(int x, int y)
{
return IsInBounds(x, y) ? &myCells[myGridWidth * y + x] : nullptr;
}
GridCell* operator()(int aId);
GridCell* operator()(float x, float y, bool aConvertCoords);
std::vector<int> AstarPath(int aStartID, int aGoalID);
std::vector<int> GetCellsInside(GridObjPtr aObj);
std::vector<int> GetNeighbours(int aId);
uint16_t GetGridWidth() const
{
return myGridWidth;
}
uint16_t GetGridHeight() const
{
return myGridHeight;
}
Tga::Vector2f GetGridOffset() const
{
return myOffset;
}
void ResetElevators();
static constexpr float CELL_SIZE = 100.f;
Tga::Vector2i Get2DIndexFromIndex(int aIndex);
private:
static constexpr float CLIMB_PENALTY = 3.f;
static constexpr float DESCENT_PENALTY = 1.f;
Tga::Vector4f GetBoxCorners(Tga::Vector2f aPos, Tga::Vector2f aSize);
bool IsInBounds(int x, int y);
void ResetAstar(int aAstarGridSize);
Tga::Vector2f myOffset;
std::vector<GridCell> myCells;
std::vector<GridCell*> myCellsToReset;
uint16_t myCellWidth;
uint16_t myCellHeight;
uint16_t myGridWidth;
uint16_t myGridHeight;
//Visual Debugging
std::vector<Tga::Color> myColors;
std::vector<Tga::Vector3f> myFromH;
std::vector<Tga::Vector3f> myFromV;
std::vector<Tga::Vector3f> myToH;
std::vector<Tga::Vector3f> myToV;
};
GridCell* Grid::operator()(float x, float y, [[maybe_unused]] bool aConvertCoords)
{
Tga::Vector2i convertedCoords{ 0 };
float tx = x - myOffset.x;
float ty = y - myOffset.y;
convertedCoords.x = static_cast<int>(floorf(tx / myCellWidth));
convertedCoords.y = static_cast<int>(floorf(ty / myCellHeight));
if (!IsInBounds(convertedCoords.x, convertedCoords.y))
{
return nullptr;
}
return &myCells[myGridWidth * convertedCoords.y + convertedCoords.x];
}
void Player::StairLogic(Tga::Vector3f aNewPos, GridObjPtr aGridObj)
{
GridCell* gridCell = myGridPtr->operator()(aNewPos.x, aNewPos.z, true);
if (abs(aNewPos.y - gridCell->height) > 125.f)
{
return;
}
auto& boxbounds = aGridObj->model->GetModel()->GetMeshData(0).Bounds;
Tga::Vector3f stairWorldForward = aGridObj->transform.GetForward();
Tga::Vector2f stairForward2D = { stairWorldForward.x, stairWorldForward.z };
Tga::Vector2f correctedForward = Tga::Vector2f{
stairForward2D.y,
-stairForward2D.x
}.GetNormalized();
Tga::Vector2f stairPos2D = {
aGridObj->transform.GetPosition().x,
aGridObj->transform.GetPosition().z
};
float travelPercentage = 0;
Tga::Vector2f topPoint = stairPos2D + (correctedForward * boxbounds.BoxExtents.x);
float dist = (topPoint - Tga::Vector2f{ aNewPos.x,aNewPos.z }).Length();
travelPercentage = dist / (boxbounds.BoxExtents.x * 2);
aNewPos.y = std::lerp(gridCell->height - boxbounds.BoxExtents.y * 2, gridCell->height, travelPercentage);
PLAYER_TRANSFORM.SetPosition(aNewPos);
}