Giter Site home page Giter Site logo

dtrack's Introduction

DTrack

DTrack (short for Dependency Tracker) is a module for Godot that lets you create reactive UI components declaratively. It's based on Vue.js, so if you've used it before, you should feel right at home!

The philosophy of this library is to augment Godot's UI capabilities, not replace it. Thanks to the engine's hierarchical node system, signals, and $nodepath syntax, all the pieces are already there. The only pain point is managing internal component state, which is where this comes in. If you haven't used a modern frontend library and are wondering how it's done these days, read this.

Installation

All you need to do to use this library is either clone or download this repo, then copy DTrack.gd into your project. Doesn't matter which folder you put it in, it'll still work. Optionally, you can copy ui.gd into your project's script_templates folder to cut down on boilerplate.

Tutorial

At its core, DTrack is just a reactive state manager. You declare some data, bind it to node properties, and those properties will update automatically whenever the data changes. Create a Button node, and add a new script to it, using the default Godot template. Add this boilerplate at the top of the script:

var dt := DTrack.new(self)
func _set(property, value):
    dt.set(property, value)
func _get(property):
    return dt.get(property)

On the first line, we create a new DTrack instance and pass the node we're on to it. This lets it call methods in this script. We also bind it to the node's getters and setters, which makes for better ergonomics and allows the node to expose state externally.

Next, let's enter the _ready() method. Set it up like this:

func label():
    return "Pressed " + str(get("number")) + " times"

func _ready():
    dt.begin()

    dt.data({
        "number": 0,
    })
    dt.computed("label")
    dt.bind(self, "text", "label")

    dt.end()

We can see a couple things happening here. We've used data() to declare some internal state: a variable called number with the value 0. computed() declares label(), which we provide outside the function, as a computed property, which means it's a property that should be modified whenever its reactive dependencies change. As seen in the definition, it returns a string that formats number. Finally, bind() binds the computed value label to the button's text field. If you run the scene now, the button should now display "Pressed 0 times".

Why are we putting our code between dt.begin() and dt.end()? Any calls to DTrack made between these two calls will invoke its reactivity recording system. Without them, DTrack wouldn't be able to see that number is a dependency of label, and that label should be updated whenever number changes.

Let's add some interactivity now. Add this to your script:

func handle_button_pressed():
    set("number", get("number") + 1)

func _ready():
    dt.begin()

    # Code from before omitted
    self.connect("pressed", self, "handle_button_pressed")

    dt.end()

If you've been using GDScript for a while, there should be no surprise here. All we're doing is connecting the button's pressed signal and incrementing the number value whenever it's emitted. What might surprise you, though, is when you run this scene now and click the button, the label automatically updates itself, even though we didn't explicity tell label to recalculate itself or the text property to update! That's the power of reactivity, and it can greatly simplify state management in more complex components.

Just to close things off, we'll use the final major tool DTrack gives us.

func print_msg():
    print("The number has been updated!")

func _ready():
    dt.begin()

    # Code from before omitted
    dt.watch(["number"], "print_msg")

    dt.end()

When we tell DTrack to watch a set of values, it'll call the provided function whenever those values update. This is useful for stuff like animation, since we want the node to do something whenever some piece of state changes. For example, you can use Anima to play a transition whenever you change a value.

Other Stuff

Two way binding

Sometimes, you want to both bind a value in DTrack to a node's property, but also have the value update whenever the property updates. For example, if you bind a value called my_text to a LineEdit's text property, updating my_text should change the content of the textbox, but editing the text in-game should also change my_text. For situations like this, DTrack supports two way binding. Most nodes will emit a signal when input properties are changed. If you add the name of this signal to the end of the bind() call, the value will now update whenever the signal is emitted. So, dt.bind($LineEdit, "text", "my_text") will perform one way binding, while dt.bind($LineEdit, "text", "my_text", "text_changed") will perform two way binding. Note that the signal provided must have zero arguments.

Props

The problem with using data() in the _ready() function is, if you instance a reactive component and try to do set("reactive_property", value) on it, it'll give you an error since the property wasn't declared yet. For public facing properties, write the following:

func props():
    return {
        "reactive_property": value,
    }

When initializing DTrack, it'll call this function and register those properties automatically, so even external code can update them.

dtrack's People

Contributors

boxxfish avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.