| Switchboard | Dependency Injection - Log Files - Zero-GC Strings

Asset Store | Documentation | GitHub Discussions | Report an Issue

Switchboard is a high-performance logging framework for Unity. The core library provides high-performance, zero-garbage log files to any C# application, not just Unity. No garbage memory is allocated while logging or generating strings. The StringMaker class is a direct replacement for StringBuilder, and can render number values that .NET cannot, with zero garbage memory allocations.

Switchboard also includes dependency injection, built for Unity. It provides a composition root where you can add code that executes when the application starts, regardless of which scenes are loaded. From there you can inject dependencies automatically without reflection. This enables loose coupling, so you can depend on interfaces rather than specific types. It even works when instantiating new objects at run time. You don’t need singletons or certain scenes to load first.

Switchboard is highly compatibile with other projects and plugins. No need for MonoBehaviours to inherit from a certain base class. Disabling domain reload and scene reload is fully supported, so you can enter play mode instantly.

Features

  • High Performance Log Files - Persistent, rolling log files, that have an absolutely minimal impact on performance. Write UTF-8 log files to disc without allocating a single byte of garbage memory! Now you can have log files in your released games without any concern for performance impact. Log files work in any C# project, not just unity, and are thread safe. Compare to other popular logger frameworks, particularly in terms of performance. ILogger interface matches standard .NET practices.
  • Zero Garbage Strings - StringBuilder still allocates garbage memory when you append variables in Unity 2021. StringMaker is a direct replacement that never allocates garbage, and is compatible with TextMesh Pro for zero GC text in Unity. Performance is equal to or greater than the .NET system library, and can render values that .NET is incapable of rendering! Even the Visual Studio debugger is wrong!
  • Dependency Injection - Serves as a complete replacement for singletons and static references. Unlike other DI frameworks, it uses no reflection! Performance is instantaneous. Disabling domain and scene reload is fully supported, so you can press play and test instantly.
  • Composition Root - Create code that lives outside of scenes and runs when you press play, before MonoBehaviours awake.
  • Expandable Object Fields - Object fields, like MonoBehaviours and ScriptableObjects, can be expanded to display their properties in place without having to select the object.
1 Like

I think the asset store link might be wrong, or its not up yet on the store. Searching takes me to your published page of assets but Switchboard is not there.

Thank you! Fixed.

no problem at all :wink: thanks

Edit: All vouchers have been claimed.

If you would like a free copy of Switchboard, just reply to this thread and say that you would like one. I will award 3 total copies to the first three people who respond here, one per person.

2 Likes

Jason Weimann recently posted this video titled “1 Line Fix for your Unity Project.” The one line he is referring to is the [RuntimeInitializeOnLoadMethod] attribute, which allows you to bootstrap your application with start up code as soon as you press play, regardless of which scenes are loaded. However, I can tell you from experience that if you want to create a solid bootstrapping solution to launch from there, you’re going to need to write a little more than one line of code. :wink: Why not let Switchboard do the heavy lifting for you?

2 Likes

I am not great at coding and do this more as a hobby, but this does sound easy to use ( once understood ) and is a good tool for Unity.

1 Like

Great! Thank you for your interest. I have sent you a code to redeem on the asset store. I hope you find it useful. If you have any questions or feedback please just reach out here, on the dedicated forums, or however you prefer and I will be happy to help.

1 Like

If that is still valid, I’d like a copy. Trying to decide between this, Init(args) and other solutions for my next project.

1 Like

Of course, I’ll send the voucher to you in a direct message. There is still one on offer for anyone who is interested.

If you have any questions please don’t hesitate to reach out here via the forums, or open a discussion topic on the Switchboard forum, or any of the other methods listed on the feedback section of the documentation. I’ll be happy to work with you and answer any questions you may have.

I don’t think I can send you a message directly. So, if you wouldn’t mind, just send me a message, and I will send you the voucher.

2 Likes

I’d like to try it if you still got a voucher for it.

