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 for Emscripten WebIDL binder:
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); }
or use an Effect Hook π₯
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();
}
}
or use a Hook π₯
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/AppImage 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
- Bringing the web up to speed with WebAssemblyΒ Haas et al., PLDI 2017
It's good enough
but WebAssembly will make it even faster
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 Conf 2018, React Boston 2018 and React Next 2018
- 4,223