Disclaimer
What do you need?
- C# knowledge
 - A bit of Unity knowledge
 - A bit of "VAM code" knowledge (included in VAM as a VS solution)
 
What is the goal of the tutorial?
It is meant to show you how to create a script / plugin for VAM which will be able to control a standard Unity prefab in a CUA (Custom Unity Asset) atom.
This is not a tutorial about Unity and how to create prefabs. If you don't know how to create prefabs and bundle them, you better start with that. MacGruber did an excellent guide about that.
You can also read the definitive guide I did a while back.
I'm not gonna gonna go step by with the code since this is pretty basic C#. I made an heavily commented example that you'll just have to read.
You can download the var example which contains the script, demo CUA and a default scene.
This "perfect" version of the tutorial aims to remove the old "hacky" approach relying on timers/amount of tries to get the asset. Over the years I've fiddled a lot in VAM's code and I improved my plugins a lot. Months ago I found a proper way to handle the CUA loading process and hadn't had the time to document it. This is now a thing of the past... : )
This new system also removes the side effect of the previous version: the plugin will work even with slower PCs suffering from huge load times.
1 - Create your asset in Unity and package it
First, you need to create your asset. In this case, we're gonna create an empty object containing three shapes, a sphere, a cylinder and a cube. In Unity, the hierarchy of my object looks like this :
ExampleCUA and root are just transforms ( just because I like things ordered and neat )
sphere, cube and cylinder are basic gameobjects with a mesh filter, mesh renderer and no collision.
Save your object as a prefab, pack it and put it in your VAM directory.
2 - Write your script
Create an empty scene, add the CUA and load your freshly packed prefab from Unity. Then start to write your script.
		C#:
	
	using UnityEngine;
using UnityEngine.UI;
using System;
using System.Collections;
using System.Collections.Generic;
using SimpleJSON;
using System.Linq;
using System.IO;
// Perfect CUA Editor Tutorial
//
// Shows off the exact process to properly handle a CUA for VAM without dirty timers hacks
// CC-BY @ Hazmhox
// - https://hub.virtamate.com/members/hazmhox.351/#about
// - https://hub.virtamate.com/resources/authors/hazmhox.351/
namespace PerfectCUAEditorPlugin
{
    public class PerfectCUAEditor : MVRScript
    {
        // Statics
        private static string PLUGIN_NAME = "Perfect CUA Editor Tutorial"; // Your wonderful plugin name, it is used to prefix the error / log messages or UI titles
        // Storables - Your controls, storables to edit the CUA, obviously, whatever you want or need!
        public JSONStorableFloat sphereYPosition;
        public JSONStorableFloat cubeRotation;
        public JSONStorableFloat cylinderZPosition;
        public JSONStorableBool cubeActive;
     
        // UI Components
        private UIDynamicTextField _InfosText; // The area where you're gonna do some user feedback
        private List<UIDynamicSlider> uiSliders = new List<UIDynamicSlider>(); // All UIDynamic components for sliders (used to control the UI)
        private List<UIDynamicToggle> uiToggles = new List<UIDynamicToggle>(); // All UIDynamic components for toggles (used to control the UI)
     
        // EXAMPLE UI COMPONENTS : You could use color pickers, popups... etc...
        // These are not used, it's an example to show what you could do with other UIDynamics
        private List<UIDynamicColorPicker> uiColorPickers = new List<UIDynamicColorPicker>();
        private List<UIDynamicPopup> uiPopups = new List<UIDynamicPopup>();
     
        // CUA / Transforms / Components
        // Misc components or transform cached for the rest of the script
        protected CustomUnityAssetLoader currentCUALoader;
        protected Transform sphereTR;
        protected Transform cubeTR;
        protected Transform cylinderTR;
        // Flags
        private bool cuaLoaderInitialized = false;
        private bool meshInitialized = false;
        protected bool _isSceneLoading { get { return SuperController.singleton.isLoading; } }
        public bool isSceneFrozen { get { return SuperController.singleton.freezeAnimation; } }
        public bool isInEditMode { get { return SuperController.singleton.gameMode == SuperController.GameMode.Edit; } }
     
        public override void Init()
        {          
            try
            {
                // Temporary vars
                UIDynamicTextField tmpTextfield;
                UIDynamicPopup tmpPopup;
                UIDynamicSlider tmpSlider;
                UIDynamicColorPicker tmpColor;
                UIDynamicButton tmpButton;
                UIDynamicToggle tmpToggle;
                UIDynamic tmpSpacer;
             
                // *********************************************
                // A basic title and description, not mandatory...
                // but always cool to give information to your users/players
                // *********************************************
                CreateStaticDescriptionText(this,"Title",
                    "<color=#000><size=35><b>" + PLUGIN_NAME + "</b></size>\n\n<size=28>Demo for an example CUA editor/controller\n\n" +
                    "<b>To see how the plugin behaves, try:</b>\n" +
                    "- Loading any CUA\n" +
                    "- Unloading the CUA (select 'None' in the asset tab)\n" +
                    "- Loading the script on another atom\n\n" +
                    "Everytime you try something, open the plugin window to see how the UI behaves.\n\n" +
                    "That demo script is made with the CUA contained in the var file in mind.\n\nWhich means that loading any other CUA " +
                    "will show that the CUA is not compatible. This is a way for you to see how that kind of plugin can behave and help the " +
                    "player understand what type of CUA he can or cannot use." +
                    "</size></color>"
                    ,false,650,TextAnchor.MiddleLeft, true, false);
                // Creating the base storables for the UI
                // Here we simply initialize the storables without the UI,
                // the UI will be created afterwards
                sphereYPosition = new JSONStorableFloat("sphereYPosition", 0f, 0f, 2f);
                sphereYPosition.setCallbackFunction += (val) => { OnChangeSpherePosition(); };
                RegisterFloat(sphereYPosition);
             
                cubeRotation = new JSONStorableFloat("cubeRotation", 0f, 0f, 180f);
                cubeRotation.setCallbackFunction += (val) => { OnChangeCubeRotation(); };
                RegisterFloat(cubeRotation);
             
                cylinderZPosition = new JSONStorableFloat("cylinderZPosition", 0f, -3f, 3f);
                cylinderZPosition.setCallbackFunction += (val) => { OnChangeCylinderPosition(); };
                RegisterFloat(cylinderZPosition);
             
                cubeActive = new JSONStorableBool("cubeActive", true);
                cubeActive.setCallbackFunction += (val) => { OnChangeCubeState(); };
                RegisterBool(cubeActive);
            }
            catch(Exception e)
            {
                logError(PLUGIN_NAME + " - Exception caught: " + e);
            }
        }
     
        void Start()
        {
            try
            {
                // A basic alert for the user if he tries to add the plugin to anything else but a CustomUnityAsset atom
                if (containingAtom.type != "CustomUnityAsset")
                {
                    SuperController.singleton.Alert("<b><color=green>"+PLUGIN_NAME+"</color></b>\nYou must add this script on a\nCustomUnityAsset atom.", () => {});
                    _InfosText = CreateStaticDescriptionText(this,"Title","<b><color=blue><size=30>This plugin can't work with a " + containingAtom.type + " atom. Please add it on a CustomUnityAsset atom.</size></color></b>",true,150,TextAnchor.MiddleLeft);
                    return;
                }
             
                // First, maybe we're in a situation where the script is added on a CUA already loaded.
                // So we're trying to immediately get the prefab
                InitMesh(TryGetCUAPrefab());
             
                // Then we add the callbacks that will control the behavior of the UI depending on the interaction
                // on the asset tab
                Transform tr = GetChildNamed(containingAtom.reParentObject, "rescaleObject");
                currentCUALoader = tr.GetComponent<CustomUnityAssetLoader>();
                if (currentCUALoader != null)
                {
                    cuaLoaderInitialized = true;
                    currentCUALoader.RegisterAssetLoadedCallback(OnCUALoaded);
                    currentCUALoader.RegisterAssetClearedCallback(OnCUAUnloaded);
                }
                else
                {
                    logError(PLUGIN_NAME + " - Exception caught: Could not find a Custom Unity Asset loader.");
                }
            }
            catch(Exception e)
            {
                logError(PLUGIN_NAME + " - Exception caught: " + e);
            }
        }
        /** The usual Update method, nothing is needed here if you don't need it, you can safely remove it **/
        void Update()
        {
            // Things? maybe?
        }
     