You got it! I’ll send you a message.

2 Likes

I’m excited to announce that version 1.1.0 has been submitted to the Asset Store. Hopefully, it should be approved and go live within a few days. The documentation website has already been updated. I spent a lot of time polishing it to make it more user friendly, so please take a look to see what has changed.
https://swipetrack.github.io/switchboard/manual/introduction.html

When installing the update, I advise you to delete the Switchboard directory and perform a fresh install! However, don’t accidentally delete any of your own files you may have put in the Switchboard directory. Generally, you should keep any files you create outside of the Switchboard folder.

Release Notes for Version 1.1.0

  • Simplified the InjectorLocator pattern.

  • Removed the IInjectable interface.

  • Changed the IInjector method signature from bool Inject(out T) to T Get().

  • Changed the InjectorLocator to simply provide an IInjector reference via GetInjector() method, and to no longer allow assigning multiple delegates to an event. Rather, only a single delegate can be assigned or removed. This is to discourage attempts at nesting dependency containers via the functionality of InjectorLocator. If a dependency nesting solution is desired, it should be provided at a higher layer of the protocol with a custom DependencyInjector implementation at the CompositionRoot.

  • InjectorLocator throws exceptions directly, not via helper method to help with inlining, because the exceptions are thrown from the AssignLocatorDelegate and RemoveLocatorDelegate methods, so they occur infrequently.

  • DependencyInjector no longer logs a warning if it fails to provide an instance of a type. That is better left to be handled by the derived DependencyInjector, or the dependent client code.

  • DependencyInjector now just makes IInjector.Get() an abstract method to be overloaded directly in derived classes. This helps make the IInjector implementation less mysterious.

  • DependencyInjector removed InvalidOperationException from Get() method. The expected use case is that the CompositionRoot will always have activated the one DependencyInjector instance using it. Removing the exception can only be beneficial for performance. If inactive, it will not invoke the Get method, but it will not throw an exception either.

  • DependencyInjector now uses a State enum to control Activation() and Deactivation().

  • CompositionRoot avoids the overloaded Unity null equality comparison on the DependencyInjector when deactivating. This allows Deactivation() to still be called, even if the dependency injector asset is deleted from the project while playing.

  • SwitchboardLogger.RootInstance changed to SwitchboardLogger.Root.

  • Changed the name of the ActionIn delegate to ActionWithInput.

  • Changed PlatformStats to PlatformStatsLog, since it’s an odd class and I don’t want to cause name collisions with other assemblies.

  • Made it more obvious that the CompositionRootAsset in the Switchboard settings menu is readonly.

  • Moved the Expandable attribute assembly from the Interface directory to the Unity directory.

  • Moved StringMakerILoggerExtensions to its own assembly.

  • Moved PlatformStats to the LogFileManager assembly.

Reason for Update

Thanks to the inspiration I received from certain interested parties, I thought long and hard about this over the recent holiday. I decided that the InjectorLocator pattern needed to be changed, and it needed to be changed as soon as possible. I have begun recording video of myself starting a new Unity project from scratch, to show off how Switchboard can be used to build an app. But, before I do that, I needed to standardize the main interface once and for all.

Before you read on, if you aren’t interested in the nitty gritty technical details, you don’t need to read further to use Switchboard. You can just skip to the revised documentation. But, thank you to all of you who have shown interest and given me motivation!

I was having some nagging thoughts over the break regarding nested dependency injectors. At first, the InjectorLocator used an event to get registered IInjector instances. I thought that multiple implementations of IInjector could register with the event, nesting themselves to all be passed to the IInjectable MonoBehaviour, in order of registration. However, when thinking through all of the edge cases around nested containers, I decided it was a half baked idea, opening the door for other issues.

