Conrod - New and Improved API

Following last month’s introductory post to Conrod there has been some excellent feedback via HN, reddit and IRC!

One of the most promising and repeatedly occurring suggestions was to switch the API over to a “builder pattern”. So we did!

Old Style

button::draw(&mut gl, // The OpenGL instance used to draw the GUI.
             &mut ui_context, // A user interface context keeps track of state.
             unique_id, // Each widget needs it's own UI_ID.
             Point::new(x, y), // Screen location.
             width, // Rectangle width f64.
             height, // Rectangle height f64.
             Frame(frame_width, frame_color), // Or perhaps `NoFrame` if you don't want one.
             Color::new(r, g, b, a), // The color of the rectangle.
             Label("PRESS", font_size, font_color), // Here you can pass Label(...) or NoLabel.
             || {
    // Callback closure - Do your things here!
});

New Style with “Builder Pattern”

uic.button(unique_id)
    .dimensions(width, height)
    .position(x, y)
    .rgba(r, g, b, a)
    .frame(frame_width, frame_color)
    .label("PRESS", font_size, font_color)
    .callback(|| /* do stuff */)
    .draw(gl);

Benefits of the builder pattern include:

  • All methods are optional apart from .draw(..). The old style was much more verbose due to the lack of default arguments.
  • All methods apart from .draw(..) can be called in any order which saves trying to remember arg ordering.
  • Once themes and positioning-helper-methods are implemented, you could do something like this uic.set_theme("awesome_theme") in your load/setup, and then widgets could look more like this:
uic.button(uiid)
    .down(padding)
    .callback(|| /* do_stuff */)
    .draw(gl);
  • Removes the need for the old enums that were necessary to handle defaults, etc.
  • the Callable trait (which offers the callback method) is generic. If my impression of associated types is correct, this means that handling different kinds of callbacks could be done statically rather than doing them dynamically with a Callback enum which we were previously considering.
  • this pattern is much more consistent with the rust-graphics api (on which Conrod depends), meaning that users hanging around the Piston ecosystem will move between APIs more easily.

Still to come

  • Themes!
  • Positioning methods i.e. .down(padding), right_from(other_uiid, padding), etc.
  • More widgets.
  • Optimisation and faster performance.
  • Lots more

We could use help on all of these things! Feel free to drop by the github issues and pitch in or add your own ideas :)

https://github.com/PistonDevelopers/conrod

mitchmindtree

Rust-Event: The new Piston library for event logic

Piston is a Rust game engine developed by the open source organization PistonDevelopers.

http://www.piston.rs/

Some profiled projects are:

We are now working on another project that we call “Rust-Event”.

https://github.com/PistonDevelopers/rust-event

Rust-Event has been in experimental phase for several months, now it is time to test out the ideas for real. It is still early to say if this is the right direction, so you should consider it very unstable.

Event Threading

Traditionally, event programming has been dealt with using event handlers (similar to observer pattern or signal/slot systems).

To illustrate how event threading differs from traditional event programming, here is some pseudo code:

Event handler

fn button_click(app, args) {
    // click
}

event_center.add_handler(button.click(), button_click);
event_center.update(app);

Event threading

for e in event_iter {
   if button::draw(..., || { /* click */ });
}

Notice that event threading looks more low level, because it handles all the events through the same loop. This is how all low level window APIs deal with events.

Event threading has the down side that all interaction between widgets, that are not window events, must be performed manually. You can not “connect” them with a signal/slot system, even though you might design a system for such a purpose. However, in an immediate mode GUI the widget exists as function calls, so you have to solve this problem anyway. By combining event threading and immediate mode you get a more specialized structure for events instead of a general pattern.

On the upside you don’t loose the stack context at each event, which means you can shift from one stack environment to another without having to build a new application structure. For example, if you have two different scenes, you can write them as two functions, instead of writing an application structure that supports “scenes” in general. Experienced GUI programmers notice that event handlers are very buggy when you try to change the overall behavior of the application from one state to another. This is unfortunate because many applications require special modes, such as when connecting, rendering or loading in new data.

