Interactions system in a grid-based game world in Unity

 by • Reading Time: 8 minutes

Introduction

Most of the times as game developers we have to introduce some kind of interactions system into our game mechanics. We would like our characters to pick up items from treasure chests, pull levers to open up gates and read that message on the wall. In other words, this fundamental concept is required for most games so that our player could interact with the world of our character. In this tutorial we are going to look at how we can develop such interactions system for a grid-based game.

Table of contents:

  1. The interaction system concept
  2. Adjusting character prefab
  3. The core of the interactions system – the ‘interactable’ object
  4. Interacting with the detected ‘interactable’ object

1. The interactions system concept

In this tutorial we’re going to expand the grid-based movement feature implemented in the previous post. The script will be enhanced with the capability of detecting what is in front of the character at all times. I’ll do this by once again taking the advantage of the 2D physics system built into Unity. After that, we will allow our character to ‘interact’ with the entity that is in front of him. That will of course depend on whether the detected entity is on the list of our level’s interactables.

The idea behind this is pretty straightforward. The ‘interactable’ object in the scene will pick up a collision with a marker that is assigned to the player. Once the marker is detected within its bounding volume it will be registered as object a character can interact with. Upon the detection we can inform any kind of HUD panel we may have about the change. This is to communicate to the player that the object standing in front of the character can be interacted with. The sequence diagram below shows events flow in a typical case scenario.

Sequence Diagram of interactions system

Once I have a reference to the interactable object, I can perform the interaction with it. That in turn may trigger various effects in other classes. However, I also want to update the HUD panels to communicate to the player that interaction took place. In order to do that automatically I’ll use the ‘function delegate‘ system built into Unity.

2. Adjusting character prefab

As already mentioned, I’m going to need a ‘marker’ which is going to detect any objects within characters reaching range. I’ll make it so that only object in front of the character will be detected. To do that let’s create and add an ‘Empty’ object to character prefab we’ve made last time. Add ‘Box Collider 2D‘ and ‘Rigid Body 2D‘ components to it. Set “Body Type” of the rigid body component to “Kinematic”. After that, I’ll open the “PlayerController” script as I need to make few changes there. In case you missed it, please refer to previous post for more details.

The interactions system script on the player’s side

I’m going to write the interactions system with the assumption that only the player’s marker is capable of registering the interactable object in the scene. I’ll add callback delegates so that any other game object in the scene may subscribe to them. I’m going to need them to communicate the changes that occur inside the class at specific moments. For instance, if new interactable object is detected and registered, I want to communicate that change immediately to the script responsible for updating the HUD panels. This is how you define them inside the script:

public Interactable lastInteractable;

public delegate void OnInteractableChangedCallback();
public OnInteractableChangedCallback onInteractableChangedCallback;

public delegate void OnInteractionCallback();
public OnInteractionCallback onInteractionCallback;

I’ll also need references of our marker at runtime. Specifically, I’ll need the last movement operation that player took and the position of the marker itself.

public Vector2 lastMovement;
public Transform lastDirectionPoint;

Since we are going to position our marker at each step in relation to the world coordinate space, I’ll unparent it immediately in the beginning.

void Start()
{
    movePoint.parent = null;
    lastDirectionPoint.parent = null;
}

After this we want to make sure that our marker is always placed in front of our character in the direction of his last move. To achieve this we need to move it one cell further than the ‘movePoint’. I’ll modify the existing code in the Update() function that we already have.

void Update()
{
    SetMovementVector();
    NotifyMovementAnimator();
     
    transform.position = Vector3.MoveTowards(transform.position, movePoint.position, moveSpeed * Time.fixedDeltaTime);
 
    lastDirectionPoint.position = Vector3.MoveTowards(lastDirectionPoint.position, movePoint.position + new Vector3(lastMovement.x, lastMovement.y, 0), 8 * Time.fixedDeltaTime);


    if (Vector3.Distance(transform.position, movePoint.position) <= .001f)
    {
        if (Mathf.Abs(movement.x) == 1f)
        {
            // we add 0.5f to 'y' component of the 'position'
            // to account the bottom pivot point of the sprite
            if (!Physics2D.OverlapCircle(movePoint.position + new Vector3(movement.x, 0.5f, 0f), .2f, stopMovementMask))
            {
                movePoint.position += new Vector3(movement.x, 0f, 0f);
            }

            lastMovement = movement;

        } 
        else if (Mathf.Abs(movement.y) == 1f)
        {
            // we add 0.5f to 'y' component of the 'position'
            // to account the bottom pivot point of the sprite
            if (!Physics2D.OverlapCircle(movePoint.position + new Vector3(0, movement.y + 0.5f, 0f), .2f, stopMovementMask))
            {
                movePoint.position += new Vector3(0f, movement.y, 0f); 
            }

            lastMovement = movement;

        }
    }
 
}

The highlighted lines above should be self-explanatory. I’m simply placing the marker in front of the character that is one cell further than the cell he last stepped in. To do this, I’m using the character’s last recorded movement step. In order to make the placement fairly fast I’m using the ‘Time.fixedDeltaTime’ multiplied by 8.

3. The core of the interactions system – the ‘interactable’ object

Now that we have set up our marker we need to make it detect the objects. These can be anything we imagine: treasure chests, levers, stairs and even the enemies! What these objects have in common is that they can be interacted with, but in different ways. To clarify, we want our character to pick up the items, walk up the stairs to another level of the tower and open up a gate by pulling the lever.

In addition, we want to ensure that we’re not duplicating tons of code for each of these case scenarios. To that end, we are going to write one super class we’ll inherit from. I’ll do this to create a wide array of interactions with different objects in the game. To ensure that our super class contains the necessary functions, I’ll further write an interface for it.

