354 lines
No EOL
15 KiB
C#
354 lines
No EOL
15 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
|
|
/// <summary>
|
|
/// singleton for a single source of truth game state and flow management
|
|
/// </summary>
|
|
public class GameManager : MonoBehaviour
|
|
{
|
|
/// <summary>
|
|
/// singleton pattern: define instance field for accessing the singleton elsewhere
|
|
/// </summary>
|
|
public static GameManager Instance;
|
|
|
|
/// <summary>
|
|
/// ui manager object for handling ui state and flow
|
|
/// </summary>
|
|
public UIManager ui;
|
|
|
|
/// <summary>
|
|
/// list of callbacks to call when the local player data changes
|
|
/// </summary>
|
|
private readonly List<Action<LocalPlayerData>> _onLocalPlayerDataChangeCallbacks = new();
|
|
|
|
/// <summary>
|
|
/// the local player data object for storing player data
|
|
/// </summary>
|
|
private LocalPlayerData _data;
|
|
|
|
/// <summary>
|
|
/// backend object for handling communication with the firebase backend
|
|
/// </summary>
|
|
public Backend Backend;
|
|
|
|
/// <summary>
|
|
/// gameplay object for handling game loop
|
|
/// </summary>
|
|
public Gameplay Gameplay;
|
|
|
|
/// <summary>
|
|
/// read-only property for accessing the local player data outside this class
|
|
/// </summary>
|
|
public LocalPlayerData Data => _data;
|
|
|
|
/// <summary>
|
|
/// enforces singleton behaviour; sets doesn't destroy on load and checks for multiple instances
|
|
/// </summary>
|
|
private void Awake()
|
|
{
|
|
// check if instance hasn't been set yet
|
|
if (Instance == null)
|
|
{
|
|
Debug.Log("awake as singleton instance, setting self as the forever-alive instance");
|
|
Instance = this;
|
|
DontDestroyOnLoad(gameObject);
|
|
}
|
|
// check if instance is already set and it's not this instance
|
|
else if (Instance != null && Instance != this)
|
|
{
|
|
Debug.Log("awake as non-singleton instance, destroying self");
|
|
Destroy(gameObject);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// start modifying state
|
|
/// </summary>
|
|
private void Start()
|
|
{
|
|
Debug.Log("GameManager starts here");
|
|
_data.LoadFromTheWorld(FireLocalPlayerDataChangeCallbacks);
|
|
}
|
|
|
|
/// <summary>
|
|
/// initialise variables and ui elements
|
|
/// </summary>
|
|
private void OnEnable()
|
|
{
|
|
ui = UIManager.Instance;
|
|
|
|
// load the local player data and refresh the ui
|
|
_data = new LocalPlayerData();
|
|
|
|
Backend = new Backend();
|
|
Backend.Initialise(status =>
|
|
{
|
|
Debug.Log("initialised backend, setting connection status text");
|
|
|
|
ui.UI.Q<Label>("ConnectionStatusText").text = status switch
|
|
{
|
|
Backend.FirebaseConnectionStatus.Connected => "Status: Connected",
|
|
Backend.FirebaseConnectionStatus.Updating => "Status: Updating... (Retrying in a bit!)",
|
|
Backend.FirebaseConnectionStatus.NotConnected => "Status: Disconnected",
|
|
Backend.FirebaseConnectionStatus.UpdateRequired =>
|
|
"Status: Disconnected (Device Component Update Required)",
|
|
Backend.FirebaseConnectionStatus.ExternalError => "Status: Disconnected (External/Device Error)",
|
|
Backend.FirebaseConnectionStatus.InternalError => "Status: Disconnected (Internal Error)",
|
|
_ => "Status: Disconnected (unknown fcs state, this is unreachable and a bug)"
|
|
};
|
|
|
|
if (status == Backend.FirebaseConnectionStatus.Connected) return;
|
|
|
|
// if we're not connected, hide any online 'features'
|
|
ui.UI.Q<Button>("LeaderboardButton").style.display = DisplayStyle.None;
|
|
ui.UI.Q<Button>("AccountButton").style.display = DisplayStyle.None;
|
|
});
|
|
|
|
// register a callback to refresh the ui when the player signs in
|
|
Backend.RegisterOnSignInCallback(_ =>
|
|
{
|
|
Debug.Log("sign in callback, refreshing GameManager-controlled SideView UI");
|
|
_data.LoadFromTheWorld(FireLocalPlayerDataChangeCallbacks);
|
|
});
|
|
|
|
Backend.RegisterOnConnectionStatusChangedCallback(status =>
|
|
{
|
|
Debug.Log($"post-fcStatus change, deciding to show/hide buttons based on new status: {status}");
|
|
ui.UI.Q<Button>("LeaderboardButton").style.display =
|
|
status == Backend.FirebaseConnectionStatus.Connected
|
|
? DisplayStyle.Flex
|
|
: DisplayStyle.None;
|
|
ui.UI.Q<Button>("AccountButton").style.display =
|
|
status == Backend.FirebaseConnectionStatus.Connected
|
|
? DisplayStyle.Flex
|
|
: DisplayStyle.None;
|
|
});
|
|
|
|
Gameplay = new Gameplay(ui.UI);
|
|
}
|
|
|
|
/// <summary>
|
|
/// called when the application is quitting, saves the local player data
|
|
/// </summary>
|
|
private void OnApplicationQuit()
|
|
{
|
|
Debug.Log("running deferred cleanup/save functions");
|
|
Backend.Cleanup();
|
|
_data.SaveToTheWorld();
|
|
}
|
|
|
|
/// <summary>
|
|
/// function to register a callback to be called when the local player data changes
|
|
/// </summary>
|
|
/// <param name="callback">callback function that takes a <c>LocalPlayerData</c> object</param>
|
|
public void RegisterOnLocalPlayerDataChangeCallback(Action<LocalPlayerData> callback)
|
|
{
|
|
_onLocalPlayerDataChangeCallbacks.Add(callback);
|
|
Debug.Log($"registering LocalPlayerDataChangeCallback ({_onLocalPlayerDataChangeCallbacks.Count})");
|
|
}
|
|
|
|
/// <summary>
|
|
/// function to fire all local player data change callbacks
|
|
/// </summary>
|
|
public void FireLocalPlayerDataChangeCallbacks(LocalPlayerData data)
|
|
{
|
|
Debug.Log($"firing LocalPlayerDataChangeCallbacks ({_onLocalPlayerDataChangeCallbacks.Count})");
|
|
foreach (var callback in _onLocalPlayerDataChangeCallbacks)
|
|
try
|
|
{
|
|
callback.Invoke(data);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogError($"error invoking LocalPlayerDataChangeCallback: {e.Message}");
|
|
}
|
|
}
|
|
|
|
public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
|
|
{
|
|
Debug.Log("signalling game end");
|
|
|
|
// calculate historical averages
|
|
var historicalLightnessAcc = 0f;
|
|
var historicalChromaAcc = 0f;
|
|
var historicalHueAcc = 0f;
|
|
var historicalPerceivedAcc = 0f;
|
|
var historicalGames = 0;
|
|
|
|
foreach (var localScore in _data.RecentLocalScores.Take(LocalPlayerData.MaxRecentScores))
|
|
{
|
|
historicalLightnessAcc += localScore.AvgLightnessAccuracy;
|
|
historicalChromaAcc += localScore.AvgChromaAccuracy;
|
|
historicalHueAcc += localScore.AvgHueAccuracy;
|
|
historicalPerceivedAcc += localScore.AvgPerceivedAccuracy;
|
|
historicalGames++;
|
|
}
|
|
|
|
foreach (var onlineScore in _data.RecentOnlineScores.Take(LocalPlayerData.MaxRecentScores))
|
|
{
|
|
historicalLightnessAcc += onlineScore.AvgLightnessAccuracy;
|
|
historicalChromaAcc += onlineScore.AvgChromaAccuracy;
|
|
historicalHueAcc += onlineScore.AvgHueAccuracy;
|
|
historicalPerceivedAcc += onlineScore.AvgPerceivedAccuracy;
|
|
historicalGames++;
|
|
}
|
|
|
|
foreach (var onlineScore in _data.BestOnlineScores.Take(LocalPlayerData.MaxBestScores))
|
|
{
|
|
historicalLightnessAcc += onlineScore.AvgLightnessAccuracy;
|
|
historicalChromaAcc += onlineScore.AvgChromaAccuracy;
|
|
historicalHueAcc += onlineScore.AvgHueAccuracy;
|
|
historicalPerceivedAcc += onlineScore.AvgPerceivedAccuracy;
|
|
historicalGames++;
|
|
}
|
|
|
|
historicalGames = Math.Max(1, historicalGames);
|
|
historicalLightnessAcc /= historicalGames;
|
|
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;
|
|
var gameChromaAcc = 0d;
|
|
var gameHueAcc = 0d;
|
|
var gamePerceivedAcc = 0d;
|
|
var templateColour = Color.clear;
|
|
var responseColour = Color.clear;
|
|
var roundNumber = 1;
|
|
foreach (var distance in playedRounds.Take(Gameplay.RoundsPerGame).Select(round =>
|
|
Colorimetry.CalculateDistance(templateColour = round.TemplateColour,
|
|
responseColour = round.ResponseColour)))
|
|
{
|
|
var dLCh = Colorimetry.CalculateLChSimilarityPercentage(distance);
|
|
|
|
Debug.Log(
|
|
$"processing round: template={templateColour}, response={responseColour} (dL%={dLCh.L}, dC%={dLCh.C}, dh%={dLCh.h}, dEok={distance.dE:F})");
|
|
|
|
var roundLightnessAcc = Math.Clamp(dLCh.L * 100d, 0d, 100d);
|
|
var roundChromaAcc = Math.Clamp(dLCh.C * 100d, 0d, 100d);
|
|
var roundHueAcc = Math.Clamp(dLCh.h * 100d, 0d, 100d);
|
|
var roundPerceivedAcc = Math.Clamp((1d - distance.dE) * 100d, 0d, 100d);
|
|
|
|
gameLightnessAcc += roundLightnessAcc;
|
|
gameChromaAcc += roundChromaAcc;
|
|
gameHueAcc += roundHueAcc;
|
|
gamePerceivedAcc += roundPerceivedAcc;
|
|
|
|
var showcaseTemplate = ui.UI.Q<VisualElement>($"ShowcasePair{roundNumber}TemplateColour");
|
|
var showcaseResponse = ui.UI.Q<VisualElement>($"ShowcasePair{roundNumber}ResponseColour");
|
|
var showcaseInfo = ui.UI.Q<Label>($"ShowcasePair{roundNumber}Info");
|
|
|
|
if (!(showcaseTemplate == null || showcaseResponse == null || showcaseInfo == null))
|
|
{
|
|
showcaseTemplate.style.backgroundColor = templateColour;
|
|
showcaseResponse.style.backgroundColor = responseColour;
|
|
showcaseInfo.text =
|
|
$"{roundLightnessAcc * (roundPerceivedAcc / 100d):N0}% {roundChromaAcc * (roundPerceivedAcc / 100d):N0}% {roundHueAcc * (roundPerceivedAcc / 100d):N0}% ({roundPerceivedAcc:N0}%)";
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError($"showcase pair {roundNumber} not found");
|
|
}
|
|
|
|
roundNumber++; // used for ui querying
|
|
}
|
|
|
|
gameLightnessAcc /= Gameplay.RoundsPerGame;
|
|
gameChromaAcc /= Gameplay.RoundsPerGame;
|
|
gameHueAcc /= Gameplay.RoundsPerGame;
|
|
gamePerceivedAcc /= Gameplay.RoundsPerGame;
|
|
|
|
var adjustedHistoricalLightnessAcc = historicalLightnessAcc * (historicalPerceivedAcc / 100d);
|
|
var adjustedHistoricalChromaAcc = historicalChromaAcc * (historicalPerceivedAcc / 100d);
|
|
var adjustedHistoricalHueAcc = historicalHueAcc * (historicalPerceivedAcc / 100d);
|
|
var adjustedGameLightnessAcc = gameLightnessAcc * (gamePerceivedAcc / 100d);
|
|
var adjustedGameChromaAcc = gameChromaAcc * (gamePerceivedAcc / 100d);
|
|
var adjustedGameHueAcc = gameHueAcc * (gamePerceivedAcc / 100d);
|
|
var gameAccuracy = (gameLightnessAcc + gameChromaAcc + gameHueAcc + gamePerceivedAcc) / 4d;
|
|
|
|
// make comparison texts
|
|
var lAccDeltaText = (adjustedGameLightnessAcc > adjustedHistoricalLightnessAcc ? "+" : "-") +
|
|
$"{Math.Abs(adjustedGameLightnessAcc - adjustedHistoricalLightnessAcc):F1}%";
|
|
var cAccDeltaText = (adjustedGameChromaAcc > adjustedHistoricalChromaAcc ? "+" : "-") +
|
|
$"{Math.Abs(adjustedGameChromaAcc - adjustedHistoricalChromaAcc):F1}%";
|
|
var hAccDeltaText = (adjustedGameHueAcc > adjustedHistoricalHueAcc ? "+" : "-") +
|
|
$"{Math.Abs(adjustedGameHueAcc - adjustedHistoricalHueAcc):F1}%";
|
|
|
|
var score = new LocalPlayerData.Score(DateTime.Now,
|
|
playedRounds.Count,
|
|
(float)adjustedGameLightnessAcc,
|
|
(float)adjustedGameChromaAcc,
|
|
(float)adjustedGameHueAcc,
|
|
(float)gamePerceivedAcc);
|
|
|
|
// breakpoint here: why is gamePerceivedAcc always "100"?
|
|
var oldRating = _data.CalculateUserRating();
|
|
_data.RegisterLocalScore(score);
|
|
FireLocalPlayerDataChangeCallbacks(Instance.Data);
|
|
|
|
Debug.Log("submitting score to backend");
|
|
|
|
Backend.SubmitScore(score,
|
|
submitRes =>
|
|
{
|
|
if (submitRes != Backend.TransactionResult.Ok)
|
|
{
|
|
Debug.Log("couldn't submit score");
|
|
TransitionToResultsView(_data.CalculateUserRating());
|
|
return;
|
|
}
|
|
|
|
Backend.CalculateUserRating((urcRes, userRating) =>
|
|
{
|
|
if (urcRes != Backend.TransactionResult.Ok)
|
|
{
|
|
Debug.Log("couldn't calculate user rating");
|
|
TransitionToResultsView(_data.CalculateUserRating());
|
|
return;
|
|
}
|
|
|
|
Backend.UpdateUserRating(updateRes =>
|
|
{
|
|
if (updateRes != Backend.TransactionResult.Ok)
|
|
{
|
|
Debug.Log("calculated user rating but couldn't update it");
|
|
TransitionToResultsView(userRating);
|
|
return;
|
|
}
|
|
|
|
TransitionToResultsView(userRating);
|
|
});
|
|
});
|
|
});
|
|
|
|
return;
|
|
|
|
void TransitionToResultsView(float rating)
|
|
{
|
|
Debug.Log("signal GameManager-UIManager transition to results view");
|
|
|
|
var ratingDifferenceDescriptor = rating > oldRating ? "increased" : "decreased";
|
|
var ratingText = rating >= 0
|
|
? $"\nYour rating has {ratingDifferenceDescriptor} by {Math.Abs(rating - oldRating):F2}."
|
|
: "\nYour rating could not be calculated.";
|
|
|
|
// build the result text and show the results view
|
|
ui.UI.Q<Label>("ResultsText").text = string.Join(Environment.NewLine,
|
|
$"Over {playedRounds.Count:N0} rounds,",
|
|
$"you were {LocalPlayerData.UserRatingScalingF(gameAccuracy):F2}% accurate. (raw ΔEok={gameAccuracy:F2}%)",
|
|
"",
|
|
$"Lightness was {adjustedGameLightnessAcc:F2}% accurate. ({lAccDeltaText} from your average)",
|
|
$"Chroma was {adjustedGameChromaAcc:F2}% accurate. ({cAccDeltaText} from your average)",
|
|
$"Hue was {adjustedGameHueAcc:F2}% accurate. ({hAccDeltaText} from your average)") + ratingText;
|
|
|
|
ui.SetDisplayState(UIManager.DisplayState.ResultsView);
|
|
}
|
|
}
|
|
} |