MVP of the plane throwing game (#77)
Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com> Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: #77
This commit is contained in:
300
Assets/Scripts/Minigames/Airplane/UI/AirplaneAbilityButton.cs
Normal file
300
Assets/Scripts/Minigames/Airplane/UI/AirplaneAbilityButton.cs
Normal file
@@ -0,0 +1,300 @@
|
||||
using Core;
|
||||
using Input;
|
||||
using Minigames.Airplane.Abilities;
|
||||
using Minigames.Airplane.Core;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Minigames.Airplane.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// UI button for activating airplane special abilities.
|
||||
/// Handles input, visual feedback, and cooldown display.
|
||||
/// Implements ITouchInputConsumer to properly handle hold/release for Jet ability.
|
||||
/// </summary>
|
||||
public class AirplaneAbilityButton : MonoBehaviour, ITouchInputConsumer
|
||||
{
|
||||
#region Inspector References
|
||||
|
||||
[Header("UI Components")]
|
||||
[SerializeField] private Button button;
|
||||
[SerializeField] private Image abilityIcon;
|
||||
[SerializeField] private Image cooldownFill;
|
||||
[SerializeField] private TextMeshProUGUI cooldownText;
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool showDebugLogs;
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
|
||||
private BaseAirplaneAbility currentAbility;
|
||||
private AirplaneController currentAirplane;
|
||||
private bool isHoldAbility; // Jet plane needs hold mechanic
|
||||
private bool isHolding; // Track if button is currently being held
|
||||
|
||||
#endregion
|
||||
|
||||
#region Lifecycle
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (button != null)
|
||||
{
|
||||
button.onClick.AddListener(OnButtonClick);
|
||||
}
|
||||
|
||||
// Hide by default
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (currentAbility == null) return;
|
||||
|
||||
// Update cooldown display
|
||||
if (currentAbility.IsOnCooldown)
|
||||
{
|
||||
// Fill starts at 1 and reduces to 0 over cooldown duration
|
||||
float fillAmount = currentAbility.CooldownRemaining / currentAbility.CooldownDuration;
|
||||
if (cooldownFill != null)
|
||||
{
|
||||
cooldownFill.fillAmount = fillAmount;
|
||||
}
|
||||
|
||||
// Show timer text
|
||||
if (cooldownText != null)
|
||||
{
|
||||
cooldownText.text = $"{currentAbility.CooldownRemaining:F1}s";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Cooldown complete - fill at 0, no text
|
||||
if (cooldownFill != null)
|
||||
cooldownFill.fillAmount = 0f;
|
||||
|
||||
if (cooldownText != null)
|
||||
cooldownText.text = "";
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// Unsubscribe from events
|
||||
if (currentAbility != null)
|
||||
{
|
||||
currentAbility.OnAbilityActivated -= HandleAbilityActivated;
|
||||
currentAbility.OnAbilityDeactivated -= HandleAbilityDeactivated;
|
||||
currentAbility.OnCooldownChanged -= HandleCooldownChanged;
|
||||
}
|
||||
|
||||
// Unregister from input system
|
||||
if (InputManager.Instance != null)
|
||||
{
|
||||
InputManager.Instance.UnregisterOverrideConsumer(this);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// Setup button with airplane and ability reference.
|
||||
/// </summary>
|
||||
public void Setup(AirplaneController airplane, BaseAirplaneAbility ability)
|
||||
{
|
||||
currentAirplane = airplane;
|
||||
currentAbility = ability;
|
||||
isHolding = false;
|
||||
|
||||
// Set icon and show immediately
|
||||
if (abilityIcon != null && ability != null)
|
||||
{
|
||||
abilityIcon.sprite = ability.AbilityIcon;
|
||||
abilityIcon.enabled = true;
|
||||
}
|
||||
|
||||
// Initialize cooldown display
|
||||
if (cooldownFill != null)
|
||||
{
|
||||
cooldownFill.fillAmount = 0f;
|
||||
}
|
||||
|
||||
if (cooldownText != null)
|
||||
{
|
||||
cooldownText.text = "";
|
||||
}
|
||||
|
||||
// Check if this is a hold ability (Jet)
|
||||
isHoldAbility = ability is JetAbility;
|
||||
|
||||
// Subscribe to ability events
|
||||
if (ability != null)
|
||||
{
|
||||
ability.OnAbilityActivated += HandleAbilityActivated;
|
||||
ability.OnAbilityDeactivated += HandleAbilityDeactivated;
|
||||
ability.OnCooldownChanged += HandleCooldownChanged;
|
||||
|
||||
if (showDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[AirplaneAbilityButton] Subscribed to ability events for: {ability.AbilityName}");
|
||||
}
|
||||
}
|
||||
|
||||
// Show UI
|
||||
gameObject.SetActive(true);
|
||||
|
||||
if (showDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[AirplaneAbilityButton] Setup complete with ability: {ability?.AbilityName ?? "None"}, Hold: {isHoldAbility}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hide and cleanup button.
|
||||
/// </summary>
|
||||
public void Hide()
|
||||
{
|
||||
if (currentAbility != null)
|
||||
{
|
||||
currentAbility.OnAbilityActivated -= HandleAbilityActivated;
|
||||
currentAbility.OnAbilityDeactivated -= HandleAbilityDeactivated;
|
||||
currentAbility.OnCooldownChanged -= HandleCooldownChanged;
|
||||
}
|
||||
|
||||
// Unregister from input system
|
||||
if (InputManager.Instance != null)
|
||||
{
|
||||
InputManager.Instance.UnregisterOverrideConsumer(this);
|
||||
}
|
||||
|
||||
currentAbility = null;
|
||||
currentAirplane = null;
|
||||
isHolding = false;
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Input Handling
|
||||
|
||||
private void OnButtonClick()
|
||||
{
|
||||
if (currentAirplane == null || currentAbility == null) return;
|
||||
if (!currentAbility.CanActivate) return;
|
||||
|
||||
// Activate ability
|
||||
currentAirplane.ActivateAbility();
|
||||
|
||||
// For hold abilities (Jet), mark as holding and register for input
|
||||
if (isHoldAbility)
|
||||
{
|
||||
isHolding = true;
|
||||
|
||||
// Register as override consumer to receive hold/release events
|
||||
if (InputManager.Instance != null)
|
||||
{
|
||||
InputManager.Instance.RegisterOverrideConsumer(this);
|
||||
}
|
||||
|
||||
if (showDebugLogs)
|
||||
{
|
||||
Logging.Debug("[AirplaneAbilityButton] Started holding ability, registered for input");
|
||||
}
|
||||
}
|
||||
|
||||
// For non-hold abilities (Bobbing, Drop), this is all we need
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
private void HandleAbilityActivated(BaseAirplaneAbility ability)
|
||||
{
|
||||
if (showDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[AirplaneAbilityButton] Ability activated: {ability.AbilityName}");
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleAbilityDeactivated(BaseAirplaneAbility ability)
|
||||
{
|
||||
if (showDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[AirplaneAbilityButton] Ability deactivated: {ability.AbilityName}");
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleCooldownChanged(float remaining, float total)
|
||||
{
|
||||
if (showDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[AirplaneAbilityButton] OnCooldownChanged: remaining={remaining:F2}, total={total:F2}");
|
||||
}
|
||||
|
||||
// When cooldown starts (remaining == total), set fill to 1
|
||||
if (remaining >= total - 0.01f && cooldownFill != null)
|
||||
{
|
||||
cooldownFill.fillAmount = 1f;
|
||||
|
||||
if (showDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[AirplaneAbilityButton] Cooldown started: {total}s, fill set to 1");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ITouchInputConsumer Implementation
|
||||
|
||||
public void OnTap(Vector2 position)
|
||||
{
|
||||
// If Jet ability is active (holding), next tap anywhere deactivates it
|
||||
if (isHoldAbility && isHolding)
|
||||
{
|
||||
isHolding = false;
|
||||
currentAirplane?.DeactivateAbility();
|
||||
|
||||
// Unregister from input system after tap
|
||||
if (InputManager.Instance != null)
|
||||
{
|
||||
InputManager.Instance.UnregisterOverrideConsumer(this);
|
||||
}
|
||||
|
||||
if (showDebugLogs)
|
||||
{
|
||||
Logging.Debug("[AirplaneAbilityButton] Tap detected - deactivated Jet ability, unregistered");
|
||||
}
|
||||
}
|
||||
// Handle as button click for non-hold abilities
|
||||
else if (!isHoldAbility)
|
||||
{
|
||||
OnButtonClick();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnHoldStart(Vector2 position)
|
||||
{
|
||||
// Not used - button click handles activation, tap handles deactivation
|
||||
}
|
||||
|
||||
public void OnHoldMove(Vector2 position)
|
||||
{
|
||||
// Not used
|
||||
}
|
||||
|
||||
public void OnHoldEnd(Vector2 position)
|
||||
{
|
||||
// Not used - tap handles deactivation for Jet ability
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44f826b6d40c47c0b5a9985f7f793278
|
||||
timeCreated: 1764976132
|
||||
@@ -0,0 +1,38 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Minigames.Airplane.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Component for individual airplane selection buttons.
|
||||
/// Handles visual highlight feedback via show/hide of a highlight image.
|
||||
/// </summary>
|
||||
public class AirplaneSelectionButton : MonoBehaviour
|
||||
{
|
||||
[Header("Highlight Visual")]
|
||||
[SerializeField] private Image highlightImage;
|
||||
|
||||
/// <summary>
|
||||
/// Show the highlight visual.
|
||||
/// </summary>
|
||||
public void HighlightStart()
|
||||
{
|
||||
if (highlightImage != null)
|
||||
{
|
||||
highlightImage.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hide the highlight visual.
|
||||
/// </summary>
|
||||
public void HighlightEnd()
|
||||
{
|
||||
if (highlightImage != null)
|
||||
{
|
||||
highlightImage.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4ccf530e55324aec8dc6e09eb827f123
|
||||
timeCreated: 1765132601
|
||||
297
Assets/Scripts/Minigames/Airplane/UI/AirplaneSelectionUI.cs
Normal file
297
Assets/Scripts/Minigames/Airplane/UI/AirplaneSelectionUI.cs
Normal file
@@ -0,0 +1,297 @@
|
||||
using System;
|
||||
using Core;
|
||||
using Minigames.Airplane.Data;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Minigames.Airplane.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// UI for selecting airplane type before game starts.
|
||||
/// Displays buttons for each available airplane type.
|
||||
/// </summary>
|
||||
public class AirplaneSelectionUI : MonoBehaviour
|
||||
{
|
||||
#region Inspector References
|
||||
|
||||
[Header("UI References")]
|
||||
[SerializeField] private Button jetPlaneButton;
|
||||
[SerializeField] private Button bobbingPlaneButton;
|
||||
[SerializeField] private Button dropPlaneButton;
|
||||
[SerializeField] private Button confirmButton;
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool showDebugLogs;
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
|
||||
private AirplaneAbilityType selectedType;
|
||||
private AirplaneSelectionButton selectedButtonComponent;
|
||||
private bool hasConfirmed;
|
||||
|
||||
public bool HasSelectedType => hasConfirmed;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
public event Action<AirplaneAbilityType> OnTypeSelected;
|
||||
public event Action<AirplaneAbilityType> OnConfirmed;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Lifecycle
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Setup button listeners
|
||||
if (jetPlaneButton != null)
|
||||
jetPlaneButton.onClick.AddListener(() => SelectType(AirplaneAbilityType.Jet, jetPlaneButton));
|
||||
|
||||
if (bobbingPlaneButton != null)
|
||||
bobbingPlaneButton.onClick.AddListener(() => SelectType(AirplaneAbilityType.Bobbing, bobbingPlaneButton));
|
||||
|
||||
if (dropPlaneButton != null)
|
||||
dropPlaneButton.onClick.AddListener(() => SelectType(AirplaneAbilityType.Drop, dropPlaneButton));
|
||||
|
||||
if (confirmButton != null)
|
||||
{
|
||||
confirmButton.onClick.AddListener(ConfirmSelection);
|
||||
confirmButton.interactable = false; // Disabled until selection made
|
||||
}
|
||||
|
||||
// Hide by default (deactivate container child, not root)
|
||||
if (transform.childCount > 0)
|
||||
{
|
||||
Transform container = transform.GetChild(0);
|
||||
container.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// Show the selection UI.
|
||||
/// Activates the immediate child container.
|
||||
/// Script should be on Root, with UI elements under a Container child.
|
||||
/// </summary>
|
||||
public void Show()
|
||||
{
|
||||
selectedType = AirplaneAbilityType.None;
|
||||
selectedButtonComponent = null;
|
||||
hasConfirmed = false;
|
||||
|
||||
if (confirmButton != null)
|
||||
confirmButton.interactable = false;
|
||||
|
||||
// Reset all button highlights
|
||||
ResetButtonHighlights();
|
||||
|
||||
// Populate icons from settings
|
||||
PopulateButtonIcons();
|
||||
|
||||
// Activate the container (immediate child)
|
||||
if (transform.childCount > 0)
|
||||
{
|
||||
Transform container = transform.GetChild(0);
|
||||
container.gameObject.SetActive(true);
|
||||
|
||||
if (showDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[AirplaneSelectionUI] Shown. Activated container: {container.name}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logging.Error("[AirplaneSelectionUI] No child container found! Expected structure: Root(script)->Container->UI Elements");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hide the selection UI.
|
||||
/// Deactivates the immediate child container.
|
||||
/// </summary>
|
||||
public void Hide()
|
||||
{
|
||||
// Deactivate the container (immediate child)
|
||||
if (transform.childCount > 0)
|
||||
{
|
||||
Transform container = transform.GetChild(0);
|
||||
container.gameObject.SetActive(false);
|
||||
|
||||
if (showDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[AirplaneSelectionUI] Hidden. Deactivated container: {container.name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the selected airplane type.
|
||||
/// </summary>
|
||||
public AirplaneAbilityType GetSelectedType()
|
||||
{
|
||||
return selectedType;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private void SelectType(AirplaneAbilityType type, Button button)
|
||||
{
|
||||
if (type == AirplaneAbilityType.None)
|
||||
{
|
||||
Logging.Warning("[AirplaneSelectionUI] Attempted to select None type!");
|
||||
return;
|
||||
}
|
||||
|
||||
selectedType = type;
|
||||
|
||||
// Get the AirplaneSelectionButton component (on same GameObject as Button)
|
||||
var buttonComponent = button.GetComponent<AirplaneSelectionButton>();
|
||||
if (buttonComponent == null)
|
||||
{
|
||||
Logging.Warning($"[AirplaneSelectionUI] Button {button.name} is missing AirplaneSelectionButton component!");
|
||||
return;
|
||||
}
|
||||
|
||||
selectedButtonComponent = buttonComponent;
|
||||
|
||||
// Update visual feedback
|
||||
ResetButtonHighlights();
|
||||
HighlightButton(buttonComponent);
|
||||
|
||||
// Enable confirm button
|
||||
if (confirmButton != null)
|
||||
confirmButton.interactable = true;
|
||||
|
||||
// Fire event
|
||||
OnTypeSelected?.Invoke(type);
|
||||
|
||||
if (showDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[AirplaneSelectionUI] Selected type: {type}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfirmSelection()
|
||||
{
|
||||
if (selectedType == AirplaneAbilityType.None)
|
||||
{
|
||||
Logging.Warning("[AirplaneSelectionUI] Cannot confirm - no type selected!");
|
||||
return;
|
||||
}
|
||||
|
||||
hasConfirmed = true;
|
||||
|
||||
// Fire event
|
||||
OnConfirmed?.Invoke(selectedType);
|
||||
|
||||
// Hide UI
|
||||
Hide();
|
||||
}
|
||||
|
||||
private void ResetButtonHighlights()
|
||||
{
|
||||
// End highlight on all buttons
|
||||
if (jetPlaneButton != null)
|
||||
{
|
||||
var component = jetPlaneButton.GetComponent<AirplaneSelectionButton>();
|
||||
if (component != null) component.HighlightEnd();
|
||||
}
|
||||
|
||||
if (bobbingPlaneButton != null)
|
||||
{
|
||||
var component = bobbingPlaneButton.GetComponent<AirplaneSelectionButton>();
|
||||
if (component != null) component.HighlightEnd();
|
||||
}
|
||||
|
||||
if (dropPlaneButton != null)
|
||||
{
|
||||
var component = dropPlaneButton.GetComponent<AirplaneSelectionButton>();
|
||||
if (component != null) component.HighlightEnd();
|
||||
}
|
||||
}
|
||||
|
||||
private void HighlightButton(AirplaneSelectionButton buttonComponent)
|
||||
{
|
||||
if (buttonComponent != null)
|
||||
{
|
||||
buttonComponent.HighlightStart();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populate button icons from airplane settings.
|
||||
/// Assumes Image component is on the same GameObject as the Button.
|
||||
/// </summary>
|
||||
private void PopulateButtonIcons()
|
||||
{
|
||||
// Get airplane settings
|
||||
var settings = GameManager.GetSettingsObject<AppleHills.Core.Settings.IAirplaneSettings>();
|
||||
if (settings == null)
|
||||
{
|
||||
if (showDebugLogs)
|
||||
{
|
||||
Logging.Warning("[AirplaneSelectionUI] Could not load airplane settings for icons");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Populate Jet button icon
|
||||
if (jetPlaneButton != null)
|
||||
{
|
||||
var jetConfig = settings.GetAirplaneConfig(AirplaneAbilityType.Jet);
|
||||
if (jetConfig != null && jetConfig.previewSprite != null)
|
||||
{
|
||||
var image = jetPlaneButton.GetComponent<Image>();
|
||||
if (image != null)
|
||||
{
|
||||
image.sprite = jetConfig.previewSprite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate Bobbing button icon
|
||||
if (bobbingPlaneButton != null)
|
||||
{
|
||||
var bobbingConfig = settings.GetAirplaneConfig(AirplaneAbilityType.Bobbing);
|
||||
if (bobbingConfig != null && bobbingConfig.previewSprite != null)
|
||||
{
|
||||
var image = bobbingPlaneButton.GetComponent<Image>();
|
||||
if (image != null)
|
||||
{
|
||||
image.sprite = bobbingConfig.previewSprite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate Drop button icon
|
||||
if (dropPlaneButton != null)
|
||||
{
|
||||
var dropConfig = settings.GetAirplaneConfig(AirplaneAbilityType.Drop);
|
||||
if (dropConfig != null && dropConfig.previewSprite != null)
|
||||
{
|
||||
var image = dropPlaneButton.GetComponent<Image>();
|
||||
if (image != null)
|
||||
{
|
||||
image.sprite = dropConfig.previewSprite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showDebugLogs)
|
||||
{
|
||||
Logging.Debug("[AirplaneSelectionUI] Populated airplane icons from settings");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6463ce42d43142878816170f53a0f5bd
|
||||
timeCreated: 1764976150
|
||||
230
Assets/Scripts/Minigames/Airplane/UI/TargetDisplayUI.cs
Normal file
230
Assets/Scripts/Minigames/Airplane/UI/TargetDisplayUI.cs
Normal file
@@ -0,0 +1,230 @@
|
||||
using Core;
|
||||
using Core.Lifecycle;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Minigames.Airplane.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Displays target information: icon and distance remaining to target.
|
||||
/// Updates in real-time as the airplane moves.
|
||||
/// </summary>
|
||||
public class TargetDisplayUI : ManagedBehaviour
|
||||
{
|
||||
#region Inspector References
|
||||
|
||||
[Header("UI Elements")]
|
||||
[Tooltip("Image to display target icon")]
|
||||
[SerializeField] private Image targetIcon;
|
||||
|
||||
[Tooltip("Text to display distance remaining")]
|
||||
[SerializeField] private TextMeshProUGUI distanceText;
|
||||
|
||||
[Header("Display Settings")]
|
||||
[Tooltip("Format string for distance display (e.g., '{0:F1}m')")]
|
||||
[SerializeField] private string distanceFormat = "{0:F1}m";
|
||||
|
||||
[Tooltip("Update distance every N frames (0 = every frame)")]
|
||||
[SerializeField] private int updateInterval = 5;
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool showDebugLogs;
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
|
||||
private Transform _planeTransform;
|
||||
private Transform _launchPointTransform;
|
||||
private Vector3 _targetPosition;
|
||||
private bool _isActive;
|
||||
private int _frameCounter;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Lifecycle
|
||||
|
||||
internal override void OnManagedAwake()
|
||||
{
|
||||
base.OnManagedAwake();
|
||||
|
||||
// Hide by default
|
||||
Hide();
|
||||
|
||||
// Validate references
|
||||
if (targetIcon == null)
|
||||
{
|
||||
Logging.Warning("[TargetDisplayUI] Target icon image not assigned!");
|
||||
}
|
||||
|
||||
if (distanceText == null)
|
||||
{
|
||||
Logging.Warning("[TargetDisplayUI] Distance text not assigned!");
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Only update if active and we have at least one transform to calculate from
|
||||
if (!_isActive) return;
|
||||
if (_planeTransform == null && _launchPointTransform == null) return;
|
||||
|
||||
// Update distance at specified interval
|
||||
_frameCounter++;
|
||||
if (updateInterval == 0 || _frameCounter >= updateInterval)
|
||||
{
|
||||
_frameCounter = 0;
|
||||
UpdateDistance();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// Setup the target display with icon and target position.
|
||||
/// Activates tracking using launch point for distance calculation.
|
||||
/// </summary>
|
||||
/// <param name="targetSprite">Sprite to display as target icon</param>
|
||||
/// <param name="targetPosition">World position of the target</param>
|
||||
/// <param name="launchPoint">Launch point transform (used for distance when plane not available)</param>
|
||||
public void Setup(Sprite targetSprite, Vector3 targetPosition, Transform launchPoint)
|
||||
{
|
||||
_targetPosition = targetPosition;
|
||||
_launchPointTransform = launchPoint;
|
||||
|
||||
// Set icon
|
||||
if (targetIcon != null && targetSprite != null)
|
||||
{
|
||||
targetIcon.sprite = targetSprite;
|
||||
targetIcon.enabled = true;
|
||||
}
|
||||
|
||||
// Activate tracking so distance updates even before plane spawns
|
||||
_isActive = true;
|
||||
_frameCounter = 0;
|
||||
|
||||
// Update distance immediately using launch point
|
||||
UpdateDistance();
|
||||
|
||||
if (showDebugLogs)
|
||||
{
|
||||
Logging.Debug($"[TargetDisplayUI] Setup with target at {targetPosition}, launch point at {launchPoint?.position ?? Vector3.zero}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start tracking the airplane and updating distance.
|
||||
/// Switches distance calculation from launch point to airplane position.
|
||||
/// Note: Does not automatically show UI - call Show() separately.
|
||||
/// </summary>
|
||||
/// <param name="planeTransform">Transform of the airplane to track</param>
|
||||
public void StartTracking(Transform planeTransform)
|
||||
{
|
||||
_planeTransform = planeTransform;
|
||||
_isActive = true;
|
||||
_frameCounter = 0;
|
||||
|
||||
// Update distance immediately if visible (now using plane position)
|
||||
if (gameObject.activeSelf)
|
||||
{
|
||||
UpdateDistance();
|
||||
}
|
||||
|
||||
if (showDebugLogs)
|
||||
{
|
||||
Logging.Debug("[TargetDisplayUI] Started tracking airplane");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop tracking the airplane.
|
||||
/// Reverts to using launch point for distance calculation if available.
|
||||
/// Note: Does not automatically hide UI - call Hide() separately.
|
||||
/// </summary>
|
||||
public void StopTracking()
|
||||
{
|
||||
_planeTransform = null;
|
||||
// Keep _isActive true so we can show distance from launch point
|
||||
// Will be set false when Hide() is called
|
||||
|
||||
// Update immediately to show launch point distance again
|
||||
if (_launchPointTransform != null && gameObject.activeSelf)
|
||||
{
|
||||
UpdateDistance();
|
||||
}
|
||||
|
||||
if (showDebugLogs)
|
||||
{
|
||||
Logging.Debug("[TargetDisplayUI] Stopped tracking airplane, reverted to launch point");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show the UI.
|
||||
/// </summary>
|
||||
public void Show()
|
||||
{
|
||||
gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hide the UI and deactivate tracking.
|
||||
/// </summary>
|
||||
public void Hide()
|
||||
{
|
||||
_isActive = false;
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal
|
||||
|
||||
/// <summary>
|
||||
/// Update the distance text based on current plane position.
|
||||
/// Uses launch point if plane isn't available yet.
|
||||
/// </summary>
|
||||
private void UpdateDistance()
|
||||
{
|
||||
if (distanceText == null) return;
|
||||
|
||||
// Use plane position if available, otherwise use launch point
|
||||
Vector3 currentPosition;
|
||||
if (_planeTransform != null)
|
||||
{
|
||||
currentPosition = _planeTransform.position;
|
||||
}
|
||||
else if (_launchPointTransform != null)
|
||||
{
|
||||
currentPosition = _launchPointTransform.position;
|
||||
}
|
||||
else
|
||||
{
|
||||
// No reference available
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate horizontal distance (X-axis only for side-scroller)
|
||||
float distance = Mathf.Abs(_targetPosition.x - currentPosition.x);
|
||||
|
||||
// Update text
|
||||
distanceText.text = string.Format(distanceFormat, distance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update distance and ensure UI is shown.
|
||||
/// Call when showing UI to refresh distance display.
|
||||
/// </summary>
|
||||
public void UpdateAndShow()
|
||||
{
|
||||
UpdateDistance();
|
||||
Show();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6aadeed064b648a78ec13b9a76d2853b
|
||||
timeCreated: 1764943474
|
||||
Reference in New Issue
Block a user