Add pinch controls to statue dressup game

This commit is contained in:
Michal Pikulski
2025-12-10 15:15:11 +01:00
parent 082ce98f79
commit 0b4def8a05
21 changed files with 599 additions and 1487 deletions

View File

@@ -1,8 +1,8 @@
using UnityEngine;
using Minigames.TrashMaze.Objects;
using UnityEditor;
using Minigames.TrashMaze.Objects;
using UnityEngine;
namespace Minigames.TrashMaze.Editor
namespace Editor.CustomEditorsAndDrawers
{
[CustomEditor(typeof(RevealableObject))]
public class RevealableObjectEditor : UnityEditor.Editor

File diff suppressed because one or more lines are too long

View File

@@ -2010,6 +2010,7 @@ Transform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!212 &831113526
SpriteRenderer:
serializedVersion: 2
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
@@ -2055,6 +2056,7 @@ SpriteRenderer:
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 1
m_MaskInteraction: 0
m_Sprite: {fileID: -9176826819293939900, guid: 7b00cd0000ef4424985cd21fb4f197ee, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_FlipX: 0
@@ -2064,7 +2066,6 @@ SpriteRenderer:
m_AdaptiveModeThreshold: 0.5
m_SpriteTileMode: 0
m_WasSpriteAssigned: 1
m_MaskInteraction: 0
m_SpriteSortPoint: 0
--- !u!1 &948124904
GameObject:
@@ -6571,6 +6572,10 @@ PrefabInstance:
propertyPath: maxSpeed
value: 15
objectReference: {fileID: 0}
- target: {fileID: 7852204877518954380, guid: 8ac0210dbf9d7754e9526d6d5c214f49, type: 3}
propertyPath: maxAcceleration
value: 1000
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []

View File

@@ -228,6 +228,10 @@ namespace AppleHills.Core.Settings
bool EnableStatePersistence { get; }
string StateSaveKey { get; }
int MaxSavedDecorations { get; }
// Pinch Controls
float MinDecorationScale { get; }
float MaxDecorationScale { get; }
}
/// <summary>

View File

@@ -90,6 +90,13 @@ namespace Core.Settings
[Tooltip("Maximum number of decorations to save")]
[SerializeField] private int maxSavedDecorations = 50;
[Header("Pinch Controls")]
[Tooltip("Minimum scale for decorations when using pinch controls")]
[SerializeField] private float minDecorationScale = 0.1f;
[Tooltip("Maximum scale for decorations when using pinch controls")]
[SerializeField] private float maxDecorationScale = 2.0f;
// IStatueDressupSettings implementation - Decoration Display
public Vector2 DefaultAuthoredSize => defaultAuthoredSize;
@@ -136,6 +143,10 @@ namespace Core.Settings
public string StateSaveKey => stateSaveKey;
public int MaxSavedDecorations => maxSavedDecorations;
// IStatueDressupSettings implementation - Pinch Controls
public float MinDecorationScale => minDecorationScale;
public float MaxDecorationScale => maxDecorationScale;
public override void OnValidate()
{
base.OnValidate();
@@ -168,6 +179,10 @@ namespace Core.Settings
// Validate state persistence
maxSavedDecorations = Mathf.Max(1, maxSavedDecorations);
// Validate pinch controls
minDecorationScale = Mathf.Max(0.01f, minDecorationScale);
maxDecorationScale = Mathf.Max(minDecorationScale + 0.1f, maxDecorationScale);
}
}
}

View File

