Skip to content

Instantly share code, notes, and snippets.

@koras
Created December 7, 2025 12:02
Show Gist options
  • Select an option

  • Save koras/85cc7657c303401ce5710b8f301092d1 to your computer and use it in GitHub Desktop.

Select an option

Save koras/85cc7657c303401ce5710b8f301092d1 to your computer and use it in GitHub Desktop.
pinch
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.EnhancedTouch;
using Touch = UnityEngine.InputSystem.EnhancedTouch.Touch;
using TMPro;
namespace Input
{
public class PinchToZoomAndPan : MonoBehaviour
{
[Header("Camera / Map")]
public Camera targetCamera; // Камера, через которую смотрим на карту
[SerializeField] public float zoomSpeed = 0.2f; // чувствительность пинч-зума
[SerializeField] public float minZoom = 10f; // минимальный размер (минимальный Zoom)
[SerializeField] private TMP_Text _textMinZoom = null;
[SerializeField] private TMP_Text _textMaxZoom = null;
[SerializeField] private TMP_Text _textCurrentZoom = null;
[SerializeField] public float maxZoom = 20f; // максимальный Zoom
[Header("Pan")]
[SerializeField] public float panSpeed = 1f; // скорость перемещения карты (панорамирования)
[Header("Mouse Wheel Zoom")]
[SerializeField] public float scrollZoomSpeed = 0.02f; // скорость зума от колёсика мыши
[Header("Camera Bounds")]
[SerializeField] private float minX = -10f; // левая граница движения камеры
[SerializeField] private float maxX = 10f; // правая граница
[SerializeField] private float minY = -10f; // нижняя
[SerializeField] private float maxY = 10f; // верхняя
// внутренние данные пинча
float _previousPinchDistance;
bool _wasPinching;
// внутренние данные пана
bool _isPanning; // в данный момент тянем карту?
Vector3 _lastPanWorldPos; // позиция курсора/пальца в мире на предыдущем кадре
// шаг зума по кнопкам UI
[SerializeField] private float buttonZoomStep = 1f;
// Кнопка "приблизить" — уменьшает зум (orthoSize)
public void ZoomInButton()
{
ApplyZoom(-buttonZoomStep);
}
// Кнопка "отдалить"
public void ZoomOutButton()
{
ApplyZoom(buttonZoomStep);
}
// UI кнопка: увеличить minZoom
public void AddMinZoom()
{
minZoom += 0.1f;
UpdateMinZoomText();
}
// UI кнопка: уменьшить minZoom
public void SubMinZoom()
{
minZoom -= 0.1f;
UpdateMinZoomText();
}
// UI кнопка: увеличить maxZoom
public void AddMaxZoom()
{
maxZoom += 1.0f;
UpdateMaxZoomText();
}
// UI кнопка: уменьшить maxZoom
public void SubMaxZoom()
{
maxZoom -= 1.0f;
UpdateMaxZoomText();
}
// Перевод позиции экрана в мировую координату на плоскости Z=0
// Вся логика пана строится на разнице двух таких точек
private Vector3 ScreenToWorldOnMap(Vector2 screenPos)
{
Plane plane = new Plane(Vector3.forward, Vector3.zero); // плоскость карты (XY)
Ray ray = targetCamera.ScreenPointToRay(screenPos);
if (plane.Raycast(ray, out float enter))
return ray.GetPoint(enter);
return Vector3.zero;
}
// Обновление UI текущего зума
void UpdateCurrentZoomText()
{
if (_textCurrentZoom == null)
return;
float zoomValue =
targetCamera.orthographic ? targetCamera.orthographicSize : targetCamera.fieldOfView;
_textCurrentZoom.text = "z " + zoomValue.ToString("0.0");
}
// Инициализация текста
void Start()
{
UpdateMinZoomText();
UpdateMaxZoomText();
UpdateCurrentZoomText();
}
// Обновление текста maxZoom
void UpdateMaxZoomText()
{
if (_textMaxZoom != null)
_textMaxZoom.text = "" + maxZoom.ToString("0.0");
}
// Обновление текста minZoom
void UpdateMinZoomText()
{
if (_textMinZoom != null)
_textMinZoom.text = "" + minZoom.ToString("0.0");
}
void Awake()
{
// если пользователь камеру не указал
if (targetCamera == null)
targetCamera = Camera.main;
}
void OnEnable()
{
// новый Input System для тачей требует включения
EnhancedTouchSupport.Enable();
}
void OnDisable()
{
EnhancedTouchSupport.Disable();
}
void Update()
{
// Если активны пальцы — работаем в режиме тачей
if (Touch.activeTouches.Count > 0)
HandleTouchInput();
else
HandleMouseInput(); // иначе мышка
}
// ==================== TOUCH INPUT ====================
void HandleTouchInput()
{
int touchCount = Touch.activeTouches.Count;
if (touchCount >= 2)
{
// ПИНЧ-ЗУМ
var t0 = Touch.activeTouches[0];
var t1 = Touch.activeTouches[1];
float currentDistance = Vector2.Distance(t0.screenPosition, t1.screenPosition);
if (!_wasPinching)
{
// начало пинча
_previousPinchDistance = currentDistance;
_wasPinching = true;
}
else
{
// изменение расстояния между пальцами
float delta = currentDistance - _previousPinchDistance;
_previousPinchDistance = currentDistance;
// зумим карту
float zoomChange = -delta * zoomSpeed * Time.deltaTime;
ApplyZoom(zoomChange);
}
_isPanning = false; // при двух пальцах пан недоступен
}
else if (touchCount == 1)
{
var touch = Touch.activeTouches[0];
if (touch.phase == UnityEngine.InputSystem.TouchPhase.Began)
{
// начало пана
_isPanning = true;
_wasPinching = false;
_lastPanWorldPos = ScreenToWorldOnMap(touch.screenPosition);
}
else if (touch.phase == UnityEngine.InputSystem.TouchPhase.Moved && _isPanning)
{
// продолжаем панить — считаем разницу позиций
Vector3 curWorld = ScreenToWorldOnMap(touch.screenPosition);
Vector3 delta = _lastPanWorldPos - curWorld;
Pan(delta);
_lastPanWorldPos = curWorld;
}
else if (touch.phase == UnityEngine.InputSystem.TouchPhase.Ended ||
touch.phase == UnityEngine.InputSystem.TouchPhase.Canceled)
{
// конец касания
_isPanning = false;
_wasPinching = false;
}
}
else
{
// если нет пальцев — сброс
_isPanning = false;
_wasPinching = false;
}
}
// ==================== MOUSE INPUT ====================
void HandleMouseInput()
{
// начало пана — ЛКМ нажата
if (Mouse.current.leftButton.wasPressedThisFrame)
{
_isPanning = true;
_lastPanWorldPos = ScreenToWorldOnMap(Mouse.current.position.ReadValue());
}
else if (Mouse.current.leftButton.isPressed && _isPanning)
{
// движение мыши во время пана
Vector3 curWorld = ScreenToWorldOnMap(Mouse.current.position.ReadValue());
Vector3 delta = _lastPanWorldPos - curWorld;
Pan(delta);
_lastPanWorldPos = curWorld;
}
else if (Mouse.current.leftButton.wasReleasedThisFrame)
{
// конец пана
_isPanning = false;
}
// Зум колёсиком (без deltaTime — событие дискретное)
float scroll = Mouse.current.scroll.ReadValue().y;
if (Mathf.Abs(scroll) > 0.01f)
{
float zoomChange = -scroll * scrollZoomSpeed;
ApplyZoom(zoomChange);
}
}
// ==================== PAN LOGIC ====================
void Pan(Vector3 delta)
{
// смещаем камеру на разницу позиций курсора в мировых координатах
targetCamera.transform.position += delta * panSpeed;
// применяем границы движения
Vector3 pos = targetCamera.transform.position;
pos.x = Mathf.Clamp(pos.x, minX, maxX);
pos.y = Mathf.Clamp(pos.y, minY, maxY);
targetCamera.transform.position = pos;
}
// ==================== ZOOM LOGIC ====================
void ApplyZoom(float zoomChange)
{
// ортографическая камера — меняем orthographicSize
if (targetCamera.orthographic)
{
float size = targetCamera.orthographicSize;
size += zoomChange;
targetCamera.orthographicSize = Mathf.Clamp(size, minZoom, maxZoom);
}
else
{
// перспектива — меняем fieldOfView
float fov = targetCamera.fieldOfView;
fov += zoomChange;
targetCamera.fieldOfView = Mathf.Clamp(fov, minZoom, maxZoom);
}
UpdateCurrentZoomText();
}
}
}
@inthesquare
Copy link

chat gpt moment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment