Skip to content

Instantly share code, notes, and snippets.

@FronkonGames
Last active November 16, 2022 21:49
Show Gist options
  • Save FronkonGames/e14d6f02fe90df7a2e212b11ee2f0a3c to your computer and use it in GitHub Desktop.
Save FronkonGames/e14d6f02fe90df7a2e212b11ee2f0a3c to your computer and use it in GitHub Desktop.
Dragging And Dropping 3D Cards
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2022 Martin Bustos @FronkonGames <fronkongames@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of
// the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
using UnityEngine;
using FronkonGames.TinyTween;
/// <summary>
/// Drag card.
/// </summary>
[RequireComponent(typeof(Collider))]
public sealed class CardDrag : MonoBehaviour, IDrag
{
public bool IsDraggable { get; private set; } = true;
public bool Dragging { get; set; }
[SerializeField]
private Ease riseEaseIn = Ease.Linear;
[SerializeField]
private Ease riseEaseOut = Ease.Linear;
[SerializeField, Range(0.0f, 5.0f)]
private float riseDuration = 0.2f;
[SerializeField]
private Ease dropEaseIn = Ease.Linear;
[SerializeField]
private Ease dropEaseOut = Ease.Linear;
[SerializeField, Range(0.0f, 5.0f)]
private float dropDuration = 0.2f;
[SerializeField]
private Ease invalidDropEase = Ease.Linear;
[SerializeField, Range(0.0f, 5.0f)]
private float invalidDropDuration = 0.2f;
private Vector3 dragOriginPosition;
public void OnPointerEnter(Vector3 position) { }
public void OnPointerExit(Vector3 position) { }
public void OnBeginDrag(Vector3 position)
{
dragOriginPosition = transform.position;
IsDraggable = false;
TweenFloat.Create()
.Origin(dragOriginPosition.y)
.Destination(position.y)
.Duration(riseDuration)
.EasingIn(riseEaseIn)
.EasingOut(riseEaseOut)
.OnUpdate(tween => transform.position = new Vector3(transform.position.x, tween.Value, transform.position.z))
.OnEnd(_ => IsDraggable = true)
.Owner(this)
.Start();
}
public void OnDrag(Vector3 deltaPosition, IDrop droppable)
{
deltaPosition.y = 0.0f;
transform.position += deltaPosition;
}
public void OnEndDrag(Vector3 position, IDrop droppable)
{
if (droppable is { IsDroppable: true } && droppable.AcceptDrop(this) == true)
TweenFloat.Create()
.Origin(transform.position.y)
.Destination(position.y)
.Duration(dropDuration)
.EasingIn(dropEaseIn)
.EasingOut(dropEaseOut)
.OnUpdate(tween => transform.position = new Vector3(transform.position.x, tween.Value, transform.position.z))
.Owner(this)
.Start();
else
{
IsDraggable = false;
transform.TweenMove(dragOriginPosition, invalidDropDuration, invalidDropEase).OnEnd(_ => IsDraggable = true);
}
}
private void OnEnable()
{
dragOriginPosition = transform.position;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2022 Martin Bustos @FronkonGames <fronkongames@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of
// the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary>
/// Card tilter.
/// </summary>
public sealed class CardTilter : MonoBehaviour
{
[Header("Pitch")]
[SerializeField, Label("Force")]
private float pitchForce = 10.0f;
[SerializeField, Label("Minimum Angle")]
private float pitchMinAngle = -25.0f;
[SerializeField, Label("Maximum Angle")]
private float pitchMaxAngle = 25.0f;
[Space]
[Header("Roll")]
[SerializeField, Label("Force")]
private float rollForce = 10.0f;
[SerializeField, Label("Minimum Angle")]
private float rollMinAngle = -25.0f;
[SerializeField, Label("Maximum Angle")]
private float rollMaxAngle = 25.0f;
[Space]
[SerializeField]
private float restTime = 1.0f;
// Pitch angle and velocity.
private float pitchAngle, pitchVelocity;
// Roll angle and velocity.
private float rollAngle, rollVelocity;
// To calculate the velocity vector.
private Vector3 oldPosition;
// The original rotation
private Vector3 originalAngles;
private void Awake()
{
oldPosition = transform.position;
originalAngles = transform.rotation.eulerAngles;
}
private void Update()
{
// Calculate offset.
Vector3 currentPosition = transform.position;
Vector3 offset = currentPosition - oldPosition;
// Limit the angle ranges.
if (offset.sqrMagnitude > Mathf.Epsilon)
{
pitchAngle = Mathf.Clamp(pitchAngle + offset.z * pitchForce, pitchMinAngle, pitchMaxAngle);
rollAngle = Mathf.Clamp(rollAngle + offset.x * rollForce, rollMinAngle, rollMaxAngle);
}
// The angles have 0 with time.
pitchAngle = Mathf.SmoothDamp(pitchAngle, 0.0f, ref pitchVelocity, restTime * Time.deltaTime * 10.0f);
rollAngle = Mathf.SmoothDamp(rollAngle, 0.0f, ref rollVelocity, restTime * Time.deltaTime * 10.0f);
// Update the card rotation.
transform.rotation = Quaternion.Euler(originalAngles.x + pitchAngle,
originalAngles.y,
originalAngles.z - rollAngle);
oldPosition = currentPosition;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2022 Martin Bustos @FronkonGames <fronkongames@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of
// the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
using System;
using UnityEngine;
/// <summary>
/// Drag and Drop manager.
/// </summary>
public sealed class DragAndDropManager : MonoBehaviour
{
// Layer of the objects to be detected.
[SerializeField]
private LayerMask raycastMask;
[SerializeField, Range(0.1f, 2.0f)]
private float dragSpeed = 1.0f;
// Height at which we want the card to be in a drag operation.
[SerializeField, Range(0.0f, 10.0f)]
private float height = 1.0f;
[SerializeField]
private Vector2 cardSize;
// Object to which we are doing a drag operation
// or null if no drag operation currently exists.
private IDrag currentDrag;
// IDrag objects that the mouse passes over.
private IDrag possibleDrag;
// To know the position of the drag object.
private Transform currentDragTransform;
// How many impacts of the beam we want to obtain.
private const int HitsCount = 5;
// Information on the impacts of shooting a ray.
private readonly RaycastHit[] raycastHits = new RaycastHit[HitsCount];
// Information on impacts from the corners of a card.
private readonly RaycastHit[] cardHits = new RaycastHit[4];
// Ray created from the camera to the projection of the mouse
// coordinates on the scene.
private Ray mouseRay;
// To calculate the mouse offset (in world-space).
private Vector3 oldMouseWorldPosition;
private Vector3 MousePositionToWorldPoint()
{
Vector3 mousePosition = Input.mousePosition;
if (Camera.main.orthographic == false)
mousePosition.z = 10.0f;
return Camera.main.ScreenToWorldPoint(mousePosition);
}
private void ResetCursor() => Cursor.SetCursor(null, Vector2.zero, CursorMode.Auto);
/// <summary>
/// Returns the Transfrom of the object closest to the origin
/// of the ray.
/// </summary>
/// <returns>Transform or null if there is no impact.</returns>
private Transform MouseRaycast()
{
Transform hit = null;
// Fire the ray!
if (Physics.RaycastNonAlloc(mouseRay,
raycastHits,
Camera.main.farClipPlane,
raycastMask) > 0)
{
// We order the impacts according to distance.
System.Array.Sort(raycastHits, (x, y) => x.distance.CompareTo(y.distance));
// We are only interested in the first one.
hit = raycastHits[0].transform;
}
return hit;
}
/// <summary>Detects an IDrop object under the mouse pointer.</summary>
/// <returns>IDrop or null.</returns>
private IDrop DetectDroppable()
{
IDrop droppable = null;
// The four corners of the card.
Vector3 cardPosition = currentDragTransform.position;
Vector2 halfCardSize = cardSize * 0.5f;
Vector3[] cardConner =
{
new(cardPosition.x + halfCardSize.x, cardPosition.y, cardPosition.z - halfCardSize.y),
new(cardPosition.x + halfCardSize.x, cardPosition.y, cardPosition.z + halfCardSize.y),
new(cardPosition.x - halfCardSize.x, cardPosition.y, cardPosition.z - halfCardSize.y),
new(cardPosition.x - halfCardSize.x, cardPosition.y, cardPosition.z + halfCardSize.y)
};
int cardHitIndex = 0;
Array.Clear(cardHits, 0, cardHits.Length);
// We launch the four rays.
for (int i = 0; i < cardConner.Length; ++i)
{
Ray ray = new(cardConner[i], Vector3.down);
int hits = Physics.RaycastNonAlloc(ray, raycastHits, Camera.main.farClipPlane, raycastMask);
if (hits > 0)
{
// We order the impacts by distance from the origin of the ray.
Array.Sort(raycastHits, (x, y) => x.transform != null ? x.distance.CompareTo(y.distance) : -1);
// We are only interested in the closest one.
cardHits[cardHitIndex++] = raycastHits[0];
}
}
if (cardHitIndex > 0)
{
// We are looking for the nearest possible IDrop.
Array.Sort(cardHits, (x, y) => x.transform != null ? x.distance.CompareTo(y.distance) : -1);
if (cardHits[0].transform != null)
droppable = cardHits[0].transform.GetComponent<IDrop>();
}
return droppable;
}
/// <summary>Detects an IDrag object under the mouse pointer.</summary>
/// <returns>IDrag or null.</returns>
public IDrag DetectDraggable()
{
IDrag draggable = null;
mouseRay = Camera.main.ScreenPointToRay(Input.mousePosition);
Transform hit = MouseRaycast();
if (hit != null)
{
draggable = hit.GetComponent<IDrag>();
if (draggable is { IsDraggable: true })
currentDragTransform = hit;
else
draggable = null;
}
return draggable;
}
private void Update()
{
if (currentDrag == null)
{
IDrag draggable = DetectDraggable();
// Left mouse button pressed?
if (Input.GetMouseButtonDown(0) == true)
{
// Is there an IDrag object under the mouse pointer?
if (draggable != null)
{
// We already have an object to start the drag operation!
currentDrag = draggable;
//currentDragTransform = hit;
oldMouseWorldPosition = MousePositionToWorldPoint();
// Hide the mouse icon.
Cursor.visible = false;
// And we lock the movements to the window frame,
// so we can't move objects out of the camera's view.
Cursor.lockState = CursorLockMode.Confined;
// The drag operation begins.
currentDrag.Dragging = true;
currentDrag.OnBeginDrag(new Vector3(raycastHits[0].point.x, raycastHits[0].point.y + height, raycastHits[0].point.z));
}
}
else
{
// Left mouse button not pressed?
// We pass over a new IDrag?
if (draggable != null && possibleDrag == null)
{
// We execute its OnPointerEnter.
possibleDrag = draggable;
possibleDrag.OnPointerEnter(raycastHits[0].point);
}
// We are leaving an IDrag?
if (draggable == null && possibleDrag != null)
{
// We execute its OnPointerExit.
possibleDrag.OnPointerExit(raycastHits[0].point);
possibleDrag = null;
ResetCursor();
}
}
}
else
{
IDrop droppable = DetectDroppable();
// Is the left mouse button held down?
if (Input.GetMouseButton(0) == true)
{
// Calculate the offset of the mouse with respect to its previous position.
Vector3 mouseWorldPosition = MousePositionToWorldPoint();
Vector3 offset = (mouseWorldPosition - oldMouseWorldPosition) * dragSpeed;
// OnDrag is executed.
currentDrag.OnDrag(offset, droppable);
oldMouseWorldPosition = mouseWorldPosition;
}
else if (Input.GetMouseButtonUp(0) == true)
{
// The left mouse button is released and
// the drag operation is finished.
currentDrag.Dragging = false;
currentDrag.OnEndDrag(raycastHits[0].point, droppable);
currentDrag = null;
currentDragTransform = null;
// We return the mouse icon to its normal state.
Cursor.visible = true;
Cursor.lockState = CursorLockMode.None;
}
}
}
private void OnEnable()
{
possibleDrag = null;
currentDragTransform = null;
ResetCursor();
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2022 Martin Bustos @FronkonGames <fronkongames@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of
// the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
using UnityEngine;
/// <summary>
/// Draggable object.
/// </summary>
public interface IDrag
{
/// <summary> Can it be draggable? </summary>
public bool IsDraggable { get; }
/// <summary> A Drag operation is currently underway. </summary>
public bool Dragging { get; set; }
/// <summary> Mouse enters the object. </summary>
/// <param name="position">Mouse position.</param>
public void OnPointerEnter(Vector3 position);
/// <summary> Mouse exits object. </summary>
/// <param name="position">Mouse position.</param>
public void OnPointerExit(Vector3 position);
/// <summary> Drag begins. </summary>
/// <param name="position">Mouse position.</param>
public void OnBeginDrag(Vector3 position);
/// <summary>A drag is being made. </summary>
/// <param name="deltaPosition"> Mouse offset position. </param>
/// <param name="droppable">Object on which a drop may be made, or null.</param>
public void OnDrag(Vector3 deltaPosition, IDrop droppable);
/// <summary> The drag operation is completed. </summary>
/// <param name="position">Mouse position.</param>
/// <param name="droppable">Object on which a drop may be made, or null.</param>
public void OnEndDrag(Vector3 position, IDrop droppable);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2022 Martin Bustos @FronkonGames <fronkongames@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of
// the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <summary>
/// Accept draggable objects.
/// </summary>
public interface IDrop
{
/// <summary> Is it droppable? </summary>
public bool IsDroppable { get; }
/// <summary> Accept an IDrag? </summary>
/// <param name="drag">Object IDrag.</param>
/// <returns>Accept or not the object.</returns>
public bool AcceptDrop(IDrag drag);
/// <summary> Performs the drop option of an IDrag object. </summary>
/// <param name="drag">Object IDrag.</param>
public void OnDrop(IDrag drag);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment