game: working leaderboard

This commit is contained in:
Mark Joshwel 2024-11-19 21:03:04 +08:00
parent 4e3e0c16c3
commit 18c91a7393
7 changed files with 159 additions and 56 deletions

View file

@ -137,6 +137,7 @@ GameObject:
- component: {fileID: 133964673}
- component: {fileID: 133964674}
- component: {fileID: 133964675}
- component: {fileID: 133964678}
m_Layer: 5
m_Name: UI
m_TagString: Untagged
@ -243,6 +244,19 @@ MonoBehaviour:
lightness: 0
chroma: 0
hue: 0
--- !u!114 &133964678
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 133964670}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f58221274607ad145b45792b8649c87f, type: 3}
m_Name:
m_EditorClassIdentifier:
leaderboardEntryTemplate: {fileID: 9197481963319205126, guid: a1369c8749f2489cbd8359616a351762, type: 3}
--- !u!1 &447905425
GameObject:
m_ObjectHideFlags: 0

View file

@ -782,9 +782,35 @@ private void GetBestScores(Action<TransactionResult, List<LocalPlayerData.Score>
/// callback function that takes in a <c>TransactionResult</c> enum and a <c>List&lt;LeaderboardEntry&gt;</c>
/// </param>
public void GetLeaderboard(
Action<TransactionResult, LeaderboardEntry[]> callback)
Action<TransactionResult, List<LeaderboardEntry>> callback)
{
throw new NotImplementedException();
_db.Child("users")
.OrderByChild("rating")
.LimitToLast(LeaderboardUI.MaxEntries)
.GetValueAsync()
.ContinueWithOnMainThread(task =>
{
if (!task.IsCompletedSuccessfully)
{
Debug.LogError(task.Exception);
callback(TransactionResult.Error, new List<LeaderboardEntry>(0));
return;
}
var entries = new List<LeaderboardEntry>();
foreach (var child in task.Result.Children)
try
{
var entry = new LeaderboardEntry(child.Value as Dictionary<string, object>);
entries.Add(entry);
}
catch (Exception e)
{
Debug.LogError(e);
}
callback(TransactionResult.Ok, entries);
});
}
/// <summary>
@ -867,21 +893,4 @@ private void GetBestScores(Action<TransactionResult, List<LocalPlayerData.Score>
throw new ArgumentOutOfRangeException(nameof(target), target, null);
}
}
/// <summary>
/// struct for a leaderboard entry
/// </summary>
public struct LeaderboardEntry
{
public string Username;
public float Rating;
public int PlayCount;
public LeaderboardEntry(string username, float rating, int playCount)
{
Username = username;
Rating = rating;
PlayCount = playCount;
}
}
}

View file

@ -211,6 +211,8 @@ public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
historicalHueAcc /= historicalGames;
historicalPerceivedAcc /= historicalGames;
Debug.Log($"historical averages: L={historicalLightnessAcc:F2}, C={historicalChromaAcc:F2}, h={historicalHueAcc:F2}, dE={historicalPerceivedAcc:F2}");
// calculate round averages
var gameLightnessAcc = 0d;
var gameChromaAcc = 0d;

View file

