Visitor: How I Mastered the Toughest Programming Pattern

แชร์
ฝัง
  • เผยแพร่เมื่อ 29 มิ.ย. 2024
  • Unity C# Architecture: This is how I finally understood the Visitor Programming Pattern - Building a Power Up System in Unity.
    🔔 Subscribe for more Unity Tutorials / @git-amend
    #unity3d #gamedev #indiedev
    ▬ Contents of this video ▬▬▬▬▬▬▬▬▬▬
    0:00 Classic Visitor
    5:58 Intrusive Visitor
    7:13 Reflective Visitor
    9:43 GetOrAdd Extension Method
    Extension Methods and Utils
    github.com/adammyhre/3D-Platf...
    Assets Shown In This Video (Affiliate Links)
    Odin: assetstore.unity.com/publishe...
    Dungeon Mason Tiny Hero Duo: (FREE): assetstore.unity.com/packages...
    Chromisu: Handpainted Forest MEGA Pack assetstore.unity.com/packages...
    SineVFX: Better Crystals assetstore.unity.com/packages...
    VFX Trees: assetstore.unity.com/packages...
    Kronnect Cloud Shadows: assetstore.unity.com/packages...
    Kronnect Beautify: assetstore.unity.com/packages...
    Kyeoms Hyper Casual FX 2: assetstore.unity.com/packages...
    MalberS Animations: Forest Golems: assetstore.unity.com/packages...
    Follow me!
    linktr.ee/gitamend
  • เกม

