game: working leaderboard
This commit is contained in:
parent
4e3e0c16c3
commit
18c91a7393
|
@ -137,6 +137,7 @@ GameObject:
|
||||||
- component: {fileID: 133964673}
|
- component: {fileID: 133964673}
|
||||||
- component: {fileID: 133964674}
|
- component: {fileID: 133964674}
|
||||||
- component: {fileID: 133964675}
|
- component: {fileID: 133964675}
|
||||||
|
- component: {fileID: 133964678}
|
||||||
m_Layer: 5
|
m_Layer: 5
|
||||||
m_Name: UI
|
m_Name: UI
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
|
@ -243,6 +244,19 @@ MonoBehaviour:
|
||||||
lightness: 0
|
lightness: 0
|
||||||
chroma: 0
|
chroma: 0
|
||||||
hue: 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
|
--- !u!1 &447905425
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
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>
|
/// callback function that takes in a <c>TransactionResult</c> enum and a <c>List<LeaderboardEntry></c>
|
||||||
/// </param>
|
/// </param>
|
||||||
public void GetLeaderboard(
|
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>
|
/// <summary>
|
||||||
|
@ -867,21 +893,4 @@ private void GetBestScores(Action<TransactionResult, List<LocalPlayerData.Score>
|
||||||
throw new ArgumentOutOfRangeException(nameof(target), target, null);
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -211,6 +211,8 @@ public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
|
||||||
historicalHueAcc /= historicalGames;
|
historicalHueAcc /= historicalGames;
|
||||||
historicalPerceivedAcc /= historicalGames;
|
historicalPerceivedAcc /= historicalGames;
|
||||||
|
|
||||||
|
Debug.Log($"historical averages: L={historicalLightnessAcc:F2}, C={historicalChromaAcc:F2}, h={historicalHueAcc:F2}, dE={historicalPerceivedAcc:F2}");
|
||||||
|
|
||||||
// calculate round averages
|
// calculate round averages
|
||||||
var gameLightnessAcc = 0d;
|
var gameLightnessAcc = 0d;
|
||||||
var gameChromaAcc = 0d;
|
var gameChromaAcc = 0d;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Unity.VisualScripting;
|
using System.Linq;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using UnityEngine.UIElements;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// class that loads leaderboard data and displays it in the UI
|
/// class that loads leaderboard data and displays it in the UI
|
||||||
|
@ -11,12 +12,22 @@ public class LeaderboardUI : MonoBehaviour
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// maximum number of entries to display in the leaderboard
|
/// maximum number of entries to display in the leaderboard
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const int MaxEntries = 10;
|
public const int MaxEntries = 50;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// uxml template for a leaderboard entry
|
||||||
|
/// </summary>
|
||||||
|
[SerializeField] private VisualTreeAsset leaderboardEntryTemplate;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// leaderboard data
|
/// leaderboard data
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// register callbacks
|
/// register callbacks
|
||||||
|
@ -25,11 +36,12 @@ private void OnEnable()
|
||||||
{
|
{
|
||||||
UIManager.Instance.RegisterOnDisplayStateChangeCallback((_, newState) =>
|
UIManager.Instance.RegisterOnDisplayStateChangeCallback((_, newState) =>
|
||||||
{
|
{
|
||||||
if (newState == UIManager.DisplayState.LeaderboardView)
|
if (newState == UIManager.DisplayState.LeaderboardView) LoadLeaderboardData();
|
||||||
{
|
|
||||||
LoadLeaderboardData();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_leaderboardScrollView = UIManager.Instance.UI.Q<ScrollView>("LeaderboardListContent");
|
||||||
|
if (_leaderboardScrollView == null)
|
||||||
|
throw new NullReferenceException("leaderboard scroll view not found in the UI");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -60,10 +72,69 @@ private void LoadLeaderboardData()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// render leaderboard data
|
/// render leaderboard data in the UI
|
||||||
/// </summary>
|
/// </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 = "")
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -209,6 +209,25 @@ public float CalculateUserRating()
|
||||||
return (float)rating;
|
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
|
public struct Score
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -277,10 +296,10 @@ public Score(Dictionary<string, object> data)
|
||||||
if (!data.ContainsKey("noOfRounds") || data["noOfRounds"] is not long noOfRounds)
|
if (!data.ContainsKey("noOfRounds") || data["noOfRounds"] is not long noOfRounds)
|
||||||
throw new ArgumentException("noOfRounds not found or invalid");
|
throw new ArgumentException("noOfRounds not found or invalid");
|
||||||
|
|
||||||
var avgLightnessAccuracy = GetFloatyKey("avgLightnessAccuracy");
|
var avgLightnessAccuracy = GetFloatyKey(data, "avgLightnessAccuracy");
|
||||||
var avgChromaAccuracy = GetFloatyKey("avgChromaAccuracy");
|
var avgChromaAccuracy = GetFloatyKey(data, "avgChromaAccuracy");
|
||||||
var avgHueAccuracy = GetFloatyKey("avgHueAccuracy");
|
var avgHueAccuracy = GetFloatyKey(data, "avgHueAccuracy");
|
||||||
var avgPerceivedAccuracy = GetFloatyKey("avgPerceivedAccuracy");
|
var avgPerceivedAccuracy = GetFloatyKey(data, "avgPerceivedAccuracy");
|
||||||
|
|
||||||
Timestamp = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime;
|
Timestamp = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime;
|
||||||
NoOfRounds = (int)noOfRounds;
|
NoOfRounds = (int)noOfRounds;
|
||||||
|
@ -288,19 +307,6 @@ public Score(Dictionary<string, object> data)
|
||||||
AvgChromaAccuracy = avgChromaAccuracy;
|
AvgChromaAccuracy = avgChromaAccuracy;
|
||||||
AvgHueAccuracy = avgHueAccuracy;
|
AvgHueAccuracy = avgHueAccuracy;
|
||||||
AvgPerceivedAccuracy = avgPerceivedAccuracy;
|
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>
|
/// <summary>
|
||||||
|
|
|
@ -108,19 +108,20 @@
|
||||||
</ui:VisualElement>
|
</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: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: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: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="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="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: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>
|
||||||
<ui:VisualElement name="LeaderboardEntryReference" style="display: flex;">
|
<ui:ScrollView name="LeaderboardListContent" vertical-scroller-visibility="Hidden" horizontal-scroller-visibility="Hidden">
|
||||||
<ui:Label tabindex="-1" text="#1" parse-escape-sequences="true" display-tooltip-when-elided="true" name="EntryRankPosition" style="white-space: nowrap; width: 10%;" />
|
<ui:VisualElement name="LeaderboardEntryReference" style="display: flex;">
|
||||||
<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="#1" parse-escape-sequences="true" display-tooltip-when-elided="true" name="EntryRankPosition" style="white-space: nowrap; width: 10%;" />
|
||||||
<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:Label tabindex="-1" text="Jitomi Monoe" parse-escape-sequences="true" display-tooltip-when-elided="true" name="EntryNameText" style="white-space: nowrap; width: 70%;" />
|
||||||
</ui:VisualElement>
|
<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:VisualElement>
|
||||||
<ui:ListView name="LeaderboardListView" style="display: flex; visibility: visible;" />
|
|
||||||
</ui:VisualElement>
|
</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: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;" />
|
<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
|
useOnDemandResources: 0
|
||||||
accelerometerFrequency: 60
|
accelerometerFrequency: 60
|
||||||
companyName: Mark Joshwel
|
companyName: Mark Joshwel
|
||||||
productName: Colour Me OK
|
productName: ColourMeOK
|
||||||
defaultCursor: {fileID: 0}
|
defaultCursor: {fileID: 0}
|
||||||
cursorHotspot: {x: 0, y: 0}
|
cursorHotspot: {x: 0, y: 0}
|
||||||
m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1}
|
m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1}
|
||||||
|
|
Reference in a new issue