Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Is this an example of good abstraction, or terrible coupling?

Discussion in 'Scripting' started by Sendatsu_Yoshimitsu, Jul 20, 2017.

  1. Sendatsu_Yoshimitsu

    Sendatsu_Yoshimitsu

    Joined:
    May 19, 2014
    Posts:
    691
    I'm working on an RPG where a lot of common utilities, like spawning a character or changing levels, need to be triggered from a variety of narrowly scoped objects. A character whose conversation triggers a cutscene doesn't need to know anything about the cutscene system, and doesn't care what happens after it makes the request; it just needs to find some way of requesting that a given scene be loaded and played.

    To accomplish this, I ended up with something which is basically a singleton masquerading as a service locator- I made a GameManager class with a static instance that allows any object to access it at runtime, but gave it no internal functionality. All of its public-facing methods are things like SpawnCharacter(characterID, location) or LoadCutscene(cutsceneID). Aside from these methods, the only stuff it contains are private references to the cutscene manager, character database, audio system and so forth. In the case of SpawnCharacter, for instance, all the game manager actually does is take the information passed in, and directly invokes CharacterDatabase.SpawnSomebody(ID, location). It doesn't know or care what happens, so long as the data is passed on.

    This feels like relatively decent design; it's serving as a messenger, not an actor, and any bugs caused by calls to the game manager are going to be instantly traceable back to the class those calls get routed to.

    On the flip side, however, single monolithic classes that everyone talks to are practically the definition of code smell, and I've coupled a huge amount of game functionality through the manager just by giving it access to a bunch of unrelated manager classes.

    So is this architecture a really bad idea that's going to explode in my face as the codebase grows, or am I safe to iterate around this concept as long as the manager only serves as a messenger, and never actually grows to contain functionalities beyond passing information and requests where they need to go?
     
  2. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,294
    What does:

    callingCode -> GameManager.SpawnCharacter(characterID, location) -> CharacterDatabase.SpawnCharacter(characterID, location)

    provide that you don't get from:

    callingCode->CharacterDatabase.SpawnCharacter(characterID, location)

    ?

    It decouples your calling code from the Character Database, yes, but it now couples it with a GameManager. If your calling code needs to spawn characters, then it's going to be coupled with the character database somehow.


    Essentially, if your game manager doesn't do anything, why does it need to exist?
     
    Kiwasi likes this.
  3. Sendatsu_Yoshimitsu

    Sendatsu_Yoshimitsu

    Joined:
    May 19, 2014
    Posts:
    691
    It would be easy enough to obliterate the game manager entirely by making each of the manager objects their own self-contained unit, but I would still end up making each of those a singleton, since the core architectural need I'm trying to address is allowing objects to access a common set of utilities without making the entire project a huge network of dependency chains. If I make every single manager class static, I've suddenly made a huge portion of my code global, which at least intuitively feels more schizophrenic than having everyone blindly send data to a single manager who routes it.
     
  4. Dave-Carlile

    Dave-Carlile

    Joined:
    Sep 16, 2012
    Posts:
    967
    You mentioned Service Locator, which seems like a good way to handle this sort of thing. But you seem to just have a monolithic class that lets you call functions. Rather than that it seems that a service locator is supposed to return an interface to a set of functionality. In your example you'd get an interface to the character spawner, or the cutscene manager. Anyone can get that reference and use it as it needs, then release the reference when it's no longer needed.

    It's decoupled because your're only working with interfaces that you ask for from the locator.

    This Game Programming Patterns book is a good resource for these sorts of things. It's free online, but I always encourage people to purchase it since it's a quality book.
     
  5. Sendatsu_Yoshimitsu

    Sendatsu_Yoshimitsu

    Joined:
    May 19, 2014
    Posts:
    691
    That is a great resource, thank you for the link! This is probably a silly question, but after pouring over it I'm having a bit of trouble figuring out how the dependency injection would be structured in Unity- is the idea that I would just create a single Locator class with a static instance for each manager/category of functionality I was indexing, then manually drag & drop its reference to the prefab for whichever manager class it needed to point to?
     
  6. daxiongmao

    daxiongmao

    Joined:
    Feb 2, 2016
    Posts:
    412
    Dave-Carlie's can be a good idea. I am not sure I would go with a pure interface based setup or just get a reference to the actual implementation. It depends on if there may be more than one implementation.

    The other setup I have used Is i have a singleton that that has events for those things. So I would fire off the actor action method, and then if someone was interested in that event the would be listening for it. If no one cares at that time then nothing happens. a publish subscribe type of system.

    But in your case if you will always have one available or require something else to react then either having direct singletons or the service type patter can be used.

    But with all singleton type patterns there always comes issues with finding them and initilization order.
    Especially since unity likes monobehaviors for everything. Then being able to free things if they are no longer required.

    A lot will come down to your usage pattern and lifetime requirements. Right now I use separate singletons. Other times I have used a centralized approach.
     
  7. BlackPete

    BlackPete

    Joined:
    Nov 16, 2016
    Posts:
    970
    Just keep in mind that whatever you do, it's supposed to make your job easier in the long term.

    Single monolithic classes are fast to write in the short term, but are horrible to maintain in the long term. Especially when you need to add more stuff like new classes, skills, etc.

    However on the other side of that coin, things abstracted and broken up so much that you have to touch dozens of files each time you need to add something is, well, inefficient. It's also a good way to introduce bugs if you overlook something.

    Find a nice balance where it's easily extensible without a whole bunch of plumbing and updating a whole bunch of stuff.
     
    Dave-Carlile likes this.
  8. Sendatsu_Yoshimitsu

    Sendatsu_Yoshimitsu

    Joined:
    May 19, 2014
    Posts:
    691
    I really appreciate all of the advice, thank you guys. :)

    Just to clarify, on implementing a Locator object to allow whatever external interfaces need to use them, would the general idea be to do something like the following?

    Code (csharp):
    1.  
    2.     public static Locator instance;
    3.  
    4.     public ConversationManager conversationManager; //public so I can set this from the inspector;
    5.  
    6.     public ConversationManager GetConversationManager()
    7.     {
    8.         return conversationManager;
    9.     }
    10.  
    That's not technically dependency injection, but since I'm doing this without explicit pointers it seems like a rough approximation of the same process. Once this is set up everything else becomes private and non-static, and any of my manager interfaces that need to effect functionality look something like:

    Code (csharp):
    1.  
    2. public class SomeExternalClass : MonoBehaviour{
    3.     public void StartConversation(conversationID)
    4.     {
    5.         Locator.instance.GetConversationManager().StartConversation(conversationID);
    6.     }
    7. }
    8.  
     
  9. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,514
    1) you can use SerializeField to allow private fields be visible in the inspector:
    https://docs.unity3d.com/ScriptReference/SerializeField.html

    2) What's the point of making GetConversationManager() if all it does is return an already public field?

    3) If you're going to make GetConversationManager() to return a field... you could make it static and thusly do the work of accessing instance for you.
     
  10. Sendatsu_Yoshimitsu

    Sendatsu_Yoshimitsu

    Joined:
    May 19, 2014
    Posts:
    691
    I think I'm missing something really obvious with regards to how dependency injection is supposed to work. What I'm trying to do with the Locator example above is replicate the functionality from the end of the linked tutorial's pseudocode example:

    Code (csharp):
    1.  
    2. class Locator
    3. {
    4. public:
    5.   static Audio* getAudio() { return service_; }
    6.  
    7.   static void provide(Audio* service)
    8.   {
    9.    service_ = service;
    10.   }
    11.  
    12. private:
    13.   static Audio* service_;
    14. };
    Since I'm doing this in Unity I can't directly use pointers in a safe context, but the next-closest equivalent seems to be using an exposed field to manually give Locator references to whichever classes it's supposed to supply to third parties, at which point we hit your point and I would be better off just making those classes explicit singletons to begin with. Is there really any benefit to forcing DI to fit here? I'm trying to make the foundational architecture as expansible as I can, but it's increasingly feeling like the path of least resistance is to nix the gamemanager, and convert each of the smaller manager classes it communicates with to singletons.
     
  11. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,294
    I've yet to see a DI scheme in Unity that doesn't boil down to an absurdly cumbersome way to globally fetch global data. Then again, people keep saying that DI is The Second Coming, so I might be missing something.

    By all means, hide the access to the stuff doing your work behind an abstraction when that becomes necessary. For now, it sounds like you should just use the bog standard singletons and then refactor once that becomes cumbersome.

    Refactoring isn't dangerous! People should do it more often.
     
    Kiwasi likes this.
  12. Sendatsu_Yoshimitsu

    Sendatsu_Yoshimitsu

    Joined:
    May 19, 2014
    Posts:
    691
    Alright, thank you very much for putting up with my inane questions- I've always found it far too tempting to think in monoliths, so getting feedback is incredibly helpful. :)
     
  13. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,294
    I mean, it could be that doing a full-blown DI framework before you implement... any features is a great idea, and I'm wrong about everything.

    You never know!
     
  14. Sendatsu_Yoshimitsu

    Sendatsu_Yoshimitsu

    Joined:
    May 19, 2014
    Posts:
    691
    That's one reason I always feel so silly about asking programming questions in the format of "Is <X> a good idea"- there are cases where it's valuable to look before you leap, and I think foundational game architecture is one of them, but I almost always learn more when I just go "Screw it," implement and see if it works, then iterate through fixing the problems that arise. (And then in retrospect, go "I'm never doing that again, holy crap" :) )
     
  15. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,539
    In the enterprise world it's incredibly useful. Less so in game development and especially when you're working on top of a pre-existing engine. If nothing else, the code bases tend to be dramatically smaller and Unity is already handling your platform deployment for you. Rare is the Unity-equivalent of "I need to inject a DataSource interface so that the backing implementation can change when I deploy to 4 different flavors of database".
     
    Kiwasi likes this.
  16. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,294
    Heh. We've got 7 different definitions of our save-to-file and load-from-file code (PC standalone, Steam, GOG, WiiU, Switch, XBoxOne, PS4). So there are still instances where it's necessary :p. We've just done that through 7 versions of the same file, each wrapped with it's own #if UNITY_PLATFORM. Maybe we should've used injection instead? Problem is that the save code for one platform does not compile/is disallowed for the next, so all calls to the platform-specific code must be conditionally compiled.
     
    KelsoMRK likes this.
  17. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,539
    Yeah that might have been a good candidate :)

    Only way to DI that would be to put each in their own external lib I guess...but at that point is it any better than conditional compilation?
     
  18. fvde

    fvde

    Joined:
    May 15, 2014
    Posts:
    19
    This thread makes me happy, because you are taking the time to ask yourself these questions, and sad at the same time because of the amount of floating misinformation...

    What you are doing is indeed very high, global coupling. It saddens me how much Unity encourages this. And no it is not sending messages, it's sending actions. What you're considering now is not DI, it has nothing to do with pointers and the key to DI is IOC, Inversion of Control (worth googling!). That allows for truly decoupled services and crucially allows isolated unit testing by mocking everything else.

    With all that being said: Do you need any of this? Probably not. From personal experience I can say that once you really grasp DI you will never want to go back, regardless of game or enterprise service.

    TLDR: There's very good DI frameworks, google IOC, pub sub models
     
  19. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,539
    Except game development tends to not be very service oriented and unit testing is notoriously difficult (especially in Unity).

    From personal experience it's awesome for enterprise and next to worthless for game development :)

    I know this is why you're saying these things and I 100% agree with you outside of a Unity context :)
     
  20. BlackPete

    BlackPete

    Joined:
    Nov 16, 2016
    Posts:
    970
    I like this quote from a 2006 article: "Dependency Injection" is a 25-dollar term for a 5-cent concept.

    The locator code you posted above might as well be a property.

    Don't get me wrong, there are cases where DI is useful (try working in a team with dozens if not over a hundred developers), but to repeat myself: Don't add code for the sake of adding code. Do it only if it's going to make your job easier in the long run.

    A framework can make sense in a large team because you want the code base to be consistent and therefore easier to navigate, regardless of who wrote the code you're currently looking at. It also reduces the odds of stepping on toes.

    If you're a one-man team, all that might be overkill. That's not to say it's a bad idea, but if you're increasing the amount of work you need to do to keep the code safer from... yourself, you increase the chances of burnout.
     
    Kiwasi and lordofduct like this.
  21. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,514
    Oh... this is about dependency injection.

    That's not dependency injection... that's the Service Locator pattern you're setting up:
    https://en.wikipedia.org/wiki/Service_locator_pattern

    I mean... it is related to dependency injection in that it support decoupling the creation of dependencies from the object that uses it. But it's not done by injection, it's done by a service locator.

    I'm familiar with this, this is my Service locator here:
    https://github.com/lordofduct/spacepuppy-unity-framework/blob/master/SpacepuppyBase/Service.cs

    ...

    I don't know... this masturbating over dependency injection is something that confuses me.

    Do I think it's bad? No... far from! I use some version of it all the time! Of course, that's only when I really need it... and in game design it's usually on a small enough scale that it's fairly simple to implement, rather than use some framework.

    And that's why I've honestly never could figured out why I would need a framework for it in a video game. The most I've seen the benefits of it is that they often allowed arbitrary dependency injection onto an object, so that other objects could arbitrarily find dependencies on them... which honestly, sounds like the component model that Unity already uses!

    All you have to do is abstract the component into an interface... and vuala, DEPENDENCY INJECTION!

    Or am I missing something here?
     
  22. Sendatsu_Yoshimitsu

    Sendatsu_Yoshimitsu

    Joined:
    May 19, 2014
    Posts:
    691
    Hmm, I refactored my monolithic god class out of the project entirely, but I still suspect that what I'm doing is very counterproductive; at the very least it's highly coupled, but I can't quite get my head around the alternative, would you mind critiquing my current setup so I can at least see how you're supposed to accomplish this in the context of a game architecture?

    Going off of the principal that every object should only do one thing, I broke the gamemanager down into a number of smaller singletons: CameraManager determines what the camera is looking at and which camera is active, DatabaseManager curates dictionaries of assets (character models, items, etc) and has a bunch of public GameObject GetCharacter(somekey) methods, LevelManager loads/unloads/streams levels, and so forth.

    There's no longer a single centralized control flow, but individual methods are now in charge of requesting functionality- if I open a treasure chest it calls CameraManager.FocusCamera(myGameObject), plays an opening animation, then calls player.AddItem(int itemKey). Anything in the inventory that wants to display that item's data does so by checking DatabaseManager.GetItem(itemKey).

    The upside to this, of course, is that it's extremely easy to implement functionality, and since each manager is isolated and has a very narrow range of functionality, debugging will be pretty easy. However, by doing it this way I've basically coupled my entire project to those manager classes. My kneejerk reaction is to say "That's bad, but it's probably fine," since it's easy to maintain, but I can't shake the feeling that just like the single monolithic game manager, this will eventually grow into a monster that's impossible to refactor.
     
  23. daxiongmao

    daxiongmao

    Joined:
    Feb 2, 2016
    Posts:
    412
    I am not sure that is bad, look at the beginning of your statement. "Easy", "Easy". It sounds like a good approach. If you never connect these things how can you ever do anything? Your are making a game not a camera library for someone else to use. The fact these classes are in your project you have already coupled them together.

    With that model you want your higher level objects to use these services together like you describe. What you want to try to avoid is the services using each other. That is where your dependencies will get all messed up. But at some point the higher level things will have to use these services to actually do something.

    And by having the higher level code use them then its a lot easier to combine them in unique ways with different objects. To accomplish different tasks.

    As to refactoring, I doubt you would every refactor your camera manager if your interface is thought out enough. But as long as you stick with the same interface it wouldn't matter how you refactored it internally. Or your save game manager has multiple implementations for each platform. The interface staying consistent is the important thing. After that you can refactor inside all you want.

    The rest then is back to your original question how do you get these services in the first place. But like everyone said its an important topic but at the same time not. I would say just be consistent.
     
  24. makeshiftwings

    makeshiftwings

    Joined:
    May 28, 2011
    Posts:
    3,350
    To your original question: Yes, a monolithic GameManager class is absolutely the worst way to architect anything. :p

    I love Dependency Injection and would definitely not want to go back after using it for a while. I use Zenject ( https://github.com/modesttree/Zenject )

    I think most people who say "Sure, X is good in the business world, but in game development, it's totally different and you can do whatever you want" are kind of missing the entire point of software architecture. Sure, you CAN do whatever you want and have a terrible architecture if you're working alone or at a game company where your boss knows nothing about programming and you are underpaid and overworked and need to just get something done as fast as possible, but that has nothing to do with "games" vs "business", it has to do with "bad working conditions" vs "better working conditions". Any software can benefit from more thoughtful software architecture; games are not some special case.

    They're similar but different. Using Zenject as an example, to me the main differences are:

    1) Dependency Injection can be used in plain classes, not just MonoBehaviours. The more functionality I can move into plain classes the better, because it makes unit testing easier and helps encapsulate classes. I can be fairly certain that the injected dependencies in the constructor of the "Sword" plain class are its only actual dependencies, whereas if it were a MonoBehaviour, it might be making some call out to GetComponent() which means there's a hidden dependency on some entirely different thing and a requirement for a whole Unity scene with the class attached to a working GameObject to be up and running.

    2) DI forces you to fill out the actual dependencies all in one place, a composition layer, so you have a sort of centralized location for what concrete classes are implementing what interfaces, and it's easy to switch those out in different scenes or for tests. It also makes it easy to ensure that a dependency is a singleton or linked to certain specific objects. By contrast, Unity's GetComponent usually appears all over the place in different places. Similarly, dependency declaration is constrained to constructors or initializers, which makes it easy to see what depends on what, whereas GetComponent might be anywhere. Sure you could make a rule for yourself to only call GetComponent in Start and Awake but having a more rigid system helps encourage it.

    I mean ultimately, like most things in programming, there's no reason you HAVE to use DI and there are a million different ways to accomplish anything, but DI's primary purpose is to force you to think about dependencies and make them more explicit. Is it easier to not worry about it and just pass references around to whatever whenever you need it, or just use singletons for everything? Of course. But that tends to come back and bite you later.
     
    fvde likes this.
  25. makeshiftwings

    makeshiftwings

    Joined:
    May 28, 2011
    Posts:
    3,350
    There are a few different things that will help you decouple:

    1) Dependency Injection. It's great, but you'd need to learn it, and that takes time. Like I mentioned, Zenject is pretty great.

    2) Use parameters instead of singletons. Instead of this:
    Code (csharp):
    1. public void OpenChest() {
    2.   Player.AddItem(itemKey);
    3.   CameraManager.FocusCamera(gameObject);
    4. }
    do this:
    Code (csharp):
    1. public void OpenChest(Character openingCharacter, CameraManager linkedCamera) {
    2.   if (openingCharacter != null) openingCharacter.AddItem(itemKey);
    3.   if (linkedCamera != null) linkedCamera.FocusCamera(gameObject);
    4. }
    Now you've got rid of the singletons and hidden coupling, plus you've added the ability for monsters to open chests or to more easily have multiplayer with different players opening chests.

    3) Use events. Instead of this:
    Code (csharp):
    1. public class LevelUpManager {
    2.   public void LevelUp() {
    3.     DoLevelUpStuff();
    4.     ParticleManager.SpawnGoldParticles();
    5.     Player.DoDanceAnimation();
    6.     MainGui.Write("You leveled up!");
    7.     CameraManager.FlashAndSpin();
    8.   }
    9. }
    do this:
    Code (csharp):
    1. public class LevelUpManager {
    2.   public event Action OnLevelUp;
    3.  
    4.   public void LevelUp() {
    5.     DoLevelUpStuff();
    6.     if (OnLevelUp != null) OnLevelUp();
    7.   }
    8. }
    9.  
    10. // ....
    11. public class ParticleManager {
    12.   public void Initialize(LevelUpManager levelUpMgr) {
    13.     levelUpMgr.OnLevelUp += SpawnGoldParticles;
    14.   }
    15. }
    16.  
    17. public class Player {
    18.   public void Initialize(LevelUpManager levelUpMgr) {
    19.     levelUpMgr.OnLevelUp += DoDanceAnimation;
    20.   }
    21. }
    22.  
    23. //etc
    Now instead of the LevelUpManager needing to know all about the different things that should happen when you level up, it just throws an event saying that you leveled up, and you can have any class you want subscribe to that event and do some cool effects without needing to go back and change the LevelUpManager code to add yet another dependency. You can also now test LevelUpManager by itself without needing to have a ParticleManager, etc all set up and ready to go.
     
    KelsoMRK, Stardog and fvde like this.
  26. Sendatsu_Yoshimitsu

    Sendatsu_Yoshimitsu

    Joined:
    May 19, 2014
    Posts:
    691
    So this is an obvious follow-up, but how does whatever's invoking the methods actually get the parameters they pass in? Running with the treasure chest example, in this setup the character objects, player or otherwise, are the ones who will invoke OpenChest, is the implication that whatever spawns them provides them with references to the camera?

    Trying to use your example in context, I'm looking at my cutscene system since it's by far the most coupled part of the entire project. In pseudocode, a typical invocation looks like this:

    Code (csharp):
    1.  
    2. class CutsceneManager{
    3.  
    4.      void AdvanceCutscene(CutsceneData currentCutscene){
    5.           MenuManager.instance.ShowSpeechBubble(currentCutscene.currentDialogue)
    6.          SpawnManager.SpawnStuff(currentCutscene.itemsOrCharactersToSpawn)
    7.          if (currentCutscene.newCameraInstruction!=null){
    8.              CameraManager.instance.ChangeCamera(newCameraInstruction.newCamera);
    9.              CameraManager.instance.ChangeFocus(newCameraInstruction.newtarget);
    10.         }
    11.      }
    12. }
    13.  
    In theory, this makes sense- by definition a cutscene involves spawning characters/props in the right place, then coordinated between camera, UI, and the AI of spawned characters to show off a pre-created sequence. Would your recommended approach be initializing the cutscene manager with something that can see all the manager classes and manually registers events, such that the camera just has some Event GameObjectMessage FocusObject that other methods are invoking?
     
  27. makeshiftwings

    makeshiftwings

    Joined:
    May 28, 2011
    Posts:
    3,350
    So it's a complex question without one correct answer; it kind of depends on your whole game and how complicated the moving parts are. First the big picture: You can't decouple forever, you can just keep "pushing up" the connections between things until eventually you hit a wall where you have to actually connect everything with everything. With full Dependency Injection, this is called the composition layer, which is some code that essentially fills in all the blanks and connects things.

    So without using strict DI, you need some kind of initializing class (this could be a "GameSetup" class) that handles connecting everything to everything. This class would have references to all the other classes, which on the surface sounds just like the giant GameManager class you're trying to avoid, but the critical difference is that it's only responsible for setting up the dependencies here; it just has a startup method where it connects everything to everything, but it's never called by any other class and it's never really used after that initialization. So they're not "tightly coupled"... the GameSetup class knows about tons of stuff, but nothing knows about the GameSetup class. This is more or less what Dependency Injection does as well, just in a more complicated way. So it might look something like this:

    Code (csharp):
    1. public class GameSetup {
    2.   public void Initialize() {
    3.     SetupPlayer();
    4.     SetupCamera();
    5.     SetupOtherStuff();
    6.   }
    7.  
    8.   private void SetupPlayer() {
    9.     player.Initialize(cameraOne, cameraTwo, playerParticleManager, playerHealthBarGui, playerInventoryManager);
    10.   }
    11. }
    In GameSetup maybe you get all those parameters from dragging public components into the inspector. In Player, those things you pass in initialize will be private class variables, so you use those instead of static singletons. Some of them could be interfaces instead of classes, so you could pass different things in initialization depending on whether you're testing or running the actual full game.

    So as to who makes the actual call to OpenChest, it depends on how your input is handled and things are set up in general. In my game, there's an "Interactable" class for anything you can click on or use, and an "Interactor" interface for anything that can click or use stuff. The player has an Interactor component and when you press the interact button it checks what's under the crosshairs and the Player class calls interactable.Interact(this) to pass itself as the interactor. The chest then knows who opened it and can check if the interactor has an inventory to transfer items into or whatever it needs to do.

    I think in this case, the only thing I would change is to change the singletons into private variables and have an Initialize method that sets them, and call that from some GameSetup class. Mostly just because a "Cutscene" feels like it should have as much control as possible over the order that everything's happening in and the timing; it's going to usually be a strict series of "this then this then this", and it will usually require everything to actually happen in order to work correctly. I'd use events for things that are less strictly ordered, for when one component just needs to know that another component did something. The Level Up is a better example for events because you know you want to spawn some particles, play some sounds, jump around, etc., but the exact order and timing is not particularly important, and leveling up should work fine even if you turn off the particles or the sound isn't connected.
     
    Dave-Carlile likes this.
  28. Sendatsu_Yoshimitsu

    Sendatsu_Yoshimitsu

    Joined:
    May 19, 2014
    Posts:
    691
    That is incredibly helpful, thank you for taking the time to detail your approach to this- I know it's not rocket science, but I find decoupling to be absurdly hard to think around sometimes :)
     
    makeshiftwings likes this.