If multiple IInjectors nested themselves to the InjectorLocator, a new object would be injected with all of them, but an object that had already been injected would not! Removing a registered IInjector from the event would not notify all dependent objects that the nested IInjector had gone out of scope. Notifying dependent objects of the change to nested IInjector status was obviously a problem. Adding functionality to InjectorLocator is out of the question. The whole point of InjectorLocator is that this Service Locator should be the most lean implementation in the world. I could conceive of an alternate InjectorLocator that allows dependents to register in a way to be notified of changes to nested IInjectors, but it would all just lead to more complexity.

That means I needed to remove the ability to register multiple IInjectors with the InjectorLocator. I am confident that dependency nesting solutions can be created, but they should be developed in a more bespoke fashion at the custom DependencyInjector layer, not at the lower level InjectorLocator. You can create some type of service in your DependencyInjector that is capable of handling nesting and overriding, like when a scene changes, but it can all be done through that service module.

However, I am glad that my first implementation leveraged the power of an event because one very important feature of the InjectorLocator is that the global state of this static class cannot be corrupted by objects other than the IInjector that registered itself. One criticism of using a Service Locator (I think I’ve heard them all by now) is that because it is global, any object could come along and alter the global state in unexpected ways. However, when the global value is a delegate Func that returns the actual desired value, then access can be kept private for the setter. Delegates can point to private methods, and only the object that owns those private methods can create a delegate object instance that points to them. So, the InjectorLocator is able to check whether the request to remove the registered IInjector comes directly from that class or not. With that, the global state criticism is negated. Nothing can change the registered IInjector, except the registered IInjector.

Since there is only one IInjector registered at a time, this also allowed me to simplify the way the InjectorLocator is used from a MonoBehaviour. Now, the MonoBehaviour no longer needs to be IInjectable, and I was able to remove that interface entirely. Now, you just ask for the current IInjector directly from the InjectorLocator, on one line. No need to implement a custom method anymore. You can just add InjectorLocator.GetInjector() to MonoBehaviour.Awake() and that MonoBehaviour will have a loosely coupled IInjector reference to the concrete DependencyInjector assigned to the CompositionRoot in the editor.

Also, I simplified the IInjector interface to just be T Get(). Extension methods can be wrapped around that to get the same method signature I had before (bool Get(out T)) if you really wanted to still use it that way. But, I wanted to solidify extreme simplicity into the InjectorLocator pattern. In addition, I changed the DependencyInjector class to just have an abstract T Get(), so that when you derive your custom DependencyInjector, it’s much more obvious how that concrete class is an implementation of the IInjector interface. Before it was more mysterious how DependencyInjector assets related to the IInjector interface, and the way it was used in Switchboard left too much magic behind the scenes. The original motivation was to have some internal helper methods to make things simpler for you, but I think it only served to make things more convoluted in the end. So, now it’s more straightforward.

I hope I didn’t go overboard with that explanation. It was a bit of a stream of consciousness, but hopefully it all made sense.

public static class InjectorLocator
{
    private static Func<IInjector> LocatorDelegate;

    public static IInjector GetInjector() => LocatorDelegate?.Invoke();

    public static void AssignLocatorDelegate(Func<IInjector> locatorDelegate)
    {
        if(locatorDelegate == null)
            throw new ArgumentNullException(nameof(locatorDelegate));

        if(LocatorDelegate != null)
            throw new InvalidOperationException("The locator delegate is already assigned.");

        LocatorDelegate = locatorDelegate;
    }

    public static void RemoveLocatorDelegate(Func<IInjector> locatorDelegate)
    {
        if(locatorDelegate == null)
            throw new ArgumentNullException(nameof(locatorDelegate));

        if(LocatorDelegate == locatorDelegate)
            LocatorDelegate = null;
    }
}
public interface IInjector
{
    T Get<T>() where T : class;
}
public class ExampleInjector : BasicInjector
{
    // Add properties.

    protected override void Activation()
    {
        base.Activation();

        // Activate your application.
    }

    protected override void Deactivation()
    {
        // Deactivate your application.

        base.Deactivation();
    }

    // Provide dependencies via IInjector.Get<T>().
    public override T Get<T>()
    {
        Type type = typeof(T);

        if(type == typeof(IService))
            return Service as T;

        return base.Get<T>();
    }
}
public class ExampleMonoBehaviour : MonoBehaviour
{
    private ILogger Logger;
    private IService RequiredDependency;

    private void OnEnable()
    {
        IInjector injector = InjectorLocator.GetInjector();

        // Get Logger
        Logger ??= injector?.Get<ILogger>();

        // Get Required Dependency
        RequiredDependency ??= injector?.Get<IService>();
        if(RequiredDependency == null)
        {
            enabled = false;
            Logger?.LogError("A required dependency was not provided!");
            return;
        }
    }

    private void OnDisable()
    {
        // If you release the reference OnDisable, a new instance can be injected OnEnable.
        RequiredDependency = null;
    }

    private void Start()
    {
        Logger?.LogInformation("Hello world! I definitely have my required dependencies!");
    }
}

Thank you to all of the users that have already showed interest in this plugin! You have made all the difference!

1 Like

EDIT: The ExampleMonoBehaviour above is a better example. I will leave this for historical purposes. I’m not sure why I was overcomplicating things, but the example injection code above is more straightforward.

I want to add an update to best practices and the ExampleMonoBehaviour sample code. At first, my examples just called InjectorLocator.GetInjector() from the Awake method, and threw an exception if a required depedency like ILogger was not injected. However, I think it’s actually better to inject from OnEnable, and not throw exceptions. I want to explain the logic behind that, and break down the particular way I’ve coded it in the new examples. The new example code has already been updated directly above this post.

Awake is only ever called once per MonoBehaviour, so if it fails, there’s no second chance. Switchboard is all about being able to inject dynamically, without having to stop play, or recompile. If you throw an exception from Awake, the component will be disabled, but then you can just enable it again via the editor or through scripts. Awake will not be called again, and it will be enabled and updating without the required dependencies.

When dropping a prefab into a scene, if required dependencies fail to inject, the MonoBehaviour should be disabled. This is similar to how a plain C# class fails to instantiate if it throws an exception from the constructor, like an ArgumentNullException when you pass null to a required argument. However, that exact same functionality cannot be achieved. The component is still created, so the next best thing is to disable it.

Throwing an exception from the Awake method automatically disables the component. (Note: Throwing an exception from OnEnable does not.) That works fine in the editor when dropping an object into a scene, but it’s not ideal when attempting to add a component to a game object through scripts. If Awake() can throw an exception, then AddComponent method calls would need to be wrapped in a try/catch block. I don’t think that’s a very good thing for me to expect of users.

If a dependency fails to inject from OnEnable, you can simply set enabled = false and return from the method. Then, Start() and Update() will not be called. You can check the enabled property of a MonoBehaviour to determine if it is ready to run. If you attempt to AddComponent, or enable a disabled MonoBehaviour, then it will attempt to enable and fail immediately if injection fails. Enabled will already be set false again, and you can check that to see if your attempt to enable the component succeeded or failed.

This also allows you to re-attempt injection each time the component is enabled. So, if you fix a problem at the composition root while the game is running, you can just enable the component and it will be properly injected, and start running as expected. Furthermore, this can help with testing. Even if injection was successful the first time, you can release your dependencies in OnDisable, and when the component is re-enabled it will get the latest instance from the injector. So, in the editor you can change the dependencies at the composition root, and toggle components to re-inject them directly in the editor without having to stop play.

So, let’s break down the new injection example line by line. First I create a local IInjector variable because we may need to call InjectorLocator.GetInjector(), or we may not. It’s possible this component is just disabled for some ordinary reason, and injection is working just fine. In that case, we may not need get get an IInjector reference. However, we may need to attempt injection again, and we can’t always be sure which dependency will need re-injection. So, it’s good to just start with this placeholder IInjector set to null in case we need it, and if we do we will only call InjectorLocator.GetInjector() once, and keep the local reference for any further injections.

