Making a Dialog & Localization System in Godot

Поделиться
HTML-код
  • Опубликовано: 27 дек 2024

Комментарии • 92

  • @jembawls
    @jembawls  Год назад +8

    To try everything Brilliant has to offer-free-for a full 30 days, visit brilliant.org/JaceVarlet . The first 200 of you will get 20% off Brilliant’s annual premium subscription.

  • @redundantpancake
    @redundantpancake Год назад +27

    Dialogue history is such a must feature for any game that has a lot of dialogue and has puzzles/interactions based on those past dialogues. I wish my 90s adventure games had it xD

  • @haliosold
    @haliosold Год назад +14

    Really cool video! As a programmer/CS student who had only very little contact with programming games (and even less with localization), this video had really nice pacing and depth. It was interesting, well structured and (for me) very understandable, both why and how you did things - even without knowing anything about the engine. I of course can't speak for people who don't have any knowledge about programming, but I feel like the different levels of explaining, showing demos, file structures etc. really had something for everyone to understand without it feeling either repetitive or too complex. So really outstanding work on that video! Also happy to see you getting some sponsorship and looking forward to more videos like this one!

    • @jembawls
      @jembawls  Год назад +4

      thank you! I'm glad you enjoyed the depth and pacing. Worked pretty hard on that!

  • @mmseng2
    @mmseng2 Год назад +10

    I've always wondered how localization was generally implemented. Definitely interesting to get a peek under the hood of some popular localization components and a walkthrough. Thanks Jace, helps a POT.

  • @whilefree
    @whilefree Год назад +5

    Smart system. Awesome! And congratulations for your first sponsorship.

  • @NeonDripKitty
    @NeonDripKitty Год назад +1

    that was a very smooth sponser plug at 17:00, I think it works so well cos its a subject/topic you would bring up naturally even without a sponsor

  • @k4gi
    @k4gi Год назад +4

    it's very cool that you've got the dialog system working with most of the features you wanted :) thanks for talking us through it
    i worry a little that the code for showing dialog is separated from where you're writing dialog. and is that gonna be hard to manage in the future when there's a lot more of it?
    i dunno. i've been trying to build a dialog system for a fair while, and i can't get it straight in my head how things should best be organised. your solution is at least easy to understand!

    • @jembawls
      @jembawls  Год назад

      The decision to separate the dialog data and the dialog functionality was a very deliberate one as I don't really want dialog in my code. I also considered a system that could just output the dialog as they are ordered in the container which would mean I wouldn't need to look up and push indices. The downside with that is that not all dialog is sequential. However, I did create a PUSH_DIALOG_RANGE function (can't remember if i briefly mentioned it in the video or not) which can easily push a range of uninterrupted dialog that was ordered in the container.
      As for what will happen when there is more dialog: that is where the containers come in! Since the containers will only have a very small subset of dialog that I will be working with, it makes it really easy to find the relevant dialog and implement it.

  • @SatisfactoryNews
    @SatisfactoryNews Год назад +17

    A RUclips sponsorship and a Twitch sponsorship all in one week! Congrats Jace, that's huge! Fascinating video as well, thanks for the insight into how the game works.

  • @ben.mcintosh
    @ben.mcintosh Год назад +5

    Great video and congrats on the sponsorship! I realized early on when writing a game that dialog gets out of control pretty quickly especially with many branches and conditions. I ended up writing my own dialog design app (Chat Mapper) but then realized that this would be a full time job by itself and got burnt out. 😅 It’s really interesting to see your thought process and I will be following your progress! Also a huge fan of C# and I try to use it on projects whenever I can.

  • @fryingpanjoe
    @fryingpanjoe 4 месяца назад +1

    Really enjoy your content, keep up the good work! Stumbled upon this video in search for inspiration for how to organize my own dialog system.

  • @SkoobasArt
    @SkoobasArt Год назад +2

    Really appreciating all the time and attention you put into these videos and explaining everything!

  • @owenlloyd2528
    @owenlloyd2528 11 месяцев назад +2

    Thank you. This was extremely interesting and very useful in the conceptual and, uuum, ?system analysis/design? aspects. I am not a professional programmer, just a self-teaching hobbyist so I found your coverage of, and suggestions about, the broader aspects (even things like getting the localisation in place up front) very helpful. Thanks again.

  • @markcollins3856
    @markcollins3856 Год назад +1

    Undertale is really good example! Sans slowly losing with you is so eerie

  • @P-NoyBoy
    @P-NoyBoy Год назад +2

    I love the insider knowledge on your takes and reasoning behind what you're doing with your game. I've seen how difficult translations can be from fans trying to translate games and it's taken several years. Glad to see this being thought of so early.
    Also, happy to see the sponsor, congrats :)

  • @madalee_com
    @madalee_com Год назад +1

    Excellent work with this! It's great that you put it in a plugin, having the gui interfaces added in to the Godot editor is perfect for the tool dev process that frees you to be creative ❤

  • @MCNeko6554
    @MCNeko6554 Год назад +1

    I love the condensed overview of your Twitch stream progress. Unfortunately I can't make some of your streams and fall behind on the VODs so it's nice to get a summary or check-in. I hope to be able to make my own plugins as well one day, so this video has been very inspirational!
    I'm currently working on my own game and I have been terrified of the dialogue process since I'm not the best programmer and I'm also new to game development, animation, art, etc. The project is turning out to be a monumental task, but I've done a lot of learning and it's getting smoother each day. I've recently figured out exactly how I want to do my animations and kind of settled on an art style as well. I think it's finally time to tackle dialogue...

    • @jembawls
      @jembawls  Год назад +1

      Best of luck! I'm rooting for you!

  • @obake6290
    @obake6290 Год назад +1

    This was interesting and engaging while also being fairly easy to understand. I think you did a great job explaining what you've been working on and how it works.
    I do have some rudimentary understanding of C++, but I wouldn't call myself a programmer at all.

  • @funjobie
    @funjobie Год назад +2

    very interesting!
    three points come to mind:
    since you showed an PO editor - will the translators get to see the portrait? for example if you choose irony/jesting portraits maybe that context gets lost to them unless you describe it in more words.
    second point: how long did it take to setup? feels like a 200h task. i'm constantly underestimating the stuff i want to implement so would be curious.
    and last: maybe you also need a system for non-modal dialogs? e.g. vendors screaming without stopping your movement / chase sequences / etc

    • @jembawls
      @jembawls  Год назад +1

      First point: No, actually! And this is a great point and a big oversight on my part - thank you! It would be great to embed some information regarding that. Perhaps I can hook the portrait/emote name into the translator comment that might help a bunch.
      Second point: The system itself took a couple weeks worth of work to design and set up but i was also learning the engine at the same time so a lot of time was spent lost on that. The plugin was about a week or so to learn and implement.
      Third point: non-modal is definitely something that is also on the to do list and i also forgot to mention in the video. I'm still undecided if I want all dialog to be in a big, traditional dialog box or floating over people's heads. But if I go with the traditional box, I would still like some overhead stuff for emotes or short phrases that don't interrupt the game flow.
      Thanks for the feedback and also great questions!

  • @pfqniet
    @pfqniet Год назад +3

    I would definitely suggest giving your dialogue entries names/identifiers, rather than referring to them by number. This decouples them from "whatever order you happened to create them in" and would allow you to rearrange them without needing to worry about indexes!

    • @jembawls
      @jembawls  Год назад +2

      I considered this too but I feel that it just moves the goal posts around. Right now the indices don't *have* to be in order so re-ordering isn't completely necessary and can handle last minute additions by just appending them at the end of the container but add them earlier in the code. For blocks of dialog (not interrupted by pauses or actions) I can use PUSH_DIALOG_RANGE to push a range of dialog regardless of order and so inserting new dialog and extending the range works great here.
      Using entry names would involve coming up with meaningful names, spelling them correctly, and if the content of the dialog changes then i might need to change the name and then change the code anyway. So names does solve the re-ordering issue, but currently I'm not seeing how it would meaningfully improve the workflow overall.
      Thanks for the feedback!

    • @pfqniet
      @pfqniet Год назад

      @@jembawls Fair enough! There are pros and cons to both. In particular my own system doesn't work too well with adding more lines of dialogue into a cutscene block - it certainly works but not as easily as yours. Your video has given me some ideas!

    • @tacklemcclean
      @tacklemcclean Год назад

      I guess one benefit would be to easily see the dialog contents directly in the editor, if there were meaningful names. Could be useful when you a couple of months later edit the files.
      Could a plugin give annotations in the editor? Show the actual content. Would be really cool.

  • @rremnar
    @rremnar Год назад

    Creating a que for a system I found is necessary sometimes. In my game I have a roll up scoring system. I had to do a bit of debugging to find out why it never counted accurately when it scores really fast. My solution was to put the scores into a que (in my case an array), and let a timer handle the displaying of those scores, using a tween and a function. That way the scoring can occur even when the roll up display is still going with a previous score.

  • @hominoidea6838
    @hominoidea6838 Год назад +2

    WOOOOO GAME DEV LOG

  • @BenWharf
    @BenWharf Год назад

    Well done on the spon, upwards and onwards Jace :)

  • @luckyknot
    @luckyknot Год назад

    Being able to watch these insights in the code of a skilled Game Dev, and so well explained, it's an amazing learning source for aspiring game devs, thanks a lot for sharing this!

  • @KraylusGames
    @KraylusGames Год назад

    Great video! Localization is much harder to do as an afterthought so building it into the framework early on is smart.

  • @morgan0
    @morgan0 Год назад

    so related to dialog history, i’d love to see some sort of way to attach info to who characters are, past notable events, etc. it’s something i often run into with long news stories with a bunch of people, remembering who tf jones is after no mention for 8 paragraphs.

  • @Genesis8934
    @Genesis8934 Год назад

    On the accessibility part with bigger font sizes, something I've not seen done in games that have heavy dialog is TTS for dialog menus. There's voices now for all kinds of PDA type smart speakers, etc., and we're no longer limited to Microsoft Sam-era TTS.
    If you can't find a free TTS voice pack, maybe crowdfund it to pay a VA to provide a sampling that you can use to train a voice model for your game. Or just have them read out the lines I guess and extend the reusable dialog box to accept a sound string.
    edit: oof, the editor UI gonna need some work. Depending on the size of your game scope, I think you'll have a tedious time with the dialog later. :)
    edit 2: As a programmer, your dialog should probably dynamically load the scene featuring the dialog and populate it from a json or other type of file. Something like,
    bool Dialog.Show(string title, string text, Character source) where "title" and "text" can be key lookups in your localization, and "source" is a simple info struct containing character state.

  • @uninfamous
    @uninfamous Год назад

    I liked the detail presented in the video.

  • @wearwolf2500
    @wearwolf2500 Год назад

    If possible, it would be good if you could have the plugin generate a file for you that contains the container names as constant strings. Then you could use those in your code and have less worries about misspelling something or issues from renaming things. Magic strings are more descriptive than magic numbers but it's still nice to have the compiler check that kind of stuff for you.

  • @amaryllis0
    @amaryllis0 Год назад

    If you want to improve the system further, have you considered using signals? You could simply have one dialog_trigger signal with an attached string parameter, and then you could easily set things up by just writing the string id of an event, like "walked_behind_counter", in your dialog plugin, and then sending the same string id in your code, without having to worry about hardcoding the specific dialogue location
    You could also use something like an enum but this is a bit more flexible, so e.g. you can configure the dialogue first before adding the actual triggering code
    You could also have a dialog_advanced signal that attaches the data of the previous dialogue, which again you could then just configure all that entirely within the plugin

  • @Tsudico
    @Tsudico Год назад

    I noticed that you are doing two separate dialogs for the multi choice options depending on whether the multiple choice includes the amongus or not. That means there are two separate translations for that multiple choice that are duplicated. If there are a lot of places where the actions of the player might affect the number of choices to a dialog but not the dialog itself, then all the duplication might cause issues in the future.

  • @clarfonthey
    @clarfonthey Год назад

    This is a really nice system! The only difference, honestly, between what you made and what a "professional" plugin would look like is probably the actual UI for adding dialogue, but you'll do well with what you've got.
    Honestly the only change I would make is making the dialogue in a container and the multiple choice options have string IDs instead of numbers, since the performance implications are negligible but it basically forces you to write in the code which dialogue option you meant to use. Comments are nice, but forcing you to write what you would put in a comment in the code is nicer.

    • @jembawls
      @jembawls  Год назад +2

      Thanks! Glad you like it!
      As for the string IDs: The short answer is I don’t think I like the workflow of coming up with string IDs every time I create or meaningfully edit a dialog string is appealing to me. It’s true that having the string ID would add more context to the code, but the code is already filled with context (dialog container, function/event/cutscene name, comments) so I’m not sure if the trade away from indices is worth it.
      But my mind is open, and I’ll keep it in consideration as I continue to work on the game in case i can see more value in it.
      Thanks for the comment!

  • @jacoblojewski8729
    @jacoblojewski8729 Год назад +1

    Dunno how much experience you have with l10n, so feel free to ignore if you're good there. You mentioned you have to work on the portion that handles wildcard substitution - whatever system CS uses for this, if possible use named substitution rather than positional - this allows translations to swap the order of wildcards if needed by the language. I while ago I had to go through my code-base (Python) and change all of my translated strings from positional (ex: 'Put the %s in the %s' vs 'Put the {item_name} in the {container_name}').

    • @jembawls
      @jembawls  Год назад +1

      Indeed! The current system does allow for the wildcards to be repositioned when localized.

  • @xXVantXx2928
    @xXVantXx2928 Год назад +1

    Congrats on the sponsor!

  • @theonlyaeasir
    @theonlyaeasir Год назад +1

    I think tracking dialoge by Npc instead of location for your game might make finding things even easier!
    Maybe then add a "scene" subfolder or similar to really be able to tweak and find everything easier should you have a couple thousend lines of code.

    • @jembawls
      @jembawls  Год назад

      This is what I had initially intended actually but very quickly realised when it came time to implement dialog that tracking only by NPC doesn't really make a lot of sense. First of all: which NPC should the chips on the shelf be allocated under? 😂
      Secondly, if you have 2 NPC's talking to each other in 1 cutscene - which NPC should the dialog be under? Their respective text could of course remain under their own NPC containers, but that then means loading 2 containers and trying to find the right dialog in 2 different locations, which may be very tricky because NPC's will have different amounts of dialog and their dialog will be paced differently.
      I find it makes the most sense for the containers to represent ~~whatever makes sense~~. So containers can be a location, an NPC, or a cutscene - whatever makes organising and executing that text easiest.

    • @theonlyaeasir
      @theonlyaeasir Год назад

      @@jembawls You are absolutely right! I did not consider those situations!
      I do have to say from my experience with these kind of "document" Organisations having a good system set-up at the start will save a lot of pain points down the line!
      Maybe you could introduce tags for each NPC to quickly find specific ones. That way you could even tag a single dialogue with multiple tags such as NPC 1, npc 2 and active quest / situation. That way saving them in folders based on location gives you two search angles so to speak.
      But in the end it needs to work for you so as long as it does whatever works works!
      Really like how transparent you are making the whole development process!
      Keep up the good work :)

  • @TheMostUt
    @TheMostUt Год назад

    Not sure if you have plans for it yet, but I think animations (or at least different portraits for different scenarios per NPC) in the portraits during dialog would be nifty difty. While I haven't done any programming in a number of years, I do remember quite a bit of it. I read much better than I write. I like the programming content in the videos. For the dialog timing, I would try to add a new toggle to the dialog builder box: "Requires user interaction". Enabled would be the standard click to go system, disabled would enable a timer box in ms that you can use to program animation speed. Each dialog would take a bit of manual jiggery pokery, from a short message to a long one; as the total message length will affect the on screen time via the timer. Maybe someone else has a better idea for controlling the text animation speed.

  • @GreyHak
    @GreyHak Год назад

    I love the character's hat!!!

  • @Medardusai
    @Medardusai Год назад

    You could additionally implement an feature for dialogue between NPCs and NPCs talking to themselves to make the world seem more alive (or to convey information for quests or lore etc.pp.)

  • @cristiadu
    @cristiadu Год назад

    Add support for custom backgrounds and adjust the scrolling a bit because I feel some dialogues didn't look scrollable at first

  • @kioshiki4519
    @kioshiki4519 Год назад

    Nice work and thanks for sharing this. I do wonder about the interaction classes and conditions for different dialogue. You only had one if statement and a single Boolean value in this first example.
    It would work to have however many conditions in the code in a bunch of if statements, maybe even nested or from a function call.
    But could that get messy to maintain? Have you already thought of how more complicated conditional dialogue might be coded?

  • @scribblingjoe
    @scribblingjoe 5 месяцев назад

    You can eliminate the need to make the word coin plural by associating your coins with a symbol ( like ¢ or $). It's just less information you have to process.

  • @MOSULI
    @MOSULI 7 месяцев назад

    🙏🏾🙏🏾🙏🏾🙏🏾 awesome video!!!

  • @apollolux
    @apollolux Год назад

    Any info on the .po/.pot file formats so that other tools and parsers could be found and/or written to work with those files?

  • @Jearil
    @Jearil Год назад

    I'm curious. If you change a file that already has translations, how does the system handle that? Like say you change the dialog to add an extra sentence in one branch. Did it overwrite the translations with an empty translation file? Can you tell an entry that's changed vs one that hasn't?

    • @jembawls
      @jembawls  Год назад +1

      In this case I can regenerate the .POT file and distribute that to translators. They can then update their .PO files using the .POT file and continue translating. As far as I can see, the edited strings get updated via their context and flagged as needing review in Poedit.

  • @McWolke92
    @McWolke92 Год назад

    have you played CrossCode? your dialog system reminds me a lot of theirs, which is a good thing!

  • @crystalferrai
    @crystalferrai Год назад

    I foresee a problem with your localization setup. It looks like you are using the default English text as the localization key. This means that if you change any default text, such as to fix spelling, grammar or make something bold, you are changing the lookup key. Now all languages need to be updated with new translations even though they most likely will end up with the same text as before. In localizations systems I have worked with, the lookup key and the default text are always two different strings. The lookup key would only get changed in cases where you intend for it to go through translation again. This way you can decide case by case whether the change warrants new translations. I think you will find that more often than not, changes end up being minor and should not impact translations. This is especially true later in the development cycle when bug fixing and polishing.
    You don't want to end up in a situation where minor changes to default text have a major cost associated with them.

  • @Tenkeskap
    @Tenkeskap Год назад

    Hey! I was wondering what you meant by plural support. Does that have to do with how different languages handle plurals differently or something else entirely?

    • @jembawls
      @jembawls  Год назад +1

      So plural support is to swap out translations based on a number you provide.
      So for example: if j have a wildcard string “I have {quantity} banana”. If we have 1, we would show “I have 1 banana” but if I have 2 it would show “I have 2 bananas”. gettext (according to my research) can also has support for language specific pluralisation. Some languages (Japanese, for example) don’t have plurals, and so the Japanese translation file won’t have required entries for plurals. But there are other languages that have multiple forms of plurals depending on the quantity.
      Hope this helps!

    • @Tenkeskap
      @Tenkeskap Год назад

      @@jembawls thank you! Yes, different languages will require different number of strings when there are placeholders standing in for numbers from (typically 1 to 4). Hungarian for example only needs one because although it does have plural forms, it uses singular noun for all "number" + "noun" structures. It's great that your system will be able to handle more complicated scenarios as well as context help for translators. I have seen it happen that even though the system is ok to handle plurals, the info on which string covers which cases is too general or doesn't go all the way to the people who work on the strings. So if the context strings can help with that, this issue won't happen here.

  • @ThisIsFez
    @ThisIsFez Год назад

    it took 10 secs to get hooked lmao

  • @SomeoneBloodyRandom
    @SomeoneBloodyRandom Год назад

    Hmmmmmm,So many flavours of chips. I dunno which to choose!

  • @noodle302
    @noodle302 Год назад

    Hey Jace, how are you finding the C# support in Godot? I use C# in my day job but decided to use GDScript as it looked like C# support was not super polished yet and probably fewer resources online for it

  • @rremnar
    @rremnar Год назад

    I can see the need to create a custom dialog box, that uses a richtext label. I don't remember using richtext except in Godot 3.x and it was kind of jank. I am not sure if that's true anymore, but I guess I'll find out when I need that type of label again. I am not sure how you are designing your dialog system with other languages in mind. One thing that does come to mind is that some characters are double wide, like for Chinese or other symbolic languages. I assume just make the sizing dynamic, and if the dialog doubles in size, you have made room for it, or cut the dialog into smaller pieces to be displayed. Another issue: I only speak English and bad English. How could I possibly test if my system properly formats and displays all languages?
    Edit: IMO, I think translations should be done by the OS. There are exeptions for text in images, but otherwise, why do we need special code for multiple languages?

  • @signpostmarv
    @signpostmarv Год назад +1

    "there are other things in my code that make the context obvious"
    please do a dev stream some time after release where you read your code and see if you can figure out how it works? 😅🤔

  • @Frandibujacosas
    @Frandibujacosas 7 месяцев назад

    Good video. I managed with Godot 4.2.2 to localize 2 languages. In options when you click on Spanish or English you put one language or the other.
    The problem comes when I close the game. It doesn't save the language I selected. could you help me?

  • @captain-syntax
    @captain-syntax Год назад

    Why not used named constants for dialog identifiers instead of auto incrementing numerical IDs? If the IDs change (due to an insert instead of an append to the list), then all of the code needs to change. If they are named constants, those have e very low chance of changing.

    • @jembawls
      @jembawls  Год назад

      The IDs being numerical come with other advantages (such as pushing ranges) while at the same time not actually needing to be ordered (unless pushing ranges). They also don’t require coming up with a named constant every time I create or insert a new dialog string, or when meaningfully editing a string such that it’s constant is no longer contextually accurate.
      The nature of using the containers also means that unordered dialog (or re-ordering dialog) in an event is not very hard to do and won’t have knock on effects elsewhere so it’s not that error prone - EXCEPT when I opt to use location-based containers (where different events may share the same container) in which case I’ll need to be careful, or just split up the events into their own containers in a sub-folder.
      I can maybe see some value for potentially adding an optional tag value to the dialog strings that I can look up and increment from (or something like “add dialog up to a tag”) but I won’t embark on that journey unless I really feel the workflow calling for it.
      But a few people have suggested named constants so I’ll keep it in mind as I work with the system and consider it as a solution if (WHEN! 😂) I run into issues.
      Thanks for your comment!

  • @JaiLeeroy
    @JaiLeeroy Год назад

    Will you be uploading your plug-in to the Godot asset store ( or if it already there, I didn't get a chance to look yet)

    • @jembawls
      @jembawls  Год назад

      The plugin is DEEPLY tied to the rest of my system which I don't think is suitable as a general-purpose dialog/localization system, so I don't think I will unfortunately. Maybe as I work on the game and develop more features for system it might become more worthy of generalising and distributing.

  • @leonhoffs
    @leonhoffs Месяц назад

    Where can I find the plugin?

    • @jembawls
      @jembawls  Месяц назад

      This plugin is not publicly available. It's just my personal tool I use for my game.

  • @ProjectGreenfieldSolutions
    @ProjectGreenfieldSolutions 2 месяца назад

    Jace or Chat,
    Is this plugin available, if so where at?

    • @jembawls
      @jembawls  2 месяца назад +1

      It is not available. It is a plugin I've custom built for my project.

  • @cristiadu
    @cristiadu Год назад

    Did you choose to keep using C# because it's the industry standard and you're more comfortable with it or do you dislike GDScript? (or both?)
    As a web services dev (backend and frontend) I tend to prefer GDScript, but I can see it's more because I'm not a game developer

  • @i_draw_whatever18
    @i_draw_whatever18 6 месяцев назад +1

    *I see the among us reference there*

  • @tuskiomisham
    @tuskiomisham Год назад

    Jace, if I do the uwu :3 translations for the game, would you put it in as a supported language?

  • @g2000411
    @g2000411 Год назад

    Congrats on your first sponsorship! Have you considered other video platforms like Nebula? I would love to actually pay for your videos and support you that way. I also think you videos would fit quite well there.

  • @tlacmen
    @tlacmen Год назад +2

    WHY IS YOUR CODE SCREAMING ALL THE TIME? ;)

    • @jembawls
      @jembawls  Год назад +2

      PUSH_DIALOG( "Because I'm using all caps functions help distinguish global helper functions from regular code");

  • @LORDSofCHAOS333
    @LORDSofCHAOS333 8 месяцев назад

    hi bro

  • @tlpthx
    @tlpthx Год назад

    REALLY ANNOYING background sounds in the intro. I felt like there was something ringing in the next room. Quite anxiety inducing 🫨

    • @MasonzeroDigitalWorks
      @MasonzeroDigitalWorks Год назад +1

      Editor here, I'm sorry you had that reaction :( I even put an audio filter on the background music so that the higher-pitched elements weren't as loud. I'll consider music with less high-pitched melodies in the future. I find it annoying as well, so I try to make it blend as well as possible.

    • @tlpthx
      @tlpthx Год назад

      @@MasonzeroDigitalWorks thanks 🙂. It wasn't so bad once I realized it.