        /**
         * Getting the CUA root object
         *
         * This is used to grab the gameobject instanciated by the CUA Atom
         * It is used in combination with the InitMesh method
         */
        private Transform TryGetCUAPrefab()
        {
            Transform tmpTr = GetChildNamed(containingAtom.reParentObject, "rescaleObject");
            // This is ultra weird, at the moment you can grab the child when that loaded event is triggered,
            // the first one/previous one is not yet removed
            // Don't think too much... this is some VAM weirdness as usual ;)
            // no need to change anything here
            Transform cuaClone = null;
            if (tmpTr.childCount > 0)
            {
                if( tmpTr.childCount > 1 ) {
                    cuaClone = tmpTr.GetChild(tmpTr.childCount - 1);
                }
                else
                {
                    cuaClone = tmpTr.GetChild(0);
                }            
            }
            return cuaClone;
        }
     
        /**
         * Initializing the mesh / Validating the CUA
         *
         * This is triggered when during the init phase or through the callback below : OnCUALoaded
         * The goal of this method is to check everything is good and then do some stuffs to initialize your CUA (if needed)
         */
        private void InitMesh(Transform parentClone)
        {
            if (parentClone == null)
            {
                _InfosText = CreateStaticDescriptionText(this,"Title","<b><color=blue><size=30>No asset selected in this CUA, please select an asset first using the Asset tab on the left side of this plugin.</size></color></b>",true,150,TextAnchor.MiddleLeft);
                return;
            }
            // Clearing the text info since we're gonna recreate it based on initialization
            if (_InfosText != null) RemoveTextField(_InfosText);
         
            // Checking the asset, here we can check if the clone is ok.
            // Anything is possible, you could skip that step
            if (parentClone.name == "ExampleCUA(Clone)")
            {
                // Now that I have my parent, I'm checking if my children hierarchy is what I'm looking for
                // Again, this is something you do as you wish... go nutz!
             
                // In the demo situation, I'm ensuring my CUA has my three children : a cube, a sphere and a cylinder
                sphereTR = GetChildNamed(parentClone, "sphere");
                cubeTR = GetChildNamed(parentClone, "cube");
                cylinderTR = GetChildNamed(parentClone, "cylinder");
                if ( sphereTR == null || cubeTR == null || cylinderTR == null )
                {
                    _InfosText = CreateStaticDescriptionText(this,"Title","<color=red><b><size=30>Could not find the proper hierarchy for the CUA, please fix the asset or load the correct CUA.</size></b></color>",true,120,TextAnchor.MiddleLeft);
                    return;
                }
            }
            else
            {
                _InfosText = CreateStaticDescriptionText(this,"Title","<color=orange><b><size=30>Your CUA is not compatible with this Editor. Please use the asset contained in the var file.</size></b></color>",true,120,TextAnchor.MiddleLeft);
                return;
            }
         
            CreateEditorUI();
            meshInitialized = true;
         
            // My mesh is initialized, method did not exit anywhere,
            // I can now initialize my CUA
            // This is where you'd call manually your callbacks for the JSONStorables that are saved into the scene
            OnCUAInitAllProperties();
        }
        /**
         * Clearing the mesh / Invalidating the CUA
         *
         * This is triggered when the CUA unloads through the callback below : OnCUAUnloaded
         * In that case (the demo) nothing more than changing a flag.
         *
         * But let's imagine you'd use the CUA to create new dynamic game objects, or whatever you could imagine
         * You would cleanup all your game objects here before invalidating the CUA
         */
        private void ClearMesh()
        {
            meshInitialized = false;
        }
        /**
         * Creating the Editor UI
         * This is triggered when the CUA is valid in the InitMesh method above
         */
        private void CreateEditorUI()
        {
            // Temporary vars
            // All sorts of vars you can use to create your UIDynamic elements
            HSVColor hsvcDefault = HSVColorPicker.RGBToHSV(1f, 1f, 1f);
            UIDynamicTextField tmpTextfield;
            UIDynamicPopup tmpPopup;
            UIDynamicSlider tmpSlider;
            UIDynamicColorPicker tmpColor;
            UIDynamicButton tmpButton;
            UIDynamicToggle tmpToggle;
            UIDynamic tmpSpacer;
         
            tmpSlider = CreateSlider(sphereYPosition, false);
            tmpSlider.label = "Sphere Vertical Position";
            uiSliders.Add(tmpSlider);
         
            tmpSlider = CreateSlider(cubeRotation, false);
            tmpSlider.label = "Cube Rotation";
            uiSliders.Add(tmpSlider);
         
            tmpSlider = CreateSlider(cylinderZPosition, false);
            tmpSlider.label = "Cylinder Forward Position";
            uiSliders.Add(tmpSlider);
         
            tmpToggle = CreateToggle(cubeActive, false);
            tmpToggle.label = "Cube active";
            uiToggles.Add(tmpToggle);
        }
     
