Getting Started with UdonPie - the Python-based UDON-assembly compiler

UdonPie is a Python-like way to write logic for your VRChat worlds that compiles down to UDON assembly.

This guide assumes some level of programming skills, so experience with at least some programming language is required (python is optional but recommended).

This guide will walk you through setting up and creating some simple graphs using UdonPie.

Requirements

Minimal

Recommended

Extras

  • An editor of choice: anything will do, but Visual Studio Code will be used as a reference
  • GitHub Desktop for updating UdonPie

I highly recommend going the “Recommended” route, otherwise you might miss out on the latest bugfixes and updates from zz-roba

Installation (minimal)

  1. Create a new Unity Project and import VRCSDK3 and UDONSDK
  2. Unpack the downloaded files and copy them into your project, something like Assets/UdonPieCompiler will work
  3. Extract the udon_classes archive into the root of your project

Installation (full)

  1. Install Python 3.6
  2. Create a new Unity Project and import VRCSDK3 and UDONSDK
  3. Download the UdonPie compiler either directly from the github page using the “Clone or Download” -> “Download ZIP” button, or by using GithubDesktop. Download / unpack directly into your unity project to make compiling easier. Somewhere like Assets/UdonPieCompiler will work.
    image
  4. Run pip install -r requirements.txt where you unpacked the UdonPie
  5. Extract the udon_classes archive into the root of your project

Usage

If you understand how UdonPie coding process works and you want to learn how to make your development experience better, or just wanna discover the coding part by yourself, skip to the Development Experience Tips section below

Now that you have your project going it’s time to write some code! Open the project in your editor of choice and create a new python file, like FirstAssembly.py

Making a Hello World is boring and there are some more abstract examples available on the UdonPie github page, both in the main readme and in the samples folder.

Let’s make something that you can actually use in your final project, like a program that changes a material of an object when it enters a collider!

  • Create a new scene and place a sphere in the middle
  • Attach an Udon Behaviour component to it and leave empty for now

Time to write some code, i’ll explain as we go. In your FirstAssembly.py write this

from udon_classes import * # IGNORE_LINE
SystemVoid = SystemVoid # IGNORE_LINE # pylint: disable=undefined-variable
this_trans = this_trans # IGNORE_LINE # pylint: disable=undefined-variable
this_gameObj = this_gameObj # IGNORE_LINE # pylint: disable=undefined-variable

def _start():
  UnityEngineDebug.Log(SystemObject("First Assembly started!"))

The first line is purely to improve your dev experience. If you’re using an editor that has native Python support you should get autompletion. The following 3 lines just make sure your linter wouldn’t complain about missing global variables.

def _start() is your Start event. Everything that you write in it will be run on program start. Following that logic you can use any event that is present in udon by writing this in camelCase, like _onTriggerEnter and _onDisable.

Now we need to compile this into Udon assembly. Save your file and open your terminal / command prompt in the directory where you have your UdonPieC.exe or the extracted source code.

If using the exe, run:

UdonPieC.exe ../FirstAssembly.py ../FirstAssembly.uasm

If using the source:

python udon_compiler.py ../FirstAssebly.py ../FirstAssembly.uasm

Now you should have a new file in your unity project which you can drag and drop on your sphere, into the Program Source slot

image
image
If you run your scene now - you should get the log in your console!
image

7 Likes

Now let’s make something actually interesting. First, we’ll need to define a public variable for our material and then assign that material when entering the trigger.

  • Add a rigidbody to your sphere (required for triggers to… trigger), uncheck the Use Gravity option and set it to Kinematic
  • Add a new cube to the scene, scale it up a bit, disable the mesh renderer and mark the BoxCollider as Trigger

Now in your python code, above the def _start(): write the following

global material
material = UnityEngineMaterial

In the start method add a line to get and save the renderer component so it looks like this

def _start():
  UnityEngineDebug.Log(SystemObject("First Assembly started!"))
  renderer = UnityEngineMeshRenderer(this.gameObj.GetComponent("MeshRenderer"))

Below the start add the _onTriggerEnter(): function

def _onTriggerEnter(enterCollider: UnityEngineCollider):
  oldMaterial = renderer.get_material()
  renderer.set_material(material)

And then the _onTriggerExit

def _onTriggerExit(exitCollider: UnityEngineCollider):
  renderer.set_material(oldMaterial)

In short, this does the following

  • On start we save the current renderer to a variable since we’ll be modifying its properties down the line
  • On trigger enter we save the old material and assign the new material to our sphere
  • On trigger exit - we revert back

Now we can assign materials and test it out!

  • Create 2 materials, let’s call them normal and active, make them different color
  • Assign normal material to your sphere
  • Add active material to the public variable material on the udon behaviour component

