EDIT: Much more comfortable after a dialogue with Brian

ORIGINAL:
Is there going to be a follow up showing how this system works under the hood?

The first course was amazing in this regard, and the second half of this inventory course is great as we dig in a bit.

Seems like quite a waste to not make it complete and provide an explanation of how the drag handling and similar “black box” mechanics here work.

There are a few sections that the explanations are weak on. The good news is that I can answer any specific questions you have on them.

1 Like

Hi Brian,

Appreciate it thank you. The main issue is that without a higher level generic overview of the structure of each under-the-hood class - it’s hard to know what my specific questions would be.

Granted - I can see that the aim of this course is to give us the skills to be more self-directed and make use of the debugger to understand code written by others, and that it won’t always be spoon-fed in industry.

Maybe if there was a dossier of each under-the-hood class in the pack and a brief explanation of its purpose, members, and a quick summary of any Unity or System callbacks / interfaces / other API things that we haven’t come across in Part 1, that could go a long way.

Kind regards,
Kieran

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;
        }
    }
}
1 Like

Thanks again, really appreciate the time.

This helps quite a lot - I think a big part of the problem was the initial shock of seeing a bunch of interfaces in the IDE hierarchy (some which I now realise are Unity interfaces) and the use of generic types. It was a bit of a “war flashback” to the time when I first “View Definition” decompiled a System class like List<> or IEnumerator<> (thinking I would understand what I saw, instead ending up with a face full of new syntax and tooltips about Linq, or “covariance” and “contravariance”).

In reality I can see that this isn’t quite as alien as classes in the System libraries - the interfaces are simple and the use of generics is fairly straightforward. And DragItem is the only actual substantive new class in the first commit. Also, if I understand correctly, some of these Unity interfaces work almost like events, where by implementing them you’re effectively subscribing to an event (in the form of Sent/Broadcasted messages) - I can wrap my head around that.

Looking back it almost would have only taken one or two extra lectures to make this course more aligned with the original teaching style. Maybe if Sam ever gets bored on a weekend oneday :laughing:

Hopefully my thread can at least calm down any future student in a similar position who is feeling overwhelmed :slight_smile:

1 Like

This topic was automatically closed 20 days after the last reply. New replies are no longer allowed.

Privacy & Terms