This is part of why we abandoned this format (believe it or not, when we did the Core Combat course, the backers were saying “this is too much follow as I type, give us this format instead”, but it didn’t go over as well as was thought. The future courses go back to a more traditional format).
I’ll see what I can do, but I have to spread my resources (time) amongst all of the students, and the demand for the under the hood on things like the Drag handling is not as large as one might think, hence it’s easier to answer specific questions rather than outlining a whole section. If you look through the topics tagged Inventory, you’ll see some of these questions answered.
I have done a markup on the DragItem script you may find helpful:
DragItem.cs
using UnityEngine;
using UnityEngine.EventSystems;
namespace GameDevTV.Core.UI.Dragging
{
/// <summary>
/// Allows a UI element to be dragged and dropped from and to a container.
///
/// Create a subclass for the type you want to be draggable. Then place on
/// the UI element you want to make draggable.
///
/// During dragging, the item is reparented to the parent canvas.
///
/// After the item is dropped it will be automatically return to the
/// original UI parent. It is the job of components implementing `IDragContainer`,
/// `IDragDestination and `IDragSource` to update the interface after a drag
/// has occurred.
/// </summary>
/// <typeparam name="T">The type that represents the item being dragged.</typeparam>
public class DragItem<T> : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
where T : class
{
// PRIVATE STATE
Vector3 startPosition;
Transform originalParent;
IDragSource<T> source;
// CACHED REFERENCES
Canvas parentCanvas;
// LIFECYCLE METHODS
private void Awake()
{
parentCanvas = GetComponentInParent<Canvas>();
source = GetComponentInParent<IDragSource<T>>();
}
// PRIVATE
void IBeginDragHandler.OnBeginDrag(PointerEventData eventData)
{
startPosition = transform.position;
originalParent = transform.parent;
// Else won't get the drop event.
GetComponent<CanvasGroup>().blocksRaycasts = false;
transform.SetParent(parentCanvas.transform, true);
}
void IDragHandler.OnDrag(PointerEventData eventData)
{
transform.position = eventData.position;
}
void IEndDragHandler.OnEndDrag(PointerEventData eventData)
{
transform.position = startPosition; //This resets the Image location back to start as we only needed it to move to show the drag.
GetComponent<CanvasGroup>().blocksRaycasts = true;
transform.SetParent(originalParent, true); //We also need to ensure that the parent of the image is also reset.
IDragDestination<T> container;
if (!EventSystem.current.IsPointerOverGameObject())
{
//This is generally how the destination will be determined
container = parentCanvas.GetComponent<IDragDestination<T>>();
}
else
{
//This is more of a fallback, in the event that IsPointerOverGameObject() incorrectly reads false.
container = GetContainer(eventData);
}
//At this point, we either have an IDragDestination<T> or we don't. If it's null, then we're done, everything is
//snapped back, if it's not null then we can attempt to drop the item into the container.
if (container != null)
{
DropItemIntoContainer(container);
}
}
private IDragDestination<T> GetContainer(PointerEventData eventData)
{
if (eventData.pointerEnter)
{
var container = eventData.pointerEnter.GetComponentInParent<IDragDestination<T>>();
return container;
}
return null;
}
// This is the method that gives people the most trouble reading. There's a lot going on here.
private void DropItemIntoContainer(IDragDestination<T> destination)
{
//First, make sure that we didn't just drop the item on itself (awkward).
if (object.ReferenceEquals(destination, source)) return;
var destinationContainer = destination as IDragContainer<T>; //Just determines if we will get something back to swap
var sourceContainer = source as IDragContainer<T>; //Determines if we can accept something back.
// Swap won't be possible ** Because to Swap, we need both the destination and the source to be Containers, not just
// destinations or sources. There is also no need to swap if the destination has no item or the destination contains
// The same item.
if (destinationContainer == null || sourceContainer == null ||
destinationContainer.GetItem() == null ||
object.ReferenceEquals(destinationContainer.GetItem(), sourceContainer.GetItem()))
{
//If any of those conditions are true, we should be able to attempt a simple transfer. See notes in
//AttemptSimpleTransfer for the logic employed to make the swap.
AttemptSimpleTransfer(destination);
return;
}
// In the case that both source and destination are both IDragContainer<T> and both contain items that are different from each other,
// Then we need to attempt a swap
AttemptSwap(destinationContainer, sourceContainer);
}
private void AttemptSwap(IDragContainer<T> destination, IDragContainer<T> source)
{
// Provisionally remove item from both sides.
var removedSourceNumber = source.GetNumber();
var removedSourceItem = source.GetItem();
var removedDestinationNumber = destination.GetNumber();
var removedDestinationItem = destination.GetItem();
//We're going to take both items away and hold them in the variables above. If the swap is impossible, we'll simply put them back.
source.RemoveItems(removedSourceNumber);
destination.RemoveItems(removedDestinationNumber);
var sourceTakeBackNumber = CalculateTakeBack(removedSourceItem, removedSourceNumber, source, destination);
var destinationTakeBackNumber = CalculateTakeBack(removedDestinationItem, removedDestinationNumber, destination, source);
// Do take backs (if needed)
// If the takeback is zero, we need do nothing, but if it's a real positive number, then we need to put the excess
// parts back into the correct containers. We also need to update the amount of each item that we're still holding
// for swap purposes. We do this for both source and Destination
if (sourceTakeBackNumber > 0)
{
source.AddItems(removedSourceItem, sourceTakeBackNumber);
removedSourceNumber -= sourceTakeBackNumber;
}
if (destinationTakeBackNumber > 0)
{
destination.AddItems(removedDestinationItem, destinationTakeBackNumber);
removedDestinationNumber -= destinationTakeBackNumber;
}
// Abort if we can't do a successful swap. This is one last test to determine if we can truly swap the items.
// If this test fails, then we simply put the items back where they were before we started this and
// carefully step back.
if (source.MaxAcceptable(removedDestinationItem) < removedDestinationNumber ||
destination.MaxAcceptable(removedSourceItem) < removedSourceNumber)
{
destination.AddItems(removedDestinationItem, removedDestinationNumber); //These AddItems correspond to where we got them from.
source.AddItems(removedSourceItem, removedSourceNumber);
return;
}
// Do swaps. If we have a positive number in either buffer, we add the item to the container.
if (removedDestinationNumber > 0)
{
source.AddItems(removedDestinationItem, removedDestinationNumber); //Note that these AddItems are a true swap: Destination to Source
}
if (removedSourceNumber > 0)
{
destination.AddItems(removedSourceItem, removedSourceNumber); // And Source to Destination.
}
// That's it for complex swapping.
}
/// <summary>
/// Since there aren't any items in the destination to concern ourselves with swapping, or the items are the same,
/// this simple swap will generally be used instead.
/// </summary>
/// <param name="destination"></param>
/// <returns></returns>
private bool AttemptSimpleTransfer(IDragDestination<T> destination)
{
var draggingItem = source.GetItem();
var draggingNumber = source.GetNumber();
//Get the max number of items we can move into the container
var acceptable = destination.MaxAcceptable(draggingItem);
//And from this, we ensure that we can't take any more than acceptable.
var toTransfer = Mathf.Min(acceptable, draggingNumber);
if (toTransfer > 0)
{
//Deduct what we can legally move from the source
source.RemoveItems(toTransfer);
//And add these items to the Destination.
destination.AddItems(draggingItem, toTransfer);
return false;
}
return true;
}
/// <summary>
/// This is really a method to determine, in the case of a stack of items, the amount that should be returned
/// that MaxAcceptable is not an infinite number. So imagine that you have set it up that a Potion can only
/// be in stacks of 5 on the ActionBar, if you drag a stack of 10 into the action bar slot, the takeback
/// will be 5 (leaving 5 to drop in the spot). This will be called once on the Source and once on the
/// Destination.
/// </summary>
/// <param name="removedItem"></param>
/// <param name="removedNumber"></param>
/// <param name="removeSource"></param>
/// <param name="destination"></param>
/// <returns></returns>
private int CalculateTakeBack(T removedItem, int removedNumber, IDragContainer<T> removeSource, IDragContainer<T> destination)
{
var takeBackNumber = 0;
//Get the max acceptable of the item in question in the Destination container.
var destinationMaxAcceptable = destination.MaxAcceptable(removedItem);
//Bit of hand wavy stuff here:
if (destinationMaxAcceptable < removedNumber)
{
//If the MaxAcceptable is greater than the number dragged into the container, then our takeBackNumber will be the excess
takeBackNumber = removedNumber - destinationMaxAcceptable;
//Then we check to ensure that the source can take those excess back. Generally speaking, this should never happen, but
//it's a sanity check.
var sourceTakeBackAcceptable = removeSource.MaxAcceptable(removedItem);
// Abort and reset
if (sourceTakeBackAcceptable < takeBackNumber)
{
return 0;
}
}
//we simply return the takeback number, which will usually be zero unless you enforcing stack limits (and in the Equipment
//which will always limit to 1 in each container.
return takeBackNumber;
}
}
}