@@ -1,32 +1,33 @@
using Core.SaveLoad;
using UnityEditor.Animations;
using UnityEngine;
using System.Collections;
public class FrakkeAnimEventTrigger : MonoBehaviour
namespace DamianExperiments.Dump
{
[Header("Frakke Crashing References")]
public AppleMachine FrakkeSMRef;
public GameObject stateToChangeToAfterCrashing;
[Header("Fence Breaking References")]
public AppleMachine FenceSMRef;
public GameObject FenceStateToSet;
public void OnFrakkeCrashEnded()
public class FrakkeAnimEventTrigger : MonoBehaviour
{
FrakkeSMRef.ChangeState(stateToChangeToAfterCrashing);
}
[Header("Frakke Crashing References")]
public AppleMachine FrakkeSMRef;
public GameObject stateToChangeToAfterCrashing;
public void OnFenceBroken()
{
if (FenceSMRef != null)
[Header("Fence Breaking References")]
public AppleMachine FenceSMRef;
public GameObject FenceStateToSet;
public void OnFrakkeCrashEnded()
{
FenceSMRef.ChangeState(FenceStateToSet);
FrakkeSMRef.ChangeState(stateToChangeToAfterCrashing);
}
else
public void OnFenceBroken()
{
Debug.LogWarning("FrakkeRevUpCrashBehaviour: FenceSMRef is not assigned.");
if (FenceSMRef != null)
{
FenceSMRef.ChangeState(FenceStateToSet);
}
else
{
Debug.LogWarning("FrakkeRevUpCrashBehaviour: FenceSMRef is not assigned.");
}
}
}
}

View File

@@ -1,19 +1,20 @@
using Core.SaveLoad;
using UnityEditor.Animations;
using UnityEngine;
public class FrakkeCrashedBehaviour : MonoBehaviour
namespace DamianExperiments.Dump
{
public Animator FrakkeAnimControllerRef;
private void OnEnable()
public class FrakkeCrashedBehaviour : MonoBehaviour
{
if (FrakkeAnimControllerRef != null)
{
FrakkeAnimControllerRef.SetTrigger("Crashes");
}
}
public Animator FrakkeAnimControllerRef;
private void OnEnable()
{
if (FrakkeAnimControllerRef != null)
{
FrakkeAnimControllerRef.SetTrigger("Crashes");
}
}
}
}

View File

@@ -1,21 +1,21 @@
using Core.SaveLoad;
using UnityEditor.Animations;
using UnityEngine;
using System.Collections;
public class FrakkeRevUpCrashBehaviour : MonoBehaviour
namespace DamianExperiments.Dump
{
public Animator FrakkeAnimControllerRef;
private void OnEnable()
public class FrakkeRevUpCrashBehaviour : MonoBehaviour
{
if (FrakkeAnimControllerRef != null)
public Animator FrakkeAnimControllerRef;
private void OnEnable()
{
FrakkeAnimControllerRef.SetTrigger("SpeedsOff");
if (FrakkeAnimControllerRef != null)
{
FrakkeAnimControllerRef.SetTrigger("SpeedsOff");
}
}
}
}

View File

@@ -285,7 +285,7 @@ namespace Input
};
var results = new System.Collections.Generic.List<RaycastResult>();
EventSystem.current.RaycastAll(eventData, results);
EventSystem.current.RaycastAll(eventData, results);
foreach (var result in results)
{

View File

@@ -26,9 +26,8 @@ namespace Minigames.StatueDressup.Controllers
[SerializeField] private GameObject statue;
[SerializeField] private DecorationDraggableInstance draggablePrefab; // Prefab for spawning decorations
[Header("Edit UI")]
[SerializeField] private UI.DecorationEditUI editUIPrefab; // Prefab for edit UI
private UI.DecorationEditUI _editUIInstance;
[Header("Pinch Controls")]
private UI.DecorationPinchController _pinchControllerInstance;
[Header("UI Pages")]
[SerializeField] private UI.PlayAreaPage playAreaPage;
@@ -460,35 +459,37 @@ namespace Minigames.StatueDressup.Controllers
}
/// <summary>
/// Show edit UI for a placed decoration
/// Show pinch controls for a placed decoration
/// </summary>
public void ShowEditUI(DecorationDraggableInstance decoration)
{
if (decoration == null)
{
Logging.Warning("[StatueDecorationController] Cannot show edit UI - decoration is null");
Logging.Warning("[StatueDecorationController] Cannot show pinch controls - decoration is null");
return;
}
// Create edit UI instance if needed
if (_editUIInstance == null)
// Create pinch controller instance if needed
if (_pinchControllerInstance == null)
{
if (editUIPrefab == null)
{
Logging.Error("[StatueDecorationController] Edit UI prefab is not assigned!");
return;
}
// Instantiate as child of canvas (find appropriate parent)
// Find canvas transform
Transform canvasTransform = statueArea != null ? statueArea.root : transform.root;
_editUIInstance = Instantiate(editUIPrefab, canvasTransform);
_editUIInstance.transform.SetAsLastSibling(); // Ensure it's on top
Logging.Debug("[StatueDecorationController] Created edit UI instance");
// Create new GameObject with DecorationPinchController component
GameObject pinchControllerObj = new GameObject("DecorationPinchController");
pinchControllerObj.transform.SetParent(canvasTransform, false);
// Add the component (it will auto-create RectTransform, Image, CanvasGroup, PinchGestureHandler in Awake)
_pinchControllerInstance = pinchControllerObj.AddComponent<UI.DecorationPinchController>();
// Ensure it's on top
pinchControllerObj.transform.SetAsLastSibling();
Logging.Debug("[StatueDecorationController] Created pinch controller instance dynamically");
}
// Show the UI
_editUIInstance.Show(decoration);
// Show the pinch controls
_pinchControllerInstance.Show(decoration);
}
/// <summary>

View File

@@ -1,288 +0,0 @@
using Core;
using UnityEngine;
using UnityEngine.UI;
using Minigames.StatueDressup.DragDrop;
namespace Minigames.StatueDressup.UI
{
/// <summary>
/// UI panel for editing a placed decoration's scale and rotation.
/// Shows sliders for scale (0.1x - 2x) and rotation (-180° to 180°).
/// Changes are applied immediately to the decoration's transform.
/// </summary>
public class DecorationEditUI : MonoBehaviour
{
[Header("UI References")]
[SerializeField] private Slider scaleSlider;
[SerializeField] private Slider rotationSlider;
[SerializeField] private Button confirmButton;
[SerializeField] private Button resetButton;
[SerializeField] private CanvasGroup canvasGroup;
[Header("Slider Ranges")]
[SerializeField] private float minScale = 0.1f;
[SerializeField] private float maxScale = 2.0f;
[SerializeField] private float minRotation = -180f;
[SerializeField] private float maxRotation = 180f;
private DecorationDraggableInstance _targetDecoration;
private RectTransform _rectTransform;
private float _originalRotation;
private bool _isInitialized;
private void Awake()
{
// Get RectTransform
_rectTransform = GetComponent<RectTransform>();
// Ensure canvas group exists
if (canvasGroup == null)
{
canvasGroup = GetComponent<CanvasGroup>();
if (canvasGroup == null)
{
canvasGroup = gameObject.AddComponent<CanvasGroup>();
}
}
// Setup slider ranges
if (scaleSlider != null)
{
scaleSlider.minValue = minScale;
scaleSlider.maxValue = maxScale;
scaleSlider.onValueChanged.AddListener(OnScaleChanged);
}
if (rotationSlider != null)
{
rotationSlider.minValue = minRotation;
rotationSlider.maxValue = maxRotation;
rotationSlider.onValueChanged.AddListener(OnRotationChanged);
}
// Setup buttons
if (confirmButton != null)
{
confirmButton.onClick.AddListener(OnConfirm);
}
if (resetButton != null)
{
resetButton.onClick.AddListener(OnReset);
}
// Start hidden
gameObject.SetActive(false);
_isInitialized = true;
}
private void OnDestroy()
{
// Clean up listeners
if (scaleSlider != null)
{
scaleSlider.onValueChanged.RemoveListener(OnScaleChanged);
}
if (rotationSlider != null)
{
rotationSlider.onValueChanged.RemoveListener(OnRotationChanged);
}
if (confirmButton != null)
{
confirmButton.onClick.RemoveListener(OnConfirm);
}
if (resetButton != null)
{
resetButton.onClick.RemoveListener(OnReset);
}
}
/// <summary>
/// Show the edit UI for the given decoration
/// </summary>
public void Show(DecorationDraggableInstance decoration)
{
if (!_isInitialized)
{
Logging.Error("[DecorationEditUI] Attempted to show before initialization!");
return;
}
if (decoration == null)
{
Logging.Error("[DecorationEditUI] Cannot show edit UI - decoration is null!");
return;
}
_targetDecoration = decoration;
// Store original rotation for reference
_originalRotation = decoration.transform.localEulerAngles.z;
// Normalize rotation to -180 to 180 range
if (_originalRotation > 180f)
{
_originalRotation -= 360f;
}
// Initialize sliders from current transform values
if (scaleSlider != null)
{
// Use X component for uniform scale
float currentScale = decoration.transform.localScale.x;
scaleSlider.value = Mathf.Clamp(currentScale, minScale, maxScale);
}
if (rotationSlider != null)
{
rotationSlider.value = Mathf.Clamp(_originalRotation, minRotation, maxRotation);
}
// Disable decoration raycasts during editing
CanvasGroup decorationCanvasGroup = decoration.GetComponent<CanvasGroup>();
if (decorationCanvasGroup != null)
{
decorationCanvasGroup.blocksRaycasts = false;
}
// Position UI centered over the decoration (context menu style)
PositionOverDecoration(decoration);
// Show UI immediately
gameObject.SetActive(true);
if (canvasGroup != null)
{
canvasGroup.alpha = 1f;
}
Logging.Debug($"[DecorationEditUI] Showing edit UI for: {decoration.Data?.DecorationName}");
}
/// <summary>
/// Position the UI centered over the decoration (context menu style)
/// </summary>
private void PositionOverDecoration(DecorationDraggableInstance decoration)
{
if (_rectTransform == null || decoration == null) return;
// Get decoration's world position
Vector3 decorationWorldPos = decoration.transform.position;
// Convert to canvas space if using screen space overlay
Canvas canvas = GetComponentInParent<Canvas>();
if (canvas != null && canvas.renderMode == RenderMode.ScreenSpaceOverlay)
{
// For overlay canvas, world position is already correct
_rectTransform.position = decorationWorldPos;
}
else if (canvas != null)
{
// For other canvas modes, convert properly
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvas.transform as RectTransform,
RectTransformUtility.WorldToScreenPoint(canvas.worldCamera, decorationWorldPos),
canvas.worldCamera,
out Vector2 localPoint
);
_rectTransform.localPosition = localPoint;
}
Logging.Debug($"[DecorationEditUI] Positioned at decoration location: {decorationWorldPos}");
}
/// <summary>
/// Hide the edit UI
/// </summary>
public void Hide()
{
if (_targetDecoration != null)
{
// Re-enable decoration raycasts
CanvasGroup decorationCanvasGroup = _targetDecoration.GetComponent<CanvasGroup>();
if (decorationCanvasGroup != null)
{
decorationCanvasGroup.blocksRaycasts = true;
}
}
_targetDecoration = null;
gameObject.SetActive(false);
Logging.Debug("[DecorationEditUI] Edit UI hidden");
}
/// <summary>
/// Handle scale slider change
/// </summary>
private void OnScaleChanged(float value)
{
if (_targetDecoration == null) return;
// Apply uniform scale (X, Y, Z all the same)
_targetDecoration.transform.localScale = Vector3.one * value;
}
/// <summary>
/// Handle rotation slider change
/// </summary>
private void OnRotationChanged(float value)
{
if (_targetDecoration == null) return;
// Apply Z rotation only
_targetDecoration.transform.localEulerAngles = new Vector3(0f, 0f, value);
}
/// <summary>
/// Handle confirm button - save changes and close
/// </summary>
private void OnConfirm()
{
if (_targetDecoration != null)
{
// Trigger auto-save through the controller
var controller = Controllers.StatueDecorationController.Instance;
if (controller != null)
{
// The controller's RegisterDecoration already triggers SaveStatueState
// Since the decoration is already registered, we just need to trigger a save
// This happens automatically on the next RegisterDecoration/UnregisterDecoration call
Logging.Debug("[DecorationEditUI] Changes confirmed - will be auto-saved");
}
}
Hide();
}
/// <summary>
/// Handle reset button - restore original values
/// </summary>
private void OnReset()
{
if (_targetDecoration == null) return;
// Reset to authored size (scale 1.0) and 0 rotation
float defaultScale = 1.0f;
float defaultRotation = 0f;
if (scaleSlider != null)
{
scaleSlider.value = defaultScale;
}
if (rotationSlider != null)
{
rotationSlider.value = defaultRotation;
}
// Values are applied through the slider callbacks
Logging.Debug("[DecorationEditUI] Reset to defaults");
}
}
}