@ -1,7 +1,8 @@
using System;
using System.Collections.Generic;
using Unity.VisualScripting;
using System.Linq;
using UnityEngine;
using UnityEngine.UIElements;
/// <summary>
/// class that loads leaderboard data and displays it in the UI
@ -11,12 +12,22 @@ public class LeaderboardUI : MonoBehaviour
/// <summary>
/// maximum number of entries to display in the leaderboard
/// </summary>
private const int MaxEntries = 10;
public const int MaxEntries = 50;
/// <summary>
/// uxml template for a leaderboard entry
/// </summary>
[SerializeField] private VisualTreeAsset leaderboardEntryTemplate;
/// <summary>
/// leaderboard data
/// </summary>
private List<Backend.LeaderboardEntry> _leaderboardData = new(MaxEntries);
private List<LeaderboardEntry> _leaderboardData = new(MaxEntries);
/// <summary>
/// reference to the leaderboard scroll view
/// </summary>
private ScrollView _leaderboardScrollView;
/// <summary>
/// register callbacks
@ -25,11 +36,12 @@ private void OnEnable()
{
UIManager.Instance.RegisterOnDisplayStateChangeCallback((_, newState) =>
{
if (newState == UIManager.DisplayState.LeaderboardView)
{
LoadLeaderboardData();
}
if (newState == UIManager.DisplayState.LeaderboardView) LoadLeaderboardData();
});
_leaderboardScrollView = UIManager.Instance.UI.Q<ScrollView>("LeaderboardListContent");
if (_leaderboardScrollView == null)
throw new NullReferenceException("leaderboard scroll view not found in the UI");
}
/// <summary>
@ -60,10 +72,69 @@ private void LoadLeaderboardData()
}
/// <summary>
/// render leaderboard data
/// render leaderboard data in the UI
/// </summary>
/// <param name="message">message to display in the leaderboard in lieu of actual data</param>
/// <exception cref="NullReferenceException">thrown when the leaderboard scroll view is missing or when the leaderboard entry</exception>
private void RenderLeaderboardData(string message = "")
{
// render leaderboard data
_leaderboardScrollView.Clear();
if (!string.IsNullOrEmpty(message))
{
Debug.Log($"rendering leaderboard with message entry-in-lieu: '{message}'");
_leaderboardScrollView.Add(BuildEntryElement("", message, ""));
return;
}
Debug.Log($"rendering {_leaderboardData.Count} leaderboard entries");
foreach (var (entry, index) in _leaderboardData.Take(MaxEntries).Reverse().Select((entry, index) => (entry, index)))
_leaderboardScrollView.Add(BuildEntryElement((index + 1).ToString(), entry.Username, $"{entry.Rating:F3}"));
}
/// <summary>
/// build a leaderboard entry element
/// </summary>
/// <param name="position">leaderboard position as a string</param>
/// <param name="username">score holder's username</param>
/// <param name="rating">score holder's rating</param>
/// <returns>a visual element representing a leaderboard entry</returns>
/// <exception cref="NullReferenceException">thrown when the leaderboard entry template is missing required elements</exception>
private TemplateContainer BuildEntryElement(string position, string username, string rating)
{
var template = leaderboardEntryTemplate.Instantiate();
var templatePositionText = template.Q<Label>("EntryRankPosition");
var templateUsernameText = template.Q<Label>("EntryNameText");
var templateRatingText = template.Q<Label>("EntryRatingText");
if (templatePositionText == null || templateUsernameText == null || templateRatingText == null)
throw new NullReferenceException("leaderboard entry template is missing required elements");
templatePositionText.text = position;
templateUsernameText.text = username;
templateRatingText.text = rating;
return template;
}
}
public readonly struct LeaderboardEntry
{
public readonly string Username;
public readonly float Rating;
// public LeaderboardEntry(string username, float rating)
// {
// Username = username;
// Rating = rating;
// }
//
public LeaderboardEntry(Dictionary<string, object> data)
{
if (!data.ContainsKey("username") || data["username"] is not string username)
throw new ArgumentException("data['username'] not found or invalid");
Username = username;
Rating = LocalPlayerData.GetFloatyKey(data, "rating");
}
}

View file

@ -209,6 +209,25 @@ public float CalculateUserRating()
return (float)rating;
}
/// <summary>
/// safely get a float value from a dictionary
/// </summary>
/// <param name="data">the dictionary to get the value from</param>
/// <param name="key">the key to get the value from</param>
/// <returns>the float value</returns>
/// <exception cref="ArgumentException">thrown if the key is not found, or the value is not a valid float</exception>
public static float GetFloatyKey(Dictionary<string, object> data, string key)
{
if (!data.TryGetValue(key, out var possibleFloat)) throw new ArgumentException($"{key} not found");
return possibleFloat switch
{
double f => (float)f,
long l => l,
_ => throw new ArgumentException($"data['{key}'] not a valid float")
};
}
public struct Score
{
/// <summary>
@ -277,10 +296,10 @@ public Score(Dictionary<string, object> data)
if (!data.ContainsKey("noOfRounds") || data["noOfRounds"] is not long noOfRounds)
throw new ArgumentException("noOfRounds not found or invalid");
var avgLightnessAccuracy = GetFloatyKey("avgLightnessAccuracy");
var avgChromaAccuracy = GetFloatyKey("avgChromaAccuracy");
var avgHueAccuracy = GetFloatyKey("avgHueAccuracy");
var avgPerceivedAccuracy = GetFloatyKey("avgPerceivedAccuracy");
var avgLightnessAccuracy = GetFloatyKey(data, "avgLightnessAccuracy");
var avgChromaAccuracy = GetFloatyKey(data, "avgChromaAccuracy");
var avgHueAccuracy = GetFloatyKey(data, "avgHueAccuracy");
var avgPerceivedAccuracy = GetFloatyKey(data, "avgPerceivedAccuracy");
Timestamp = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime;
NoOfRounds = (int)noOfRounds;
@ -288,19 +307,6 @@ public Score(Dictionary<string, object> data)
AvgChromaAccuracy = avgChromaAccuracy;
AvgHueAccuracy = avgHueAccuracy;
AvgPerceivedAccuracy = avgPerceivedAccuracy;
return;
float GetFloatyKey(string key)
{
if (!data.TryGetValue(key, out var possibleFloat)) throw new ArgumentException($"{key} not found");
return possibleFloat switch
{
double f => (float)f,
long l => l,
_ => throw new ArgumentException($"{key} not a valid float")
};
}
}
/// <summary>