Class diagram for the Interactable Objects

The superclass ‘Interactable’ is where most of the code responsible for detecting and registering the collisions is going to be. I will override the virtual function ‘Interact()’ in child classes to introduce different kind of interactions. It will depend on the context and type of an object. However, I’m not going to simply leave this function completely empty. I’ll run few checks before I execute the code in the child counterparts.

public virtual void Interact()
{
    if (entity == null || hasInteracted) {
        return;
    } else if (entity != null) {
    // This method is supposed to be overridden by the child classes
        Debug.Log("Interacting with " + entity.name);
    }
}

I’m simply checking if the object has been detected and whether the player has already interacted with it. That is to say, I’m assuming that the character can interact with an object just once. However, this logic can be easily changed.

The currently active interactable object

All interactable objects in the scene have one thing in common. They are always being picked up by the player’s marker. When that happens, the object is registered and can be interacted with. In other words, I’m going to use the built-in collider’s trigger functions to detect the collisions. Furthermore, I’ll use these to set the given object as the one the player can interact with at the time.

void OnTriggerEnter2D(Collider2D other)
{
   if (other != null &amp;&amp; !hasInteracted) {
      entity = this.gameObject;
      playerController.SetLastInteractable(this);
   if (isTriggerInstant)
      playerController.onPressAction();
   }
}

void OnTriggerStay2D(Collider2D other)
{
   if (other != null &amp;&amp; !hasInteracted) {
      entity = this.gameObject;
      playerController.SetLastInteractable(this);
   } 
   else if(other != null &amp;&amp; hasInteracted )
   {
      entity = null;
      playerController.SetLastInteractable(null);
   }
}

void OnTriggerExit2D(Collider2D other)
{
   entity = null;
   playerController.SetLastInteractable(null);
}

On the highlighted lines in the code above I’m setting up the interactable object via functions in PlayerController. The reason why I’m not assigning these values directly to their corresponding fields is the fact I also want to notify other classes interested in this change automatically. In the PlayerController script I’ll define two functions. They will be responsible for registering, triggering the interaction and notifying other scripts about the state of the detected object.

public void SetLastInteractable(Interactable aLastInteractable)
{
    lastInteractable = aLastInteractable;
    // notify all listeners
    onInteractableChangedCallback.Invoke();     
}

public void onPressAction()
{
    if (lastInteractable != null)
    {
        lastInteractable.Interact();
        // notify all listeners that player has interacted with 'faced interactable object'
        onInteractionCallback.Invoke();     
    }
}

Updating the HUD with delegates callbacks

The highlighted lines in the last code listing send notifications about changes to all subscribed scripts. In my example, I’m updating the HUD panel to communicate this back to the player. In the ‘HUDManager’ Start() function I’m simply assigning functions that will fire every time the notification is being sent out. You can do this in a following way:

public class HUDManager : MonoBehaviour
{

    void Start()
    {
        playerController = PlayerController.instance;
        playerController.onInteractableChangedCallback += UpdateActionButtonIcon;
        playerController.onInteractionCallback += UpdateInteractionText;
    }

    // update the HUD when marker picks up an 'interactable' object
    void UpdateActionButtonIcon() 
    {
       ...
    }

    // update the HUD when player interacted with the object
    void UpdateInteractionText()
    {
        ...
    }

}

One would wonder why not simply execute functions directly in the scripts rather than do it with delegate callbacks. Of course that is possible. However, imagine that we would like to add more effects upon interaction at later stages in the development. We would have to not only update scripts that want to send notifications but also keep track of all scripts that need to do something about those notifications. With the subscription model we simply send out notification to interested scripts. In addition, we don’t have to care what they are going to do with it. That way we can also keep our code clean, concise and more maintainable.

4. Interacting with the detected ‘interactable’ object

Now we have everything to start placing different interactable objects inside our level! Let’s say I want to have treasure chests scattered through the scene with items inside them. Once the chest is open I would like to pick up the inside item and inform the player about it. I’ll write a script that inherits from our ‘Interactable’ superclass called ‘Pickable’.

public class Pickable : Interactable
{

    public Item item;
    Sprite hasInteractedIcon = null;

    public void Awake()
    {
        if (hasInteracted)
        {
            GetComponent<SpriteRenderer>().sprite = hasInteractedIcon;
        }
    }

    public override void Interact()
    {
        base.Interact();
        PickUp();        
    }

    void PickUp()
    {
        Inventory.instance.AddItem(item);
        GetComponent<SpriteRenderer>().sprite = hasInteractedIcon;
        hasInteracted = true;
    }
}

On line 4 I’m assigning a specific item data (Scriptable Object) to the pickable game object. After that, on line 15 I’m executing an ‘Interact()’ function that has been inherited and overridden from the superclass ‘Interactable’. Subsequently, I’m adding the item to the character’s inventory, change the sprite look and mark it as ‘already has been interacted with’ starting from line 23.

From this point onward you can expand on this concept however you like! You may add interactions with doors, levers, water and whatever your heart desires! The interactions system presented in this tutorial can be used in many different ways. Above all, it provides a solid foundation for interactions with the elements inside your game world. The final result can look like this:

Picking up items functionality

Conclusion

In this tutorial I’ve implemented an interactions system that is easy to maintain during the game development. I created few scripts responsible for registering and interacting with objects in our level. I showed how different interactable objects can trigger different effects when they are interacted with by the player. It depends on their type and context they can be used in. We’ve done this with the use of superclass and child classes. At the end I showed how this system can be used with the example of pickable object.