Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

Making Custom Game Engine

In my previous article I covered what's ECS in overall, and what's ECSY, some other tools and potential bottlenecks.

In this article I'll cover some in-depth benchmarks of what I built and play around it a bit.

Behind the scenes, before writing this article, I have quickly rewritten .js code examples to .ts and got rid of ECSY library:

languages@x2

ECSY has neat concept but I figured out I don't like its internals and replaced it with my own ECS implementation. And even though my goal was just to get everything sorted and look good, clean, and typed, - it allowed me to achieve like at least x20 less GPU usage:

frames@x2

I initially got bombuzled because I thought it's all on my CPU now, and I broke all the performance. Of course you can't notice the difference on such a simple example without hacky tricks: it runs smoothly at full 60fps in both cases. But with x6 throttling on CPU, I quickly realized I now have better CPU usage, too:

6x-throttle@x2

So it turns out clean code and types lead to performant applications and games. Who knew?

Now the project structure:

. # root folder with all the config files
src # folder with source code
src/asm # experimental folder with AssemblyScript files that are later compiled into `.wasm` modules
src/components # the first whale of ECS pattern: components
src/elements # `.tsx` React Elements to render UI like buttons and menus
src/entities # second whale of ECS pattern: entities
src/game # the rest files related to game logic, like World.ts
src/helpers # little utility functions
src/main # electron "main process"-related files like background.ts and create-window.ts
src/pages # all possible screens that I'll have in game, at this step is not actively used though
src/systems # finally third whale of ECS pattern: systems

Since components are basically javascript objects for data storage, my whole ECS implementation ended up with those three files:

src/game/Entity.ts # Base class for all entities to inherit
src/game/System.ts # Base class for all systems to inherit
src/game/World.ts # Almost like singleton-class, no inheritance needed

And instead of so-called ECS "Queries", well... Have you heard about such a thing as objects? Hashes? Associative arrays, anyone? Why everything should be stored in classes? So I got rid of such class as "Component" and consecutively I no more need those weird "Queries". Also, I don't store anything in another consuming data structure called "Collections" (basically arrays of objects). Collections are overrated and simply slow. So I use only plain arrays and nested objects, never an object inside an array (except for input arguments but never for storing). Pretty much like React, actually... This way I get beautiful syntax for accessing data out of the box.

So instead of this awful API to extract components:

entity.getComponent(Acceleration).acceleration
entity.getComponent(Position).position
// etc...

I can extract components much simpler now:

entity.props.velocity
entity.props.position
// etc...

And the best part is that they're perfectly typed, so unlike in ECSY, I now have hints for the components and their data:

types@x2

Those hints are based on each particular System, so it can only access those properties related to a system:

const Renderable = { ...Position, ...Shape } // <-- I can only access those components, related to the system

export default class RenderableSystem extends System<typeof Renderable> { ... }

So it turns out I don't need any special structures for querying data: JSON is my API. I guess this is one of the main reasons why my implementation got more lightweight and performant than ECSY framework.

Basically it's all just a web application on steroids, so in my root React component I use a couple React Hooks to do the following.

const world = useMemo(() => {
  if (ctx) {
    const wd = new World({
      systems: [
        /* World initialized with these listed systems */
        /* It is able to fill them up dynamically later */
      ],
      entities: [
        /* Might initialize world with some entities */
        /* Also able to add them dynamically later */
      ],
    })
    return wd
  }
}, [ctx])

And this is how game loop gets started, when world is initialized:

useEffect(() => {
  if (world) {
    console.info('Game loaded!')
    world.run(performance.now(), setFps)
  }
}, [world])

At this point we're basically out of React paradigm, and can make all the logic inside our ECS paradigm.

I can still use React to render whatever UI on top of the game, accessing systems, entities, their components and data. Which is kinda cool: React for user interface menus and buttons, ECS for game loop and continuous game logic handling.

Is ECSY dead?

I heard this question on GitHub, and the answer tends to be "yes", - in addition to all the performance problems and awful syntax it involves, it seems to be unmaintained too.

I initially liked ECSY but quickly realized I like the ECS pattern, not the ECSY library.

Some people still try to adopt it by making things like ecstra, though I'm sceptic about it, since it's still based on ECSY with all its weird queries and component-classes.

Even with custom ECS implementation, I still face the same problem I mentioned in my previous article though: RenderableSystem. It's very naive and still re-draws the whole screen entirely as before, with no optimizations, so it's still the bottleneck of whatever engine I'm coming up with, and still upon my attentive observation. First candidate for replacement.

