Creating your own nodes
Extending Map Graph's by writing your own node classes is easy. There are some important considerations you need to take note of though.
Here's an example of a class for a custom node:
using System;
using System.Collections.Generic;
using InsaneScatterbrain.ScriptGraph;
using UnityEngine;
// The ScriptNode attribute makes sure that the node gets added to the context menu, so you can actually
// add them to your graphs.
// The serializable attribute is necessary, to be able to store the node in the map graph asset file.
[ScriptNode("Add Two Ints", "Math"), Serializable]
public class AddTwoIntsNode : ProcessorNode
{
// By adding the InPort attribute, a port will automatically be created and assigned to this field.
// In this case, it will be a port named "Int A", accepting an integer as input.
// By passing "true" as a third parameter, the port will require a connection for the graph to run.
// Using SerializeReference here is necessary to make sure that the ports and their connections get persisted.
[InPort("Int A", typeof(int), true), SerializeReference]
private InPort intAIn = null; // Initializing it to null is merely done to avoid the compiler from showing the "never used" warning.
[InPort("Int B", typeof(int)), SerializeReference]
private InPort intBIn = null;
/// Adding the OutPort attribute works the same as InPort.
[OutPort("Total", typeof(int)), SerializeReference]
private OutPort sumOut = null;
protected override void OnProcess()
{
// This is the method that is called when the node is asked to perform its task.
// First we pull the values from our input ports.
var intA = intAIn.Get<int>();
var intB = intBIn.Get<int>();
// Then we do the work.
var total = intA + intB;
// And we assign the values we want to output to the output ports.
sumOut.Set(() => total);
}
}
If you want to see some more complex examples, you can just take a look at the code of some of the existing nodes.
Important considerations!
There are some important things you need to take note of whenever you create your own nodes.
Coordinate system
Map Graph uses a coordinate system where the origin is in the bottom left corner of the map.
Be careful when deleting or renaming your nodes and their namespaces
Map Graph uses the SerializeReference attribute to store its nodes and ports. This attribute is a relatively new addition to Unity and while it is very useful, it may or may not cause issues, depending on which version of Unity you're using. This section describes how to work around these issues.
You might run into issues when node classes are deleted, renamed or moved to another namespaces, if these nodes are present in any of your project's graphs.
The issue is described in the Unity Issue Tracker here: https://issuetracker.unity3d.com/issues/serializereference-serialized-reference-data-lost-when-the-class-name-is-refactored
If a node class is deleted, renamed or moved to a different namespace, Unity throws an Unknown managed type referenced exception and will fail to load the graph.
There are some ways to deal with this issue.
Deleting a node class
If you want to delete a node class, make sure you've removed all of the nodes of that type from all of your project's graphs, before doing so.
Renaming a node class or its namespace
When renaming a node class or its namespace, there are two options.
1. Just leave it
The easiest way to deal with this issue, is to just completely avoid renaming a class or moving it to a different namespace, after you've started using it in your graphs.
If you want to give it a different name in the Map Graph editor, you can do so in the ScriptNode attribute.
2. Use the MovedFrom attribute
If you do want to rename a node's classname or move it to another namespace, you can use the MovedFrom attribute as described in the Unity Issue Tracker.
For example, if you've renamed your class from ExampleNode to RenamedExampleNode you can fix it like this:
...
using UnityEngine.Scripting.APIUpdating;
[MovedFrom(false, null, null, "ExampleNode")]
public class RenamedExampleNode : ProcessorNode
{
...
If you've moved the class to a different namespace:
[MovedFrom(false, "Nodes.OriginalNamespace", null, null)]
If you've renamed it and moved it to a different namespace:
[MovedFrom(false, "Nodes.OriginalNamespace", null, "ExampleNode")]
Once you've recompiled your project with the attribute, the error should be gone and your graphs should work again. At this point you can safely remove the MovedFrom attribute.
Make sure your node class and its ports have the right attributes
Nodes need to have the ScriptNode and Serializable attribute.
If you don't add the ScriptNode attribute it won't show up in the context menu and you won't be able to use it in your graphs. If you don't add the Serializable attribute, data won't persist, meaning its ports and their connections.
Ports need to have the SerializeReference attribute (not to be confused with the SerializeField attribute) assigned to them, for the same reason nodes need the Serializable attribute. SerializeField won't work, as it won't persist the references.
Making copies of reference types
If you pull a reference type from an input port and you want to make changes to that object, you'll need to make a copy of it first, and apply your changes on that copy instead. If you don't make a copy, other input ports that pull data from the same output port, will be presented with the changed version, which will lead to unexpected results.
Ensuring consistent output with a static seed
In the likely event that you need to generate random numbers, normally, you'd probably use either the static methods on Unity's Random class or you'd use an instance of .NET's Random class. However, in Map Graph's nodes you should avoid doing so, because it won't guarantee a consistent output when using a static seed.
Instead, you'll want to get and use the Rng object, like so:
var rng = Get<Rng>();
var randomNumber = rng.Next(0, 100);
Please note: There are two Rng classes, InsaneScatterbrain.Services.Rng and InsaneScatterbrain.RandomNumberGeneration.Rng. You should use the former.
Async support
It's possible for Map Graph to run graphs asynchronously on a separate thread.
In order for your node to support this, you need to make sure that any of your node's code that needs to run on the main thread, such as most Unity API calls, does so.
Implementing OnProcessMainThread or OnProcessMainThreadCoroutine
You can do this by overriding either the OnProcessMainThread or OnProcessMainThreadCoroutine method on your node. The difference between the two being, that the former will be run in a single frame, whereas the latter will be run as a coroutine, allowing to spread execution of the node over multiple frames. Because of this OnProcessMainThreadCoroutine does tend to take longer to complete, but can further reduce any stuttering or freezing.
When executing OnProcessMainThreadCoroutine, Map Graph will attempt to execute it in chunks of time equal to the time set as in "Target Time Per Frame" setting on the graph runner component, assuming "Enable Main Thread Time Per Frame Limit" has been enabled.
You can implement either of these methods along side the normal OnProcess (which will not run on the main thread), which will be called right before the OnProcessMainThread or OnProcessMainThreadCoroutine will be run.
Implementing OnProcessMainThread
It's as simple as overriding the OnProcessMainThread inside of your custom node and put your logic inside of it.
protected override void OnProcessMainThread()
{
// Make your Unity API calls here.
}
Implementing OnProcessMainThreadCoroutine
Implementing OnProcessMainThreadCoroutine is almost as easy, but it needs to provide some feedback about when
is a valid moment to pause execution. You can do so by doing a yield return null;
.
In a standard coroutine doing a yield return null;
will stop execution until the next frame. This is not
necessarily the case here.
In this case, these are basically the points to check whether the execution has exceeded the allotted time for this frame. If it has, execution is paused and continued in the next frame, if it's not we keep going either until it has exceeded the allotted time or until the method execution is complete.
For example, let's say you want to loop through the colors of some texture data and modify them in some way. A
good place to do a yield return null;
might be after each iteration of that loop.
protected override IEnumerator OnProcessMainThreadCoroutine()
{
// Do some prepare stuff, here like reading the texture colors
for (var i = 0; i < textureData.ColorCount; ++i)
{
// Get the color and do some stuff to it.
// If the allotted time has execeeded, this is where we stop now and continue next frame.
yield return null;
}
}
Which one should you use?
Obviously, you'll only have to keep this if you need async support. If you're only going to run your graphs synchronously (on the main thread). You don't need to do any of this and you can call the Unity API whenever you want. Just be aware that if you choose to run your graph asynchronously later on, there will be errors and it will fail.
If the node's execution time isn't likely to exceed your acceptable frame time or if you aren't able to cut execution up into smaller pieces, you'll want to implement OnProcessMainThread.
If the node's execution time is long enough that it might start to cause freezing or stuttering and it's possible to cut up the execution of the code into smaller parts, OnProcessMainThreadCoroutine might be the better choice.
It can also be useful to override both methods. In this case, Map Graph will automatically pick the appropriate one to execute, based on whether "Enable Main Thread Time Per Frame Limit" has been enabled or not. This can be useful if in some situations you want to enable the time per frame limit and in some you don't and the most efficient solution for either situations is different.
For example, the Texture To Tilemap node has both methods implemented, where the OnProcessMainThreadCoroutine method assigns tiles using the SetTile method and the OnProcessMainThread uses the SetTilesBlock method. The latter method is faster, but can't be used when cutting the assigning of tiles up into chunks.
Using MainThreadCommands
The aforementioned methods are build on top of the main thread commands system. Instead of using these methods, you can make your own main thread commands, like so:
// You might want to store the command somewhere so that it can be reused.
var command = new MainThreadCommand(MethodToExecuteOnMainThread);
MainThread.Execute(command);
Or the coroutine variant:
// You might want to store the command somewhere so that it can be reused.
var command = new MainThreadCoroutineCommand(MethodToExecuteAsACoroutine());
MainThread.Execute(command);
When should you use this over the OnProcessMainThread and OnProcessMainThreadCoroutine methods?
You probably shouldn't.
You might think that if you have a node where it's a mix of code that must run on the main thread and code that could run on another thread, it might be a good idea to run all the main thread parts in their own MainThreadCommands. However that's more likely to negatively impact your performance than to improve it. This is because the overhead of multiple main thread calls are likely to have a bigger impact on your node's execution time than what you gain from having more of the code run on a separate thread. Therefore, sticking to a single MainThread call will probably yield the best results.
For example, it's better to do something like this:
MainThread.Execute(() => {
for (var i = 0; i < 1000; ++i)
{
NonUnityApiCall();
UnityApiCall();
}
});
Then something like this:
for (var i = 0; i < 1000; ++i)
{
NonUnityApiCall();
MainThread.Execute(UnityApiCall);
}
The latter might seem more efficient at first glance, because you'll only execute the Unity API dependant code on the main thread, but the additional overhead for making many MainThread.Execute isn't worth it.
One way in which MainThreadCommands can save you some time though, is if you have a piece of code that needs to run on the main thread, that the rest of graph doesn't depend on.
In which case, you can just send the command the MainThread without waiting for the result.
This is a pretty niche use case though, as it's basically executed completely outside of the flow of the current graph execution and cannot do anything that the current node (or any nodes afterwards) depend on reliably.
// Passing "false" tells the Main Thread system not to wait for completion before moving on.
MainThread.Execute(processingCompletedCommand, false);
How this works and caveats
Whenever a main thread command (through the overridable methods or otherwise) is executed, the execution on the separate thread is halted, until the main thread has time to execute the code inside the command.
Obviously, this can increase the execution time considerably, as execution is halted until the main thread has done its job each time you do this. For this reason, you should keep the MainThread.Execute calls (and therefore calls to the Unity API) to a minimum.
There are are some simple things you can do to make your node classes less dependent on the Unity API.
For example, for Map Graph's own nodes, Texture2D objects get converted to TextureData objects, so changes can be made to that data instead, without having to call the Unity API each time. These objects need to be converted back to Texture2D objects at some point and that's where you need the Unity API, but only in that single place.