Piston 0.1 is released

The Piston core is now 0.1 and working on Rust Beta!

There are still some important libraries that requires Rust Nightly for overall game and app development, so Rust Nightly is recommended until further notice.

Getting started

To get started, I recommend looking at the piston-examples or piston-tutorials.

The Piston core

Today, the Piston core is minimalistic in design, and consists of 4 libraries:

  • pistoncore-event
  • pistoncore-event_loop
  • pistoncore-window
  • pistoncore-input

These 4 libraries are reexported in one crate called “piston”. The main purpose of the core is to provide an event loop:

for e in piston::events(window) { ... }

This loop emits events that tells you when to render, update or handle input. It is deterministic, uses fixed time step for updates and maximum frame rate. By default this is 120 ups and 60 max fps. You can pick your own settings:

for e in piston::events(window).max_fps(30).ups(200) { ... }

Some glue code is required to initialize the window. To do that you need to pick a window back-end. Piston has back-ends for GLFW/Glutin/SDL2. To learn how to set up a window, check out the examples

The iterator design for the game loop makes it easier to swap context between scenes. For example, if you are making an adventure game you could make one game loop per room, or nest them within each other!

for e in piston::events(window.clone()) {
    // Inside the temple of doom.
    ...
    if open_secret_door {
        for e in piston::events(window.clone()) {
            // Grab the ancient sword
            ...
        }
    }
}

Another usage is having a separate loop for an in-game editor. The flexibility of the loop makes it possible to load assets in a separate step from application setup, such that when the player dies in a level he or she does not have to wait for the level to reload. Using the stack for reloading a level also eliminates a class of bugs where the application state is not properly reset.

Most window APIs in Rust provides an event loop, but not a game loop. Often you have to write your own, and this is hard to get right.

In some applications running background tasks can make it less responsive. The “idle” event tells you how many seconds the thread is expected to sleep, and when running micro background tasks taking shorter than this interval, the input accurary becomes improved, making it more responsive! This is also true for input events that takes some time to process.

for e in piston::events(window) {
    if let Some(args) = e.idle_args() {
        if args.dt > 0.001 {
          // Run a micro background task..
        }
    }
}

In some games, the rendering might take too long time for a few frames, and fixing this to stay at max fps can be very hard. This leads to a small delay in gameplay. One way to fix this by extrapolating the delta time in the render event, and use this to interpolate animation/physics when rendering.

for e in piston::events(window) {
    if let Some(args) = e.render_args() {
        // Extrapolate the motion to make it appear smoother.
        let p = pos + dir * args.ext_dt;
        ...
        // render
    }
}

Event programming

piston::event::GenericEvent is a trait that uses Any to build an abstraction for other events. For example, RenderEvent is implemented for all objects that implements GenericEvent. When you are calling a method that takes E: GenericEvent, you do not know which events it handles. This type of design is called “event threading”.

This has both drawbacks and benefits, but the major benefits are:

  • Custom events can be added to a specific back-end without changing code
  • Supporting future hardware without major redesign
  • When using a controller, you only need to call one function
  • It is easy to combine with other event logic patterns

A widget often captures the all input from the user. In a game, the user input often goes to the player controller object. For example, feeding events to a player object could look like this:

player.event(&e);

This is so simple design as it can be, with no event handlers to keep track of.

For more advanced event logic there are various libraries you can use. There is a lot of research going on in this area, including AI behavior trees and Functional Reactional Programming. These patterns targets more complex event logic problems.

AI behavior trees is a way to describe sequantial, conditional and parallel events in a declarative manner which gets transformed into a state machine. This is suitable to problems like “the character is running down the stairs while pointing with the gun”, with other words, things that happens at the same time but simulated sequentially. One can assign a “behavior” to objects and this automatically keeps track of the state. A surprising attribute of this logic is that it is very intuitive and close to common sense, and therefore nice for programming game AI. The groundwork for this was completed last year, and needs more real world testing.

Functional Reactional Programming is hot topic in event logic. If you are interested in this, then you might want to check out Carboxyl. This is not currently an active research topic in the Piston project, but if you want to work on this, then you are very welcome!

Graphics

Piston is a bit special in the way that the graphics API is decoupled from the core. You can write a game using any 2D/3D API you like.

Piston-Graphics is a project to create a back-end agnostic 2D graphic API for 2D games or widgets.