        /**
         * Clearing the Editor UI
         * This is triggered upon conditions depending on what the user does with the CUA Atom
         */
        private void ClearEditorUI()
        {
            foreach( UIDynamicSlider uiEl in uiSliders ) {
                RemoveSlider(uiEl);
            }
            uiSliders.Clear();
         
            foreach( UIDynamicToggle uiEl in uiToggles ) {
                RemoveToggle(uiEl);
            }
            uiToggles.Clear();
         
            /*** ************************
             * EXAMPLE UIDynamic for other components
             * **************************
            foreach( UIDynamicPopup uiEl in uiPopups ) {
                RemovePopup(uiEl);
            }
            uiPopups.Clear();
         
            foreach( UIDynamicColorPicker uiEl in uiColorPickers ) {
                RemoveColorPicker(uiEl);
            }
            uiColorPickers.Clear();
            ************************ ***/
        }
        // **************************
        // Callbacks
        // **************************
     
        /**
         * This is the callback used when the CUA is loaded
         * There's technically nothing to do here besides calling the InitMesh method
         * as it is only when the mesh is initialized that you can do things.
         *
         * So if you need to apply things after the CUA is loaded, do it in the InitMesh method
         */
        private void OnCUALoaded()
        {
            InitMesh(TryGetCUAPrefab());
        }
     
        /** This is the callback used when the CUA is unloaded **/
        private void OnCUAUnloaded()
        {
            // When the CUA is unloaded we can do some cleaning actions
            // In that situation, ClearMesh simply invalidates the mesh initialization
            // and I'm clearing the UI to avoid any control while there is no CUA
            ClearMesh();
            ClearEditorUI();
         
            // I'm also updating the UI to give some feedback to the user
            if (_InfosText != null) RemoveTextField(_InfosText);
            _InfosText = CreateStaticDescriptionText(this,"Title","<b><color=blue><size=30>No asset selected in this CUA, please select an asset first using the Asset tab on the left side of this plugin.</size></color></b>",true,150,TextAnchor.MiddleLeft);
        }
        /***
         * The misc methods to control your CUA
         * This would obviously be completely custom for your use case, anything is possible
         ***/
        private void OnChangeSpherePosition()
        {
            // All the methods can check if the CUA is actually active and properly initialized
            // This avoid any critical error log up until the game object is usable or when the game object is invalidated
            if (!meshInitialized) return;
            Vector3 sphereNewPosition = sphereTR.localPosition;
            sphereNewPosition.y = sphereYPosition.val;
            sphereTR.localPosition = sphereNewPosition;
        }
     
        private void OnChangeCubeRotation()
        {
            if (!meshInitialized) return;
            Quaternion sphereNewRotation = Quaternion.Euler(0f, cubeRotation.val, 0f);
            cubeTR.localRotation = sphereNewRotation;
        }
     
