21 releases (9 breaking)
0.10.0 | Nov 14, 2024 |
---|---|
0.8.1 | Nov 2, 2024 |
0.2.1 | Apr 23, 2024 |
#74 in Game dev
501 downloads per month
600KB
4.5K
SLoC
NodeTree
NodeTree is a framework to create large scalable programs and games through a tree of processes. Each process is fully autonomous and is capable of storing its own state or data, and communicating with other processes. These processes are known as Nodes.
⚠️WARNING⚠️
This crate is in early development. Beware of possible bugs or safety violations.
Getting Started!
Simply either run cargo add node_tree
at the terminal directed towards the directory of your project, or add node_tree = X.X
to your cargo.toml
file.
To begin creating a program in Rust that utilizes a NodeTree
, we must first create a root Node
. In order to reduce boilerplate, we will use the included class!
macro to implement the required Dynamic
, NodeAbstract
, and Node
traits.
use node_tree::prelude::*;
class! {
dec NodeA;
// Fields are declared as such:
let given_name: String;
// Fields can have custom attributes.
default let default_field: u8; // Initialized as its default value.
export let saveable_field: String; // Can also be `export default` if the value supports it.
unique let unique_field: *mut c_void; // Value that is not cloned with the node.
// Overrideable system functions are known as hooks and start with `hk`.
/// Constructors are declared via `_init()`. These will automatically generate a
// `new()` function.
hk _init(given_name: String) {} // Fields are initialized by introducing a variable
// of the same name into scope.
/// Runs right before the `ready()` function for a `Node` that was loaded from the disk,
/// when said node is added back to the scene tree.
hk loaded(&mut self) {
// Run set up code here to reinitialize unique or non-export/default fields...
}
/// Runs once the Node is added to the NodeTree.
hk ready(&mut self) {
// To show off how you could add children nodes.
if self.depth() < 3 {
let new_depth: usize = self.depth() + 1;
self.add_child(NodeA::new(format!("{}_Node", new_depth)));
self.add_child(NodeA::new(format!("{}_Node", new_depth)));
self.add_child(NodeA::new(format!("{}_Node", new_depth)));
}
if self.is_root() {
println!("{:?}", self.children());
}
}
/// Runs once per frame. Provides a delta value in seconds between frames.
hk process(&mut self, delta: f32) {
// Example of using the delta value to calculate the current framerate.
println!("{} | {}", self.name(), 1f32 / delta);
// Using the NodePath and TreePointer, you can reference other nodes in the NodeTree from this node.
if self.is_root() {
match self.get_node::<NodeA>(NodePath::from_str("1_Node/2_Node1/3_Node2")).to_option() {
Some(node) => println!("{:?}", node),
None => ()
}
}
// Nodes can be destroyed. When destroyed, their references from the NodeTree are cleaned up as well.
// If the root node is destroyed, then the program automatically exits. (There are other ways to
// terminate the program such as the queue_termination() function on the NodeTree instance).
if self.children().is_empty() {
self.free(); // We test the progressive destruction of nodes from the tip of the tree
// to the base.
}
}
/// Runs once a Node is removed from the NodeTree, whether that is from the program itself terminating or not.
hk terminal(&mut self, reason: TerminationReason) {} // We do not do anything here for this example.
/// Returns this node's process mode.
/// Each process mode controls how the process() function behaves when the NodeTree is paused or not.
/// (The NodeTree can be paused or unpaused with the pause() or unpause() functions respectively.)
hk process_mode(&self) -> ProcessMode {
ProcessMode::Inherit // We will return the default value, which inherits the behaviour from
// the parent node.
}
}
Finally, in order to activate our NodeTree
, we must instance the root Node
and feed it into the NodeTree
constructor.
// ...previous implementations
use node_tree::trees::tree_simple::TreeSimple;
fn main() -> () {
// Create the tree.
let root: NodeA = NodeA::new("Root".to_string());
let tree: Box<TreeSimple> = TreeSimple::new(root, LoggerVerbosity::NoDebug);
// Begin operations on the tree.
while tree.process().is_active() {}
}
Node Scenes
You may also input a NodeScene
when initializing a NodeTree
or adding a child via add_child
:
use node_tree::prelude::*;
let child_scene: NodeScene = scene! {
NodeA("2_Node", 3) { // Arguments can be fed right in the scene! macro.
NodeA("3_Node", 4),
NodeA("3_Node", 5),
NodeA("3_Node", 6) {
NodeA("4_Node", 7),
NodeA("4_Node", 8)
}
}
};
let parent_scene: NodeScene = scene! {
NodeA("1_Node", 2) {
$child_scene, // You can use `$` to reference other scenes as children.
$child_scene,
$child_scene,
}
};
let scene: NodeScene = scene! {
NodeA("Root", 1) {
$parent_scene,
$parent_scene,
$parent_scene,
}
};
// Scenes can also be cloned, stored, and reused.
//
// # Note
// Saved node scenes are stored in .scn files, with a toml format.
let cloned_scene: NodeScene = scene.clone();
cloned_scene.save(Path::new(""), "foo").unwrap(); // Pass the directory and the scene name.
let loaded_scene: NodeScene = NodeScene::load(Path::new("foo.scn")).unwrap();
// A built in hashing function allows for structural integrity of scenes to be checked.
// (`NodeScene` has a custom implementation for `std::hash::Hash`.)
//
// # Note
// This only hashes the tree's layout, note types, and ownership.
// This does not hash or keep any reference to the node's fields.
assert_eq!(scene.structural_hash(), loaded_scene.structural_hash());
Logging
Logging is also supported. Here is an example setup with an output of a warning and a crash. Note that the crash header/footer are customizable, and that the output is actually colored in a real terminal.
use node_tree::prelude::*;
use node_tree::trees::tree_simple::TreeSimple;
class! {
dec NodeA;
hk ready(&mut self) {
if self.depth() == 2 && self.name() == "NodeA1" {
self.post(Log::Warn("Failed to Initialize!"));
}
if self.depth() == 1 && self.name() == "NodeA" {
self.get_node::<NodeA>(NodePath::from_str("Foo/Bar")).unwrap();
}
}
}
fn main() {
let scene: NodeScene = scene! {
NodeA {
NodeA,
NodeA,
NodeA {
NodeA,
NodeA,
NodeA
}
}
};
let mut tree: Box<TreeSimple> = TreeSimple::new(scene, LoggerVerbosity::All);
while !tree.process().has_terminated() {}
}
Signals
Signals are introduced in order to allow for easy communication between various nodes. An example is shown below:
use node_tree::prelude::*;
use node_tree::trees::TreeSimple;
class! {
dec NodeA;
sig on_event(count: u8);
let count: u8 = 0;
hk ready(&mut self) {
let child: Tp<NodeB> = self.get_child(0).unwrap();
connect! { on_event -> child.listener }; // You can also use `~>` which designates a one-shot connection!
}
hk process(&mut self, _delta: f32) {
self.on_event.emit(self.count);
self.count += 1;
}
}
class! {
dec NodeB;
fn listener(&self, count: &u8) {
if *count == 3 {
panic!("This was successful!");
}
}
}
fn main() {
let scene: NodeScene = scene! {
NodeA {
NodeB
}
};
let mut tree: Box<TreeSimple> = TreeSimple::new(scene, LoggerVerbosity::All);
while tree.process().is_active() {}
}
About Cloning
All nodes are expected to implement the Clone
trait since there are a few implementations that depend on it, such as NodeScene
. However, it is possible to mark a field of a node so that it either has a special clone attribute or is uncloneable via provided types by this crate:
use node_tree::prelude::{ Doc, Eoc, Voc }; // All of these types implement Deref & DerefMut!
#[derive(Debug, Clone, Abstract)]
pub struct SpecializedNode {
base: NodeBase,
resets: Doc<YourUniqueTypeHere>, // Grabs the ::default() of your type when cloned!
errors: Eoc<YourUncloneableType>, // Panics when cloned! Good as an assertion.
voids: Voc<YourUnownableType> // Doesn't panic when cloned, but the cloned copy is unusable.
}
Supported Features
glam
- Enables support with glam's (v0.29.*) types when it comes with saving and loading.
Highlights
- 🏗️ An easy abstraction framework for different processes to communicate and interact with each other in a scalable manner. Inspired by Godot!
- ⏯️ The ability to
pause()
andunpause()
theNodeTree
, and fine tune individualNode
behaviours for when a tree is paused/unpaused. - 📡 Various methods to communicate with other nodes, such as
owner()
,parent()
,get_child()
,children()
, andget_node()
, as well as methods to automate the process such as signals. - 🔗 An abstracted smart pointer known as
Tp<T>
andTpDyn
which clones implicitly to reduce syntax noise and allows for low boilerplate. - 📚 A caching system hosted on the
NodeTree
to act as a safe interface to ensureTp<T>
/TpDyn
soundness, and increase performance! - 👪 The ability to manage nodes with
add_child()
andremove_child()
. - 📝 Includes a dynamic logging and error handling system that is deeply integrated with the node framework.
- 🌲 Allows for the direct referencing of the
NodeTree
through a node'sroot()
function. - 📜 Includes functionality to save, load and handle individual node scenes, such as the handy visual macro
scene!
.
Dependencies
~3–9MB
~102K SLoC