406 lines
14 KiB
C#
406 lines
14 KiB
C#
using System;
|
|
using Core;
|
|
using Core.Lifecycle;
|
|
using Input;
|
|
using UnityEngine;
|
|
|
|
namespace Common.Input
|
|
{
|
|
/// <summary>
|
|
/// Base class for drag-to-launch mechanics (Angry Birds style).
|
|
/// Provides core drag logic, force calculation, and input handling.
|
|
/// Uses SlingshotConfig for all settings - fully configuration-driven.
|
|
/// Subclasses implement visual feedback and specific launch behavior.
|
|
/// </summary>
|
|
public abstract class DragLaunchController : ManagedBehaviour, ITouchInputConsumer
|
|
{
|
|
#region Events
|
|
|
|
/// <summary>
|
|
/// Fired when drag starts. Parameters: (Vector2 startPosition)
|
|
/// </summary>
|
|
public event Action<Vector2> OnDragStart;
|
|
|
|
/// <summary>
|
|
/// Fired during drag update. Parameters: (Vector2 currentPosition, Vector2 direction, float force)
|
|
/// </summary>
|
|
public event Action<Vector2, Vector2, float> OnDragUpdate;
|
|
|
|
/// <summary>
|
|
/// Fired when drag ends. Parameters: (Vector2 endPosition, Vector2 direction, float force)
|
|
/// </summary>
|
|
public event Action<Vector2, Vector2, float> OnDragEnd;
|
|
|
|
/// <summary>
|
|
/// Fired when launch occurs. Parameters: (Vector2 direction, float force)
|
|
/// </summary>
|
|
public event Action<Vector2, float> OnLaunch;
|
|
|
|
#endregion
|
|
|
|
#region Settings
|
|
|
|
private SlingshotConfig _config;
|
|
|
|
protected SlingshotConfig Config
|
|
{
|
|
get
|
|
{
|
|
if (_config == null)
|
|
{
|
|
_config = GetSlingshotConfig();
|
|
}
|
|
return _config;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Subclasses implement to return their slingshot configuration
|
|
/// from their specific settings object
|
|
/// </summary>
|
|
protected abstract SlingshotConfig GetSlingshotConfig();
|
|
|
|
/// <summary>
|
|
/// Subclasses implement to return the projectile prefab that will be launched.
|
|
/// Used for reading Rigidbody2D properties (mass, gravityScale) for trajectory calculations.
|
|
/// </summary>
|
|
protected abstract GameObject GetProjectilePrefab();
|
|
|
|
#endregion
|
|
|
|
#region Inspector Properties
|
|
|
|
[Header("Launch Settings Overrides (leave 0 to use config)")]
|
|
[Tooltip("Override max drag distance (0 = use config)")]
|
|
[SerializeField] protected float maxDragDistanceOverride = 0f;
|
|
|
|
[Tooltip("Override max force (0 = use config)")]
|
|
[SerializeField] protected float maxForceOverride = 0f;
|
|
|
|
[Header("References")]
|
|
[Tooltip("Launch anchor point (spawn/slingshot position)")]
|
|
[SerializeField] protected Transform launchAnchor;
|
|
|
|
[Header("Debug")]
|
|
[SerializeField] protected bool showDebugLogs;
|
|
|
|
#endregion
|
|
|
|
#region Computed Properties
|
|
|
|
protected float MaxDragDistance => maxDragDistanceOverride > 0 ? maxDragDistanceOverride : Config?.maxDragDistance ?? 5f;
|
|
protected float MaxForce => maxForceOverride > 0 ? maxForceOverride : Config?.baseLaunchForce ?? 20f;
|
|
|
|
#endregion
|
|
|
|
#region State
|
|
|
|
private bool _isDragging;
|
|
private Vector2 _dragStartPosition;
|
|
private bool _isEnabled = false;
|
|
private bool _isRegistered = false;
|
|
|
|
public bool IsDragging => _isDragging;
|
|
public bool IsEnabled => _isEnabled;
|
|
|
|
#endregion
|
|
|
|
#region Lifecycle
|
|
|
|
internal override void OnManagedAwake()
|
|
{
|
|
base.OnManagedAwake();
|
|
|
|
if (launchAnchor == null)
|
|
{
|
|
launchAnchor = transform;
|
|
}
|
|
|
|
// Auto-find trajectory preview if not assigned
|
|
if (trajectoryPreview == null)
|
|
{
|
|
trajectoryPreview = GetComponent<Common.Visual.TrajectoryPreview>();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Enable/Disable
|
|
|
|
/// <summary>
|
|
/// Enable the launch controller and register with InputManager
|
|
/// </summary>
|
|
public virtual void Enable()
|
|
{
|
|
_isEnabled = true;
|
|
|
|
// Register with InputManager as override consumer
|
|
if (InputManager.Instance != null && !_isRegistered)
|
|
{
|
|
InputManager.Instance.RegisterOverrideConsumer(this);
|
|
_isRegistered = true;
|
|
if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Registered with InputManager");
|
|
}
|
|
|
|
// Show preview visuals
|
|
ShowPreview();
|
|
|
|
if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Enabled");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disable the launch controller and unregister from InputManager
|
|
/// </summary>
|
|
public virtual void Disable()
|
|
{
|
|
_isEnabled = false;
|
|
_isDragging = false;
|
|
|
|
// Unregister from InputManager
|
|
if (InputManager.Instance != null && _isRegistered)
|
|
{
|
|
InputManager.Instance.UnregisterOverrideConsumer(this);
|
|
_isRegistered = false;
|
|
if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Unregistered from InputManager");
|
|
}
|
|
|
|
// Hide preview visuals
|
|
HidePreview();
|
|
|
|
if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Disabled");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ITouchInputConsumer Implementation
|
|
|
|
public void OnTap(Vector2 worldPosition)
|
|
{
|
|
// Drag-to-launch uses hold/drag, not tap
|
|
}
|
|
|
|
public void OnHoldStart(Vector2 worldPosition)
|
|
{
|
|
if (!_isEnabled) return;
|
|
StartDrag(worldPosition);
|
|
}
|
|
|
|
public void OnHoldMove(Vector2 worldPosition)
|
|
{
|
|
if (!_isEnabled || !_isDragging) return;
|
|
UpdateDrag(worldPosition);
|
|
}
|
|
|
|
public void OnHoldEnd(Vector2 worldPosition)
|
|
{
|
|
if (!_isEnabled || !_isDragging) return;
|
|
EndDrag(worldPosition);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Drag Handling
|
|
|
|
/// <summary>
|
|
/// Start drag operation
|
|
/// </summary>
|
|
protected virtual void StartDrag(Vector2 worldPosition)
|
|
{
|
|
_isDragging = true;
|
|
// Use launch anchor as the reference point (like Angry Birds)
|
|
_dragStartPosition = launchAnchor.position;
|
|
|
|
if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Started drag at {worldPosition}, anchor at {_dragStartPosition}");
|
|
|
|
OnDragStart?.Invoke(worldPosition);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update drag operation
|
|
/// </summary>
|
|
protected virtual void UpdateDrag(Vector2 currentWorldPosition)
|
|
{
|
|
// Calculate drag vector from anchor to current drag position
|
|
// Pull back (away from anchor) = launch forward (toward anchor direction)
|
|
Vector2 dragVector = _dragStartPosition - currentWorldPosition;
|
|
|
|
// Calculate force and direction
|
|
float dragDistance = dragVector.magnitude;
|
|
float dragRatio = Mathf.Clamp01(dragDistance / MaxDragDistance);
|
|
|
|
// Use config to calculate force with multipliers
|
|
float force = Config?.CalculateForce(dragDistance, dragRatio) ?? (dragRatio * MaxForce);
|
|
Vector2 direction = dragVector.normalized;
|
|
float mass = GetProjectileMass();
|
|
|
|
// Warn if mass is zero or invalid
|
|
if (mass <= 0f && showDebugLogs)
|
|
{
|
|
Logging.Warning($"[{GetType().Name}] Projectile mass is {mass}! Trajectory calculation will be inaccurate. Override GetProjectileMass().");
|
|
}
|
|
|
|
// Update visuals with mass parameter
|
|
UpdateVisuals(currentWorldPosition, direction, force, dragDistance, mass);
|
|
|
|
OnDragUpdate?.Invoke(currentWorldPosition, direction, force);
|
|
}
|
|
|
|
/// <summary>
|
|
/// End drag operation and potentially launch
|
|
/// </summary>
|
|
protected virtual void EndDrag(Vector2 currentWorldPosition)
|
|
{
|
|
_isDragging = false;
|
|
|
|
// Hide preview
|
|
HidePreview();
|
|
|
|
// Calculate final launch parameters
|
|
Vector2 dragVector = _dragStartPosition - currentWorldPosition;
|
|
float dragDistance = dragVector.magnitude;
|
|
float dragRatio = Mathf.Clamp01(dragDistance / MaxDragDistance);
|
|
|
|
// Use config to calculate force with multipliers
|
|
float force = Config?.CalculateForce(dragDistance, dragRatio) ?? (dragRatio * MaxForce);
|
|
Vector2 direction = dragVector.normalized;
|
|
|
|
// Get minimum force from config
|
|
float minForce = Config?.GetMinForce() ?? (MaxForce * 0.1f);
|
|
|
|
if (showDebugLogs)
|
|
Logging.Debug($"[{GetType().Name}] Drag ended - Force: {force:F2}, Min: {minForce:F2}, Distance: {dragDistance:F2}");
|
|
|
|
OnDragEnd?.Invoke(currentWorldPosition, direction, force);
|
|
|
|
// Launch if force exceeds minimum
|
|
if (force >= minForce)
|
|
{
|
|
if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Launching with force {force:F2}");
|
|
PerformLaunch(direction, force);
|
|
OnLaunch?.Invoke(direction, force);
|
|
}
|
|
else
|
|
{
|
|
if (showDebugLogs) Logging.Debug($"[{GetType().Name}] Drag too short - force {force:F2} < min {minForce:F2}");
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Abstract Methods - Subclass Implementation
|
|
|
|
/// <summary>
|
|
/// Perform the actual launch (spawn projectile/airplane, apply force, etc.)
|
|
/// </summary>
|
|
protected abstract void PerformLaunch(Vector2 direction, float force);
|
|
|
|
#endregion
|
|
|
|
#region Virtual Methods - Visual Feedback (Override if needed)
|
|
|
|
/// <summary>
|
|
/// Update visual feedback during drag (trajectory preview, rubber band, etc.)
|
|
/// Default: Updates trajectory preview using prefab's physics properties.
|
|
/// Override for custom visuals.
|
|
/// </summary>
|
|
protected virtual void UpdateVisuals(Vector2 currentPosition, Vector2 direction, float force, float dragDistance, float mass)
|
|
{
|
|
if (trajectoryPreview != null && dragDistance > 0.1f)
|
|
{
|
|
GameObject prefab = GetProjectilePrefab();
|
|
if (prefab != null)
|
|
{
|
|
trajectoryPreview.UpdateTrajectory(launchAnchor.position, direction, force, prefab);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Show preview visuals when controller is enabled.
|
|
/// Default: Shows trajectory preview.
|
|
/// Override for custom visuals.
|
|
/// </summary>
|
|
protected virtual void ShowPreview()
|
|
{
|
|
trajectoryPreview?.Show();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hide preview visuals when controller is disabled.
|
|
/// Default: Hides trajectory preview.
|
|
/// Override for custom visuals.
|
|
/// </summary>
|
|
protected virtual void HidePreview()
|
|
{
|
|
trajectoryPreview?.Hide();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Virtual Methods - Optional Override
|
|
|
|
/// <summary>
|
|
/// Get projectile mass for trajectory calculation.
|
|
/// Reads from the prefab's Rigidbody2D component.
|
|
/// Subclasses can override for custom behavior (e.g., if mass changes dynamically).
|
|
/// </summary>
|
|
protected virtual float GetProjectileMass()
|
|
{
|
|
GameObject prefab = GetProjectilePrefab();
|
|
if (prefab == null)
|
|
{
|
|
if (showDebugLogs)
|
|
Logging.Warning($"[{GetType().Name}] GetProjectilePrefab() returned null!");
|
|
return 0f;
|
|
}
|
|
|
|
var rb = prefab.GetComponent<Rigidbody2D>();
|
|
if (rb == null)
|
|
{
|
|
if (showDebugLogs)
|
|
Logging.Warning($"[{GetType().Name}] Projectile prefab '{prefab.name}' has no Rigidbody2D!");
|
|
return 0f;
|
|
}
|
|
|
|
return rb.mass;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get gravity value for trajectory calculation.
|
|
/// Uses Physics2D.gravity.magnitude * prefab's Rigidbody2D gravityScale.
|
|
/// </summary>
|
|
protected virtual float GetGravity()
|
|
{
|
|
GameObject prefab = GetProjectilePrefab();
|
|
if (prefab == null)
|
|
{
|
|
// Fallback to project gravity
|
|
return Physics2D.gravity.magnitude;
|
|
}
|
|
|
|
var rb = prefab.GetComponent<Rigidbody2D>();
|
|
float gravityScale = rb != null ? rb.gravityScale : 1f;
|
|
|
|
return Physics2D.gravity.magnitude * gravityScale;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Cleanup
|
|
|
|
internal override void OnManagedDestroy()
|
|
{
|
|
base.OnManagedDestroy();
|
|
|
|
// Ensure we unregister from InputManager
|
|
if (_isRegistered && InputManager.Instance != null)
|
|
{
|
|
InputManager.Instance.UnregisterOverrideConsumer(this);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|
|
|