Home

Awesome

Fiber

Fiber is a declarative library for creating games in Unity. It is derived and inspired by web libraries such as React and Solid.

Example

<img src="/docs/rotating-cubes-example.gif" />
using System;
using UnityEngine;
using Fiber;
using Fiber.GameObjects;
using Fiber.Suite;
using Signals;

public class RotatingCubesExample : MonoBehaviour
{
    [Serializable]
    public class Materials
    {
        public Material CubeDefault;
        public Material CubeHovered;
    }

    [SerializeField]
    private Materials _materials;

    public class CubeComponent : BaseComponent
    {
        private Vector3 _position;

        public CubeComponent(Vector3 position)
        {
            _position = position;
        }

        public override VirtualBody Render()
        {
            var _ref = new Ref<GameObject>();
            F.CreateUpdateEffect((deltaTime) =>
            {
                _ref.Current.transform.Rotate(new Vector3(25, 25, 25) * deltaTime);
            });

            var isHovered = new Signal<bool>(false);
            var clicked = new Signal<bool>(false);

            return F.GameObject(
                name: "Cube",
                _ref: _ref,
                position: _position,
                localScale: F.CreateComputedSignal((clicked) => clicked ? Vector3.one * 1.5f : Vector3.one, clicked),
                primitiveType: PrimitiveType.Cube,
                children: F.Children(
                    F.GameObjectPointerEvents(
                        onClick: () => { clicked.Value = !clicked.Value; },
                        onPointerEnter: () => { isHovered.Value = true; },
                        onPointerExit: () => { isHovered.Value = false; }
                    ),
                    F.MeshRenderer(
                        material: F.CreateComputedSignal((isHovered) => isHovered ?
                            G<Materials>().CubeHovered : G<Materials>().CubeDefault,
                            isHovered
                        )
                    )
                )
            );
        }
    }

    public class RotatingCubesComponent : BaseComponent
    {
        public override VirtualBody Render()
        {
            return F.GameObjectPointerEventsManager(F.Children(
                new CubeComponent(new Vector3(1.2f, 0, 0)),
                new CubeComponent(new Vector3(-1.2f, 0, 0))
            ));
        }
    }

    void Start()
    {
        var fiber = new FiberSuite(rootGameObject: gameObject, globals: new()
        {
            { typeof(Materials), _materials }
        });
        fiber.Render(new RotatingCubesComponent());
    }
}

Disclaimer: This example is inspired and taken from @react/three-fiber. Since there is a lot of overlap between the projects, but they are operating in different tech stacks, it is interesting to compare how the 2 differ when rendering the same scene.

Installation

Add the package via Unity's package manager using the git url:

https://github.com/unity-atoms/fiber.git?path=/Assets/Fiber

See Unity's docs for more info.

Packages

Reactivity

Fiber is built upon reactivity and the ability to track changes to data.

Signals

It is possible to use Signals and Computed Signals in your game without using Fiber's renderer.

Signals are reactive primitives that wraps a value. It is possible to both retrieve and imperatively set the value of a signal. When a signal is updated, Fiber will only update the parts of the UI that depends on that signal.

Useful built-in signals:

Computed signals

Computed signals are signals that are derived from other signals. When a signal that a computed signal depends on is updated, the computed signal will also be updated. Computed signals are read only.

Useful built-in computed signals:

How signals work

A signal in itself can't be subscribed to directly. Instead, all signals have a dirty flag, called dirty bit. When a signal is updated, the dirty bit is incremented. Underlying primitives and systems (eg. effects or SignalSubscribtionManager) are polling and checking if the dirty bit has changed. For example when a computed signal's value is read, it will check if the dirty bit of any of its dependencies have changed, and if it has it will recompute its value.

Effects

Effects takes one or more signals and calls a function each time a signal is updated. Effects are useful to perform side effects, eg. updating a game object's transform based on a signal. Note that effects are not called immediately when a signal is updated, but instead will be called by Fiber when there is time to do so, which most of the time is in the next frame.

Example of an effect that updates if a game object with a rigidbody is kinematic or not:

public class PhysicsObjectComponent : BaseComponent
{
    BaseSignal<bool> IsKinematicSignal; // Created and set by a parent component
    public PhysicsObjectComponent(BaseSignal<bool> isKinematicSignal)
    {
        IsKinematicSignal = isKinematicSignal;
    }
    public override VirtualBody Render()
    {
        var _ref = new Ref<GameObject>();
        CreateEffect((isKinematic) =>
        {
            _ref.Current.GetComponent<Rigidbody>().isKinematic = isKinematic;
            return null;
        }, IsKinematicSignal, runOnMount: true);
        return F.GameObject(_ref: _ref, createInstance: () =>
        {
            var go = new GameObject();
            go.AddComponent<Rigidbody>();
            return go;
        });
    }
}

Rendering

Rendering is the process of taking virtual nodes (user defined components of built-ins) and create native nodes. Native nodes are objects that wrap native Unity entities, eg. GameObject or VisualElement.

Entry

The entry point for rendering can easiest be defined using Fiber.Suite:

    var fiber = new FiberSuite(rootGameObject: gameObject, defaultPanelSettings: _myDefaultPanelSettings);
    fiber.Render(new MyComponent());

It is possible to define several entries in the same app in order to just Fiber in different smaller parts of your app. This can be useful if you for example want to gradually migrate an existing app to Fiber.

Components

Components are self contained and re-useable pieces of code that defines one part of your app.

All built-in components can be added via the F property, eg. F.GameObject.

User defined

A user defined component uses built in components and other user defined components to define a part of your app. The component can be re-used in other components and in multiple places in your app.

Children

Components can be nested to create a tree and a hierarchy of components. The children of a component are defined by the children prop. The component itself should not care what children it renders, just where they are rendered.

