Cleaner approach for taking actions after SetOwner/OnOwnershipTransferred?

There does not seem to be a clean, end-to-end process for instructing a networked object to do some simple task that takes parameters, and have that task reflect in everyone else’s copy of the behavior. I’m looking for recommendations on best practice, and maybe a bit of a suggestion on how to perhaps accomplish it.

The set up/situation:

  • Have a multi-player game world
  • Have a script that is running on the object owner’s computer only, generating “work” around the world. Work is distributed on a first-come-first-served basis. Everyone has a chance to get the work item, but only one person can claim it. When one person claims it, then it goes away for everyone else.
  • The work item has a name and other parameters, which everyone should see the same values for. The number of and types of parameters might be arbitrary.

The current way to do it (help):

This is where I’m a little fuzzy, since I’m new to Udon, but this appears to be the general approach supported by what Udon has now (PLEASE! correct me if I’m wrong or there’s a better way). I can’t use a custom networked event, since I can’t put parameters in it. That won’t late-join sync anyway, either.


[UdonSynced] public string CurrentWorkItemName;
[UdonSynced] public float CurrentWorkItemValue;
private string _intentCreateWorkItemName;
private float _intentCreateWorkItemValue;
private int intentLastTime;

internal WorkItem WorkItem;

private void DoThing() {
  this._intentCreateWorkItemName = "Work Item 12345";
  this._intentCreateWorkItemValue = 9.99f;
  this._intentLastTime = Time.time; //Add a timestamp to prevent random ownership transfers from causing duplicate work
  
  Networking.SetOwner(someGameObject);
}

private void CreateWorkItem(string s, float m) {
   //Pseudo code, instantiation and object props
   var gameObj = InstantiateWorkItem();
   gameObj.GetComponent
   this.WorkItem = theComponent;
   this.WorkItem.Name = s; 
   this.WorkItem.Value = m;
}

public override bool OnOwnershipRequest(VRCPlayerApi requestingPlayer, VRCPlayerApi requestedOwner)
{
	return true;
}

public override void OnOwnershipTransferred(VRCPlayerApi newOwner)
{
   if(newOwner == Networking.LocalPlayer && (this._intentLastTime < (Time.time - 10))) 
   {
      //Figure out what we wanted to do
	  if(this._intentCreateWorkItemName != null) {
	     this.CreateWorkItem(this._intentCreateWorkItemName, this._intentCreateWorkItemValue);
		 this._intentCreateWorkItemName = null;
		 this._intentCreateWorkItemValue = 0;
		 RequestSerialization();
	  }
	  //Other intentions
   }
}

public override void OnDeserialization()
{
	if(this.WorkItem == null && this.CurrentWorkItemName != null) {
		//Pseudocode; Instantiate and GetComponent
		this.WorkItem = new WorkItem(this.CurrentWorkItemName, this.CurrentWorkItemValue);
	} else {
	    Destroy(this.WorkItem.gameObject);
		this.WorkItem = null;
	}
}

A cleaner way (maybe a proposal?):

The purpose of SetOwner seems to be to acquire a “writer” lock on a game object for synchronization purposes. Ensuring we don’t have some race condition or deadlock on a resource like, in my example, a queue of work items.

If I could do more with full-featured C#, I’d probably write a network call like below, which would create some callbacks to deal with accepted or rejected transfer. This would operate essentially atomically and make the code way cleaner and easier to follow.


GameObject theObjectIwantToTakeActionsOn = ...;
SomeUdonBhvr myBehave = theObjectIwantToTakeActionsOn.GetComponent<SomeUdonBhvr>();

[...]

Networking.SetOwner(
     theObjectIwantToTakeActionsOn,
     () => {
          myBehave.CreateWorkItem("Work Item 12345", 9.99f);
          RequestSerialization();
     },
     () => { Debug.Log("Couldn’t get ownership"); }
);

The SetOwner call, in this instance, takes the gameObject to lock, followed by a callback function (closure) that does some logic when the ownership is successfully transferred, and a callback function that does some logic when the ownership is not successfully transferred.

What this accomplishes is it encapsulates the intent I had when I called the asynchronous SetOwner method, and it doesn’t require me to create spurious non-syncd variables in my behavior. I still have to write OnDeserialize and stuff for subordinate players to respond to, but there’s no more “intent” stuff lingering around.

Undocumented behavior:
I’ve seen some scripts do the actions immediately after calling SetOwner, but I think this is a non-guaranteed, undocumented feature. This seems to work, but idk, its spooky.


private void DoThing() {
	Networking.SetOwner(theObjectIwantToTakeActionsOn);
	myBehave.CreateWorkItem("Work Item 12345", 9.99f);
	RequestSerialization();
}