Deck of Cards (Networked, Udonsharp)

I ended up making a quick funemployed world to play with friends and I figured it might be useful to share how the deck of cards worked.

This example uses prespawned cards, with the full deck of cards being held under the map. So the cards are drawn by moving them out of their spawn. I’m working under the assumption that VRChat syncs transform components only when they are changed, so the amount of network traffic is pretty small until you reset the decks.

The basic idea is that the master client (world instance owner) handles all of the logic relating to the deck, so when someone takes a card they simply make a network request for the owner to move a card. The owner also changes the name of the gameobject to “Free”, or “Deck” depending on it’s status. This can be done locally on the owner as they are the only one with control over the deck, though it means if the owner leaves the new owner would need to reset the cards because they would not have correct deck tracking.

Lets start with a button to request a card. This button will need to make sure it is the owner running it (by sending a networked event to the owner if the local player is not), which will then call the move event on the deck script. Because we cannot pass parameters to events in udon, I also set a public variable on the deck which keeps track of which transform to move the card to.

    using UdonSharp;
    using VRC.SDKBase;
    using VRC.Udon;

    public class GetCardScript : UdonSharpBehaviour
    {
        public UdonBehaviour ControllerBehaviour;
        public int SpawnIndex = 0;

        public override void Interact()
        {
            SendIfOwner();
        }

    	//Ensures Send is only run on the master client
        void SendIfOwner()
        {
            if (Networking.LocalPlayer.isMaster)
            {
                Send();
            }
            else
            {

                this.SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.Owner, "Send");
            }
        }

    	//Actual send event that runs on the owner
        public void Send()
        {
            ControllerBehaviour.SetProgramVariable("TargetSpawnIndex", SpawnIndex);
            ControllerBehaviour.SendCustomEvent("ShuffleAndSpawn");
        }
    }

All that’s left to do is to get a random free card in the deck controller and move it to the location marked by the index the button provided. I also use a basic shuffle function to move around the tracked cards in their array. I did this every time a card was requested bc laziness, but really it should be done on start and reset instead.

    using UdonSharp;
    using UnityEngine;
    using VRC.SDKBase;

    public class DeckControllerScript : UdonSharpBehaviour
    {
        public Transform[] Cards;
        public int TargetSpawnIndex;
        public Transform[] TargetSpawns;

        private Transform tempCard;
        private int tempInt;

        void Shuffle()
        {
            for (int i = 0; i < Cards.Length; i++)
            {
                tempInt = Random.Range(0, Cards.Length);
                tempCard = Cards[tempInt];
                Cards[tempInt] = Cards[i];
                Cards[i] = tempCard;
            }
        }

        public void DeckReset()
        {
            for (int i = 0; i < Cards.Length; i++)
            {
                //Reset the owner because position is synced to owner
                //and picking it up switches owners to others
                Networking.SetOwner(Networking.LocalPlayer, Cards[i].gameObject);
                Cards[i].name = "Deck";
                Cards[i].position = transform.position;
            }
        }

        public void ShuffleAndSpawn()
        {
            Shuffle();
            for (int i = 0; i < Cards.Length; i++)
            {
                if (Cards[i].name != "Free")
                {
                    Cards[i].name = "Free";
                    Cards[i].position = TargetSpawns[TargetSpawnIndex].position;
                    Cards[i].localRotation = TargetSpawns[TargetSpawnIndex].localRotation;
                    break;
                }
            }
        }
    }

The deck reset button is the same as the card request button, but with a different event.

    using UdonSharp;
    using VRC.SDKBase;
    using VRC.Udon;

    public class DeckResetScript : UdonSharpBehaviour
    {
        public UdonBehaviour ControllerBehaviour;


        public override void Interact()
        {
            SendIfOwner();

        }

        void SendIfOwner()
        {
            if (Networking.LocalPlayer.isMaster)
            {
                Send();
            }
            else
            {
                SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.Owner, "Send");
            }
        }

        public void Send()
        {
            ControllerBehaviour.SendCustomEvent("DeckReset");
        }
    }

That’s pretty much it, I also ended up making a editor script to generate the cards gameobjects, though then I took one look at the api for writing udonbehaviour variables and decided to just manually drag them in to the deck controller instead of automating that part.

The only really gotchas that I encountered were events needing to be public to be called despite being in the same udonsharp class, and needing to be the object owner to move it (if you are not it will snap back).

Here’s a bonus graph for syncing flipping cards, you could probably improve performance by using a vertex shader for the flipping (and set a material property block if udon supports that) instead of an animator, but hey:

The funemployed world’s here:https://www.vrchat.com/home/launch?worldId=wrld_5232f86d-f37e-4790-8dc8-664c12411aae