IInjector injector = null;

The first dependency that I almost always inject is ILogger. A logger is one of the most fundamental dependencies of any software application. Without a logger we can’t create a record of what’s going on. So, it’s a natural fit that we would attempt to inject it first. This gives us an example of a required dependency. If the logger is not injected, the first thing I do is set enabled = false. This is to guarantee that the component will be disable due to this, even in the very unlikely event that the next line of code throws an exception (I know for a fact it is possible, however unlikely). Then, I log the error using Debug.Log. This is an inferior logging method, but it’s better than nothing, and I don’t want to throw an exception. Then I just return early from OnEnable.

Logger ??= (injector ??= InjectorLocator.GetInjector())?.Get<ILogger>();
if(Logger == null)
{
    enabled = false;
    Debug.LogError("No Logger!");
    return;
}

Now I want to explain the line of code that does the injection. I suspect it might appear strange to some users. The good news is that you don’t need to know anything about how this line of code works to use it. It’s just a bunch of syntactic sugar to put this common operation on a single line. You can learn more about null-coalescing operators here: ?? and ??= operators - null-coalescing operators - C# reference | Microsoft Learn You never want to use these operators on UnityEngine.Object derived types because of the way the overloaded equality operator works, but it’s totally fine to use them here because we are working with interfaces. So, if we were to unpack this:

Logger ??= (injector ??= InjectorLocator.GetInjector())?.Get<ILogger>();

It would be the same as writing this:

if(Logger == null)
{
    if(injector == null)
        injector = InjectorLocator.GetInjector();
    if(injector != null)
        Logger = injector.Get<ILogger>();
}

That allows us to check if a dependency is null, check if we already have an injector, get the injector if we don’t already have it, and attempt to inject the dependency, before we then check the dependency for null again to see if any part of that process failed. (Technically the first time we do this, we don’t need to check if injector is null because we know it is. But, what if we re-arranged the order of the code? Writing it the same way every time does no harm, and it prevents us from making any copy/paste errors.) It’s just taking that common operation that can be performed universally as you go down the list of dependencies, and putting it all on one line of code. You don’t have to fully understand it, you can just copy and paste it, and edit the member being injected at the beginning, and the type being requested at the end.

Now that we have the ILogger, when we attempt to inject this optional dependency, we can just log a warning and move on. That’s why I inject the ILogger first. Once we have it, we can use it to properly log errors or warnings as we continue the injection process. This part is supposed to represent an optional dependency that this component can work with, but is not strictly required for the component to enable and update.

OptionalDependency ??= (injector ??= InjectorLocator.GetInjector())?.Get<IOptional>();
if(OptionalDependency == null)
    Logger.LogWarning("Optional dependency unavailable.");

Then, from OnDisable, I release the optional dependency. I don’t release the ILogger reference because I think it’s a totally reasonable expectation for the ILogger to not change over the course of the application, or at the very least to be allowed to use the first one I was given. Other dependencies may not work the exact same way and you may not have the same expectations. It may make more sense to release references to a type of dependency that you were given. This does two things: If that dependency has been pruned from the composition root, releasing your references will allow it to be garbage collected. Also, it allows the code we’ve written, that wonky line of syntactic sugar, to re-inject a dependency if it is null. Now, you could hold onto the reference even while disabled, and re-inject on every OnEnable, without checking if the one you kept is null. But, that would change the way the wonky example injection line is written, and I don’t see why we would need to hold the reference while disabled. So, just releasing references that you don’t want to maintain from the first injection in OnDisable allows them to be re-injected, and you can copy and paste that one line of code for injection without having to think too much about it.

private void OnDisable()
{
    OptionalDependency = null;
}