View file

@ -108,19 +108,20 @@
</ui:VisualElement>
<ui:VisualElement name="LeaderboardView" style="flex-grow: 1; display: flex; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin-top: 3.25%; margin-right: 3.25%; margin-bottom: 3.25%; margin-left: 3.25%; flex-direction: column; justify-content: space-between;">
<ui:Label tabindex="-1" text="Leaderboard" parse-escape-sequences="true" display-tooltip-when-elided="true" name="LeaderboardHeader" style="font-size: 58px; -unity-font-style: normal;" />
<ui:VisualElement name="Deliberate" style="flex-grow: 0; display: none;">
<ui:VisualElement name="LeaderboardContent" style="height: 80%; flex-direction: column;">
<ui:VisualElement name="LeaderboardEntryHeader">
<ui:Label tabindex="-1" text="Rank" parse-escape-sequences="true" display-tooltip-when-elided="true" name="EntryRankPosition" style="white-space: nowrap; width: 10%;" />
<ui:Label tabindex="-1" text="Username" parse-escape-sequences="true" display-tooltip-when-elided="true" name="EntryNameText" style="white-space: nowrap; width: 70%;" />
<ui:Label tabindex="-1" text="Rating" parse-escape-sequences="true" display-tooltip-when-elided="true" name="EntryRatingText" style="white-space: nowrap; width: 20%; -unity-text-align: upper-right;" />
</ui:VisualElement>
<ui:ScrollView name="LeaderboardListContent" vertical-scroller-visibility="Hidden" horizontal-scroller-visibility="Hidden">
<ui:VisualElement name="LeaderboardEntryReference" style="display: flex;">
<ui:Label tabindex="-1" text="#1" parse-escape-sequences="true" display-tooltip-when-elided="true" name="EntryRankPosition" style="white-space: nowrap; width: 10%;" />
<ui:Label tabindex="-1" text="Jitomi Monoe" parse-escape-sequences="true" display-tooltip-when-elided="true" name="EntryNameText" style="white-space: nowrap; width: 70%;" />
<ui:Label tabindex="-1" text="000.000" parse-escape-sequences="true" display-tooltip-when-elided="true" name="EntryRatingText" style="white-space: nowrap; width: 20%; -unity-text-align: upper-right;" />
</ui:VisualElement>
</ui:ScrollView>
</ui:VisualElement>
<ui:ListView name="LeaderboardListView" style="display: flex; visibility: visible;" />
</ui:VisualElement>
<ui:VisualElement name="AccountView" style="flex-grow: 1; display: none; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin-top: 3.25%; margin-right: 3.25%; margin-bottom: 3.25%; margin-left: 3.25%; flex-direction: column; justify-content: space-between;">
<ui:Label tabindex="-1" text="You are not signed in." parse-escape-sequences="true" display-tooltip-when-elided="true" name="AccountHeader" style="font-size: 58px; -unity-font-style: normal;" />

View file

@ -13,7 +13,7 @@ PlayerSettings:
useOnDemandResources: 0
accelerometerFrequency: 60
companyName: Mark Joshwel
productName: Colour Me OK
productName: ColourMeOK
defaultCursor: {fileID: 0}
cursorHotspot: {x: 0, y: 0}
m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1}