        private void OnChangeCylinderPosition()
        {
            if (!meshInitialized) return;
            Vector3 cylinderNewPosition = cylinderTR.localPosition;
            cylinderNewPosition.z = cylinderZPosition.val;
            cylinderTR.localPosition = cylinderNewPosition;
        }
     
        private void OnChangeCubeState()
        {
            if (!meshInitialized) return;
            cubeTR.gameObject.SetActive(cubeActive.val);
        }
        /**
         * This method is called when the CUA is initialized, and is triggering all callbacks
         */
        private void OnCUAInitAllProperties()
        {
            OnChangeSpherePosition();
            OnChangeCubeRotation();
            OnChangeCylinderPosition();
            OnChangeCubeState();
        }
     
        // **************************
        // Time to cleanup !
        // **************************
        void OnDestroy() {
            // Cleaning up callbacks for the CUA
            // Always do that because you're a nice and smart coder ;)
            if (cuaLoaderInitialized)
            {
                currentCUALoader.DeregisterAssetLoadedCallback(OnCUALoaded);
                currentCUALoader.DeregisterAssetClearedCallback(OnCUAUnloaded);
            }
        }
     
        // **************************
        // Local Tools
        //
        // You could obviously keep this here, or create a "utils" library you'd put in all your plugins
        // **************************
        private void logDebug( string debugText ) {
            SuperController.LogMessage( debugText );
        }
     
        private void logError( string debugText ) {
            SuperController.LogError( debugText );
        }
     
        private Transform GetChildNamed( Transform parent, string childName ) {
            foreach( Transform child in parent )
            {
                if( child.name == childName ) {
                    return child;
                } else {
                    Transform childSearch = GetChildNamed(child,childName);
                    if (childSearch != null) return childSearch;
                }
            }
     
            return null;
        }
     
        private UIDynamicTextField CreateStaticDescriptionText(MVRScript script, string DescTitle, string DescText, bool rightSide, int fieldHeight, TextAnchor textAlignment = TextAnchor.UpperLeft, bool disableBackground = true, bool disableScroll = true ) {
            JSONStorableString staticDescString = new JSONStorableString(DescTitle,DescText) {isStorable=false,isRestorable=false};
            staticDescString.hidden = true;
            UIDynamicTextField staticDescStringField = CreateTextField(staticDescString, rightSide);
            if( disableBackground ) staticDescStringField.backgroundColor = new Color(1f, 1f, 1f, 0f);
            staticDescStringField.UItext.alignment = textAlignment;
            LayoutElement sdsfLayout = staticDescStringField.GetComponent<LayoutElement>();
            sdsfLayout.preferredHeight = sdsfLayout.minHeight = fieldHeight;
            staticDescStringField.height = fieldHeight;
            if( disableScroll ) DisableScrollOnText(staticDescStringField);
            return staticDescStringField;
        }
     
        private void DisableScrollOnText(UIDynamicTextField target) {
            // THIS is important, it's a "useless" temporary canvas group that will prevent the event from the mousewheel
            CanvasGroup tmpCG = target.UItext.transform.parent.transform.parent.transform.parent.gameObject.AddComponent<CanvasGroup>();
            tmpCG.blocksRaycasts = false;
         
            ScrollRect targetSR = target.UItext.transform.parent.transform.parent.transform.parent.GetComponent<ScrollRect>();
            if( targetSR != null ) {
                targetSR.horizontal = false;
                targetSR.vertical = false;
            }
        }
    }
}
	3 - Make your own thing, be creative
This script shows one thing : when you have declared VAM's UI / JSONStorables, and initialized your CUA properly we are in a well know territory... Unity.
You then have virtually almost no limit to what you can do.
4 - Conclusion
That's all I guess... if some things are not clear don't hesitate to ask for clarification.
I'll just add something about performances : I've seen a long time ago someone who did implement something similar for an asset. I don't remember what it was but the script was making modifications on the asset in the Update function.
Be really careful with that since update is called every frame. Unless your prefab is meant to do something every frame (on purpose)... modifying basic properties of an object that should not change can be done with callbacks.