using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using JetBrains.Annotations; using Unity.Profiling; using UnityEngine; using VRC.SDK3.Components; using VRC.SDKBase; using VRC.Udon.Common; using VRC.Udon.Common.Attributes; using VRC.Udon.Common.Enums; using VRC.Udon.Common.Interfaces; using VRC.Udon.Serialization.OdinSerializer; using VRC.Udon.VM; using Logger = VRC.Core.Logger; using Object = UnityEngine.Object; #if UNITY_EDITOR && !VRC_CLIENT using UnityEditor.SceneManagement; #endif namespace VRC.Udon { public sealed class UdonBehaviour : AbstractUdonBehaviour, ISerializationCallbackReceiver { #region Odin Serialized Fields [PublicAPI] public IUdonVariableTable publicVariables = new UdonVariableTable(); #endregion #region Serialized Public Fields [Obsolete("Use VRCObjectSync instead")] [PublicAPI] // ReSharper disable once InconsistentNaming public bool SynchronizePosition; // ReSharper disable once InconsistentNaming [PublicAPI] public readonly bool SynchronizeAnimation = false; //We don't support animation sync yet, coming soon. // ReSharper disable once InconsistentNaming [Obsolete("Use VRCObjectSync instead")] [PublicAPI] public bool AllowCollisionOwnershipTransfer = true; // ReSharper disable once InconsistentNaming [HideInInspector, Obsolete("Use SyncMethod instead")] public bool Reliable = false; // ReSharper disable once InconsistentNaming [SerializeField] private Networking.SyncType _syncMethod = Networking.SyncType.Unknown; public Networking.SyncType SyncMethod { get { // Old Scene? if(_syncMethod == Networking.SyncType.Unknown) { #pragma warning disable 618 _syncMethod = Reliable ? Networking.SyncType.Manual : Networking.SyncType.Continuous; #pragma warning restore 618 } return _syncMethod; } set { _syncMethod = value; if(value == Networking.SyncType.None) { return; } // All synced UdonBehaviours on one GameObject must use the same sync method. foreach(UdonBehaviour ub in gameObject.GetComponents()) { if(ub != null && ub._syncMethod != Networking.SyncType.None) { ub._syncMethod = value; } } } } public bool SyncIsContinuous => SyncMethod == Networking.SyncType.Continuous; public bool SyncIsManual => SyncMethod == Networking.SyncType.Manual; #endregion #region Serialized Private Fields [SerializeField] private AbstractSerializedUdonProgramAsset serializedProgramAsset; #if UNITY_EDITOR && !VRC_CLIENT [SerializeField] public AbstractUdonProgramSource programSource; #endif #endregion #region Public Fields and Properties [PublicAPI] public static Action OnInit { get; set; } = null; [PublicAPI] public static Action RequestSerializationHook { get; set; } = null; [PublicAPI] public static Action SendCustomNetworkEventHook { get; set; } = null; [PublicAPI] public override bool DisableInteractive { get; set; } [PublicAPI] [ExcludeFromUdonWrapper] public override bool IsNetworkingSupported { get => _isNetworkingSupported; set { if(_initialized) { throw new InvalidOperationException( "IsNetworkingSupported cannot be changed after the UdonBehaviour has been initialized."); } _isNetworkingSupported = value; } } public override bool IsInteractive => _hasInteractiveEvents && !DisableInteractive; // ReSharper disable once InconsistentNaming public const string ReturnVariableName = "__returnValue"; internal int UpdateOrder => _program?.UpdateOrder ?? 0; #endregion #region Private Fields and Properties private UdonManager _udonManager; private IUdonProgram _program; private IUdonVM _udonVM; private bool _isReady; private int _debugLevel; private bool _hasError; private bool _hasDoneStart; private bool _initialized; private bool _isNetworkingSupported = false; private bool _hasInteractiveEvents; private bool _hasUpdateEvent; private bool _hasLateUpdateEvent; private bool _hasFixedUpdateEvent; private bool _hasPostLateUpdateEvent; private readonly Dictionary> _eventTable = new Dictionary>(); private readonly Dictionary<(string eventName, string symbolName), string> _symbolNameCache = new Dictionary<(string, string), string>(); private static ProfilerMarker _managedUpdateProfilerMarker = new ProfilerMarker("UdonBehaviour.ManagedUpdate()"); private static ProfilerMarker _managedLateUpdateProfilerMarker = new ProfilerMarker("UdonBehaviour.ManagedLateUpdate()"); private static ProfilerMarker _managedFixedUpdateProfilerMarker = new ProfilerMarker("UdonBehaviour.ManagedFixedUpdate()"); private static ProfilerMarker _postLateUpdateProfilerMarker = new ProfilerMarker("UdonBehaviour.PostLateUpdate()"); private readonly SortedDictionary _variableToChangeEvent = new SortedDictionary(); private readonly List _eventProxies = new List(); #endregion #region Editor Only #if UNITY_EDITOR && !VRC_CLIENT public void RunEditorUpdate(ref bool dirty) { if (programSource == null) { return; } programSource.RunEditorUpdate(this, ref dirty); if (!dirty) { return; } EditorSceneManager.MarkSceneDirty(gameObject.scene); } #endif #endregion #region Private Methods private bool LoadProgram() { if(serializedProgramAsset == null) { return false; } _program = serializedProgramAsset.RetrieveProgram(); IUdonSymbolTable symbolTable = _program?.SymbolTable; IUdonHeap heap = _program?.Heap; if(symbolTable == null || heap == null) { return false; } foreach(string variableSymbol in publicVariables.VariableSymbols) { if(!symbolTable.HasAddressForSymbol(variableSymbol)) { continue; } uint symbolAddress = symbolTable.GetAddressFromSymbol(variableSymbol); if(!publicVariables.TryGetVariableType(variableSymbol, out Type declaredType)) { continue; } publicVariables.TryGetVariableValue(variableSymbol, out object value); if(declaredType == typeof(GameObject) || declaredType == typeof(UdonBehaviour) || declaredType == typeof(Transform)) { if(value == null) { value = new UdonGameObjectComponentHeapReference(declaredType); declaredType = typeof(UdonGameObjectComponentHeapReference); } } heap.SetHeapVariable(symbolAddress, value, declaredType); } return true; } private void RegisterEventProxy() where T : AbstractUdonBehaviourEventProxy { // Exit early if we already have a match. foreach(AbstractUdonBehaviourEventProxy existingProxy in _eventProxies) { if(!(existingProxy is T existingProxyAsT)) { continue; } if(existingProxyAsT.EventReceiver.Equals(this)) { return; } } AbstractUdonBehaviourEventProxy proxy = gameObject.AddComponent(); #if UNITY_EDITOR proxy.hideFlags = HideFlags.HideInInspector | HideFlags.DontSaveInEditor | HideFlags.DontSaveInBuild; #endif proxy.EventReceiver = this; proxy.enabled = enabled; _eventProxies.Add(proxy); } private void ProcessEntryPoints() { if(_program.EntryPoints.HasExportedSymbol("_interact")) { _hasInteractiveEvents = true; } if(_program.EntryPoints.HasExportedSymbol("_update")) { _hasUpdateEvent = true; } if(_program.EntryPoints.HasExportedSymbol("_lateUpdate")) { _hasLateUpdateEvent = true; } if(_program.EntryPoints.HasExportedSymbol("_fixedUpdate")) { _hasFixedUpdateEvent = true; } if(_program.EntryPoints.HasExportedSymbol("_postLateUpdate")) { _hasPostLateUpdateEvent = true; } DetectExistingProxies(); if(_program.EntryPoints.HasExportedSymbol("_onRenderObject")) { RegisterEventProxy(); } if(_program.EntryPoints.HasExportedSymbol("_onWillRenderObject")) { RegisterEventProxy(); } if(_program.EntryPoints.HasExportedSymbol("_onTriggerStay") || _program.EntryPoints.HasExportedSymbol("_onPlayerTriggerStay")) { RegisterEventProxy(); } if(_program.EntryPoints.HasExportedSymbol("_onCollisionStay") || _program.EntryPoints.HasExportedSymbol("_onPlayerCollisionStay")) { RegisterEventProxy(); } if(_program.EntryPoints.HasExportedSymbol("_onAnimatorMove")) { RegisterEventProxy(); } if (_program.EntryPoints.HasExportedSymbol("_onAudioFilterRead")) { RegisterEventProxy(); } RegisterUpdate(); _eventTable.Clear(); foreach(string entryPoint in _program.EntryPoints.GetExportedSymbols()) { uint address = _program.EntryPoints.GetAddressFromSymbol(entryPoint); if(!_eventTable.ContainsKey(entryPoint)) { _eventTable.Add(entryPoint, new List()); } _eventTable[entryPoint].Add(address); _udonManager.RegisterInput(this, entryPoint, true); // check whether this is a variableChangedEvent if(entryPoint.StartsWith(VariableChangedEvent.EVENT_PREFIX)) { string variableName = entryPoint.Remove(0, VariableChangedEvent.EVENT_PREFIX.Length); // ensure the variable with the matching name exists if(_program.SymbolTable.TryGetAddressFromSymbol(variableName, out uint variableAddress)) { // the old variable is only added if it's used, so just store default if it's not _program.SymbolTable.TryGetAddressFromSymbol(string.Concat(VariableChangedEvent.OLD_VALUE_PREFIX, variableName), out uint oldVariableAddress); // add variable > event address lookup _variableToChangeEvent.Add(variableAddress, (address, oldVariableAddress)); } } } } // GameObjects may sometimes already have proxies for this UdonBehaviour. // For example if the GameObject was cloned from a scene GameObject, // or the component was for some reason added in the editor (this is not an expected workflow). // In either case Unity will serialize the reference from the proxy to this UdonBehaviour, // but will not serialize the contents of the _eventProxies List so we need to build it. // If we don't do this then we may create a proxy where one already exists and events will run twice. private void DetectExistingProxies() { GetComponents(_eventProxies); for(int i = _eventProxies.Count - 1; i >= 0; i--) { AbstractUdonBehaviourEventProxy proxy = _eventProxies[i]; if(proxy == null) { _eventProxies.RemoveAt(i); continue; } UdonBehaviour proxyEventReceiver = proxy.EventReceiver; // Destroy and remove all copied proxy components which don't have an EventReceiver. if(proxyEventReceiver == null) { Destroy(proxy); _eventProxies.RemoveAt(i); continue; } // Remove all copied proxy components which aren't for this UdonBehaviour. if(proxyEventReceiver == this) { continue; } _eventProxies.RemoveAt(i); } } private bool ResolveUdonHeapReferences(IUdonSymbolTable symbolTable, IUdonHeap heap) { bool success = true; foreach(string symbolName in symbolTable.GetSymbols()) { uint symbolAddress = symbolTable.GetAddressFromSymbol(symbolName); object heapValue = heap.GetHeapVariable(symbolAddress); if(!(heapValue is UdonBaseHeapReference udonBaseHeapReference)) { continue; } if(!ResolveUdonHeapReference(heap, symbolAddress, udonBaseHeapReference)) { success = false; } } return success; } private bool ResolveUdonHeapReference(IUdonHeap heap, uint symbolAddress, UdonBaseHeapReference udonBaseHeapReference) { switch(udonBaseHeapReference) { case UdonGameObjectComponentHeapReference udonGameObjectComponentHeapReference: { Type referenceType = udonGameObjectComponentHeapReference.type; if(referenceType == typeof(GameObject)) { heap.SetHeapVariable(symbolAddress, gameObject); return true; } else if(referenceType == typeof(Transform)) { heap.SetHeapVariable(symbolAddress, gameObject.transform); return true; } else if(referenceType == typeof(UdonBehaviour)) { heap.SetHeapVariable(symbolAddress, this); return true; } else if(referenceType == typeof(Object)) { heap.SetHeapVariable(symbolAddress, this); return true; } else { Logger.Log( $"Unsupported GameObject/Component reference type: {udonBaseHeapReference.GetType().Name}. Only GameObject, Transform, and UdonBehaviour are supported.", _debugLevel, this); return false; } } default: { Logger.Log( $"Unknown heap reference type: {udonBaseHeapReference.GetType().Name}", _debugLevel, this); return false; } } } #endregion #region Managed Unity Events internal void ManagedUpdate() { using(_managedUpdateProfilerMarker.Auto()) { if(!_hasDoneStart && _isReady) { _hasDoneStart = true; RunEvent("_onEnable"); RunEvent("_start"); if(!_hasUpdateEvent) { _udonManager.UnregisterUdonBehaviourUpdate(this); } } RunEvent("_update"); } } internal void ManagedLateUpdate() { using(_managedLateUpdateProfilerMarker.Auto()) { RunEvent("_lateUpdate"); } } internal void ManagedFixedUpdate() { using(_managedFixedUpdateProfilerMarker.Auto()) { RunEvent("_fixedUpdate"); } } internal void PostLateUpdate() { using(_postLateUpdateProfilerMarker.Auto()) { RunEvent("_postLateUpdate"); } } #endregion #region Unity Events public void OnAnimatorIK(int layerIndex) { RunEvent("_onAnimatorIK", ("layerIndex", layerIndex)); } internal void ProxyOnAnimatorMove() { RunEvent("_onAnimatorMove"); } internal void ProxyOnAudioFilterRead(float[] data, int channels) { RunEvent("_onAudioFilterRead", ("data", data), ("channels", channels)); } public void OnBecameInvisible() { RunEvent("_onBecameInvisible"); } public void OnBecameVisible() { RunEvent("_onBecameVisible"); } public void OnCollisionEnter(Collision other) { var player = VRCPlayerApi.GetPlayerByGameObject(other.gameObject); if(player != null) { RunEvent("_onPlayerCollisionEnter", ("player", player)); } else if(!UdonManager.Instance.IsBlacklisted(other)) { RunEvent("_onCollisionEnter", ("other", other)); } } public void OnCollisionEnter2D(Collision2D other) { if(!UdonManager.Instance.IsBlacklisted(other)) { RunEvent("_onCollisionEnter2D", ("other", other)); } } public void OnCollisionExit(Collision other) { var player = VRCPlayerApi.GetPlayerByGameObject(other.gameObject); if(player != null) { RunEvent("_onPlayerCollisionExit", ("player", player)); } else if(!UdonManager.Instance.IsBlacklisted(other)) { RunEvent("_onCollisionExit", ("other", other)); } } public void OnCollisionExit2D(Collision2D other) { if(!UdonManager.Instance.IsBlacklisted(other)) { RunEvent("_onCollisionExit2D", ("other", other)); } } internal void ProxyOnCollisionStay(Collision other) { var player = VRCPlayerApi.GetPlayerByGameObject(other.gameObject); if(player != null) { RunEvent("_onPlayerCollisionStay", ("player", player)); } else if(!UdonManager.Instance.IsBlacklisted(other)) { RunEvent("_onCollisionStay", ("other", other)); } } public void OnCollisionStay2D(Collision2D other) { if(!UdonManager.Instance.IsBlacklisted(other)) { RunEvent("_onCollisionStay2D", ("other", other)); } } public void OnDestroy() { if(_program == null) { return; } foreach(string entryPoint in _program.EntryPoints.GetExportedSymbols()) { _udonManager.RegisterInput(this, entryPoint, false); } RunEvent("_onDestroy"); _udonVM = null; _program = null; foreach(AbstractUdonBehaviourEventProxy proxy in _eventProxies) { if(proxy) { Destroy(proxy); } } } public void OnDisable() { UnregisterUpdate(); RunEvent("_onDisable"); } public void OnDrawGizmos() { RunEvent("_onDrawGizmos"); } public void OnDrawGizmosSelected() { RunEvent("_onDrawGizmosSelected"); } public void OnEnable() { if(_initialized) { RegisterUpdate(); } RunEvent("_onEnable"); } public void OnJointBreak(float breakForce) { RunEvent("_onJointBreak", ("force", breakForce)); } public void OnJointBreak2D(Joint2D brokenJoint) { RunEvent("_onJointBreak2D", ("joint", brokenJoint)); } public void OnMouseDown() { RunEvent("_onMouseDown"); } public void OnMouseDrag() { RunEvent("_onMouseDrag"); } public void OnMouseEnter() { RunEvent("_onMouseEnter"); } public void OnMouseExit() { RunEvent("_onMouseExit"); } public void OnMouseOver() { RunEvent("_onMouseOver"); } public void OnMouseUp() { RunEvent("_onMouseUp"); } public void OnMouseUpAsButton() { RunEvent("_onMouseUpAsButton"); } public void OnParticleCollision(GameObject other) { var player = VRCPlayerApi.GetPlayerByGameObject(other.gameObject); if(player != null) { RunEvent("_onPlayerParticleCollision", ("player", player)); } else { RunEvent("_onParticleCollision", ("other", other)); } } public void OnParticleTrigger() { RunEvent("_onParticleTrigger"); } public void OnPostRender() { RunEvent("_onPostRender"); } public void OnPreCull() { RunEvent("_onPreCull"); } public void OnPreRender() { RunEvent("_onPreRender"); } public void OnRenderImage(RenderTexture src, RenderTexture dest) { if(!_eventTable.ContainsKey("_onRenderImage") || _eventTable["_onRenderImage"].Count == 0) { Graphics.Blit(src, dest); return; } RunEvent("_onRenderImage", ("src", src), ("dest", dest)); } internal void ProxyOnRenderObject() { RunEvent("_onRenderObject"); } public void OnTransformChildrenChanged() { RunEvent("_onTransformChildrenChanged"); } public void OnTransformParentChanged() { RunEvent("_onTransformParentChanged"); } public void OnTriggerEnter(Collider other) { var player = VRCPlayerApi.GetPlayerByGameObject(other.gameObject); if(player != null) { RunEvent("_onPlayerTriggerEnter", ("player", player)); } else if(!UdonManager.Instance.IsBlacklisted(other)) { RunEvent("_onTriggerEnter", ("other", other)); } } public void OnTriggerEnter2D(Collider2D other) { if(!UdonManager.Instance.IsBlacklisted(other)) { RunEvent("_onTriggerEnter2D", ("other", other)); } } public void OnTriggerExit(Collider other) { var player = VRCPlayerApi.GetPlayerByGameObject(other.gameObject); if(player != null) { RunEvent("_onPlayerTriggerExit", ("player", player)); } else if(!UdonManager.Instance.IsBlacklisted(other)) { RunEvent("_onTriggerExit", ("other", other)); } } public void OnTriggerExit2D(Collider2D other) { if(!UdonManager.Instance.IsBlacklisted(other)) { RunEvent("_onTriggerExit2D", ("other", other)); } } internal void ProxyOnTriggerStay(Collider other) { var player = VRCPlayerApi.GetPlayerByGameObject(other.gameObject); if(player != null) { RunEvent("_onPlayerTriggerStay", ("player", player)); } else if(!UdonManager.Instance.IsBlacklisted(other)) { RunEvent("_onTriggerStay", ("other", other)); } } public void OnTriggerStay2D(Collider2D other) { if(!UdonManager.Instance.IsBlacklisted(other)) { RunEvent("_onTriggerStay2D", ("other", other)); } } public void OnValidate() { RunEvent("_onValidate"); } internal void ProxyOnWillRenderObject() { RunEvent("_onWillRenderObject"); } #endregion #region VRCSDK Events #if VRC_CLIENT [PublicAPI] private void OnNetworkReady() { _isReady = true; } #endif //Called through Interactable interface public override void Interact() { RunEvent("_interact"); } public override void OnDrop() { RunEvent("_onDrop"); } public override void OnPickup() { RunEvent("_onPickup"); } public override void OnPickupUseDown() { RunEvent("_onPickupUseDown"); } public override void OnPickupUseUp() { RunEvent("_onPickupUseUp"); } //Called via delegate by UdonSync [PublicAPI] public void OnPreSerialization() { if(_syncMethod == Networking.SyncType.None) { return; } RunEvent("_onPreSerialization"); } //Called via delegate by UdonSync [PublicAPI] public void OnPostSerialization(SerializationResult result) { if(_syncMethod == Networking.SyncType.None) { return; } RunEvent("_onPostSerialization", ("result", result)); } //Called via delegate by UdonSync [PublicAPI] public void OnDeserialization() { if(_syncMethod == Networking.SyncType.None) { return; } RunEvent("_onDeserialization"); } #endregion #region RunProgram Methods [PublicAPI] public override void RunProgram(string eventName) { if(_program == null) { return; } if(!_program.EntryPoints.GetExportedSymbols().Contains(eventName)) { return; } uint address = _program.EntryPoints.GetAddressFromSymbol(eventName); RunProgram(address); } private void RunProgram(uint entryPoint) { if(_hasError) { return; } if(_udonVM == null) { return; } uint originalAddress = _udonVM.GetProgramCounter(); UdonBehaviour originalExecuting = _udonManager.currentlyExecuting; _udonVM.SetProgramCounter(entryPoint); _udonManager.currentlyExecuting = this; _udonVM.DebugLogging = _udonManager.DebugLogging; try { uint result = _udonVM.Interpret(); if(result != 0) { Logger.LogError( $"Udon VM execution errored, this UdonBehaviour will be halted.", _debugLevel, this); _hasError = true; enabled = false; } } catch(UdonVMException error) { Logger.LogError( $"An exception occurred during Udon execution, this UdonBehaviour will be halted.\n{error}", _debugLevel, this); _hasError = true; enabled = false; } _udonManager.currentlyExecuting = originalExecuting; if(originalAddress < 0xFFFFFFFC) { _udonVM.SetProgramCounter(originalAddress); } } [PublicAPI] public ImmutableArray GetPrograms() { return _program?.EntryPoints.GetExportedSymbols() ?? ImmutableArray.Empty; } #endregion #region Serialization [SerializeField] private string serializedPublicVariablesBytesString; [SerializeField] private List publicVariablesUnityEngineObjects; [SerializeField] private DataFormat publicVariablesSerializationDataFormat = DataFormat.Binary; void ISerializationCallbackReceiver.OnAfterDeserialize() { DeserializePublicVariables(); } private void DeserializePublicVariables() { byte[] serializedPublicVariablesBytes = Convert.FromBase64String(serializedPublicVariablesBytesString ?? ""); publicVariables = SerializationUtility.DeserializeValue( serializedPublicVariablesBytes, publicVariablesSerializationDataFormat, publicVariablesUnityEngineObjects ) ?? new UdonVariableTable(); // Validate that the type of the value can actually be cast to the declaredType to avoid InvalidCastExceptions later. foreach(string publicVariableSymbol in publicVariables.VariableSymbols.ToArray()) { if(!publicVariables.TryGetVariableValue(publicVariableSymbol, out object value)) { continue; } if(value == null) { continue; } if(!publicVariables.TryGetVariableType(publicVariableSymbol, out Type declaredType)) { continue; } if(declaredType.IsInstanceOfType(value)) { continue; } if(declaredType.IsValueType) { publicVariables.TrySetVariableValue(publicVariableSymbol, Activator.CreateInstance(declaredType)); } else { publicVariables.TrySetVariableValue(publicVariableSymbol, null); } } } void ISerializationCallbackReceiver.OnBeforeSerialize() { SerializePublicVariables(); } private void SerializePublicVariables() { byte[] serializedPublicVariablesBytes = SerializationUtility.SerializeValue( publicVariables, publicVariablesSerializationDataFormat, out publicVariablesUnityEngineObjects); serializedPublicVariablesBytesString = Convert.ToBase64String(serializedPublicVariablesBytes); } #endregion #region IUdonBehaviour Interface public bool TryToInterrogateUdon(string eventName, out object returnValue, params (string symbolName, object value)[] programVariables) { return TryToInterrogateUdon(eventName, out returnValue, programVariables); } public bool TryToInterrogateUdon(string eventName, out T returnValue, params (string symbolName, object value)[] programVariables) { if(!_initialized || !enabled || !_hasDoneStart) { #if VRC_CLIENT || UNITY_EDITOR if(UdonManager.Instance.DebugLogging) { Logger.Log($"{gameObject.name} not ready to respond to {eventName}: initialized={_initialized} enabled={enabled} hasStarted={_hasDoneStart}", _debugLevel); } #endif returnValue = default(T); return false; } if(!_eventTable.ContainsKey(eventName)) { #if VRC_CLIENT || UNITY_EDITOR if(UdonManager.Instance.DebugLogging) { Logger.Log($"{gameObject.name} will not respond to {eventName}", _debugLevel); } #endif returnValue = default(T); return false; } if(!RunEvent(eventName, programVariables)) { #if VRC_CLIENT || UNITY_EDITOR if(UdonManager.Instance.DebugLogging) { Logger.LogError($"{gameObject.name} failed to respond to {eventName}", _debugLevel); } #endif returnValue = default(T); return false; } returnValue = GetProgramVariable(ReturnVariableName); return true; } public override bool RunEvent(string eventName, params (string symbolName, object value)[] programVariables) { if(!_isReady) { return false; } if(!_hasDoneStart) { return false; } if(_hasError) { return false; } if(_udonVM == null) { return false; } if(!_eventTable.TryGetValue(eventName, out List entryPoints)) { return false; } //TODO: Replace with a non-boxing interface before exposing to users foreach((string symbolName, object value) in programVariables) { SetEventVariable(eventName, symbolName, value); } foreach(uint entryPoint in entryPoints) { RunProgram(entryPoint); } foreach((string symbolName, object _) in programVariables) { SetProgramVariable(symbolName, null); } return true; } public override void RunInputEvent(string eventName, UdonInputEventArgs args) { if(!_isReady) { return; } if(!_hasDoneStart) { return; } if(!_program.EntryPoints.GetExportedSymbols().Contains(eventName)) { return; } // Set value arg switch(args.eventType) { case UdonInputEventType.AXIS: SetEventVariable(eventName, "floatValue", args.floatValue); break; case UdonInputEventType.BUTTON: SetEventVariable(eventName, "boolValue", args.boolValue); break; } // Set event args SetEventVariable(eventName, "args", args); RunProgram(eventName); } private void SetEventVariable(string eventName, string symbolName, T value) { if(!_symbolNameCache.TryGetValue((eventName, symbolName), out string newSymbolName)) { newSymbolName = $"{eventName.Substring(1)}{char.ToUpper(symbolName.First())}{symbolName.Substring(1)}"; _symbolNameCache.Add((eventName, symbolName), newSymbolName); } SetProgramVariable(newSymbolName, value); } public override void InitializeUdonContent() { if(_initialized) { return; } SetupLogging(); _udonManager = UdonManager.Instance; if(_udonManager == null) { enabled = false; Logger.LogError( $"Could not find the UdonManager; the UdonBehaviour on '{gameObject.name}' will not run.", _debugLevel, this); return; } if(!LoadProgram()) { enabled = false; Logger.Log( $"Could not load the program; the UdonBehaviour on '{gameObject.name}' will not run.", _debugLevel, this); return; } // Let UdonManager apply any processing or scans. _udonManager.ProcessUdonProgram(_program); IUdonSymbolTable symbolTable = _program?.SymbolTable; IUdonHeap heap = _program?.Heap; if(symbolTable == null || heap == null) { enabled = false; Logger.Log( $"Invalid program; the UdonBehaviour on '{gameObject.name}' will not run.", _debugLevel, this); return; } if(!ResolveUdonHeapReferences(symbolTable, heap)) { enabled = false; Logger.Log( $"Failed to resolve a GameObject/Component Reference; the UdonBehaviour on '{gameObject.name}' will not run.", _debugLevel, this); return; } _udonVM = _udonManager.ConstructUdonVM(); if(_udonVM == null) { enabled = false; Logger.LogError( $"No UdonVM; the UdonBehaviour on '{gameObject.name}' will not run.", _debugLevel, this); return; } _udonVM.LoadProgram(_program); ProcessEntryPoints(); #if !VRC_CLIENT _isReady = true; #else if(!_isNetworkingSupported) { _isReady = true; } #endif _initialized = true; RunOnInit(); } [PublicAPI] public void RunOnInit() { if(OnInit == null) { return; } try { OnInit(this, _program); } catch(Exception exception) { enabled = false; Logger.LogError( $"An exception '{exception.Message}' occurred during initialization; the UdonBehaviour on '{gameObject.name}' will not run. Exception:\n{exception}", _debugLevel, this ); } } private void RegisterUpdate() { if(_udonManager == null) { return; } if(!isActiveAndEnabled) { return; } if(_hasUpdateEvent || !_hasDoneStart) { _udonManager.RegisterUdonBehaviourUpdate(this); } if(_hasLateUpdateEvent) { _udonManager.RegisterUdonBehaviourLateUpdate(this); } if(_hasFixedUpdateEvent) { _udonManager.RegisterUdonBehaviourFixedUpdate(this); } if(_hasPostLateUpdateEvent) { _udonManager.RegisterUdonBehaviourPostLateUpdate(this); } foreach(AbstractUdonBehaviourEventProxy proxy in _eventProxies) { proxy.enabled = true; } } private void UnregisterUpdate() { if(_udonManager == null) { return; } if(_hasUpdateEvent) { _udonManager.UnregisterUdonBehaviourUpdate(this); } if(_hasLateUpdateEvent) { _udonManager.UnregisterUdonBehaviourLateUpdate(this); } if(_hasFixedUpdateEvent) { _udonManager.UnregisterUdonBehaviourFixedUpdate(this); } if(_hasPostLateUpdateEvent) { _udonManager.UnregisterUdonBehaviourPostLateUpdate(this); } foreach(AbstractUdonBehaviourEventProxy proxy in _eventProxies) { proxy.enabled = false; } } #region IUdonEventReceiver and IUdonSyncTarget Interface #region IUdonEventReceiver Only public override void SendCustomEvent(string eventName) { RunProgram(eventName); } public override void SendCustomNetworkEvent(NetworkEventTarget target, string eventName) { #if UNITY_EDITOR SendCustomEvent(eventName); #else SendCustomNetworkEventHook?.Invoke(this, target, eventName); #endif } public override void RequestSerialization() { RequestSerializationHook?.Invoke(this); } public override void SendCustomEventDelayedSeconds(string eventName, float delaySeconds, EventTiming eventTiming = EventTiming.Update) { UdonManager.Instance.ScheduleDelayedEvent(this, eventName, delaySeconds, eventTiming); } public override void SendCustomEventDelayedFrames(string eventName, int delayFrames, EventTiming eventTiming = EventTiming.Update) { UdonManager.Instance.ScheduleDelayedEvent(this, eventName, delayFrames, eventTiming); } public override string InteractionText { get => interactText; set => interactText = value; } #endregion #region IUdonSyncTarget public override IUdonSyncMetadataTable SyncMetadataTable => _program?.SyncMetadataTable; #endregion #region Shared public override Type GetProgramVariableType(string symbolName) { if(!_program.SymbolTable.HasAddressForSymbol(symbolName)) { return null; } uint symbolAddress = _program.SymbolTable.GetAddressFromSymbol(symbolName); return _program.Heap.GetHeapVariableType(symbolAddress); } public override void SetProgramVariable(string symbolName, T value) { if(_program == null) { return; } if(!_program.SymbolTable.TryGetAddressFromSymbol(symbolName, out uint symbolAddress)) { return; } SetHeapVariable(symbolAddress, value); } public override void SetProgramVariable(string symbolName, object value) { if(_program == null) { return; } if(!_program.SymbolTable.TryGetAddressFromSymbol(symbolName, out uint symbolAddress)) { return; } SetHeapVariable(symbolAddress, value); } private void SetHeapVariable(uint symbolAddress, T newValue) { if(_variableToChangeEvent.TryGetValue(symbolAddress, out (uint eventAddress, uint oldVariableAddress) data)) { // cache value before changing T value = _program.Heap.GetHeapVariable(symbolAddress); // check for change and trigger event if(!value?.Equals(newValue) ?? newValue != null) { // change the variable on the heap _program.Heap.SetHeapVariable(symbolAddress, newValue); // change the old variable on the heap if(data.oldVariableAddress != uint.MaxValue) { _program.Heap.SetHeapVariable(data.oldVariableAddress, value); } // trigger the event RunProgram(data.eventAddress); } } else { // just change the variable on the heap _program.Heap.SetHeapVariable(symbolAddress, newValue); } } public override T GetProgramVariable(string symbolName) { if(_program == null) { return default; } if(!_program.SymbolTable.TryGetAddressFromSymbol(symbolName, out uint symbolAddress)) { return default; } return _program.Heap.GetHeapVariable(symbolAddress); } public override object GetProgramVariable(string symbolName) { if(_program == null) { return null; } if(!_program.SymbolTable.TryGetAddressFromSymbol(symbolName, out uint symbolAddress)) { #if UNITY_EDITOR Logger.LogError($"Could not find symbol {symbolName}; available: [{string.Join(",", _program.SymbolTable.GetSymbols())}]", _debugLevel); #endif return null; } return _program.Heap.GetHeapVariable(symbolAddress); } public override bool TryGetProgramVariable(string symbolName, out T value) { value = default; if(_program == null) { return false; } if(!_program.SymbolTable.TryGetAddressFromSymbol(symbolName, out uint symbolAddress)) { return false; } return _program.Heap.TryGetHeapVariable(symbolAddress, out value); } public override bool TryGetProgramVariable(string symbolName, out object value) { value = null; if(_program == null) { return false; } if(!_program.SymbolTable.TryGetAddressFromSymbol(symbolName, out uint symbolAddress)) { return false; } return _program.Heap.TryGetHeapVariable(symbolAddress, out value); } #endregion #endregion #endregion #region Logging Methods private void SetupLogging() { _debugLevel = GetType().GetHashCode(); if(Logger.DebugLevelIsDescribed(_debugLevel)) { return; } Logger.DescribeDebugLevel(_debugLevel, "UdonBehaviour"); Logger.AddDebugLevel(_debugLevel); } #endregion #region Manual Initialization Methods [PublicAPI] public void AssignProgramAndVariables(AbstractSerializedUdonProgramAsset compiledAsset, IUdonVariableTable variables) { serializedProgramAsset = compiledAsset; publicVariables = variables; } #endregion } }