The goal is that by plugging in a graphics back-end into your project, you can use the libraries in the Piston ecosystem together with the API you choose. This makes it easier to share code across projects.

Conrod is an immediate mode user interface for Piston. It is built on top of piston-graphics and completely back-end agnostic. This is still early in the development, but we welcome people who wants to contribute!

Other projects - find a project you are interested in, or start your own!

Some projects running in parallel:

  • image a library for encoding/decoding images and image processing
  • Hematite has the goal of making client/server for Minecraft in Rust
  • skeletal_animation character animation with import from Collada 3D
  • VisualRust a Rust plugin to Visual Studio
  • sprite a library for sprite animation

If you are interested in starting your own project, please open an issue on the Piston repo, we can help you!

Duck Typing in Piston

Summary

Other news:

Also, notice audio libraries are moved to a new organization: RustAudio. mindtree is working on these libraries frequently and probably would love some contributions!

Experimenting with abstractions for game development in Rust

Disclaimer: The opinions in this article are not meant to be taken seriously, this is to just to make it sound less boring! Neither does it represent the official view of the 90 other Piston developers…

The Piston project is an active open source organization that started out with a back-end agnostic 2D graphics library followed by an pure Rust image format and processing library and a Minecraft-world renderer in Rust and an immediate mode UI for Rust and a Visual Studio plugin for Rust, an AI behavior tree library, a Wavefront OBJ format library, a sprite animation library, bindings for PhysFS, bindings for FreeType, a texture packer library, a library for better-than-globals, called “current objects” and a few more.

There is new library in town: Piston-Quack.

The safety of Rust is a huge win when you are maintaining a large codebase, but to keep it modular, you need good abstractions.

A good abstraction has the following properties:

  • It needs to be understandable, so you can fix it if it goes wrong
  • It should not force you to reinvent the wheel for every library

Piston-Quack builds on the idea of duck typing.

Since Rust does not have a garbage collector, you have to use &RefCell<T>, Rc<RefCell<T>> or unsafe code to share objects. If you are coming from garbage collected language, this is something you should pay attention to. The lack of a universal sharing mechanism put restrictions on the abstractions you can do. For example, if you write a structure like this:

pub struct Foo<T: Bar> {
    bar: T,
}

You figure out that T must be shared, and you can change it into:

pub struct Foo<T: Bar> {
    bar: Rc<RefCell<T>>,
}

You can’t use Foo<Rc<RefCell<T>> because Rc<RefCell<T>> does not implement Bar.
This means when writing generic code, you have to think about whether the object is shared or not.

In game development, sharing objects is so common that this becomes an obstacle. For most cases, you don’t care about this at all, only that the object is accessed by one thread at a time. If you do it wrong and get an runtime error, then that’s an acceptable cost compared to the time it takes to reason about it. At the same time, you care a lot about performance, so putting a smart pointer everywhere is not an option.

Imagine game developers as the “worst customer” experience for language designers: Alice, a game developer, goes in a programming language store owned by Bob. Bob puts the latest and shiniest language on the desk, and shows the amazing features. Then Bob asks Alice what she thinks about the language. Alice starts to point out “I don’t want this, I don’t want that, this shouldn’t be there…”. Then Bob asks what Alice wants, and Alice says “I want to not think about it”.

Abstractions that requires thinking under composition or refactoring leads to friction in the game development process. If you try to make it better, it is still not good enough, because you want to not think about it at all… except when it matters, and suddenly you want full control. So we need a language that lets you do both.

This is why Rust is an excellent language for game development!
(comment: needs photo of everybody jumping with their arms in the air)

Rust comes with a powerful type system. Not only can you make it good, you can make it nice

Let us look at piece of code from Conrod:

quack! {
    env: EnvelopeEditor['a, E, F]
    get:
        fn () -> Size [where E: EnvelopePoint] { Size(env.dim) }
        fn () -> DefaultWidgetState [where E: EnvelopePoint] {
            DefaultWidgetState(Widget::EnvelopeEditor(State::Normal))
        }
        fn () -> Id [where E: EnvelopePoint] { Id(env.ui_id) }
    set:
        fn (val: Color) [where E: EnvelopePoint] { env.maybe_color = Some(val) }
        fn (val: Callback<F>) [where E: EnvelopePoint, F: FnMut(&mut Vec<E>, usize) + 'a] {
            env.maybe_callback = Some(val.0)
        }
        fn (val: FrameColor) [where E: EnvelopePoint] { env.maybe_frame_color = Some(val.0) }
        fn (val: FrameWidth) [where E: EnvelopePoint] { env.maybe_frame = Some(val.0) }
        fn (val: LabelText<'a>) [where E: EnvelopePoint] { env.maybe_label = Some(val.0) }
        fn (val: LabelColor) [where E: EnvelopePoint] { env.maybe_label_color = Some(val.0) }
        fn (val: LabelFontSize) [where E: EnvelopePoint] { env.maybe_label_font_size = Some(val.0) }
        fn (val: Position) [where E: EnvelopePoint] { env.pos = val.0 }
        fn (val: Size) [where E: EnvelopePoint] { env.dim = val.0 }
    action:
}

When we implement traits based on these types, the trait gets “lifted” to Rc<RefCell<T>>.

/// A trait that indicates whether or not a widget
/// builder is positionable.
pub trait Positionable {
    fn point(self, pos: Point) -> Self;
    fn position(self, x: f64, y: f64) -> Self;
    fn down<C>(self, padding: f64, uic: &UiContext<C>) -> Self;
    fn up<C>(self, padding: f64, uic: &UiContext<C>) -> Self;
    fn left<C>(self, padding: f64, uic: &UiContext<C>) -> Self;
    fn right<C>(self, padding: f64, uic: &UiContext<C>) -> Self;
    fn down_from<C>(self, ui_id: UIID, padding: f64, uic: &UiContext<C>) -> Self;
    fn up_from<C>(self, ui_id: UIID, padding: f64, uic: &UiContext<C>) -> Self;
    fn left_from<C>(self, ui_id: UIID, padding: f64, uic: &UiContext<C>) -> Self;
    fn right_from<C>(self, ui_id: UIID, padding: f64, uic: &UiContext<C>) -> Self;
}

/// Position property.
#[derive(Copy)]
pub struct Position(pub [Scalar; 2]);

impl<T> Positionable for T
    where
        (Position, T): Pair<Data = Position, Object = T> + SetAt
{

    #[inline]
    fn point(self, pos: Point) -> Self {
        self.set(Position(pos))
    }
    ...
}

Since widgets have many properties in common, and lot of functionality depends on a few properties, building traits on top of these properties reduces the number of lines of code. If the API lacks something, you can make your own trait, because this is Rust.

  • You don’t have to worry about an object is shared or not, it will just work
  • It improves type safety, for example by distinguishing between Transform and ViewTransform
  • It can be upgraded to support new smart pointers in the future, such as garbage collected pointers
  • It fits great with APIs that typically run into the expression problem

Builder methods comes for free with a consistent look-and-feel across libraries.

let foo = Foo::new.set(Bar([w, h])).set(Baz([x, y]);

If you don’t like this style, then you can auto implement traits on top with all the bells and whistles.

There is also no performance overhead, the compiler will optimize it for different use cases. The quack! macro inlines everything, so large methods should be refactored out of it.

A couple things that might be improved in the future:

  • The ugly Pair<Data = Position, Object = T> might disappear when Rust gets fully equality constraints
  • The brackets [] for where clauses might disappear, they serve as work-around for macro parsing rules

Since Rust now refines impls by generic constraints, you can also implement the traits for other types.

Piston-Quack was originally based on rust-modifier by reem, but expanded to work nicer with generics and the API requirements we had in Piston. It replaced the old type-currying API in piston-graphics, and got rid of macros in Conrod.

Thanks to quack!, you can write your own window back-end for Piston in a few hundreds lines of code. Piston has currently 5 window back-ends:

It is currently being tested in piston-event_loop, piston-event, piston-window, piston-graphics and Conrod.

“I’ve heard get and set methods are bad! Don’t like it!”

If you follow discussions about library design, then you might come across arguments against accessors (get & set). Piston-Quack is not about accessors, it is about duck typing. It this context it makes sense to use get/set, because it replaces the direct access to struct members.

Accessors in other languages, for example C#, often just wrap a member variable. Duck typing on the other hand, gives you a small “interface” for each accessor. These interfaces can be used to build larger interfaces, where you in C# would use inheritance. (might be changed now, it has been a while since I used C#)

When each property is defined as a type, you can add more fields to it, or even expand it to its own struct that fits better with the use case. You can also add lifetimes and generic parameters.

The argument also is focusing on building methods instead of accessors. When you don’t want get/set, you can use action. Here is an example from sdl2_window:

action:
    fn (__: SwapBuffers) -> () [] { _obj.window.gl_swap_window() }
    fn (__: PollEvent) -> Option<Input> [] { _obj.poll_event() }

If you have an better idea of how to do this, please open an issue here.

A thank you to the Rust core team

During my entire experience with Rust, the core team has been supportive and helped on a lot of occations. One of the most important contributions to the Piston project, has been the collaboration to make Cargo support this kind of infrastructure. My favourite feature in Cargo is the ability to override dependencies locally. This goes to the top of my time-saving feature list that makes Rust suitable for coding-in-the-large. Just a reminder of how a small feature can be extremely useful in a large project.

I want to thank Mozilla for leading this effort and wish you good luck toward 1.0.

The road to high level libraries

Notice! This is a work in progress and not the final design!

Piston-Current is a library I have been working on with the goal of exploring “current objects” in library design.

High level libraries is the idea that Rust libraries can be written in a such way for game engines, that makes them very easy to use and can be composed together without adding complexity. I think the expression “high level” is awfully inaccurate, but I have not yet come up with a better word for it. Unfortunately I don’t have the libraries yet to show what I mean, but I hope to explain something about it in this post.

Piston-Current is designed with the intention that code based on it is easy to translate from back-end specific to generic, or building a high level library on top of it. Therefore, Piston-Current is considered one of the “core” libraries of Piston. Having a “core” does not mean all library need to depend on it. For example, “image” and “vecmath” are completely independent. Libraries that depends on one of the core libraries have a piston2d-, piston3d- or piston- prefix in the package name.

To get closer to my dream of high level libraries, I have to come up with a design that is a little like globals or singletons, but not quite the same. It is also not entirely safe, but not unsafe either if used properly. Since this is new, and people are used to think #$!!??#%@# about globals, it takes some time to get used to the idea, but don’t worry, it is completely optional to use. If you want to chat with me, I am frequently at the #rust-gamedev IRC channel under the nickname “bvssvni”.

It all begins with “current objects”…

Motivation of current objects

In game programming, there are many kinds of “current” values:

  • The current window
  • The current device
  • The current sound driver
  • The current player object

By setting these up as “current” values, you don’t have to pass them around to each method. For example, you can write code like this:

e.press(|button| {
    if button == SHOOT {
        unsafe { current_gun() }.shoot(unsafe { current_player() }.aim);
    }
});

(If you worry about the unsafe blocks, these will be explained later in the article)

This makes it easier to decouple data from each other in the application structure.

The major motivation for this library is to have a convention that works across libraries.

Problems Piston-Current solves

  • Make current objects and &RefCell<T> work with generics
  • Composable high level libraries
  • Solving cases when the borrow checker is not powerful enough
  • Convenient way of storing application structure
  • Stepwise setup and rollback of application state
  • Allows fn () -> T to compute something from application structure, which reduces code
  • The balance between safety and convenience that fits with game programming

Get, Set and Action

To make current objects and &RefCell<T> work with generics, the object must use the traits GetFrom, SetAt and ActOn. Get, Set and Action are auto implemented from these traits.

These 3 traits simplifies library design, and it gives a consistent behavior across Piston libraries. Set allows one to use the builder pattern. It makes is possible to write generic code, where you can choose between T, Current<T> or &RefCell<T>.

To make it work with generics, use where clauses like this:

impl<W, I, E: EventMap<I>>
Iterator<E>
for Events<W>
    where
        ShouldClose: GetFrom<W>,
        Size: GetFrom<W>,
        SwapBuffers: ActOn<W, ()>,
        PollEvent: ActOn<W, Option<I>>

This works with duplicate constraints, because the type of the constrained object is concrete. For example, you can have Size: GetFrom<U> + GetFrom<V>.

Current<T> is just a wrapper type that auto implements these traits. You can implement these traits for your own custom wrapper type, if you want to.

What are high level libraries?

This is something I am excited about!

When I say “high level” I mean different from “normal” or “low level” because of the way the library is used, not because it is further away or closer to the hardware. It is because such libraries usually are designed for higher concepts that involves bigger pieces of game programming, and they can be combined to build the features you want. So “high level” means something like “high level game library for Piston” and does not refer to programming in general.

A high level library requires just a few lines of code to set up, and adds functionality to the application without adding complexity.

For example, the piston repo is currently a high level library that sets up a window and OpenGL context through the function piston::start.

Example:

piston::start(
    piston::shader_version::OpenGL::_3_2,
    piston::WindowSettings {
        title: "Deform".to_string(),
        size: [300, 300],
        fullscreen: false,
        exit_on_esc: true,
        samples: 4,
    },
    || start()
);

Because the library sets up current objects, there are no objects to keep track of and pass to other functions. If the piston library adds some features, you don’t have to change your function signatures to use the new features.

Calling piston::set_title lets you change the title of the current window. If you forget to call piston::start the task will panic with a message about which current object is missing.

It also lets you start a new game loop with the following code:

for e in piston::events() {
    ...
}

A game loop can run inside another game loop to display a different scene temporarily. It makes it possible to divide the application logic into logical parts and run them in separate functions. You can also call piston::start again to pop up a new window.

You can also set up application state in steps and roll back. For example, in the game Sea Birds’ Breakfast I loaded in the assets in a separate function from setting up application state. When the player won or lost, it was not necessary to reset the state manually, or reload the assets.

fn load_assets(f: ||) {
    // Load assets and set them up as current objects
    ...

    // Restart level if not quiting.
    while !piston::should_close() {
        // Loop infinite times. 
        background_music.play(-1).unwrap();
        
        f();
    }

    // Drop current guards
    ...
}

Such functions can be reused in a new project with a similar setup of game assets.

All high level libraries share these properties for their domain of functionality. For example, you can develop a library for rendering landscapes, and change the landscape for a scope, or roll back changes made to the current landscape, or reuse the landscape in another project.

Conditional compilation

The piston repo lets you add features = ["include_gfx"] in the Cargo.toml to set up a Gfx. If you want to compile code conditionally based on whether piston compiles with Gfx, then you can add a [features] tag to your Cargo.toml:

[features]
include_gfx = ["piston/include_gfx"]

Now, you can use #[cfg(feature = "include_gfx")] for Gfx specific features.

Composing high level libraries

Currently the piston repo is the only high level library, but the plan is to extend the Piston ecosystem with new high libraries over time.

A high level library can be used with another library if they share a high level dependency, even those libraries were not designed intentionally to work together. This is because current objects are unique per type, so no information is required to glue one library to another.

Generic libraries that uses GetFrom, SetAt and ActOn are not considered high level. It is only high level if you don’t have to keep track of any objects. Piston-Current makes it easy to write high level interface on top of generic libraries, and the traits is the requirement.

For example:

/// Returns an event iterator for the event loop
pub fn events() -> event::Events<Current<WindowBackEnd>> {
    unsafe {
        Events::new(current_window())
    }
}

The Events object can take T, Current<T> or &RefCell<T>.

Using generic libraries on top of Piston-Current does not mean you have to use current objects. Current objects are usually used in application or a high level library. It can be exposed in a high level library if there is a need to work around the limitations of the API.

For example, the piston library exposes the current window:

pub unsafe fn current_window() -> Current<WindowBackEnd> { Current::new() }

You can use piston::set_title to change the title of the current window without unsafe block:

/// Sets title of the current window.
pub fn set_title(text: String) {
    unsafe {
        current_window().set_mut(window::Title(text));
    }
}

Most functions in a high level library looks like the one above, with a few lines of code. The major part of the functionality can be built in safe generic code.

A high level library author can choose to wrap the most frequent operations in a safe interface, and let users write their own functions.

When a library has a high level dependency, it should import the function prefixed with current_ instead of the type. This is because the dependency then can be recompiled and change the type without breaking code. For example, if you fork piston and recompile it to use GLFW, then there will be no breaks because the library does the following:

use piston::current_window;

The same thing happens when you use the conventions from Piston-Current in an application. Application logic looks similar to high level library logic. It also makes it easier to split the application into modules by functionality, for example to keep rendering code separate from update code.

Example:

The sea birds module can easily be refactored into a library, and then used to add similar entity behavior in other 2D games. This is independent of how these entities are rendered. A similar thing can be done for 3D entities that navigates through a “current map”.

One consequence of this is that users that follow Piston-Current conventions, produces code that is suitable for high library design while working on their application. The learning curve to write a high level library is pretty low. This is important, because there will always be new people using Piston, and they can find ways to contribute without expert knowledge of the internals. At the same time, high level libraries what we want most, so the “new, cool features” can be developed by anybody. This will put pressure off maintainers to come up with new features, and they can focus on more important stuff for maintenance, for example stability, API design and problem solving.

A library can start out as high level and be redesigned as a normal library, change the back-end specific code, or be rewritten to use a back-end agnostic abstraction, often without breaking code.

Many people can work in parallel on different libraries without having to know about each other, which is very important in a larger community.

Safety vs convenience

Current objects can not be used without unsafe blocks, which makes them a bit hard to accept for people who only want to write in safe code. It is easy to avoid the pitfalls, but since the compiler can not enforce these rules, there is room for human error.

Scoped thread local variable and &RefCell<T> can achieve a similar functionality safely, but can only be used with closure callbacks. Therefore, Piston-Current supports this as a safer alternative.

When calling a function that gives you a mutable reference to a current object in a closure, it is possible to create two mutable references in scope by calling the same function inside the closure:

render(Some(white), |c, g1| {
    render(Some(white), |c, g2| {
        // there are 2 mutable borrows to the gl back-end in scope
    });
});

This is the only unsafe case in high level library design that uses current objects, so in order to solve this a DANGER struct must be added as first argument:

render(unsafe { current::DANGER::new() }, Some(white), |c, g| {
    ...
}

When you see DANGER, the documentation of the function must include a notice about what the unsafety is about. This way, if there is no unsafe block in the closure, the code is guaranteed to be safe. Scoped thread locals can avoid this, but will cause task panic in the same situation. Piston-Current lets you choose either one.

Using current objects in library design requires some knowledge about the danger of mutable aliased pointers. Dereferencing, borrowing and then assigning with Current multiple times in same scope is considered unsafe. For example, the following prints out “bar”:

let foo = unsafe { & *current_window() };
let bar = unsafe { &mut *current_window() };
bar.set_mut(Title("bar".to_string()));
let Title(text) = foo.get();
println!("{}", text);

Calling a method on a current object is not unsafe, only if two objects of same type are brought into scope. In order to meet the standard of safety, Current::<T> can not be constructed safely.

For example:

unsafe { current_gun() }.shoot(unsafe { current_player() }.aim);

By making the functions current_gun and current_player safe, it would look like:

current_gun().shoot(current_player().aim);

The code would still be safe, but because it then would theoretically possible to use those functions in an unsafe way, the convention is to mark such functions with unsafe.

unsafe fn current_window() -> Current<Window> { Current::new() }

When working on your own game project, it is up to you whether you want unsafe functions or not. This is easy to add or remove.

In generic code, unsafe blocks are not required.

Current objects should usually be used within global functions. For impls, use generics instead.

Conclusion

The rules for reasoning about safety are clear and easy enough to avoid most errors. Unfortunately, these rules can not be enforced easily with the compiler or without runtime task panic.

It will be important to communicate the guidelines for high level library authors. Normal libraries should not use current objects, but the Piston-Current design makes it possible to add this with a few lines code either in the application or a high level library. Because current objects are optional to use, and the design of Piston-Current has other benefits, I think it is worth continuing this direction.

In my opinion current objects boosts producivity significantly in the way I want to use Piston. It is in important for quick coding, but I find it easy enough to reason about the safety to not change back. The code can be structured such that I can easily see where current objects are used. For example, I don’t use it inside impls, but only within global functions.

I made a scoped thread local version to compare the two designs. Scoped thread locals combined with &RefCell<T> is harder to refactor to safer code. Because of the convenience of changing back and forth, I am in favor of the current design.

Besides, since &RefCell<T> is supported by Piston-Current, it can be combined with scoped thread locals to get same functionality. This is great for people that want better safety guarantees and choose to not use current objects. The scoped thread local version does not add new features or makes it safer to use. However, just in case, the “scoped” branch will be kept until Rust 1.0 is released.

High level library design is something I wish to explore further, but I don’t know how to achieve this without Piston-Current yet. Perhaps we can build the libraries first and then see how we can change them to make them safer? I picture when we get some high level libraries, which are easy to build, then we understand better what we need and can make better and safer abstractions.

In summary, I feel the Piston core has a clear path for the architectural problem it is supposed to solve, and I don’t expect big changes in the future, but I am looking forward to more high level libraries!

Older Newer