Beyond Web-Apps

React, Javascript and WebAssembly to Port Legacy Native Apps

@FlorianRival

gdevelop-app.com

2008

 open-source, cross-platform game creator designed to be used by everyone

  • An editor for games
  • Visual programming
  • Output HTML5 games

2016

The editor is getting old

  • Lots of cross-platforms issues with the UI toolkit, support for macOS and Linux is bad.
  • Iterating on the software is slow (C++ 😬)
  • Building UI is slow...
  • ...and limited to old UI components...
  • ...and UX would need some enhancements 😇
  • The entry barrier for new contributors is high (C++ 😬)

It's time to react!

What do we want?

  • A new editor!
    • Cross-platform, better UI building paradigm, faster development, lower entry barrier
  • No full rewrite!
    • Full compatibility with the existing projects.
    • Nobody got time to wait for a rewrite.

Could web technologies help?

GDevelop C++ codebase

New GDevelop editor

GUI (windows, dialogs...)

Filesystem

Core classes (game, scene, object, textures...)
Game exporter
libGDevelop.js

React.js powered interface

Node.js/browser adapters

X-platform toolkit (wxWidgets)

LLVM IR
(bitcode)

Using Emscripten

git clone https://github.com/juj/emsdk.git && cd emsdk
./emsdk install latest && ./emsdk activate latest
./emcc tests/hello_world.c
node a.out.js

For large projects, Emscripten provides replacements that swap GCC or Clang by Emscripten in build system (automake, CMake...)

Expose existing classes

class ObjectsContainer {
  Object& InsertObject(const Object& object, std::size_t position) { ... };
  Object& GetObject(const String& name) { ... };
}

class Layout : public ObjectsContainer {
public:
  void setName(const gd::String & name_) { name = name_; };
  const gd::String & getName() { return name; };

  // ...
}

interface Layout {
  void setName([Const] DOMString name);
  [Const, Ref] DOMString getName();

  // Inherited from ObjectsContainer
  [Ref] Object insertObject([Ref] gdObject object, unsigned long pos);
  [Ref] Object getObject([Const] DOMString name);
}

"WebIDL" bindings:

Use Emscripten classes

Module().then(gd => {
  const layout = new gd.Layout();
  layout.setName("My game level");
  const object = new gd.Object();
  object.setName("My character");
  layout.insertObject(object, 0);

  console.log(layout.getName()); // "My game level"
  console.log(object.getObject("My character")); // Returns a gd.Object
});
  • Primitive types are converted, including JS strings (to char*)
  • Objects references are converted to pointers (or references) by the webIDL generated glue code
  • std::iostream is binded to console.log

Things to know

const layout = new gd.Layout();
// ...
gd.destroy(layout); // Binded objects need manual destruction
  • Memory management requires care!
  • Output files can be large (~3mb for GDevelop), but gzip helps.
  • For React:
componentDidMount() { this._project = new gd.Project(); }
componentWillUnmount() { gd.destroy(this._project); }

Things to know

  • A complete test set is invaluable for ensuring that no errors are in the bindings.
abort(16). Build with -s ASSERTIONS=1 for more info.
  • Wrong types of parameters
  • Parameter forgotten
  • Using a deleted object

Finding a component library

The goal is to get started with all the classical components of a user interface without rewriting everything

  • Extensive list of high quality components
  • Good theming support
  • Support for (nested) dialogs
  • Flexibility of components
  • Documentation quality
  • Performance (blazing fast? ⚡️)

Large lists (with drag'n'drop)

We need to display lists from dozens to hundred of elements

👉 Virtualized lists

const SortableObjectRow = SortableElement(props => (
  <ObjectRow ... />
));

const ObjectsList = (props) => (
  <List
    rowRenderer={({ index, key, style }) => {
      <SortableObjectRow />
    })
  />
);

const SortableObjectsList = SortableContainer(ObjectsList);

<SortableObjectsList
  onSortEnd={({ oldIndex, newIndex }) => ...}
  ...
/>

+ react-sortable-hoc

const ObjectRow = () => <div>...</div>;

const ObjectsList = (props) => (
  props.map(group => <ObjectRow ... />)
)

Moveable editors

Game editors are traditionally composed of multiple editors.

react-mosaic

👉 Use React tiling window manager libraries

Keyboard handling

Focus/tab browsing

Shortcuts

👉 Give focus() to your components

componentDidMount() {
  if (this.props.focusOnMount) {
    this._searchBar.focus();
  }
}