Another benefit with event threading is that you don’t need to connect each signal to each slot. If there is a new type of event, for example multitouch, you only need to update the widget library. In Conrod, all the input received from the window is passed down to widgets which then decide what input they respond to. This means you write 1 or 2 lines of code to create a new widget and not hooking up 3-4 different event in a GUI editor. You can also write simple controllers with this technique, for example a first person 3D camera.

AI Behavior Trees

While event threading is good enough for widgets and simple controllers, it is not good enough to express more complex behavior. For example, if you have an animated character, it needs to respond differently to events depending on the state of the character, such as jumping, sliding or fighting. What if you want do tune the behavior a little for particular scene? Then you need to go back and extend the character code to support the new behavior. It is even harder for game AI, where each character might do planning in parallel, with different sets of goals. Still, you are interacting with these objects in the same way as for widgets, through keyboard press, mouse button clicks etc.

In a component/entity system you create one system for each behavior and turn on/off the components of an entity when they want that specific behavior. One problem with this design is that you still have to express the logic of turning on/off components. The result is often not reusable across projects. While a component/entity system is very powerful to organize application data, it does not solve the problem of how to express behavior.

An AI behavior tree is a data structure that describes a behavior, often through a high level language that makes it understandable, because it hides the details of how it is executed. For example, if you are programming a robot, then you could use the Sequence node to describe commands such as “move forward 3 m”, “turn right”, “wait 3 seconds” etc. You could use another If node to pick actions depending on some condition. Very soon you discover this looks like normal programming. So why not use a normal programming language? The problem is that a robot might run out of batteries, it might face an obstacle, or somebody wants to turn it off safely. All these actions are “behaviors” in the sense they describe something in isolation, but put together they influence each other. Therefore, all behaviors must deal with failure and delay as an integrated part of the logic.

A problem with behavior trees is that you can not describe parallel events with side effects perfectly, because when you look at two behaviors in isolation they might not behave exactly how you expected when putting them together. For example, if an event A fails after B fails in WhenAll(A, B), the behavior fails at time A even if A logically happens after B, but because A was evaluated before B. This is because you don’t know who succeeds first before they get evaluated, at which point it no longer matters because the side effects already happen! The way we solve this in Rust-Event is that behavior is expected to be correct down to the delta time update, but order of evaluation might determine behavior for shorter intervals. It is basically the same problem as for physics collision where character A gets to the door before B because A was evaluated first. For applications where you just want to describe the overall behavior and have a way to recover from failures, this is good enough.

Behavior trees are not only useful to describe AI logic, but also for simpler situations. For example, if you want a behavior that succeeds if the user is pressing A, holding for 1 second and then releasing A, it will look like this (a bit simplified for reading purposes):

Sequence(Pressed(A), After(Wait(1.0), Released(A)))

If you wrote it another way, that looks almost the same, you would get a different behavior:

Sequence(Pressed(A), Wait(1.0), Released(A))

This is different because while you wait for 1 second, the user might release the A button and the last part in the sequence never terminates.

Programming with behavior trees is almost like learning a new programming language. It requires you to think differently from normal programming. In particular, the part where failure and delay is baked into the logic, will take some time to grasp.

Actions

When you describe a behavior, you can define an action which when encountered, calls a closure. The action is generic, so you can define as many complex actions as you like as long it implements the Clone trait.

Here is an example that increases or decreases a counter:

let mut counter: u32 = 0;
let seq = Sequence(vec![Wait(1.0), Action(Inc), Wait(1.0), Action(Dec)]);
let mut state = State::new(seq);
for e in event_iter {
     state.update(&e, |dt, action| {
          match *action {
               Inc => { counter += 1; (event::Success, dt) },
               Dec => { counter -= 1; (event::Success, dt) },
          }
     });
}