You will need to scroll past the assembly gibberish to get to the Public Variables list. To learn how to get them to the very top and move the Assmebled/Disassembled graph out of the way - check out the Developer Experience Tips Below

With things assign you’re ready to test! Run your scene and move the sphere into the trigger and see how it goes.

Wasn’t that hard, was it! Here’s the whole thing combined for reference

from udon_classes import * # IGNORE_LINE
SystemVoid = SystemVoid # IGNORE_LINE # pylint: disable=undefined-variable
this_trans = this_trans # IGNORE_LINE # pylint: disable=undefined-variable
this_gameObj = this_gameObj # IGNORE_LINE # pylint: disable=undefined-variable

global material

material = UnityEngineMaterial

def _start():
  UnityEngineDebug.Log(SystemObject("First Assembly started!"))
  renderer = UnityEngineMeshRenderer(this_gameObj.GetComponent("MeshRenderer"))

def _onTriggerEnter(enterCollider: UnityEngineCollider):
  oldMaterial = renderer.get_material()
  renderer.set_material(material)

def _onTriggerExit(exitCollider: UnityEngineCollider):
  renderer.set_material(oldMaterial)

Don’t forget to recompile the python program into uasm every time!

This is a very basic example but it touches on msot of the crucial points like:

  • Defining public variables using global keyword and supplying types for them below
  • Hooking into built-in events system
  • Pre-saving needed components on start to not call GetComponent every time.

From here i highly recommend just trying to convert your existing udon graphs to UdonPie, which will be a nice and easy practice as you already have the logic built and you’ll learn all the names and types along the way!

I recommend using UdonExternSearch website to get the list of methods you have at your disposal.

And now to some more hacky stuff that should immensly improve your UdonPie dev experience!

4 Likes

Development Experience Tips

Udon is great and UdonPie is great, but we can always improve things, right? So let’s do this.

Disclaimer: I’m not a C# Developer, so if you feel like what i’m doing is blasphemy, please forgive me and suggest a better solution :slight_smile:
I also do not guarantee this won’t get broken / fixed by the vrc team

Force refresh the assembly (thx to Foorack for the tip!)

  • Open up the UdonBehaviourEditor file and on line 81 make sure it always runs. The simplest way to do it (without modifying too much) is to simply add an or clause to the condition. Somthing like this will work

image

This will technically make the UI redraw all the time, but this is the best way to make sure public variables are always up to date with your assembly.

Public Variables on the top

I’m not sure how much should we share the editor edits, but I personally think as long is it improves the experience and doesnt add anything that is not meant to be accessed - it’s a fair game, so let’s improve those editors!

  • Open up the UdonAssemblyProgramAsset C# file and move the base.RunProgramSourceEditor call on the top of RunProgramSourceEditor function. That way the program variables will be drawn on top instead of being drawn all the way below the assembly code. Very handy!
    image

This will result it in something resembling normal C# component inspectors
image

But we still have a ton of code right below, you say. Let’s change that too!

3 Likes

Collapsing Assembly and Disassembly code

Udon does this for us when using normal graphs, but for some reason direct assembly inspectors are just showing all of the code with no way to hide it, which isn’t too nice. Let’s add a nice folding group so we can have it hidden unless we want to read the assembly directly.

  • Open the UdonAssemblyProgramAsset and add a new variable to track the state of our foldout group. Something like showUdonAssembly should work

image

  • Now implement the group in the RunProgramSourceEditor
        base.RunProgramSourceEditor(publicVariables, ref dirty); // what we moved earlier
        EditorGUI.BeginChangeCheck(); // track the change
        bool newShowDisAssembly = EditorGUILayout.Foldout(showUdonAssembly, "Assembled Graph"); // create the foldout group
        if (EditorGUI.EndChangeCheck())
        {
            Undo.RecordObject(this, "Toggle Assembly Foldout");
            showUdonAssembly = newShowDisAssembly ; // toggle between folded/unfolded
        }

        if (!showUdonAssembly)
        {
            return; // do not draw any udong assemblies when folded
        }

        EditorGUI.indentLevel++; // indent the inspectors so it looks like our stuff is inside the foldout (it's all fake!)
        // now we call the existing methods that were on the top originally
        DrawAssemblyTextArea(!Application.isPlaying, ref dirty);
        DrawAssemblyErrorTextArea();
        EditorGUI.indentLevel--; // unindent to not affect anything below

This shoudl take care of assembly and produce something like this.

J9o5KXodeN