ความคิดเห็น • 89

  • @git-amend
    @git-amend  8 หลายเดือนก่อน +10

    Hi Everyone! Hope you find this explanation of the Visitor pattern useful - it can help to implement your own Visitor and try it out; you'll see it's not too much code!
    Links in description to Extension methods and more!

  • @dan.ec90
    @dan.ec90 8 หลายเดือนก่อน +23

    Thank you so much for making this channel!
    I've been developing in Unity for a decade now and after discovering git-amend I found out I was thirsty for more advanced tutorials and I didn't even know.
    Please, never stop!

    • @git-amend
      @git-amend  8 หลายเดือนก่อน +3

      Thank you, I really appreciate that! Lots more topics to come!

  • @cit0110
    @cit0110 2 หลายเดือนก่อน +1

    learned the visitor design pattern in my compilers class, i couldnt wrap my head around it until we finally got to the code gen part. my dopamine spiked so high when i realized what was going on and was able to implement it haha

    • @git-amend
      @git-amend  2 หลายเดือนก่อน

      Haha nice!

  • @GettingRektGaming
    @GettingRektGaming 8 หลายเดือนก่อน +14

    This channel is so good. I always run into issues when it comes to "mutators" and buffs/debuffs systems. Thats when my technical debt either starts, or comes to collect because of my previous sins.

    • @git-amend
      @git-amend  8 หลายเดือนก่อน +1

      Haha I know the feeling!

  • @trustytea3136
    @trustytea3136 8 หลายเดือนก่อน +9

    I was recommend to this TH-cam channel and I’ve got to say, you’ve quickly become one of my favorite coding TH-cam channels to watch, great stuff!

    • @git-amend
      @git-amend  8 หลายเดือนก่อน +1

      Wow, thanks! I'm really happy to hear that!

  • @feildpaint
    @feildpaint 7 หลายเดือนก่อน +1

    Wow that null coalescing operator tidbit at the end was super helpful! Just started using them on a new project because they looked fancy haha. Thanks for the great video!

    • @git-amend
      @git-amend  7 หลายเดือนก่อน

      Glad it helped!

  • @BooleanIndecisive
    @BooleanIndecisive 8 หลายเดือนก่อน +3

    Nice walkthrough showing how to apply a general pattern to Unity!

    • @git-amend
      @git-amend  8 หลายเดือนก่อน

      Thank you!

  • @Sindrijo
    @Sindrijo 8 หลายเดือนก่อน +15

    To avoid reflection you could just add a type parameter to the visitor interface method:
    public interface IVisitor
    {
    void Visit(T visitable) where T : Component, IVisitable
    }
    public interface IVisitable
    {
    void Accept(IVisitor);
    }
    public MyComponent : MonoBehaviour, IVisitable {
    public void Accept(IVisitor visitor)
    {
    visitor.Visit(this); // Type param is inferred to be MyComponent
    }
    }
    public MyVisitor : MonoBehaviour, IVisitor {
    public void Visit(T visitable) where T : Component
    {
    // use pattern matching etc.
    if(visitable is MyComponent myComponent)
    {
    // do stuff
    }
    }
    }
    Now you could actually also create a concrete interface that visits only a specific type of component, and then register those visitor on an aggregating visitor.
    public interface IVisitor where T : Component, IVisitable
    {
    void Visit(T component);
    }
    public MyAdvancedVisitor : MonoBehaviour, IVisitor {
    private Dictionary visitors = new Dictionary();
    public void RegisterVisitor(IVisitor visitor)
    {
    visitors[typeof(T)] = visitor;
    }
    public void Visit(T visitable) where T : Component
    {
    // this is a bit naive, you could easily add support for inheritance
    // e.g. a IVisitor should be callable with a DerivedType
    if(!visitors.TryGetValue(typeof(T), out var boxedVisitor)
    {
    return;
    }
    if(!boxedVisitor is IVisitor concreteVisitor)
    {
    return; // or throw or log error whatever
    }
    concreteVisitor.Visit(visitable);
    }
    }

    • @git-amend
      @git-amend  8 หลายเดือนก่อน +2

      I like the generic interface approach as well. Thanks for the detailed breakdown!

    • @PetrVejchoda
      @PetrVejchoda 7 หลายเดือนก่อน

      Yeah, the reflection scared me. Generics is much better approach.

    • @KhanhLe-in7yi
      @KhanhLe-in7yi 6 หลายเดือนก่อน

      RegisterVisitor(IVisitor visitor) where T : Component, IVisitable how to use this

    • @bxb5625
      @bxb5625 2 หลายเดือนก่อน

      Looks nice, but I would have tried smthg like IVisitor where T IVisitable. This would allow you to combine multiple IVisitor like MyComponent : Monobehaviour, IVisitor, IVisitor .
      Making maybe clearer what visitable your visitor target. Only thing is how would it behave if we had both component on our object, I m thinking about this at 10:30 pm in bed XD, I might say not useable things 😭

    • @r1pfake521
      @r1pfake521 6 วันที่ผ่านมา

      Now you defeated one of the main purpose of the visitor pattern in modern programming languages, that the uploader also missed, compile time error checks if you add a new visitable type later and forgot to add visitor implementation for the new type. With your implementation you would get a runtime error at best.

  • @Krahkarq
    @Krahkarq 7 หลายเดือนก่อน

    Great video & very happy I've found your channel!

    • @git-amend
      @git-amend  7 หลายเดือนก่อน

      Awesome! Thanks!

  • @ragerungames
    @ragerungames 8 หลายเดือนก่อน

    I love your channel man! Newly discovered! :)

    • @git-amend
      @git-amend  8 หลายเดือนก่อน +1

      Right on! Glad to hear it!

  • @user-cx4ex4ds9t
    @user-cx4ex4ds9t 4 หลายเดือนก่อน

    God bless your channel! One of the best I'v seen on TH-cam so far. Please keep making videos like this one, they're really helpful

    • @git-amend
      @git-amend  4 หลายเดือนก่อน

      Thank you! Will do!

  • @Scott_Stone
    @Scott_Stone 7 หลายเดือนก่อน +1

    This video has been on my recommendation for a week. Before the algorithm changes, I only saw such things only with the biggest channels.

    • @git-amend
      @git-amend  7 หลายเดือนก่อน +1

      Interesting... well I'm glad to hear that, hope you like the video!

  • @FidosWideWorld
    @FidosWideWorld 7 หลายเดือนก่อน

    Brain got bigger, stuff has been learned, you've earned a subscriber. Keep chadding ö7

    • @git-amend
      @git-amend  7 หลายเดือนก่อน

      Haha thanks!

  • @quixadhal
    @quixadhal 7 หลายเดือนก่อน +3

    It's funny how part of "object oriented" programming seems to be giving formal names to things we just used to DO in the old days. This is effectively making local dispatch tables with function pointers and varargs argument lists... but fancied up so it's pretty and the compiler can do a little bit more checking that you're using it correctly.

  • @deathtidegame
    @deathtidegame 8 หลายเดือนก่อน +1

    I never knew this was a pattern or that it had a specific name when I implemented a buff/debuff system for my game using classes that "attach" themselves to objects (such as a health system or movement system) and interact with them (reduce max health, increase/decrease move speed etc).

    • @git-amend
      @git-amend  8 หลายเดือนก่อน

      Nice! That’s awesome!

  • @marmont8005
    @marmont8005 หลายเดือนก่อน

    I was just thinking about the best way to incorporate collision logic into my game while minimizing the coupling between game objects.
    I'm currently using a collision manager that only checks if the game objects are touching.
    I came across the Visitor pattern by chance and discovered your channel.
    I am in the process of implementing it and it sounds very interesting.

    • @marmont8005
      @marmont8005 หลายเดือนก่อน

      but I just realized that with a visitorpattern you have a higher cohesion between the individual game objects, because to handle collisions you still need the class of the object to handle this. My conventional approach is to use a centralized collision manager to reduce the coupling between game objects.

    • @git-amend
      @git-amend  หลายเดือนก่อน

      The visitor is really more about performing some logic on the object being visited than it is about collision detection. In many cases that does mean they might collide before the visitor 'Visits' the visitable object, and sometimes they would need to know what class is being visited so that the correct operation can be performed. But if you work with abstractions, this can be minimized. It really comes down to the design of your game. In future videos we use the visitor pattern more frequently, so you might get some other ideas. Glad you found the channel!

  • @cheesymcnuggets
    @cheesymcnuggets 7 หลายเดือนก่อน

    Wow i really need to do more research on general programing, all this just went through one ear and out the other without me processing any of it. I've managed just fine only worrying about what I need to know but I think it would be worth knowing more than I need to if I want to a smoother experience.

    • @git-amend
      @git-amend  7 หลายเดือนก่อน

      Good to hear! There is always more to learn!

  • @crazyfox55
    @crazyfox55 8 หลายเดือนก่อน +4

    The reflection visitor is a really interesting idea that I have not come across yet. However, I think to avoid writing lots of visit functions it would be better to use a base visitor class that has empty functions for each visitable. The visitor interface will still need to grow as more visitable objects are added.
    I'm glad to see you're posting about this pattern, everyone on my team was fearful of this pattern and stayed out of the codebase where it was heavily used. But I had to dive into it to add features and fix bugs, it was very challenging at the beginning. I think if you went into more detail about how to debug this pattern that would be great. I think navigating the indirection within the IDE was my big breakthrough.

    • @git-amend
      @git-amend  8 หลายเดือนก่อน +3

      The base class is a great solution. Another option I've been thinking about would be to keep a base class as you mention, but instead of one interface, the base class could implement several smaller interfaces instead. As long as there was a clear separation of concerns, that might work well. I'll keep debugging in mind, I'm sure I'll be using the Visitor in other videos in the future. Thanks for your comment!

  • @LimitedInput
    @LimitedInput 8 หลายเดือนก่อน

    Amazing

    • @git-amend
      @git-amend  8 หลายเดือนก่อน

      Thank you! Cheers!

  • @sahilmishra2945
    @sahilmishra2945 8 หลายเดือนก่อน +1

    that null coalescing bit is cool to hear about, these videos are great do you get that examples from a textbook?

    • @git-amend
      @git-amend  8 หลายเดือนก่อน +1

      Thanks! I don't recall exactly where I first saw the Classic Visitor used in this way, but I think it was in a book - I'll try to remember, it's been a while. I think the PowerUp is a common paradigm for learning the Visitor. The Reflective and Intrusive versions of Visitor come from enterprise software engineering.

  • @Director414
    @Director414 7 หลายเดือนก่อน +2

    Great video!☺️ Many thanks for sharing.
    I dont really get the basic premise i guess, what problem are we trying to solve here? How does a scrappy, noobish-way, of doing this look like? I just wanna wrap my head around the actual problem we are solving..
    Cheers! ☺️

    • @git-amend
      @git-amend  7 หลายเดือนก่อน +1

      The Visitor pattern solves the problem of managing interactions between game elements in a scalable way. Without this pattern, you might directly hardcode interactions in each object's script, which becomes messy and hard to manage as your game grows. The Visitor pattern abstracts these interactions: pickups 'visit' objects, triggering appropriate responses (like health increase) defined by the object.
      A noobish version of this might look something like:
      public class Hero : MonoBehaviour {
      public healthComponent;
      public manaComponent;
      ...
      void OnTriggerEnter(Collider other) {
      if (other.gameObject.CompareTag("HealthPickup")) {
      health.addHealth(20);
      Destroy(other.gameObject); // Remove the pickup object
      }
      if (other.gameObject.CompareTag("ManaPickup")) {
      mana.addMana(20);
      Destroy(other.gameObject);
      }
      ... and so on
      }
      }
      Here is another good resouce which describes the Vistor pattern in a more abstract way:
      refactoring.guru/design-patterns/visitor

  • @friedcrumpets
    @friedcrumpets 8 หลายเดือนก่อน +1

    WOW; you need more views

    • @git-amend
      @git-amend  8 หลายเดือนก่อน

      Thanks!

  • @Jose-kr1li
    @Jose-kr1li 7 หลายเดือนก่อน

    Hello, I have a doubt with this specific implementation. When the PuckUp class detects a collision, it takes the first IVisitable component that the object that is colliding with has. In this case is the class Hero, but in the same gameObject we have two more components that extents from IVisitable (HealthComponent and ManaComponent), so we would have to take care of the order of the components if we wanted to take Hero in order to visit both HealtComponent and ManaComponent right?

    • @git-amend
      @git-amend  7 หลายเดือนก่อน +1

      Good question. The order of components retrieved when using GetComponents is always the order they are in the inspector, so in this example Hero will always be the first one retrieved. You could strictly enforce that by introducing another interface or putting the components onto children if you wanted to.

  • @Cloud-Yo
    @Cloud-Yo 4 หลายเดือนก่อน

    Idk, this one was fairly easy for me to get. Credit is obviously due to the quality of the explanation on your video. I think of it as a modular way of adding methods to a Visitable class and its sort of what I was looking for :) Unrelated question: what package (if any) are you using to display the related code in your console when displaying the debug statements. seems useful.

    • @git-amend
      @git-amend  4 หลายเดือนก่อน +1

      Thanks for the comment - I think the asset you are looking for is Editor Console Pro, I've been using it for years now, it's great!
      assetstore.unity.com/packages/tools/utilities/editor-console-pro-11889

    • @Cloud-Yo
      @Cloud-Yo 4 หลายเดือนก่อน

      @@git-amend Awesome, thanks! turns out I had it in my assets already. Must have snuck in on one of the Humble Bundles I bought 👍

  • @pierredalaya444
    @pierredalaya444 8 หลายเดือนก่อน +2

    I'm confused, could you explain what is good about this pattern ? In the classic and intrusive, you create dependencies between your scriptable objects and your components, and in the reflective, well; it uses reflection. I am failing to see any benefits to this one. Would be great to have more insight about pros

    • @git-amend
      @git-amend  8 หลายเดือนก่อน +2

      The Visitor pattern, by its nature, will introduce some level of dependency between the Visitor and the object being Visited. However, the separation of concerns and the ability to add new operations without modifying existing code can outweigh this disadvantage in many scenarios.
      Separation of Concerns: The Visitor pattern helps to separate concerns by keeping the operations separate from the component.
      Adding New Behavior: It makes it easier to add new behaviors to existing object structures without modifying the objects themselves.
      Single Responsibility Principle: Each visitor is typically responsible for a single operation.

    • @pierredalaya444
      @pierredalaya444 7 หลายเดือนก่อน

      I see, thanks for clarifying. I would need to try it myself, but I am not a fan of adding a new type for a simple ability that just modifies an int. Note: you can get the same benefits without the dependencies by using SO architecture. @@git-amend

  • @EskemaGames
    @EskemaGames 7 หลายเดือนก่อน +1

    Not bad implementation, but too coupled and uses reflection, which you don't need at all. You already have all the code in there to make it useful, you need more abstraction to make the pattern shine

  • @malawann
    @malawann 7 หลายเดือนก่อน +1

    Can you explain how to support the removal of buffs in this system? Which means to restore the health value to the state before the buff was added.

    • @git-amend
      @git-amend  7 หลายเดือนก่อน +1

      Removable or temporary buffs is a bit too complex to describe in comments, but is a good subject for a future video. There are many ways you could accomplish this - one idea might be to allow the Visitor to add a Decorator which affects a particular component and has a limited lifespan. If you just wanted to restore the state of a component to a previous state, check out my video on the Memento pattern.

    • @malawann
      @malawann 7 หลายเดือนก่อน

      @@git-amendThanks for answering. Can't wait for the future video.

  • @StephenBuergler
    @StephenBuergler 8 หลายเดือนก่อน +2

    As a java dev there were always three C# features that I thought were really nice.. linq query syntax, structs, and dynamic variables. I thought that C# people never really needed to use the visitor pattern because they can use dynamic variables instead. Is that true?

    • @git-amend
      @git-amend  8 หลายเดือนก่อน +4

      Yes, I know what you mean - and it would be great if we could use dynamic. However, the dynamic keyword in C# is not supported by Unity's IL2CPP scripting backend because it requires Just-In-Time (JIT) compilation, which IL2CPP, being an Ahead-Of-Time (AOT) compiler, doesn't allow.
      Check out: docs.unity3d.com/2023.3/Documentation/Manual/ScriptingRestrictions.html

  • @niuage
    @niuage 7 หลายเดือนก่อน

    This channel is criminally underrated…

    • @git-amend
      @git-amend  7 หลายเดือนก่อน

      Starting to grow a bit now!

    • @niuage
      @niuage 7 หลายเดือนก่อน

      @@git-amend Cool :) Yea I bet if you keep it up it's only gonna go up faster and faster :)

  • @melpeslier
    @melpeslier 7 หลายเดือนก่อน

    Maybe you could have explained the visitor pattern and what you were diving in before

  • @evggg
    @evggg 5 หลายเดือนก่อน

    What if Pickup object in OnTriggerEnter receives another component with IVisitable interface. Not our Hero, but HealthComponent directly?

    • @git-amend
      @git-amend  5 หลายเดือนก่อน +1

      Good question. The order of components retrieved when using GetComponents is always the order they are in the inspector, so in this example Hero will always be the first one retrieved. You could strictly enforce that by introducing another interface or putting the components onto children if you wanted to. If you had it setup so the HealthComponent is the one retrieved, it will only visit that component.

  • @ethanwasme4307
    @ethanwasme4307 8 หลายเดือนก่อน +18

    unity devs are spoilt 😢😢

  • @nanaschi
    @nanaschi 8 หลายเดือนก่อน

    Maybe next time MVP/ MVC patterns could be explained? It would be great!

    • @git-amend
      @git-amend  8 หลายเดือนก่อน

      Great idea, I'll put that on the to-do list. I'm actually building a new feature using MVC at work this week.

    • @nanaschi
      @nanaschi 8 หลายเดือนก่อน

      @@git-amend Cool to hear it coincided with your current workflow. From my perspective, I'd like to see examples of real game scenarios of using these patterns rather than just UI IMHO

    • @clamhammer2463
      @clamhammer2463 7 หลายเดือนก่อน

      @@nanaschi The mvc pattern would be overkill for just a UI. that is only 1 component of the 3 withing MVC, v=view

  • @user-ju3vb6xc4u
    @user-ju3vb6xc4u 2 หลายเดือนก่อน

    I love your channel I just don't understand what problem the Visitor pattern is trying to solve.

    • @git-amend
      @git-amend  2 หลายเดือนก่อน +1

      The main purpose of the Visitor is extending the functionality of a class hierarchy without modifying the classes themselves. Maybe checkout my more recent video about Stats and Modifiers for another example.

  • @captainnoyaux
    @captainnoyaux 7 หลายเดือนก่อน

    what do you use for the voice ? Seems like AI but it sounds OK

    • @git-amend
      @git-amend  7 หลายเดือนก่อน

      That's my own voice, I don't use a voiceover.

    • @captainnoyaux
      @captainnoyaux 7 หลายเดือนก่อน

      @@git-amend whatttt ? XD it sounds like AI to me, my bad !

  • @ranejeb
    @ranejeb 7 หลายเดือนก่อน +1

    What's the reasoning behind using reflective visitor? I can imagine some real commercial projects to use visitor that would have logic complex enough that you wouldn't really want to have one class to contain all Visit(HealthComponent), Visit(ManaComponent), etc. Why can't we use generic types instead to both 1) get read of slow reflection 2) handle one particular case of visiting at a time in a separate class

    • @git-amend
      @git-amend  7 หลายเดือนก่อน +2

      The flexibility of a reflective visitor is useful in systems where new component types might be added frequently or where the range of component types is extensive. However, if performance is critical and the types you're dealing with are known and relatively stable, using generic types might be the better option. The choice between these two approaches often boils down to a trade-off between flexibility and performance.

    • @r1pfake521
      @r1pfake521 6 วันที่ผ่านมา

      The "reflectiv visitor" implementation defeats the whole purpose of the visitor pattern and it isn't even a real visitor pattern implementation anymore. In a real commercial project the visitor pattern is use if you want ALL the logic in one visitor class, you want to implement all these visit methods for every type in one visitor class, that's the only reason to use the pattern. His example shows a wrong use case of the pattern which lead him to belive that the "reflectiv visitor" was a good idea, but it isn't.

    • @r1pfake521
      @r1pfake521 6 วันที่ผ่านมา

      Btw the visitor pattern is use very rarely, not because it is too complex or anything but because modern programming languages have so many features like pattern matching and other stuff that you rarely need a actual visitor pattern aynmore.

  • @lucasrgoes
    @lucasrgoes 8 หลายเดือนก่อน +4

    You could simplify the reflection by just doing a switch on the type:
    public void Visit(IVisitable visitable)
    {
    switch (visitable)
    {
    case HealthComponent health:
    Visit(health);
    break;
    case ManaComponent mana:
    Visit(mana);
    break;
    }
    }

    • @r1pfake521
      @r1pfake521 6 วันที่ผ่านมา +1

      His example is bad and the reflection visitor isn't even a real visitor pattern implementation. Your example shows a use case for the real visitor pattern. So you can do a switch with the type checks, but what if you create a new component and forgot to add a case and visit method for it? There would be no compiler error or anything it would be skipped or you get a runtime error, depending on how you implement your switch. But with the correct implementation of the visitor pattern (a new visit method per type) you are forced to implement both, the correct visit method and to call it in the accept method of the newly added type, otherwise you would get a compile time error, this is the only benefit of using a (real) visitor pattern instead of a swich based type check. This is also a reason why it is rarely used anymore, because in most cases a switch type check, like yours, is "good enough" for 99% cases.

  • @civo4457
    @civo4457 7 หลายเดือนก่อน

    a visitor...

  • @JacobNax
    @JacobNax 7 หลายเดือนก่อน

    Toughest?? Have you tried DOTS my friend?

  • @Gurem
    @Gurem 7 หลายเดือนก่อน

    isnt this just a Interactible system. Thank god its not something that'll have me refactoring
    Edit: spoke too soon.

  • @Sweenus987
    @Sweenus987 7 หลายเดือนก่อน

    Interesting pattern but feels a little dirty for some reason