Toon proxy builder, part one

The scene builder builds the Viewports while the proxy builder creates duplicates controlled with RemoteControl nodes. We call these the proxies.

In this chapter, we’ll make the proxy builder to create proxies and remote transforms.

Building the proxy builder

Add a MeshInstance to the scene and assign it a sphere. We use that as a guinea pig to develop the add on with.

Add a new Node to the mesh and assign it a script. Save it in addons/gdquest.toon-controller/Tools/ToonProxyBuilder.gd.

It’s an editor script so needs the tool keyword. We give it a class so Godot adds it to the Add Node dialog. Since it’s inside the addons folder, Godot will list it when the plugin is activate but not before.

tool
class_name ToonProxyBuilder
extends Node

Data

When assigned to lights, the proxy keeps track of what kind of light the parent is (key, fill or kick) using an enum. We add a setter since it has to change data on the proxy lights.

It stores whether a light should emit shadows.

enum LightRole { KEY, FILL, KICK }

export (LightRole) var light_role := 0 setget _set_light_role
export var emits_shadows := false setget _set_emits_shadows


func _set_light_role(value: int) -> void:
    pass


func _set_emits_shadows(value: bool) -> void:
    pass

It also holds a reference to the proxies and RemoteTransform nodes it creates.

var light_proxy: Node
var specular_proxy: Node

var light_remote: RemoteTransform
var specular_remote: RemoteTransform

Initialization

It first finds the scene root and the ToonSceneBuilder, as it depends on it to find viewports and data.

onready var scene_root: Node = (
    get_tree().edited_scene_root
    if Engine.editor_hint
    else get_tree().root
)
onready var builder: ToonSceneBuilder = scene_root.find_node("ToonSceneBuilder")


func _ready():
    if not builder:
        return

It first grabs its parent, and uses its name to find its proxies on the light and specular viewports. Note the use of the optional boolean in find_node. The first defaults to true and uses a recursive search, but the second is false because the viewports do not own the proxies (the scene root does).

var parent := get_parent()

light_proxy = builder.light_data.find_node(parent.name, true, false)
specular_proxy = builder.specular_data.find_node(parent.name, true, false)

If they’re missing and we’re in the editor, we build them. We create a function _build_missing_proxies which takes the parent as its argument.

func _build_missing_proxies(parent: Node) -> void:
    var light_missing: bool = light_proxy == null
    var specular_missing: bool = specular_proxy == null

If either of them are missing, we remove the proxy from the object. Otherwise, it would get duplicated and the proxy code would run again until Godot either crashes or detects an error! We add it again once we’re done.

Note the call to yield before we start messing around with hierarchy. When you add the node directly from the Add Child Node dialog, the hierarchy is already established and there’s no issue. But when loading the scene, Godot is busy setting children up and it would throw errors. We wait a frame for it to finish before proceeding.

yield(get_tree(), "idle_frame")

if light_missing or specular_missing:
    parent.remove_child(self)

When one of the proxies is missing, we need to build and configure it. For that, we build yet another function _build_remote_duplicates which takes the parent and the viewport. It returns a duplicate node and a RemoteTransform inside a Dictionary.

func _build_remote_duplicates(parent: Node, type: int) -> Dictionary:
    var proxy: Node = parent.duplicate()
    var proxy_remote := RemoteTransform.new()

    parent.add_child(proxy_remote)
    proxy_remote.owner = scene_root

We name the remote and add the proxy as a child to the correct viewport based on the data type we specify. We also set the initial color of any light to red since the default is the key light. The setter is what changes it to fill or kick.

match type:
    ToonSceneBuilder.DataType.LIGHT:
        builder.light_data.add_child(proxy)
        if proxy is Light:
            proxy.light_color = Color.red
        proxy_remote.name = "LightRemote"
    ToonSceneBuilder.DataType.SPECULAR:
        builder.specular_data.add_child(proxy)
        proxy_remote.name = "SpecularRemote"

proxy.owner = scene_root

We set the remote’s remote_path, and then return the proxy and the remote in a dictionary.

proxy_remote.remote_path = "../%s" % parent.get_path_to(proxy)

return {"proxy": proxy, "proxy_remote": proxy_remote}

Back in _build_missing_proxies, we call this new function for the missing light and specular, and extract the information into the proxy and remote variables. If they’re not missing, we just grab the remote by name.

if light_missing:
    var result := _build_remote_duplicates(
        parent, ToonSceneBuilder.DataType.LIGHT
    )

    light_proxy = result.proxy
    light_remote = result.proxy_remote
else:
    light_remote = parent.find_node("LightRemote", true, false)

if specular_missing:
    var result := _build_remote_duplicates(
        parent, ToonSceneBuilder.DataType.SPECULAR
    )

    specular_proxy = result.proxy
    specular_remote = result.proxy_remote
else:
    specular_remote = parent.find_node("SpecularRemote", true, false)

If the light or specular were missing, we re-add the builder to the parent.

if light_missing or specular_missing:
    parent.add_child(self)
    owner = scene_root

Now, go back to _ready to finally call the _build_missing_proxies function.

if Engine.editor_hint:
    _build_missing_proxies(parent)