BoxHead - DirectX 11

Made in DirectX 11 Engine

Return

Using a DirectX 11 component based engine I created Boxhead in 3D, an old Flash game I’ve played a lot when I was young. I started off with a boilerplate framework and expanded its feature set by adding hardware skinning/animation, static batching, shadow mapping, PhysX cloth, post processing shaders and pathfinding. Besides this I did some research in steering behaviors, A* pathfinding and behavior trees. First in Unity3D and then in C++.

Challenges

  • Static batching
  • Data driven design using json
  • Shadow mapping
  • Physx Cloth
  • Using winsock2 for online leaderboards
  • Flow field pathfinding

Technology Used

  • Visual Studio
  • Unity3D
  • Adobe Photoshop
  • Autodesk 3ds max

Language

  • C++
  • C#
  • HLSL
  • json
Source Build AI Research paper

Project Details

Flow field pathfinding

Since Boxhead has enemies that have to find a way through the level, they needed some kind of pathfinding. There are many kinds of pathfinding and I chose to try A* pathfinding and flow field pathfinding. The game strongly relies on a grid system for object placement so I could leverage that to simplify a pathfinding system. I began with researching how flow fields work and how they were created. I started with making the heat map which indicates how far all the cells are from the target position. Every cell stores a number that holds that distance. After doing this, I had the data I needed to create the actual flow field vectors for each cell. Calculating the vectors for each cell is actually quite easy, you simple subtract the left cell’s distance from the right cell for the x value and the same for the y value but then with the lower and the upper cell respectively. Visualizing this resulted in a quite satisfying image. (see image) With the flow field, I could simply calculate the closest cell to the agent and set the velocity of that agent to the vector of that cell.

Flow field pathfinding and A* pathfinding

The main reason I chose to implement two different types of pathfinding is because I was interested to know how flow field agents compete with A* agents. I read almost everywhere that A* pathfinding is the standard and the “best” pathfinding solution. My goal was to both have them working in the game and eventually compare them both in different situations. As expected, A* pathfinding performed better in general, with few agents but because Boxhead is a never-ending game, the amount of agents keeps increasing meaning that the A* algorithm needs to calculate the path for every agent which had a great impact on the performance. Of course, there are a few simple improvements I wrote so that the paths are only recalculated when needed and that agents can acquire paths from other agents. But still, after a while, it hits the performance quite hard. That’s where flow fields come in to play. The big advantage is that a flow field only has to be calculated once for all agents meaning that it is more efficient from a certain amount of agents because it takes longer to calculate a flow field than an agents path with A*. To visualize this, I made a very simple graph.

I’ve always been interested in interaction with network but I had absolutely no experience with that. I wanted to make a highscore system so that players could see other players their scores. This required me to do some reading up on networking. I found a great website called Dreamlo.com that hosts free scoreboards using a really simple system. To get all the highscores you make an http GET request and it sends all the highscores. To upload a highscore, you just add the name of the player and his score to the URL. The page being targeted is a php page that has logic to query a database. Since not every player would be able to connect to the internet when they were playing, I made a class that handles the online scores and a class that handles the offline scores that are saved on the client’s computer. Both systems are quite alike only that the online system makes the http request. The method that makes the request itself is made so that it can do both the uploading of scores and the receiving of scores. On the right is a part of the OnlineHighscore class. For this, I also made a small exception handling class called “SocketError” to I could easily debug any event and log it to the console.

void OnlineLeaderboard::UploadScore(const string& name, int score, float m_PlayTime) const
{
	//Format the name
	string newName = FormatName(name);
	Logger::LogInfo(L"OnlineLeaderboard::UploadScore > Adding new score...");
	
	//Make path
	stringstream path;
	path << "/lb/" << PRIVATE_LINK << "/add/" << newName << "/" << score << "/" << (int)trunc(m_PlayTime);

	//Make a request to the server
	try{
		string response = HttpGet(HOST, path.str());
	}
	catch (SocketError &error){
		Logger::LogWarning(error.GetMessageW());
	}
	Logger::LogInfo(L"OnlineLeaderboard::UploadScore() > Successful");
}

string OnlineLeaderboard::HttpGet(string host, string path) const
{
	WSADATA wsaData;
	int result;

	// Initialize Winsock
	result = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (result != 0)
		throw SocketError(L"WSAStartUp", result);

	// Resolve the server address and port
	addrinfo * pAddrInfo;
	result = getaddrinfo(host.c_str(), "80", 0, &pAddrInfo);
	if (result != 0)
		throw SocketError(L"addrinfo", result);

	//Create the socket
	SOCKET sock = socket(pAddrInfo->ai_family, pAddrInfo->ai_socktype, pAddrInfo->ai_protocol);
	if (sock == INVALID_SOCKET)
		throw SocketError(L"Socket", WSAGetLastError());

	// Connect to server.
	result = connect(sock, pAddrInfo->ai_addr, pAddrInfo->ai_addrlen);
	if (result != 0)
		throw SocketError(L"Connect", WSAGetLastError());

	const string request = "GET " + path + " HTTP/1.1\nHost: " + host + "\n\n";

	// Send an initial buffer
	result = send(sock, request.c_str(), request.size(), 0);
	if (result == SOCKET_ERROR)
		throw SocketError(L"Send", WSAGetLastError());

	// shutdown the connection since no more data will be sent
	result = shutdown(sock, SD_SEND);
	if (result == SOCKET_ERROR)
		throw SocketError(L"Close send connection", WSAGetLastError());

	// Receive until the peer closes the connection
	string response;
	char buffer[BUFFER_SIZE];
	int bytesRecv = 0;

	for (;;)
	{
		result = recv(sock, buffer, sizeof(buffer), 0);
		if (result == SOCKET_ERROR)
			throw SocketError(L"Recv", WSAGetLastError());
		if (result == 0)
			break;
		response += string(buffer, result);
		Logger::LogFormat(LogLevel::Info, L"OnlineLeaderboard::HttpGet() > Bytes received: %i", result);
		bytesRecv += result;
	}
	Logger::LogFormat(LogLevel::Info, L"OnlineLeaderboard::HttpGet() > Recv Completed. Total bytes received: %i", bytesRecv);

	// cleanup
	result = closesocket(sock);
	if (result == SOCKET_ERROR)
		throw SocketError(L"Closesocket", WSAGetLastError());

	result = WSACleanup();
	if (result == SOCKET_ERROR)
		throw SocketError(L"WSACleanup", WSAGetLastError());
	freeaddrinfo(pAddrInfo);
	Logger::LogFormat(LogLevel::Info, L"OnlineLeaderboard::HttpGet() > Cleanup successful", bytesRecv);

	return response;
}

Click on an image to enlarge