Last active
October 14, 2024 12:15
-
-
Save FronkonGames/e14d6f02fe90df7a2e212b11ee2f0a3c to your computer and use it in GitHub Desktop.
Dragging And Dropping 3D Cards
This file contains hidden or 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
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// | |
// Copyright (c) 2022 Martin Bustos @FronkonGames <[email protected]> | |
// | |
// 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; | |
} | |
} |
This file contains hidden or 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
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// | |
// Copyright (c) 2022 Martin Bustos @FronkonGames <[email protected]> | |
// | |
// 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; | |
} | |
} |
This file contains hidden or 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
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// | |
// Copyright (c) 2022 Martin Bustos @FronkonGames <[email protected]> | |
// | |
// 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(); | |
} | |
} |
This file contains hidden or 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
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// | |
// Copyright (c) 2022 Martin Bustos @FronkonGames <[email protected]> | |
// | |
// 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); | |
} |
This file contains hidden or 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
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// | |
// Copyright (c) 2022 Martin Bustos @FronkonGames <[email protected]> | |
// | |
// 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