using BrewMonster; using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; public class Joystick : MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler { public float Horizontal { get { return input.x; } } public float Vertical { get { return input.y; } } public Vector2 Direction { get { return input; } } public float HandleRange { get { return handleRange; } set { handleRange = Mathf.Abs(value); } } public float DeadZone { get { return deadZone; } set { deadZone = Mathf.Abs(value); } } public AxisOptions AxisOptions { get { return AxisOptions; } set { axisOptions = value; } } public bool SnapX { get { return snapX; } set { snapX = value; } } public bool SnapY { get { return snapY; } set { snapY = value; } } [SerializeField] private float handleRange = 1; [SerializeField] private float deadZone = 0; [SerializeField] private AxisOptions axisOptions = AxisOptions.Both; [SerializeField] private bool snapX = false; [SerializeField] private bool snapY = false; [SerializeField] protected RectTransform background = null; [SerializeField] private RectTransform handle = null; private RectTransform baseRect = null; private Canvas canvas; private Camera cam; private Vector2 input = Vector2.zero; private Vector2 previousSnappedInput = Vector2.zero; protected virtual void Start() { HandleRange = handleRange; DeadZone = deadZone; baseRect = GetComponent(); canvas = GetComponentInParent(); if (canvas == null) Debug.LogError("The Joystick is not placed inside a canvas"); Vector2 center = new Vector2(0.5f, 0.5f); background.pivot = center; handle.anchorMin = center; handle.anchorMax = center; handle.pivot = center; handle.anchoredPosition = Vector2.zero; } public virtual void OnPointerDown(PointerEventData eventData) { previousSnappedInput = Vector2.zero; OnDrag(eventData); } public void OnDrag(PointerEventData eventData) { cam = null; if (canvas.renderMode == RenderMode.ScreenSpaceCamera) cam = canvas.worldCamera; Vector2 position = RectTransformUtility.WorldToScreenPoint(cam, background.position); Vector2 radius = background.sizeDelta / 2; input = (eventData.position - position) / (radius * canvas.scaleFactor); FormatInput(); HandleInput(input.magnitude, input.normalized, radius, cam); handle.anchoredPosition = input * radius * handleRange; // Send event when value changes significantly (for 360-degree smooth movement) Vector2 currentSnapped = new Vector2(SnapToDiscrete(input.x), SnapToDiscrete(input.y)); if ((currentSnapped.x == -1 || currentSnapped.x == 1 || currentSnapped.y == -1 || currentSnapped.y == 1) && currentSnapped != previousSnappedInput) { //BMLogger.LogError($"Joystick snapped to: {currentSnapped}"); EventBus.Publish(new JoystickPressEvent()); } else if (currentSnapped == Vector2.zero && previousSnappedInput != Vector2.zero) { EventBus.Publish(new JoystickRealeaseEvent()); } previousSnappedInput = currentSnapped; } protected virtual void HandleInput(float magnitude, Vector2 normalised, Vector2 radius, Camera cam) { if (magnitude > deadZone) { // Normalize to ensure speed is always 1 (full speed) or 0 (no movement) // This keeps 360-degree direction but binary speed input = normalised; } else input = Vector2.zero; } private void FormatInput() { if (axisOptions == AxisOptions.Horizontal) input = new Vector2(input.x, 0f); else if (axisOptions == AxisOptions.Vertical) input = new Vector2(0f, input.y); } private Vector2 SnapTo8Directions(Vector2 input) { // Snap to 8 directions: N, NE, E, SE, S, SW, W, NW // Returns values of -1, 0, or 1 for each axis if (input.magnitude < 0.4f) return Vector2.zero; // Calculate angle in degrees (0 = up/North, 90 = right/East) float angle = Mathf.Atan2(input.x, input.y) * Mathf.Rad2Deg; // Normalize angle to 0-360 if (angle < 0) angle += 360f; // Snap to 8 directions (every 45 degrees) // 0° = N, 45° = NE, 90° = E, 135° = SE, 180° = S, 225° = SW, 270° = W, 315° = NW float snappedAngle = Mathf.Round(angle / 45f) * 45f; // Convert back to direction vector float rad = snappedAngle * Mathf.Deg2Rad; Vector2 snapped = new Vector2(Mathf.Sin(rad), Mathf.Cos(rad)); // Ensure values are exactly -1, 0, or 1 snapped.x = Mathf.Round(snapped.x); snapped.y = Mathf.Round(snapped.y); return snapped; } private float SnapToDiscrete(float value) { // Snap to -1, 1, or 0 for 8-directional movement // Use a small threshold to ensure diagonal movement works const float threshold = 0.1f; if (Mathf.Abs(value) < threshold) return 0; return value > 0 ? 1 : -1; } private float SnapFloat(float value, AxisOptions snapAxis) { if (value == 0) return value; if (axisOptions == AxisOptions.Both) { float angle = Vector2.Angle(input, Vector2.up); if (snapAxis == AxisOptions.Horizontal) { if (angle < 22.5f || angle > 157.5f) return 0; else return (value > 0) ? 1 : -1; } else if (snapAxis == AxisOptions.Vertical) { if (angle > 67.5f && angle < 112.5f) return 0; else return (value > 0) ? 1 : -1; } return value; } else { if (value > 0) return 1; if (value < 0) return -1; } return 0; } public virtual void OnPointerUp(PointerEventData eventData) { input = Vector2.zero; handle.anchoredPosition = Vector2.zero; previousSnappedInput = Vector2.zero; EventBus.Publish(new JoystickRealeaseEvent()); } protected Vector2 ScreenPointToAnchoredPosition(Vector2 screenPosition) { Vector2 localPoint = Vector2.zero; if (RectTransformUtility.ScreenPointToLocalPointInRectangle(baseRect, screenPosition, cam, out localPoint)) { Vector2 pivotOffset = baseRect.pivot * baseRect.sizeDelta; return localPoint - (background.anchorMax * baseRect.sizeDelta) + pivotOffset; } return Vector2.zero; } } public enum AxisOptions { Both, Horizontal, Vertical } public struct JoystickPressEvent { } public struct JoystickRealeaseEvent { }