And, that’s it! Injecting this way is much more robust and flexible. It works if you’re dropping an object into a scene, adding the MonoBehaviour to a game object through script, instantiating a prefab, loading a level, whatever! If a required dependency is not injected, the component will just disable. If you are working with scripts, any attempt to create or enable the component will immediately call OnEnable(), and if it fails, enabled will be set to false immediately. So, you can easily have conditional code that knows whether that component was injected properly or not. Also, it will log warnings and errors itself for you to see. Also, if injection fails for any reason, or if you change something at the composition root, you can just toggle the enabled status and it will properly re-inject itself, even live without having to stop playing.

If you are concerned that you may fail to provide some required dependencies in the DependencyInjector you have assigned to the composition root, you can simply create a unit test with a comprehensive list of all the types of dependencies you expect to be required by your app. Just run the DependencyInjector in question through the test. Easy! This can be as simple as a MonoBehaviour in an empty scene that just tries to inject itself with every type of dependency you have in your game.

I hope that makes sense to anyone who may be working with Switchboard. If you have any questions just let me know.

1 Like

Version 1.2 is now on available!
(As usual, I suggest you delete your current Switchboard directory before installing the update.)

Thanks to some valuable user feedback, Switchboard now has some nice quality of life updates to make it easier to use log files and StringMaker.

Logs

Previously, a helper class named LogFileManager was used by the BasicInjector to handle the boilerplate configuration and operation of log files. That class was in its own compiled assembly, which had dependencies on the ClockSynchronizer, PreciseClock, and Ticker assemblies.

Now, the FileLogger MonoBehaviour is a simple component that opens a log file when it is enabled, and can be easily configured via editor or script. The full C# script file is included in the Scripts folder, and available in the Examples section of the GitHub repo, so you can easily read it and see how it uses the Core LogFileWriter class. It is an ILogger, so it can be used directly like any other ILogger, or easily attached to the main, static SwitchboardLogger.Root as one of many log output targets. FileLogger has no dependencies on assemblies other than the ones it needs for writing log files, and the LogFileManager and ClockSynchronizer assemblies have been removed.

So, if you are only interested in log files, it is easier than ever to delete the other aspects of Switchboard if you want to remove them from your project. FileLogger is a simple MonoBehaviour that opens log files, and the BasicInjector provides an example of how it instantiates a FileLogger and attaches it to SwitchboardLogger.Root when it activates.

StringMaker

The StringMaker interface has also improved. Now, the + operator is overloaded for quick and easy appending, as shown in the following example.

using Switchboard;
using TMPro;
using UnityEngine;
using ILogger = Switchboard.ILogger;

public class TestScript : MonoBehaviour
{
    public TextMeshProUGUI Text;
    private float Timer;

    private ILogger Logger;
    private static StringMaker StringMaker = StringMaker.Shared;

    private void OnEnable()
    {
        IInjector injector = InjectorLocator.GetInjector();
        Logger ??= injector?.Get<ILogger>();
    }

    private void Update()
    {
        Timer += Time.deltaTime;
        if(Text != null)
            Text.SetText(StringMaker.Clear() + "Time: " + Timer);

        Logger?.LogDebug(StringMaker.Clear() + nameof(TestScript) + GetInstanceID() + " Timer: " + Timer);
    }
}

It’s just a quick example I made, but it shows how you can easily concatenate strings while assigning them to UI elements, or writing out to a persistent log file on disk, without allocating a single byte of memory for the garbage collector.

Release Notes

Bug Fixes:

  • CompositionRoot now always sets its DependencyInjector to null and removes itself from the InjectorLocator when it begins activation, to prevent corrupted state between play sessions in the editor. It will only assign itself to the InjectorLocator if the DependencyInjector state is truly active, with no exceptions. Now, it always saves a reference to the assigned dependency injector BEFORE it attempts to activate it. So, if activating the DependencyInjector throws an exception, it will still deactivate it when play stops.
  • A log file is now opened immediately when the BasicInjector activates.
  • LogFileWriter no longer throws an AggregateException every time it fails to open a new file, only if an exception also occurred when closing the old file.
  • StandardLogFormatter and UnityLogger are now more resilient to caller info that has been manually set to null or empty.

