Loot Magnet

Loot Magnet

In Lamina Island we have something we call a Loot Magnet. This pulls all the loot towards the player when the player gets in range. It’s a common enough feature in games where there is loot on the ground, and you don’t want the player to waste their time

Mine Square KHBBS.gif
Kingdom Hearts loot magnet

Having loot in 3D space is great for a non-serious game as it gives the player a very ‘feel good’ reaction to collecting things. Kingdom hearts is one of the best at doing this. All you are usually collecting is munny, health, and occasionally an item. But the surplus of it. The chaos of all that noise, makes it feel like you just got a ton of things at once. There are like twenty 3d loot objects on the ground. The player had full health, so all they got from it was 30 munny, which does not amount to much in the game, but none of that matters, because it felt good.

In this animation you can also see their loot magnet, which pulls the objects to the player. Some games will even let you increase the distance this magnetic effect extends to.

Before we can begin working on the loot magnet itself, we need to make sure our loot is set up correctly. We cover how and why we decided to run with a 2d sprite in 3d space for Lamina Island here. What we care about however here is not how the loot looks, but how it acts physically.

The first collider, the ‘box collider’ is made to be smaller, and deals with collisions against the ground and other objects. This along with the rigid body allows it to take place in the 3d world as any standard object. The sphere collider however, is added specifically for the purposes of the loot magnet. Making it ‘trigger’ and changing the loot code to have the player collect it on trigger rather than collision is important because otherwise when we project the loot at the player at high speeds, the collision is going to smack him around. While humorous to watch the player get hit repeatedly, it is not the intended behavior.

Now that the loot itself is set up, we can work on the loot magnet which would be attached to the player.

public class LootMagnet : MonoBehaviour
{

 [SerializeField]
 private float searchDelay = .1f;

  private void Awake()
    {
       StartCoroutine(SearchPulse());
    }

    private IEnumerator SearchPulse()
    {
        while (true)
        {
            RunSearchLogic();
            yield return new WaitForSeconds(searchDelay);
        }
    }

  private void RunSearchLogic()
  {
  }

}

So we start by creating the search pulse coroutine & and empty run logic function. You could just as easily do this by just calling RunLogic from and update function.

So the first thing our loot magnet needs to be able to do is figure out which objects are affected by our magnetic field.

[SerializeField]
private float magnetDistance = 3f;

[SerializeField]
private LayerMask lootLayer;

private Vector3 MagnetPoint => transform.position;


private void RunSearchLogic()
{ 
   Collider[] hitColliders = Physics.OverlapSphere(MagnetPoint, magnetDistance,
                                      lootLayer, QueryTriggerInteraction.Collide);
}

So we accomplish this by calling a physics function called ‘OverlapSphere’ there returns any colliders that exist in a given area. Note that we use QueryTriggerInteraction.Collide to tell it that want colliders set to trigger as well as normal colliders. This allows us to grab the loot without needing to be right on top of the loot itself, giving it a bigger area to grab at.

We create a couple variables to set in the inspector like magnet distance & loot layer. And of course magnet distance can always be injected into the class at a later point, maybe based off the character’s stats. This would allow you to upgrade the size of the magnet over time as needed.

We also add MagnetPoint as a read only property, so that we can shorthand transform.position, as we are going to need it a few times.

So the last thing we want to do in this function before moving on, is add the colliders rigidbody to a list that we can reference later

private HashSet<Rigidbody> items = new HashSet<Rigidbody>();

private void RunSearchLogic()
{ 
  Collider[] hitColliders = Physics.OverlapSphere(MagnetPoint, magnetDistance,
                                      lootLayer, QueryTriggerInteraction.Collide);
  foreach (Collider hitCollider in hitColliders)
  {
    Rigidbody potentialAdd = hitCollider.GetComponent<Rigidbody>();
    if (potentialAdd != null)
    {
     items.Add(potentialAdd);
    }
  }
}

We chose a hashset instead of a list so that multiple search pulses don’t just double add the same loot’s rigid body multiple times.

So all that’s left is to act upon the items that we have a list for

[SerializeField]
private float forceFactor = 500;

private void FixedUpdate()
{
  foreach (Rigidbody item in items)
  {
     Vector3 direction = MagnetPoint - item.position;

     Vector3 velocity = direction * forceFactor;
     item.velocity = direction * forceFactor;
  }
}

And again, we give ourselves an inspector variable to edit the speed that the object comes at us.

So the very last thing we need to do, is handle removing the rigidbody from our list when we are done with them. Either because they hit the player and the player collected them, or something else destroyed them. The end result is the same, the collider will return as null in unity. So we just have to check for that.

  private void FixedUpdate()
  {
    List<Rigidbody> deletedItems = new List<Rigidbody>();


    foreach (Rigidbody item in items)
    {
      //items that were destroyed or disabled 
      if (item == null || !item.gameObject.activeSelf)
      {
        deletedItems.Add(item);
        { continue; }
      }

       ...
    }

    foreach (Rigidbody item in deletedItems)
    {
      items.Remove(item);
    }
  }

Notice that rather than trying to remove the items during the foreach loop, we just add them to a seperate collection and delete them in their own loop. This is because you can’t modify a collection while you are looping through it. This is a safe way to remove the unwanted items.

Now this is a very basic. ‘go straight’ loot magnet. And there are several ways we can extend it.

One way would be to add debugging to allow us to see how big our magnet is.

[SerializeField]
private bool debugMagnetSize;

private void OnDrawGizmos()
  {
    if (debugMagnetSize)
    {
      Gizmos.DrawWireSphere(MagnetPoint, magnetDistance);
    }
  }

Another thing we can do is make it so that the loot doesn’t immediately get thrown to the user, but has a small delay.

[SerializeField]
private float addDelayInSeconds = .25f;

private void RunSearchLogic()
  {
       ...
    foreach (Collider hitCollider in hitColliders)
    {
      Rigidbody potentialAdd = hitCollider.GetComponent<Rigidbody>();
      if (potentialAdd != null)
      {
        this.WaitXSecondsAndThen(addDelayInSeconds, () =>  
                          AddDelayed(potentialAdd));
      }
    }
  }

private void AddDelayed(Rigidbody potentialAdd)
  {
    if (potentialAdd != null)
    {
      items.Add(potentialAdd);
    }
  }

We’ll cover the ‘waitXSecondsAndThen’ extension method in another post.

Another thing we can add at a later point to make the loot magnet feel good, is a wiggle animation to the loot while it’s in the delay period to let the player know it’s about to be picked up. Maybe even a distance check after the delay so if the player walks close to it and then walks away before it triggers, then it’ll just play the wiggle animation and then stop.