View File

@@ -1,12 +0,0 @@
fileFormatVersion: 2
guid: 7f3e2a1b9c4d5e6f7a8b9c0d1e2f3a4b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,266 @@
using Core;
using Minigames.StatueDressup.Controllers;
using Minigames.StatueDressup.DragDrop;
using UnityEngine;
using UnityEngine.UI;
namespace Minigames.StatueDressup.UI
{
/// <summary>
/// Manages pinch-to-scale and pinch-to-rotate controls for placed decorations.
/// Shows a semi-transparent backdrop and listens for pinch gestures.
/// Tap anywhere to dismiss and save changes.
/// </summary>
public class DecorationPinchController : MonoBehaviour
{
[Header("UI References")]
[SerializeField] private PinchGestureHandler gestureHandler;
[SerializeField] private CanvasGroup backdropCanvasGroup;
[SerializeField] private Image backdropImage;
[Header("Backdrop Settings")]
[SerializeField] private Color backdropColor = new Color(0f, 0f, 0f, 0.5f);
[Tooltip("Sort order for the active decoration to render above backdrop")]
[SerializeField] private int activeDecorationSortOrder = 100;
private DecorationDraggableInstance _targetDecoration;
private AppleHills.Core.Settings.IStatueDressupSettings _settings;
private bool _isInitialized;
// Canvas management for rendering order
private Canvas _temporaryDecorationCanvas;
private int _backdropSortOrder;
private void Awake()
{
// Ensure components exist
if (gestureHandler == null)
{
gestureHandler = GetComponentInChildren<PinchGestureHandler>();
if (gestureHandler == null)
{
// Create gesture handler if not found
GameObject handlerObj = new GameObject("PinchGestureHandler");
handlerObj.transform.SetParent(transform, false);
gestureHandler = handlerObj.AddComponent<PinchGestureHandler>();
}
}
if (backdropCanvasGroup == null)
{
backdropCanvasGroup = GetComponent<CanvasGroup>();
if (backdropCanvasGroup == null)
{
backdropCanvasGroup = gameObject.AddComponent<CanvasGroup>();
}
}
if (backdropImage == null)
{
backdropImage = GetComponent<Image>();
if (backdropImage == null)
{
backdropImage = gameObject.AddComponent<Image>();
}
}
// Configure backdrop
backdropImage.color = backdropColor;
backdropImage.raycastTarget = true;
// Ensure fullscreen
RectTransform rectTransform = GetComponent<RectTransform>();
if (rectTransform != null)
{
rectTransform.anchorMin = Vector2.zero;
rectTransform.anchorMax = Vector2.one;
rectTransform.offsetMin = Vector2.zero;
rectTransform.offsetMax = Vector2.zero;
}
// Subscribe to gesture events
if (gestureHandler != null)
{
gestureHandler.OnPinchScale += HandlePinchScale;
gestureHandler.OnPinchRotate += HandlePinchRotate;
gestureHandler.OnDismissTap += HandleDismiss;
}
// Load settings
_settings = DecorationDataManager.Instance?.Settings;
// Get backdrop's canvas sort order for reference
Canvas backdropCanvas = GetComponentInParent<Canvas>();
if (backdropCanvas != null)
{
_backdropSortOrder = backdropCanvas.sortingOrder;
}
// Start hidden
gameObject.SetActive(false);
_isInitialized = true;
}
private void OnDestroy()
{
// Unsubscribe from gesture events
if (gestureHandler != null)
{
gestureHandler.OnPinchScale -= HandlePinchScale;
gestureHandler.OnPinchRotate -= HandlePinchRotate;
gestureHandler.OnDismissTap -= HandleDismiss;
}
}
/// <summary>
/// Show pinch controls for the given decoration
/// </summary>
public void Show(DecorationDraggableInstance decoration)
{
if (!_isInitialized)
{
Logging.Error("[DecorationPinchController] Attempted to show before initialization!");
return;
}
if (decoration == null)
{
Logging.Error("[DecorationPinchController] Cannot show pinch controller - decoration is null!");
return;
}
_targetDecoration = decoration;
// Add temporary Canvas component to decoration to control render order
_temporaryDecorationCanvas = decoration.gameObject.GetComponent<Canvas>();
if (_temporaryDecorationCanvas == null)
{
_temporaryDecorationCanvas = decoration.gameObject.AddComponent<Canvas>();
}
// Configure canvas to override sorting and render above backdrop
_temporaryDecorationCanvas.overrideSorting = true;
_temporaryDecorationCanvas.sortingOrder = _backdropSortOrder + activeDecorationSortOrder;
// Disable decoration raycasts during editing
CanvasGroup decorationCanvasGroup = decoration.GetComponent<CanvasGroup>();
if (decorationCanvasGroup != null)
{
decorationCanvasGroup.blocksRaycasts = false;
}
// Show UI
gameObject.SetActive(true);
if (backdropCanvasGroup != null)
{
backdropCanvasGroup.alpha = 1f;
backdropCanvasGroup.blocksRaycasts = true;
}
// Activate gesture detection
if (gestureHandler != null)
{
gestureHandler.Activate();
}
Logging.Debug($"[DecorationPinchController] Showing pinch controls for: {decoration.Data?.DecorationName}");
}
/// <summary>
/// Hide pinch controls
/// </summary>
public void Hide()
{
if (_targetDecoration != null)
{
// Remove temporary Canvas component to restore original rendering
if (_temporaryDecorationCanvas != null)
{
Destroy(_temporaryDecorationCanvas);
_temporaryDecorationCanvas = null;
}
// Re-enable decoration raycasts
CanvasGroup decorationCanvasGroup = _targetDecoration.GetComponent<CanvasGroup>();
if (decorationCanvasGroup != null)
{
decorationCanvasGroup.blocksRaycasts = true;
}
}
// Deactivate gesture detection
if (gestureHandler != null)
{
gestureHandler.Deactivate();
}
_targetDecoration = null;
gameObject.SetActive(false);
Logging.Debug("[DecorationPinchController] Pinch controls hidden");
}
/// <summary>
/// Handle pinch scale gesture
/// </summary>
private void HandlePinchScale(float scaleDelta)
{
if (_targetDecoration == null || _settings == null) return;
// Get current scale
Vector3 currentScale = _targetDecoration.transform.localScale;
float newScaleValue = currentScale.x + scaleDelta;
// Clamp to settings constraints
newScaleValue = Mathf.Clamp(newScaleValue, _settings.MinDecorationScale, _settings.MaxDecorationScale);
// Apply uniform scale
_targetDecoration.transform.localScale = Vector3.one * newScaleValue;
}
/// <summary>
/// Handle pinch rotate gesture
/// </summary>
private void HandlePinchRotate(float angleDelta)
{
if (_targetDecoration == null) return;
// Get current rotation
Vector3 currentRotation = _targetDecoration.transform.localEulerAngles;
// Apply rotation delta (Z-axis only for 2D)
float newZRotation = currentRotation.z + angleDelta;
// Normalize to -180 to 180 range for cleaner values
while (newZRotation > 180f) newZRotation -= 360f;
while (newZRotation < -180f) newZRotation += 360f;
_targetDecoration.transform.localEulerAngles = new Vector3(0f, 0f, newZRotation);
}
/// <summary>
/// Handle dismiss tap - save and close
/// </summary>
private void HandleDismiss()
{
if (_targetDecoration != null)
{
// Trigger auto-save through the controller
var controller = Controllers.StatueDecorationController.Instance;
if (controller != null)
{
// The decoration is already registered, but we need to trigger a save
// We can force this by unregistering and re-registering
controller.UnregisterDecoration(_targetDecoration);
controller.RegisterDecoration(_targetDecoration);
Logging.Debug("[DecorationPinchController] Changes saved on dismissal");
}
}
Hide();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1c8a81e283ec41b7b257c0a6824d32cf
timeCreated: 1765369467

View File

@@ -0,0 +1,212 @@
using System;
using Core;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.EnhancedTouch;
using Touch = UnityEngine.InputSystem.EnhancedTouch.Touch;
namespace Minigames.StatueDressup.UI
{
/// <summary>
/// Detects and processes pinch gestures for scaling and rotation using Unity's new Input System (EnhancedTouch).
/// Also detects single-tap dismissal and provides keyboard shortcuts for editor testing.
/// </summary>
public class PinchGestureHandler : MonoBehaviour
{
[Header("Gesture Settings")]
[Tooltip("Minimum distance change to register as pinch scale gesture")]
[SerializeField] private float minPinchDistance = 10f;
[Tooltip("Sensitivity multiplier for scale changes")]
[SerializeField] private float scaleSensitivity = 0.01f;
[Tooltip("Sensitivity multiplier for rotation changes")]
[SerializeField] private float rotationSensitivity = 1.0f;
[Header("Editor Testing (Keyboard)")]
[Tooltip("Keyboard rotation speed in degrees per second")]
[SerializeField] private float keyboardRotationSpeed = 90f;
[Tooltip("Keyboard scale speed per second")]
[SerializeField] private float keyboardScaleSpeed = 0.5f;
// Events
public event Action<float> OnPinchScale;
public event Action<float> OnPinchRotate;
public event Action OnDismissTap;
private bool _isActive;
private float _previousDistance;
private float _previousAngle;
private double _touchStartTime;
private bool _wasTouchTracked;
/// <summary>
/// Activate gesture detection
/// </summary>
public void Activate()
{
_isActive = true;
_previousDistance = 0f;
_previousAngle = 0f;
_wasTouchTracked = false;
// Enable enhanced touch support for new Input System
if (!EnhancedTouchSupport.enabled)
{
EnhancedTouchSupport.Enable();
}
Logging.Debug("[PinchGestureHandler] Activated");
}
/// <summary>
/// Deactivate gesture detection
/// </summary>
public void Deactivate()
{
_isActive = false;
Logging.Debug("[PinchGestureHandler] Deactivated");
}
private void Update()
{
if (!_isActive) return;
// Handle touch input (mobile) - using new Input System
if (Touch.activeTouches.Count > 0)
{
HandleTouchInput();
}
// Handle keyboard input (editor testing)
else if (Application.isEditor)
{
HandleKeyboardInput();
}
}
/// <summary>
/// Process touch input for pinch gestures and single-tap dismissal
/// </summary>
private void HandleTouchInput()
{
var activeTouches = Touch.activeTouches;
// Single tap for dismissal
if (activeTouches.Count == 1)
{
Touch touch = activeTouches[0];
// Track touch start time
if (touch.phase == UnityEngine.InputSystem.TouchPhase.Began)
{
_touchStartTime = touch.startTime;
_wasTouchTracked = true;
}
// Detect tap (touch began and ended quickly - within 0.3 seconds)
else if (touch.phase == UnityEngine.InputSystem.TouchPhase.Ended && _wasTouchTracked)
{
double touchDuration = Time.timeAsDouble - _touchStartTime;
if (touchDuration < 0.3)
{
Logging.Debug("[PinchGestureHandler] Single tap detected for dismissal");
OnDismissTap?.Invoke();
}
_wasTouchTracked = false;
}
}
// Two-finger pinch for scale and rotation
else if (activeTouches.Count == 2)
{
Touch touch0 = activeTouches[0];
Touch touch1 = activeTouches[1];
// Calculate current positions and distance
Vector2 currentTouch0 = touch0.screenPosition;
Vector2 currentTouch1 = touch1.screenPosition;
float currentDistance = Vector2.Distance(currentTouch0, currentTouch1);
float currentAngle = Mathf.Atan2(currentTouch1.y - currentTouch0.y, currentTouch1.x - currentTouch0.x) * Mathf.Rad2Deg;
// Initialize on first frame of pinch
if (touch0.phase == UnityEngine.InputSystem.TouchPhase.Began || touch1.phase == UnityEngine.InputSystem.TouchPhase.Began)
{
_previousDistance = currentDistance;
_previousAngle = currentAngle;
Logging.Debug("[PinchGestureHandler] Pinch gesture started");
return;
}
// Process pinch scale (distance change)
if (touch0.phase == UnityEngine.InputSystem.TouchPhase.Moved || touch1.phase == UnityEngine.InputSystem.TouchPhase.Moved)
{
if (_previousDistance > minPinchDistance)
{
float distanceDelta = currentDistance - _previousDistance;
float scaleDelta = distanceDelta * scaleSensitivity;
if (Mathf.Abs(scaleDelta) > 0.001f)
{
OnPinchScale?.Invoke(scaleDelta);
Logging.Debug($"[PinchGestureHandler] Scale delta: {scaleDelta:F3}");
}
}
// Process pinch rotation (angle change)
float angleDelta = Mathf.DeltaAngle(_previousAngle, currentAngle) * rotationSensitivity;
if (Mathf.Abs(angleDelta) > 0.5f)
{
OnPinchRotate?.Invoke(angleDelta);
Logging.Debug($"[PinchGestureHandler] Rotation delta: {angleDelta:F1}°");
}
// Update previous values
_previousDistance = currentDistance;
_previousAngle = currentAngle;
}
}
}
/// <summary>
/// Process keyboard input for editor testing
/// Q/E for rotation, +/- for scale, Escape for dismissal
/// </summary>
private void HandleKeyboardInput()
{
var keyboard = Keyboard.current;
if (keyboard == null) return;
// Rotation with Q/E
if (keyboard.qKey.isPressed)
{
float rotateDelta = -keyboardRotationSpeed * Time.deltaTime;
OnPinchRotate?.Invoke(rotateDelta);
}
else if (keyboard.eKey.isPressed)
{
float rotateDelta = keyboardRotationSpeed * Time.deltaTime;
OnPinchRotate?.Invoke(rotateDelta);
}
// Scale with +/- (equals key is where + is on keyboard)
if (keyboard.equalsKey.isPressed || keyboard.numpadPlusKey.isPressed)
{
float scaleDelta = keyboardScaleSpeed * Time.deltaTime;
OnPinchScale?.Invoke(scaleDelta);
}
else if (keyboard.minusKey.isPressed || keyboard.numpadMinusKey.isPressed)
{
float scaleDelta = -keyboardScaleSpeed * Time.deltaTime;
OnPinchScale?.Invoke(scaleDelta);
}
// Dismissal with Escape or Space
if (keyboard.escapeKey.wasPressedThisFrame || keyboard.spaceKey.wasPressedThisFrame)
{
Logging.Debug("[PinchGestureHandler] Keyboard dismissal detected");
OnDismissTap?.Invoke();
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: daa669f49a29434a927eafbd291e251f
timeCreated: 1765369442

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 3a935f5e791c46df8920c2c33f1c24c0
timeCreated: 1765361215

View File

@@ -25,11 +25,12 @@ MonoBehaviour:
useRigidbody: 0
defaultHoldMovementMode: 1
followerMovement:
followDistance: 1.5
manualMoveSmooth: 8
thresholdFar: 2.5
thresholdNear: 0.5
followDistance: 5
manualMoveSmooth: 1
thresholdFar: 10
thresholdNear: 3
stopThreshold: 0.1
followUpdateInterval: 0.1
followerSpeedMultiplier: 1.2
followerSpeedMultiplier: 0.9
heldIconDisplayHeight: 2
trashMazeVisionRadius: 8

View File

@@ -72,3 +72,5 @@ MonoBehaviour:
enableStatePersistence: 1
stateSaveKey: StatueDecorationState
maxSavedDecorations: 50
minDecorationScale: 0.1
maxDecorationScale: 2

View File

@@ -143,7 +143,7 @@ PlayerSettings:
loadStoreDebugModeEnabled: 0
visionOSBundleVersion: 1.0
tvOSBundleVersion: 1.0
bundleVersion: 0.6
bundleVersion: 0.7
preloadedAssets: []
metroInputSource: 0
wsaTransparentSwapchain: 0
@@ -177,7 +177,7 @@ PlayerSettings:
iPhone: 0
tvOS: 0
overrideDefaultApplicationIdentifier: 1
AndroidBundleVersionCode: 2
AndroidBundleVersionCode: 3
AndroidMinSdkVersion: 25
AndroidTargetSdkVersion: 0
AndroidPreferredInstallLocation: 1