DEV Community

Cover image for Methods of object interaction in Unity. How to work with patterns and connections in your code
Devs Daddy
Devs Daddy

Posted on

Methods of object interaction in Unity. How to work with patterns and connections in your code

Introduction

Hey, everybody. When creating any game - in it, your entities always have to interact in some way, regardless of the goals - whether it's displaying a health bar to a player or buying an item from a merchant - it all requires some architecture to communicate between the entities. Today we're going to look at what methods you can use to achieve this and how to reduce the CPU load in your projects.

First, let's define some example. Let's say we have some store where the player will buy some item.

Direct access to references and methods

If we want to go head-on, we explicitly specify references on our mono-objects. The player will know about a particular merchant, and execute the merchant's buy method by passing the parameters of what he wants to buy, and the merchant will find out from the player if he has resources and return the result of the trade.

Direct Access Scheme Example

Let's represent this as abstract code:



class Player : MonoBehaviour {
    // Direct Links
    public Trader;

    // Player Data
    public long Money => money;
    private long money = 1000;
    private List<int> items = new List<int>();

    public bool HasItem(int itemIndex){
        return items.ContainsKey(itemId);
    }

    public void AddMoney(long addMoney){
        money += addMoney;
    }

    public void AddItem(int itemId){
        items.Add(itemId);
    }
}

class Trader : MonoBehaviour {
    private Dictionary<int, long> items = new Dictionary<int, long>();

    // Purchase Item Method
    public bool PurchaseItem(Player player, int itemId){
        // Find item in DB and Check Player Money
        if(!items.ContainsKey(itemId)) return false;
        if(player.Money < items[itemId]) return false;

        // Check Player Item
        if(player.HasItem(itemId) return false;
        player.AddMoney((-1)*items[itemId]);
        player.AddItem(itemId);
    }
}


Enter fullscreen mode Exit fullscreen mode

So, what are the problematic points here?

  • The player knows about the merchant and keeps a link to him. If we want to change the merchant, we will have to change the reference to him.
  • The player directly accesses the merchant's methods and vice versa. If we want to change their structure, we will have to change both.

Next, let's look at the different options for how you can improve your life with different connections.

Singleton and Interfaces

The first thing that may come to mind in order to detach a little is to create a certain handler class, in our case let it be Singleton. It will process our requests, and so that we don't depend on the implementation of a particular class, we can translate the merchant to interfaces.

Singleton Scheme

So, let's visualize this as abstract code:



// Abstract Player Interface
interface IPlayer {
    bool HasItem(int itemIndex);
    bool HasMoney(long money);
    void AddMoney(long addMoney);
    void AddItem(int itemId);
}

// Abstract Trader Interface
interface ITrader {
    bool HasItem(int itemId);
    bool PurchaseItem(int itemId);
    long GetItemPrice(int itemId);
}

class Player : MonoBehaviour {
    // Direct Links
    public Trader;

    // Player Data
    public long Money => money;
    private long money = 1000;
    private List<int> items = new List<int>();

    public bool HasItem(int itemIndex){
        return items.ContainsKey(itemId);
    }

    public bool HasMoney(long needMoney){
       return money > needMoney;
    }

    public void AddMoney(long addMoney){
        money += addMoney;
    }

    public void AddItem(int itemId){
        items.Add(itemId);
    }
}

class Trader : MonoBehaviour, ITrader {
    private Dictionary<int, long> items = new Dictionary<int, long>();

    public bool PurchaseItem(int itemId){
        if(!items.ContainsKey(itemId)) return false;
        items.Remove(items[itemId]);
        return true;
    }

    public bool HasItem(int itemId){
        return items.ContainsKey(itemId);
    }

    public long GetItemPrice(int itemId){
        return items[itemId];
    }
}

// Our Trading Management Singleton
class Singleton : MonoBehaviour{
   public static Singleton Instance { get; private set; }
   public ITrader trader;

   private void Awake() {
       if (Instance != null && Instance != this) { 
          Destroy(this); 
       } else { 
          Instance = this; 
       }
   }

   public bool PurchaseItem(IPlayer player, int itemId){
        long price = trader.GetItemPrice(itemId);
        if(!trader.HasItem(itemId)) return false;
        if(!player.HasMoney(price)) return false;

        // Check Player Item
        if(player.HasItem(itemId) return false;
        trader.PurchaseItem(itemId);
        player.AddMoney((-1)*price);
        player.AddItem(itemId);
   }
}


Enter fullscreen mode Exit fullscreen mode

What we did:

