Making a level editor, but as an external tool

4
skymen's avatar
skymen
  • 30 Jun, 2020
  • 2,184 words
  • ~9-15 mins
  • 1,527 visits
  • 2 favourites

Hi, I...

Uh….

...I have been away for a while.

I’m back with lots to say, so I’ll probably post a few new entries to this blog, starting with this one.

First, some backstory

The year is 2019…

...the day is the 18th and the month is January. I received a message:

We had been organizing a game jam in the Construct Community discord server, and people had 2 weeks to post cool little games:

Subscribe to Construct videos now

We even made a website and hosted it ourselves at the time.

Sadly, one community member thought it would be a good idea to take a construct project from the Scirra Arcade, change a few sprites and send it to the jam. We quickly took it down, and this is how I first met InsaneHawk.

Hawk was working on a rhythm Bullet hell game on Construct 3 with plans to complete it within a few years and publish it on Steam. I was already very stoked by the Scirra Arcade demo, so I asked him to keep in touch.

The year is still 2019…

...but in September this time.

I had kept in touch with him, and had followed the game’s progress and joined the beta testing program.

Some evening, I was watching his dev stream, and another beta tester asked if the game would have a level editor. He said no, because it would be way too much work for him.

I thought “Hey, his levels are just a huge list of actions that are ran at specific timings, I’m 100% sure this can be automated”, and I told him I’d be down to work on it as soon as I was done with my current work.

A few weeks later, I was officially in the team.

The year is now 2020…

...6 months after I joined the team, the level editor was finally announced properly and showcased.

I even made a super dope teaser video

Subscribe to Construct videos now

The final product has been made available in May in Early Access on Steam.

The process

Experimenting designs

The very first thing I did was design some mockups. We talked for a long while with Hawk about what the perfect level editor would be, and after looking at similar level editors, I presented this design:

We sent ideas back and forth, showed the mockup to the community to gauge reactions, and we ended up with this new more complex design:

Early on, we listed what we wanted the level editor to do:

  • Change music, theme and other level metadata
  • List actions and events
  • Reorder them easily, and instantly see on a timeline when they happen
  • Being able to preview the game either from the editor or in game from any given point in time.

The idea for the editor was fairly clear early in development: A level editor for a music game is not exactly the most complex thing to design. Execution however is something else.

Experimenting Techs

Construct’s UI elements are very limited and making a full editor in it would be pretty hard, but if it’s not made in Construct, that means it will need to be done as an external tool, and that comes with its own set of problems.

Try 1: Let’s hack Construct’s DOM

My first idea was to write JS that would inject additional HTML and JS to the game’s page and would toggle between showing and hiding them at will.

That would allow us to have HTML’s freedom, while still writing all the code inside of Construct. That would also allow us to clearly distinguish between the game and the level editor code as it would be purely JS with some glue code to expose functions to Construct’s functions system.

And since it’s purely JS, I decided to experiment with lightweight component based frameworks like Preact, and Lit HTML.

Failure 1: It’s hard 😔

Since I had to write everything within Construct, I had to write very weird code just so preact could work. On top of that, I had to write hacks that worked around Construct’s linter that would block requires to libraries.

This is what I had to write, just to add a simple “Hello world” on top of the canvas:

eval(`globalThis.myImport = (arg) => {return import(arg)}; console.log("done")`);
Promise.all([
	globalThis.myImport('https://unpkg.com/preact?module'),
	globalThis.myImport('https://unpkg.com/htm?module')
]).then(([{ h, Component, render }, {default:htm}]) => {
	// Initialize htm with Preact
	const html = htm.bind(h);

	const app = html`<div>Hello World!</div>`
	render(app, getApp());
})

These 10 lines of code were enough to reveal two linter bugs (that were since fixed) and I hadn’t even written a single component…

github.com/Scirra/Construct-3-bugs/issues/3673

github.com/Scirra/Construct-3-bugs/issues/3672

Soon, I realised that it would be especially hard to write components in individual script files and have them load in the right order.

Even once everything was done properly, I realised that C3 updates the canvas CSS to center it in the window, and it was done with pixels instead of percentage, so moving the canvas to a smaller parent would wildly offset it based on window size.

A real nightmare…

Try 2: Pro UI

While I was busy breaking the game’s DOM, Aekiro released Pro UI for Construct 3. Pro UI is a set of addons designed to allow for making Game UI very quickly, and it was our best chance at making the editor entirely inside Construct.

Failure 2: Still not enough 😕

Pro UI is great, but adding it to the project meant having a new 3rd party plugin to maintain.

It also meant having two separate UI systems working in the same project which is bad for code clarity.

Finally, ProUI doesn’t have any way to properly make an audio timeline, and writing one from scratch using the Construct Audio plugin would be another nightmare I am not prepared for yet.

Try 3: Serendipity

At this point, I had been making tests for months, and Hawk was telling me how much he’d love having an editor to work with because he couldn’t bear his tedious workflow anymore.

I was sick of making no progress at all, and he was sick of wasting full afternoons making 20 seconds of a level.

So I opened powershell, and typed magic words vue create rhythmy-editor and decided to write an external tool for hawk so he could at least make levels. Within a few hours I had this:

Working with Vue is very efficient, and so within a few days I already had this:

Soon, the editor’s first version was feature complete

Subscribe to Construct videos now

Success!

Working with Vue proved to be very efficient and allowed me to write a good tool very quickly, and I had written the features in under two weeks, and the community did not miss that fact 😀.

