Solo game jam project, made during my 1st year at BUas
last updated 01.12.22
POLYSPACE is a game I made in October 2020 as a weekend game jam organized by our school. It ended up winning the #1 spot :D
I wanted to make a cool and slightly hard speedrunning game with lots of visual juice because it seemed fun, so I went ahead and did it.
The game features a few interesting mechanics and many VFX to spice it up. The use of the double-jump dictates how fast you're going to be.
I thought it would be a cool idea to implement a global leaderboard, to keep players coming back to the game and create a sense of competition between players. This was a very successful idea and it really created competition between the top 10 players.
🏅 Global Leaderboard
Player Name | Time |
---|---|
Loading... | Loading... |
Loading... | Loading... |
Loading... | Loading... |
Loading... | Loading... |
Loading... | Loading... |
Loading... | Loading... |
Loading... | Loading... |
Loading... | Loading... |
Loading... | Loading... |
Loading... | Loading... |
I really like escaping from the engine and enabling the game to communicate with external resources or do remote things, so for POLYSPACE I implemented a global leaderboard system.
At large, it communicates through HTTP requests to my server, which stores the scores, sorts them by lowest time, and returns them to a static leaderboard page, which I then pull back & display in the game.
The in-game part in made in C++ which exposes some functions to Blueprints to tie the system together, whereas the server part is made in PHP. The data is sent in JSON format to make it easy for both ends.
💫 The catch of POLYSPACE
Because it's such a small game with few mechanics, I thought I'd put a spin on those mechanics. The double jump is a vital part of the game.
In short, the double jump pushes you higher, the sooner you follow up the first jump with the second one. This is the other way around than what you would usually see with other double jump implementations.
When playtesting, this was often confusing and players wouldn't even manage to finish the first level, but an interesting pattern emerged...
While some players couldn't even pass a level, those who could ended up playing for upwards of 1-2 hours, to improve their performance and score!
⚙️ Global Leaderboard - System Breakdown
1 2 3 4 5 6 7 8 9 10 11 12 13 | void ALeaderboardManager::FetchScores(FString ActualPlayerName) { TSharedRef<IHttpRequest> Request = Http->CreateRequest(); Request->OnProcessRequestComplete().BindUObject(this, &ALeaderboardManager::OnResponseReceived); PlayerName = ActualPlayerName; Request->SetURL("https://stats.kronorite.com/polyspace/leaderboard.php"); Request->SetVerb("GET"); Request->SetHeader(TEXT("User-Agent"), "X-UnrealEngine-Agent"); Request->SetHeader("Content-Type", TEXT("application/json")); Request->ProcessRequest(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | void ALeaderboardManager::PushScores(FString ActualPlayerName, float FinishTime) { TSharedRef<IHttpRequest> Request = Http->CreateRequest(); FetchScores(ActualPlayerName); int seconds = FinishTime*100; FString FFinish = FString::Printf(TEXT("%d"), seconds); Request->SetURL("https://stats.kronorite.com/polyspace/retrieve.php?playername=" + ActualPlayerName + "&score=" + FFinish); Request->SetVerb("GET"); Request->SetHeader(TEXT("User-Agent"), "X-UnrealEngine-Agent"); Request->SetHeader("Content-Type", TEXT("application/json")); Request->ProcessRequest(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | void ALeaderboardManager::OnResponseReceived(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) { ScoreTableString.Empty(); ScoreTableInt.Empty(); TPair<int, FString> Entry; TArray<TPair<int, FString>> ScoreTable; TSharedPtr<FJsonObject> JsonObject; TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString()); if (FJsonSerializer::Deserialize(Reader, JsonObject)) { // fetch data for (auto it : JsonObject->Values) { Entry.Key = it.Value->AsNumber(); Entry.Value = it.Key; ScoreTable.Add(Entry); if (it.Key == PlayerName) { int miliseconds = Entry.Key; FString minutes = FString::Printf(TEXT("%d"), (miliseconds / 6000)); FString seconds = FString::Printf(TEXT("%d"), ((miliseconds / 100) % 60)); FString FFinish = FString::Printf(TEXT("%02d:%02d:%02d"), (miliseconds / 6000), ((miliseconds / 100) % 60), (miliseconds % 100)); BestScore = FFinish; } } ScoreTable.Sort(); for (int i = 0; i < 10 && i < ScoreTable.Num(); i++) { int miliseconds = ScoreTable[i].Key; FString minutes = FString::Printf(TEXT("%d"), (miliseconds / 6000)); FString seconds = FString::Printf(TEXT("%d"), ((miliseconds / 100) % 60)); FString FFinish = FString::Printf(TEXT("%02d:%02d:%02d"), (miliseconds / 6000), ((miliseconds / 100) % 60), (miliseconds % 100)); ScoreTableString.Add(ScoreTable[i].Value); ScoreTableInt.Add(FFinish); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?php session_start(); $score = $_GET['score']; $playername = $_GET['playername']; // debug echo $playername . ": " . $score; $content = $score; if (!file_exists($playername . ".dat") || $score < file_get_contents($playername . ".dat")) { $fp = fopen($playername . ".dat", "w"); fwrite($fp,$content); fclose($fp); } else {echo '';} ?> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?php $fileList = glob("*.dat"); $filecount = count($fileList); foreach ($fileList as $fl) { $contents = file_get_contents($fl); $filename = substr($fl, 0, strrpos($fl, '.')); $output[$filename] = $contents; } echo json_encode($output); ?> |
The End!
Go check out one of my other projects!