Japanese learning game
A friend (let's call her Jane) and I (among a couple other friends) prototyped a video game back in 2014 with the purpose of teaching Japanese through virtual immersion. We named it Pera Pera. Jane came up with the idea, created all of the graphics, and put together all the animations. I glued it all together with code to make it an interactive experience on desktop and mobile in the browser. At the end of the project we reserved a booth at a local fair to demonstrate our prototype to the public.
Game design
From the start Jane was dreaming up a world where you could immerse yourself in an interactive world to learn Japanese. Taking inspiration from games she had previously enjoyed, combined with some design intuition, she decided she wanted 2D vector graphics to display a variety of environments from an isometric third person camera perspective. You would navigate your Japanese environment by reading Japanese signs and labels, listening to Japanese people speak, and responding in Japanese; proving your Japanese knowledge in various ways as you progress the story.
Technical design
We wanted to make the game work on the web and I had just begun learning about web development through my first professional job.
Backend
Being a static site, the backend was simply a web server that would send the website's files to clients that asked for them.
Frontend
Having very limited experience in the JavaScript world, I didn't know about JavaScript build tools. I'm also convinced that JavaScript tooling was just beginning to rapidly evolve around that time period. However, I knew enough about the web to understand how to build a static site and I heard about how you could use JavaScript to draw to a canvas while reacting in real time to user input; basically all you need to build a game.
So I wrote an HTML page, put a canvas on that page, and specified just over 50 individual <script>
s (no bundlers, no module system). All of the scripts used immediately invoked function expressions (IIFE) to prevent their local declarations from polluting the global scope. In modern module terminology, exporting from these IIFE modules was done by manually attaching exported data to the global scope window
.
// immediately invoke the function
At the beginning of this project I researched a bunch of game engine development techniques and decided to follow a pattern very similar to that which is commonly known as entity component system (ECS).
Services
The browser provides a set of tools for playing audio, displaying graphics, and gathering input. At the bottom of the game engine I built simple abstractions around these three browser capabilities and called them services. Some additional services were created to abstract some of the more logical needs of the engine: physics, user interfaces, scene management, and scheduling.
CreateJS
Graphics, sound, and input are all abstracted into a nice API by the library known as CreateJS. Part of the reason it was chosen is because it had official support from Adobe as a plugin for Adobe Flash (now known as Adobe Animate) that would allow Jane to export her 2D vector animations into a format that could be easily rendered by the game engine.
Entities
One layer above services are entities. An entity might represent something like a character or an item on the ground. The entity class simply behaves as a collection of components.
Components
Components act as simple classes that represent the properties an entity might have. For example a character would have a position in the scene, so one of the character entity's components might be a "position component". A character would also need to be displayed on screen with some kind of graphic, so the character entity would contain a "graphic component".
Scenes
With this particular game it made sense to have multiple entities interacting together in a scene. A scene's role is primarily to contain entities, but also to provide some specialized scene logic. For example an "world scene" would allow the character to walk around, pick up items, and talk with NPCs in a given world definition; and a "dialog scene" would manage the state of a given dialog graph.
Scene stack
In the given scene examples above, you might imagine that you'd be exploring the world within a "world scnee", then begin dialog with an NPC, transitioning to the "dialog scene". The expectation would be that the dialog scene would be displayed as an overlay on top of the graphical contents of the world scene and all inputs would be directed to the dialog scene instead of the world scene. To handle this, the scene service acts as a stack of scenes, where the top-most scene receives all the inputs and is rendered in the foreground; then all other scenes in the stack are rendered from top to bottom, ignoring certain systems like physics (essentially pausing the game). When the dialog graph exits, the dialog scene would pop itself off the scene stack and the world scene would resume from its paused state.
graph TB %% Entities subgraph Stack[Scene Stack] subgraph First["[0] / Top"] Menu[Menu Scene] end subgraph Second["[1]"] Dialog[Dialog Scene] end subgraph Third["[2]"] World[World Scene] end null end %% Relationships First -- next --> Second Second -- next --> Third Third -- next --> null
Scheduler
All of this logic is performed one step at a time in typical game loop fashion: get input, update game logic, render graphics, render audio, repeat. Steps need to occur fast enough that they happen in real time, so one of the services I built is called a scheduler. Its purpose is to run the game loop at a preconfigured rate measured in frames per second (FPS). A large portion of CreateJS was dedicated to rendering graphics and animations, so it provided a convenient method for "ticking" at a given FPS value. That function was invoked, which would trigger the scheduler to step through the game loop.
Game loop
The game loop was simply the scheduler telling every service to take one step forward in time.
Overview
Here's a diagram illustrating a significant subset of these pieces and how they fit together:
graph TB %% Entities Entry subgraph Services Scheduler InputSvc[Input] SceneMgrSvc[Scene Manager] PhysicsSvc[Physics] GraphicsSvc[Graphics] SoundSvc[Sound] end subgraph Scenes WorldScene[World] DialogScene[Dialog] MenuScene[Menu] end subgraph Components GraphicsCmp[Graphics] PhysicsCmp[Physics] end Entity CreateJS %% Relationships Entry -- starts --> Scheduler Scheduler -- steps --> InputSvc Scheduler -- steps --> SceneMgrSvc Scheduler -- steps --> PhysicsSvc Scheduler -- steps --> GraphicsSvc Scheduler -- steps --> SoundSvc CreateJS -- sends input events to --> InputSvc GraphicsSvc -- renders to --> CreateJS SoundSvc -- renders to --> CreateJS SceneMgrSvc -- steps through stack of --> Scenes Scenes -- process one or more --> Entity Entity -- contains one or more --> Components GraphicsSvc -- renders --> GraphicsCmp PhysicsSvc -- processes --> PhysicsCmp
Level editor
Jane was building full levels as 2D vector images on an isometric grid, but we needed some way to annotate the levels with metadata such as walkable regions, character start point, NPC start points, and trigger zones. We weren't aware of any software that would fit our needs out of the box, so I added some logic to the scene loader that look for particularly named layers and interpret their geometry in different ways depending on the layer name.
For example, Jane would edit the graphics in Adobe Flash in one or more visible layers, then edit a mesh of walkable regions in an invisible layer named "walkable". This paired well with our no-build project because we could edit live in Adobe Flash, save the file, then refresh the webpage to play with our changes almost immediately. No need to build a custom level editor or integrate with one of the poorly suited level editors available on the market.
Performance roadblock
Our game didn't perform well from the start, but one day Jane added some complex pattern to a sprite and the caused the game to render at an unplayable framerate. After some profiling and research, I learned that CreateJS was drawing shapes using the canvas API, which was very slow compared to blitting an image to a canvas. It also used keyframes to define shape movements over time, which scales the performance inversely with the framerate. This was all great because it would allow us to render animations at any canvas resolution and time resolution with great clarity.
During this research I stumbled upon a set of functions provided by CreateJS that allowed me to build a cache of any given graphical object. Internally CreateJS would use the given framerate and resolution to render the graphics to a headless canvas, save the image in memory, and use blit that image instead of shape rendering calls. I added code that would create said cache for all graphics on game load and the result was a framerate smoother than we've ever had before.
I was worried it would significantly slow down our page startup time, but the change to startup time was almost imperceptible. Memory consumption was also a concern with a huge cache of high-framerate images being stored in memory, but the game even ran great on modest mobile phone hardware of 2014.
Jane was now free to add as much complexity to her graphics as her heart desired.
Collaboration
Jane and I used Git to track all of our progress and to maintain a distributed backup. I would create code referencing stub assets. She would interate on her assets, committing her changes to the Git repo regularly. I would pull the repo regularly to see how her changes integrated with mine. We worked together in close proximity to frequently share progress, test the integration of our individual work, and brainstorm.
After rapid, initial prototyping we started coming up with lists of things to work on; something like a Kanban board containing prioritized features and bugs. Up until the final minutes before our public demonstration we continued working on tasks from this list. I think I even fixed a bug mid-demo.
Results
It has been roughly 11 years since we worked on this game, but while writing about this project I was surprised to find that it was just as easy to run the game as it was back then: just start a web server at the root of the repo (python -m http.server
) and point your browser to that web server, http://localhost:1111/.
At some point in those 11 years I learned more about frontend development and came to the conclusion that my no-build static website was lame an immature. However, in the last few years I've come to a much greater appreciation of no-build or minimal-build projects because the browser is built to be a backwards compatible platform and more modern build systems like NPM change so often that I wouldn't trust it to be as resilient as simple HTTP, HTML, CSS, and JS. These days on personal projects I tend to prefer this much simpler structure when possible.
Over those years the JavaScript standards have improved such that I think a lot of the original development pain could be alleviated:
-
IIFEs no longer necessary with locally scoped variables in a [JavaScript module].
-
Import order no longer important because [JavaScript module]s guarantee any module's dependencies are loaded.
-
Class definitions now have a dedicated syntax.
// old way MyClass.prototype.myMethod = // new way
Our project ended after our public demonstration, but we received great feedback from event attendees that stopped by our booth. To this day we occasionally express the desire to work on a project together again. I'm not ready to give up on this one, so I'll mark it as "on hold".