UMU, modular tools for Unity, by BMSoft1024

    UMU

    Dokumentáció

    Az UMU minden moduljának teljes leírása: editor ablakok, Inspector mezők, működés és C# kódpéldák.

    A dokumentáció angol nyelvű, ugyanez a szöveg található a Unity csomagban.

    UMU is a collection of Unity plugins that automates common, repetitive development tasks: editor extensions, a panel based UI framework, audio, logging, save files, and platform specific build settings. Every module can be used on its own. Most modules build on the Common module.

    • Author: BMSoft1024 (Miklós Borsi)
    • Support: support@bmsoft1024.com (subject: UMU)

    1. Requirements and installation #

    Requirements

    • Unity 6000.3 or newer (developed and tested on 6000.3.20f1)
    • Unity packages: com.unity.ugui, com.unity.inputsystem, com.unity.nuget.newtonsoft-json (they are not bundled, to avoid duplicates)

    Installation

    1. Import the UMU package through Assets > Import Package > Custom Package (or the Package Manager "My Assets" tab).
    2. Wait for the compilation to finish. All UMU files live under Assets/UMU.
    3. Open the UMU menu in the Unity menu bar. All editor tools are listed there.

    Updating

    Delete the Assets/UMU folder before importing a newer version, so no stale files remain. Keep your own settings assets outside the UMU folder, or back them up first.

    How UMU is delivered

    The code of UMU is delivered as two precompiled, obfuscated assemblies: UMU.dll (runtime) in Assets/UMU and UMU.Editor.dll (editor only) in Assets/UMU/Editor. The public API is intact, so you can call every class and method documented here, and you can derive from the classes that are meant to be extended (for example GuiManager, GuiPanel). Prefabs, scenes and ScriptableObjects reference the scripts inside the DLLs and work as in a source project.

    2. Package contents #

    ModulePurpose
    Common + UtilityShared infrastructure: serialization, reflection helpers, inspector attributes, save files, editor basics
    GUI ManagerPanel and popup based UI framework with history, transitions, safe area support
    AudioRuntime audio system: SFX pool, music crossfade, mixer routing with ducking, volume persistence
    Audio CutterEditor wizard that splits audio clips into separate files
    MessengerMessage broadcast between components and scenes
    LoggerUnified logging layer with tags and a console window
    Debug ModeDevelopment settings window: log level, debug switch, Android keystore password helper
    Internet CheckerAsynchronous, non blocking internet connection check
    Hardware DetectorRule based device tier evaluation (GPU, CPU, RAM, resolution, iOS generation)
    Android Manifest EditorVisual editor for AndroidManifest.xml
    Xcode Settings EditorEdit Xcode project settings inside Unity for iOS builds
    Resource CheckerMemory audit of the open scene (third-party public domain tool)
    Demo contentDemo scenes, prefabs, UI art, audio clips and 3D models (only in the package variant with demo)

    The UMU editor menu contains one entry per tool.

    3. GUI Manager #

    The GUI Manager module opens, closes and tracks full screen panels and layered popups, keeps a navigation history, locks buttons during transitions, and ships a set of Unity UI selectables, anchoring and safe area helpers, and editor tools.

    All runtime types live in the UMU.GuiManager namespace unless noted otherwise. Inside a namespace that starts with UMU, the simple name GuiManager resolves to the namespace, so write UMU.GuiManager.GuiManager there. In the global namespace, using UMU.GuiManager; is enough.

    Core concepts used throughout this reference:

    • Panel: a full screen screen. Only one panel is current at a time. Opening a panel closes the current one and records it in the history.
    • Popup: a window that opens on top of the current panel. Several popups can be open at once, ordered by layer.
    • Panel id: the string used to open a panel or popup. It is the path of the prefab inside a Resources folder, without extension (for example Demo Popup 1). The Settings window can generate a GuiPanels constants class with one constant per prefab.
    • Messages: an optional Dictionary<object, object> passed to OpenPanel or OpenPopup and delivered to the panel's OnPreOpen.

    Editor menu and window reference #

    Menu overview #

    Menu pathWhat it does
    UMU > Gui Manager > SettingsOpens the "Gui Manager Settings" window.
    UMU > Gui Manager > Panel OverviewOpens the "GUI Panel Overview" window.
    UMU > Gui Manager > Panel OptimizerAudits every panel prefab and writes a report to the Console. Changes nothing.
    UMU > Gui Manager > Panel Optimizer - Apply Safe FixesRuns the same audit and also applies the safe raycast fixes.
    UMU > Gui Manager > Replace Unity UI To UMU GuiConverts Unity UI selectables under the selected GameObject to their UMU Gui counterparts.
    GameObject > UMU_Gui > Gui ButtonCreates a Gui Button.
    GameObject > UMU_Gui > Gui ToggleCreates a Gui Toggle.
    GameObject > UMU_Gui > Gui SliderCreates a Gui Slider.
    GameObject > UMU_Gui > Gui ScrollbarCreates a Gui Scrollbar.
    GameObject > UMU_Gui > Gui DropdownCreates a Gui Dropdown.
    GameObject > UMU_Gui > Gui Input FieldCreates a Gui Input Field.

    The GameObject > UMU_Gui items are listed in that order in the menu (Button, Toggle, Slider, Scrollbar, Dropdown, Input Field).

    Settings window (UMU > Gui Manager > Settings) #

    Window title: "Gui Manager Settings". The window has a fixed size of 800 x 135 pixels. The values are stored in the Gui Manager plugin save file.

    UMU > Gui Manager > Settings
    UMU > Gui Manager > Settings
    Control (as shown)What it doesDefaultNotes
    Gui panel prefabs resources path (text field)Project relative folder that contains your panel and popup prefabs. The panel id drop downs in the Inspector list the prefabs found in this folder, and the constants class is generated from it.emptyThe label turns red while the folder does not exist. Ids are built relative to the Resources folder, so the folder should be inside a Resources folder for the panels to load at runtime.
    search path (button, first row)Opens a folder picker for the prefab folder.n/aThe chosen absolute folder is converted to a project relative path.
    Gui panel id script path (text field)Project relative folder where the generated GuiPanels.cs script is written.emptyThe label turns red while the folder does not exist.
    search path (button, second row)Opens a folder picker for the script folder.n/aSame conversion as above.
    Transition mode (enum popup)Project wide default engine for GuiScreenTransition components that use the global mode: Builtin or DOTween.BuiltinSee section 2.5 for the engine behaviour.
    save settings (button)Writes the settings to the plugin save file and closes the window.n/a

    Generated constants (GuiPanels):

    • Whenever a prefab inside the prefab folder is imported, deleted or moved, the constants script is regenerated and the Console logs "Created all gui constans".
    • The script contains public static class GuiPanels with one public const string per prefab found in the folder (including sub folders).
    • Constant names are derived from the prefab path: lower case, spaces and path separators replaced by underscores, characters other than letters, digits and underscores removed, and a panel_ prefix added when the name does not start with a letter (for example Demo Popup 1 becomes demo_popup_1).
    • Constant values are the path after the last Resources/ folder, without extension.
    • Any other file named GuiPanels.cs under Assets that is not inside the configured script folder is deleted when the script is generated.
    • Nothing is generated while the prefab folder path is empty.

    Panel Overview window (UMU > Gui Manager > Panel Overview) #

    Window title: "GUI Panel Overview". It shows a grid of every prefab in the project whose root object carries a GuiPanel component, sorted by name.

    UMU > Gui Manager > Panel Overview
    UMU > Gui Manager > Panel Overview
    Control (as shown)What it doesDefaultNotes
    Refresh (button)Rescans the project for panel prefabs and rebuilds the grid.n/aThe scan also runs when the window opens.
    Search fieldFilters the tiles by prefab name.emptyCase insensitive "contains" match.
    "All prefabs containing a GuiPanel component." (label)Description of the list.n/a
    Tile (one per prefab, 160 x 100)Shows the prefab name, the panel type (Panel, Popup or PopupAndPanel), and for popups a "Layer: n" line.n/aThe layer line appears only when the type is Popup.
    Select (button on a tile)Pings the prefab in the Project window and selects it.n/a

    Panel Optimizer (UMU > Gui Manager > Panel Optimizer) #

    Scans every prefab in the project that has a GuiPanel on the root or on any child. The result is written to the Console as one report. Nothing is modified.

    For each such prefab the report is printed when the prefab has any issue or any Graphic with Raycast Target on. Each printed entry contains the asset path and a line "graphics: N, raycastTarget on: N, masks: N" (masks counts Mask and RectMask2D components), followed by the issues:

    Finding textMeaning
    non-interactive Graphic with raycastTarget on: <path>A Graphic that has Raycast Target on, is not under any Selectable, and is not the Target Graphic of any Selectable. It blocks raycasts and costs performance without being clickable.
    Selectable targetGraphic has raycastTarget off (unclickable): <path>A Selectable whose Target Graphic has Raycast Target off, so it cannot be clicked.
    GuiPanel without firstSelected (no gamepad/keyboard entry point): <path>A GuiPanel whose First Selected field is empty.

    The final line is "Panel Optimizer done. prefabs: N, issues: N, applied fixes: N".

    Panel Optimizer - Apply Safe Fixes #

    Runs the same scan and also changes the prefabs, then saves the assets:

    • Graphics reported as "non-interactive Graphic with raycastTarget on" get Raycast Target turned off. In this mode they are not listed as issues, only counted in "fixed: N" for the prefab.
    • Target Graphics reported as "unclickable" get Raycast Target turned on (the finding is still listed).
    • The "GuiPanel without firstSelected" finding is only reported, never fixed.

    The report line "applied fixes" shows the total number of changed Graphics.

    Replace Unity UI To UMU Gui #

    Works on the GameObject selected in the Hierarchy (Selection.activeGameObject) and on all of its children. With nothing selected it logs the error "No selected gameObject!".

    It converts these Unity UI components to their UMU counterparts, copying the component values and keeping the GameObject:

    Unity UIUMU Gui
    ButtonGuiButton
    ScrollbarGuiScrollBar
    DropdownGuiDropdown
    InputFieldGuiInputField
    ToggleGuiToggle
    SliderGuiSlider

    Converted GameObjects are renamed to "<menu item name>_<old name>", for example "Gui Button_Button". Components that are already UMU Gui components are kept.

    GameObject > UMU_Gui creators #

    Each item runs Unity's own GameObject > UI > <type> command, then converts the new object to the UMU Gui component and names it.

    Menu itemUnity UI command usedResulting componentGameObject name
    Gui ButtonButtonGuiButtonGui Button
    Gui ToggleToggleGuiToggleGui Toggle
    Gui SliderSliderGuiSliderGui Slider
    Gui ScrollbarScrollbarGuiScrollBarGui Scrollbar
    Gui DropdownDropdownGuiDropdownGui Dropdown
    Gui Input FieldInput FieldGuiInputFieldGui Input Field

    Notes:

    • Select a GameObject first (normally the panel or a container under a Canvas). The new object is parented to the selected object.
    • All of the Inspector fields of the Unity component are kept, because the Gui components derive from the Unity components.

    Inspector tools that belong to the editor module #

    ControlWhereWhat it does
    Panel id drop down (popup with the prefab list, first entry "Empty")Every field marked as a panel id: Initially Open, Pre Loaded Panels, Pre Loaded Popups, the Panel and Popup fields of Gui Button, and Previous Panel Id in panel change settingsLists the prefabs found in the Gui panel prefabs resources path (without extension). Choosing "Empty" clears the id.
    Select (button next to the drop down)Same fieldsSelects the chosen prefab asset in the Project window. The tooltip shows the search folder.
    o (small button next to Select)Same fieldsToggles a search mode. The Select button becomes a text field that filters the drop down list (case insensitive). Pressing o again leaves the search mode and restores the previously selected id.
    Panel Change State sectionGuiPanel InspectorSee section 2.2.
    Preset buttons Stretch, Top-Left, Top-Right, Bottom-Left, Bottom-Right, CenterOverrideAnchor InspectorSee section 2.8.

    Inspector reference #

    Unity shows serialized fields with their nicified names (for example blockInputDuringTransitions is shown as "Block Input During Transitions"). Components without a custom Inspector use these default labels. Fields marked [HideInInspector] are not listed unless the table says that the custom Inspector shows them.

    GuiManager #

    Add it to one GameObject in the scene. There is no custom Inspector. GuiManager.Instance finds the first manager in the scene and is also set in Awake.

    GuiManager component (Gui Manager object)
    GuiManager component (Gui Manager object)
    LabelTypeDefaultBehaviour
    Open Automatic Initially PanelboolfalseSerialized, but not read by the current manager code. The initial panel opens whenever Initially Open holds an id.
    Initially OpenGuiPanelID (panel id drop down)emptyId of the panel opened in Start. Empty means no panel is opened automatically.
    Panel RootRectTransformnoneParent of all loaded panels. Required before a panel can be loaded.
    Popup RootRectTransformnoneParent of all loaded popups. Also the parent of the transition blocker and the root searched for a focus target while a popup is open.
    Pre Loaded PanelsList of GuiPanelIDemptyPanels instantiated, registered and deactivated at startup, so that the first open has no loading cost.
    Pre Loaded PopupsList of GuiPanelIDemptySame for popups.
    External PanelsList of GuiPanelemptyPanel prefabs from the project (no Resources folder needed). At startup each one is instantiated and registered under the prefab name as id.
    External PopupsList of GuiPanelemptySame for popups.
    Block Input During TransitionsbooltrueWhile a panel switch or any panel or popup open or close transition runs, a full screen transparent raycast blocker named UMU_TransitionBlocker is shown on top, so no click reaches elements mid animation.
    Persist Across ScenesboolfalseWhen on, the root GameObject of the manager is marked DontDestroyOnLoad. On every active scene change all loaded panels and popups are destroyed, history and queues are cleared, and the external and preload lists are loaded again. When off, a scene change closes the current panel.
    Use Panel Pair Transition SettingsbooltrueWhen on, entries in the closing panel's Panel Change Process Settings list (matched by the id of the panel being opened) override the default Has Change Process and Waiting For Change Process flags for that switch.
    Dynamic Popup LayersboolfalseWhen on, a newly opened popup (not Always On Top) gets the highest currently open popup layer plus one, ignoring its serialized Layer.
    Ensure SelectionboolfalseWhen on, the manager keeps a Selectable selected inside the active root (the popup root while a popup is open, otherwise the current panel). If the selection leaves it, the panel's First Selected is selected when it is interactable, otherwise the first interactable Selectable found. Selection history keeps up to 32 entries.

    Runtime behaviour worth knowing:

    • Opening a panel with the id of the current panel, or with an empty id, does nothing. If a panel switch is already running, the new request is queued (only the latest queued request is kept) and starts when the running switch ends.
    • The history records the panel that is left. Opening a panel that is already in the history cuts the history back to the entry before it. BackToPreviousPanel() opens the last history entry; with an empty history it only fires onBackPresedOnEmptyHistory.
    • A popup opens immediately only if its layer is higher than the layer of every open popup that is not Always On Top (or it is Always On Top, or Dynamic Popup Layers applies). Otherwise it is queued and opened when a popup closes (the queued popup with the highest layer first).
    • The popup is placed at local z = 0.001 x layer (Always On Top popups use 1,000,000 + layer). Children of the panel and popup roots are re-sorted by active state and z after each open.
    • While a popup is open, the buttons (Lock) of the panel below and of lower layer popups are disabled. When popups close, the buttons of the top popup, or of the current panel when no popup is left, are restored.
    • Back input: in the Editor and on Android the Escape key (the Android back button) is handled each frame, unless busy. With open popups it closes the top popup, otherwise it goes back to the previous panel. A panel with Allow Back Input off ignores it. SetBackInputChecker(false) disables the check.
    • Panels are loaded with Resources.Load from their id when they are not registered yet. RegisterPanel registers an already instantiated panel instead.

    Public events (plain Action fields, not shown in the Inspector): onBackPresedOnEmptyHistory, onOpenedPanel(string), onClosedPanel(string), onOpenedPopup(string), onClosedPopup(string), onPreOpenPanel(string), onPreClosePanel(string), onPreOpenPopup(string), onPreClosePopup(string).

    GuiPanel (abstract base of every panel and popup) #

    GuiPanel requires an Image and a CanvasRenderer. It has a custom Inspector: the default fields first, then the sections below.

    LabelTypeDefaultBehaviour
    TypePanelTypePanelPanel, Popup or PopupAndPanel. The manager decides panel versus popup by the method you call (OpenPanel or OpenPopup), not by this field. The field controls whether Layer is visible in the Inspector (hidden for Panel) and how the panel is labelled in Panel Overview.
    Inspector EventsInspectorEventall emptyFour events, see the nested type below.
    Panel Change Process SettingsList of PanelChangeProcessDataemptyPer target panel overrides, see the nested type below.
    First SelectedSelectablenoneSelected through the EventSystem when the open finished, and restored after a popup above this panel closes. Used as the keyboard and gamepad entry point.
    Allow Back InputbooltrueWhen off, the Escape or Android back input is ignored while this panel is current or is the top popup.
    Always On TopboolfalseOverlay flag for popups. The popup always opens regardless of the layer of other popups and never blocks lower popups from opening. It is ignored when the manager picks the top popup for back input and CloseTopPopup.
    Replace Builtin TransitionboolfalseWhen on and onCustomTransition has subscribers, the built-in transition of the panel is skipped and only the custom transition runs.
    Has Change Process (in the Panel Change State section)booltrueWhen on, opening and closing wait until the panel reports that its transition is over (onPanelChangeProcessIsOver). When off, the lifecycle does not wait; the built-in panels also skip their animation.
    Waiting For Change Process (in the Panel Change State section)booltrueShown only when Has Change Process is on (turning Has Change Process off also turns this off). When on, the manager waits until this panel finished closing before it activates the next panel. When off, the next panel opens while this one is still closing.
    Layer (below the sections)GuiLayer (int value)0Shown only when Type is not Panel. Popup order: a higher layer is on top. Also read by GuiLayerFollower.

    Fields that exist but are not in the Inspector: id (string, assigned by the manager), result (object, see the popup result example), onClickSelectableItem, onPanelChangeProcessIsOver, onCustomTransition.

    Lifecycle order when the manager opens a panel or popup (the manager level event is raised first):

    1. PrepareOpen() (same frame as the activation, before a one frame wait).
    2. Manager event onPreOpenPanel or onPreOpenPopup, then OnPreOpen(messages), then all buttons are locked.
    3. StartOpenedAnimation(); the lifecycle waits until onPanelChangeProcessIsOver is raised (when Has Change Process is on).
    4. Manager event onOpenedPanel or onOpenedPopup, then OnOpened().
    5. The lifecycle waits while the previous panel is still closing, then the buttons are restored and First Selected is selected.

    Closing: manager event onPreClosePanel or onPreClosePopup, OnPreClose(), buttons locked, StartClosedAnimation(), wait, OnWillClosed(), manager event onClosedPanel or onClosedPopup, then the GameObject is deactivated.

    Nested type InspectorEvent (shown as Inspector Events):

    LabelTypeRaised by
    On Pre OpenGuiDictEvent (UnityEvent<Dictionary<object, object>>)Base OnPreOpen(messages); receives the messages (may be null).
    On OpenedGuiEvent (UnityEvent)Base OnOpened().
    On Pre CloseGuiEventBase OnPreClose().
    On Will ClosedGuiEventBase OnWillClosed().

    An override that does not call the base method does not raise the matching Inspector event.

    Nested type PanelChangeProcessData (one entry of Panel Change Process Settings, shown as a foldout):

    LabelTypeDefaultBehaviour
    Default SettingsboolfalseOnly hides the Previous Panel Id field in the Inspector while on. The runtime lookup does not read it.
    Exists Panel Change ProcessbooltrueReplaces the panel's Has Change Process for this pair.
    Previous Panel Idstring (panel id drop down)emptyVisible when Default Settings is off. At runtime the entry is used when this panel closes because the panel with this id is being opened.
    Waiting For Panel Change ProcessbooltrueVisible when Exists Panel Change Process is on (forced off when it is turned off). Replaces Waiting For Change Process for this pair.

    An entry applies only when the manager's Use Panel Pair Transition Settings is on. Only the Exists and Waiting values of the entry take part in the decision whether the manager waits for the closing panel.

    Nested type GuiLayer: a serializable wrapper of an int value with implicit conversion from and to int, the arithmetic operators +, -, *, and the comparison operators.

    Public API of GuiPanel: ActivateButtons(), DeactivateButtons(), SelectableItemReInitialize<T>(T selectableObject, bool interactable = true) (registers or re-registers a Gui selectable that was added at runtime; the panel collects the Gui selectables in its children when the manager registers it), GetChangeProcessSettings(string otherPanelId).

    GuiTweenPanel #

    Requires a GuiScreenTransition on the same GameObject. It adds no serialized fields of its own; the Inspector shows the GuiPanel fields. It starts the open transition in StartOpenedAnimation and the close transition in StartClosedAnimation, and applies the hidden start state in PrepareOpen so that the finished window is never drawn for one frame. When Replace Builtin Transition is on and a custom transition is subscribed, PrepareOpen does nothing and only the custom transition runs.

    GuiTweenPanel component
    GuiTweenPanel component

    GuiTweenControlledPanel and GuiAnimatorControlledPanel #

    Two alternative ready made panels (both derive from GuiPanel).

    GuiTweenControlledPanel (no Animator, no separate transition component):

    LabelTypeDefaultBehaviour
    Open TransitionTweenTypeFadeEffect when opening.
    Close TransitionTweenTypeFadeEffect when closing.
    Durationfloat (seconds)0.25Duration in unscaled time. A value of 0 or less, or the type None, finishes at once.
    EasingAnimationCurveEaseInOutEvaluated over normalized time.

    TweenType values: None (no effect), Fade (alpha), SlideFromLeft, SlideFromRight, SlideFromTop, SlideFromBottom (moves from one panel width or height away to the original position), Scale (from almost zero to the original scale), FadeAndScale. A CanvasGroup is added automatically when missing. With Has Change Process off no effect is played.

    GuiAnimatorControlledPanel (requires an Animator):

    LabelTypeDefaultBehaviour
    AnimatorAnimatornoneNot assigned automatically. When opening it plays the state named Open, when closing the state named Close (constants openAnimationState, closeAnimationState), on layer 0, and waits for the clip length (adjusted by the state speed) plus 0.05 seconds in unscaled time. Without an Animator, or with Has Change Process off, TestAnimation runs instead (a 0.5 second wait when Has Change Process is on).

    GuiScreenTransition #

    Fade, slide or scale transition driven through a GuiTweenPanel. It adds a CanvasGroup to the GameObject when missing and remembers the anchored position and scale found in Awake as the resting state.

    GuiScreenTransition component
    GuiScreenTransition component
    LabelTypeDefaultBehaviour
    Use Global Transition ModebooltrueWhen on, the engine comes from the project setting (Settings window, Transition mode). When off, Transition Mode below is used.
    Transition ModeGuiTransitionModeDOTweenBuiltin runs a coroutine without dependencies. DOTween uses DOTween when an assembly named DOTween is loaded in the project; otherwise it silently falls back to Builtin. Used only when Use Global Transition Mode is off.
    Ignore Time ScalebooltrueTransitions advance in unscaled time (both engines).
    Open StyleStyleFadeStyle of the open transition.
    Close StyleStyleFadeStyle of the close transition.
    Open Durationfloat (seconds)0.25A value of 0 or less jumps to the end state at once.
    Close Durationfloat (seconds)0.2Same for closing.
    CurveAnimationCurveEaseInOutEasing curve for both engines.

    Style values and their start state when opening (closing plays the same motion in reverse; every style except None also fades the alpha between 0 and 1):

    StyleMeaning
    NoneNo effect. The panel jumps to the end state and the completion callback fires at once.
    FadeAlpha only.
    SlideLeftStarts one parent width to the left of the resting position and slides in.
    SlideRightStarts one parent width to the right.
    SlideUpStarts one parent height above the resting position.
    SlideDownStarts one parent height below.
    ScaleUpStarts at scale 0 and grows to the resting scale.
    ScaleDownStarts at 1.2 times the resting scale and shrinks to it.

    After a close the alpha is 0 and the position and scale are restored. If the GameObject is inactive when a transition is requested, the end state is applied and the completion callback fires immediately.

    Public members: EffectiveMode (the resolved engine), PlayOpen(Action), PlayClose(Action), PrepareOpen(), KillTransition() (stops a running transition without raising its completion callback).

    Project setting (GuiSettings): GuiSettings.Current.transitionMode (default Builtin) can also be assigned from code to override the engine at runtime.

    GuiButton and the other Gui selectables #

    All Gui selectables derive from the Unity UI component of the same name and keep every Unity Inspector field (Interactable, Transition, Target Graphic, Navigation, events and so on). Their custom Inspectors are the Unity editors of the base components. When the panel is registered it initializes them: they report user changes to the panel (which forwards them to GuiManager.OnClickSelectableItem), and the panel locks and restores them (interactable set to false during transitions and below popups; restored to the value interactable had at initialization).

    GuiButton component
    GuiButton component
    ComponentDerives fromReports to the panel when
    GuiButtonButtonclicked
    GuiToggleTogglevalue changed
    GuiSliderSlidervalue changed
    GuiScrollBarScrollbarvalue changed
    GuiDropdownDropdownvalue changed
    GuiInputFieldInputFieldtext changed

    GuiButton adds three Inspector fields and runs a panel action on click:

    LabelTypeDefaultBehaviour
    Special Gui FunctionSpecialFunctionNoneAction executed on click, see the list below.
    PanelGuiPanelID (panel id drop down)emptyShown only for OpenPanel. Panel to open.
    PopupGuiPanelID (panel id drop down)emptyShown for OpenPopup and ClosePopup. Popup to open or close.

    SpecialFunction values: None (does nothing besides reporting the click), OpenPanel (GuiManager.Instance.OpenPanel(panel.id)), BackToPreviousPanel, OpenPopup (OpenPopup(popup.id)), ClosePopup (ClosePopup(popup.id)), CloseAllPopup, CloseTopPopup.

    GuiEventSystem derives from Unity's EventSystem and adds no fields.

    SafeAreaFitter #

    Keeps a RectTransform inside the device safe area (notches, camera cutouts, home indicator) by setting its anchors. Requires a RectTransform. Apply it to a full stretch container, and put the interactive content inside it.

    SafeAreaFitter component
    SafeAreaFitter component
    LabelTypeDefaultBehaviour
    Apply LeftbooltrueWhen off, the left anchor is left unchanged.
    Apply RightbooltrueSame for the right side.
    Apply TopbooltrueSame for the top.
    Apply BottombooltrueSame for the bottom.
    Refresh On Orientation ChangebooltrueWhen on, the fitter re-applies in Update whenever the screen orientation or the safe area changes (and every frame while Simulate In Editor is on). The initial application in Awake always runs.
    Simulate In EditorboolfalseEditor only. Uses the insets below instead of the real safe area, because the Game view has no safe area.
    Simulated InsetsVector4(0.056, 0.054, 0.056, 0)Screen fractions: x = left, y = bottom, z = right, w = top. Used only when Simulate In Editor is on.

    Apply() can be called from code.

    OverrideAnchor #

    Positions and sizes a RectTransform relative to the edges of other RectTransforms (world space edge positions plus an offset). Requires a RectTransform. The custom Inspector shows a "General" section, and a "Normal Anchor" or "Advanced Anchor" section depending on Type.

    OverrideAnchor component
    OverrideAnchor component
    LabelTypeDefaultBehaviour
    TypeAnchorTypeNoneNone does nothing. Normal uses one target for all four sides. Advanced uses one target per side.
    Update TypeUpdateTypeOnStartWhen the anchors are applied: OnStart, OnEnable, OnUpdate, OnLateUpdate, OnFixedUpdate, or OnOrientationChange (checked each frame; applies only when the screen orientation changed).

    Normal Anchor (Type = Normal). Nothing is applied while Target is empty. After placing the four edges, the size is set to the distance between the right and left, and the top and bottom positions.

    LabelTypeDefaultBehaviour
    TargetRectTransformnoneThe rectangle whose edges are followed.
    TopnGUI_VerticalAnchorTargetTopWhich edge or center of the target the top edge follows: TargetTop, TargetBottom, TargetCenter.
    (value after Top)float0Offset added to that position (world units).
    RightnGUI_HorizontalAnchorTargetRightTargetLeft, TargetRight, TargetCenter.
    (value after Right)float0Offset.
    BottomnGUI_VerticalAnchorTargetBottomAs Top.
    (value after Bottom)float0Offset.
    LeftnGUI_HorizontalAnchorTargetLeftAs Right.
    (value after Left)float0Offset.

    Preset buttons (Normal Anchor section): Stretch, Top-Left, Top-Right, Bottom-Left, Bottom-Right, Center. A preset sets Type to Normal, writes the four anchor choices, and writes the four offsets. Stretch follows the target's top, bottom, left and right edges with zero offsets. The other presets pin all four sides to the same target edge (or to the target center for Center) and compute the offsets from the current width and height of the element. A preset can be undone with Undo.

    Advanced Anchor (Type = Advanced). Each side has its own target, anchor choice and offset, with the same choices and defaults as Normal (Top, Right, Bottom and Left, each as: target, anchor, offset). A side without a target is left unchanged. The height is set only when both Top and Bottom have targets, and the width only when both Left and Right have targets.

    ApplyAnchorSettings() can be called from code.

    GuiLayerFollower #

    Keeps the Unity sorting order of this object and its children in step with the GuiPanel layer found in a parent. Attach it to particle effects or nested canvases inside a panel or popup prefab.

    GuiLayerFollower component
    GuiLayerFollower component
    LabelTypeDefaultBehaviour
    Order Offsetint0Added to the panel layer before it is written to Renderer.sortingOrder and Canvas.sortingOrder of this object and all its children (including inactive ones). The values are updated in Update whenever the panel layer changes.

    GuiObjectPlatformChecker (namespace UMU.Platform) #

    Shows or hides a GameObject depending on the platform, in Awake.

    GuiObjectPlatformChecker component
    GuiObjectPlatformChecker component
    LabelTypeDefaultBehaviour
    Active ObjectGameObjectnoneObject to show or hide. When empty, the GameObject that carries the component is used.
    Enabled Platform TypesList of EnabledPlatformTypesemptyThe object is shown when any entry matches the current platform. With an empty list the object is hidden.
    Disabled Platform TypesList of EnabledPlatformTypesemptyAny matching entry hides the object, even when an enabled entry matched.
    Activate Pre Condition LogicPreConditionLogicNoneDebugModeActive or DebugModeInactive. When the condition holds, the object is shown and the platform lists are ignored. Checked first.
    Deactivate Pre Condition LogicPreConditionLogicNoneSame, but the object is hidden. Checked second.

    EnabledPlatformTypes values: All (always matches), Editor, Iphone, Android, AndroidTv. PreConditionLogic values: None, DebugModeActive, DebugModeInactive. The Android and Android TV platforms are told apart at runtime, so Android does not match a TV device.

    GuiCamera #

    LabelTypeDefaultBehaviour
    Gui CameraCameranoneOverwritten in Awake with the Camera component on the same GameObject. GuiCamera.Instance returns the first GuiCamera in the scene.

    GuiEventSystem #

    Derives from Unity's EventSystem. It has no fields of its own.

    Demo behaviours (Demo folder) #

    The demo panels (DemoMainMenuPanel, DemoPanel1 to DemoPanel3, DemoPopup1 to DemoPopup4) derive from DemoProjectPanel or DemoProjectPopup, which derive from GuiTweenPanel. They add no fields. DemoGuiManager derives from GuiManager and shows the static Instance pattern.

    ComponentLabelTypeDefaultBehaviour
    DemoBounceAmplitudefloat20Height of the vertical sine movement (local position, units).
    DemoBounceSpeedfloat2Speed of the movement; each instance starts at a random phase.
    DemoRotatorSpeedVector3(0, 25, 0)Rotation in degrees per second around the world axes.

    Inheritance guide #

    Which members can be overridden #

    Members of GuiPanel and its derived panels:

    MemberAccessibilityOverride rule
    OnPreOpen(Dictionary<object, object> messages)protected virtualCall base.OnPreOpen(messages) to raise the On Pre Open inspector event. messages can be null.
    OnOpened()protected virtualCall base to raise On Opened.
    OnPreClose()protected virtualCall base to raise On Pre Close.
    OnWillClosed()protected virtualCall base to raise On Will Closed.
    StartOpenedAnimation()protected abstractMust be implemented in a direct subclass of GuiPanel. Must lead to exactly one call of onPanelChangeProcessIsOver().
    StartClosedAnimation()protected abstractSame for closing.
    PrepareOpen()protected internal virtualOverride with protected override from your own assembly. Called when the panel is activated, one frame before Open. Put the hidden start state here.
    RunTransition(bool opening, Action<Action> playBuiltin)protected (not virtual)Helper for StartOpenedAnimation and StartClosedAnimation, see 3.4.
    UsesBuiltinTransitionprotected propertyFalse when a custom transition replaces the built-in one.
    panelChangeProcessingprotected internal fieldTrue while the open or close lifecycle waits for the transition.
    onPanelChangeProcessIsOverpublic ActionInvoke it to end the transition.
    onCustomTransitionpublic event Action<bool, Action>Subscribe from outside.
    Init(), Open(...), Close()internalNot accessible; the manager calls them.

    Members of GuiTweenPanel: PrepareOpen() is protected internal override, StartOpenedAnimation() and StartClosedAnimation() are protected override, and can be overridden again.

    Members of GuiAnimatorControlledPanel: TestAnimation(Action finished) and WaitForAnimToFinish(string animationStateName, Action finished) are protected virtual coroutines.

    Members of GuiManager:

    MemberAccessibilityOverride rule
    static GuiManager Instancepublic staticHide it with new in your subclass (see 3.5).
    instance (static field)protected staticSet in Awake, cleared in OnDestroy when it is this object.
    Awake()protected (not virtual)Do not hide it. Override Initializer() instead.
    Initializer()protected virtualCalled from Awake before the scene change subscription. Base runs SetupInputModules(), registers the External lists, and preloads panels and popups.
    Start(), Update(), OnDestroy(), OnApplicationFocus(bool)protected virtualCall base. Update runs the back input check, the transition blocker and the selection watcher. Start opens the initial panel.
    LoadPrefab<T>(Transform root, string id, Dictionary<string, T> loaded)protected virtualGeneric method with the constraint where T : GuiPanel (inherited, do not repeat it).
    SetupInputModules()protected virtualEmpty in the base.
    CheckBackInput()protected virtualPer frame back input check.
    SetBackInputChecker(bool enabled = true)public virtual
    OnClickSelectableItem(IGuiSelectable selectable)protected virtualCalled when any Gui selectable of a loaded panel reports a user action.
    OnPreOpenPanel, OnPreClosePanel, OnPreOpenPopup, OnPreClosePopup, OnOpenedPanel, OnClosedPanel, OnOpenedPopup, OnClosedPopup (each (string id))protected virtualCall base. The base raises the matching public Action. OnClosedPopup also keeps the popup stack, button locking and the OpenPopup(id, onClosed) callback working.

    Deriving from GuiPanel #

    A direct subclass must implement both animation methods. The lifecycle sets panelChangeProcessing to true, calls your Start...Animation, and waits until onPanelChangeProcessIsOver is invoked (when Has Change Process is on). Invoke it exactly once, either at the end of an animation or immediately when there is nothing to animate.

    C#
    using System.Collections;
    using System.Collections.Generic;
    using UMU.GuiManager;
    using UnityEngine;
    
    public class FadePanel : GuiPanel
    {
        public float duration = 0.3f;
    
        private CanvasGroup group;
        private string title;
    
        private CanvasGroup Group
        {
            get
            {
                if (group == null)
                    group = GetComponent<CanvasGroup>();
    
                if (group == null)
                    group = gameObject.AddComponent<CanvasGroup>();
    
                return group;
            }
        }
    
        // Hidden start state, applied when the panel is activated, before the first visible frame.
        protected override void PrepareOpen()
        {
            base.PrepareOpen();
            Group.alpha = 0f;
        }
    
        protected override void OnPreOpen(Dictionary<object, object> messages)
        {
            base.OnPreOpen(messages); // raises the On Pre Open inspector event
    
            title = null;
    
            if (messages != null && messages.TryGetValue("title", out object value))
                title = value as string;
        }
    
        protected override void OnOpened()
        {
            base.OnOpened();
            Debug.Log("Opened with title: " + title);
        }
    
        protected override void OnPreClose()
        {
            base.OnPreClose();
        }
    
        protected override void OnWillClosed()
        {
            base.OnWillClosed();
        }
    
        protected override void StartOpenedAnimation()
        {
            StartCoroutine(Fade(0f, 1f));
        }
    
        protected override void StartClosedAnimation()
        {
            StartCoroutine(Fade(1f, 0f));
        }
    
        private IEnumerator Fade(float from, float to)
        {
            float elapsed = 0f;
    
            while (elapsed < duration)
            {
                elapsed += Time.unscaledDeltaTime;
                Group.alpha = Mathf.Lerp(from, to, Mathf.Clamp01(elapsed / duration));
                yield return null;
            }
    
            Group.alpha = to;
            onPanelChangeProcessIsOver(); // exactly once, ends the transition
        }
    }

    A panel with no animation:

    C#
    using UMU.GuiManager;
    
    public class InstantPanel : GuiPanel
    {
        protected override void StartOpenedAnimation()
        {
            onPanelChangeProcessIsOver();
        }
    
        protected override void StartClosedAnimation()
        {
            onPanelChangeProcessIsOver();
        }
    }

    Deriving from GuiTweenPanel #

    GuiTweenPanel already plays the GuiScreenTransition and implements both animation methods. Override only the hooks you need. Keep the base calls so the transition and the inspector events keep working.

    C#
    using System.Collections.Generic;
    using UMU.GuiManager;
    using UnityEngine;
    using UnityEngine.UI;
    
    public class ShopPanel : GuiTweenPanel
    {
        public Text coinsLabel;
    
        protected override void PrepareOpen()
        {
            base.PrepareOpen();
            // refresh content here, before the first visible frame
        }
    
        protected override void OnPreOpen(Dictionary<object, object> messages)
        {
            base.OnPreOpen(messages);
    
            if (coinsLabel != null && messages != null && messages.TryGetValue("coins", out object coins))
                coinsLabel.text = coins.ToString();
        }
    
        protected override void OnOpened()
        {
            base.OnOpened();
            // the open transition has finished
        }
    
        protected override void OnPreClose()
        {
            base.OnPreClose();
        }
    
        protected override void OnWillClosed()
        {
            base.OnWillClosed();
        }
    
        // Optional: extend the open transition. Keep the base call, it ends the transition.
        protected override void StartOpenedAnimation()
        {
            base.StartOpenedAnimation();
        }
    }

    The panel prefab needs a GuiScreenTransition on the same GameObject (added automatically by RequireComponent).

    Custom transitions (onCustomTransition and replaceBuiltinTransition) #

    A host script can attach its own animation (for example a DOTween sequence or a coroutine) to any panel that calls RunTransition, which GuiTweenPanel, GuiAnimatorControlledPanel and your own panels can do. The subscriber receives (bool opening, Action finished) and must call finished() once when its animation is over. Until then the panel counts as busy (GuiManager.IsBusy stays true and the transition blocker stays visible).

    • With replaceBuiltinTransition off (default), the built-in and the custom transition run in parallel and the lifecycle waits for both.
    • With replaceBuiltinTransition on, only the custom transition runs.

    Using RunTransition in your own panel:

    C#
    using System;
    using System.Collections;
    using UMU.GuiManager;
    using UnityEngine;
    
    public class ScalePanel : GuiPanel
    {
        protected override void StartOpenedAnimation()
        {
            // playBuiltin receives the callback to invoke when the built-in part ends.
            RunTransition(true, finished => StartCoroutine(Scale(0f, 1f, finished)));
        }
    
        protected override void StartClosedAnimation()
        {
            RunTransition(false, finished => StartCoroutine(Scale(1f, 0f, finished)));
        }
    
        private IEnumerator Scale(float from, float to, Action finished)
        {
            float elapsed = 0f;
    
            while (elapsed < 0.25f)
            {
                elapsed += Time.unscaledDeltaTime;
                transform.localScale = Vector3.one * Mathf.Lerp(from, to, elapsed / 0.25f);
                yield return null;
            }
    
            transform.localScale = Vector3.one * to;
            finished();
        }
    }

    Subscribing from a host script. Put it on an object that stays active (not on the panel itself, because a closed panel is deactivated):

    C#
    using System;
    using System.Collections;
    using UMU.GuiManager;
    using UnityEngine;
    
    public class ShopPanelDecor : MonoBehaviour
    {
        public string panelId = "Shop";
        public RectTransform title;
    
        private GuiPanel panel;
    
        private void Start()
        {
            panel = GuiManager.Instance.GetPanelWithId(panelId); // loads the panel when needed
    
            if (panel == null)
                return;
    
            panel.replaceBuiltinTransition = true; // the custom transition owns the animation
            panel.onCustomTransition += Play;
        }
    
        private void OnDestroy()
        {
            if (panel != null)
                panel.onCustomTransition -= Play;
        }
    
        private void Play(bool opening, Action finished)
        {
            StartCoroutine(Run(opening, finished));
        }
    
        private IEnumerator Run(bool opening, Action finished)
        {
            float elapsed = 0f;
            float from = opening ? 0f : 1f;
            float to = opening ? 1f : 0f;
    
            while (elapsed < 0.4f)
            {
                elapsed += Time.unscaledDeltaTime;
                title.localScale = Vector3.one * Mathf.Lerp(from, to, elapsed / 0.4f);
                yield return null;
            }
    
            title.localScale = Vector3.one * to;
            finished(); // required, exactly once
        }
    }

    Deriving from GuiManager #

    The static Instance pattern hides the base property and casts the protected instance field. instance is assigned in Awake, so use Instance from Start onward or from other objects' Start.

    C#
    using System.Collections;
    using System.Collections.Generic;
    using UMU.GuiManager;
    using UnityEngine;
    
    public class MyGuiManager : GuiManager
    {
        public static new MyGuiManager Instance => (MyGuiManager)instance;
    
        // Called from Awake. Replaces overriding Awake, which is not virtual.
        protected override void Initializer()
        {
            base.Initializer(); // SetupInputModules, external lists, preloading
            Debug.Log("MyGuiManager initialized");
        }
    
        protected override void SetupInputModules()
        {
            base.SetupInputModules();
        }
    
        protected override void Start()
        {
            base.Start(); // opens the initially open panel
        }
    
        protected override void Update()
        {
            base.Update(); // back input, transition blocker, selection watcher
        }
    
        protected override void OnDestroy()
        {
            base.OnDestroy();
        }
    
        protected override void CheckBackInput()
        {
            base.CheckBackInput();
        }
    
        protected override void OnClickSelectableItem(IGuiSelectable selectable)
        {
            base.OnClickSelectableItem(selectable);
            // for example, play a click sound
        }
    
        // Panel events. Keep the base call, it raises the public Action events.
        protected override void OnPreOpenPanel(string id) { base.OnPreOpenPanel(id); }
        protected override void OnPreClosePanel(string id) { base.OnPreClosePanel(id); }
        protected override void OnOpenedPanel(string id) { base.OnOpenedPanel(id); }
        protected override void OnClosedPanel(string id) { base.OnClosedPanel(id); }
    
        // Popup events.
        protected override void OnPreOpenPopup(string id) { base.OnPreOpenPopup(id); }
        protected override void OnPreClosePopup(string id) { base.OnPreClosePopup(id); }
        protected override void OnOpenedPopup(string id) { base.OnOpenedPopup(id); }
        protected override void OnClosedPopup(string id) { base.OnClosedPopup(id); } // keeps popup callbacks and button locking working
    
        // The default loader uses Resources.Load with the panel id. You can wrap it, for example to log failures.
        protected override T LoadPrefab<T>(Transform root, string id, Dictionary<string, T> loaded)
        {
            T panel = base.LoadPrefab(root, id, loaded);
    
            if (panel == null)
                Debug.LogWarning("Panel prefab not found: " + id);
    
            return panel;
        }
    }

    Loading panels from a source other than Resources (Addressables, asset bundles). Init() is internal, so a LoadPrefab override cannot initialize a panel it instantiated itself. Use RegisterPanel instead, before the first OpenPanel or OpenPopup of that id. Do not call RegisterPanel from inside LoadPrefab: the manager subscribes its panel handlers again after LoadPrefab returns, so they would be subscribed twice.

    RegisterPanel(string id, GuiPanel panel, bool isPopup = false) parents the instance under the panel or popup root, initializes it, subscribes the manager events, deactivates it, and returns it. If the id is already registered, the existing panel is returned and the new instance is not used (destroy it yourself).

    C#
    using System.Collections;
    using UMU.GuiManager;
    using UnityEngine;
    
    public class MyGuiManager2 : GuiManager
    {
        public static new MyGuiManager2 Instance => (MyGuiManager2)instance;
    
        // Asynchronous loading, then registration, then opening.
        // The same shape works with Addressables: replace the load step with the
        // loaded prefab from your Addressables request.
        public IEnumerator LoadAndOpen(string id, string resourcesPath, bool isPopup)
        {
            if (!(isPopup ? IsLoadedPopup(id) : IsLoadedPanel(id)))
            {
                ResourceRequest request = Resources.LoadAsync<GuiPanel>(resourcesPath);
                yield return request;
    
                GuiPanel prefab = request.asset as GuiPanel;
    
                if (prefab == null)
                    yield break;
    
                RegisterPanel(id, Instantiate(prefab), isPopup);
            }
    
            if (isPopup)
                OpenPopup(id);
            else
                OpenPanel(id);
        }
    }

    Start it with StartCoroutine(MyGuiManager2.Instance.LoadAndOpen("Shop", "Panels/Shop", false)); from any MonoBehaviour. Without any code, the External Panels and External Popups lists register prefabs by their prefab name at startup.

    Opening, closing, messages and popup results #

    Messages are an arbitrary Dictionary<object, object>; they reach OnPreOpen(messages) and the On Pre Open inspector event.

    C#
    using System.Collections.Generic;
    using UMU.GuiManager;
    using UnityEngine;
    
    public class MenuController : MonoBehaviour
    {
        public void OpenShop()
        {
            var messages = new Dictionary<object, object>
            {
                { "coins", 120 },
                { "title", "Shop" }
            };
    
            GuiManager.Instance.OpenPanel("Shop", messages);
        }
    
        public void Back()
        {
            GuiManager.Instance.BackToPreviousPanel();
        }
    
        public void AskQuit()
        {
            // The callback runs when the popup has closed. It receives the popup panel;
            // read panel.result for the value the popup stored before closing.
            GuiManager.Instance.OpenPopup("ConfirmQuit", popup =>
            {
                bool confirmed = popup.result is bool answer && answer;
    
                if (confirmed)
                    Application.Quit();
            });
        }
    }

    The popup stores its result and closes itself with its own id:

    C#
    using UMU.GuiManager;
    
    public class ConfirmQuitPopup : GuiTweenPanel
    {
        // Assign these methods to the OnClick events of the buttons.
        public void Yes()
        {
            result = true;
            GuiManager.Instance.ClosePopup(id);
        }
    
        public void No()
        {
            result = false;
            GuiManager.Instance.ClosePopup(id);
        }
    }

    Notes:

    • There is one pending onClosed callback per popup id; opening the same popup again with a new callback replaces the previous one. The callback runs once, and the manager does not change result itself.
    • OpenPopup(id, messages) and OpenPopup(id, onClosed, messages) are the two overloads. Pass onClosed as a lambda or method group; passing a bare null as the second argument is ambiguous.
    • Other calls: ClosePopup(id), CloseTopPopup(), CloseAllPopup(bool removeAllRequestToOpenPopups = false), CloseCurrentPanel(), GetCurrentPanelId(), GetPreviousPanelId(), GetTopPopupId(), GetOpenedPopupIds(), HasOpenPopup, OpenPopupCount, IsBusy, PreloadPanel(id), PreloadPopup(id), SetInitiallyOpen(panelId, preloadPanelIds, preloadPopupIds) (call before the manager's Start for the initial panel).
    • A Gui selectable created at runtime inside a loaded panel is not known to the panel. Register it with panel.SelectableItemReInitialize(selectableComponent).
    • Closing a popup from a derived GuiPanel uses GuiManager.Instance.ClosePopup(id); id is assigned by the manager when the panel is loaded or registered.

    4. Audio #

    The Audio module provides a pooled SFX player, a dual-source BGM player with crossfade, a voice channel, a dedicated UI channel, AudioMixer routing, linear to decibel volume control, persisted volume and mute state, and platform pause handling. Sounds are played by string id from an AudioDatabase, so game code never binds AudioClip assets directly.

    Editor menu and window reference #

    Menu itemWhat it doesDefaultNotes
    UMU > Audio > Create UMU Main MixerCreates the reference mixer asset UmuMainMixer at Assets/UMU/Audio/Mixer/UmuMainMixer.mixer.Path is fixed.If an asset already exists at that path it is reused and nothing is changed. See the mixer layout below.
    UMU > Audio > Create Audio Import ConfigOpens a save dialog (title "Create Audio Import Config", default name AudioImportConfig, extension asset) and creates an AudioImportConfig asset with three starter rules.Rules for Assets/Audio/Music/, Assets/Audio/SFX/, Assets/Audio/Voice/.The new asset is selected after creation.
    UMU > Audio > Audio CutterOpens the Audio Cutter window. See section 2.
    Assets > UMU > Audio CutterSame window, from the Project window context menu.Enabled only when the selected asset is an AudioClip.
    Add Component > UMU > Audio > Audio ManagerAdds AudioManager.
    Add Component > UMU > Audio > Audio Settings Slider BinderAdds AudioSettingsSliderBinder.Requires a Slider on the same object.
    Add Component > UMU > Audio > GuiButton Audio BridgeAdds GuiButtonAudioBridge.Requires a GuiButton on the same object.
    Add Component > UMU > Audio > GuiPanel Audio BridgeAdds GuiPanelAudioBridge.Requires a GuiPanel on the same object.
    Add Component > UMU > Audio > UI Audio TriggerAdds UIAudioTrigger.
    Assets > Create > UMU > Audio > Audio DatabaseCreates an AudioDatabase asset.File name AudioDatabase.

    UmuMainMixer layout. The Master group contains four child groups: BGM, SFX, Voice, UI. The snapshot is named Normal. The BGM group has a Duck Volume effect (Threshold -20, Ratio 4, Attack Time 100, Release Time 500, Make-up Gain 0, Knee 10). The Voice group has a Send to the duck effect at 0 dB and the SFX group has a Send at -6 dB, so voice and SFX duck the music. Five volume parameters are exposed: MasterVolume, BGMVolume, SfxVolume, VoiceVolume, UIVolume. The mixer is built through Unity internal editor classes (reflection). If the Duck Volume effect cannot be created, the mixer is built without ducking and a warning is logged.

    Audio import automation. AudioImportPostprocessor applies the first rule of the AudioImportConfig whose path prefix matches the imported audio file. It runs only on the first import of a file (when Unity has no stored importer settings yet). A file that was already imported keeps its settings. The first AudioImportConfig asset found in the project is used.

    [AudioId] drawer. String fields marked [AudioId(channel)] are drawn as a dropdown filled from every AudioDatabase asset in the project (cached for 2 seconds). The channel selects the list: BGM lists BGM ids, Voice lists voice ids, every other channel lists SFX ids. The first entry is (none) and stores an empty string. A stored id that no database defines stays visible as <id> (missing). When no database defines any id for the channel, the plain text field is shown with an info box "No AudioDatabase ids found".

    [AudioCategory] drawer. String fields marked [AudioCategory] are drawn as a dropdown of the categories list of the owning AudioDatabase. An empty list falls back to AudioCategories.Defaults. A stored value that is not in the list is shown as <value> (missing) (or (none) when empty).

    Inspector reference #

    AudioManager #

    Persistent singleton (AudioManager.Instance). Placing it in the first scene is optional: if no instance exists after the first scene has loaded, a GameObject named "UMU AudioManager" is created automatically. A scene-placed instance always wins. A duplicate instance is destroyed. The object is moved to the root and marked DontDestroyOnLoad.

    LabelTypeDefaultBehaviour
    Mixer (header Routing)AudioMixernoneMixer that routes the channels. If empty, the manager loads UMU/Audio/UmuMainMixer from a Resources folder. In the Editor it then also tries Assets/UMU/Audio/Mixer/UmuMainMixer.mixer. Without a mixer, volumes are applied as source gains (Master through the listener volume).
    Database (header Routing)AudioDatabasenoneSound catalogue. If empty, the manager loads UMU/Audio/AudioDatabase from a Resources folder.
    Sfx Pool Size (header SFX Pool)int, minimum 110Number of pooled SFX sources. When all are busy, sources are stolen in turn.
    Pitch Variation (header SFX Pool)float, 0 to 0.20.05Random pitch of plus or minus this value, used only for SFX entries whose Pitch Range is (1, 1).
    Mute On Background (header Lifecycle)boolfalseWhen on, the whole AudioListener is paused when the app loses focus or is paused. When off, every managed source is paused and resumed individually.

    Runtime facts:

    • Sources: two BGM sources (crossfade), one voice source, 4 UI sources (child "UI Pool"), Sfx Pool Size SFX sources (child "SFX Pool"). Each source is routed to the mixer group with the channel name (Master, BGM, SFX, Voice, UI).
    • Volumes and mute states are saved to the file umu_audio_settings.json through the UMU save manager. Saving is delayed by 0.5 seconds after the last change, and also happens on pause, focus loss, destroy and quit (FlushSettings()).
    • Persisted volumes are re-applied once on the frame after Awake.
    • Linear volume (0 to 1) is converted to decibels with AudioManager.LinearToDb: values at or below 0.0001 give -80 dB, otherwise 20 * log10(value) clamped to the range -80 to 0.
    • Mixer parameter names are available as constants: ParamMasterVolume ("MasterVolume"), ParamBgmVolume ("BGMVolume"), ParamSfxVolume ("SfxVolume"), ParamVoiceVolume ("VoiceVolume"), ParamUiVolume ("UIVolume").

    Public API:

    MemberDescription
    static AudioManager InstanceThe active manager, or null before bootstrap.
    AudioMixer Mixer, AudioDatabase DatabaseCurrent references (read only).
    void SetDatabase(AudioDatabase)Replaces the database.
    void SetMixer(AudioMixer)Replaces the mixer, re-routes all sources and re-applies volumes.
    void PlaySfx(AudioClip clip, float volume = 1f)Plays a clip on the SFX pool.
    void PlaySfxById(string id)Plays a database SFX entry on the SFX channel.
    void PlayUiSfx(string id)Plays a database SFX entry on the UI channel.
    void StopAllSfx()Stops the SFX pool sources.
    void PlayMusic(AudioClip clip, bool loop = true)Crossfades to a clip over 1 second.
    void PlayMusicById(string id, float crossfadeSeconds = 1f)Crossfades to a BGM entry. Playing the clip that is already playing (or fading in) does nothing.
    void StopMusic()Stops both BGM sources.
    void PlayVoice(AudioClip clip, float volume = 1f)Plays on the single voice source (replaces the current voice line).
    void PlayVoiceById(string id)Plays a database voice entry.
    void SetMasterVolume / SetBgmVolume / SetSfxVolume / SetVoiceVolume / SetUiVolume(float linear01)Set and persist a channel volume.
    void SetVolume(AudioChannel, float linear01)Same, with a channel argument. The value is clamped to 0..1.
    float GetVolume(AudioChannel)Persisted linear volume.
    void SetMuted(AudioChannel, bool) / bool IsMuted(AudioChannel)Mute state per channel (the volume value is kept).
    void FlushSettings()Writes pending changes to disk immediately.
    static float LinearToDb(float linear01)Linear to decibel conversion.

    PlaySfxById, PlayUiSfx, PlayMusicById, PlayVoiceById do nothing when the id is empty, unknown, has no clip, or no database is assigned. The crossfade uses unscaled time, so it also finishes while Time.timeScale is 0.

    AudioChannel (enum) #

    Master, BGM, SFX, Voice, UI.

    AudioCategories (static class) #

    Constants UI ("UI"), Gameplay, Ambient, Notification, and the array Defaults containing all four. Categories are plain strings, so a project can use any names.

    AudioDatabase (ScriptableObject) #

    LabelTypeDefaultBehaviour
    Categorieslist of stringUI, Gameplay, Ambient, NotificationCategory names offered by the Category dropdown of SFX entries.
    Sfx Entriesarray of SfxEntryemptySFX definitions.
    Bgm Entriesarray of BgmEntryemptyMusic definitions.
    Voice Entriesarray of VoiceEntryemptyVoice line definitions.

    Lookups are built lazily and rebuilt in OnValidate. A duplicate id inside one array logs a warning in the Console and the later entry wins. API: SfxEntry GetSfx(string id), BgmEntry GetBgm(string id), VoiceEntry GetVoice(string id) (return null for an empty or unknown id), void BuildLookups() (call it after changing the entry arrays from code).

    SfxEntry #

    LabelTypeDefaultBehaviour
    IdstringemptyStable id used to play the sound.
    Variantsarray of AudioClipemptyOne variant is picked at random on every play. An entry without variants (or with a null picked clip) is silent.
    Categorystring (dropdown)"UI"Category label from the database list. Informational.
    Base Volumefloat, 0 to 11Volume multiplier of the entry.
    Pitch RangeVector2(1, 1)Random pitch between X and Y. (1, 1) means "use the manager's Pitch Variation". Keep values within about 0.1 of 1 to avoid a robotic sound.

    BgmEntry #

    LabelTypeDefaultBehaviour
    IdstringemptyId used by PlayMusicById.
    ClipAudioClipnoneMusic clip.
    LoopbooltrueWhether the music loops.

    VoiceEntry #

    LabelTypeDefaultBehaviour
    IdstringemptyId used by PlayVoiceById.
    ClipAudioClipnoneVoice clip.
    Base Volumefloat, 0 to 11Volume multiplier.

    UmuAudioSettings (serializable class, persisted state) #

    Fields masterVolume, bgmVolume, sfxVolume, voiceVolume, uiVolume (float, default 1) and masterMuted, bgmMuted, sfxMuted, voiceMuted, uiMuted (bool, default false). Methods GetVolume/SetVolume/GetMuted/SetMuted(AudioChannel ...), static Load(string fileName), Save(string fileName).

    AudioSettingsSliderBinder #

    Requires a Slider. On enable the slider snaps to the persisted volume (retried in Start because the lazy manager may appear after OnEnable). Moving the slider sets and saves the volume. Any slider min/max range is mapped onto 0..1.

    AudioSettingsSliderBinder component
    AudioSettingsSliderBinder component
    LabelTypeDefaultBehaviour
    ChannelAudioChannelMasterChannel controlled by the slider.
    Mute ToggleTogglenoneOptional. Switching it on mutes the channel without changing the slider value.

    GuiButtonAudioBridge #

    Requires a GuiButton. Plays on the UI channel. The hover sound is skipped for touch input and for non interactable buttons.

    GuiButtonAudioBridge component
    GuiButtonAudioBridge component
    LabelTypeDefaultBehaviour
    Click Sfx Idstring ([AudioId] SFX)"ui_click"Played when the button is clicked. Empty means silent.
    Hover Sfx Idstring ([AudioId] SFX)emptyPlayed when the pointer enters the button. Empty means silent.

    GuiPanelAudioBridge #

    Requires a GuiPanel. Plays on the UI channel. Every transition has a "Pre" slot (starts together with the animation) and a "Post" slot (when the animation has finished). Any slot may stay empty.

    GuiPanelAudioBridge component
    GuiPanelAudioBridge component
    LabelTypeDefaultBehaviour
    Open Pre Sfx Idstring ([AudioId] SFX)"popup_open"Plays when the open animation starts.
    Open Post Sfx Idstring ([AudioId] SFX)emptyPlays when the open animation has finished.
    Close Pre Sfx Idstring ([AudioId] SFX)"popup_close"Plays when the close animation starts.
    Close Post Sfx Idstring ([AudioId] SFX)emptyPlays when the close animation has finished, right before the panel is deactivated.

    UIAudioTrigger #

    Works on any uGUI element (it implements the pointer click and enter handlers). If a Selectable is on the same object, its interactable state is honoured (including a parent CanvasGroup). Plays on the UI channel. Hover is ignored for touch input.

    UIAudioTrigger component
    UIAudioTrigger component
    LabelTypeDefaultBehaviour
    Click Sfx Idstring ([AudioId] SFX)emptyPlayed on click.
    Hover Sfx Idstring ([AudioId] SFX)emptyPlayed when the pointer enters.
    Disabled Sfx Idstring ([AudioId] SFX)emptyPlayed instead of the click sound when the Selectable is not interactable.

    AudioImportConfig (editor ScriptableObject, namespace UMU.EditorTools.Audio) #

    LabelTypeDefaultBehaviour
    Rulesarray of PathRuleempty (3 starter rules when created from the menu)Checked in order. The first rule whose pathPrefix matches (case insensitive) is applied.

    PathRule fields:

    LabelTypeDefaultBehaviour
    Path PrefixstringemptyAsset path prefix, for example Assets/Audio/Music/. An empty prefix never matches.
    Load TypeAudioClipLoadTypeDecompressOnLoadApplied to the default sample settings.
    Compression FormatAudioCompressionFormatVorbisApplied to the default sample settings.
    Qualityfloat, 0 to 10.85Compression quality.
    Preload Audio DatabooltruePreload flag of the importer.
    Load In BackgroundboolfalseLoads the clip on a separate thread.

    Starter rules created by the menu item:

    Path prefixLoad typeCompressionQualityPreloadBackground
    Assets/Audio/Music/StreamingVorbis0.75offon
    Assets/Audio/SFX/DecompressOnLoadVorbis0.85onoff
    Assets/Audio/Voice/DecompressOnLoadVorbis0.80onon

    Code examples #

    Playing sounds, reading and writing volumes:

    C#
    using UnityEngine;
    using UMU.Audio;
    
    public class AudioExample : MonoBehaviour
    {
        [AudioId(AudioChannel.SFX)] [SerializeField] private string coinSfxId = "coin";
        [AudioId(AudioChannel.BGM)] [SerializeField] private string menuMusicId = "menu_theme";
        [AudioId(AudioChannel.Voice)] [SerializeField] private string greetingVoiceId = "greeting";
        [SerializeField] private AudioClip directClip;
    
        private void Start()
        {
            // Instance can be null in Awake (the lazy instance appears after the first scene loads).
            AudioManager audio = AudioManager.Instance;
            if (audio == null)
                return;
    
            audio.PlayMusicById(menuMusicId, 2f);   // 2 second crossfade
            audio.PlayVoiceById(greetingVoiceId);
            audio.PlaySfx(directClip, 0.8f);        // direct clip, no database lookup
        }
    
        public void OnCoinCollected()
        {
            AudioManager.Instance?.PlaySfxById(coinSfxId);
            // UI channel (follows the UI volume):
            AudioManager.Instance?.PlayUiSfx("ui_click");
        }
    
        public void ApplyMusicVolumeFromSlider(float value01)
        {
            AudioManager.Instance?.SetBgmVolume(value01);   // persisted automatically
        }
    
        public void ToggleSfxMute()
        {
            AudioManager audio = AudioManager.Instance;
            if (audio == null)
                return;
    
            audio.SetMuted(AudioChannel.SFX, !audio.IsMuted(AudioChannel.SFX));
            Debug.Log("SFX volume: " + audio.GetVolume(AudioChannel.SFX) + ", muted: " + audio.IsMuted(AudioChannel.SFX));
            audio.FlushSettings();                          // write to disk now
        }
    
        public void StopEverything()
        {
            AudioManager.Instance?.StopMusic();
            AudioManager.Instance?.StopAllSfx();
        }
    }

    Building a database in code and assigning it at runtime (a mixer can be assigned the same way with SetMixer):

    C#
    using UnityEngine;
    using UMU.Audio;
    
    public class RuntimeDatabaseExample : MonoBehaviour
    {
        [SerializeField] private AudioClip[] jumpVariants;
        [SerializeField] private AudioClip levelMusic;
    
        private void Start()
        {
            AudioDatabase db = ScriptableObject.CreateInstance<AudioDatabase>();
            db.sfxEntries = new[]
            {
                new SfxEntry
                {
                    id = "jump",
                    variants = jumpVariants,
                    category = AudioCategories.Gameplay,
                    baseVolume = 0.9f,
                    pitchRange = new Vector2(0.95f, 1.05f)
                }
            };
            db.bgmEntries = new[] { new BgmEntry { id = "level", clip = levelMusic, loop = true } };
            db.BuildLookups();
    
            AudioManager.Instance.SetDatabase(db);
            AudioManager.Instance.PlayMusicById("level");
            AudioManager.Instance.PlaySfxById("jump");
        }
    }

    AudioManager has no virtual members and its lifecycle methods are private, so extend it by composition. A facade that exposes game specific operations:

    C#
    using UnityEngine;
    using UMU.Audio;
    
    public static class GameAudio
    {
        public static void PlayPurchase()
        {
            AudioManager audio = AudioManager.Instance;
            if (audio == null)
                return;
    
            audio.PlayUiSfx("purchase");
        }
    
        public static void SetAllVolumes(float value01)
        {
            AudioManager audio = AudioManager.Instance;
            if (audio == null)
                return;
    
            foreach (AudioChannel channel in System.Enum.GetValues(typeof(AudioChannel)))
                audio.SetVolume(channel, value01);
        }
    }

    AudioDatabase is not sealed, so a project can derive a database type with extra data. The manager accepts it through SetDatabase or the Inspector field:

    C#
    using System.Collections.Generic;
    using UnityEngine;
    using UMU.Audio;
    
    [CreateAssetMenu(fileName = "GameAudioDatabase", menuName = "MyGame/Game Audio Database")]
    public class GameAudioDatabase : AudioDatabase
    {
        public List<string> footstepIds = new List<string> { "step_a", "step_b" };
    
        public string RandomFootstepId()
        {
            return footstepIds.Count == 0 ? null : footstepIds[Random.Range(0, footstepIds.Count)];
        }
    }

    Custom component that binds a volume slider and reacts to the stored value:

    C#
    using UnityEngine;
    using UnityEngine.UI;
    using UMU.Audio;
    
    public class VolumeLabel : MonoBehaviour
    {
        [SerializeField] private Slider slider;
        [SerializeField] private Text label;
    
        private void Update()
        {
            AudioManager audio = AudioManager.Instance;
            if (audio == null || label == null)
                return;
    
            label.text = Mathf.RoundToInt(audio.GetVolume(AudioChannel.Master) * 100f) + " %";
        }
    }

    5. Audio Cutter #

    Splits an audio clip into equal pieces measured in musical beats, optionally appends a short amplified bridging piece to each cut, and saves the pieces as WAV files or as OGG files (through an external encoder).

    Editor menu and window reference #

    UMU > Audio > Audio Cutter
    UMU > Audio > Audio Cutter
    Menu itemDescription
    UMU > Audio > Audio CutterOpens the window titled "Audio Cutter".
    Assets > UMU > Audio CutterSame window from the Project window context menu. Enabled only when the selected asset is an AudioClip.

    The window follows the Project selection: selecting an AudioClip asset automatically puts it into "Selected Clip". The window is divided into three sections.

    Source

    FieldTypeDefaultBehaviour
    Selected ClipAudioClip object fieldnoneClip to cut. A clip must be set before pressing Cut.

    Cutting

    FieldTypeDefaultBehaviour
    BPMfloat177Tempo. One beat lasts 60 / BPM seconds.
    Beat Number Per Piecefloat8Length of each piece in beats.
    Append One Fourth BridgingtoggleonAppends to every piece a bridging part of one quarter of a beat, taken from the start of the following piece.
    One Fourth Bridging Amplifierfloat0.05Gain applied to the bridging part. Visible only when the toggle above is on.
    Min File Name Indexint1Number of the first output file.
    Special Charactertext, one character_Separator between the clip name and the index. An empty value becomes _.

    Output

    FieldTypeDefaultBehaviour
    Out Pathtext + button "Use Selected Folder"emptyOutput folder, relative to the project Assets folder (for example Audio\Pieces). "Use Selected Folder" takes the folder currently selected in the Project window.
    Qualityslider, -2 to 104Vorbis quality passed to the encoder. Used for OGG output only.
    Out Channelslider, 1 to 21With OGG output, 1 downmixes multi-channel sources to mono and 2 keeps the channels. Used for OGG output only.
    Out Extensionenum wav, oggwavOutput format.
    Ogg Encoder (oggenc)text + button "Browse"emptyPath of an external oggenc executable (vorbis-tools or oggenc2). Visible only when Out Extension is ogg. UMU does not ship an encoder.

    Button Cut runs the operation.

    Behaviour details:

    • File names are <clip name><special character><two digit index>, for example track_01.wav, track_02.wav. Indexes below 10 are padded with a zero.
    • The WAV file is always written first (16 bit PCM, source sample rate and channel count).
    • With OGG output the encoder is called for each piece with the arguments: input wav, -o output ogg, --downmix (only when Out Channel is 1 and the source has more than one channel), -q <Quality>, --resample 44100, -Q, --discard-comments.
    • Encoder lookup: the "Ogg Encoder (oggenc)" path if the file exists, otherwise oggenc2 and then oggenc (with and without .exe) on the PATH. If none is found, a dialog asks to set the path, add the encoder to the PATH, or switch to wav, and nothing is cut.
    • With OGG output, the intermediate .wav files written by the current run are deleted after encoding. Other files in the output folder are never touched.
    • A progress bar is shown while cutting. Pressing Cancel on it stops the cut after the current piece. When finished, the window settings are saved, the AssetDatabase is refreshed and the window closes.
    • The settings of the window are remembered between sessions.

    Inspector reference #

    The Audio Cutter has no component or ScriptableObject. All options are in the window described above. The runtime helper classes are listed in the code examples.

    Code examples #

    The same helpers the window uses are public and can be used from code (for example inside an editor script):

    C#
    using System.Collections.Generic;
    using System.IO;
    using UnityEngine;
    using UMU.AudioCutter;
    
    public static class CutterApiExample
    {
        // Cut the first half of a clip, append a quiet copy of the next 10 percent and save as WAV.
        public static void CutAndSave(AudioClip source, string folder)
        {
            AudioClip main = AudioUtility.Cut(source, 0f, 0.5f);              // start/end as 0..1 ratios
            AudioClip bridge = AudioUtility.Cut(source, 0.5f, 0.6f, 0.05f);    // last argument: amplifier
            AudioClip joined = AudioUtility.AppendClips(new List<AudioClip> { main, bridge });
    
            bool saved = AudioClipManager.Save(joined, source.name + "_part", folder, ".wav");
            Debug.Log(saved ? "Saved to " + Path.Combine(folder, source.name + "_part.wav") : "Nothing saved");
        }
    
        // Remove leading and trailing silence (samples at or below the threshold).
        public static AudioClip Trim(AudioClip clip)
        {
            return clip.TrimSilence(0.001f);
        }
    }

    Other public helpers: AudioUtility.RemoveDataRange(clip, fromRatio, toRatio), AudioUtility.RemoveDataExceptRange(clip, fromRatio, toRatio), AudioUtility.CreateSpeakerSpecificClip(clip, channels, targetChannel), AudioUtility.DrawWaveForm(clip, width, height, waveColor, backColor, samplesStep, channelIndex).

    6. Messenger #

    A small mailbox plus push notification system for passing data between objects and across scene loads. Messages are stored with a lifetime, and listeners can additionally subscribe to be called immediately.

    Editor menu and window reference #

    The Messenger has no menu items, windows or wizards. The Messenger MonoBehaviour is created automatically (object name UMU_Messenger) the first time Messenger.Instance is used and no instance exists in the scene. It is marked DontDestroyOnLoad.

    Inspector reference #

    The Messenger has no serialized fields.

    Message.Lifetime (enum nested in the Message struct):

    ValueMeaning
    IndefinitelyThe message stays in the mailbox until it is received with removal.
    ActualSceneThe message is dropped when a scene other than the one it was sent in is loaded.
    NextSceneThe message survives the first scene change after sending and is dropped on the next one (so it is available in the following scene).

    Message fields: lifetime, id, actualScene, nextSceneLoaded, data.

    Public API of Messenger:

    MemberDescription
    static Messenger InstanceFinds or creates the messenger.
    void SendMessage(string id, Message.Lifetime lifetime, Hashtable messageData)Stores a message with a Hashtable payload and notifies subscribers.
    void SendMessage<T>(string id, Message.Lifetime lifetime, T messageData)Stores a message with a typed payload and notifies subscribers.
    IDisposable Subscribe<T>(string id, Action<T> handler)Calls the handler immediately whenever a message with this id and a payload of type T is sent. Dispose the result to unsubscribe. Returns null if the id is empty or the handler is null. A throwing handler is logged and does not stop the other subscribers.
    List<Hashtable> ReceiveMessage(string id, bool removeMessage = true)Returns the Hashtable payloads of all messages with the id.
    List<T> ReceiveMessages<T>(string id, bool removeMessage = true)Returns all payloads of type T with the id.

    Receiving removes the matched messages from the mailbox by default (removeMessage = true). A warning is logged when no message with the id exists. Subscribing does not remove anything: a sent message still lives in the mailbox with its lifetime. When Debug Mode is on, messages dropped on scene load are logged.

    Code examples #

    C#
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UMU.Messenger;
    
    public class ScoreSender : MonoBehaviour
    {
        public void SendScore(int score)
        {
            // Typed payload, available to the next scene as well.
            Messenger.Instance.SendMessage<int>("score", Message.Lifetime.NextScene, score);
        }
    
        public void SendPlayerInfo()
        {
            var data = new Hashtable();
            data["name"] = "Anna";
            data["level"] = 7;
            Messenger.Instance.SendMessage("player_info", Message.Lifetime.ActualScene, data);
        }
    }
    
    public class ScoreReceiver : MonoBehaviour
    {
        private IDisposable scoreSubscription;
    
        private void OnEnable()
        {
            // Push style: called immediately when "score" is sent.
            scoreSubscription = Messenger.Instance.Subscribe<int>("score", OnScore);
        }
    
        private void OnDisable()
        {
            scoreSubscription?.Dispose();
            scoreSubscription = null;
        }
    
        private void OnScore(int score)
        {
            Debug.Log("Score received: " + score);
        }
    
        private void Start()
        {
            // Mailbox style: read what was sent earlier (removes it from the mailbox).
            List<int> scores = Messenger.Instance.ReceiveMessages<int>("score");
            foreach (int s in scores)
                Debug.Log("Stored score: " + s);
    
            // Peek without removing:
            List<Hashtable> info = Messenger.Instance.ReceiveMessage("player_info", false);
            if (info.Count > 0)
                Debug.Log("Player: " + info[0]["name"]);
        }
    }
    
    public class TemporarySubscription : MonoBehaviour
    {
        private void Start()
        {
            // using block: subscribed only for the duration of the block.
            using (IDisposable sub = Messenger.Instance.Subscribe<string>("chat", text => Debug.Log(text)))
            {
                Messenger.Instance.SendMessage<string>("chat", Message.Lifetime.ActualScene, "hello");
            }
        }
    }

    7. Logger #

    The Logger module contains UMU.Development.Logger (an event based logger), the LogEntry data type and the Logger Console editor window. The window shows both Unity console messages and messages sent through Logger.Log.

    Note: in code, the names Logger and LogType also exist in UnityEngine. When both namespaces are imported, qualify the UMU types (for example UMU.Development.Logger) or use a using alias.

    Editor menu and window reference #

    UMU > Development > Logger Console
    UMU > Development > Logger Console
    Menu itemDescription
    UMU > Development > Logger ConsoleOpens the "Logger Console" window.

    The window records entries only while it is open. Its content is stored between play mode changes.

    Toolbar controls (left to right):

    ControlBehaviour
    Clear (button)Removes all entries.
    Collapse (toggle)When on, an entry with the same type and message as the entry shown directly before it is hidden.
    Clear on Play (toggle)When on, all entries are removed every time the Editor enters Play mode.
    Error Pause (toggle)When on, the Editor pauses (Play mode) as soon as an Error, Assert or Exception entry arrives.
    Log counter (toggle, info icon)Shows the number of Log entries (999+ above 999). When off, Log entries are hidden from the list. On by default.
    Warning counter (toggle, warning icon)Number of Warning entries. When off, Warning entries are hidden. On by default.
    Error counter (toggle, error icon)Number of Error entries. When off, Error and Assert entries are hidden. On by default.
    Exception counter (toggle, error icon)Number of Exception entries. When off, Exception entries are hidden. On by default.

    The toggle states are kept in the Editor preferences (keys with the prefix UMU_LOGGER_). The console collects entries while its window is open, so open it before you start the game to capture everything.

    List behaviour:

    ActionResult
    Click an entrySelects it and highlights the script asset that produced it in the Project window.
    Double click an entry, or press Return on the selected entryOpens the script at the logged line.
    Up and Down arrow keysMove the selection.
    Mouse wheel or scroll barScrolls the list. The list always fills the space above the detail panel.
    Drag the splitter above the detail panelResizes the detail panel (minimum 40 px). The height is stored in the Editor preferences (UMU_LOGGER_v2_DetailHeight) and defaults to 110 px.

    Each row shows the first line of the message and the first relevant call stack line. The detail panel at the bottom shows the full message and call stack of the selected entry (selectable text, scrollable). Entries come from two sources: Unity messages (Application.logMessageReceived: Log, Warning, Error, Assert, Exception) and Logger.Log calls.

    Inspector reference #

    The Logger module has no component or ScriptableObject.

    LogEntry (serializable struct):

    FieldTypeMeaning
    logTypeUMU.Development.LogTypeEntry type.
    callStackstringStack trace text.
    callerScriptPathstringAsset path of the script that logged (parsed from the stack trace, null if not found).
    callerLineintLine number in that script (-1 if unknown).
    tagstringOptional tag.
    logMessagestringMessage text.
    collapseNumberintRepeat counter, default 1.

    UMU.Development.LogType values: Log, Warning, Assert, Error, Exception. LogEntry.Convert(UnityEngine.LogType) converts from the Unity type.

    Logger API:

    MemberDescription
    static Action<LogEntry> logEventRaised for every Logger.Log call. The Logger Console subscribes to it.
    static void Log(string log)Creates a Log level LogEntry (with the current stack trace) and raises logEvent. It does not write to the Unity Console.
    static void FirstCall(string stackTrace, out string scriptPath, out int line)Extracts the caller script path and line from a stack trace string.

    DebugLogger (non static class in UMU.Development, static methods) writes to the Unity Console and respects the Debug Mode log level (section 5):

    MethodActive when the log level is at least
    DebugLogger.Log(object msg)Log
    DebugLogger.Log(object msg, string color) (color is a hex value without #, for example "ff8800")Log
    DebugLogger.LogWarning(object msg)Warning
    DebugLogger.LogError(object msg)Error
    DebugLogger.LogException(Exception exception)Exception

    Code examples #

    C#
    using System;
    using UnityEngine;
    using UmuLogger = UMU.Development.Logger;   // avoids the clash with UnityEngine.Logger
    using UMU.Development;
    
    public class LoggerExample : MonoBehaviour
    {
        private void OnEnable()
        {
            UmuLogger.logEvent += OnEntry;
        }
    
        private void OnDisable()
        {
            UmuLogger.logEvent -= OnEntry;
        }
    
        private void Start()
        {
            // Shows up in the Logger Console window (not in the Unity Console).
            UmuLogger.Log("Player spawned");
    
            // Unity Console, filtered by the Debug Mode log level.
            DebugLogger.Log("Loaded level 3");
            DebugLogger.Log("Special event", "ff8800");
            DebugLogger.LogWarning("Low memory");
            DebugLogger.LogError("Save failed");
            DebugLogger.LogException(new InvalidOperationException("Example"));
        }
    
        private void OnEntry(LogEntry entry)
        {
            // UMU.Development.LogType, written in full because UnityEngine.LogType also exists.
            if (entry.logType == UMU.Development.LogType.Error)
                Debug.Log("Error entry from " + entry.callerScriptPath + " line " + entry.callerLine);
        }
    
        private void ParseStack()
        {
            string path;
            int line;
            UmuLogger.FirstCall(StackTraceUtility.ExtractStackTrace(), out path, out line);
            Debug.Log(path + ":" + line);
        }
    
        private void BuildEntryManually()
        {
            var entry = new LogEntry(UMU.Development.LogType.Warning, "", null, -1, "Custom entry", "MyTag");
            UmuLogger.logEvent(entry);   // delivered to every subscriber, including the Logger Console
        }
    }

    8. Debug Mode #

    Debug Mode is a project wide switch (stored in a settings asset) plus a log level. It controls DebugLogger, the in-game console, the FPS counter, and the debug options of other UMU components. Several tools are active only in the Editor and in development builds.

    Editor menu and window reference #

    Menu itemDescription
    UMU > Development > SettingsOpens the "Development Settings" window (minimum size 260 x 190).
    UMU > Development > Debug Mode SettingsOpens the "Debug Mode Settings" wizard.
    UMU > Development > Scan Scene For Zero-Z Scaled TransformsScans the open scenes. See below.
    UMU > Development > Logger ConsoleSee section 4.

    Development Settings window

    ControlTypeBehaviour
    Debug ModetoggleDebugMode.IsDebug. Applied immediately.
    Log Levelenum popup (DebugMode.LOG)DebugMode.LogType. Applied immediately.
    Keystore Passwordpassword fieldAndroid keystore password.
    Add Password to PlayerSettings (button)If the field is not empty, sets the Android keystore and key alias password in Player Settings and saves the password in the file password.txt located two folders above Assets (the parent of the project folder), as plain text. If the field is empty, an error is logged.
    Open Player Settings (button)Opens Edit > Project Settings > Player.

    When the Editor loads, a saved password.txt is read automatically and applied to the Android keystore and key alias password in Player Settings.

    Debug Mode Settings wizard (button "Apply")

    FieldTypeDefaultBehaviour
    Debug Modetoggle (tooltip "Change Debug Mode")current valueSets DebugMode.IsDebug.
    Log Levelenum (tooltip "Change Log Mode")current valueSets DebugMode.LogType.

    Both fields start with the current stored values. Apply writes both values.

    Scan Scene For Zero-Z Scaled Transforms. Finds every Transform (including inactive ones) in the open scenes whose localScale.z is zero or negative. Each one is logged as an error with its hierarchy path (clicking the message pings the object), all of them are selected, and a summary warning is logged. If none is found, an info message is logged.

    Inspector reference #

    DebugModeSettings (ScriptableObject) #

    Stored at Assets/UMU/Debug Mode/Resources/DebugModeSettings.asset and created automatically in the Editor if missing. It is loaded at runtime from Resources under the name DebugModeSettings.

    DebugModeSettings asset
    DebugModeSettings asset
    LabelTypeDefaultBehaviour
    Is DebugboolfalseMaster debug switch (DebugMode.IsDebug).
    Log TypeDebugMode.LOGLogLog level (DebugMode.LogType). The shipped asset uses Log, so all messages are shown. Setting None turns Unity Debug.Log output off for the whole project.

    DebugMode.LOG values and effect (a level enables itself and every level listed before it, except None):

    ValueEnabled message kinds
    NoneNone. Debug.unityLogger.logEnabled is set to false.
    ExceptionException
    ErrorException, Error
    WarningException, Error, Warning
    LogException, Error, Warning, Log

    Debug.unityLogger.logEnabled is true for any level other than None. In the Editor, changing the level also updates the Player Settings: usePlayerLog, logObjCUncaughtExceptions and the stack trace type of each log type (ScriptOnly when the level is enabled, None otherwise).

    DebugMode API: static bool IsDebug { get; set; }, static DebugMode.LOG LogType { get; set; }, static bool LogLevel(DebugMode.LOG log) (true if that kind is enabled), static void Awake() (re-applies the log mode). In the Editor, setting a value also saves the asset. Reading IsDebug returns false and LogType returns None when no settings asset exists.

    FpsCounter (MonoBehaviour, requires a uGUI Text) #

    Shows the frame rate in the text. Active only in the Editor and development builds (in release builds the component disables itself). The text is green at 60 FPS or more, yellow below 60, red below 30.

    FpsCounter component
    FpsCounter component
    LabelTypeDefaultBehaviour
    Update Intervalfloat0.5Seconds between text updates.
    Require Debug ModebooltrueWhen on, the text is visible only while DebugMode.IsDebug is true.

    InGameDebugConsole (MonoBehaviour) #

    Draws a log overlay with IMGUI for device testing. Works only in the Editor and development builds (the component destroys itself in release builds) and only while DebugMode.IsDebug is true (checked in Awake). The object is marked DontDestroyOnLoad. A "Show Logs"/"Hide Logs" button sits in the top right corner; the overlay opens automatically when an Error, Exception or Assert arrives. The overlay has the toggles Auto-scroll, Log, Warning, Error and a Clear button.

    InGameDebugConsole component
    InGameDebugConsole component
    LabelTypeDefaultBehaviour
    Max Entriesint200Ring buffer size. The oldest entry is removed first.
    Overlay Height Ratiofloat0.45Overlay height as a fraction of the screen height.
    Show LogbooltrueShow Log messages.
    Show WarningbooltrueShow Warning messages.
    Show ErrorbooltrueShow Error, Exception and Assert messages.

    ScreenshotCreator (MonoBehaviour) #

    Takes a screenshot when F12 is pressed (works with the Input System package or the legacy Input Manager). Active only in the Editor and development builds. Files are saved as PNG to <persistentDataPath>/Screenshots/Screenshot <yyyyMMddHHmmss> <width>x<height>.png.

    ScreenshotCreator component
    ScreenshotCreator component
    LabelTypeDefaultBehaviour
    Res Widthint2550Width of the multi camera capture.
    Res Heightint3300Height of the multi camera capture.
    Cameras In Orderarray of CameraemptyWhen empty, the current screen is captured with ScreenCapture. Otherwise the cameras are rendered in the given order into a render texture of Res Width x Res Height.

    API: void TakeScreenshot() (ignored while a capture is running), static string ScreenShotName(int width, int height).

    Code examples #

    C#
    using UnityEngine;
    using UMU.Development;
    
    public class DebugModeExample : MonoBehaviour
    {
        private void Start()
        {
            // Read the state
            Debug.Log("Debug mode: " + DebugMode.IsDebug + ", level: " + DebugMode.LogType);
    
            // Change the state (saved to the settings asset in the Editor)
            DebugMode.LogType = DebugMode.LOG.Warning;
            DebugMode.IsDebug = true;
    
            // Level check before doing expensive logging work
            if (DebugMode.LogLevel(DebugMode.LOG.Log))
                Debug.Log("Verbose output is enabled");
    
            // Debug only features
            if (DebugMode.IsDebug && (Application.isEditor || Debug.isDebugBuild))
                ShowCheatPanel();
        }
    
        private void ShowCheatPanel()
        {
            Debug.Log("Cheat panel shown");
        }
    }

    Adding the debug tools from code:

    C#
    using UnityEngine;
    using UnityEngine.UI;
    using UMU.Development;
    
    public class DebugToolsBootstrap : MonoBehaviour
    {
        [SerializeField] private Text fpsText;
        [SerializeField] private Camera mainCamera;
    
        private void Awake()
        {
            var console = gameObject.AddComponent<InGameDebugConsole>();
            console.maxEntries = 500;
            console.overlayHeightRatio = 0.6f;
            console.showLog = false;
    
            if (fpsText != null)
            {
                var fps = fpsText.gameObject.AddComponent<FpsCounter>();
                fps.updateInterval = 0.25f;
                fps.requireDebugMode = true;
            }
    
            var shots = gameObject.AddComponent<ScreenshotCreator>();
            shots.resWidth = 1920;
            shots.resHeight = 1080;
            shots.camerasInOrder = new[] { mainCamera };
    
            // Manual capture (same as pressing F12)
            shots.TakeScreenshot();
        }
    }

    9. Internet Checker #

    Lightweight reachability check. It resolves host names (DNS) on a background thread and reports the result on the main thread. The callback is invoked exactly once. It also works in edit mode (the component is [ExecuteAlways]; in the Editor a queue pump drains pending callbacks).

    Editor menu and window reference #

    The Internet Checker has no menu items, windows or wizards.

    Inspector reference #

    InternetChecker (MonoBehaviour) #

    InternetChecker component
    InternetChecker component
    LabelTypeDefaultBehaviour
    Default Urlsstring arrayexample.com, unity3d.comHost names used by IsConnected(Action<bool>) and as a fallback when an empty list is passed. The check reports online as soon as one host resolves.
    Direct Network OffboolfalseDebug only. Forces every check to report offline. Works only in the Editor or development builds with Debug Mode on. Never ship enabled.
    Artificial DelayboolfalseDebug only. Waits "Artificial Delay Seconds" before answering. Same conditions as above. In edit mode the delay is skipped.
    Artificial Delay Secondsfloat3Delay used when Artificial Delay is on.

    API:

    MemberDescription
    static InternetChecker InstanceFinds the instance in the scene (may be null).
    static InternetChecker EnsureInstance()Returns the instance, creating a GameObject "InternetChecker" if none exists.
    void IsConnected(Action<bool> result)Checks defaultUrls.
    void IsConnected(string url, Action<bool> result)Checks one host.
    void IsConnected(string[] urlArray, Action<bool> result)Checks the given hosts in order. A null or empty array uses defaultUrls.
    static void DrainQueueNow()Edit mode pump (called by the Editor).

    A host name that fails to resolve is reported with a warning (through DebugLogger, so it follows the Debug Mode log level). A null callback does nothing.

    Code examples #

    C#
    using UnityEngine;
    using UMU.Net;
    
    public class ConnectivityExample : MonoBehaviour
    {
        private void Start()
        {
            InternetChecker checker = InternetChecker.EnsureInstance();
    
            // Default hosts
            checker.IsConnected(online => Debug.Log("Online (default hosts): " + online));
    
            // One host
            checker.IsConnected("example.org", online => Debug.Log("example.org reachable: " + online));
    
            // Custom list, any host that resolves counts as online
            string[] hosts = { "my-game-server.example.com", "cloudflare.com", "unity3d.com" };
            checker.IsConnected(hosts, online =>
            {
                if (online)
                    StartOnlineFeatures();
                else
                    ShowOfflineMessage();
            });
        }
    
        private void StartOnlineFeatures() { Debug.Log("Online features started"); }
        private void ShowOfflineMessage() { Debug.Log("No connection"); }
    }

    Changing the defaults from code:

    C#
    using UnityEngine;
    using UMU.Net;
    
    public class ConnectivityConfig : MonoBehaviour
    {
        private void Awake()
        {
            InternetChecker checker = InternetChecker.EnsureInstance();
            checker.defaultUrls = new[] { "my-game-server.example.com" };
        }
    }

    10. Hardware Detector #

    Evaluates a rule set against the live device (SystemInfo, screen resolution and, on iOS, the device generation) and returns a performance tier: Low, Medium or High. The result can drive Unity's quality level.

    Editor menu and window reference #

    The Hardware Detector has no menu items, windows or wizards and no custom inspector. Configure it through the default Inspector of the HardwareDetector component (it has no Add Component menu entry; find it under Scripts or add it by name).

    Inspector reference #

    HardwareDetector (MonoBehaviour) #

    HardwareDetector component
    HardwareDetector component
    LabelTypeDefaultBehaviour
    RulesHardwareDetectionRulesemptyThe rule set. See below.
    Evaluate On Start (header Evaluation)booltrueEvaluates automatically in Start.
    Only First RunbooltrueWhen on, the automatic evaluation runs only once per install. The marker is stored in PlayerPrefs.
    First Run KeystringUMU_HardwareDetector_FirstRunPlayerPrefs key of that marker.
    Apply To Unity Quality (header Quality application)boolfalseWhen on, the tier is applied with QualitySettings.SetQualityLevel(level, true).
    Quality Level By Tierint array0, 1, 2Quality level index per tier: element 0 is Low, 1 is Medium, 2 is High. An index outside the number of quality levels is ignored.

    Event and methods:

    MemberDescription
    static HardwareDetector InstanceFinds the instance in the scene (may be null).
    event Action<HardwareTier> onTierEvaluatedRaised after every Evaluate() call.
    HardwareTier Evaluate()Evaluates the rules against the device, applies the quality level when enabled, raises the event and returns the tier.
    void ApplyToUnityQuality(HardwareTier tier)Applies a tier through qualityLevelByTier.

    HardwareTier (enum) #

    Low = 0, Medium = 1, High = 2.

    HardwareDetectionRules #

    LabelTypeDefaultBehaviour
    Ruleslist of HardwareDetectionRuleemptyEvaluated on every platform. The lowest matching tier wins.
    iOS Rules (header iOS)list of iOSDetectionRuleemptyEvaluated only on iOS devices.

    HardwareDetectionRule #

    LabelTypeDefaultBehaviour
    TierHardwareTierLowTier applied when any filter group below fully matches. Tooltip: "Tier applied when any [OR] group below fully matches the device."
    Filter Groups (header [OR])list of HardwareFilterGroupemptyGroups combined with OR.

    HardwareFilterGroup #

    LabelTypeDefaultBehaviour
    Terms (header [AND])list of HardwareFilterTermemptyEvery term must match for the group to match. A group without terms never matches.

    HardwareFilterTerm #

    LabelTypeDefaultBehaviour
    InfoHardwareDeviceInfoGraphicsDeviceNameWhat is compared.
    Int Filter OperationIntFilterOperationEqualHow the number is compared.
    Int Filterint-1Value to compare against. A value of -1 or lower disables the numeric check (it always passes).
    String Filterslist of stringemptyUsed only by GraphicsDeviceName.

    HardwareDeviceInfo values:

    ValueCompared value
    GraphicsDeviceNameSystemInfo.graphicsDeviceName (lower case). Every non empty string filter must be contained in the name (case insensitive). Then, if the name contains a number, the first number found is compared with Int Filter (for example 730 in "Adreno (TM) 730"). If the name has no number the numeric check passes.
    SystemMemorySizeSystemInfo.systemMemorySize (MB).
    ProcessorCountSystemInfo.processorCount.
    ProcessorFrequencySystemInfo.processorFrequency (MHz).
    ScreenResolutionScreen.currentResolution width multiplied by height (pixels).

    IntFilterOperation values: Equal (device value equals Int Filter), LessThanOrEqual (device value is at most Int Filter), GreaterThanOrEqual (device value is at least Int Filter).

    iOSDetectionRule #

    LabelTypeDefaultBehaviour
    TierHardwareTierLowTier applied when the rule matches.
    Generation OperationIntFilterOperationEqualComparison between the device generation and the rule generation.
    GenerationUnityEngine.iOS.DeviceGenerationfirst enum valueiOS device generation to compare against.

    Evaluation algorithm #

    1. The result starts as High.
    2. Each rule in Rules whose filter groups include at least one fully matching group lowers the result to the rule tier if that tier is lower than the current result.
    3. On iOS devices, each matching iOSRules entry lowers the result in the same way.
    4. A null rule set returns High.

    HardwareTierEvaluator (static class) is usable on its own: static HardwareTier Evaluate(HardwareDetectionRules rules) and static void LogAllDeviceData() (writes the key SystemInfo values through DebugLogger).

    Code examples #

    Reacting to the result and driving the quality level:

    C#
    using UnityEngine;
    using UMU.Hardware;
    
    public class HardwareExample : MonoBehaviour
    {
        [SerializeField] private HardwareDetector detector;
    
        private void OnEnable()
        {
            detector.onTierEvaluated += OnTier;
        }
    
        private void OnDisable()
        {
            detector.onTierEvaluated -= OnTier;
        }
    
        private void OnTier(HardwareTier tier)
        {
            switch (tier)
            {
                case HardwareTier.Low:
                    Application.targetFrameRate = 30;
                    break;
                case HardwareTier.Medium:
                    Application.targetFrameRate = 45;
                    break;
                default:
                    Application.targetFrameRate = 60;
                    break;
            }
        }
    
        public void ReEvaluate()
        {
            HardwareTier tier = detector.Evaluate();
            Debug.Log("Tier: " + tier);
        }
    }

    Creating the detector and its rules fully in code:

    C#
    using System.Collections.Generic;
    using UnityEngine;
    using UMU.Hardware;
    
    public class HardwareRulesInCode : MonoBehaviour
    {
        private void Awake()
        {
            var detector = gameObject.AddComponent<HardwareDetector>();
            detector.evaluateOnStart = false;
            detector.applyToUnityQuality = true;
            detector.qualityLevelByTier = new[] { 0, 1, 2 };
    
            // Low tier if the device has 3 GB RAM or less, OR it has an Adreno GPU numbered 530 or lower.
            var lowByMemory = new HardwareFilterGroup
            {
                terms = new List<HardwareFilterTerm>
                {
                    new HardwareFilterTerm
                    {
                        info = HardwareDeviceInfo.SystemMemorySize,
                        intFilterOperation = IntFilterOperation.LessThanOrEqual,
                        intFilter = 3072
                    }
                }
            };
    
            var lowByGpu = new HardwareFilterGroup
            {
                terms = new List<HardwareFilterTerm>
                {
                    new HardwareFilterTerm
                    {
                        info = HardwareDeviceInfo.GraphicsDeviceName,
                        intFilterOperation = IntFilterOperation.LessThanOrEqual,
                        intFilter = 530,
                        stringFilters = new List<string> { "adreno" }
                    }
                }
            };
    
            // Medium tier if at most 6 CPU cores AND a screen with 2 million pixels or fewer.
            var mediumGroup = new HardwareFilterGroup
            {
                terms = new List<HardwareFilterTerm>
                {
                    new HardwareFilterTerm { info = HardwareDeviceInfo.ProcessorCount, intFilterOperation = IntFilterOperation.LessThanOrEqual, intFilter = 6 },
                    new HardwareFilterTerm { info = HardwareDeviceInfo.ScreenResolution, intFilterOperation = IntFilterOperation.LessThanOrEqual, intFilter = 2000000 }
                }
            };
    
            detector.rules = new HardwareDetectionRules
            {
                rules = new List<HardwareDetectionRule>
                {
                    new HardwareDetectionRule { tier = HardwareTier.Low, filterGroups = new List<HardwareFilterGroup> { lowByMemory, lowByGpu } },
                    new HardwareDetectionRule { tier = HardwareTier.Medium, filterGroups = new List<HardwareFilterGroup> { mediumGroup } }
                },
                iOSRules = new List<iOSDetectionRule>
                {
                    new iOSDetectionRule
                    {
                        tier = HardwareTier.Low,
                        generationOperation = IntFilterOperation.LessThanOrEqual,
                        generation = UnityEngine.iOS.DeviceGeneration.iPhone6
                    }
                }
            };
    
            detector.onTierEvaluated += tier => Debug.Log("Detected tier: " + tier);
            detector.Evaluate();
        }
    }

    Using the evaluator without a component:

    C#
    using System.Collections.Generic;
    using UnityEngine;
    using UMU.Hardware;
    
    public static class StandaloneTierCheck
    {
        public static HardwareTier Check()
        {
            var rules = new HardwareDetectionRules();
            rules.rules.Add(new HardwareDetectionRule
            {
                tier = HardwareTier.Low,
                filterGroups = new List<HardwareFilterGroup>
                {
                    new HardwareFilterGroup
                    {
                        terms = new List<HardwareFilterTerm>
                        {
                            new HardwareFilterTerm { info = HardwareDeviceInfo.ProcessorCount, intFilterOperation = IntFilterOperation.LessThanOrEqual, intFilter = 2 }
                        }
                    }
                }
            });
    
            HardwareTierEvaluator.LogAllDeviceData();
            return HardwareTierEvaluator.Evaluate(rules);
        }
    }

    11. Common and Utility #

    Editor menu and window reference #

    Menu items #

    Menu itemOpensNotes
    UMU > Plugin Save FilesThe Plugin Save Files windowLists the save file of every UMU module that keeps one.
    UMU > AboutThe About windowFixed size window, not resizable. Always the last item of the UMU menu, separated from the other items.

    Plugin Save Files window (UMU > Plugin Save Files) #

    The window is a Unity wizard. It is built from the public fields of the wizard class, so it uses the standard Inspector look.

    UMU > Plugin Save Files
    UMU > Plugin Save Files
    Control or field (as shown)What it doesDefaultNotes
    Public Data > Plugin Save Files (list)One element per UMU module that stores settings in a .umusf save file. Shipped modules that appear: Audio Cutter, Gui Manager, Xcode Project Editor.Filled each time the window opensThe list is rebuilt every time the window is opened. Changes made to the list are not stored anywhere.
    Plugin Save Files > Element > Plugin NameName of the module.Module nameRead it as a label. It is the file name without the .umusf extension.
    Plugin Save Files > Element > PathPath of the save file relative to the Assets folder, for example UMU\Common\umu Save Files\Gui Manager.umusf.Path of the fileThe file is searched under Assets by name. If it does not exist it is created with the content {} in the folder Assets/UMU/Common/umu Save Files (or in an existing folder called umu Save Files).
    Ok buttonCloses the wizard.n/aIt does not write anything.

    Additional behaviour:

    • Opening the window checks every module save file and creates the missing ones.
    • After every asset import the same check runs again, so a deleted save file is recreated automatically.
    • Save files use the extension .umusf (constant UMU_Constants.SaveFileExtension).

    About window (UMU > About) #

    UMU > About
    UMU > About
    ElementWhat it does
    Title "UMU"Plain title.
    Link "UMU on bmsoft1024.com: documentation, release notes"Left click opens https://bmsoft1024.com/product/umu in the browser.
    Banner imagePlugin banner. The window height follows the image aspect ratio.
    Link "UMU on the Unity Asset Store"Left click opens https://u3d.as/4ejN.
    Logo and text "Created and edited by BMSoft1024 (Miklós Borsi), 2026"Information only.

    The window is 500 pixels wide, with min and max size locked to the content height.

    Constants (UMU.UMU_Constants) #

    ConstantValue
    UMU"UMU"
    Assets"Assets"
    Assets_UMU"Assets/UMU"
    Settings"Settings"
    SaveFileExtension".umusf"
    SubMark"/"
    SaveDirectoryName"umu Save Files"
    SaveDirectoryPath"UMU/Common/umu Save Files"
    AboutPlugin"About"
    AudioPlugin"Audio"
    AudioCutterPlugin"Audio Cutter"
    GuiManagerPlugin"Gui Manager"
    XcodeProjectEditorPlugin"Xcode Project Editor"
    PluginSaveFilesEditorPlugin"Plugin Save Files"
    DevelopmentPlugin"Development"
    LoggerPlugin"Logger"
    ConsolePlugin"Console"
    AndroidManifestPlugin"Android Manifest Editor"
    SaveFileEditor"UMU/Plugin Save Files"
    AboutEditor"UMU/About"
    XcodeProjectEditor"UMU/Xcode Project Editor"
    AndroidManifestEditor"UMU/Android Manifest Editor"
    GuiManagerEditor"UMU/Gui Manager"
    AudioEditor"UMU/Audio"
    AudioCutterEditor"UMU/Audio/Audio Cutter"
    DevelopmentEditor"UMU/Development"

    UMU_Constants.GetSaveFile(string pluginID) returns the full path of the save file pluginID + ".umusf", creating it when missing.

    Inspector reference #

    Property attributes and decorators #

    These attributes change how a field or method is drawn in the Inspector. They are used on your own MonoBehaviour or ScriptableObject fields.

    Attribute (namespace)TargetParametersExact Inspector behaviour
    [Button] (UMU)Parameterless method (instance or static, any access level)label (string, default null)Draws a button below the default Inspector. The button text is label, or the method name converted to a readable name when no label is given. Click records an Undo step, invokes the method and marks the object dirty. Works on MonoBehaviour and ScriptableObject types that do not have their own custom editor. Methods that take parameters are ignored.
    [ShowIf(fieldName)] (UMU)FieldfieldName: name of a serialized bool, enum or int field of the same objectThe field is shown only when the named bool is true. If the named field is not found the field is always shown.
    [ShowIf(fieldName, compareValue)] (UMU)FieldcompareValue (object)Bool condition: the field is shown when the bool equals compareValue. Enum condition: shown when the enum index (position in the declaration, starting at 0) equals (int)compareValue. Int condition: shown when the int equals (int)compareValue. Other condition field types: always shown.
    [ReadOnly] (UMU)FieldnoneThe field is drawn greyed out and cannot be edited, including children.
    [Required(message)] (UMU)Object reference fieldmessage (string, default null)While the reference is empty an error help box is drawn under the field. Default text is <Field label> is required. Has no effect on non reference fields.
    [Comment(text, messageType)] (UMU.Utility)Field or any targetcomment; messageType (CommentMessageType.None, Info, Warning, Error, default None)Draws a help box with the text above the field. The box height follows the text length.
    [ConditionalHide(defaultShow, maxDepth)] (UMU.Utility)Field or propertydefaultShow (bool, default false), maxDepth (int, default 5)The field is hidden or shown according to a [HideCondition] member of the same object (see below). defaultShow is used when the condition cannot be evaluated. maxDepth is the nesting depth searched for the condition member.
    [HideCondition(propertyName)] (UMU.Utility)Public bool field, property or method; may be used several times on one memberpropertyName: name of the field that carries [ConditionalHide]The member decides whether the named field is visible. A field or property value is converted with Convert.ToBoolean. A method is called with the field name as its single string argument and must return a bool. The target field is shown when the result is true.
    [AssetPath(...)] (UMU.Utility)string fieldsee next tableReplaces the text field with a drop down of the matching assets or folders.

    Rules for [ConditionalHide] and [HideCondition]:

    • The two attributes must be on different members. [HideCondition] on a bool field, [ConditionalHide] on the field to hide.
    • A condition field must be a public field. A condition method may be public or non public.
    • When the same condition name exists in nested serializable classes, the member whose path matches the target field most closely is used.
    • A hidden field takes no space in the Inspector.

    [AssetPath] parameters and drawer controls #

    Constructor: AssetPath(string filter, string searchInFolder, bool isUnityFilter = true, bool isDirectory = false, bool cutoffExtension = true, bool recursiveSearch = true)

    ParameterMeaning
    filterWhen isUnityFilter is true: a Unity asset search filter such as t:Texture2D or *.png-style names passed to the asset database. When false: a file system search pattern such as *.*.
    searchInFolderFolder to search. For the Unity filter it is an asset path such as Assets/Sounds. For file system mode it is a folder path such as Assets. The stored value is relative to this folder.
    isUnityFiltertrue uses the asset database, false uses plain file or folder enumeration.
    isDirectoryFile system mode only: list folders instead of files.
    cutoffExtensionRemoves the file extension from the stored value. Not applied to folders.
    recursiveSearchFile system mode only: search sub folders too.

    Drawer controls (shown for a string field with [AssetPath]):

    ControlWhat it does
    Drop down (field label)First entry is Empty. Choosing an entry stores the path relative to searchInFolder. Choosing Empty stores an empty string.
    Select button (files only)Selects the asset searchInFolder/value plus its extension in the Project window. Its tooltip shows the search folder. Not shown for folder lists.
    o buttonTurns the search filter on or off. When on, the Select button is replaced by a text box; typing narrows the drop down (case insensitive contains match). Turning it off restores the previous selection.

    If the field is not a string the drawer shows the message "Use AssetPath with string."

    Code examples #

    Attributes on fields and methods #

    C#
    using UnityEngine;
    using UMU;
    using UMU.Utility;
    
    public enum SpawnMode { Single, Burst, Stream }
    
    public class Spawner : MonoBehaviour
    {
        [Comment("Configure how enemies are spawned.", CommentMessageType.Info)]
        public SpawnMode mode;
    
        [ShowIf("mode", SpawnMode.Burst)]
        public int burstCount = 5;
    
        public bool useCustomName;
    
        [ShowIf("useCustomName")]
        public string customName;
    
        [ReadOnly]
        public int spawnedSoFar;
    
        [Required("Assign the enemy prefab.")]
        public GameObject enemyPrefab;
    
        [AssetPath("*.*", "Assets", false, true)]
        public string outputFolder;
    
        [Button]
        private void SpawnOne()
        {
            if (enemyPrefab != null)
                Instantiate(enemyPrefab, transform.position, Quaternion.identity);
    
            spawnedSoFar++;
        }
    
        [Button("Reset counter")]
        public void ResetCounter()
        {
            spawnedSoFar = 0;
        }
    }

    ConditionalHide with HideCondition #

    C#
    using UnityEngine;
    using UMU.Utility;
    
    public class WeaponSettings : MonoBehaviour
    {
        // The bool decides about the field named "reloadTime".
        [HideCondition("reloadTime")]
        public bool needsReload;
    
        [ConditionalHide]
        public float reloadTime = 1.5f;
    
        public int level;
    
        // A method can decide too. It receives the field name and returns a bool.
        [HideCondition("legendaryBonus")]
        private bool ShowLegendaryBonus(string propertyName)
        {
            return level >= 10;
        }
    
        [ConditionalHide(defaultShow: true)]
        public float legendaryBonus = 2f;
    }

    AssetUtility #

    C#
    using UnityEngine;
    using UMU.Utility;
    
    public class AssetUtilityExample : MonoBehaviour
    {
        private void Start()
        {
            // Instantiate a prefab from a Resources path and remove the "(Clone)" suffix from its name.
            GameObject bullet = AssetUtility.CreateInstance<GameObject>("Prefabs/Bullet");
    
            // Same with position and rotation.
            GameObject bullet2 = AssetUtility.CreateInstance<GameObject>("Prefabs/Bullet", Vector3.zero, Quaternion.identity);
    
            // Convert an absolute path inside the project to an "Assets/..." path.
            string relative = AssetUtility.AbsoluteToRelativePath(Application.dataPath + "/Textures/Hero.png");
            Debug.Log(relative); // Assets/Textures/Hero.png
        }
    
    #if UNITY_EDITOR
        [ContextMenu("Find asset by name")]
        private void FindAsset()
        {
            // Searches Application.dataPath recursively; the name is a file search pattern.
            string path = AssetUtility.GetAssetPathWithName("Hero.png");
            Texture2D tex = AssetUtility.GetAssetWithName<Texture2D>("Hero.png"); // editor only, null in a player
            Debug.Log(path + " " + tex);
        }
    #endif
    }

    GetAssetWithName<T> needs the editor hook that UMU registers at editor load, so it returns null outside the editor.

    SaveFileManager #

    C#
    using UnityEngine;
    using UMU.Utility;
    
    public class SaveFileExample : MonoBehaviour
    {
        private void OnEnable()
        {
            SaveFileManager.onFileNotExistsEvent += OnMissing;
            SaveFileManager.onExistsFileNotOverrideEvent += OnExists;
        }
    
        private void OnDisable()
        {
            SaveFileManager.onFileNotExistsEvent -= OnMissing;
            SaveFileManager.onExistsFileNotOverrideEvent -= OnExists;
        }
    
        private void OnMissing(FileErrorData data) { Debug.Log("Missing: " + data.path); }
        private void OnExists(FileErrorData data) { Debug.Log("Exists, not overwritten: " + data.path); }
    
        private void Start()
        {
            // persistentDataPath/saves/profile.txt
            bool saved = SaveFileManager.Save("profile.txt", SavePathPrefix.ApplicationDirectory, "saves", "hello", true);
    
            if (SaveFileManager.ExistsFile("profile.txt", SavePathPrefix.ApplicationDirectory, "saves"))
            {
                string text = SaveFileManager.Load("profile.txt", SavePathPrefix.ApplicationDirectory, "saves");
                Debug.Log(saved + " " + text);
            }
    
            // Absolute path overloads.
            string full = System.IO.Path.Combine(Application.persistentDataPath, "direct.txt");
            SaveFileManager.Save(full, "direct content", true);
            Debug.Log(SaveFileManager.Load(full));
    
            // Clean up.
            SaveFileManager.RemoveFile("profile.txt", SavePathPrefix.ApplicationDirectory, "saves");
            SaveFileManager.DeleteDirectory(SavePathPrefix.ApplicationDirectory, "saves");
        }
    }

    Notes: Save without overrideFile refuses to replace an existing file, raises onExistsFileNotOverrideEvent and returns false. In the editor outside play mode an overwrite question dialog is shown. Files are written as UTF-8 without BOM. SavePathPrefix.ApplicationDirectory is Application.persistentDataPath, SavePathPrefix.ProjectDirectory is Application.dataPath. Reading from ProjectDirectory while the game is playing loads a text asset from a Resources folder instead of the file system.

    JsonSaveManager with version and encryption #

    C#
    using System;
    using System.Collections.Generic;
    using UnityEngine;
    using UMU.Utility;
    
    [Serializable]
    public class PlayerSave
    {
        public string playerName;
        public int level;
        public List<string> items = new List<string>();
    }
    
    public class JsonSaveExample : MonoBehaviour
    {
        private const string FileName = "player.sav";
        private const string Key = "my secret pass phrase";
        private const int CurrentVersion = 2;
    
        public void Save()
        {
            PlayerSave data = new PlayerSave { playerName = "Ada", level = 7 };
            data.items.Add("sword");
    
            // Stored as { "version": 2, "payload": "<AES encrypted, Base64>" } in persistentDataPath.
            bool ok = JsonSaveManager.Save(FileName, data, CurrentVersion, Key);
            Debug.Log("Saved: " + ok);
        }
    
        public PlayerSave Load()
        {
            int version;
            PlayerSave data = JsonSaveManager.Load<PlayerSave>(FileName, out version, Key);
    
            if (data == null)
                return new PlayerSave(); // missing file or wrong key
    
            if (version < CurrentVersion)
                data.level = Math.Max(1, data.level); // migrate old data here
    
            return data;
        }
    
        public PlayerSave LoadIgnoringVersion()
        {
            return JsonSaveManager.Load<PlayerSave>(FileName, Key);
        }
    }

    Signatures: Save<T>(string fileName, T data, int version = 1, string encryptionKey = null), Load<T>(string fileName, out int loadedVersion, string encryptionKey = null), Load<T>(string fileName, string encryptionKey = null). Without a key the payload is plain JSON. With a key the payload is encrypted with AES, the key is derived from your text with SHA 256 and a random initialisation vector is stored with the data. A load failure logs an error and returns default(T). JsonSaveManager uses the plain Newtonsoft converter, so store Unity structs as their components or serialize them with Serialization first.

    Serialization of Unity types #

    C#
    using UnityEngine;
    using UMU.Utility;
    
    public class SerializationExample : MonoBehaviour
    {
        [System.Serializable]
        public class Marker
        {
            public Vector3 position;
            public Quaternion rotation;
            public Color color;
            public Vector2Int cell;
            public RectInt area;
            public string label;
            public string notSet; // null values are skipped
        }
    
        private void Start()
        {
            Marker marker = new Marker
            {
                position = new Vector3(1, 2, 3),
                rotation = Quaternion.identity,
                color = Color.red,
                cell = new Vector2Int(4, 5),
                area = new RectInt(0, 0, 10, 20),
                label = "A"
            };
    
            string json = Serialization.Serialize(marker);
            Debug.Log(json);
    
            Marker copy = Serialization.Deserialize<Marker>(json);
            Debug.Log(copy.position + " " + copy.color);
        }
    }

    Supported Unity types: Vector2, Vector3, Vector4, Vector2Int, Vector3Int, Quaternion, Color, Color32, RectInt. Output is indented, null values are not written and reference loops are ignored. Deserialize of an empty or null string returns default.

    EditorDataSaveManager (class EditorDataManager) in an editor script #

    The helper keeps the data of an editor window across play mode changes and editor restarts using EditorPrefs.

    C#
    #if UNITY_EDITOR
    using UnityEditor;
    using UnityEngine;
    using UMU.EDITOR;
    
    public class CounterWindow : EditorWindow
    {
        [System.Serializable]
        public struct CounterData : EditorDataManager.IEditorData
        {
            public bool initialized;
            public int counter;
    
            public EditorDataManager.IEditorData DefaultEditorData()
            {
                initialized = true;
                counter = 0;
                return this;
            }
    
            public bool Initialized() { return initialized; }
        }
    
        private const string EditorId = "MyCompany.CounterWindow";
        private CounterData data;
    
        [MenuItem("Window/Counter Window")]
        private static void Open() { GetWindow<CounterWindow>("Counter"); }
    
        private void OnEnable()
        {
            EditorApplication.playModeStateChanged += OnPlayModeChanged;
            EditorDataManager.CheckNullableEditorData(EditorId, ref data);
        }
    
        private void OnDisable()
        {
            EditorApplication.playModeStateChanged -= OnPlayModeChanged;
            EditorDataManager.SaveMyVariableValue(EditorId, data);
        }
    
        private void OnPlayModeChanged(PlayModeStateChange state)
        {
            EditorDataManager.OnPlayModeStateChanged(state, EditorId, ref data);
        }
    
        private void OnGUI()
        {
            data.counter = EditorGUILayout.IntField("Counter", data.counter);
        }
    }
    #endif

    API: OnPlayModeStateChanged<T>(PlayModeStateChange state, string editorId, ref T data), CheckNullableEditorData<T>(string editorId, ref T data), SaveMyVariableValue<Type>(string id, Type obj), LoadMyVariableValue<Type>(string id), interface IEditorData with bool Initialized() and IEditorData DefaultEditorData(). The data type must have a parameterless constructor and be serializable by Serialization.

    RichTextUtils #

    C#
    using UnityEngine;
    using UMU.Utility;
    
    public class RichTextExample : MonoBehaviour
    {
        private void Start()
        {
            string a = RichTextUtils.Colorize("Warning", Color.yellow);
            string b = RichTextUtils.Colorize("Error", new Color32(255, 0, 0, 255));
            string c = RichTextUtils.Colorize("Custom", "FF8800");      // RRGGBB or RRGGBBAA
            string d = RichTextUtils.Bold(RichTextUtils.Italic("Both"));
            string e = RichTextUtils.Size("Big", 32);
            byte byteValue = RichTextUtils.ToByte(0.5f);                // 127
    
            Debug.Log(a + " " + b + " " + c + " " + d + " " + e + " " + byteValue);
        }
    }

    Simplex.Noise #

    Namespace Simplex. Calc1D, Calc2D and Calc3D return values in the range 0 to 256 centred on 128 (the noise value in the range -1 to 1 multiplied by 128, plus 128). Generate(float x) returns the raw 1D value in the range -1 to 1. Setting Seed to a non zero value builds a new random permutation table, setting it to 0 restores the default one.

    C#
    using UnityEngine;
    
    public class NoiseExample : MonoBehaviour
    {
        private void Start()
        {
            Simplex.Noise.Seed = 1234;
    
            float[] line = Simplex.Noise.Calc1D(64, 0.05f);
            float[,] map = Simplex.Noise.Calc2D(128, 128, 0.02f);
            float[,,] volume = Simplex.Noise.Calc3D(16, 16, 16, 0.1f);
            float raw = Simplex.Noise.Generate(3.7f);
    
            Texture2D tex = new Texture2D(128, 128);
    
            for (int x = 0; x < 128; x++)
                for (int y = 0; y < 128; y++)
                {
                    float v = map[x, y] / 255f;
                    tex.SetPixel(x, y, new Color(v, v, v));
                }
    
            tex.Apply();
            Debug.Log(line.Length + " " + volume.GetLength(0) + " " + raw);
        }
    }

    ListShuffler #

    C#
    using System.Collections.Generic;
    using UnityEngine;
    using UMU.Utility;
    
    public class ShuffleExample : MonoBehaviour
    {
        private void Start()
        {
            List<int> cards = new List<int> { 1, 2, 3, 4, 5, 6 };
    
            cards.Shuffle();                          // uses UnityEngine.Random
    
            System.Random seeded = new System.Random(42);
            cards.Shuffle(seeded);                    // reproducible order
    
            int[] array = { 1, 2, 3 };
            array.Shuffle();                          // works on any IList<T>
        }
    }

    ComponentExtension #

    Namespace UMU. Copies fields and properties from one component to another. Members that cannot be copied are skipped and the exception is logged.

    C#
    using UnityEngine;
    using UMU;
    
    public class CopyExample : MonoBehaviour
    {
        public MyScript template;
    
        private void Start()
        {
            // Add a new component to this object and copy all values from the template.
            MyScript added = gameObject.AddComponent(template);
    
            // Copy values onto an existing component.
            MyScript other = gameObject.AddComponent<MyScript>();
            other.GetCopyOf(template);
    
            // Copy only the members shared between a parent type and a child type.
            // gameObject.AddComponent<ParentScript, ChildScript>(parentInstance);
        }
    }
    
    public class MyScript : MonoBehaviour
    {
        public int value = 3;
        public string title = "x";
    }

    Signatures: GetCopyOf<T>(this Component comp, T other, bool seeAllBase = true), GetCopyOf<TParent, TChild>(this Component comp, TParent parent, bool seeAllBase = true), AddComponent<T>(this GameObject go, T toAdd), AddComponent<TParent, TChild>(this GameObject go, TParent toAdd). With seeAllBase true the members of base classes up to Component are copied as well.

    ClassCloneCreator #

    Namespace UMU.Reflection. Makes a copy of a class instance by copying all public fields. Arrays are copied with Array.Clone, List<T> and LinkedList<T> get new lists. A field value that implements ICloneableClass is cloned with its own Clone(), every other value is copied by assignment.

    C#
    using System.Collections.Generic;
    using UnityEngine;
    using UMU.Reflection;
    
    public class Item : ICloneableClass
    {
        public string name = "";
        public int power;
    
        public object Clone()
        {
            return new Item { name = name, power = power };
        }
    }
    
    public class Inventory
    {
        public int gold;
        public Item main = new Item();
        public List<Item> bag = new List<Item>();
        public int[] slots = new int[4];
    }
    
    public class CloneExample : MonoBehaviour
    {
        private void Start()
        {
            Inventory original = new Inventory { gold = 10 };
            original.bag.Add(new Item { name = "Potion", power = 2 });
    
            Inventory copy = ClassCloneCreator.Clone(original); // T must have a public parameterless constructor
            copy.bag[0].power = 99;
    
            Debug.Log(original.bag[0].power); // 2
        }
    }

    Reflection helpers (UMU.Reflection.ReflectionUtilility, REF) #

    The utility class name is spelled ReflectionUtilility in the code. It provides IsCollection, IsCloneable, IsArray, IsList, GetList(Type), GetLinkedList(Type), SearchObjectDataWithName, SearchObjectWithAttribute<T>, SearchObject and LengthOfMatch. The REF class holds SearchParameters, SearchCondition, ObjectInfoType, ObjectInfo and ObjectData.

    C#
    using System;
    using System.Collections.Generic;
    using UnityEngine;
    using UMU.Reflection;
    
    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Method)]
    public class MarkAttribute : Attribute { }
    
    public class Config
    {
        [Mark] public int speed = 3;
        public string title = "demo";
    }
    
    public class ReflectionExample : MonoBehaviour
    {
        private void Start()
        {
            Config root = new Config();
    
            REF.SearchParameters parameters = new REF.SearchParameters(REF.SearchCondition.BasedOnAttributes, typeof(MarkAttribute), root, 5);
            List<REF.ObjectData<MarkAttribute>> found = ReflectionUtilility.SearchObjectWithAttribute<MarkAttribute>(parameters);
    
            foreach (REF.ObjectData<MarkAttribute> data in found)
                Debug.Log(data.objectPath + " = " + data.objectReference);
    
            List<REF.ObjectData> byName = ReflectionUtilility.SearchObjectDataWithName(root, "title");
            Debug.Log(byName.Count);
        }
    }

    The search walks public fields, then properties, then (for attribute searches) methods. Depth is limited by maxDepth.

    JsonFormatter #

    C#
    using UnityEngine;
    using UMU.Utility;
    
    public class JsonFormatterExample : MonoBehaviour
    {
        private void Start()
        {
            string compact = "{\"a\":1,\"b\":[1,2,3],\"c\":{\"d\":\"x\"}}";
            string pretty = JsonFormatter.FormatJson(compact); // 4 space indentation
            Debug.Log(pretty);
        }
    }

    MiniJSON #

    Namespace UMU.Utility. Objects decode to Hashtable, arrays to ArrayList, every number to double. The parser does not throw; use lastDecodeSuccessful() to check the result.

    C#
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UMU.Utility;
    
    public class MiniJsonExample : MonoBehaviour
    {
        private void Start()
        {
            string json = "{\"name\":\"Ada\",\"scores\":[1,2,3],\"level\":7}";
    
            Hashtable table = MiniJSON.jsonDecode(json) as Hashtable;
    
            if (!MiniJSON.lastDecodeSuccessful())
            {
                Debug.LogError(MiniJSON.getLastErrorIndex() + ": " + MiniJSON.getLastErrorSnippet());
                return;
            }
    
            string name = (string)table["name"];
            double level = (double)table["level"];
            ArrayList scores = (ArrayList)table["scores"];
            Debug.Log(name + " " + level + " " + scores.Count);
    
            // Encode.
            Hashtable output = new Hashtable();
            output["id"] = 5;
            output["tags"] = new ArrayList { "a", "b" };
            string text = MiniJSON.jsonEncode(output);
    
            // Extension methods.
            Hashtable again = text.hashtableFromJson();
            ArrayList list = "[1,2,3]".arrayListFromJson();
            string fromDict = new Dictionary<string, string> { { "k", "v" } }.toJson();
            string fromTable = output.toJson();
            Debug.Log(again.Count + " " + list.Count + " " + fromDict + " " + fromTable);
        }
    }

    TextureToBase64Tool #

    Namespace TeamUtilityEditor. Texture2DToBase64(Texture2D) copies the texture into an RGB24 texture with Graphics.CopyTexture, encodes it as JPG (quality 100) and returns the Base64 text. Base64ToTexture2D(string) returns a new RGB24 texture with point filtering and the flag HideAndDontSave.

    C#
    using UnityEngine;
    using TeamUtilityEditor;
    
    public class Base64Example : MonoBehaviour
    {
        public Texture2D source;
    
        private void Start()
        {
            string text = TextureToBase64Tool.Texture2DToBase64(source);
            Texture2D back = TextureToBase64Tool.Base64ToTexture2D(text);
            Debug.Log(text.Length + " " + back.width);
        }
    }

    Editor helper classes (editor only) #

    Class (namespace UMU)Purpose
    UMU_InspectorEditorBase class for UI Toolkit inspectors. Override CreateInspectorGUI. Helpers: ShowProperty(root, property, name = "") adds a bound field, ShowSeparator(root) adds a thin line.
    UMU_PropertyDrawerBase class for UI Toolkit property drawers. Override Initialize(SerializedProperty, VisualElement root). Helpers: LabelField, ShowProperty(root, relativeProperty, name, maxWidth), ShowSeparator, PropertyIntValue.
    UMU_EditorWindowBase class for IMGUI windows. Helpers: GetLabelWidth, GetLabelHeight, GetButtonWidth, GetButtonHeight, LabelField, ShowSeparator.
    UMU_EditorUIStatic UI Toolkit helpers: Section(parent, title), SectionBox(title, out box), PrimaryButton(text, onClick), Row(gap), extension methods Grow and MarginRight.
    UMU_EditorToolsStatic IMGUI helpers: DrawHeader, BeginContents, EndContents, DrawProperty, DrawPaddedProperty, DrawListProperty, ListIterator.
    UMU_ButtonDrawerDrawButtons(Object target) draws the [Button] buttons of any target. Call it from your own custom editor to keep buttons there.
    C#
    #if UNITY_EDITOR
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.UIElements;
    using UMU;
    
    public class HeroData : MonoBehaviour
    {
        public int health = 100;
        public float speed = 3f;
    }
    
    [CustomEditor(typeof(HeroData))]
    public class HeroDataEditor : UMU_InspectorEditor
    {
        public override VisualElement CreateInspectorGUI()
        {
            VisualElement root = new VisualElement();
            VisualElement section = UMU_EditorUI.Section(root, "Stats");
            ShowProperty(section, "health", "Health");
            ShowSeparator(section);
            ShowProperty(section, "speed", "Speed");
            root.Add(UMU_EditorUI.PrimaryButton("Heal", () => { ((HeroData)target).health = 100; EditorUtility.SetDirty(target); }));
            return root;
        }
    }
    #endif

    12. Android Manifest Editor #

    Edits the file Assets/Plugins/Android/AndroidManifest.xml with a form, without touching XML.

    Editor menu and window reference #

    Menu item: UMU > Android Manifest Editor. The window title is "Android Manifest Editor". The window content scrolls.

    UMU > Android Manifest Editor
    UMU > Android Manifest Editor

    If the manifest file or its folder does not exist, the editor creates Assets/Plugins/Android and writes a default manifest with:

    • manifest attributes: package="", theme @android:style/Theme.NoTitleBar, versionName 1.0, versionCode 1, installLocation preferExternal
    • supports-screens for all sizes
    • an application (icon @drawable/app_icon, label @string/app_name, debuggable false) with one activity com.unity3d.player.UnityPlayerNativeActivity (landscape, singleTask, exported true, MAIN and LAUNCHER intent filter, Unity meta data)
    • uses-sdk with minSdkVersion 23 and targetSdkVersion 36
    • uses-feature glEsVersion 0x00020000

    Window level controls #

    Control (as shown)What it doesDefault or stateNotes
    Application: (foldout)Shows the attributes of the application element (see 2.2).CollapsedNot shown for templates. Templates show a "Name:" text field instead.
    Application Level: (label)Starts the group of elements stored inside application.n/aGroups below: Activities, Receivers, Services, Providers, Meta-Data, Uses-Library.
    Activities: (n)List of activity elements, with count.CollapsedContains one foldout per activity and a "New Activity" button.
    Receivers: (n)List of receiver elements.Collapsed"New Receiver" button.
    Services: (n)List of service elements.Collapsed"New Service" button.
    Providers: (n)List of provider elements.Collapsed"New Provider" button.
    Meta-Data: (n)meta-data elements that are direct children of application.Collapsed"New Meta-Data" button.
    Uses-Library: (n)uses-library elements.Collapsed"New Uses-Library" button.
    Manifest Level: (label)Starts the group of elements stored directly inside manifest.n/aGroups below: Permissions, Uses-Permissions, Uses-Features.
    Permissions: (n)permission elements.Collapsed"New Permission" button.
    Uses-Permissions: (n)uses-permission elements.Collapsed"New Uses-Permission" button.
    Uses-Features: (n)uses-feature elements that have an android:name.Collapsed"New Uses-Feature" button.
    Apply ChangesWrites all edited, new and removed elements into the XML and saves the file.Green when there are unsaved changes, grey otherwiseOnly elements that were changed are rewritten. After saving, the changed state is cleared.
    Clear ChangesThrows away unsaved edits and reloads the manifest from disk.Red when there are unsaved changes, grey otherwise
    ValidateChecks the manifest against Android 12, 13 and 14 rules (see 2.4).n/aChecks the file on disk. Edits that were not applied are not included.
    Template name:Text box for the name of a new template.Empty
    Save as TemplateStores the current manifest content as a template under the entered name.n/aIf a template with the same name exists the dialog "Save over existing template?" appears with Yes and No.
    Template foldout "<name>:"Shows the template content, editable like the main manifest.Collapsed
    Import (per template)Merges the template into the current manifest and marks it changed.n/aEach template element is appended to the proper parent. An existing element with the same name is marked as removed and replaced. Use Apply Changes afterwards.
    Delete (per template)Deletes the template.n/a
    Apply Changes / Clear Changes (inside an expanded template)Saves or reverts the edits of that template.n/aA template is stored in the editor preferences, not in the manifest file.

    Templates are kept in the Unity editor preferences (keys BBMANIFEST0, BBMANIFEST1, and so on), so they are available in every project on the same machine and user account.

    Every element has its own Remove button (the button text differs per element, see the tables below). Removing is applied when you press Apply Changes.

    Inspector reference (fields of every manifest element) #

    The model classes are not Inspector components. Their fields are shown in the window. The following tables list every field as shown. Attributes that are empty or equal to the Android default are not written to the file, except android:exported, which is always written for activities, receivers, services and providers.

    Application #

    LabelTypeDefaultWritten as
    Name:textemptyandroid:name
    Allow Task Reparenting:toggleoffallowTaskReparenting (only when on)
    Allow Backup:toggleoffallowBackup (only when on)
    Backup Agent:textemptybackupAgent
    Debuggabletoggleoffdebuggable (only when on)
    Description:text areaemptydescription
    Enabled:toggleonenabled="false" only when off
    Has Code:toggleonhasCode="false" only when off
    Hardware AcceleratedtoggleonhardwareAccelerated="false" only when off
    Icon:textemptyicon
    Kill After Restore:toggleonkillAfterRestore="false" only when off
    Large Heap:toggleofflargeHeap
    Label:textemptylabel
    Logo:textemptylogo
    Manage Space Activity:textemptyread from the file, shown in the form
    Permission:textemptypermission
    Persistent:toggleoffpersistent
    Process:textemptyprocess
    Restore Any Version:toggleoffrestoreAnyVersion
    Required Account Type:textemptyrequiredAccountType
    Restricted Account Type:textemptyrestrictedAccountType
    Supports Rtl:toggleoffsupportsRtl
    Task AffinitytextemptytaskAffinity
    Test OnlytoggleofftestOnly
    Themetextemptytheme
    UI Options:textemptyuiOptions
    VM Safe Mode:toggleoffvmSafeMode

    Activity (foldout "Activity: <name>") #

    LabelTypeDefaultNotes
    Name:textemptyandroid:name. Written always.
    Attributes: (foldout)groupcollapsedContains the fields below.
    Allow Task Reparenting:toggleoff
    Always Retain Task State:toggleoff
    Clear Task On Launch:toggleoff
    Config Changes:multi selectnoneOptions: mcc, mnc, locale, touchscreen, keyboard, keyboardHidden, navigation, screenLayout, fontScale, uiMode, orientation, screenSize, smallestScreenSize. Written as values joined with |.
    Enabled:toggleonenabled="false" only when off.
    Exclude From Recents:toggleoff
    Exported:toggleonAlways written as true or false.
    Hardware Accelerated:toggleoff
    Icon:textempty
    Label:textempty
    Launch Mode:drop downstandardOptions: standard, singleTop, singleTask, singleInstance. standard is not written.
    Multiprocess:toggleoff
    No History:toggleoff
    Permission:textempty
    Process:textempty
    Screen Orientation:drop downunspecifiedOptions: unspecified, behind, landscape, portrait, reverseLandscape, reversePortrait, sensorLandscape, sensorPortrait, userLandsacpe, userPortrait, sensor, fullSensor, nosensor, user, fullUser, locked. unspecified is not written.
    State Not Neededtoggleoff
    Task Affinity:textempty
    Theme:textempty
    UI Options:drop downnoneOptions: none, splitActionBarWhenNarrow.
    Window Soft Input Mode:multi selectnoneOptions: stateUnspecified, stateUnchanged, stateHidden, stateAlwaysHidden, stateVisible, stateAlwaysVisible, adjustUnspecified, adjustResize, adjustPan.
    Intent-Filters: (n)listcollapsedButton "New Intent-Filter". See intent filter below.
    Meta-Data: (n)listcollapsedButton "New Meta-Data".
    Remove Activitybuttonn/aMarks the activity for removal.

    Receiver (foldout "Receiver: <name>") #

    Fields: Name:, Attributes: with Enabled: (on), Exported: (on, always written), Icon:, Label:, Permission:, Process:. Lists: Intent-Filters: (n) with "New Intent-Filter", Meta-Data: (n) with "New Meta-Data" (the count shown next to Meta-Data in a receiver is the number of intent filters). Button: Remove Receiver.

    Service (foldout "Service: <name>") #

    Fields: Name:, Attributes: with Enabled: (on), Exported: (on, always written), Icon:, Isolated Process: (off), Label:, Permission:, Process:, Foreground Service Type: (multi select: camera, connectedDevice, dataSync, health, location, mediaPlayback, mediaProjection, microphone, phoneCall, remoteMessaging, shortService, specialUse, systemExempted). Lists: Intent-Filters: (n), Meta-Data: (n). Button: Remove Service.

    Provider (foldout "Provider: <name>") #

    Fields: Name:, Attributes: with one text box per authority (each with a Remove button, authorities are written joined with ;), Enabled: (on), Exported: (on, always written), Grant URI Permissions: (off), Icon:, Init Order: (0, not written when 0), Label:, Multiprocess: (off), Permission:, Process:, Read Permission:, Syncable: (off), Write Permission:. Lists: Meta-Data: (n), Grant-Uri-Permissions: (n) with "New Grant-Uri-Permission", Path-Permissions: (n) with "New Path-Permission". Button: Remove Provider. Authorities that already exist in the file are listed and can be edited or removed.

    Intent-Filter (foldout "Intent-Filter: <name>") #

    LabelTypeDefaultNotes
    Icon:textempty
    Label:textempty
    Priority:integer0Not written when 0.
    Action Name:textemptyOne row per action. Button "Remove Action".
    Category Name:textemptyOne row per category. Button "Remove Category".
    Data: (foldout)groupcollapsedFields: Scheme:, Host:, Port:, Path:, PathPattern:, PathPrefix:, Mime Type: and a Remove button.
    New Action: / New Category: / New Data:buttonsn/aAdd a row.
    Remove Intent-Filterbuttonn/a

    Meta-Data (foldout "Meta-Data: <name>") #

    LabelTypeDefaultNotes
    Name:textempty
    Value:textempty
    Is Resource:toggleoffOff writes android:value, on writes android:resource.
    Removebuttonn/a

    Uses-Library #

    LabelTypeDefaultNotes
    Name:textempty
    Required:toggleoff for a new entry, on when read from a file without the attributerequired="false" is written when off.
    Remove Uses-Librarybuttonn/a

    Permission (foldout "Permission: <name>") #

    LabelTypeDefaultNotes
    Name:textempty
    Protection Level:drop downNORMALNORMAL, DANGEROUS, SIGNATURE, SIGNATUREORSYSTEM.
    Label:textemptyRemoved from the file when emptied.
    Description:text areaemptyRemoved from the file when emptied.
    Icon:textemptyRemoved from the file when emptied.
    Removebuttonn/a

    Uses-Permission (foldout "Uses-Permission: <name>") #

    LabelTypeDefaultNotes
    Permission:drop down or textNONEDrop down with the classic Android permissions plus the newer ones (Bluetooth scan, connect and advertise, POST_NOTIFICATIONS, READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_AUDIO, READ_MEDIA_VISUAL_USER_SELECTED, ACCESS_MEDIA_LOCATION, NEARBY_WIFI_DEVICES, USE_FULL_SCREEN_INTENT, SCHEDULE_EXACT_ALARM, USE_EXACT_ALARM, BODY_SENSORS_BACKGROUND, FOREGROUND_SERVICE and the FOREGROUND_SERVICE_* types). The file value is the name with the prefix android.permission..
    Standard Permission:toggleonOn shows the drop down. Off shows a free text box.
    Max SDK Version:slider 9 to 3636A value of 36 means no maxSdkVersion is written.
    Removebuttonn/a

    Uses-Feature #

    LabelTypeDefaultNotes
    Feature:drop downNONEHardware and software features such as hardware.camera, hardware.location.gps, hardware.touchscreen.multitouch, software.live_wallpaper. The file value is the name with the prefix android..
    Required:toggleonrequired="false" is written when off.
    Removebuttonn/a

    Grant-Uri-Permission #

    Fields: Path:, PathPattern:, PathPrefix:, button "Remove ". Path is always written, the other two only when not empty.

    Path-Permission #

    Fields: Path:, PathPattern:, PathPrefix:, Permission:, Read Permission:, Write Permission:, button "Remove Path-Permission ".

    Template storage and merge behaviour #

    • Save as Template stores the template name followed by the full manifest XML in the editor preferences.
    • Import appends the template elements and replaces elements with the same name.
    • Templates can be edited and saved independently of the manifest file.

    Validate button rules #

    Validate loads the manifest from disk and reports each finding as an error or a warning in a dialog and in the Console log. The dialog title shows the number of errors and warnings. With no findings it shows "No issues found" for Android 12, 13 and 14.

    RuleSeverity
    An activity, activity-alias, receiver, service or provider that has an intent-filter but no android:exported attribute (Android 12 and later reject the install)Error
    A component with an intent-filter and android:exported="true"Warning (check that other apps are really allowed to call it)
    A service with android:foregroundServiceType but without the FOREGROUND_SERVICE permissionError
    A foreground service type without its matching permission FOREGROUND_SERVICE_<TYPE> (for example mediaPlayback needs FOREGROUND_SERVICE_MEDIA_PLAYBACK). Types specialUse, systemExempted and shortService are skipped.Error
    targetSdkVersion 33 or higher and POST_NOTIFICATIONS is not declaredWarning
    SCHEDULE_EXACT_ALARM declared with targetSdkVersion 33 or higherWarning
    READ_EXTERNAL_STORAGE declared with targetSdkVersion 33 or higher and none of READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_AUDIOWarning
    Missing manifest root elementError

    The target SDK is read from uses-sdk. If it is missing, the value is treated as 0 and the SDK dependent rules are not triggered.

    Code examples #

    All classes are in the namespace UMU and are editor only. Place the code in an Editor folder.

    Validate a manifest string #

    C#
    #if UNITY_EDITOR
    using System.Collections.Generic;
    using UnityEditor;
    using UnityEngine;
    using UMU;
    
    public static class ManifestValidationExample
    {
        [MenuItem("Tools/Validate Sample Manifest")]
        private static void Run()
        {
            string xml =
                "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">" +
                "<uses-sdk android:targetSdkVersion=\"34\" />" +
                "<application><activity android:name=\"com.example.Main\">" +
                "<intent-filter><action android:name=\"android.intent.action.MAIN\" /></intent-filter>" +
                "</activity></application></manifest>";
    
            AndroidXmlEditor editor = new AndroidXmlEditor(xml);
            List<ManifestValidator.Issue> issues = ManifestValidator.Validate(editor.document);
    
            foreach (ManifestValidator.Issue issue in issues)
                Debug.Log((issue.isError ? "ERROR " : "WARN ") + issue.element + ": " + issue.message);
        }
    }
    #endif

    AndroidXmlEditor() (no argument) opens the real file Assets/Plugins/Android/AndroidManifest.xml. AndroidXmlEditor(string xml) works on text only and never writes the file.

    Add a permission and an activity with the model API #

    C#
    #if UNITY_EDITOR
    using UnityEditor;
    using UnityEngine;
    using UMU;
    
    public static class ManifestEditExample
    {
        [MenuItem("Tools/Add Internet Permission")]
        private static void AddPermission()
        {
            AndroidWindowData data = AndroidWindowData.CreateInstance(new AndroidXmlEditor());
    
            ManifestUsesPermission permission = ManifestUsesPermission.CreateInstance();
            permission.name = "INTERNET"; // written as android.permission.INTERNET
            data.usesPermissionList.Add(permission);
    
            ManifestActivity activity = ManifestActivity.CreateInstance();
            activity.name = "com.example.SecondActivity";
            data.activityList.Add(activity);
    
            data.ApplyChanges(); // saves Assets/Plugins/Android/AndroidManifest.xml
            Debug.Log("Manifest updated");
        }
    
        [MenuItem("Tools/List Manifest Content")]
        private static void List()
        {
            AndroidWindowData data = AndroidWindowData.CreateInstance(new AndroidXmlEditor());
            Debug.Log("Activities: " + data.activityList.Count);
            Debug.Log("Services: " + data.serviceList.Count);
            Debug.Log("Uses-permissions: " + data.usesPermissionList.Count);
    
            foreach (AndroidWindowData template in AndroidTemplateManager.GetTemplates())
                Debug.Log("Template: " + template.name);
        }
    }
    #endif

    Useful public members: AndroidWindowData has the lists activityList, receiverList, serviceList, providerList, metaDataList, usesLibraryList, permissionList, usesPermissionList, usesFeatureList and the field application. Merge(AndroidWindowData) imports a template, ApplyChanges() saves. AndroidTemplateManager offers SaveTemplate, GetTemplates, ReloadTemplate(int), DeleteTemplate. Every element class has a static CreateInstance() for a new element and CreateInstance(XmlNode) for an existing one. Elements store their edit state in ElementEditStatus (NONE, REMOVED, EDITED); set it to REMOVED to delete an element on the next ApplyChanges.

    13. Xcode Settings Editor #

    Collects iOS project changes in a form and stores them in a JSON settings file. After an iOS build, a post process applies the settings to the generated Xcode project.

    Editor menu and window reference #

    Menu item: UMU > Xcode Project Editor. The window title is "Xcode Project Editor", minimum size 600 by 400. The content scrolls.

    UMU > Xcode Project Editor
    UMU > Xcode Project Editor

    The settings are stored in the file UMU_XCODE_SETTINGS.iosProjmod. The editor looks for this file anywhere under Assets. If it does not exist, it is created as Assets/UMU/XcodeSettings/UMU_XCODE_SETTINGS.iosProjmod.

    Top button rows #

    Button (as shown)What it does
    Add My FrameworkOpens a folder picker (title ".framework"). Only a folder whose path contains "framework" is accepted. It is added to the file list under the current "File Import Group".
    Add FileOpens a file picker ("Include in Xcode project"). The file is added to the file list under the current "File Import Group".
    Add FolderOpens a folder picker ("Include folder in Xcode project"). The folder is added to the folder list under the current "File Import Group".
    Add iOS SDK FileAdds a new entry to the SDK Files list and opens that list.
    Header Search PathAdds an empty entry to the Header Search Paths list and opens it.
    Library Search PathAdds an empty entry to the Library Search Paths list and opens it.
    Plist EntryAdds an empty entry to the Info.plist Editor list and opens it.
    Add Build SettingsAdds an empty entry to the Build Settings list and opens it.

    Adding a path that is already in the list logs the warning "Already importing: <path>" and adds nothing. Paths inside the Assets folder are stored relative to Assets.

    Global fields #

    Field (as shown)What it doesDefaultNotes
    File Import Group:Xcode group name assigned to files and folders added afterwards.emptyEach file and folder keeps its own group and shows it in its own "Group:" field.
    Linker Flags:Flags for the Xcode setting "Other Linker Flags", separated by spaces.-weak_framework CoreMotion -weak-lSystem (used when the settings file has no linker flags)Duplicates are removed when saved.
    Excludes:Regular expressions, separated by spaces, for files that are skipped while adding folders.^.*.meta$ ^.*.mdown$ ^.*.pdf$ (used when the settings file has no excludes)

    Lists #

    Foldout (as shown)Entry controlsNotes
    SDK Files: (n)Foldout with the SDK name and a Remove button. Opened: a filter text box and a drop down (width 200) with the available frameworks and libraries of the installed iOS SDK.The SDK is read from /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/ (the newest version found). On a computer without it, for example on Windows, the list shows placeholder entries named Test.SDK, Test2.SDK, Test3.SDK and Test4.SDK.
    Files && My Frameworks: (n)A "Group:" label, then a foldout with the file name and a Remove button. Opened: a "Path:" label and a "Group:" text box.An entry whose file cannot be found is drawn in red and an error is logged once.
    Folders: (n)Same as files.
    Header Search Paths: (n)Text box, a button "recursive" or "non recursive", a "Set" button and a "Remove" button.The recursive button adds or removes the suffix /**. Set opens a folder picker and stores the path relative to the project.
    Library Search Paths: (n)Same controls as Header Search Paths.Stored under the JSON key libs (see 3.3).
    Info.plist Editor: (n)One box per entry: foldout "Plist Entry: <key>" with a Remove button. Opened: "Key:" text, "Type:" drop down (STRING, BOOL, NUMBER, DATA, DATE, ARRAY, DICTIONARY) and a "Value:" field matching the type. ARRAY and DICTIONARY show nested entries and a "New Plist Entry" button.Entries inside an array are labelled "Array Entry <index>" and have no key.
    Build Settings: (n)Per entry: "Setting Name: ", "Setting New Value: ", "Build Configuration Name: " (default all) and a Remove button.Stored as text name|value|configuration.

    Bottom buttons #

    ButtonWhat it doesNotes
    Apply ChangesWrites the whole form into the settings file (formatted JSON).Green when there are unsaved changes, grey otherwise.
    Cancel ChangesReloads the form from the settings file.Red when there are unsaved changes, grey otherwise.

    Changes are not saved until Apply Changes is pressed.

    Inspector reference #

    This module has no components or assets in the Inspector. All settings are edited in the window described above.

    JSON settings file and post process behaviour #

    Keys written by the window:

    JSON keyWritten fromContent
    BuildSettingsBuild Settings listArray of strings name|value|configuration.
    FileAndFolderPathsFiles and Folders listsArray of objects with group, files, folders and excludes.
    frameworksSDK Files listArray of framework or library names.
    libsLibrary Search Paths listArray of strings (see the note below).
    headerpathsHeader Search Paths listArray of strings. Empty entries are skipped.
    linker_flagsLinker FlagsArray of strings.
    excludesExcludesArray of regular expressions.
    plistInfo.plist EditorObject. Booleans and numbers are stored as text ("True", "3").

    Keys that the post process also understands when you write them by hand in a .iosProjmod file: group, files, folders, compiler_flags, embed_binaries, and in plist the key urltype (list of objects with name, role and schemes) for CFBundleURLTypes.

    Post process (runs after every iOS build):

    1. Every *.iosProjmod file under Assets is read and applied to the generated Xcode project: libraries (entries of libs, taken from usr/lib, with an optional :weak suffix), frameworks (an entry with :weak is added as a weak link), files, folders (respecting excludes), header search paths, compiler flags, linker flags, embedded binaries, Info.plist items and finally the BuildSettings entries (the configuration name all or a specific one).
    2. Values in plist are written to Info.plist and typed from their text: a number text becomes a number, true or false becomes a boolean, a date text becomes a date, anything else a string.
    3. After the project is saved, every .m, .mm, .h and .hh file in the build output has the module import @import X; rewritten to #import <X/X.h>.
    4. A second, simpler path reads files named *.iosPostProcess.json under Assets: {"buildSettings":[{"key":"","value":""}],"frameworks":["name"],"plistKeys":[{"key":"","value":""}]}. Build settings are applied to the main target, frameworks are added to the UnityFramework target, plist values are written as strings. It needs the iOS Build Support module of Unity and is skipped with a warning when the module is not installed.

    If the build target is not iOS, the post process logs a warning and does nothing.

    Code examples #

    Write a settings file from an editor script #

    C#
    #if UNITY_EDITOR
    using System.Collections;
    using System.IO;
    using UnityEditor;
    using UnityEngine;
    using UMU.Utility;
    
    public static class XcodeSettingsExample
    {
        [MenuItem("Tools/Write Xcode Settings")]
        private static void Write()
        {
            Hashtable plist = new Hashtable();
            plist["NSCameraUsageDescription"] = "The camera is used to scan codes.";
            plist["UIRequiresFullScreen"] = "True";
    
            ArrayList buildSettings = new ArrayList { "ENABLE_BITCODE|NO|all" };
            ArrayList linkerFlags = new ArrayList { "-ObjC", "$(inherited)" };
    
            Hashtable root = new Hashtable();
            root["BuildSettings"] = buildSettings;
            root["linker_flags"] = linkerFlags;
            root["plist"] = plist;
    
            string json = JsonFormatter.FormatJson(MiniJSON.jsonEncode(root));
            string path = Path.Combine(Application.dataPath, "MyProject/Custom.iosProjmod");
            Directory.CreateDirectory(Path.GetDirectoryName(path));
            File.WriteAllText(path, json);
            AssetDatabase.Refresh();
        }
    }
    #endif

    Any *.iosProjmod file under Assets is applied on the next iOS build, in addition to UMU_XCODE_SETTINGS.iosProjmod.

    Settings file for the simple post process path #

    JSON
    {
      "buildSettings": [ { "key": "ENABLE_BITCODE", "value": "NO" } ],
      "frameworks": [ "CoreMotion.framework" ],
      "plistKeys": [ { "key": "NSCameraUsageDescription", "value": "Used for scanning." } ]
    }

    Save it as Assets/MyProject/MySettings.iosPostProcess.json.

    14. Resource Checker #

    Lists the textures, materials and meshes used by the objects in the open scenes, with memory size and where they are used. It is based on the public domain Unity Resource Checker.

    Editor menu and window reference #

    Menu item: UMU > Resource Checker. The window title is "Resource Checker", minimum size 475 by 475. Opening the window runs a first scan.

    UMU > Resource Checker
    UMU > Resource Checker

    Option toggles (top of the window) #

    Control (as shown)What it doesDefault
    Include disabled objectsAlso scans inactive objects in all loaded scenes. When off, only active objects are scanned.On
    Look in sprite animationsAlso reads sprite frames from the animation clips of Animator controllers.On
    Look in behavior fieldsAlso reads public Sprite, Mesh and Material fields of scripts. Resources found this way are marked as script references.On
    Look in GUI elementsAlso scans UI graphics (their textures and materials) and the sprite states of buttons.On

    The options take effect at the next Calculate.

    Buttons #

    ButtonWhat it does
    CalculateRuns the scan with the current options and rebuilds all lists.
    CleanUpCalls Resources.UnloadUnusedAssets.

    Summary line #

    Three labels: "Textures <count> - <total size>", "Materials <count>", "Meshes <count> - <vertex total> verts". Sizes are shown as kilobytes (k) or megabytes (Mb).

    If any object has a missing sprite, mesh or material, a red box "Some GameObjects are missing graphical elements." appears and a fourth tab appears.

    Tabs (toolbar) #

    TabContent
    TexturesOne row per texture, largest first. Thumbnail, name button (selects the texture), a size label (width x height, mip count, memory size, format, plus cube map and array details), "<n> Mat" button (selects the materials that use it), "<n> GO" button (selects the game objects that use it), "<n> GO (In Project Window)" button (selects the texture asset itself). A "Select All" button at the bottom selects every listed texture.
    MaterialsOne row per material. Preview, name button, shader name (or "no shader"), "<n> GO" button (selects the renderers and UI graphics that use it).
    MeshesOne row per mesh, most vertices first. Name button, vertex count, "<n> GO" button (mesh filters), "<n> skinned mesh GO" button (skinned mesh renderers; greyed "0 skinned mesh" when none).
    MissingShown only when something is missing. One row per object with a button (selects the object) and the missing type: mesh, sprite or material.

    Row colours: bluish for resources found through script fields, yellowish for GUI resources, reddish for the skybox.

    Holding Ctrl (Command on macOS) while pressing a selection button adds to the current selection. Pressing the same single object again with Ctrl removes it from the selection.

    The lists are cleared automatically when you switch between edit mode and play mode. Press Calculate again after the switch.

    Inspector reference #

    This module has no components or assets in the Inspector.

    Code examples #

    The window is operated from its menu and has no public scripting API. To open it from your own editor code:

    C#
    #if UNITY_EDITOR
    using UnityEditor;
    
    public static class OpenResourceChecker
    {
        [MenuItem("Tools/Open Resource Checker")]
        private static void Open()
        {
            EditorApplication.ExecuteMenuItem("UMU/Resource Checker");
        }
    }
    #endif

    15. Demo content #

    The package with demo contains complete, responsive demos for the modules.

    DemoWhat it shows
    GUI Manager (2D)Menu, panels and popups with slide and fade transitions, history, safe area, responsive layout
    GUI Manager (3D)A 3D scene behind the UI with clickable 3D objects that open a panel and a popup
    AudioVolume sliders, SFX, music and voice buttons, and a GuiManager popup (Audio Demo Panel prefab, a GuiPanel with GuiPanelAudioBridge) that plays sounds on open and close
    LoggerLogger demo scene with the console window

    The demo UI works from 4:3 tablets to 21:9 phones, and respects the safe area. Demo art, textures and 3D models were created for UMU. The demo audio clips were created for UMU. The demo music, WinSysAnalyzer.mp3, is the property of BMSoft1024 (https://bmsoft1024.com/product/winsysanalyzer).

    Demo scenes: GUI Manager/Demo/GuiManager2D_Rebuilt.unity, GUI Manager/Demo/GuiManager3D.unity, Audio/Demo/AudioDemo.unity, Audio/Demo/AudioDemoSecond.unity, Logger/Demo/demo.unity. The Messenger module has a small test script in its Demo folder. Delete the demo folders before shipping your game.

    The package without demo contains no demo scenes, prefabs or example assets.

    16. About window #

    UMU > About shows information about UMU, a link to the UMU website and a link to the Unity Asset Store page.

    17. Third-party notices #

    See Third-Party Notices.txt in the Assets/UMU folder. In short:

    • Resource Checker: public domain (HandCircus and Mangatome)
    • Simplex noise: public domain (Stefan Gustavson)
    • MiniJSON and the Xcode project editor are written by the author of UMU
    • Demo audio, art and models: created by BMSoft1024 for UMU
    • Demo music WinSysAnalyzer.mp3: property of BMSoft1024, https://bmsoft1024.com/product/winsysanalyzer

    18. Known limitations #

    • Code obfuscation makes reverse engineering harder but is not encryption
    • Panel loading uses Resources by default. Use externalPanels or RegisterPanel for other sources
    • The OpenPanel queue has one slot, the last request wins by design
    • An alwaysOnTop popup can only be closed explicitly with ClosePopup(id)
    • The Audio UI channel is limited to 4 simultaneous sounds
    • OGG output in Audio Cutter needs an external oggenc

    19. Support #

    Questions, bug reports and feature requests: support@bmsoft1024.com (subject: UMU).