Original Idea and Goals
When I was building the original version of the game engine, I set out with a few goals:
- Learn more about the various Web APIs, including
- Input sources such as keyboard, mouse, and gamepads
- Audio APIs
- 2D Graphics with the canvas
- Build some reusable components that can be shared to other projects
- Learn more about software architecture
- Make some interesting demos that I can put on the website
- Enable multiplayer using websockets
I figured that despite many other engines already existing targeting the frontend web browser, I would have a lot to learn from developing my own, and that creating one would help me learn more about those APIs.
With those goals in mind, I set out to create the first prototype over the period of a few days.
The engine itself was created with the idea that a developer would extend some base classes to implement a game, and all logic for things like rendering sprites, playing audio, and handling networking would be included within those classes. While this worked fine at first, I would later come to the conclusion that some changes would need to be made in order to make it easier to reuse those component I built, and also lessen the amount of code that would need to be duplicated.
Networking and Multiplayer
One of the big features I had planned for the engine was to make it easy to add multiplayer support. This type of multiplayer support was originally implemented in a fairly naive way, not taking into account networking latency and the potential for packets to get lost, or clients attempting to hijack the game by sending other updates. It ended up with a system where all clients would send the state of their gameobjects to a server which would cache the data as well as dispatch those updates to other connected clients. A client would have ownership of a gameobject, and would regularly send update messages to the server, which would then distribute those events out to the other connected clients. In addition, the server also supported sending messages directly to a client for communicating events such as keys pressed to update the gameobject owned by another client. Given that the websocket server itself knew nothing of the game, it was expected that one of the clients would act as a master state for the game.
I made a demo for this, however getting it to work was never made particularly ergonomic in terms of the code that had to be written and run. Basically, it used some old art assets I made for a separate game demo and would allow a user to move around a sprite on all connected clients. I wrote 2 versions of this demo; one version would constantly update all state, including the translation of the sprite, and the other would also update the acceleration and rely on the locally-running scripts to handle the translation. As you might expect, the latter version visually looked better given that it was relying on second-order effects for changing its state, however there were still desyncs where the sprite would jump to another place, but the motion was overall more fluid.
I realied a few things at the end of the first development run:
- The code was not ergonomic to work with as a developer
- The different parts of the game engine relied on each other and as a result made it difficult to split up the code
- I might want to use some external dependencies for some parts of the engine
- Running things directly from source was causing weird issues such as some files being imported twice
- I wanted to continue building things on top of the engine
And around that time, I finally found some time and had the energy to work on this again. I decided to tackle some of these issues with the end goal of building somethin that was easier to use and more enjoyable to work on, however this would be a big undertaking and break most features present. Given that I was the only person using this codebase, I saw no harm in moving forward with those changes.
These were my new goals:
- Publish NPM packages for the engine
- Split the code into packagable modules that could potentially be used in other contexts
- Make the code more ergonomic to work with
- Harness the power of Typescript to help fix some problems before they became hard-to-debug issues
- Make it easy to replace my modules with other ones
I began by merging all of the separate feature branches into the master branch, and then split off from there to do the refactor. This also involved refactoring lots of pieces of code into several classes.
Using this architecture should allow developer to also include their own modules, or create modules for 3rd-party packages that might be useful in that context.
Code Style and Splitting
I began by finding where accessory features to the engine could be split off into their own packages. This was an original goal, where I could simply not include the features I didn't need, but was actually not possible with the old architecture of the engine. This ended up with me building this module system to handle importing different engine features, such as input, graphics, and sound, and then moving those modules into separate NPM packages so that a developer can choose which modules to include.
In addition, I also made those modules into proper classes, where the configuration can be passed in as an object, and the module instance simply added to the engine.
Modules and NPM
In splitting those modules, I created NPM packages to house those features, in addition to the asset loader and the code engine itself. This allows a developer to simply download and include only the needed packages and nothing else.
This was one of the larger changes made in the engine. This modified the structure of the engine to allow for reusable components and less boilerplate code. In short, the Modules provide Components that can be used by Entities to add features to the game. This also involved expanding the original scripts to be descended from components.
The modules are designed to contain all of the actual implementation of the features provided by the components, and are called into during initialization and the game loop.
Taking a look at an example, the graphics module contains all of the code necessary to draw a frame to a 2D canvas. This module initializes the canvas, and contains references to all renderable components so that they can be drawn onto the canvas during the game loop so as to help optimize the process.
The Entities are called GameObjects in this engine, and serve as attachment points for components. Entities by themselves don't do much and require Components to provide some functionality.
The Components add functionality to the Entities and provide a way for modules to do work each frame in terms of running the game. Examples of components include renderable components for drawing graphics to the screen, audio sources and listeners, and even custom Scripts, which allow a developer to add extra functionality and interactivity to the game being developed.
Components generally rely on modules to do the actual work for implementing features in the engine, such as in the case of the 2D graphics module where a component may contain instructions on how or where to draw things to the screen, but the graphics module is the one to call back into the components and let that happen.
In working on this codebase, I discovered many bugs that were caused by having the wrong object in the wrong place, which caused many hard-to-debug issues. This was around the same time that i started to use Typescript in a different project and wanted to take the opportunity to learn more about it. While there was a little bit of a learning curve at first in terms of figuring out how to deal with type errors, I've found it to be very useful in finding hard-to-track bugs that would have been a pain to find otherwise, and with the right type definitions, somewhat of an insurance policy against bugs related to passing the wrong type of object into a function and other related issues.
I'm likely going to keep utilizing Typescript in other projects going forwards.
Work in Progress
As mentioned above, this project has largely been refactored to follow a different pattern than it was initially built with. This has resulted in many original features being broken during this transition and also put me in a position where I would need to (still) do a lot of work to get everything reimplemented. I would go so far as to say that this is an entirely new project from the previous iteration despite sharing the same git repo and history (which by the way is definitely worth taking a look at to see the evolution of the engine over the past few years).
What's done so far
I've been focusing on the engine and other core modules so far, including the Asset Loader and
jsge-core package. I also began rewriting and converting the Input Module, however that might need further changes as of the time of writing this. I also created new testing projects to use when developing and converting the old modules, with the end goal being to develop a game while also cleaning up the engine and making it more ergonomic to use.
There are still some important modules that need to be converted to work with the new Module / Entity / Component system as well as be rewritten in Typescript such as the Networking module which used to provide basic multiplayer functionality using Websockets, the Graphics2D module, which while converted to work with the Entity / Component system still needs to be converted to Typescript, and the Audio module which will allow sounds and music to be played in-game.
I anticipate adding some additional functionality to the Networking module to support different transport systems (such as WebRTC) in addition to Websockets, and providing a way to sync game time and gameplay across clients, but getting a working proof-of-concept is a priority.
One thing that's worth mentioning is that projects like this can snowball in complexity and amount of work needing to be done to the point where it's no longer fun to work on. While this isn't necessarily the fault of the project, I feel like this turned into that, and will need a lot of work to finish up some of the loose ends of where it was when I started the conversion to the new architecture.
Given that there are future plans to allow WwebAssembly to call Web APIs directly, I can't say for sure where the future of the project lies since many game engines already support exporting to WASM. To me it's served its purpose in terms of being a learning project for both Web APIs and Software Architecture, and I might continue to develop parts for use in other applications. That being said, I designed this to be modular so feel free to take the parts you like and write something cool with it, or build your own engine using parts of this and other libraries out there.
This project has been a good learning experience for me and, at least in the beginning, a fun project to work on. I'll put up a demo at some point for people to check out and try building with.