This project was made from scratch, using SDL to display 2D textures. It is a clone of the opensource project ‘FreeCraft’, which in its turn is a clone of the popular game ‘WarCraft2’. It was the first full solo project made as part of my education at DAE.
The FreeCraft project was shut down by Blizzard, so this project contains copyrighted materials and the only purpose of CloneCraft is purely educational.
It features level loading from an image, basic enemy AI, a great variety of units, buildings and upgrades the player can explore.
Trailer
Check out the trailer for the project here! A longer gameplay video can be found here
About the project
The project was created from scratch, using only a simple game loop and all other code was custom made for this specific game. All assets are from the opensource FreeCraft project, I don’t own any of the assets in this game.
One of the features I spent a lot of time on was creating a way to load maps in an easy way. I ended up creating an algorithm that can load the correct tile textures from a spritesheet using a simple .JPG image. (see image on the right)
The next challenging part was the movement of the units, they could not run through each other and had to be able to move with up to 9 units in one squad. This turned out to be more of a challenge than expected. Just using an A* pathfinding algorithm wasn’t enough. Units have to keep checking if the next tile in their path is still free, if it is they “lock” the tile so that no other unit can walk to the tile. If the tile is occupied by either a unit standing on it, or another unit Locked the tile, the Unit has to find another way around that tile. This detour is chosen so that the Unit can get to the shortest path again in the shortest distance possible. Once this was implemented, I had to make sure land units can’t walk on water, and air unit can fly over both water and land units. This was done by making 3 movement layers. These layers are encoded in the tiles, where every type of tile knows if a land, air or water unit can walk over it.
There are a lot of different types of units and buildings. All these units can be upgraded and some of the buildings can also be upgraded to boost stats or to unlock more units and more advanced buildings for the player to use. This required the implementation of an upgrade tracker. This class keeps track of the built buildings and the researched upgrades. This makes sure advanced buildings get unlocked once the requirements are met and the upgraded units deal more damage depending on the researched upgrades.
Code snippets
Level loading
The following two code snippets are the core of the level loading algorithm. The first function is used to get the color from a certain pixel of the image, this function is used in the main algorithm to get the color of the pixels and store them in a 2D array representing the level. Once all colors are known, the function in the second snippet wil create the actual tyle objects with the correct settings, depending on the color of the corresponding pixel. Of course, using color data for the purpose of storing just what type of tile we need, is a waste of memory. I did it this way, because I wanted an easy way to edit the terrain as I was designing the levels. It could also be done with for example a csv file containing just an index from 1-*NrOfTileTypes. However, this solution makes it harder to actually design the level. An intermediary solution would be writing a program that converts a png image to the described csv file, to save on memory and time when loading the level as I would skip loading and reading the texture and instead of colors I would need just one short value to represent all possible tile types.
The final step of the algorithm is checking what type the surrounding tiles are, so that the edges between types can automatically be generated.
1Uint32 World::getPixel(SDL_Surface * surface, int x, int y)
2{
3 int bpp = surface->format->BytesPerPixel;
4
5 Uint8 *p = (Uint8*)surface->pixels + y * surface->pitch + x * bpp;
6 switch (bpp)
7 {
8 case 1:
9 return *p;
10 case 2:
11 return *(Uint16*)p;
12 case 3:
13 if (SDL_BYTEORDER == SDL_BIG_ENDIAN)
14 return p[0] << 16 | p[1] << 8 | p[2];
15 else
16 return p[0] | p[1] << 8 | p[2] << 16;
17 case 4:
18 return *(Uint32*)p;
19 default:
20 return 0;
21 }
22}
1//Iterate over every pixel in the dataImage and set the corresponding Data ID's
2//These ID's will be used to initialize the corresponding tiles
3void World::GetWorldData(SDL_Surface* surface)
4{
5 for ( int y = 0; y < m_Height ; ++y)
6 {
7 for (int x = 0; x < m_Width; ++x)
8 {
9 uint8_t r;
10 uint8_t g;
11 uint8_t b;
12 SDL_LockSurface(surface);
13 SDL_GetRGB(getPixel(surface, x, y), surface->format, &r, &g, &b);
14 SDL_UnlockSurface(surface);
15
16 if ((r == 76) && (g == 108) && (b == 28))
17 {
18 m_pWorldTiles[y][x] = std::make_shared<Tile>(utils::TileType::lightgrass);
19 }
20 else if(r==100 && g==64 && b==40)
21 {
22 m_pWorldTiles[y][x] = std::make_shared<Tile>(utils::TileType::lightdirt);
23 }
24 else if (r == 0 && g == 40 && b == 112)
25 {
26 m_pWorldTiles[y][x] = std::make_shared<Tile>(utils::TileType::lightwater);
27 }
28 else if (r == 44 && g == 92 && b == 16)
29 {
30 m_pWorldTiles[y][x] = std::make_shared<Tile>(utils::TileType::darkgrass);
31 }
32 else if (r == 90 && g == 112 && b == 56)
33 {
34 m_pWorldTiles[y][x] = std::make_shared<Tile>(utils::TileType::lightgrasswrocks);
35 }
36 else if (r == 58 && g == 93 && b == 38)
37 {
38 m_pWorldTiles[y][x] = std::make_shared<Tile>(utils::TileType::darkgrasswrocks);
39 }
40 else if (r == 64 && g == 44 && b == 0)
41 {
42 m_pWorldTiles[y][x] = std::make_shared<Tile>(utils::TileType::darkdirt);
43 }
44 else if (r == 99 && g == 76 && b == 61)
45 {
46 m_pWorldTiles[y][x] = std::make_shared<Tile>(utils::TileType::lightdirtwrocks);
47 }
48 else if (r == 61 && g == 51 && b == 28)
49 {
50 m_pWorldTiles[y][x] = std::make_shared<Tile>(utils::TileType::darkdirtwrocks);
51 }
52 else if (r == 0 && g == 24 && b == 92)
53 {
54 m_pWorldTiles[y][x] = std::make_shared<Tile>(utils::TileType::darkwater);
55 }
56 else if (r == 255 && g == 118 && b == 18)
57 {
58 m_pWorldTiles[y][x] = std::make_shared<ResourceTile>(utils::TileType::forest);
59 }
60 else if (r == 108 && g == 108 && b == 108)
61 {
62 m_pWorldTiles[y][x] = std::make_shared<Tile>(utils::TileType::mountain);
63 }
64 else
65 {
66 m_pWorldTiles[y][x] = std::make_shared<Tile>(utils::TileType::lightgrass);
67 }
68
69 }
70 }
71}
Pathfinding
This final code snippet is the base code that makes sure the units don’t collide and walk through each other, while air units can still fly over all other units. When this function gets called, the units has a shortest path from when the movement started to its destination. The function constantly checks if the unit passed the first tile in it’s path. If it did, we check if the next tile is still passable for the current unit type. If this is the case, we can lock this tile and unlock the one we were just on. If it is not free anymore, it will keep checking subsequent tiles on the shortest path until one free tile is found. If such a tile is found, we calculate the shortest path to it in the current map state and prepend that to our previous path. I there is not a single tile left in the path, or all tiles in the path are occupied, we stop the unit and put it in its idle state.
1void Unit::MoveAlongPath(float elapsedSec)
2{
3 //First check if the unit is near the first position in the path
4 Point2f nextCheckPoint = m_Path.front();
5
6 //If the unit passed the checkpoint, set the position to the checkpoint and calculate the direction towards the next checkpoint
7 //ELse, move the unit along its direction using the elapsedSec
8 if(CheckPassed(nextCheckPoint))
9 {
10 Vector2f oldDir = m_Direction;
11
12 UpdateFOW();
13 m_Path.pop_front();
14
15 while (!m_Path.empty() && !m_pWorld.lock()->GetTileAtPosition(m_Path.front())->GetPassable(m_ObjectType.second, shared_from_this()))
16 {
17 std::deque<Point2f> toAdd;
18 m_Path.pop_front();
19
20 if(!m_Path.empty())
21 m_pWorld.lock()->GetPath(m_Position, m_Path.front(), m_ObjectType.second, shared_from_this(), toAdd);
22
23 while (!toAdd.empty())
24 {
25 m_Path.push_front(toAdd.back());
26 toAdd.pop_back();
27 }
28 }
29
30 //If the path is empty after reaching the position, make the unit stop moving
31 //Else set the direction to the new first position in the path
32 if (m_ActionState == ActionState::standGround || m_ActionState == ActionState::idle)
33 {
34 m_Path.clear();
35 SetSpriteData(m_ActionState);
36 SetSpriteCollumn(m_ActionState);
37 m_Position.x = nextCheckPoint.x;
38 m_Position.y = nextCheckPoint.y;
39 }
40 else if (m_Path.empty())
41 {
42 if (m_ActionState != ActionState::fighting)
43 {
44 m_ActionState = ActionState::idle;
45 SetSpriteData(ActionState::idle);
46 SetSpriteCollumn(ActionState::idle);
47 }
48 m_Position.x = nextCheckPoint.x;
49 m_Position.y = nextCheckPoint.y;
50 }
51 else
52 {
53 SetDirection();
54 SetOccupiedTiles();
55
56 if(m_Direction != oldDir)
57 SetSpriteCollumn(ActionState::moving);
58 }
59 }
60 else
61 {
62 if (m_pWorld.lock()->GetTileAtPosition(m_Path.front()) == m_pOccupiedTiles.front().lock())
63 m_Position = (Vector2f(m_Position.x, m_Position.y) + (m_Direction * m_UnitDetails.movementSpeed * elapsedSec)).ToPoint2f();
64 }
65}