A few person on discord and on the hub asked if it was possible to control a custom asset through script... so after fixing a bug I had with mine with the help of meshed, I promised I made a tutorial to share the knowledge. So here we go.
What do you need?
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.
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 and the demo CUA.
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 two shapes, a sphere and a cube. In Unity, the hierarchy of my object looks like this :
vam_script_tutorial is just a transform ( just because I like things ordered and neat )
sphere and cube are basic gameobjects with a mesh filter, mesh renderer and a collider.
Save your object as a prefab, pack it and put it in your VAM directory.
2 - Write your script
Create an empty scene, add a CUA and load your freshly packed prefab from Unity. Then start to write your script.
3 - Make your own thing, be creative
This script shows one thing : when you have declared VAM's UI / JSONStorables, we are in a well know territory... Unity.
You then have virtually almost no limit to what you can do.
In this example, I've changed a parameter of the material of the objects and their Active state. But you could play with transform positions, sizes. You could attach components (as long as they are components embedded in VAM's build), you can interact with all the type of object Unity offers : 3D objects, 2D objects, cameras, sounds, video player...
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... modifying basic properties of an object that should not change can be done with callbacks.
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.
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 and the demo CUA.
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 two shapes, a sphere and a cube. In Unity, the hierarchy of my object looks like this :
vam_script_tutorial is just a transform ( just because I like things ordered and neat )
sphere and cube are basic gameobjects with a mesh filter, mesh renderer and a collider.
Save your object as a prefab, pack it and put it in your VAM directory.
2 - Write your script
Create an empty scene, add a CUA and load your freshly packed prefab from Unity. Then start to write your script.
C#:
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using SimpleJSON;
using System.Linq;
using System.IO;
// CUAEditor example
//
// Base editor for a CUA (Custom Unity Asset)
namespace CUAEditorPlugin
{
public class CUAEditor : MVRScript
{
// My JSONStorable for the editor
public JSONStorableColor SphereColor;
public JSONStorableBool SphereVisibility;
public JSONStorableColor CubeColor;
public JSONStorableBool CubeVisibility;
// My misc variables to find and edit the prefab made in Unity
private string objectNameSearchString = "vam_script_tutorial";
public Transform ObjectRoot;
private Transform sphereTR;
private Transform cubeTR;
private Renderer sphereRenderer;
private Renderer cubeRenderer;
// Security and initialization
private bool editorInitialized = false;
private int initLoops = 0;
private int maxInitLoops = 3600;
// The usual init function for VAM
public override void Init()
{
try
{
// We only authorize the script to be used by a CUA
if (containingAtom.type != "CustomUnityAsset")
{
logError("CUAEditor - Please add this script on a Custom Unity Asset ( script disabled ).");
return;
}
// Default color for both shapes
HSVColor hsvc = HSVColorPicker.RGBToHSV(1f, 1f, 1f);
// Color selection for the sphere
SphereColor = new JSONStorableColor("Sphere color", hsvc, SphereColorCallback);
RegisterColor(SphereColor);
CreateColorPicker(SphereColor);
// Visibility state for the sphere
SphereVisibility = new JSONStorableBool("Sphere is visible", true, SphereVisibilityCallback);
SphereVisibility.storeType = JSONStorableParam.StoreType.Full;
CreateToggle(SphereVisibility, false);
RegisterBool(SphereVisibility);
// Color selection for the cube
CubeColor = new JSONStorableColor("Cube color", hsvc, CubeColorCallback);
RegisterColor(CubeColor);
CreateColorPicker(CubeColor);
// Visibility state for the cube
CubeVisibility = new JSONStorableBool("Cube is visible", true, CubeVisibilityCallback);
CubeVisibility.storeType = JSONStorableParam.StoreType.Full;
CreateToggle(CubeVisibility, false);
RegisterBool(CubeVisibility);
// A tiny bit of help to give some info to your fellow creator and/or player
JSONStorableString helpText = new JSONStorableString("Help",
"<color=#000><size=35><b>Help</b></size></color>\n\n" +
"<color=#333>" +
"<b>CUAEditor</b> is a tiny plugin to show you how you can create an editor for a CUA ( Custom Unity Asset ).\n\n" +
"Change the parameters on the left and you will see the CUA updating in realtime.\n\n" +
"<b>IMPORTANT :</b> If you switch objects in the Asset tab of the CUA, don't forget to reload the script to enable customization.\n\n" +
"</color>"
);
UIDynamic helpTextfield = CreateTextField(helpText, true);
helpTextfield.height = 1100.0f;
}
catch(Exception e)
{
SuperController.LogError("CUAEditor - Exception caught: " + e);
}
}
// In this case we don't need that, but maybe you do ?
void Start()
{
}
// Used for the initialization of the editor.
// When it is initialized, there's nothing done every frame
void Update()
{
// If the editor is not initialized, we're gonna try to do it
// I'm putting 3600 tries ( at 60fps it is 1min )
if( initLoops < maxInitLoops && editorInitialized == false ) {
initLoops++;
InitEditor();
}
// Triggering an error if we have reached our limit of tries
if( initLoops == maxInitLoops ) {
logError("CUAEditor : This CUA could not be initialized properly after 3600 tries. Please select a compatible object and reload the script.");
initLoops++; // To avoid spamming the log with the error
}
}
// The function initializing everything we need to update the prefab with the editor
// You can imagine whatever you want here, the only limitations are the things available in VAM's build
// In this example case, we need the main transform, a transform for the sphere and the cube
//
// Initially this function is made to return a bool state to see if it is properly initialized.
// But since the CUA loader is asynchronous when you reload a scene and I don't have a way to identify when unity's prefab is loaded
// I have to "brute force" the initialization several times until the script finds the transform I need
// It implies that your prefab is done properly and has the good name ( this is why the maxInitLoops exists )
private bool InitEditor() {
// When the editor is initialized, we don't need to do that anymore
if( editorInitialized == true ) return true;
// We need to find our main transform, getObjectRoot is gonna do that for us
// the search is based on our variable objectNameSearchString
// you need to create a prefab containing this name in Unity
// This method works for unparented and parented objects
// ( Thanks for the reParentObject tip meshed ! )
ObjectRoot = getObjectRoot( containingAtom.reParentObject );
if( ObjectRoot == null ) {
return false;
}
// Finding my sphere and cube transforms
sphereTR = ObjectRoot.Find("sphere");
cubeTR = ObjectRoot.Find("cube");
// I found my transforms, now the rest
if( sphereTR && cubeTR ) {
// Finding my sphere and cube renderers
sphereRenderer = sphereTR.GetComponent<Renderer>();
cubeRenderer = cubeTR.GetComponent<Renderer>();
// I found my renderers components, let's finish this
if( sphereRenderer && cubeRenderer ) {
// Everything is good, I can consider my editor initialized
editorInitialized = true;
// I now need initialize my different objects based on the informations in my JSONStorables
// You could go crazy and make unified functions to control the whole object instead
SphereColorCallback( SphereColor );
SphereVisibilityCallback( SphereVisibility.val );
CubeColorCallback( CubeColor );
CubeVisibilityCallback( CubeVisibility.val );
logDebug("CUAEditor : editor initialized properly");
return true;
} else {
logError("CUAEditor : your CUA has a transform with the good name but does not have a renderer.");
initLoops = maxInitLoops; // We don't have what we need, we consider the initialization failed
return false;
}
} else {
logError("CUAEditor : your CUA is missing mandatory components ( transform name cube and sphere ).");
initLoops = maxInitLoops; // We don't have what we need, we consider the initialization failed
return false;
}
return false;
}
// Recursive function searching for a transform containing the string set in objectNameSearchString
// It is used the first time for the initialization of the script
private Transform getObjectRoot( Transform parent ) {
foreach( Transform child in parent )
{
if( child.name.Contains(objectNameSearchString) ) {
return child;
} else {
Transform childSearch = getObjectRoot(child);
if (childSearch != null) return childSearch;
}
}
return null;
}
// Callback triggered when you change the color for the sphere (or you can call it manually)
protected void SphereColorCallback(JSONStorableColor color) {
if( sphereRenderer ) {
Color newColor = new Color(color.colorPicker.currentColor.r, color.colorPicker.currentColor.g, color.colorPicker.currentColor.b, 1.0f);
sphereRenderer.material.SetColor("_Color",newColor);
}
}
// Callback triggered when you change the visibility for the sphere (or you can call it manually)
protected void SphereVisibilityCallback(bool visibility) {
if( sphereTR ) {
sphereTR.gameObject.SetActive(visibility);
}
}
// Callback triggered when you change the color for the cube (or you can call it manually)
protected void CubeColorCallback(JSONStorableColor color) {
if( cubeRenderer ) {
Color newColor = new Color(color.colorPicker.currentColor.r, color.colorPicker.currentColor.g, color.colorPicker.currentColor.b, 1.0f);
cubeRenderer.material.SetColor("_Color",newColor);
}
}
// Callback triggered when you change the visibility for the sphere (or you can call it manually)
protected void CubeVisibilityCallback(bool visibility) {
if( cubeTR ) {
cubeTR.gameObject.SetActive(visibility);
}
}
// **************************
// Local Tools... just because I'm lazy
// **************************
private void logDebug( string debugText ) {
SuperController.LogMessage( debugText );
}
private void logError( string debugText ) {
SuperController.LogError( debugText );
}
}
}
3 - Make your own thing, be creative
This script shows one thing : when you have declared VAM's UI / JSONStorables, we are in a well know territory... Unity.
You then have virtually almost no limit to what you can do.
In this example, I've changed a parameter of the material of the objects and their Active state. But you could play with transform positions, sizes. You could attach components (as long as they are components embedded in VAM's build), you can interact with all the type of object Unity offers : 3D objects, 2D objects, cameras, sounds, video player...
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... modifying basic properties of an object that should not change can be done with callbacks.