DragItem.cs "Takebacks"

Hi!

I’m reading through the DragItem.cs script, and I don’t understand the part about “takebacks” in the Attempt Swap method. Is this covered in future videos?

If not, could somebody explain it, please?

Thanks :smiley:

The part I least understand is this:

if (sourceTakeBackNumber > 0)
            {
                source.AddItems(removedSourceItem, sourceTakeBackNumber);
                removedSourceNumber -= sourceTakeBackNumber;
            }
if (destinationTakeBackNumber > 0)
            {
                destination.AddItems(removedDestinationItem, destinationTakeBackNumber);
                removedDestinationNumber -= destinationTakeBackNumber;
            }

we seem to add back to the source and destination positions a number of their original items. But further along we add the items to swap into those positions as well:

if (removedDestinationNumber > 0)
            {
                source.AddItems(removedDestinationItem, removedDestinationNumber);
            }
if (removedSourceNumber > 0)
            {
                destination.AddItems(removedSourceItem, removedSourceNumber);
            }

this is all in the AttemptSwap method in DragItem.cs

Yeah I’m working on a skill system where when unlocking a skill I want the player to freely drag the skill into the slot and have recently opened up the DragItem.cs and it’s other components and immediately felt lost.

It’s a barren code land with little to no comments. Quite unfriendly if I must add to the overall section of explanations for the drag system.

2 Likes

When we make a dragging swap, there are several possiblities of state in play…

  • The target container can not support the item being dragged
  • The target container contains something else, which the source container can accept
  • The target container contains something else, which the source container cannot accept
  • The target container can support the item, but not the full amount being dragged
  • The target container can support the item, but is empty.
    The AttemptSwap tries to accomodate each of these possibilities. You’re right, it’s a messy block of code, and I don’t have time to go through it line by line this morning I’ll try to paste a commented version later tonight.
2 Likes

OK thank you very much.

Regarding my doubt in the second post, which situation out of the ones you’ve mentioned do the parts of code I posted relate to?

Because AFAIK they aren’t exclusive, and the second block of code would just overwrite the takebacks made in the first block of code.

Thanks once again

This is my biggest (maybe only?) issue with GameDev.TV courses. The commenting is so sparce that leaving the course for a while (birthday season, holidays, work gets hectic…) makes returning a very daunting process. I try and comment as best I can now as we go along, so I don’t start over from the beginning… again… But sometimes, you just get caught up in the flow and forget to comment. Or it seems simple while you’re working on it, but 9 months later all you can do is stare at the code, wondering what that does, when it is used in another script altogether and there’s no comment saying so.

Really, I wish they would leave the pseudocode in the script, so we can see exactly what is going on, and leave it up to us to remove it as we feel more able to read what is happening.

1 Like

We try (but don’t always succeed) to make what is called “self-documenting” code. As a general rule, a method should say what it does (e.g. AddItems), and related variables should be clear as to what they are for. To me, good code makes most comments seem silly and redundant.

1 Like

Ok, I’ve gone over the major parts of DragItem.cs to give a bit of an overview of what’s going on at each stage in the game.

DragItem.cs Annotated
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
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;
        }
    }
}
2 Likes

Cool, thanks!

But doesn’t this second block of code in AttemptSwap overwrite this first one?

if (sourceTakeBackNumber > 0)
            {
                source.AddItems(removedSourceItem, sourceTakeBackNumber);
                removedSourceNumber -= sourceTakeBackNumber;
            }
if (destinationTakeBackNumber > 0)
            {
                destination.AddItems(removedDestinationItem, destinationTakeBackNumber);
                removedDestinationNumber -= destinationTakeBackNumber;
            }

is overwritten by

    // 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.

as we are adding back the items to their original place in the first part, and then writing them to their new destination in the second part.

No, the first block you reference pushes the excess items back into the container. The second block transfers the main balance of the items, what’s allowed from the source to the Destination and so on.

The first block is source → source and destination-> destination while the 2nd block is source → destination and destination → source.

Imagine you have 100 apples in a barrel, and you want to put as many apples as you can into a basket. The basket can hold 10 apples. It currently holds 10 pears, and as it happens, you have a truck full of empty barrels, and a rack with lots of empty baskets.

Now in real life, this would be simpler, because how to do this seems obvious, but computers aren’t the real life and we often need to take approaches to things we wouldn’t do in real life.

So we take all of the apples and put them on the table, and we take all of the pears and we put them on the table.

removedSourceItem = apples;
removedSourceNumber = 100;
removedDestinationItem = pears;
removedDestinationNumber = 10;

We check our basket, and the basket can hold 10 apples, meaning 90 apples need to go back to where they once belonged.

if (sourceTakeBackNumber > 0)
{
      source.AddItems(removedSourceItem, sourceTakeBackNumber);
      removedSourceNumber -= sourceTakeBackNumber;
}

so now we have 90 apples left on the table. As it happens, it’s illegal to put more than 5 pears in a barrel (union rules, donchano?). This means that we have to take back 5 of the pears and return them to the destination

if(destinationTakeBackNumber > 0)
{
     destination.AddItems(removeDestinationItem, destinationTakeBackNumber);
     removedDestinationNumber -= destinationTakeBackNumber;
}

Now we’ve put the 5 pears back, and have 5 remaining on the table.

Next we have to deal with what’s on the table. We take the excess pears and put them in the next barrel on the truck

if (removedDestinationNumber>0)
{
     source.AddItems(removedDestinationItem, removedDestinationNumber);
}

As it happens, if AddItems sees that there is something other than pears in the barrel, it will move on to the next empty barrel and put them there.
Now we have 10 apples left to deal with, so we put those in the destination

if(removedSourceNumber>0)
{
    destination.AddItems(removedSourceItem, removedSourceNumber);
}

Once again, AddItems will see that there are still pears in the destination basket, and dutifully find the next available basket to put the apples in.

2 Likes

OMG I’ve finally realised that AddItems calls inventory.AddItemToSlot down the line, and this method handles the situation if the referenced slot is currently full. This was the piece I was missing, thanks!!

2 Likes

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

Privacy & Terms