What was initially meant to be a quick tool for InsaneHawk to relieve his pain ended up being good enough to show to players.

You’ll probably remember when I said this though:

Working around limitations

We have a tool that can create levels, great. Now it needs to work with the game.

In fact there are 2 points that we absolutely needed to resolve:

  • The editor must directly control the game, and at no point should both the editor and the game need to be used at the same time.
  • The editor needs to be launched from the game.

Communicate with the game

By far the most important point to address. The editor needs to communicate with the game without the need for any user input.

For this, I decided to write a node script that would run an express server as a basis for a websocket server.

const cors = require('cors');
var app = require('express')();
app.use(cors({ origin: '*' }));
 
var http = require('http').createServer(app);
var io = require('socket.io')(http);
 
var data = "{}";
 
app.get('/', (req, res) => {
 res.send(data);
});
 
io.on('connection', (socket) => {
 socket.on('message', (msg) => {
 console.log("Socket message received", msg);
 io.emit('message', msg);
 });
 
 socket.on('update data', (msg) => {
 console.log("received new data", msg)
 io.emit('data updated');
 data = msg;
 });
 console.log('a user connected');
 });
 
http.listen(3000, () => {
 console.log('listening on *:3000');
});

Since the editor is written in Electron, I thought I could just include the server directly in the app’s code and the game would connect to it. It worked for a while, but it turns out the builder runs all of the code through webpack, and the server would completely break on build.

Instead I had to include the server as extra Resource to be included on build, and run a child process that would start the server:

function createBackgroundProcess(socketName) {
 let config = [
 '--subprocess',
 app.getVersion(),
 socketName
 ];
 if (isDevelopment) {
 serverProcess = fork('./src/extraResources/server.js', config);
 } else {
 const serverPath = path.join(path.dirname(__dirname), 'extraResources','server.js');
 serverProcess = fork(serverPath, config);
 }
}

Since the server is now running on its own process, the editor needs to connect to it too, so I put together a simple socket client:

import io from 'socket.io-client';
 
var socket = io("http://localhost:3000");
 
var dataCallback = null;
var refreshCallback = null;
 
socket.on('message', function(msg) {
 console.log("Socket message received", msg);
 let data = JSON.parse(msg);
 if (data.command === "preview" && typeof refreshCallback === "function") {
 refreshCallback();
 refreshCallback = null;
 }
});
 
socket.on('data updated', function() {
 console.log("Data has been updated on the server");
 if (typeof dataCallback === "function") {
 dataCallback();
 dataCallback = null;
 }
});
 
export function sendPreview(time = 0, callback) {
 socket.send(JSON.stringify({
 command: "preview",
 time
 }))
 refreshCallback = callback;
}
 
export function updateData(data, callback) {
 socket.emit('update data', JSON.stringify(data))
 dataCallback = callback;
}

Now the game needs to have its own client too. We added a button in the main menu to start the editor.

It leads to this new screen where the game connects to the editor and tells you when you can start using it.

This editor screen exposes a functions that I want the editor to be able to trigger, and has a socket client that is created on this screen.

So in the end, this creates a flow that works fairly smoothly and that looks like this:

Integrating with the game

Now that the editor was made, and properly connected to the game, it needs to be included in the game’s files. This is just a matter of building the editor, building the game, putting the editor in the game, and then start a child process from the game to start the editor.

The issue is that this simple action becomes very tedious when you publish frequent game and editor updates, as it means pushing a new steam build every single time. What used to take a few minutes now takes almost half an hour. For each update.

We need to automate that!

I had to work with some limitations for this too.

The Linux and Windows build of the game can be done automatically on Windows.

The Mac build of the game can only be done on Mac, because of Apple signing issues.

The editor builder can only properly build for the current OS it’s on.

To solve this, I created three new git repositories: One for the HTML5 build of Rhythmy, one for the editor’s source code and one for the Linux editor build

I then wrote a bash script on Linux that would pull the editor’s source code, build and push the result to git. Hawk could then build for Linux and Windows using his own custom tool and send to Steam.

I also wrote another bash script on Mac that would pull the HTML5 build, use it to export the game to Electron for Mac, it would then pull the editor’s source code, build it and move it inside the game’s app package.

Once that’s done, I can send the Mac build to Steam, and merge with Hawk’s builds.

However, that meant I needed to have 3 laptops next to each other to build the game, one with each OS. No way I could roll with that.

Since all of them were on the same network, I decided to write a bash script on WSL (Windows Subsystem for Linux) that would connect in SSH to both machines, and run the respective scripts.

However that meant I had to start the WSL terminal to do this. So I wrote a powershell script that would start WSL and run that script.

That meant I had to start powershell to run the script. So I wrote a discord selfbot that would listen to a specific command and would run my powershell script.

That meant I had to start the selfbot each time I start my computer, so I wrote a batch script that starts my selfbot, and I added that script as a windows service that needs to be ran on startup.

PERFECTION

That's how it's done.

Subscribe

Get emailed when there are new posts!

  • 4 Comments

  • Order by
Want to leave a comment? Login or Register an account!
  • that's pretty cool, thanks for sharing

  • Quality content from skymen. Thanks for sharing!

  • Amazing breakdown of a complex process! Happy you followed through with it after a load of obstacles came up!

  • Love the dev deep cut! Nice work, very interesting how you included it all in one build for nice user experience.