Changes:

  • Converted LogFileManager into a MonoBehaviour named FileLogger, and added it to the readable Scripts directory.
  • Removed the LogFileManager and ClockSynchronizer assemblies.
  • Changed StringMaker.Clear method to just set Length = 0, just as StringBuilder does. Before, it was releasing its internal memory back to the shared CharacterArrayPool. But, you can achieve that same behavior by manually calling StringMaker.Clear().TrimExcess(). Having StringMaker.Clear() helps enable fluent interface operations, and its more performant and consistent with StringBuilder for it to just set Length = 0.
  • Overloaded the + operator to append basic types to StringMaker.
  • Renamed StringMaker.ThreadStaticInstance, ObjectPool.ThreadStaticInstance, and ThreadSafeObjectPool.StaticInstance properties to Shared.
  • Renamed the ObjectPool and ThreadSafeObjectPool RequestItem methods to TryGet.
  • Updated example code. Simplified example injection code. FloatExample demonstrates usage of StringMaker + operator.
  • Improved formatting of platform stats at the beginning of log files created by the BasicInjector.
  • StandardLogFormatter moved the caller info to come right after the time stamp.
  • Added ILogger extension methods that allow for logging an exception without having to provide a message argument.
  • UnityLogHandler.Initialize method is now private.
2 Likes

I noticed StringMaker doesn’t support the .Remove method like StringBuilder… though in my case using
.Clear(); was fine to use instead.

Should get more examples scenes, like the StringMaker one in a ugui scene for comparison would be good I’ve switched to using it for a particular ugui thing I was doing though.

The problem I have DI etc is just not alot of training material to go by… I have a SingletonScriptableObject based thing for a instance that I’d be tempted to move over to it instead it just needs to instantiate ugui canvas prefab if it doesn’t already exist in the scene but the examples don’t have a something like that

1 Like

You’re right. I should add something like the Remove method to StringMaker.

There were some methods I intentionally left out, like the entire Insert and Replace method groups. The reason is that I don’t think those methods are actually ever necessary, and they just cause unnecessary work for the CPU. If you ever need to insert or replace something while building a string, you should just append everything in the proper order as you build it. If you build it one way, then come back and start inserting or removing characters in the middle of what you’ve already appended, then it’s just a suboptimal operation. So, I didn’t want to encourage anyone to use methods that I don’t think should exist in the first place.

At first, I didn’t want to support a Remove(int startIndex, int length) because that allows you to remove from the middle of the string. Similar to what I said before, if you are adding a bunch of characters to a string, then removing some from the middle, that causes an unnecessary Array.Copy operation to happen for moving the characters at the end over to the left. You could have just appended the first part you want, then appended the next part you want, without ever having to remove anything. That would have better performance anyway.

Any programmer can easily make their own methods, including extension methods of StringMaker, to get this behavior for themselves in any type of method you want. I don’t want to include any methods in StringMaker that aren’t strictly necessary because others can add whatever kind of optional behavior they like. I just want to focus on the core functionality and make that as tight as possible. However, you are right that I should include examples of how one would go about creating similar insert and replace type functionality. I will add that to my to-do list.

My original thinking was that you don’t need a Remove method for taking characters off of the end of a string because you can just set Length = x to remove the end of any string. I’m not opposed to that type of remove operation being included as a method because it doesn’t involve any unnecessary copying of characters or extra work. If you want to erase the extra characters and release the unused character array memory back to the memory pool, you just call the TrimExcess method.

However, I realize now the importance of methods like Clear, and something like Remove, for removing characters from the end of a string, over just using the Length property. The Length property does not enable a fluent interface, which is now more important than ever with the overloaded + operator. If you start an expression with a StringMaker method, it returns that StringMaker at the left hand side of the expression, so you can begin using the overloaded + operator for the rest of the expression. Before v1.2, StringMaker.Clear released all character arrays back to the memory pool. That is not the same behavior as StringBuilder. I thought Clear could release, while Length = 0 would not, but that prevented Clear from being used like Length = 0 in a fluent interface expression. So, I made Clear work as expected in 1.2 because you can just call StringMaker.Clear().TrimExcess() to manually release the arrays if that’s what you want to do. The ability to easily build up those chained together fluent expressions is important.