Now we need to hide the disassembly as well, much in the same way.

  • Open the UdonProgramAsset file and add a new variable to track the state of the foldout. Make sure it has a different name than the one you created above! Something like showDisaAssembly will be just fine
    image
  • Now implement the group in the RunProgramSourceEditor. Edit the code inside to look like this. Logic is exactly the same, just the names are different
 if(program != null)
        {
            DrawPublicVariables(publicVariables, ref dirty); // this is what draws our public variables, and since it is the base class - that's why we moved this whole thing above the Assembly call earlier in the guide.
            EditorGUI.BeginChangeCheck();
            bool newShowDisAssembly = EditorGUILayout.Foldout(showDisAssembly, "Disassembled Graph");
            if (EditorGUI.EndChangeCheck())
            {
                Undo.RecordObject(this, "Toggle Assembly Foldout");
                showDisAssembly = newShowDisAssembly;
            }

            if (!showDisAssembly)
            {
                return;
            }

            EditorGUI.indentLevel++;
            DrawProgramDisassembly(); 
            EditorGUI.indentLevel--;
        }

And now you have it all neatly hidden and out of the way :slight_smile:

drJbZuyBkk

5 Likes

Auto-compile UdonPie on save

This is strictly VSCode specific, but i bet there are similar options for any editor.

  • Search for Run on Save extension and download it.
  • Open your settings and add the following JSON to the end of it
"runOnSave.statusMessageTimeout": 3000,
    "runOnSave.commands": [
        {
            "match": ".*\\.py$",
            "command": "python ${workspaceFolder}/UdonPieCompiler/udon_compiler.py ${file} ${fileDirname}\\${fileBasenameNoExtension}.uasm",
            "runIn": "terminal",
            "runningStatusMessage": "Compiling ${fileBasename}",
            "finishStatusMessage": "${fileBasename} compiled"
        },
    ],

This implies using the source version of the compiler and it being in the Assets/UdonPieCompiler folder. Adjust as needed :slight_smile: Running in terminal mode is required for python mode or it would not find the proper working directories. If you’re using UdonPieC.exe you can probably run in backend mode. Try switching runIn variable to “backend” and see how that goes.

With that - on every save you’ll get a fresh version of UASM ready to be used.

Make Enums work

Currently Enums are kinda broken for udon inspectors, and even in direct assembly code, so there is no way to select what tracking target you want to use when grabbing player tracking data. Which is a bit of a bummer because accessing player tracking is one of the coolest features of Udon!

You can, of course, just use plain udon for that and then just grab those variables / reference those objects in UdonPie, but that’s no fun, isn’t it?

I’m really not sure about this code as I’m not a C# developer and the whole typing system is still very much an unknown to me, but this seems to work based on my tests.

  • Open UdonProgramAsset file and scroll all the way to the declaredType.IsEnum line. Here’s what we need to make it look like
          else if(declaredType.IsEnum)
            {
                Enum enumValue = (Enum)value;
                GUI.SetNextControlName("NodeField");
                EditorGUI.BeginChangeCheck();
                dynamic localVar = value; // idk how to better have a flexible type variable here so i just used dynamic
                if (enumValue == null) // by default our enum will be null, and while Udon itself will just default to the first value - the editor won't so we need to do it ourselves
                {
                    localVar = declaredType.GetEnumValues().GetValue(0);
                }
                publicVariable.value = EditorGUILayout.EnumPopup(symbol, (Enum)localVar); // now we draw our enum editor based on this copied local var
                if (EditorGUI.EndChangeCheck())
                {
                    dirty = true;
                }
            }

This should result in the enums working! So if you have code that looks like this

global targetTransform
global trackingTarget

targetTransform = UnityEngineTransform
trackingTarget = VRCSDKBaseVRCPlayerApiTrackingDataType

# ...

def start():
  player = VRCSDKBaseNetworking.get_LocalPlayer()

def _update():
    trackingData = player.GetTrackingData(trackingTarget)
    targetPos = trackingData.get_position()
    targetRot = trackingData.get_rotation()
    logContexted(SystemObject(targetPos))
    targetTransform.set_position(targetPos)
    targetTransform.set_rotation(targetRot)

You should get an editor that looks like this

YxErkQLekH

That example is straight out of my tracker script, btw. Which is a part of a collection of miscelaneous UdonPie scripts i’m currently building

Make VSCode hate your code less

Add this json to your workspace settings (adding to global settings is not recommended)

"python.linting.pylintArgs": [
    "--disable=all",
    "--enable=F,E,unreachable,duplicate-key,unnecessary-semicolon,global-variable-not-assigned,unused-variable,binary-op-exception,bad-format-string,anomalous-backslash-in-string,bad-open-mode"
  ]