Simple example panel component using the children prop:

public class PanelComponent : BaseComponent
{
    public PanelComponent(List<VirtualNode> children) : base(children) { }

    public override VirtualBody Render()
    {
        return F.View(
            style: new Style(marginRight: 10, marginBottom: 10, marginLeft: 10, marginTop: 10, backgroundColor: Color.magenta),
            children: children
        );
    }
}

Example of using the above component adding different children to each instance of the panel:

public class MyPageComponent : BaseComponent
{
    public override VirtualBody Render()
    {
        return F.Fragment(
            F.Children(
                new PanelComponent(F.Children(F.Button(text: "Button 1", onClick: (e) => { Debug.Log("Button 1 clicked"); }))),
                new PanelComponent(F.Children(
                    F.Button(text: "Button 2", onClick: (e) => { Debug.Log("Button 2 clicked"); }),
                    F.Button(text: "Button 3", onClick: (e) => { Debug.Log("Button 3 clicked"); })
                )),
                new PanelComponent(F.Children(F.Button(text: "Button 4", onClick: (e) => { Debug.Log("Button 4 clicked"); })))
            )
        );
    }
}
Fragment

A Fragment is a component does not render anything itself, but instead renders its children directly. This is useful when you want to return multiple components from a component, eg. when you want to return a list of components from a component.

    F.Fragment(children);
Context

Context is useful to pass values down the component tree without having to pass it down as props. A context can be defined like this:

    var intSignal = new Signal<int>(5);
    var myContext = new MyContext(intSignal);
    F.ContextProvider<MyContext>(value: myContext, children: children);

The above context can be accessed in any child component like this:

    var myContext = GetContext<MyContext>();
    // Alternatively the shorthand can be used:
    var myContext = C<MyContext>();
Globals

Globals are references that are injected from the outside and can be accessed from any component. Globals are useful to pass down references to services or other objects that are not part of the component tree.

Globals are injected when creating a FiberSuite instance:

    var myService = new MyService();
    new FiberSuite(
        rootGameObject: gameObject,
        globals: new()
        {
            {typeof(MyService), myService},
        }
    );

The above global can be accessed in any child component like this:

    var myService = GetGlobal<MyService>();
    // Alternatively the shorthand can be used:
    var myService = G<MyService>();

Built-ins - Fiber

ContextProvider

Built-ins - Fiber.GameObjects

GameObjectComponent

Component that renders a game object.

    F.GameObject(name: "MyGameObject", children: children);

Built-ins - Fiber.UIElements

UIDocumentComponent

Component that renders a game object with a UIDocument component.

    F.UIDocument(children: children);
ViewComponent

Component that renders a VirtualElement.

    F.View(children: children);
ButtonComponent

Component that renders a Button.

    F.Button(style: new Style(color: Color.black, fontSize: 20), text: "Click me", onClick: (e) => { Debug.Log("Clicked!"); });
TextComponent

Component that renders a TextElement.

    F.Text(style: new Style(color: Color.black, fontSize: 20), text: "Hello world!");
TextFieldComponent

Component that renders a TextField.

    var textFieldSignal = new Signal<string>("Foo");
    F.TextField(value: textFieldSignal, onChange: (e) => { textSignal.Value = e.newValue; });
ScrollViewComponent

Component that renders a ScrollView.

    F.ScrollView(children: F.Children(
        F.View(className: F.ClassName("tall-container"))
    ));

Control flow

Control flow components are built-in components that will efficiently alter what is rendered based on state.

Enable

This component enables or disables underlying nodes and their effects to react to signal updates.

    var enableSignal = new Signal<bool>(true);
    F.Enable(when: showSignal, children: F.Children(F.Text(text: "Hello world!")));
Visible

This component makes underlying native nodes visible or hidden.

    var visibleSignal = new Signal<bool>(true);
    F.Visible(when: visibleSignal, children: F.Children(F.Text(text: "Hello world!")));
Active

This component is a composition of the Enable and Visible components above.

    var activeSignal = new Signal<bool>(true);
    F.Active(when: activeSignal, children: F.Children(F.Text(text: "Hello world!")));
Mount

This component renders and mounts a component based on a signal value.

NOTE: Compareable to solidjs's Show component.

    var showSignal = new Signal<bool>(true);
    F.Mount(when: showSignal, children: F.Children(F.Text(text: "Hello world!")));
For

Renders a list of components based on a signal list. Each item in the list needs a key, which uniquely indentifies an item.

var todoItemsSignal = new ShallowSignalList<TodoItem>(new ShallowSignalList<TodoItem>());
For<TodoItem, ShallowSignalList<TodoItem>, ShallowSignalList<TodoItem>, int>(
    each: todoItemsSignal,
    children: (item, i) =>
    {
        return (item.Id, F.Text(text: item.Text));
    }
);

Architecture

The following sections describes how Fiber works under the hood.

Virtual tree

In its essence, Fiber is building and maintaining a tree structure of nodes, which represents what currently is present in your scene. The tree is made up of so called Fiber nodes, which holds information about its parent, child and direct sibling. This info makes it easy to iterate the tree. The Fiber node can also hold a reference to a native node, which is a node wrapping a native object, such as a GameObject or a VisualElement. It also holds a reference to a virtual node, which is the underying component that was used to create the Fiber node.

Work loop

Fiber has a work loop that runs every frame. The work loop prioritize and performs some units of work:

There is a time budget for the work loop (which is configureable). If the time budget is exceeded, the work loop will yield and continue the next frame.

Node phases

A Fiber node is during its lifespan in different phases. Phases are chronlogical to the order of definitionm which means that Fiber nodes never can go back to a previous phase. The phases are: