Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question Game architecture or centralizing game logic

Discussion in 'Scripting' started by Eyellen, Aug 12, 2023.

  1. Eyellen

    Eyellen

    Joined:
    Aug 9, 2021
    Posts:
    27
    Hey, can someone give me an advice about making centralized game logic, for example I have a lot of different systems for example UI, PlayerController, ExperienceManager, Inventory, etc. When the player takes some item the UI will show it on the screen, player's health will be displayed, etc. How I should manage it all that way so it will not be spaghetti code? I've already make a big multiplayer project for me as a beginner and I don't like the way how it's built (spaghetti code). I've learnt about Zenject and it seems nice for me but I still don't know to make all that sruff, I feel like Zenject is a tool to inject my dependencies on things I didn't built yet, maybe some pattern, some kind of manager though I've heard that managers are bad practice and service locator is better and on the other side the service locator has it's own problems and not a good solution too so I don't know what to do. I know there is no perfect solution but I need some tip of nice solution that will help me to avoid spaghetti code.

    Btw right now the best option in my mind is to make GameManager that will simple have all states of game and when the state changed it sends event with new state as a parameter and I subscribe everything I need via Zenject
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,563
    Don't touch Zenject. Unity already does 100% of the features of Zenject; dependency injection. It's exactly what Unity scenes are: dependency injection modules.

    The only meaningful way to answer your question is for YOU to identify the patterns you used in your past project that caused you issues, and then structure it to avoid those issues.

    Find the pain, eliminate the pain.

    Otherwise if you felt some pain and you just want to blindly apply patterns you read about on the internet, then you'll just have different pain.

    Make LOTS of little games... and by lots I mean make a list of simple games and just make 100% of them. Here's an example of such a list:

    https://forum.unity.com/threads/rank-these-10-games-from-easiest-to-hardest-to-program.1130023/
     
    DragonCoder, Ryiah, SisusCo and 2 others like this.
  3. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    Do you need centralised logic though?

    Not every part of your project needs to access every other part of your project. I've found keeping relationships one-way is what keeps things tidy and manageable. Some systems sit lower - at the bottom even - and some sit higher or on top and use other systems through their API.

    Then it's a matter of breaking systems down into small parts and providing the appropriate interface for any other system.

    Assembly definitions help here, as they enforce this separation of systems.

    Otherwise it's just a case of experience. Write lots of code, review your results, find better ways to achieve things. That only comes through doing.
     
  4. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    20,082
    Eyellen likes this.
  5. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,563
    Thank goodness for small mercies. I have seen many very-costly projects absolutely WRECKED by Zenject.

    I think it comes from well-meaning .NET C# guys who just cannot get their minds around the idea that Unity doesn't allow them to make constructors for most classes. They just cannot handle it! They see it and they develop a facial tick and start squirming in their seats, then stand up and go running off in really destructive directions, such as Zenject, and then the rest of the team has to live with a horribly broken miserable work environment, when the Unity work environment (using Unity scenes and prefabs as dependency injection) works so effortlessly and flawlessly out of the box.
     
    DragonCoder and Eyellen like this.
  6. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,104
    Using dependency injection is no silver bullet; adapting it won't magically make all your pain points go away. But the pattern can potentially offer some help with making your codebase more robust, and help untangle some of that spaghettiness that you tend to get when using singletons.

    The problem with singletons is that they can essentially be used by any class, any time, anywhere. You can have a random
    ToString()
    method in a random data struct somewhere access
    GameManager.Instance.Player.Name
    , which could cause the game to get stuck when this gets called for logging purposes while the game is still in the loading screen.

    Dependency injection can help get rid of issues like this, by making the dependencies between types more clear, and even making it impossible to try and access objects before they are ready to be used, or in locations where they should never be used. This is because you can build your architecture in such a way that the only way to get your hands on certain dependencies is via the dependency injection pipeline.

    So then for example that bug with the struct's ToString method would never happen, because there data struct wouldn't be able to access the Player object independently. This would force you to instead architect your code in such a way that you pass in the name of the player to the data struct from a higher level, where you can ensure that said data is actually available.
    Code (CSharp):
    1. public Player InitPlayer(Player playerPrefab, PlayerManager playerManager, InputManager inputManager)
    2. {
    3.     var id = new EntityId(Guid.NewGuid(), playerManager.PlayerName);
    4.     return playerPrefab.Instantiate(id, playerManager, inputManager);
    5. }

    In addition to this, information hiding is a key strategy in reducing coupling between classes and keeping the overall complexity of the code base in check.

    Try to create simple and deep abstractions, where the public members that are exposed are very simple, and as much of the complexity and mutable state as possible is hidden away in implementation details.

    For a great example, the
    HashSet<T>
    class is super easy to use via a handful of public methods like
    Add
    ,
    Remove
    and
    Clear
    . Even though it contains 1600 lines of complexity internally, the abstraction is so intuitive and well-designed, that it ends up just reducing the overall complexity of the code base.
     
    Last edited: Aug 13, 2023
    TheNullReference, Eyellen and Ryiah like this.
  7. Eyellen

    Eyellen

    Joined:
    Aug 9, 2021
    Posts:
    27
    Thanks yall for answers!

    Can you tell me more about it or where I can learn about it? Because I've never heard of it and I don't understand how these scene modules will communicate to each other
     
  8. TheNullReference

    TheNullReference

    Joined:
    Nov 30, 2018
    Posts:
    222
    I played around a lot with Zenject, Extenject and VContainer. They're interesting but they won't solve your problems. Probably the main reason anyone would use them, is they allow you actually use Interfaces instead of Concrete classes.

    Rely on abstractions, not concretions.

    For me, spaghetti code is where if you mapped out your dependency graph, all the interconnections between your different class in a visual scripting way, it would look like a bowl of spaghetti. Abstractions allow you to avoid these direct links, making it much more difficult to create a brittle application.

    Someone suggested initArgs, but I'll also shamelessly plug my implementation [SerializeInterface] which allows you to drag an drop interfaces in the inspector.

    https://forum.unity.com/threads/ser...nterfaces-in-the-editor-version-0-04.1465469/

    The next recommendation, is use event-driven programming. I personally recommend UniRx. This allows you to invert the control of your classes.

    So rather than you inventory having to tell your UI to display the item, the UI will instead listen for changes in the inventory, and when the inventory does have an item added, the UI will display it. This improves encapsulation as only the class itself is ever changing its own state.

    Code (csharp):
    1.  
    2. // Before
    3. public class Inventory : MonoBehaviour
    4. {
    5.     // Inventory has references that it doesn't care about.
    6.     [SerializeField] private InventoryUI _inventoryUI;
    7.  
    8.     public void AddItem(Item item)
    9.     {
    10.         // It has to mutate the state of another class.
    11.         // Every time you want something new to happen when an Item is added, you'll have to add the code here.
    12.         _inventoryUI.DisplayItem(item);
    13.     }
    14. }
    15.  
    16. public class InventoryUI : MonoBehaviour
    17. {
    18.     // Notice this is public, many classes could mutate the state of this class without being able to track it.
    19.     public void DisplayItem(Item item)
    20.     {
    21.         // Display the item.
    22.     }
    23. }
    24.  
    Code (csharp):
    1.  
    2. // After
    3. public class Inventory : MonoBehaviour, IInventory
    4. {
    5.     private Subject<Item> _onItemAdded = new Subject<Item>();
    6.     public IObservable<Item> OnItemAdded => _onItemAdded;
    7.  
    8.     public void AddItem(Item item)
    9.     {
    10.         _onItemAdded.OnNext(item);
    11.     }
    12. }
    13.  
    14. public partial class InventoryUI : MonoBehaviour
    15. {
    16.     [SerializeInterface]private IInventory _inventory;
    17.  
    18.     private void Start()
    19.     {
    20.         _inventory.OnItemAdded.Subscribe(DisplayItem);
    21.     }
    22.  
    23.     // DisplayItem is private, only this class has control over its state.
    24.     private void DisplayItem(Item item)
    25.     {
    26.         // Display logic.
    27.     }
    28. }
    29.  
    Zenject isn't worth it (outside of a few applications), but it did teach me some important concepts.

    1) Use Dependency Injection.

    Classes should not be responsible for finding their own dependencies! (service locator). This does not mean you need an IoC Container. [SerializeField], InitArgs are examples of dependency injection (in spirit).

    2) Use Interfaces.

    Allows classes to not care about each other as they never reference one another. Changing your player controller logic will not break other classes referencing it that lead to a huge cascading errors.

    3) Use Observer pattern.

    Rather than directly mutating the state of other classes, breaking encapsulation, have those classes observe events and react accordingly.

    4) Single authoritative source for Data Mutation.

    There should only be a single class responsible for changing your data, that doesn't mean wrapping your data in a Getter/Setter. Direct mutation should not be possible.

    5) Be mindful of your dependency graph.

    You probably don't think about it much, but when you're dragging and dropping components into each other, you're creating a dependency graph where your different classes rely on each other. Class A -> Class B -> Class C etc.

    If you're not mindful of this, you can create circular dependencies, or might be developing a dependency graph that comes to bite you in the butt. The one good thing about Zenject is that it forces you to think about your dependency graph in a way that Unity doesn't as it simply wont work otherwise.

    Children can reference parents but parents can't reference children. I think there's room to break the rules at the leaf nodes of the graph, otherwise Unity wouldn't be fun to work in, but I think one of the intrinsic reason Unity apps hit a wall is because not paying attention to your dependency graph can only get you so far.
     
    Last edited: Aug 13, 2023
    Eyellen and SisusCo like this.
  9. TheNullReference

    TheNullReference

    Joined:
    Nov 30, 2018
    Posts:
    222
    Misleading to say it does 100%, you wont be able to use interfaces without some kind of 'injection' or service locator class. You also wont be able to resolve dependencies for dynamically spawned objects without a custom factory class either.
     
  10. John_Leorid

    John_Leorid

    Joined:
    Nov 5, 2012
    Posts:
    620
    All you need is a simple struct that casts a MonoBehaviour reference to an interface. You can also do it manually or, as said, use a struct that does the job for you. Such Struct-Code can be found on github, asset store, wherever.

    Wrong. xD
    If you use scriptable object references, it's no issue at all. Also with any kind of static reference such as Singleton, Static Class, .. Or simply use Unitys API like
    GameObject.Find()
    or
    FindObjectByType<>()
    .
    "Won't be able [...] without a custom factory class" is just incorrect. Btw. that's exactly what the
    Awake()
    call is meant for - you can initialize and link everything you want from within a class (or even it's base class if you need it multiple times).

    If you build a factory to spawn GameObjects (such as enemies or items), you build a second factory on top of another one (because in the C++ code they are using basically a factory pattern).
     
    Kurt-Dekker likes this.
  11. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,563
    It's just the basic way you use Unity, one scene at a time.

    Don't think of scenes as being "live" and communicating with each other.

    instead, centralize communication with some kind of central state management mechanism (GameManager, AppManager, etc.) and have each scene contact that mechanism.

    And seriously, don't overthink this stuff. Just get it working. You'll probably delete all of it and change it anyway.

    ULTRA-simple static solution to a GameManager:

    https://forum.unity.com/threads/i-need-to-save-the-score-when-the-scene-resets.1168766/#post-7488068

    https://gist.github.com/kurtdekker/50faa0d78cd978375b2fe465d55b282b

    OR for a more-complex "lives as a MonoBehaviour or ScriptableObject" solution...

    Simple Singleton (UnitySingleton):

    Some super-simple Singleton examples to take and modify:

    Simple Unity3D Singleton (no predefined data):

    https://gist.github.com/kurtdekker/775bb97614047072f7004d6fb9ccce30

    Unity3D Singleton with a Prefab (or a ScriptableObject) used for predefined data:

    https://gist.github.com/kurtdekker/2f07be6f6a844cf82110fc42a774a625

    These are pure-code solutions, DO NOT put anything into any scene, just access it via .Instance

    Alternately you could start one up with a
    RuntimeInitializeOnLoad
    attribute.

    The above solutions can be modified to additively load a scene instead, BUT scenes do not load until end of frame, which means your static factory cannot return the instance that will be in the to-be-loaded scene. This is a minor limitation that is simple to work around.

    If it is a GameManager, when the game is over, make a function in that singleton that Destroys itself so the next time you access it you get a fresh one, something like:

    Code (csharp):
    1. public void DestroyThyself()
    2. {
    3.    Destroy(gameObject);
    4.    Instance = null;    // because destroy doesn't happen until end of frame
    5. }
    There are also lots of Youtube tutorials on the concepts involved in making a suitable GameManager, which obviously depends a lot on what your game might need.

    OR just make a custom ScriptableObject that has the shared fields you want for the duration of many scenes, and drag references to that one ScriptableObject instance into everything that needs it. It scales up to a certain point.

    And finally there's always just a simple "static locator" pattern you can use on MonoBehaviour-derived classes, just to give global access to them during their lifecycle.

    WARNING: this does NOT control their uniqueness.

    WARNING: this does NOT control their lifecycle.

    Code (csharp):
    1. public static MyClass Instance { get; private set; }
    2.  
    3. void OnEnable()
    4. {
    5.   Instance = this;
    6. }
    7. void OnDisable()
    8. {
    9.   Instance = null;     // keep everybody honest when we're not around
    10. }
    Anyone can get at it via
    MyClass.Instance.
    , but only while it exists.
     
    Eyellen likes this.
  12. TheNullReference

    TheNullReference

    Joined:
    Nov 30, 2018
    Posts:
    222
    The point of a framework is to handle these aspects for you so you don't have to write new code for every class.

    Unity doesn't handle it YOU do because you're the one writing the code.

    If that's your definition than technically you don't need u ity, or a coding language other than assembly because assembly can do everything Unity can do.

    The [SerializeField] is a framework capability because it reduces the amount you need to code.

    To say you don't need a framework because you can write it all by hand is technically true, frameworks don't make things possible they make them easy.

    I'd rather my scripts adhere to single responsibility and be as simple as possible, no need for awake function resolving dependencies or ugly casts.

    Though you're suggesting singletons as a viable solution so we're probably just at very different places on how we like to code. I agree with their use for personal projects, would not allow someone to create one at work.
     
    Last edited: Aug 13, 2023
    SisusCo likes this.
  13. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,104
    I think this is meant as a criticism towards using the factory pattern with game objects... but so what? Is there some actual practical downside here? Is using nested prefabs bad practice? Is nesting prefab instances inside scenes bad practice? This feels like just rationalization.

    In my opinion it's best to focus on solving real-world pain points and optimizing real-world workflow with all the tools we have at our disposal :)
     
    Last edited: Aug 14, 2023
    Kurt-Dekker likes this.
  14. John_Leorid

    John_Leorid

    Joined:
    Nov 5, 2012
    Posts:
    620
    Same, I was just mentioning them because ultimately, using static classes, scriptable objects or GetComponentOfType<> is almost the same as a Singleton that lives in the DontDestroyOnLoad Scene. All of them are immortal, single objects that can be accessed from pretty much everywhere and everyone uses one of them.

    A classic MonoBehaviour, DontDestroyOnLoad Singleton that has to exist in the Scene so other things (prefabs) can work is bad, sure. But when they instantiate themselfs on demand, it's no different than a static class or scriptable object that get's an update loop. (I still don't use them, neither at work nor in private projects)
    But that's not really the topic here, or is it?

    Comparing writing a struct to handle interface-references to writing an engine is wild.
    Same with writing initialization logic like GetComponent<> which is a one-liner, just like a zenject attribute would be.

    So "not required" also means "doesn't make things easier" in this specific case. It makes things different, but not necessarily easier.

    Maybe I was a bit harsh with my first message, I just wanted to make it clear that there are always many options to solve something and that there isn't a silver bullet that solves all of them. My personal problem with Dependency Injection is that it creates Dependencies and in Game Dev, you don't want that, you want things as isolated as possible. Every prefab should work in an empty scene. Every feature should be exchangeable. If you start creating connections between the player and the enemy manager, the sound manager and so on - a change to the AI can lead to a change to the Player, leading to changes in the UI, leading to changes in everything in the game. This is the worst case.
     
    cerestorm likes this.
  15. John_Leorid

    John_Leorid

    Joined:
    Nov 5, 2012
    Posts:
    620
    Sure. You can't drag an drop enemies into the game, while it is running, to test something. If you forget to spawn them correctly, it will throw errors - which leads to bug-hunting.
    If the factory just sets one field that is only required for one specific attack that happens once in a while, bug hunting can get tedious, you might don't know who spawned this enemy, which part of your code doesn't work. It could be anywhere, at any Instantiate call, or even worse - it could be no call at all, just an enemy the level designer placed into the level at a specific spot.

    Requireing a factory to spawn something can lead to massive issues.
     
  16. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,104
    I meant that is there any practical downside to building "a second factory on top of another one", which I understood to be your original criticism in and of itself.
    I.e. if using the factory pattern solves a problem on the C# side, then I don't see why it should be avoided even if Unity also uses it internally on the C++ side :)

    That's not necessarily true; even if you use the factory pattern in-game to decide which particular instance to return to clients, it doesn't mean that each instance can't also work independently when dragged into the scene.

    Did you perhaps mean the builder pattern? In that case I agree that is a valid downside one would have in most situations. There are definitely benefits to "batteries included" style game objects that just work when instantiated into any scene, without having any external dependencies.

    Although, having a batteries included style game object and using the builder pattern are not entirely mutually exclusive. One could make a game object work without any external dependencies with sensible defaults, but also still use a builder pattern in-game to potentially override some of those default values.

    For example, the object could use high quality assets by default, but the builder could change it to use low quality ones if the user has selected that quality setting in the menu.
    Or the player game object could work perfectly fine when just dragged to the scene, but the separate floating UI element displaying the name of the player could only appear when the object is created using the builder.
     
    Last edited: Aug 14, 2023
  17. John_Leorid

    John_Leorid

    Joined:
    Nov 5, 2012
    Posts:
    620
    When using both, there isn't really an issue, but this thread was about dependency injection and connections between different objects and systems, so I assumed that the factory or builder pattern would be used to establish those connections (to e.g. an enemy manager so they can act in groups .. or a cover manager, so they find places to hide, ..).

    In that case, they would be mutally exclusive, no? An enemy that doesn't use cover nearby because it has no connection to the CoverManager is broken, wether it throws an error in the console or not - so just catching those cases wouldn't solve the issue.

    I just think it should be avoided next to each other. If something comes out of a factory, don't put it right through the next one. (or basically get it from a factory that uses another factory would be the better way to describe it, I guess)

    Of course, anywhere else .. I don't see any problems with using patterns, which the engine also uses internally.
     
  18. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,104
    I would agree that in this kind of situation this would indeed almost always be the case in practice.

    In theory one could support dependency injection using the builder, but then also use something like a singleton as a fallback, if no dependencies to override the default ones were provided from the outside.
    Code (CSharp):
    1. public sealed class Enemy : MonoBehaviour
    2. {
    3.     public IEnemyManager EnemyManager { get; set; }
    4.  
    5.     void Start()
    6.     {
    7.         if(EnemyManager == null)
    8.         {
    9.             EnemyManager = EnemyManager.Instance;
    10.         }
    11.     }
    12. }
    But I don't think many people would in practice go through the trouble of adding all this extra complexity, if the builder already did the exact same thing...


    However, all this only applies to using just the builder pattern in particular. Many dependency injection frameworks can handle initializing game objects that are dragged to the scene on-the-fly.

    In Extenject I believe you can achieve this using the ZenjectBinding component.
    ZenjectBinding.jpg

    In Init(args) this happens automatically with shared services, or if you need to drag-and-drop references manually, you can attach an initializer to the component.
    init-arguments.png
     
    Last edited: Aug 14, 2023
  19. John_Leorid

    John_Leorid

    Joined:
    Nov 5, 2012
    Posts:
    620
    Before I fully read your message, including this very line, I was wondering why you know so much about Dependency Injection and checked your Signature. Well - of course you do, you wrote your own framework. xD

    And to be honest, the way you approach it seems pretty solid. Using unity at where it's good at, fixing things where it's bad at. It's basically the same thing I do, just with a different toolset.
    • For references within complex prefabs (such as Enemies or the Player) I'm using a blackboard (which is a fancy way to say "a MonoBehaviour with a bunch of variables that is referenced everywhere").
    • There is also a global version of said blackboard, but I rarely use it for anything. (still testing this idea)
    • Also I'm using a custom static Tag-System which a friend of mine called 'MoreTags' to find any collection of Objects I need to find. Given there are less than 500 Pickable Items, Buildings or Enemies loaded at a time, this static dictionary solves 99% of all Manager-Requirements.
    • Then the ScriptableVariables from Ryan Hipple.
    • And last but not least, a static Event Hub System.
    And with those tools I was able to solve all dependency problems so far. I think they sould be built-in, especially the event hub, which is just a more performant, better version of "SendMessage", and of course the Tags. I really do wonder who had the ingenious idea to add tags, but only one for each GameObject. xD

    So yea, idk what else to say. There are just many ways to reach a goal (good game code).
     
    Last edited: Aug 15, 2023
    CodeRonnie and SisusCo like this.
  20. TheNullReference

    TheNullReference

    Joined:
    Nov 30, 2018
    Posts:
    222
    I agree in that I like to have everything as modular as possible, and try to ensure my components are loosely coupled enough that they can be dragged an dropped, changing values in editor correctly updates the game, disabling components works as intended, even though these features have no impact on the final game.

    I like the idea of the "default" prefab with optional parameters from a factory. [SerializeField, Inject] works as far as I know, with Inject overwriting the value of the field. I like having this hybrid approach where that's a mix of ease of use, but opens the possibility for DI for more control / testing etc.

    One recent example was I have an enemy prefab, that chases a target transform. This transform can be injected, or just dragged and drop into the inspector for testing purposes. Also having a valid no-target state so the enemy is happy to chill if the transform is null.

    I'm curious how you would handle an enemy cover system without an enemy having a reference to a cover manager? I guess you could use a Singleton or FindObjectOfType, or have each enemy have their own independent cover system. Singleton I would say is worse pattern than a C# factory on-top of a C++ factory, and the others might be suitable but less run time performant.


    Do you think it's appropriate to instead create a DefaultEnemyManager or NullEnemyManager in these fall back cases?

    Code (CSharp):
    1. public sealed class Enemy : MonoBehaviour
    2. {
    3.     public IEnemyManager EnemyManager { get; set; }
    4.  
    5.     void Start()
    6.     {
    7.             EnemyManager ??= gameObject.AddComponent<FallbackEnemyManager>();
    8.     }
    9. }
    10.  
    11. public class FallbackEnemyManager : MonoBehvaiour, IEnemyManager()
    12. {
    13.     public int GetEnemyCount()
    14.     {
    15.          Debug.LogWarning($"FallbackEnemyManager is being used on {gameObject.name}", this);
    16.          return 1;
    17.     }
    18. }
    19.  
    20.  
    Actually on second thoughts, if we're dealing with "default behavior's"

    Code (csharp):
    1.  
    2. [code=CSharp]public sealed class Enemy : MonoBehaviour
    3. {
    4.     [SerializeInterface, Inject]
    5.     private IEnemyManager EnemyManager = new DefaultEnemyManager();
    6. }
    7.  
    above code has 3 possible dependency resolution. It can be injected with Zenject / VContainer, dragged and dropped with SerializeInterface or fall back to it's default value.

    That's more of a batteries included approach. The DefaultEnemyManager wont be very functional as it only applies to a single enemy, but it wont break the enemy. I think it's okay for dependency resolution to be intentional when you're wrapping things up in bigger systems.

    Ill do anything to avoid a Singleton
     
    Last edited: Aug 15, 2023
  21. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,104
    That does sound like a very flexible set of tools!

    I'm a big fan of events myself. Although, in some situations there can be a risk of the system failing silently due to initialization order related issues. I recently discovered a bug in a codebase where important events sent from the backend could sometimes be missed, because a message handler hadn't necessarily yet subscribed to receive notifications about these messages before the first ones already started coming in! And this bug had been hiding in the project for several years I think. It's exactly this sort of issues that can be avoided using dependency injection, which I think is one of its biggest benefits.

    I've never used a multi-tag system so far, but it does sound like an interesting concept. Well, I guess I have actually used a very similar system in ECS projects for querying entities by the data types that they contain, and that did work very well.
     
    Last edited: Aug 15, 2023
    John_Leorid likes this.
  22. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,104
    This is a really nice approach, I agree. Great use of the null object pattern too!

    If an EnemyManager is required for the Enemy to function properly, then I would rather have the Enemy immediately log an error during initialization, rather than using the null object pattern to suppress or delay it. But I can think of two exceptions to this:
    1. To avoid thousands of exceptions occurring, it could be preferable to just log one very informative error in Start, and then use the null object pattern to avoid further exception spamming in the Update method etc.
    2. If the enemy manager is loaded asynchronously, and could become ready later than an Enemy, then the null object pattern could be a valid option for making the Enemies just wait in place doing nothing, until the actual manager is ready and gets injected to them.
    I would prefer having an architecture where the enemy manager is guaranteed to be initialized before all the enemies are, over option #2 though.

    Having components function completely independently is very nice, but not necessarily practical in all situations. I think it's better to be honest about something vital being missing, so that this erroneous situation does not potentially go unnoticed, rather than try to patch up the holes and just keep going.
     
    TheNullReference likes this.
  23. John_Leorid

    John_Leorid

    Joined:
    Nov 5, 2012
    Posts:
    620
    If this question is targeted at me the answer is simple - just .. don't xD

    I would have a dedicated "GoToCover"-State in my Statemachine (or Leaf-Node in a BehaviourTree or Action in GOAP / UtilityAI) and this state would ask either a ScriptableObject (from the Resources folder, too lazy to drag and drop that for every of my 20ish enemies) or a static class to get a cover position.

    If the cover is procedurally generated, this will only happen once per frame or less - the first enemy that calls this system will trigger the recalculation of cover, any other enemy, requirering cover in the same frame, will use the same information.

    If the cover is setup in the scene, using transforms that need to be validated if they are visible by the opposing troops, the validation is also some kind of procedural step and I'd probably use the same system, combined with my MoreTags-System to first get all Cover Positions, then check visibility (using DOTS RaycastCommands or just over multiple frames).

    A Cover System per Enemy would always lead to the issue that two enemies pick the same cover-spot, no? Aside from Performance Issues.
     
  24. John_Leorid

    John_Leorid

    Joined:
    Nov 5, 2012
    Posts:
    620
    Very true. SendMessage() has a parameter to specify wether the event must be received or not ... maybe I should also add this feature? But actually ... when events must be heard, that's an issue in itself, no? All events should always be assumed to go nowhere and only if they are picked up by something, things will react to it.
    I'm subscribing to most events in the Awake() method, which is more or less the constructor. Of course I can't send events to objects that don't even exist yet.

    And in my specific case I log all events and I have a list of all subscribers in an EditorWindow. So debugging and testing is quite easy.

    How would you deal damage to an enemy using dependency injection?
     
  25. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,104
    I wouldn't use dependency injection for that - except for potentially something like passing a reference of the damage dealer to the enemy receiving the damage (method injection).
    Code (CSharp):
    1. void OnCollisionEnter(Collision collision)
    2. {
    3.     if(collision.gameObject.TryGetComponent(IDamageable damageable))
    4.     {
    5.         damageable.TryDamage(this);
    6.     }
    7. }
    Btw, I'm not trying to say that DI should be used as the one and only universal solution for everything, or that events should not be used at all. But I would say that it's much preferable to have tightly coupled code that works reliably, over loosely coupled code that has bugs :D