4 Likes

Useful Snippets

Also a couple snippets i set up to speed up development (written for vscode). You can just add them to your .vscode/ folder in the project. Something like .vscode/udon.code-snippets should work

Scaffold new UdonPie program

	"Udon Scaffold": {
		"scope": "python",
		"prefix": "uscaffold",
		"body": [
			"from udon_classes import * # IGNORE_LINE",
			"SystemVoid = SystemVoid # IGNORE_LINE # pylint: disable=undefined-variable",
			"this_trans = this_trans # IGNORE_LINE # pylint: disable=undefined-variable",
			"this_gameObj = this_gameObj # IGNORE_LINE # pylint: disable=undefined-variable",
			"",
			"def initGraphVars() -> SystemVoid:"
			"  version = 1"
			"  graphName = \"$1\"",
			"",
			"# LOGGER SCAFFOLD"
			"def logContexted(input: SystemObject) -> SystemVoid:",
			"  UnityEngineDebug.Log(SystemObject(\"[\" + graphName + \"]: \" + SystemConvert.ToString(input)))",
			"",
			"# PROGRAM"
			"def _start():",
			"  initGraphVars()",
			"  logContexted(SystemObject("v " + SystemConvert.ToString(version)))",
			"  "
		]
	},

This allows you to start a fresh file by typing uscaffold and pressing enter, which instantly creates a bunch of builerplate code and focuses the graphName variable. Then youcan press tab to continue to the _start(): code. You can also use logContexted instead of UnityEngineDebug.Log to log things prefixed by the graph name, which i find really useful when you have a lot of stuff running at the same time.

image

Quickly log using contexted log

	"Udon Log": {
		"scope": "python",
		"prefix": "ulog",
		"body": [
			"logContexted(SystemObject($1))"
		]
	},

To improve the experience of using the Contexted Log i made this short snipped. So you can use ulog shortcut to instantly have all the boilerplate inserted.

Log using Unity Log

	"Unity Log": {
		"scope": "python",
		"prefix": "unlog",
		"body": [
			"UnityEngineDebug.Log(SystemObject($1))"
		]
	},

If you do not want to use my logger setup, or want to log non-string stuff, here’s the snippet line for you.

Check if the program is running in editor or in game

Thanks to @foorack for sharing a better way to do this.

	"Check If Editor": {
		"scope": "python",
		"prefix": "isEditor",
		"body": [
			"def isInEditor() -> SystemBoolean:"
			"  return SystemObject(VRCSDKBaseNetworking.get_LocalPlayer()) == None\n"
		]
	}

This allows you to call the isInEditor() to check if you’re running in editor. Altho I would highly recommend saving that to a variable on start, as it won’t change during gameplay.

def _start():
	isEditor = isInEditor()

Deprecated (old) setup, left for posterity

This requires you to have a setup that looks like this

image

  • Add an empty called isEditorCheck to your hierarchy
  • Add an empty child called isEditor and tag it EditorOnly
  • Use this snippet
	"Check If Editor": {
		"scope": "python",
		"prefix": "isEditor",
		"body": [
			"editorCheck = UnityEngineGameObject.Find('isEditorCheck').get_transform()",
			"isEditor = editorCheck.get_childCount() == 1"
			"if not isEditor:",
			"  "
		]
	}

This essentially checks if the isEditor object is present. But since we can’t compare to null in Udon - we have to find workarounds, this is one of the way to do achieve such a thing.

Thanks and Conclusions

And that’s it for now! Huge thanks to zz-roba for making UdonPie possible, don’t hesitate to support them on booth! And Foorack for guiding me through initial issues and contributing to the project.

I tried to stay as brief as possible, but not sure if I succeeded. If you have any questions i’ll try to answer them, and will add a link to the collection of UdonPie scripts when i have them better ogranized and documented

Some things to look forward to are (already built and utilized in the world i’m building):

  • Universal in-game logger with graph-based prefixes and mounted to your hand
  • Animator triggers/boolean togglers
  • FloatToUiText and FloatToUISlider programs as well as a BoolToUIText
  • LerpedFollower
  • WorldOwnerChecker/Enabled
  • HeadTouchHandler

and more!

6 Likes

I love it; nicely done.

4 Likes

As an extension of this guide I started compiling all my programs into one list while also categorizing and documenting them. Alongside some extra guides.

If you’re looking for some real-world examples of UdonPie usage - this might be a good place to check them out :slight_smile:

3 Likes

on Step 5 I am having a little trouble I do not see this “udon_classes” Archive anywhere so not sure what I may be missing the only zip inside the location I see is UdonPie.

Thanks

Konchu