game: working leaderboard
This commit is contained in:
parent
4e3e0c16c3
commit
18c91a7393
|
@ -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
|
||||
|
|
|
@ -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<LeaderboardEntry></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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -210,6 +210,8 @@ public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
|
|||
historicalChromaAcc /= historicalGames;
|
||||
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;
|
||||
|
|
|
@ -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,13 +12,23 @@ 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
|
||||
/// </summary>
|
||||
|
@ -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>
|
||||
|
@ -43,7 +55,7 @@ private void LoadLeaderboardData()
|
|||
RenderLeaderboardData("Not connected to the backend, can't load leaderboard data.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
GameManager.Instance.Backend.GetLeaderboard((result, entries) =>
|
||||
{
|
||||
if (result == Backend.TransactionResult.Ok)
|
||||
|
@ -58,12 +70,71 @@ 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");
|
||||
}
|
||||
}
|
|
@ -208,6 +208,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
|
||||
{
|
||||
|
@ -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>
|
||||
|
|
|
@ -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: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 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;" />
|
||||
|
|
|
@ -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}
|
||||
|
|
Reference in a new issue