Created
August 27, 2018 04:07
-
-
Save LordZardeck/8d5fac9e56a61107593f1fef1a78acff to your computer and use it in GitHub Desktop.
Sample Card Hand Manager for Unity https://www.screencast.com/t/QVImKCWekA8l
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
using System; | |
using System.Collections.Generic; | |
using Sirenix.OdinInspector; | |
using UnityEngine; | |
[ExecuteInEditMode] | |
public class HandManager : MonoBehaviour | |
{ | |
/// <summary> | |
/// The area of a card that should be used to determine whether to select the previous, current, or next card | |
/// </summary> | |
[MinMaxSlider(0.0f, 1.0f)] | |
[InfoBox("The area of a card that should be used to determine whether to select the previous, current, or next card")] | |
public Vector2 DetectionRange = new Vector2(0.1f, 0.7f); | |
/// <summary> | |
/// How much of the screen's width should be consumed by this zone | |
/// </summary> | |
[Range(0.0f, 1.0f)] | |
[InfoBox("The area of a card that should be used to determine whether to select the previous, current, or next card")] | |
public float HandScreenWidth = 0.8f; | |
/// <summary> | |
/// The scale in which to enlarge a card when selected | |
/// </summary> | |
[InfoBox("The scale in which to enlarge a card when selected")] | |
public float EnlargedScale; | |
/// <summary> | |
/// The clamping to ensure cards don't get spaced more than this | |
/// </summary> | |
[InfoBox("The clamping to ensure cards don't get spaced more than this")] | |
public float SpacingClamp; | |
private const float ZSpacing = 0.001f; | |
private GameObject _selectedCard; | |
private float _standardCardWidth; | |
private float _standardCardHeight; | |
private float _spacing; | |
private float _startingX; | |
private GameObject _enlargedObject; | |
private readonly Dictionary<GameObject, Vector3> _originalScalingParentPosition = | |
new Dictionary<GameObject, Vector3>(); | |
/// <summary> | |
/// Add a card to this zone. Essential for scaling and positioning to work correctly. | |
/// | |
/// DO NOT ADD any game object to this zone without using this method | |
/// </summary> | |
/// <param name="card">Card to add to this zone</param> | |
public void AddCard(GameObject card) | |
{ | |
card.transform.parent = transform; | |
/** | |
* This will almost certainly cause problems when the screen resizes, but I'm not sure | |
* how to handle this live as when a card grows, it's size will change, cause a change in spacing, | |
* which will re-layout everything, potentially causing the user frustration when their mouse is no longer | |
* hovering over a card just because they hovered over a card. | |
*/ | |
_standardCardWidth = card.GetComponentInChildren<Renderer>().bounds.size.x; | |
_standardCardHeight = card.GetComponentInChildren<Renderer>().bounds.size.y; | |
// Wrap the card in a positioning parent and scaling parent for easier management | |
WrapCard(card); | |
} | |
/// <summary> | |
/// Wrap a card in a positioning and scaling parent for convenience | |
/// </summary> | |
/// <param name="card">The card to wrap</param> | |
private void WrapCard(GameObject card) | |
{ | |
int index = card.transform.GetSiblingIndex(); | |
Renderer cardRenderer = card.GetComponent<Renderer>(); | |
Vector3 positioningParentPosition = card.transform.position; | |
// Set the scaling parent's position to the bottom left of the card | |
Vector3 scalingParentPosition = cardRenderer.bounds.min; | |
// Center the scaling parent's anchor | |
scalingParentPosition.x += cardRenderer.bounds.size.x / 2; | |
GameObject positioningParent = new GameObject("Positioning Parent"); | |
positioningParent.transform.parent = transform; | |
positioningParent.transform.position = positioningParentPosition; | |
positioningParent.transform.localScale = Vector3.one; | |
positioningParent.transform.SetSiblingIndex(index); | |
GameObject scalingParent = new GameObject("Scaling Parent"); | |
scalingParent.transform.parent = positioningParent.transform; | |
scalingParent.transform.position = scalingParentPosition; | |
// Cache the original position so when we restore it's scale we can restore it's position as well | |
_originalScalingParentPosition[scalingParent] = scalingParent.transform.localPosition; | |
card.transform.parent = scalingParent.transform; | |
// Set the card's local position inverse to the scaling parent's position in order to re-center it | |
card.transform.localPosition = new Vector3( | |
-scalingParent.transform.localPosition.x, | |
-scalingParent.transform.localPosition.y, | |
0 | |
); | |
} | |
/// <summary> | |
/// Enlarges a card by scaling it's parent. This allows us to scale uniformly from the bottom center anchor | |
/// </summary> | |
/// <param name="card">The game object that contains the model of the card to scale</param> | |
public void EnlargeCard(GameObject card) | |
{ | |
// Don't double scale the same card | |
if (_enlargedObject == card) | |
{ | |
return; | |
} | |
// Store a copy of the parent's position that will be used to scale the card | |
Vector3 scalingParentPosition = card.transform.parent.position; | |
// Grab the screen point from the scaling parent in order to have the correct Z and X axis | |
Vector3 screenPoint = Camera.main.WorldToScreenPoint(scalingParentPosition); | |
// Reset the position of the scaling parent to the bottom of the screen. This works due to the scaling | |
// parent's anchor being at the bottom center of the card's GameObject | |
screenPoint.y = 0; | |
// Grab the world point where the scaling parent's anchor should be set relative to the screen | |
Vector3 worldPoint = Camera.main.ScreenToWorldPoint(screenPoint); | |
// Move the scaling parent to the bottom of the screen | |
scalingParentPosition.y = worldPoint.y; | |
card.transform.parent.position = scalingParentPosition; | |
// Uniformly scale the card from the parent in the X and Y axis | |
card.transform.parent.transform.localScale = new Vector3(EnlargedScale, EnlargedScale, 1.0f); | |
_enlargedObject = card; | |
} | |
/// <summary> | |
/// Restores a card's scaling parent to the original scale and position before being enalrged | |
/// </summary> | |
/// <param name="card"></param> | |
public void RestoreCardScale(GameObject card) | |
{ | |
card.transform.parent.localScale = Vector3.one; | |
card.transform.parent.localPosition = _originalScalingParentPosition[card.transform.parent.gameObject]; | |
if (_enlargedObject == card) | |
{ | |
_enlargedObject = null; | |
} | |
} | |
/// <summary> | |
/// Determine the width of the zone, taking into consideration the percentage width of the screen the zone | |
/// is expected to consume | |
/// </summary> | |
/// <returns> | |
/// A Vector2 of coordinates of the zone, with X being the left-most position, and Y being the right-most position | |
/// </returns> | |
private Vector2 GetZoneDimensions() | |
{ | |
Vector3 screenPoint = Camera.main.WorldToViewportPoint(transform.position); | |
float min = Camera.main.ViewportToWorldPoint(new Vector3(1.0f - HandScreenWidth, 0.0f, screenPoint.z)).x; | |
float max = Camera.main.ViewportToWorldPoint(new Vector3(HandScreenWidth, 0.0f, screenPoint.z)).x; | |
return new Vector2(min, max); | |
} | |
/// <summary> | |
/// Recalculate spacing of cards in order to fit withing the container. Expects cards to usually be a certain size. | |
/// If cards are not this normal size, there is no guarantee the cards will fit within the container. | |
/// </summary> | |
private void UpdateSpacing() | |
{ | |
if (transform.childCount == 0) | |
{ | |
_spacing = 0; | |
return; | |
} | |
Vector2 dimensions = GetZoneDimensions(); | |
_spacing = Mathf.Min( | |
(dimensions.y - dimensions.x - _standardCardWidth) / (transform.childCount - 1), | |
SpacingClamp | |
) - _standardCardWidth; | |
} | |
/// <summary> | |
/// Determines if a card should be selected to enlarge, or if one of it's siblings should be selected. | |
/// Respects a detection range of left-most and right-most points to consider valid | |
/// for previous, current, and next targets | |
/// </summary> | |
/// <param name="card">Card to base detection on</param> | |
/// <param name="point">What point on the card is being selected</param> | |
/// <returns> | |
/// Relative sibling position to select. 0 is the current card, | |
/// while -1 and 1 are index previous and next respectively | |
/// </returns> | |
private int ShouldSelectCard(GameObject card, Vector3 point) | |
{ | |
Bounds bounds = card.GetComponentInChildren<Renderer>().bounds; | |
float min = bounds.min.x + (bounds.size.x * DetectionRange.x); | |
float max = bounds.min.x + (bounds.size.x * DetectionRange.y); | |
if (DetectionRange.x > 0 && point.x < min) | |
{ | |
return -1; | |
} | |
if (DetectionRange.y < 1 && point.x > max) | |
{ | |
return 1; | |
} | |
return 0; | |
} | |
/// <summary> | |
/// Checks if a certain point is on the bottom half of the card | |
/// </summary> | |
/// <param name="card">Card to check point on</param> | |
/// <param name="point">Point to check against</param> | |
/// <returns>If the point is on the bottom half of the card</returns> | |
private static bool IsSelectingBottomHalf(GameObject card, Vector3 point) | |
{ | |
Bounds bounds = card.GetComponentInChildren<Renderer>().bounds; | |
return point.y <= bounds.min.y + (bounds.size.y * 0.5f); | |
} | |
/// <summary> | |
/// Checks the current position of the mouse and determines what card should be enlarged, if any | |
/// </summary> | |
/// <returns>The card to enlarge, if any</returns> | |
private GameObject GetCardToEnlarge() | |
{ | |
RaycastHit hit; | |
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); | |
// Check if the user is hovering over anything | |
if (!Physics.Raycast(ray, out hit, 100.0f)) | |
{ | |
return null; | |
} | |
GameObject potentialCard = hit.transform.gameObject; | |
// Ensure the card hit from the raycast is in our children | |
if (!potentialCard.transform.IsChildOf(transform)) | |
{ | |
return null; | |
} | |
// Ensure we are only selecting the top half | |
if (_selectedCard == potentialCard && !IsSelectingBottomHalf(potentialCard, hit.point)) | |
{ | |
return null; | |
} | |
// Get the sibling index of the positioning parent | |
int index = potentialCard.transform.parent.parent.GetSiblingIndex(); | |
// Check if we are hovering outside our detection range to select a card hidden behind the selected card | |
index += ShouldSelectCard(potentialCard, hit.point); | |
if (index >= 0 && index < transform.childCount) | |
{ | |
// Select the actual card object from the selected positioning parent | |
potentialCard = transform.GetChild(index).transform.GetChild(0).GetChild(0).gameObject; | |
} | |
return potentialCard; | |
} | |
/// <summary> | |
/// Reset the scaling parent of all cards except the selected card | |
/// </summary> | |
private void ResetCardScales() | |
{ | |
foreach (Transform child in transform) | |
{ | |
GameObject card = child.GetChild(0).GetChild(0).gameObject; | |
if (_selectedCard != card) | |
{ | |
RestoreCardScale(card); | |
} | |
} | |
} | |
/// <summary> | |
/// Calculate the total width of all child cards as well as cache their bounding rect | |
/// </summary> | |
/// <returns>The total width of all child cards and their cached render bounds</returns> | |
private Tuple<float, Dictionary<int, Bounds>> GetChildSizes() | |
{ | |
Dictionary<int, Bounds> childBounds = new Dictionary<int, Bounds>(); | |
float totalWidth = 0.0f; | |
foreach (Transform child in transform) | |
{ | |
Bounds bounds = child.gameObject.GetComponentInChildren<Renderer>().bounds; | |
childBounds[child.GetSiblingIndex()] = bounds; | |
totalWidth += bounds.size.x; | |
} | |
return new Tuple<float, Dictionary<int, Bounds>>(totalWidth, childBounds); | |
} | |
/// <summary> | |
/// Reposition all child cards evenly within the bounds of the zone's dimensions | |
/// </summary> | |
private void RepositionCards() | |
{ | |
Tuple<float, Dictionary<int, Bounds>> childSizes = GetChildSizes(); | |
float totalWidth = childSizes.Item1; | |
Dictionary<int, Bounds> childBounds = childSizes.Item2; | |
float currentZ = 0.0f; | |
Vector2 bounds = GetZoneDimensions(); | |
// Determine the starting position of the row of cards by finding the center of the zone, subtracting the | |
// center of the row of cards, and accounting for the spacing of all children except the last one | |
float currentX = ((bounds.y + bounds.x) / 2) - (totalWidth / 2.0f) - | |
((_spacing * (transform.childCount - 1)) / 2.0f); | |
foreach (Transform child in transform) | |
{ | |
Vector3 position = child.transform.position; | |
// We will move along the horizontal axis half the card width, to account for positioning being relative | |
// to the center of the card, not the left-most side | |
float xIncrease = (childBounds[child.GetSiblingIndex()].size.x) / 2.0f; | |
currentX += xIncrease; | |
position.x = currentX; | |
position.z = currentZ; | |
// If we have selected a card and this child is that selected card, bring it to the front | |
if (_selectedCard != null && | |
child.GetSiblingIndex() == _selectedCard.transform.parent.parent.GetSiblingIndex()) | |
{ | |
position.z = -(ZSpacing * (transform.childCount + 1)) - ZSpacing; | |
} | |
child.transform.position = position; | |
// Set the next object's starting position relative to the right-most edge of the card plus the spacing | |
currentX += xIncrease + _spacing; | |
// Set the next card to be behind this card in the z-axis | |
currentZ -= ZSpacing; | |
} | |
} | |
#region Unity Events | |
private void FixedUpdate() | |
{ | |
_selectedCard = GetCardToEnlarge(); | |
ResetCardScales(); | |
if (_selectedCard != null) | |
{ | |
EnlargeCard(_selectedCard); | |
} | |
} | |
private void LateUpdate() | |
{ | |
UpdateSpacing(); | |
RepositionCards(); | |
} | |
/// <summary> | |
/// Draw a Gizmo representing the Zone's expected max size | |
/// </summary> | |
private void OnDrawGizmosSelected() | |
{ | |
Gizmos.color = new Color(1, 0, 0, 0.5f); | |
Vector2 dimensions = GetZoneDimensions(); | |
Vector3 position = transform.position; | |
Gizmos.DrawCube( | |
transform.position, | |
new Vector3(dimensions.y - dimensions.x, Mathf.Max(1, _standardCardHeight), 1) | |
); | |
} | |
#endregion | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Please take note that for this script to compile, you'll need the Odin Inspector asset: http://sirenix.net/odininspector
To compile this without it, remove the attributes from the public properties