Feat - Add Loading menju
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
using FishNet.Managing.Timing;
|
||||
using FishNet.Object;
|
||||
using FishNet.Object.Synchronizing;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ashwild.Environment
|
||||
{
|
||||
/// <summary>
|
||||
/// Owns the synchronized day/night clock and drives the stylized procedural skybox plus the
|
||||
/// directional sun. The whole sky is a pure function of a single time value, so the server only
|
||||
/// replicates one epoch (SyncVar) and every client reconstructs the identical sky locally — no
|
||||
/// per-frame traffic, and late joiners snap to the correct time of day from the replicated epoch.
|
||||
/// Mirrors SeasonManager's global-driving / [ExecuteAlways] style but networked like the registries.
|
||||
/// </summary>
|
||||
[ExecuteAlways]
|
||||
[RequireComponent(typeof(NetworkObject))]
|
||||
[AddComponentMenu("GAME/Environment/Sky Time Manager")]
|
||||
public class SkyTimeManager : NetworkBehaviour
|
||||
{
|
||||
#region Serialized Fields
|
||||
|
||||
[Header("Cycle")]
|
||||
[Tooltip("Durée d'une journée complète (24 h de jeu) en secondes réelles.")]
|
||||
[SerializeField] private float dayLengthSeconds = 1200f;
|
||||
[Tooltip("Heure de départ du serveur. 0 = minuit, 0.25 = aube, 0.5 = midi, 0.75 = crépuscule.")]
|
||||
[Range(0f, 1f)] [SerializeField] private float initialTimeOfDay = 0.3f;
|
||||
|
||||
[Header("Rendu")]
|
||||
[SerializeField] private Material skyboxMaterial;
|
||||
[SerializeField] private Light sunLight;
|
||||
[Tooltip("Orientation (azimut) de l'arc du soleil, en degrés.")]
|
||||
[SerializeField] private float sunAzimuthDegrees = 30f;
|
||||
[Tooltip("Intensité de la lumière directionnelle en plein jour (s'éteint la nuit).")]
|
||||
[SerializeField] private float maxSunIntensity = 1.1f;
|
||||
[Tooltip("Intervalle (s) de rafraîchissement de l'ambient/reflection — coûteux, pas chaque frame.")]
|
||||
[SerializeField] private float ambientRefreshInterval = 0.25f;
|
||||
|
||||
[Header("Aperçu Éditeur")]
|
||||
[Tooltip("Heure simulée hors Play pour prévisualiser le ciel dans la Scene view.")]
|
||||
[Range(0f, 1f)] [SerializeField] private float editorPreviewTime = 0.3f;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Networked State
|
||||
|
||||
/// <summary>
|
||||
/// Server time (synchronized seconds) captured when the cycle's zero-point was set. Written
|
||||
/// once on server start and only again on an explicit SetTimeOfDay. Clients derive the current
|
||||
/// time of day from this plus their own corrected TimeManager tick.
|
||||
/// </summary>
|
||||
private readonly SyncVar<double> startServerTime = new SyncVar<double>();
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
|
||||
private static readonly int TimeOfDayId = Shader.PropertyToID("_TimeOfDay");
|
||||
private static readonly int SunDirId = Shader.PropertyToID("_SunDirection");
|
||||
private static readonly int SkyTimeId = Shader.PropertyToID("_SkyTime");
|
||||
|
||||
private float ambientTimer;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public State
|
||||
|
||||
public static SkyTimeManager Instance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current normalized time of day (0..1, midnight → midnight), identical on every client.
|
||||
/// </summary>
|
||||
public float TimeOfDay01 { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
|
||||
/// <summary>
|
||||
/// Validates the editor references and drives the editor preview when the game is not running.
|
||||
/// </summary>
|
||||
private void OnEnable()
|
||||
{
|
||||
WarnIfUnassigned();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps tuning and refreshes the sky preview live while editing. Does not rotate the sun
|
||||
/// transform here — Unity forbids transform writes during OnValidate; the shader reads the sun
|
||||
/// direction from a global computed analytically, and Update rotates the actual Light.
|
||||
/// </summary>
|
||||
private void OnValidate()
|
||||
{
|
||||
dayLengthSeconds = Mathf.Max(1f, dayLengthSeconds);
|
||||
ambientRefreshInterval = Mathf.Max(0.01f, ambientRefreshInterval);
|
||||
if (!Application.isPlaying) ApplyEditorPreview(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In Play, recomputes the synced time of day from the network clock and drives the sky; out
|
||||
/// of Play, drives the sky from the editor preview slider without touching the network stack.
|
||||
/// </summary>
|
||||
private void Update()
|
||||
{
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
ApplyEditorPreview(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!base.IsSpawned || base.TimeManager == null) return;
|
||||
|
||||
double now = base.TimeManager.TicksToTime(TickType.Tick);
|
||||
double elapsed = now - startServerTime.Value;
|
||||
|
||||
TimeOfDay01 = Mathf.Repeat(initialTimeOfDay + (float)(elapsed / dayLengthSeconds), 1f);
|
||||
ApplyVisuals(TimeOfDay01, (float)elapsed, true);
|
||||
RefreshAmbientThrottled(Time.deltaTime);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Network Lifecycle
|
||||
|
||||
/// <summary>
|
||||
/// Claims the singleton and binds the skybox material + sun so every client renders the sky.
|
||||
/// </summary>
|
||||
public override void OnStartNetwork()
|
||||
{
|
||||
base.OnStartNetwork();
|
||||
Instance = this;
|
||||
BindRenderSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-only: stamps the cycle's zero-point once so the SyncVar replicates the current time
|
||||
/// of day (offset by initialTimeOfDay) to every present and future client.
|
||||
/// </summary>
|
||||
public override void OnStartServer()
|
||||
{
|
||||
base.OnStartServer();
|
||||
startServerTime.Value = base.TimeManager.TicksToTime(TickType.Tick);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the singleton — mirrors OnStartNetwork.
|
||||
/// </summary>
|
||||
public override void OnStopNetwork()
|
||||
{
|
||||
base.OnStopNetwork();
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// Server-only: jumps the whole world to a given time of day by rewriting the epoch, which the
|
||||
/// SyncVar replicates so all clients snap together. Ignored when called off the server.
|
||||
/// </summary>
|
||||
public void SetTimeOfDay(float timeOfDay01)
|
||||
{
|
||||
if (!base.IsServerInitialized)
|
||||
{
|
||||
Debug.LogWarning("[SkyTimeManager] SetTimeOfDay ignored — only the server owns the clock.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
float target = Mathf.Repeat(timeOfDay01, 1f);
|
||||
double now = base.TimeManager.TicksToTime(TickType.Tick);
|
||||
startServerTime.Value = now - (target - initialTimeOfDay) * dayLengthSeconds;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Assigns the procedural sky to the lighting settings and registers the sun for URP ambient.
|
||||
/// </summary>
|
||||
private void BindRenderSettings()
|
||||
{
|
||||
if (skyboxMaterial != null) RenderSettings.skybox = skyboxMaterial;
|
||||
if (sunLight != null) RenderSettings.sun = sunLight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drives the sky from the editor preview slider, repainting the Scene view so the look updates
|
||||
/// live without entering Play (matches SeasonManager's preview). rotateSun is false when called
|
||||
/// from OnValidate, where rotating the Light transform is disallowed.
|
||||
/// </summary>
|
||||
private void ApplyEditorPreview(bool rotateSun)
|
||||
{
|
||||
BindRenderSettings();
|
||||
TimeOfDay01 = editorPreviewTime;
|
||||
ApplyVisuals(editorPreviewTime, editorPreviewTime * dayLengthSeconds, rotateSun);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying) UnityEditor.SceneView.RepaintAll();
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fades the sun intensity below the horizon, pushes the synced time/sun-direction/animation
|
||||
/// globals the skybox shader reads, and (when rotateSun) rotates the Light along its arc. The
|
||||
/// sun direction is computed analytically so the shader is correct even when the transform is
|
||||
/// not touched. skyTime is the continuous synced seconds driving cloud drift and shooting stars,
|
||||
/// so those events match across clients.
|
||||
/// </summary>
|
||||
private void ApplyVisuals(float timeOfDay01, float skyTime, bool rotateSun)
|
||||
{
|
||||
float altitude = Mathf.Sin((timeOfDay01 - 0.25f) * 2f * Mathf.PI);
|
||||
Quaternion sunRotation = Quaternion.Euler(altitude * 90f, sunAzimuthDegrees, 0f);
|
||||
Vector3 sunDirection = -(sunRotation * Vector3.forward);
|
||||
|
||||
if (sunLight != null)
|
||||
{
|
||||
if (rotateSun) sunLight.transform.rotation = sunRotation;
|
||||
sunLight.intensity = maxSunIntensity * Mathf.Clamp01(altitude * 1.5f + 0.05f);
|
||||
}
|
||||
|
||||
Shader.SetGlobalFloat(TimeOfDayId, timeOfDay01);
|
||||
Shader.SetGlobalVector(SunDirId, sunDirection);
|
||||
Shader.SetGlobalFloat(SkyTimeId, skyTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes skybox-based ambient and reflections on a fixed cadence — UpdateEnvironment is
|
||||
/// expensive, so it must never run every frame.
|
||||
/// </summary>
|
||||
private void RefreshAmbientThrottled(float dt)
|
||||
{
|
||||
ambientTimer += dt;
|
||||
if (ambientTimer < ambientRefreshInterval) return;
|
||||
ambientTimer = 0f;
|
||||
DynamicGI.UpdateEnvironment();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a clear error when the inspector references are missing so the sky never fails silently.
|
||||
/// </summary>
|
||||
private void WarnIfUnassigned()
|
||||
{
|
||||
if (skyboxMaterial == null)
|
||||
Debug.LogError("[SkyTimeManager] No skybox material assigned — the sky will not render.", this);
|
||||
if (sunLight == null)
|
||||
Debug.LogError("[SkyTimeManager] No directional sun Light assigned — day/night lighting will not work.", this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 541147133c27b8748b7b6b8da351322c
|
||||
@@ -0,0 +1,78 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ashwild.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Authoring container for the rotating quotes shown on the loading screen. A logic-free data
|
||||
/// asset (mirrors MusicPlaylist): create one via Assets ▸ Create ▸ UI ▸ Loading Quotes, fill the
|
||||
/// list in the inspector, and assign it to the LoadingScreen. Keeping the quotes here lets us add
|
||||
/// or reword lines without touching code, and lets the loading screen pick from them at random.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "LoadingQuotes", menuName = "UI/Loading Quotes")]
|
||||
public class LoadingQuotes : ScriptableObject
|
||||
{
|
||||
#region Types
|
||||
|
||||
/// <summary>
|
||||
/// A single line shown on the loading screen: the quote itself plus an optional author shown
|
||||
/// underneath. Leave the author empty for anonymous lines or plain loading tips.
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public struct Quote
|
||||
{
|
||||
[TextArea] public string text;
|
||||
public string author;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Serialized Fields
|
||||
|
||||
[Header("Quotes")]
|
||||
[Tooltip("The pool of quotes the loading screen cycles through at random.")]
|
||||
[SerializeField] private Quote[] quotes;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// True when at least one quote is authored — guards the loading screen against an empty asset.
|
||||
/// </summary>
|
||||
public bool HasQuotes => quotes != null && quotes.Length > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Number of authored quotes.
|
||||
/// </summary>
|
||||
public int Count => quotes != null ? quotes.Length : 0;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a random quote while avoiding the one at <paramref name="avoidIndex"/> so the same
|
||||
/// line never shows twice in a row (unless there is only one quote). Outputs the chosen index
|
||||
/// so the caller can feed it back in on the next call.
|
||||
/// </summary>
|
||||
public Quote GetRandom(int avoidIndex, out int chosenIndex)
|
||||
{
|
||||
if (!HasQuotes)
|
||||
{
|
||||
chosenIndex = -1;
|
||||
return default;
|
||||
}
|
||||
|
||||
if (quotes.Length == 1)
|
||||
{
|
||||
chosenIndex = 0;
|
||||
return quotes[0];
|
||||
}
|
||||
|
||||
int index = Random.Range(0, quotes.Length);
|
||||
if (index == avoidIndex)
|
||||
index = (index + 1) % quotes.Length;
|
||||
|
||||
chosenIndex = index;
|
||||
return quotes[index];
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8543f62e39304aa4cba9f993de5af91a
|
||||
@@ -0,0 +1,286 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
using DG.Tweening;
|
||||
using Ashwild.Player;
|
||||
|
||||
namespace Ashwild.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Full-screen loading overlay shown while a session starts and the game scene loads over the
|
||||
/// network. It is deliberately NOT a UIPanel: panels live on a per-scene UIManager and are
|
||||
/// destroyed when their scene unloads, but FishNet loads the game scene with ReplaceOption.All —
|
||||
/// so the menu (and anything on it) is torn down mid-transition. This controller therefore marks
|
||||
/// itself DontDestroyOnLoad and survives the swap, covering the screen continuously from the save
|
||||
/// click until the local player has spawned. It hides the rotating menu camera and the scene
|
||||
/// switch behind a quote-cycling curtain.
|
||||
///
|
||||
/// Place it on its own high-sorting-order Canvas in the menu scene (mirror SplashScreenController).
|
||||
/// It is driven entirely by the PlayerEvents bus: SessionStarting/SessionJoining raise it,
|
||||
/// LocalPlayerSpawned lowers it, and SessionError/SessionStopped lower it as a safety net so a
|
||||
/// failed host never leaves the player stuck behind the curtain.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(CanvasGroup))]
|
||||
public class LoadingScreen : MonoBehaviour
|
||||
{
|
||||
#region Serialized Fields
|
||||
|
||||
[Header("References")]
|
||||
[Tooltip("Label that displays the current quote (and its author on a second line).")]
|
||||
[SerializeField] private TextMeshProUGUI quoteLabel;
|
||||
|
||||
[Tooltip("Pool of quotes to cycle through. Assign a Loading Quotes asset.")]
|
||||
[SerializeField] private LoadingQuotes quotes;
|
||||
|
||||
[Header("Curtain Fade")]
|
||||
[SerializeField] private float fadeInDuration = 0.35f;
|
||||
[SerializeField] private float fadeOutDuration = 0.5f;
|
||||
[SerializeField] private Ease fadeInEase = Ease.OutQuad;
|
||||
[SerializeField] private Ease fadeOutEase = Ease.InQuad;
|
||||
|
||||
[Tooltip("Minimum time the curtain stays up once raised, so an instant load doesn't flash.")]
|
||||
[SerializeField] private float minimumDisplayTime = 1.5f;
|
||||
|
||||
[Header("Quote Cycling")]
|
||||
[Tooltip("How long each quote stays on screen before swapping to the next.")]
|
||||
[SerializeField] private float quoteDuration = 4f;
|
||||
[Tooltip("Cross-fade duration when swapping one quote for the next.")]
|
||||
[SerializeField] private float quoteFadeDuration = 0.4f;
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
|
||||
/// <summary>
|
||||
/// Single persistent instance — a second copy arriving with a reloaded menu scene self-destructs.
|
||||
/// </summary>
|
||||
private static LoadingScreen instance;
|
||||
|
||||
private CanvasGroup canvasGroup;
|
||||
|
||||
/// <summary>
|
||||
/// True while the curtain is up (or being raised). Guards against double-show / double-hide.
|
||||
/// </summary>
|
||||
private bool isShown;
|
||||
|
||||
/// <summary>
|
||||
/// Unscaled timestamp at which the curtain was raised, used to enforce the minimum display time.
|
||||
/// </summary>
|
||||
private float shownAt;
|
||||
|
||||
/// <summary>
|
||||
/// Index of the quote currently displayed, so the next pick can avoid repeating it.
|
||||
/// </summary>
|
||||
private int currentQuoteIndex = -1;
|
||||
|
||||
private Tween curtainTween;
|
||||
private Sequence quoteSequence;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
|
||||
/// <summary>
|
||||
/// Enforces the singleton, persists across the menu→game scene swap, and starts hidden.
|
||||
/// </summary>
|
||||
private void Awake()
|
||||
{
|
||||
if (instance != null && instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
|
||||
canvasGroup = GetComponent<CanvasGroup>();
|
||||
canvasGroup.alpha = 0f;
|
||||
canvasGroup.blocksRaycasts = false;
|
||||
canvasGroup.interactable = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to the session lifecycle events that raise and lower the curtain.
|
||||
/// </summary>
|
||||
private void OnEnable()
|
||||
{
|
||||
PlayerEvents.SessionStarting += HandleSessionBeginning;
|
||||
PlayerEvents.SessionJoining += HandleSessionJoining;
|
||||
PlayerEvents.LocalPlayerSpawned += HandleLocalPlayerSpawned;
|
||||
PlayerEvents.SessionError += HandleSessionError;
|
||||
PlayerEvents.SessionStopped += HandleSessionStopped;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes — mirrors OnEnable exactly.
|
||||
/// </summary>
|
||||
private void OnDisable()
|
||||
{
|
||||
PlayerEvents.SessionStarting -= HandleSessionBeginning;
|
||||
PlayerEvents.SessionJoining -= HandleSessionJoining;
|
||||
PlayerEvents.LocalPlayerSpawned -= HandleLocalPlayerSpawned;
|
||||
PlayerEvents.SessionError -= HandleSessionError;
|
||||
PlayerEvents.SessionStopped -= HandleSessionStopped;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kills any running tweens so nothing animates a destroyed target on teardown.
|
||||
/// </summary>
|
||||
private void OnDestroy()
|
||||
{
|
||||
curtainTween?.Kill();
|
||||
quoteSequence?.Kill();
|
||||
if (instance == this) instance = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
/// <summary>
|
||||
/// Raises the curtain when the local player starts hosting a session.
|
||||
/// </summary>
|
||||
private void HandleSessionBeginning() => Show();
|
||||
|
||||
/// <summary>
|
||||
/// Raises the curtain when the local player begins joining a remote session.
|
||||
/// </summary>
|
||||
private void HandleSessionJoining(string code) => Show();
|
||||
|
||||
/// <summary>
|
||||
/// Lowers the curtain once the local player has spawned into the loaded game scene.
|
||||
/// </summary>
|
||||
private void HandleLocalPlayerSpawned() => Hide();
|
||||
|
||||
/// <summary>
|
||||
/// Safety net: a connection failure must drop the curtain so the menu becomes usable again.
|
||||
/// </summary>
|
||||
private void HandleSessionError(string reason) => Hide();
|
||||
|
||||
/// <summary>
|
||||
/// Safety net: if the session tears down before a player spawns, never leave the curtain up.
|
||||
/// </summary>
|
||||
private void HandleSessionStopped() => Hide();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Curtain
|
||||
|
||||
/// <summary>
|
||||
/// Fades the curtain in (blocking input underneath) and starts cycling quotes. Ignored when
|
||||
/// already shown so re-entrant session events don't restart the animation.
|
||||
/// </summary>
|
||||
private void Show()
|
||||
{
|
||||
if (isShown) return;
|
||||
isShown = true;
|
||||
shownAt = Time.unscaledTime;
|
||||
|
||||
curtainTween?.Kill();
|
||||
canvasGroup.blocksRaycasts = true;
|
||||
canvasGroup.interactable = true;
|
||||
|
||||
curtainTween = canvasGroup.DOFade(1f, fadeInDuration)
|
||||
.SetEase(fadeInEase)
|
||||
.SetUpdate(true);
|
||||
|
||||
ShowNextQuote(instant: true);
|
||||
StartQuoteCycle();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fades the curtain out and stops the quote cycle. Honours the minimum display time so an
|
||||
/// instant load still shows the loading screen for a readable beat instead of flashing.
|
||||
/// </summary>
|
||||
private void Hide()
|
||||
{
|
||||
if (!isShown) return;
|
||||
isShown = false;
|
||||
|
||||
float elapsed = Time.unscaledTime - shownAt;
|
||||
float delay = Mathf.Max(0f, minimumDisplayTime - elapsed);
|
||||
|
||||
curtainTween?.Kill();
|
||||
curtainTween = canvasGroup.DOFade(0f, fadeOutDuration)
|
||||
.SetDelay(delay)
|
||||
.SetEase(fadeOutEase)
|
||||
.SetUpdate(true)
|
||||
.OnComplete(() =>
|
||||
{
|
||||
canvasGroup.blocksRaycasts = false;
|
||||
canvasGroup.interactable = false;
|
||||
StopQuoteCycle();
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Quotes
|
||||
|
||||
/// <summary>
|
||||
/// Builds the looping cross-fade that swaps the quote every <see cref="quoteDuration"/> seconds.
|
||||
/// Runs on unscaled time so it keeps animating even while the load freezes the timescale.
|
||||
///
|
||||
/// Bails out (leaving the first quote shown statically) when there is nothing to cycle or when
|
||||
/// <see cref="quoteDuration"/> is not positive: a looping DOTween sequence with a zero total
|
||||
/// duration replays infinitely within a single frame and hard-freezes the editor, so the
|
||||
/// positive-duration guard here is mandatory, not cosmetic.
|
||||
/// </summary>
|
||||
private void StartQuoteCycle()
|
||||
{
|
||||
StopQuoteCycle();
|
||||
if (quotes == null || quotes.Count <= 1 || quoteLabel == null) return;
|
||||
if (quoteDuration <= 0f) return;
|
||||
|
||||
float fade = Mathf.Max(0f, quoteFadeDuration);
|
||||
|
||||
quoteSequence = DOTween.Sequence().SetUpdate(true);
|
||||
quoteSequence.AppendInterval(quoteDuration);
|
||||
quoteSequence.Append(quoteLabel.DOFade(0f, fade));
|
||||
quoteSequence.AppendCallback(() => ShowNextQuote(instant: false));
|
||||
quoteSequence.Append(quoteLabel.DOFade(1f, fade));
|
||||
quoteSequence.SetLoops(-1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kills the running quote cycle, if any.
|
||||
/// </summary>
|
||||
private void StopQuoteCycle()
|
||||
{
|
||||
quoteSequence?.Kill();
|
||||
quoteSequence = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Picks the next random quote and writes it to the label. When <paramref name="instant"/> is
|
||||
/// true the label is forced fully opaque (used on the very first quote, before the cross-fade
|
||||
/// loop takes over); otherwise the surrounding sequence owns the alpha.
|
||||
/// </summary>
|
||||
private void ShowNextQuote(bool instant)
|
||||
{
|
||||
if (quotes == null || !quotes.HasQuotes || quoteLabel == null) return;
|
||||
|
||||
LoadingQuotes.Quote quote = quotes.GetRandom(currentQuoteIndex, out currentQuoteIndex);
|
||||
quoteLabel.text = Format(quote);
|
||||
|
||||
if (instant)
|
||||
{
|
||||
Color c = quoteLabel.color;
|
||||
c.a = 1f;
|
||||
quoteLabel.color = c;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Composes the displayed string: the quote, plus an em-dashed author line when one is set.
|
||||
/// </summary>
|
||||
private static string Format(LoadingQuotes.Quote quote)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(quote.author)
|
||||
? quote.text
|
||||
: $"{quote.text}\n<size=80%>— {quote.author}</size>";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e417e8562dcadeb43933d3687febc562
|
||||
@@ -1,5 +1,7 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using Ashwild.Player;
|
||||
using Ashwild.Settings;
|
||||
|
||||
namespace Ashwild.UI
|
||||
@@ -28,6 +30,12 @@ namespace Ashwild.UI
|
||||
[SerializeField] private Button inviteButton;
|
||||
[SerializeField] private Button quitButton;
|
||||
|
||||
[Header("Session")]
|
||||
[Tooltip("Displays the shareable room code (host SteamID-derived) of the current session.")]
|
||||
[SerializeField] private TMP_Text roomCodeLabel;
|
||||
[Tooltip("Shown in place of the code when no online session is active.")]
|
||||
[SerializeField] private string offlinePlaceholder = "—";
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
@@ -55,5 +63,35 @@ namespace Ashwild.UI
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Panel Visibility
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the room code each time the pause menu opens, so it always reflects the
|
||||
/// session that is live at that moment rather than a value cached at spawn.
|
||||
/// </summary>
|
||||
public override void Show()
|
||||
{
|
||||
base.Show();
|
||||
RefreshRoomCode();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Writes the current shareable session code into the label, falling back to the offline
|
||||
/// placeholder when no online session is active.
|
||||
/// </summary>
|
||||
private void RefreshRoomCode()
|
||||
{
|
||||
if (roomCodeLabel == null) return;
|
||||
|
||||
string code = PlayerEvents.SessionCode;
|
||||
roomCodeLabel.text = string.IsNullOrEmpty(code) ? offlinePlaceholder : code;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ashwild.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Stamps the current build version onto a TextMeshPro label. Reads <see cref="Application.version"/>
|
||||
/// (authored in Project Settings ▸ Player ▸ Version) so the displayed string always matches the
|
||||
/// real build without anyone editing the scene. Drop this on any UGUI element that carries a
|
||||
/// TextMeshProUGUI — typically a small corner label on the main menu, pause or loading canvas.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(TextMeshProUGUI))]
|
||||
public class VersionLabel : MonoBehaviour
|
||||
{
|
||||
#region Serialized Fields
|
||||
|
||||
[Header("Target")]
|
||||
[Tooltip("The label to write the version into. Auto-filled from this GameObject if left empty.")]
|
||||
[SerializeField] private TextMeshProUGUI label;
|
||||
|
||||
[Header("Format")]
|
||||
[Tooltip("Wraps the version. Use {0} as the placeholder for Application.version, e.g. \"v{0}\".")]
|
||||
[SerializeField] private string format = "v{0}";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
|
||||
/// <summary>
|
||||
/// Caches the label reference so the component works even when assigned by RequireComponent.
|
||||
/// </summary>
|
||||
private void Awake()
|
||||
{
|
||||
if (label == null)
|
||||
label = GetComponent<TextMeshProUGUI>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the version each time the object is enabled, so re-opening a menu always shows it.
|
||||
/// </summary>
|
||||
private void OnEnable()
|
||||
{
|
||||
Refresh();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// Composes the version string from the format and pushes it onto the label. Guards against a
|
||||
/// missing label and logs the culprit so the broken object is selectable from the console.
|
||||
/// </summary>
|
||||
public void Refresh()
|
||||
{
|
||||
if (label == null)
|
||||
{
|
||||
Debug.LogError($"[VersionLabel] '{name}' has no TextMeshProUGUI assigned — cannot show version.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
label.text = string.Format(format, Application.version);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a56065a3033ee664c845e94708384c4e
|
||||
Reference in New Issue
Block a user