What's about AssemblyScript?

I currently still have no use to that basically, but I have plans on it, and I'm preparing some ground for it. I made a couple Pull Requests so it can be actually usable to me, with all the conveniences I'm used to. Like TypeScript hints: AssemblyScript#1705, and globs: AssemblyScript#1716. I'm using previously mentioned 1 + 2 = 3 little WASM module, just to make sure it's all working, with my own fork listed in my package.json, before my changes get landed into AssemblyScript:

{
  "devDependencies": {
    "assemblyscript": "https://github.com/jerrygreen/assemblyscript/tarball/jerrygreen"
  }
}

Conclusion

ECS is a neat architecture, but ECSY framework is poorly designed, non-performant, and basically unmaintained, so I came up with writing my own ECS implementation that fits my needs, fortunately the concept is real simple.

Even if I trust the rendering part to some library rather than my custom RenderableSystem class, I might still use the rest of my ECS implementation for calculating game logic, and even move it to server-side, to orchestrate all the world interactions for multiplayer, for example. Fortunately, no any RenderableSystem needed on server, so basically no such bottleneck at all.

I don't have much functionality in my game yet: still just a few flying circles and rectangles. I'm hoping to feature other interesting parts of the project I'm making. But for now...

That's it!

@wuharvey

This comment has been minimized.

Copy link

@wuharvey wuharvey commented Mar 28, 2021

This is real neat! Any plans on open sourcing your ECS implementation? I've been looking for something just like it.

@JerryGreen

This comment has been minimized.

Copy link
Owner Author

@JerryGreen JerryGreen commented Mar 28, 2021

@wuharvey I might consider it in future. Currently I don't look into something like this right now, because I don't want some hugely experimental thing like ECSY that just works on simple examples. If later I'll continue developing a game, and it will turn into something more cool and real-world rather than simple example, I might consider it battle-tested, and think about open-sourcing the engine/framework. But currently, no. I recommend you subscribing, so if anything like that will happen, you will know asap.

@ArrayKnight

This comment has been minimized.

Copy link

@ArrayKnight ArrayKnight commented May 5, 2021

Sounds very interesting. Would you be willing to share your code (even privately?) without publishing it as a package (initially?) so I could draw inspiration for developing my own system?

@JerryGreen

This comment has been minimized.

Copy link
Owner Author

@JerryGreen JerryGreen commented May 14, 2021

@ArrayKnight, sorry for late response, I was just completing my new article:

https://jerrygreen.me/blog/procedural-world-generation

Regarding your Q: I am sorry but I’m not into sharing code right now. I might well be, but I want some commercial product to be made first, which will be using this library, only then make some contributions by open-sourcing some libraries like this one. This may take large large amount of time, because the development process is pretty long time consuming thing in overall, and I’m on it only part time. I already contribute into open source by creating issues and opening PRs here and there, but that’s it for now. I might recommend you subscribing (in subscription form in the end of any article), to receive any updates about it, if there will be, and also be notified about my new other articles, which likely to be interesting for you too, since you found this one interesting :)

@djmisterjon

This comment has been minimized.

Copy link

@djmisterjon djmisterjon commented May 18, 2021

"And instead of so-called ECS "Queries", well... Have you heard about such a thing as objects? Hashes? Associative arrays, anyone? Why everything should be stored in classes? So I got rid of such class as "Component" and consecutively I no more need those weird "Queries". Also, I don't store anything in another consuming data structure called "Collections" (basically arrays of objects). Collections are overrated and simply slow."

Very nice article thanks , i just have one reserve about class for plain data object.
With this you will lose a very powerfull tool for debug memleak or your game engine with devtool
Actualy you can snap memory and search by class name all references and look if the GC do the jobs correctly, or how many instance you have, where and why.
image

edit: you can maybe add a task for production where you transpile with babel all class , but on my side i get good performance.

@JerryGreen

This comment has been minimized.

Copy link
Owner Author

@JerryGreen JerryGreen commented May 19, 2021

@djmisterjon, yeah, debugging, - is probably one of the flaws here, compared to ECSY. I already lost ECSY browser extension when (basically) decided to rewrite ECSY from scratch, even though it seems the extension helps in debugging process a lot... And your notice is probably valid, too.

It still has ways to debug ofc, and it’s reasonably convenient: personally, I have code hot reload built into my engine, so I can just put ‘console.log’ of whatever info I want, and the game almost immediately reloads, firing the ‘console.log’ with all related info (and it’s not like F5, - it preserves most of the state).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment