Weltkriegsimulator (WKS) is a vertical scrolling shoot 'em up that was created to showcase the possibilities of creating a game using the open source zCanvas library.
You can play the game directly in your browser by navigating here.
WKS is a Javascript project and uses the ES6 module-pattern in vanilla JS. It's deliberately designed to work without a framework or library just to focus on the game code. React and Vue are great, but reactivity is not required when blitting bitmaps from a game loop.
The only used libraries besides the aforementioned zCanvas are TweenMax (easing functions), Handlebars (for HTML templating) and pubsub-js (for messaging).
All source code resides in the ./src/js-directory. The main application logic is split up into:
The controllers are responsible for managing changes in the model and updating the appropriate views. These controllers are:
The views represent each of the screens used in the game. They are managed by their appropriate controllers. Note: for the game elements (the Actors) these are represented by renderers (see zCanvas in WKS below).
The models contain all data and properties. For the game's model (Game.js) this is where the state and contents of the game's "world" are held. Apart from generic world properties determining the game's behaviour and progress, this model also references the Actors. The Actors are instances of all Objects in the game (for instance: the player, bullets, powerups, enemy ships). These share common properties such as coordinates, speed, but have their own custom properties (e.g. energy, weapon damage, etc.). These Actors are defined as ES6 classes as the Object Oriented Pattern works well for inheritance purposes. The remainder of the applications business logic follows the Functional Programming pattern.
Finally, the factories are used to generate data to populate the models. ActionFactory is of interest as it generates the enemy waves and the powerups.
Communication between these models, views and controllers is done using the publish-subscribe pattern where messages are broadcast to subscribed listeners to act upon. As such, different players might be interested in the same message for the same purpose (for instance: a message of GAME_OVER might trigger the GameController to stop the action queue and freeze the game loop, while at the same time the InputController decides to stop listening to input events and the ScreenController to open the high score input screen. "Separate the concerns"!
The zCanvas is used to render the game "world". All other visual components (energy bar, score, menus, etc.) are overlaid in HTML.
The zCanvas is maintained by RenderController. It is this controller that creates the zCanvas, sizes it accordingly and manages its display list. In WKS, there are multiple layers in the world to give the illusion of depth. Each of the games actors are appended to the appropriate according layer.
The visual representation of an Actor is done using a zCanvas zSprite. We refer to these instances as renderers. All zCanvas renderers reside in the ./src/js/view/renderers-directory. A renderers only job is to visually represent the state of the Actor. Game.js knows nothing about renderers, and it doesn't have to, it merely updates the actors in the game world.
On each render cycle (60 times per second) of the zCanvas, zCanvas requests an update() from the Game, subsequently it will draw all renderers onto the screen. Consult the zCanvas wiki to learn about the API.
Note zCanvas is always on-screen, the different screens (e.g. Title, High Scores, etc.) are overlaid on top (see ScreenController.js).
TweenMax is a powerful animation engine by Greensock. Within WKS it is used to perform easing functions on Actors which in turn are visualised as animations. Additionally, TweenMax is also a convenient timing engine, instead of relying on setTimeout() (which fires when the browser tab is suspended and can drift), we use TweenMax.delayedCall() which is rock solid and can pause when the applications tab suspends.
A vertical scrolling shoot 'em up is pure mayhem with a lot of Objects being in use simultaneously, as well a lot of generation / removal of Actors.
For this purpose, this application uses pooling. When a resource is no longer needed (for instance: bullet is out of screen bounds), it is not disposed, but returned to the pool. The next time a new instance of that type (for instance: newly fired bullet) is needed, it is retrieved from the pool and updated to have the appropriate properties (e.g. new position, direction, etc.). This pool is maintained by Game.js.
Apart from game Actors pools, there is also a pool for decorative effects (e.g. explosions which are triggered by Actors, but not actually an Actor by themselves). These are managed by RenderController.js.
In a game a lot of functions are executed upon each iteration of both the A.I. and render loop. Its good to know how the V8 engine deals with garbage collection on used resources. Any allocated variable other than a primitive becomes eligible for garbage collection, meaning it can impact performance when at a sudden time outside of your control, all unused memory is freed while potentially stalling your application.
Create reusable variables outside of your function scopes to prevent these from being deallocated. Instead of creating a lot of lamba (arrow) functions, create a non instance function that receives a reference to the instance object it needs to operate on. Use the language loops (for, while, for of) instead of more expensive Array methods.
Don't go optimizing for the sake of optimizing though. Identify the critical areas in your application but also be reasonable with regards to optimizing at the expense of code clarity and maintainability.
The only requirement on your system is to have Node.js installed. With Node installed, you can resolve all dependencies via command line using:
npm install
You can start the dev mode through the following NPM task:
npm run dev
This mode will launch a local server allowing you to debug the application from your browser through the local URL http://localhost:5173. When you make changes / additions to the source files, a watcher will register the changes, rebuild the application and refresh the browser so you can see the effect of your changes directly.
You can create a production ready package by running:
npm run build