  • Created interfaces that help us decouple from a particular merchant or player implementation.
  • Created Singleton, which helps us not to address merchants directly, but to interact through a single layer that can manage more than just merchants.

Pub-Sub / Event Containers

This is all fine, but we still have bindings as bindings to specific methods and the actual class-layer itself. So, how can we avoid this? The PubSub pattern and/or any of your event containers can come to the rescue.

How does it work?
In this case, we make it so that neither the player nor the merchant is aware of the existence of one or the other in this world. For this purpose we use the event system and exchange only them.

PubSub Scheme

As an example, we will use an off-the-shelf library implementation of the PubSub pattern. We will completely remove the Singleton class, and instead we will exchange events.

For example, PubSub Library for Unity:
https://github.com/supermax/pubsub

Our code with PubSub Pattern:



// Our Purchase Request Payload
class PurchaseRequest {
   public int TransactionId;
   public long Money;
   public int ItemId;
}

// Our Purchase Response Payload
class PurchaseResult {
   public int TransactionId;
   public bool IsComplete = false;
   public bool HasMoney = false;
   public int ItemId;
   public long Price;
}


// Our Player
class Player : MonoBehaviour {
   private int currentTransactionId;
   private long money = 1000;
   private List<int> items = new List<int>();

   private void Start(){
       Messenger.Default.Subscribe<PurchaseResult>(OnPurchaseResult);
   }

   private void OnDestroy(){
       Messenger.Default.Unsubscribe<PurchaseResult>(OnPurchaseResult);
   }

   private void Purchase(int itemId){
       if(items.Contains(itemId)) return;
       currentTransactionId = Random.Range(0, 9999); // Change it with Real ID
       PurchaseRequest payload = new PurchaseRequest {
          TransactionId = currentTransactionId,
          Money = money,
          ItemId = itemId
       };
       Messenger.Default.Publish(payload);
   }

   private void OnPurchaseResult(PurchaseResult result){
       if(!result.IsComplete || !result.HasMoney) {
            // Show Error Here
            return;
       }

       // Add Item Here and Remove Money
       items.Add(result.ItemId);
       money -= result.Price;
   }
}

// Our Trader
class Trader : MonoBehaviour {
   private Dictionary<int, long> items = new Dictionary<int, long>();

   private void Start(){
       Messenger.Default.Subscribe<PurchaseRequest>(OnPurchaseResult);
   }

   private void OnDestroy(){
       Messenger.Default.Unsubscribe<PurchaseRequest>(OnPurchaseResult);
   }

   private void OnPurchaseRequest(PurchaseRequest request){
      OnPurchaseResult payload = new OnPurchaseResult { 
        TransactionId = request.TransactionId, 
        ItemId = request.ItemId,  
        IsComplete = items.Contains(request.ItemId), 
        HasMoney = request.Money < items[request.ItemId] 
      };
      payload.Price = items[request.ItemId];
      if(payload.IsComplete && payload.HasMoney)
        items.Remove(items[request.ItemId]);
      Messenger.Default.Publish(payload);
   }
}


Enter fullscreen mode Exit fullscreen mode

What we've accomplished here:

  • Decoupled from the implementation of the methods. Now a player or a merchant does not care what happens inside and in principle who will fulfill his instructions.
  • Decoupled from the relationships between objects. Now the player may not know about the existence of the merchant and vice versa

We can also replace subscriptions to specific Payload classes with interfaces and work specifically with them. This way we can accept different purchase events for different object types / buyers.

Data Layers

It's also good practice to separate our logic from the data we're storing. In this case, instead of handling merchant and player inventory and resource management, we would have separate resource management classes. In our case, we would simply subscribe to events not in the player and merchant classes, but in the resource management classes.

Data Layers Scheme

In conclusion

In this uncomplicated way, we have detached almost all the links in our code, leaving only the sending of events to our container. We can make the code even more flexible by transferring everything to interfaces, putting data into handlers (Data Layers) and displaying everything in the UI using reactive fields.

Next time I'll talk about reactivity and how to deal with query queuing issues.

Thanks and Good Luck!


My Discord | My Blog | My GitHub | Buy me a Beer

Top comments (0)