https://blueprintjs.com/docs/#core/components/hotkeys

👉 Abstract keyboard shortcuts so that it's easy to add to any component

<Hotkeys>
  <Hotkey
    global={true}
    combo="shift + a"
    label="Be awesome all the time"
    onKeyDown={() => console.log("Awesome!")}
  />
</Hotkeys>

Large trees (with drag'n'drop)

👉 Virtualization again!

react-sortable-tree

Levels rendering or visualizations

👉 The DOM is not powerful enough here

👉 The DOM can still be good enough

<svg
  onMouseMove={...}
  onMouseUp={...}
  width={this.props.imageWidth}
  height={this.props.imageHeight}
>
  <polygon
    fill="rgba(255,0,0,0.2)"
    stroke="rgba(255,0,0,0.5)"
    fileRule="evenodd"
    points={vertices.map(vertex =>
      `${vertex.getX()},${vertex.getY()}`
    ).join(' ')}
  />
  {vertices.map((vertex, j) => (
    <circle
      onMouseDown={...}
      key={`vertex-${j}`}
      fill="rgba(255,0,0,0.75)"
      cx={vertex.getX()}
      cy={vertex.getY()}
      r={5}
    />
  ))}
</svg>

When things aren't native enough

👉 Build abstractions over Electron APIs

export default class ElectronMenuImplementation {
  buildFromTemplate(template) {
    this.menuTemplate = template;
    return undefined;
  }

  showMenu(dimensions) {
    if (!electron) return;

    const { Menu } = electron.remote;
    const browserWindow = electron.remote.getCurrentWindow();
    this.menu = Menu.buildFromTemplate(this.menuTemplate);
    this.menu.popup({
      window: browserWindow,
      x: Math.round(dimensions.left),
      y: Math.round(dimensions.top + dimensions.height),
      async: true, // Ensure the UI is not blocked on macOS.
    });
  }
}
class MaterialUIContextMenu extends React.Component { ... }
class ElectronContextMenu extends React.Component { ... }
export default class MaterialUIMenuImplementation {
  buildFromTemplate(template) {
    return template.map((item, id) => {
      if (item.type === 'separator') {
        return <Divider key={'separator' + id} />;
      } else if (item.type === 'checkbox') {
        return (
          <MenuItem
            ...
          />
      } else {
        return (
          <MenuItem
            ...
          />
      }
  }

  showMenu() { ... }
}

When things aren't fast enough

👉 Double check rendering of your components

  • why-did-you-update

Don't forget to measure performance in production (React development build is way slower)

  • React 16.5 profiler
  • shouldComponentUpdate is often the answer

👉 Inspect calls to Emscripten objects

  • Beware of overhead in binding code/wasm call when calling functions, in particular if renderings
objects.filter(object => object.getType() !== "") // n wasm calls + string conversion
  .map(object => object.getVariables(project));   // n wasm calls + string conversion

Won't be noticeable in most UI situations, but can be if used in renderings or long lists/trees

  • It might be useful to store strings/values on the JS side if you know they won't change.

2017

Ultra fast iteration and testing

Feedback loop is a few seconds while developing

vs

  • Storybook
  • React Styleguidist

Modular development of UI components

  • electron-builder

Auto-updates and packaging...

...and code signing, and upload, and creating installers for Windows, .DMG for macOS, .tar.gz for Linux.

Since day one, GDevelop 5 is auto-updated.

Time between releases moved from months to weeks or days.

  • More contributors than before
  • Startup time is similar (even faster as a web-app, ~3seconds)
  • People can try the software from their browsers in seconds without downloading anything
  • Documentation can link to the editor and examples can be opened directly in browser
  • Can be later ported to iOS/Android tablets, even phones
  • Integration with Dropbox, Google Drive...

And more

Plot twist

It's (still) JavaScript!

WebAssembly is on average 33.7% faster than asm.js, with validation taking only 3% of the time it does for asm.js

It's good enough

Epilogue

What are users saying?

Wow, what a difference a year can make 

but I have to admit, once you worked a bit with it, it could be really more productive on many aspects than GD4

Amazing how much easier and streamlined the engine has become

Thanks!

@FlorianRival

gdevelop-app.com

github.com/4ian/GDevelop

Beyond Web-Apps: React and WebAssembly to Port Legacy Native Apps

By Florian Rival

Beyond Web-Apps: React and WebAssembly to Port Legacy Native Apps

Talk at React Boston 2018

  • 296
Loading comments...

More from Florian Rival