Search code examples
c#unity-game-engineeventsgame-developmentobserver-pattern

Event subscription to specific class instances?


In the observer pattern, specifically with Unity game dev, is it not possible to subscribe to events from specific instances of the same class? A simple example I came up with to explain: I have a class Dog which has a method "Bark" which does some stuff and also raises the "OnBark" event action. Dog has been instantiated twice, one golden retriever and one husky. The golden retriever is the leader of the pack and wants to subscribe specifically to the husky's bark event. How can I make this happen? Do they need to be different classes altogether?

Specifically, I am working on a sports video game (Lacrosse) that involves two teams of opposing players, and so of course I will need to implement a lot of "AI" CPU logic to play along with / against the person playing the game. The teams of players are instantiated from the same class and observe some of each other's actions depending on the context of the game, their position, etc. I can't figure out how to make an individual player instance subscribe to another's events.

Code:

First, the SimpleController class of which each player on the field is an instance. It has two arrays referencing the player's teammates and the players on the other team.

public class SimpleController : MonoBehaviour
{
    public SimpleController[] teammates;
    public SimpleController[] otherTeam;

    // the brain of the player, which gets necessary info from this parent controller
    public Brain PlayerBrain;
}

Next, the Brain class, within which I was hoping to include some simple event actions. This class is for handling logic of the players being controlled by CPU. I imagined that they can be raised by a player based on his choices and subscribed to by certain other player "brains" to react if they need to (i.e. the goalie won't subscribe to a "pass event" from his own teammate John, but an opposing player defending John certainly would want to react to a pass made by/to John).

public class Brain : IBrain
{
    // Reference to parent SimpleController for teammates and opposing team members etc.
    private SimpleController _player;

    // event raised when player's brain decides to pass the ball
    public static event Action OnPassEvent;

    // method to call when a different SimpleController's brain passes the ball
    private void reactToPass() { //.......// }
     
    public Brain(SimpleController controller) {
    
        _player = controller;
        
        // Pretend I'm goalie -> subscribe to pass events for each of the other team members
        foreach (var adversary in _player.otherTeam) {
            adversary.Brain.OnPassEvent += reactToPass;
        }
    }
}

I know this is wrong. Is the logic incorrect? Is there a better way to do what I'm trying to do?


Solution

  • For me personally, I've got a Builder or similar that's responsible for instantiating all the actors. In your first example, you might have a builder that creates the pack. The builder is what knows which dogs are in which position, so the builder is what's responsible for setting up those subscriptions. For example, if you had some pack like:

    public class Pack
    {
        public Dog alpha;
        public List<Dog> packMates = new List<Dog>();
    }
    

    and some enum or something that defines the dogs:

    public enum Breed
    {
        GoldenRetriever,
        Husky, 
        Dalmation, // ...etc.
    }
    

    Then you could pass a List<Breed> to the builder, the first dog in the list is created as the alpha from a dog factory's GetDog() method, and you'd wind up with something like:

    public static class DogFactory
    {
        public static Dog GetDog(Breed breed)
        {
            switch(breed)
            {
                case Breed.GoldenRetriever:
                  // get the dog here, etc.
            }
        }
    }
    public static class PackBuilder
    {
        public static Pack GetPack(List<Breed> breeds)
        {
            Pack pack = new Pack();
            pack.alpha = DogFactory.GetDog(breeds[0]);
            for(int i=1; i<breeds.Count; i++)
            {
                pack.packMates.Add(DogFactory.GetDog(breeds[i]));
            }
            foreach(var dog in packMates)
            {
                dog.Bark += pack.alpha.OnBark;
            }
        }
    }
    

    The point of the Builder pattern is:

    to separate the construction of a complex object from its representation.

    so as I use it here, I'm building a complex object (the pack) which I know to be an alpha and some number of subordinates. After creating the dogs themselves, the builder can then also perform any post-instantiation linking or other initializations that are required.

    Ideally, by the time I get the pack (or the lacrosse team) I don't need to know about who is reporting to whom for the purposes of communication. All of that should be established when the pack/team is created. Encapsulate that functionality in the builder!

    Your TeamBuilder class could be what differentiates the goalie from forwards, defenders, etc., but again by the time the client calls TeamBuilder.GetTeam() the client should only really care that the team exists and is cohesive.