Last active
April 13, 2024 18:15
-
-
Save t-34400/a7e0290f000513972f88662cfd4c62f1 to your computer and use it in GitHub Desktop.
Unity / Set of utility classes for Unity authoring tool application
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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); | |
} | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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