So I think I should indeed add something like Remove, but it will not be exactly the same because it will not support removing from the middle, only from the end. Do you think I should also call this method Remove, but with one int parameter? That does not overlap with any existing StringBuilder method. If so, should the parameter be for the startIndex at which you want to begin removing to the end of the string, or should the parameter be for the length of characters that you want to remove from the end? Or, should I name the method something different? I’m leaning toward calling it Remove, and having the argument be startIndex, so that if someone is converting from StringBuilder to StringMaker and their Remove call was already from startIndex to Length-startIndex, then they can just put a ); after startIndex to convert their code. But, I may be overthinking that edge case. Should it just be a method called SetLength(int length)?

Also, is this something you need soon? If so I will start working on it immediately and submit a new version this week. If it is not urgent, then I may put it off until the next natural version release. It’s no big deal if you need it soon, I just want to know if I should prioritize it over other work or not.

You’re totally right. Examples are what I have been working on since the start of this year, when not working on updates. I have been recording video of my entire process of creating a new 2022.3 project from scratch. I intend to create a small, vertical slice type demo of a whole game. That way I can demonstrate with real world examples how the framework is intended to integrate with an entire project, including UI and other systems.

I have footage of the whole process, which I will edit into tutorial and example videos. I will also make the whole project open source once I am further along. Part of what I want to demonstrate is the fact that the entire Switchboard framework is loosely coupled to the whole project. So, the open source version will only use the open source InjectorLocator pattern, and a simple composition root implementation that people are familiar with, like singletons or bootstrap scene. It will also use StringBuilder. So, part of the demonstration will be in showing how that project can be easily converted over to the Switchboard / StringMaker platform for instant performance and quality of life improvements. I will see if I have any interesting teaser images to post.

In the mean time, if you want to explain a little more about the modules you are tempted to migrate over to Switchboard, either here or in a private message, I will be happy to work with you.

1 Like

Well I don’t actually need Remove now it seems all the usages I had of it with StringBuilder were entirely fine with just using .Clear() and reusing it again with Append … so entirely upto you how you re-implement it or when, probably is worth having for something and if the summary info explains the differences sure.

Might pm my use case for SingletonScriptableObject see if I can get it converted over.

The best example scenes are the ones showcase clear improvement without all the complexity of a demo scene that has so many other things going on especially in the scripts and scene setup that may or may not be good… seperate scenes showcasing small bits with scripts written for those only… I’ve seen some packages where its a few scenes showing all sorts of loading of other scenes and scripts that are used to making the demo work under a dozen different options and its more complicated just to debug how one particular thing is all working than if it was just a simple example demo showcasing that one particular feature on its without tying in lots of other stuff. … not that you’re doing that just saying.

1 Like

That’s a fair point. If an important aspect is buried in an overwhelming mass of code it could just get lost in the sauce. I just think that right now I’m still trying to develop strong examples of a real world application of Switchboard. I don’t have exact examples fleshed out in a way I can share yet. I can’t publicly share any of the proprietary code that I use at work. If I make something relatively feature complete then real examples of how to use it will come out of that naturally. Of course you’re right that the examples included with the plugin should be more straight to the point.

I probably won’t be including the whole example project with the asset store download anyway. I am making it using free assets that anyone can easily access from the store or places like Mixamo. So, all of those asset dependencies can’t be re-packaged with my plugin anyway. Even on the open source repo they will have to all be downloaded manually from their original source by following the instructions to download the relevant assets. But, once I have something real, that will naturally expose the best examples that I can then package into isolated examples with no external dependencies to include with a future version.

1 Like