If an action takes time, it can return (event::Running, 0.0) to tell that it will run for the whole update. No matter how complex behavior you define, you only deal with the actions. You can use the same update logic on all behaviors that have actions of same type.

With this design we hope to achieve more reuse of code across projects, independently of the environment it is executed.

Conrod - A 100% Rust GUI Library

Conrod is a super-young, “immediate-mode”, graphical user interface library written entirely in Rust! Before I bore you with the details, here’s a demonstration of it in action.

click click clickity click…

The Rust Story

About two or three months ago I landed on the rust-lang.org home page after following a well-hyped HN link. Just so you know, I’m currently in the middle of developing a large real-time interactive generative music system and there’s no way in the world I’d have time to go around willy-nilly checking out new programming languages! No. Way. But then I read the headline…

“Rust is a systems programming language that runs blazingly fast, prevents almost all crashes*, and eliminates data races.”

So here I am about two or three months later - 95% through porting all of my slave-laboured C++ code to Rust, and it remains one of the best decisions I can remember making! (it should be noted that none of my laundry has been eaten… yet).

The Conrod GUI

Conrod itself is only about two weeks old, so go easy! It still requires lots of refinement and optimisation, however I think it’s showing a good amount of promise. This is also the first time I’ve had a go at “immediate-mode” UI - to be honest when I first heard of the idea I thought it sounded quite rubbish…

“How on earth would you store complex widget state without having any objects!?”

and

“Surely it would have to be so slow, having to create the widget every frame!?”.

Only after I started to have a play with the idea did I realise how well the “immediate-mode” approach is suited to Rust’s functional-esque style! The algebraic data types turned out to be perfect for both caching the necessary widget state and creating diverse yet concise draw signatures. Performance hasn’t yet been an issue, and I no longer see any good reason why it should be!

API Code

This is what drawing a Button looks like at the moment.

// Inside our render loop...

button::draw(&mut gl, // The OpenGL instance used to draw the GUI.
             &mut ui_context, // A user interface context keeps track of state.
             unique_id, // Each widget needs it's own UI_ID.
             Point::new(x, y), // Screen location.
             width, // Rectangle width f64.
             height, // Rectangle height f64.
             Frame(frame_width, frame_color), // Or perhaps `NoFrame` if you don't want one.
             Color::new(r, g, b, a), // The color of the rectangle.
             Label("PRESS", font_size, font_color), // Here you can pass Label(...) or NoLabel.
             || {
    // Callback closure - Do your things here!
});

and this is what drawing a matrix of Toggles looks like.

widget_matrix::draw(cols, // The number of columns.
                    rows, // The number of rows.
                    Point::new(x, y) // Screen location for the matrix.
                    mat_width, // Width of the matrix.
                    mat_height, // Height of the matrix.
                    |num, col, row, position, width, height| { // This is called once for each widget.

    // Now we draw the widgets with our callback params!
    toggle::draw(&mut gl, &mut ui_context, ui_id + num,
                 position, width, height,
                 Frame(frame_width, frame_color), // Rectangle frame.
                 Color::new(r, g, b, a); // Rectangle color.
                 NoLabel, // We don't feel like having a label.
                 my_value_matrix[col][row], // Toggle value (true / false)
                 |new_val| { // Our callback with the new value!
        my_value_matrix[col][row] = new_val;
    });

});

There’s still heaps of room for improvement and lots to be done however it’s definitely approaching a usable state.

EDIT: There have been so many awesome suggestions since this post that we had no choice but to do a total API overhaul. Read the relevant update here!

PistonWorks - Open Source Rust Libraries!

It should be noted that there is no way Conrod would exist without the open source PistonWorks collective! Conrod has several Cargo dependencies, and they are all Piston projects! Come and visit us / join in at [GitHub] (https://github.com/PistonDevelopers) or drop by on Mozilla’s #rust-gamedev IRC channel where most of us are normally lurking.

https://github.com/PistonDevelopers

https://github.com/PistonDevelopers/conrod

mitchmindtree

Older Newer