Skip to content

Instantly share code, notes, and snippets.

@t-34400
Last active April 13, 2024 18:15
Show Gist options
  • Save t-34400/a7e0290f000513972f88662cfd4c62f1 to your computer and use it in GitHub Desktop.
Save t-34400/a7e0290f000513972f88662cfd4c62f1 to your computer and use it in GitHub Desktop.
Unity / Set of utility classes for Unity authoring tool application
#nullable enable
using System;
using UnityEngine;
using AuthoringTool.Input;
namespace AuthoringTool.EditorAPI
{
class GizmoController : MonoBehaviour
{
[SerializeField] private MouseInputDetector mouseInputDetector = default!;
[Header("Gizmo")]
[SerializeField] private Transform gizmoAnchor = default!;
[SerializeField] private Transform gizmoVisualObject = default!;
[SerializeField] private Collider xAxisCollider = default!;
[SerializeField] private Collider yAxisCollider = default!;
[SerializeField] private Collider zAxisCollider = default!;
[SerializeField] private float scaleFactor = 0.1f;
[SerializeField] private float anchorMinY = 0;
private Axis currentWorldDirection = Axis.X;
private bool isDragging = false;
private Vector3 dragPoint = Vector3.zero;
internal event Action<Vector3>? GizmoDragged;
internal void MoveGizmo(Vector3 delta)
{
gizmoAnchor.position += delta;
}
private void Awake()
{
var button = MouseInputDetector.Button.Left;
var events = mouseInputDetector.GetMouseEvents(button);
events.OnMouseDragging += OnDragging;
events.OnMouseUp += OnMouseButtonUp;
var selector = new MouseObjectSelector(mouseInputDetector, button);
selector.OnOverlayClicked += OnOverlayClicked;
}
private void Update()
{
gizmoVisualObject.localScale = Vector3.one * GetCurrentScale();
}
private void OnOverlayClicked(Collider detectedCollider)
{
if (detectedCollider == xAxisCollider)
{
currentWorldDirection = Axis.X;
dragPoint = gizmoAnchor.position;
isDragging = true;
}
else if (detectedCollider == yAxisCollider)
{
currentWorldDirection = Axis.Y;
dragPoint = gizmoAnchor.position;
isDragging = true;
}
else if (detectedCollider == zAxisCollider)
{
currentWorldDirection = Axis.Z;
dragPoint = gizmoAnchor.position;
isDragging = true;
}
else
{
isDragging = false;
}
}
private void OnDragging(Vector3 currentPosition, Vector3 previousPosition)
{
if (isDragging)
{
var ray = Camera.main.ScreenPointToRay(currentPosition);
var worldDelta = gizmoAnchor.position - ray.origin;
var pointerDirection = ray.direction;
var targetPosition = ray.origin + GetNearestPointAlongLine(currentWorldDirection, worldDelta, pointerDirection);
if (targetPosition.y < anchorMinY)
{
targetPosition = new Vector3(targetPosition.x, anchorMinY, targetPosition.z);
}
gizmoAnchor.position = targetPosition;
}
}
private void OnMouseButtonUp(Vector3 position)
{
if (isDragging)
{
isDragging = false;
var delta = gizmoAnchor.position - dragPoint;
GizmoDragged?.Invoke(delta);
}
}
private float GetCurrentScale()
{
var currentDistance = Vector3.Distance(Camera.main.transform.position, gizmoAnchor.position);
return currentDistance * scaleFactor;
}
private Vector3 GetNearestPointAlongLine(Axis currentWorldDirection, Vector3 worldDelta, Vector3 pointerDirection)
{
worldDelta = ReplaceIndex(currentWorldDirection, worldDelta);
pointerDirection = ReplaceIndex(currentWorldDirection, pointerDirection);
var perpDelta = new Vector2(worldDelta.y, worldDelta.z);
var perpDirection = new Vector2(pointerDirection.y, pointerDirection.z);
var perpDirectionMagnitude = perpDirection.magnitude;
var perpDirectionNormalized = perpDirection.normalized;
var dot = Vector2.Dot(perpDelta, perpDirectionNormalized);
var nearestPointRatio = dot / perpDirectionMagnitude;
var nearestPointDelta = perpDelta - dot * perpDirectionNormalized;
var anchorRelativePosition = pointerDirection * nearestPointRatio + new Vector3(-0.5f * GetCurrentScale(), nearestPointDelta.x, nearestPointDelta.y);
return InverseReplaceIndex(currentWorldDirection, anchorRelativePosition);
}
private static Vector3 ReplaceIndex(Axis currentWorldDirection, Vector3 sourceVector)
{
switch (currentWorldDirection)
{
case Axis.Y:
{
return new Vector3(sourceVector.y, sourceVector.z, sourceVector.x);
}
case Axis.Z:
{
return new Vector3(sourceVector.z, sourceVector.x, sourceVector.y);
}
default:
{
return sourceVector;
}
}
}
private static Vector3 InverseReplaceIndex(Axis currentWorldDirection, Vector3 sourceVector)
{
switch (currentWorldDirection)
{
case Axis.Y:
{
return new Vector3(sourceVector.z, sourceVector.x, sourceVector.y);
}
case Axis.Z:
{
return new Vector3(sourceVector.y, sourceVector.z, sourceVector.x);
}
default:
{
return sourceVector;
}
}
}
}
enum Axis { X, Y, Z }
}
#nullable enable
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
namespace AuthoringTool.EditorAPI
{
public class HistoryManager : MonoBehaviour
{
[SerializeField] private UnityEvent<bool> canUndoUpdated = default!;
[SerializeField] private UnityEvent<bool> canRedoUpdated = default!;
private readonly List<Record> historyRecord = new ();
private int currentIndex = -1;
private bool CanUndo => currentIndex > -1 && currentIndex < historyRecord.Count;
private bool CanRedo => currentIndex + 1 > -1 && currentIndex + 1 < historyRecord.Count;
public void RegisterRecord(Record record)
{
var canUndoPreviously = CanUndo;
var canRedoPreviously = CanRedo;
while (historyRecord.Count > currentIndex + 1)
{
historyRecord.RemoveAt(currentIndex + 1);
}
historyRecord.Add(record);
++currentIndex;
if (!canUndoPreviously)
{
canUndoUpdated.Invoke(true);
}
if (canRedoPreviously)
{
canRedoUpdated.Invoke(false);
}
}
public void Undo()
{
if (!CanUndo)
{
return;
}
var canRedoPreviously = CanRedo;
historyRecord[currentIndex].undo();
--currentIndex;
if (!CanUndo)
{
canUndoUpdated.Invoke(false);
}
if (!canRedoPreviously)
{
canRedoUpdated.Invoke(true);
}
}
public void Redo()
{
if (!CanRedo)
{
return;
}
var canUndoPreviously = CanUndo;
historyRecord[currentIndex + 1].redo();
++currentIndex;
if (!CanRedo)
{
canRedoUpdated.Invoke(false);
}
if (!canUndoPreviously)
{
canUndoUpdated.Invoke(true);
}
}
private void Update()
{
if (GetControlKey() && UnityEngine.Input.GetKeyDown(KeyCode.Z))
{
if (GetShiftKey())
{
Redo();
}
else
{
Undo();
}
}
}
private static bool GetControlKey() => UnityEngine.Input.GetKey(KeyCode.LeftControl) || UnityEngine.Input.GetKey(KeyCode.RightControl);
private static bool GetShiftKey() => UnityEngine.Input.GetKey(KeyCode.LeftShift) || UnityEngine.Input.GetKey(KeyCode.RightShift);
public struct Record
{
public Action undo;
public Action redo;
}
}
}
#nullable enable
using System;
using UnityEngine;
namespace AuthoringTool.Input
{
public class MouseInputDetector : MonoBehaviour
{
private readonly MouseButtonInputDetector leftButtonInputDetector = new MouseButtonInputDetector(Button.Left);
private readonly MouseButtonInputDetector rightButtonInputDetector = new MouseButtonInputDetector(Button.Right);
private readonly MouseButtonInputDetector middleButtonInputDetector = new MouseButtonInputDetector(Button.Middle);
public event Action<float>? OnMouseScroll;
public MouseEvents GetMouseEvents(Button button)
{
switch (button)
{
case Button.Right:
{
return rightButtonInputDetector;
}
case Button.Middle:
{
return middleButtonInputDetector;
}
default:
{
return leftButtonInputDetector;
}
}
}
private void Update()
{
leftButtonInputDetector.CheckMouseInput();
rightButtonInputDetector.CheckMouseInput();
middleButtonInputDetector.CheckMouseInput();
var scroll = UnityEngine.Input.mouseScrollDelta.y;
if (scroll != 0)
{
OnMouseScroll?.Invoke(scroll);
}
}
public enum Button
{
Left = 0,
Right = 1,
Middle = 2,
}
public interface MouseEvents
{
event Action<Vector3>? OnMouseDown;
event Action<Vector3, Vector3>? OnMouseDragging;
event Action<Vector3>? OnMouseUp;
}
class MouseButtonInputDetector : MouseEvents
{
private readonly int mouseButton;
private bool isDragging = false;
private Vector3 latestMousePosition;
internal MouseButtonInputDetector(Button mouseButton)
{
this.mouseButton = (int) mouseButton;
}
private Vector3 MousePosition => UnityEngine.Input.mousePosition;
public event Action<Vector3>? OnMouseDown;
public event Action<Vector3, Vector3>? OnMouseDragging;
public event Action<Vector3>? OnMouseUp;
internal void CheckMouseInput()
{
if (isDragging && UnityEngine.Input.GetMouseButton(mouseButton))
{
var currentMousePosition = MousePosition;
OnMouseDragging?.Invoke(currentMousePosition, latestMousePosition);
latestMousePosition = currentMousePosition;
}
if (UnityEngine.Input.GetMouseButtonDown(mouseButton))
{
isDragging = true;
latestMousePosition = MousePosition;
OnMouseDown?.Invoke(latestMousePosition);
}
if (UnityEngine.Input.GetMouseButtonUp(mouseButton))
{
isDragging = false;
OnMouseUp?.Invoke(MousePosition);
}
}
}
}
}
#nullable enable
using System;
using UnityEngine;
using UnityEngine.EventSystems;
namespace AuthoringTool.Input
{
public class MouseObjectSelector : MonoBehaviour
{
[SerializeField] private MouseInputDetector mouseInputDetector = default!;
[SerializeField] private MouseInputDetector.Button button = MouseInputDetector.Button.Left;
public event Action<Collider?>? OnMouseDown;
private void Awake()
{
mouseInputDetector.GetMouseEvents(button).OnMouseDown += DetectCollider;
}
private void DetectCollider(Vector3 mousePosition)
{
if (!IsMouseInsideViewport(mousePosition) || EventSystem.current.currentSelectedGameObject != null)
{
return;
}
var ray = Camera.main.ScreenPointToRay(mousePosition);
if (Physics.Raycast(ray, out var hit))
{
var collider = hit.collider;
if (collider != null)
{
OnMouseDown?.Invoke(collider);
Debug.Log("Clicked on object with collider: " + collider.gameObject.name);
}
}
else
{
OnMouseDown?.Invoke(null);
Debug.Log("Clicked outside of any collider.");
}
}
private bool IsMouseInsideViewport(Vector3 mousePosition)
{
var viewportPosition = Camera.main.ScreenToViewportPoint(mousePosition);
return viewportPosition.x >= 0 && viewportPosition.x <= 1 && viewportPosition.y >= 0 && viewportPosition.y <= 1;
}
}
}
#nullable enable
using UnityEngine;
namespace AuthoringTool.Input
{
public class UserCameraManager : MonoBehaviour
{
private const float MAX_DISTANCE = 8.0f;
private const float MIN_DISTANCE = 0.5f;
private const float MAX_PITCH = 75;
private const float MIN_PITCH = 15;
[SerializeField] private MouseInputDetector mouseInputDetector = default!;
[SerializeField] private Camera userCamera = default!;
[SerializeField] private Transform centerAnchor = default!;
[Range(MIN_DISTANCE, MAX_DISTANCE)]
[SerializeField] private float distance = 4.0f;
[SerializeField] private float yawRotationSpeed = 180.0f;
[SerializeField] private float pitchRotationSpeed = 30.0f;
[SerializeField] private float zoomSpeed = 0.2f;
[SerializeField] private float keyMoveSpeed = 1.0f;
[Header("Anchor Visual")]
[SerializeField] private float visualLineHalfLength = 0.05f;
[SerializeField] private float lineWidth = 0.01f;
[SerializeField] private LineRenderer xLineRenderer = default!;
[SerializeField] private LineRenderer yLineRenderer = default!;
[SerializeField] private LineRenderer zLineRenderer = default!;
[SerializeField] private LineRenderer groundXLineRenderer = default!;
[SerializeField] private LineRenderer groundZLineRenderer = default!;
private float yaw = 0;
private float pitch = 45;
public float AnchorDistance
{
get => distance;
private set => distance = value;
}
public void GetCenterPositionAndRotation(out Vector3 centerPosition, out Quaternion centerRotation) => centerAnchor.GetPositionAndRotation(out centerPosition, out centerRotation);
private void Awake()
{
mouseInputDetector.GetMouseEvents(MouseInputDetector.Button.Middle).OnMouseDragging += MoveAnchor;
mouseInputDetector.GetMouseEvents(MouseInputDetector.Button.Right).OnMouseDragging += RotateCamera;
mouseInputDetector.OnMouseScroll += ZoomCamera;
SetLineRenderersProperties();
RecalculateCameraPosition();
}
private void Update()
{
var inputMove = false;
if (UnityEngine.Input.GetKey(KeyCode.A) || UnityEngine.Input.GetKey(KeyCode.LeftArrow))
{
centerAnchor.position += Quaternion.Euler(0, yaw, 0) * Vector3.right * (Time.deltaTime * keyMoveSpeed);
inputMove = true;
}
if (UnityEngine.Input.GetKey(KeyCode.W) || UnityEngine.Input.GetKey(KeyCode.UpArrow))
{
centerAnchor.position += Quaternion.Euler(0, yaw, 0) * Vector3.back * (Time.deltaTime * keyMoveSpeed);
inputMove = true;
}
if (UnityEngine.Input.GetKey(KeyCode.S) || UnityEngine.Input.GetKey(KeyCode.DownArrow))
{
centerAnchor.position += Quaternion.Euler(0, yaw, 0) * Vector3.forward * (Time.deltaTime * keyMoveSpeed);
inputMove = true;
}
if (UnityEngine.Input.GetKey(KeyCode.D) || UnityEngine.Input.GetKey(KeyCode.RightArrow))
{
centerAnchor.position += Quaternion.Euler(0, yaw, 0) * Vector3.left * (Time.deltaTime * keyMoveSpeed);
inputMove = true;
}
if (UnityEngine.Input.GetKey(KeyCode.Q))
{
centerAnchor.position += Vector3.down * (Time.deltaTime * keyMoveSpeed);
inputMove = true;
}
if (UnityEngine.Input.GetKey(KeyCode.E))
{
centerAnchor.position += Vector3.up * (Time.deltaTime * keyMoveSpeed);
inputMove = true;
}
if (inputMove)
{
RecalculateCameraPosition();
}
}
private void MoveAnchor(Vector3 currentPosition, Vector3 previousPosition)
{
if (!IsMouseInsideViewport(currentPosition))
{
return;
}
currentPosition = new Vector3(currentPosition.x, currentPosition.y, AnchorDistance);
previousPosition = new Vector3(previousPosition.x, previousPosition.y, AnchorDistance);
var worldDelta = userCamera.ScreenToWorldPoint(currentPosition) - userCamera.ScreenToWorldPoint(previousPosition);
centerAnchor.position -= worldDelta;
RecalculateCameraPosition();
}
private void RotateCamera(Vector3 currentPosition, Vector3 previousPosition)
{
if (!IsMouseInsideViewport(currentPosition))
{
return;
}
var delta = currentPosition - previousPosition;
var normalizedDeltaX = delta.x / Screen.width;
var normalizedDeltaY = delta.y / Screen.height;
yaw += normalizedDeltaX * yawRotationSpeed;
pitch = Mathf.Clamp(pitch - normalizedDeltaY * pitchRotationSpeed, MIN_PITCH, MAX_PITCH);
RecalculateCameraPosition();
}
private void ZoomCamera(float scroll)
{
if (!IsMouseInsideViewport(UnityEngine.Input.mousePosition))
{
return;
}
AnchorDistance = Mathf.Clamp(AnchorDistance - scroll * zoomSpeed, MIN_DISTANCE, MAX_DISTANCE);
RecalculateCameraPosition();
}
private void RecalculateCameraPosition()
{
var centerPosition = centerAnchor.position;
if (centerPosition.y < 0)
{
centerPosition = new Vector3(centerPosition.x, 0, centerPosition.z);
centerAnchor.position = centerPosition;
}
var cameraPositionDelta = Quaternion.Euler(-pitch, yaw, 0) * Vector3.forward * AnchorDistance;
if (cameraPositionDelta.y < 0)
{
cameraPositionDelta = new Vector3(cameraPositionDelta.x, 0, cameraPositionDelta.z);
}
var cameraPosition = centerPosition + cameraPositionDelta;
var cameraRotation = Quaternion.LookRotation(-cameraPositionDelta);
userCamera.transform.SetPositionAndRotation(cameraPosition, cameraRotation);
var groundPoint = new Vector3(centerPosition.x, 0, centerPosition.z);
groundXLineRenderer.SetPositions(new Vector3[] { groundPoint + visualLineHalfLength * Vector3.left, groundPoint + visualLineHalfLength * Vector3.right });
groundZLineRenderer.SetPositions(new Vector3[] { groundPoint + visualLineHalfLength * Vector3.forward, groundPoint + visualLineHalfLength * Vector3.back });
}
private void SetLineRenderersProperties()
{
SetLineRendererProperties(xLineRenderer, false, lineWidth);
SetLineRendererProperties(yLineRenderer, false, lineWidth);
SetLineRendererProperties(zLineRenderer, false, lineWidth);
SetLineRendererProperties(groundXLineRenderer, true, lineWidth * 0.75f);
SetLineRendererProperties(groundZLineRenderer, true, lineWidth * 0.75f);
xLineRenderer.SetPositions(new Vector3[] { visualLineHalfLength * Vector3.right, -visualLineHalfLength * Vector3.right });
yLineRenderer.SetPositions(new Vector3[] { visualLineHalfLength * Vector3.up, -visualLineHalfLength * Vector3.up });
zLineRenderer.SetPositions(new Vector3[] { visualLineHalfLength * Vector3.forward, -visualLineHalfLength * Vector3.forward });
}
private void SetLineRendererProperties(LineRenderer lineRenderer, bool useWorldSpace, float width)
{
lineRenderer.positionCount = 2;
lineRenderer.useWorldSpace = useWorldSpace;
lineRenderer.startWidth = width;
lineRenderer.endWidth = width;
}
private static bool IsMouseInsideViewport(Vector3 mousePosition)
{
var viewportPosition = Camera.main.ScreenToViewportPoint(mousePosition);
return viewportPosition.x >= 0 && viewportPosition.x <= 1 && viewportPosition.y >= 0 && viewportPosition.y <= 1;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment