Users like exploring maps, seeing all that is around. However they tend to go too far. To prevent the map from getting lost to the user it bounces back into the set bounds, as defined by a rectangle.
To do this I keep track of two rectangles, the outer rectangle that the user moves around (the map), and an inner rectangle that serves as the viewport. When the outer rectangle gets swiped all the way to the right, the left side of the inner and outer rectangles overlap. The user can still drag it to see the overhang of the map, but once they release their touch the map moves back to inside the bounds.
My implementation for this: (unity 2019.1.8f1 with C# 4.x runtime IL2CPP):
MapMover.cs
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.EventSystems;
public class MapMover : MonoBehaviour, IDragHandler, IBeginDragHandler, IEndDragHandler
{
[Flags]
private enum OutOfBoundsDirection { Inbounds = 1, Left = 2, Right = 4, Top = 8, Bottom = 16 };
private OutOfBoundsDirection outOfBounds;
[SerializeField] private GameObject mainCanvas;
[SerializeField] private BoxCollider2D maskCol;
private readonly float snapBackSpeed = 100;
private Vector3 startTouchPos;
private BoxCollider2D col;
private WaitForEndOfFrame waitFrame;
private void Start()
{
waitFrame = new WaitForEndOfFrame();
col = GetComponent<BoxCollider2D>();
}
public void OnBeginDrag(PointerEventData eventData)
{
startTouchPos = Input.mousePosition;
}
/// <summary>
/// Move the map in a 1:1 scale to the movement of the finger. To do this We need to divide the difference between the start position of the touch and the current position
/// by the scale of the canvas (and any other parent, whose scale isn't exactly 1. Right now this isn't the case).
/// </summary>
/// <param name="eventData"></param>
public void OnDrag(PointerEventData eventData)
{
var posDiff = startTouchPos - Input.mousePosition;
transform.localPosition -= posDiff / mainCanvas.transform.localScale.x;
startTouchPos -= posDiff;
}
public void OnEndDrag(PointerEventData eventData)
{
CheckBounds();
}
/// <summary>
/// Check if the map is still within the set bounds of the inner rect.
/// </summary>
private void CheckBounds()
{
Rect innerRect = new Rect(maskCol.GetCornerVertices()[3].x, maskCol.GetCornerVertices()[3].y, maskCol.size.x, maskCol.size.y);
Rect outerRect = new Rect(col.GetCornerVertices()[3].x + transform.localPosition.x, col.GetCornerVertices()[3].y + transform.localPosition.y, col.size.x, col.size.y);
if (outerRect.xMin < innerRect.xMin && outerRect.xMax > innerRect.xMax &&
outerRect.yMin < innerRect.yMin && outerRect.yMax > innerRect.yMax)
{
outOfBounds = OutOfBoundsDirection.Inbounds;
}
else
{
if ((outOfBounds &= OutOfBoundsDirection.Inbounds) == OutOfBoundsDirection.Inbounds)
{
outOfBounds ^= OutOfBoundsDirection.Inbounds;
}
if (outerRect.xMin > innerRect.xMin)
{
outOfBounds |= OutOfBoundsDirection.Left;
}
if (outerRect.xMax < innerRect.xMax)
{
outOfBounds |= OutOfBoundsDirection.Right;
}
if (outerRect.yMin > innerRect.yMin)
{
outOfBounds |= OutOfBoundsDirection.Bottom;
}
if (outerRect.yMax < innerRect.yMax)
{
outOfBounds |= OutOfBoundsDirection.Top;
}
StartCoroutine(MoveInbounds());
}
}
/// <summary>
/// Translate the map to inside the bounds
/// </summary>
/// <returns></returns>
private IEnumerator MoveInbounds()
{
if ((outOfBounds & OutOfBoundsDirection.Left) != 0)
{
transform.Translate(Vector3.left * snapBackSpeed);
}
if ((outOfBounds & OutOfBoundsDirection.Right) != 0)
{
transform.Translate(Vector3.right * snapBackSpeed);
}
if ((outOfBounds & OutOfBoundsDirection.Top) != 0)
{
transform.Translate(Vector3.up * snapBackSpeed);
}
if ((outOfBounds & OutOfBoundsDirection.Bottom) != 0)
{
transform.Translate(Vector3.down * snapBackSpeed);
}
yield return waitFrame;
CheckBounds();
}
}
BoxColliderExtensions.cs
using UnityEngine;
public static class BoxColliderExtensions
{
/// <summary>
/// Returns the local position of the four corner vertices of the collider
/// </summary>
/// <param name="col">Array containing the corner points, starting at the top right and going clockwise</param>
/// <returns></returns>
public static Vector2[] GetCornerVertices(this BoxCollider2D col)
{
Vector2[] verts = new Vector2[4];
verts[0] = col.offset + new Vector2(-col.size.x, col.size.y) * 0.5f;
verts[1] = col.offset + new Vector2(col.size.x, col.size.y) * 0.5f;
verts[2] = col.offset + new Vector2(col.size.x, -col.size.y) * 0.5f;
verts[3] = col.offset + new Vector2(-col.size.x, -col.size.y) * 0.5f;
return verts;
}
}
The code works smoothly. I've never used bitmasks before but thought this would be a nice application of it, since the map can be moved out of bounds in multiple directions at the same time (e.g. up & right). But this takes a lot of if checks to do, which I feel can be halved, as i'm pretty much checking the same thing twice in CheckBounds
and MoveInBounds
.
I'm not experiencing any performance issues with it, so don't need to micro optimize it. Mainly looking for readability improvements/getting rid of those double ifs.
new Rect(float x, float y, float width, floath height
. the vertex at index[3]
is the bottom left vertex. Starting from this vertex means I can pass in its full width/height to get the total rectangle. If I used index [0] (top left) i'd have to pass in width/-height. As you say only that single index gets used here and could indeed make a function that returns just that specific vertex, hand't thought of that. Thanks! (If you add that as an answer I can upvote it) \$\endgroup\$