Search Unity

Avoiding Circular References

Discussion in 'General Discussion' started by jasrei, Nov 10, 2018.

Thread Status:
Not open for further replies.
  1. jasrei

    jasrei

    Joined:
    Apr 19, 2018
    Posts:
    15
    I've got about 20 years of enterprise development experience (web, windows, etc.), but am pretty new to Unity. When I began building out my app, I attempted to separate the model from the logic, the same way I would with a business app.

    In this case it's an RPG type of "game". I created classes for PlayerCharacter and Monster, in addition to many others. I created "Service" classes (PlayerCharacterService, MonsterService, etc.) that would persist these objects to disk and load them as well as perform logic on them. The model classes look something like this:

    Code (CSharp):
    1.  
    2. public class PlayerCharacter
    3. {
    4.     public string Name;
    5.     public int Level;
    6.     public int CurrentHP;
    7. }
    8.  
    And then a "service" class might have something like this in it:

    Code (CSharp):
    1.  
    2. public class CombatService
    3. {
    4.     public static void PerformAttack(PlayerCharacter char, Monster monster)
    5.     {
    6.         // raise a PlayerAttackingMonsterEvent
    7.     }
    8. }
    9.  
    I then listen for PlayerAttackingMonsterEvent in my MonsterController, which is a MonoBehaviour attached to the GameObject.

    Everything was good until I created the PlayerAttackingMonster event, which I caught in my MonsterController class.

    My event has a PlayerCharacter object and a Monster object instantiated in it, but I want the monster to run toward the attacking player. In order to do that, I need the transform of the GameObject.

    So I added a field to my PlayerCharacter class:

    Code (CSharp):
    1.  
    2. public class PlayerCharacter
    3. {
    4.     public GameObject GameObject;
    5.     public string Name;
    6.     public int Level;
    7.     public int CurrentHP;
    8. }
    9.  
    There are 2 problems here. The first is that my model is now tightly coupled to Unity, which I would prefer to avoid. The second is that I have a circular reference between my GameObject and my PlayerCharacter class.

    Am I doing something structurally wrong here? How do people avoid this situation?
     
  2. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    Welcome to game development, where all buisness dev S***s itself.

    People aren't avoiding it. Like, at all. Overengineering sometimes not worth it.

    One thing I'd suggest is to use ScriptableObjects instead of storing anything to disk, because it's faster (Unity prefetches data needed on prefab / scene load), and easier for you (plus designers to do work on the data).

    Data can be placed in SO, then prefab could have a MonoBehaviour attached to the gameObject with a scriptable object data reference attached to it. Alternatively, SO's can be discarded, and all the data could be stored on the component itself and a prefab. (Which will cause different design issues).

    Another thing, you could adopt hybrid ECS, which is a beast on its own, with a lot of quirks. (And its still experimental, which will lead to some API changes in the future, not to mention possible bugs). Alternatively, any ECS (non-Unity related) package would do the same, although its not OOP from there on, but rather DOD.

    Another idea is to store persistent id for the entities running in the scene, and have a unique id stored as well.

    Lookup that covers all prefabs may be used. Something like Addressables already attempting to fill that gap (but yet again, experimental).

    TL;DR: There's things that cannot be avoided. Like this one. At some point you'd want to sync / fetch "data" to the gameObject / transform / renderer / other Unity native component.
    This is where even pure ECS struggles right now.
     
    Last edited: Nov 10, 2018
    Kiwasi and Ryiah like this.
  3. Welcome to OOP hell.

    Please, sit comfortably and try to not lean out of the window, it's dangerous.
    First thing first, please dismember your OOD and OOP knowledge and all the MVC pattern, then take one's arm and beat them to death with it.
    Then try to concentrate strictly to composition over inheritance as much as humanly possible.
    Also refresh your memory if you haven't used the component paradigm as much as possible.

    Try to forget all these manager and factory and other nonsense. You're making a game, not l'art pour l'art object-creation multiverse.
    Try to write as few managers as possible.

    Also try to encapsulate your data not in object-relation, but in logical relations. (Shifting toward DOD is in progress in Unity, it gives much better performance, it worth the pain to learn it properly)

    And please accept VergilUa's advice and try not to over-engineer. Stick to small components which can be added/removed to/from GameObjects if you stick to the OOP-world.
    You always can create connector-classes but usually it does not worth the extra resources.
     
  4. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,706
    I think most game devs eventually accept that every game is unique (otherwise why make it?) and is made to run in a specific engine, or at least in an architectural paradigm such as component-based design that's common to many game engines. Unlike enterprise development, where code reuse is important, if you over-engineer your game implementations, you end up spinning your wheels and not making any progress. Especially for your first few games, consider writing a little closer to the metal.

    For example, instead of a model-service architecture, you could use a component behavior architecture. Give both your player and your monster a Health component, an Attack component, etc. Component-based design isn't the only way, of course; but if you're asking what most people do, it's probably this.

    If you want to stick with your model-service architecture, you could pass the attacker and target as data elements in your Attacking event. The attacker and target could have services that return their engine object.
     
    xVergilx, Ryiah and Lurking-Ninja like this.
  5. jasrei

    jasrei

    Joined:
    Apr 19, 2018
    Posts:
    15
    I suppose I'll just get over my need for purity that has been drilled into me for the last 20+ years. For now I've got the GameObject reference on my PlayerCharacter class, which is a circular reference, but seems to work just fine. Eventually I'll come up with something else.

    I actually thought about caching a global list of every monster and every player character spawned in the zone, which I could use to look up the transforms that I need. This should be easy to do as I have "spawner" objects with scripts attached to them that spawn players and characters into the world. I would be simple to add code to raise an event any time a player is added (or killed) and have another "manager" populating them into a static list that I can search.

    I may do that at some point, but for now I'm mostly concentrating on learning all the different systems, so for now I'll keep the circular reference so that I can move on.
     
    Lurking-Ninja and TonyLi like this.
  6. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,706
    Smart thinking. I think a lot of game programmers fall into development hell by constantly redesigning the same system over and over rather than just getting the darn game done. :)
     
  7. MelonYoy

    MelonYoy

    Joined:
    Aug 1, 2020
    Posts:
    11
    Apologies for necroposting but reading this post rebooted my brain. Thank you so much for posting this. I have been doing this exact thing for my games because of how much shame I feel for not following principles all the time. Thank you.
     
    TonyLi likes this.
Thread Status:
Not open for further replies.