A really very questionable web game engine.
- Automatic conversion of assets from "raw" formats to multiple target formats.
- Packing of assets for fast loading times (fewer HTTP requests).
- Division of content into runtime-critical and "able-to-fail".
- Code generation from content for compile-time-safe content.
- Persistent caching of build results for fast re-builds.
- Localization-specific substitution of content.
- "Hot reload" for fast iteration.
-
Clone this repository.
This should only need to be done once.
You can do this by opening Visual Studio Code, pressing F1, then entering
clone
and pressing enter to selectGit: Clone
.You will then be prompted for the URL of the repository, then, a place to clone it into. Once it is done, a blue
Open Repository
button will appear in the bottom right. Click on it. -
Press Ctrl+Shift+B and you should see a command-line application start in the terminal at the bottom of Visual Studio Code. Use the arrow keys to navigate this application.
-
The selected game should now be testable in your browser at
http://localhost:5000
unless you selected another port or a non-development build. -
Any changes you make, to code or content, will automatically be reflected there if a development build was selected.
See File structure
for details on adding new or modifying existing games.
Build scripts are included for Travis CI and AWS CodeBuild. These will perform a production build for all localizations of all games, but do nothing else. To deploy the created artifacts, please amend these scripts in your forks.
When using AWS CodeBuild, using S3 build cache is recommended.
Only Ubuntu hosts are supported. Aseprite will be built automatically from source at the last version to be open source (v1.1.7).
The "root" of game code; use /// <reference path="{file}" />
to include other
files.
Describes the game as would be shown in a library of games. For example:
{
"title": "The title of the game.",
"description": "A short description of the game.",
"developer": {
"name": "The name of the developer of the game.",
"url": "https://the-url-of-the-developer.com/which-can/include-paths"
},
"localizations": [{
"localization": "The internal name of the localization, i.e. en-us",
"name": "The name of the localization, i.e. English (US).",
"title": "The localized title of the game.",
"description": "A localized, short description of the game.",
"developer": {
"name": "The localized name of the developer of the game.",
"url": "https://the-localized-url-of-the-developer.com/which-can/include-paths"
}
}]
}
Used as the favicon and launcher image for the localization selection menu. Additionally shown as an icon when pinned to the home screen. Expected to be square.
Used as the favicon and launcher image for a specific localization. Additionally shown as an icon when pinned to the home screen. Expected to be square.
Shown on the localization selection menu to identity a localization. Expected to be square.
Content to be included. The path is used to build a tree (the content
object).
-
kebab-case
is automatically converted tocamelCase
(in directory/file names only). -
Directories are converted into objects.
-
Some file types can include multiple pieces of content inside them. In this case, the file is treated as a directory and the contained content is treated as paths to files within that directory.
-
Directories containing directories and/or files which are sequential numbers will be turned into arrays.
-
As a special case, repeated slashes will be preserved as fonts often need to create content which is named in a way which could be confused for a directory separator.
For example:
as-array/0.png
as-array/1.png
as-array/2.png
as-array/3.png
top-level/mid-level/bottom-level.ase
subPathA/subPathB/subPathC
(frame tag)subPathA/subPathB/subPathD
(frame tag)subPathA/subPathE
(frame tag)preservedSlashesInMiddle///withContent
(frame tag)preservedSlashesInMiddle/\/withContent
(frame tag)preservedSlashesInMiddle////withContent
(frame tag)preservedSlashesInMiddle/\\/withContent
(frame tag)preservedSlashesAtEnd/
(frame tag)preservedSlashesAtEnd\
(frame tag)preservedSlashesAtEnd//
(frame tag)preservedSlashesAtEnd\\
(frame tag)
Would produce:
const content = {
asArray: [sprite, sprite, sprite, sprite],
topLevel: {
midLevel: {
bottomLevel: {
subPathA: {
subPathB: {
subPathC: sprite,
subPathD: sprite
},
subPathE: sprite
},
preservedSlashesInMiddle: {
"/": {
withContent: sprite
},
"\\": {
withContent: sprite
},
"//": {
withContent: sprite
},
"\\\\": {
withContent: sprite
}
},
preservedSlashesAtEnd: {
"/": sprite,
"\\": sprite,
"//": sprite,
"\\\\": sprite
}
}
}
}
}
See Content purposes
for details on what is done with specific file types.
Equivalent to games/{game}/{name}.{purpose}.{extension}
, but only loaded when
building the indicated localization.
Temporary files which are used to connect the Typescript compiler to the
appropriate game and engine implementation and add the imported content to the
global scope. These are where they are to make Visual Studio Code's
Intellisense function; in theory, they should be in the ephemeral
directory.
The files which should be hosted on a HTTP/S server to distribute the game.
Zip archives of the above artifacts; the contents are the localization directories.
Contains temporary resources created and used while building. It can be safely deleted while the build pipeline is not running, though this will make the next build considerably longer.
Contains shared game engine code.
Contains the scripts which perform a build.
Content is imported differently depending upon its "purpose", which is
indicated in its file name as specified in File structure
.
Some content is loaded at startup (the game will not start until it has been loaded) while others are loaded on-demand.
Some content is considered runtime-critical, and the game cannot continue without it. Others may be retried, but the game will continue regardless of whether they have been loaded; any attempts to use them will do nothing.
Some content will be automatically released when it is not referenced. Others will be kept in-memory for as long as the game runs.
Some content is packed into a single file, and therefore, when any piece of that content is loaded, all are loaded.
Purpose | At startup | Can fail | Releasable | One file | Supported extensions |
---|---|---|---|---|---|
data |
✔️ | ❌ | ❌ | ✔️ | json , csv |
sprite |
✔️ | ❌ | ❌ | ✔️ | png ase aseprite |
background |
❌ | ✔️ | ✔️ | ❌ | png ase aseprite |
sound |
❌ | ✔️ | ❌ | ✔️ | wav flac |
song |
❌ | ✔️ | ✔️ | ❌ | wav flac |
Information which is included in the content tree. This is broken down into subpaths, so it can share objects with content from other files.
Precise functionality varies by file type.
JSON files are parsed and included as-is.
CSV files are parsed, then transformed into JSON. For example, the following file:
a,b,c
null,true,false
-234.34,,Hello World
Is equivalent to:
[{
"a": null,
"b": true,
"c": false
}, {
"a": -234.34,
"c": "Hello World"
}]
Additionally, a key-value map may be specified by naming a column "key", as follows:
key,a,b,c
d,null,true,false
e,-234.34,,Hello World
Is equivalent to:
{
"d": {
"a": null,
"b": true,
"c": false
},
"e": {
"a": -234.34,
"c": "Hello World"
}
}
Small, gameplay-critical images. These are packed into an atlas of up to 4096x4096 pixels.
Empty space is automatically trimmed.
Duplicate frames are eliminated.
The center of the untrimmed source image is used as the origin.
If an ase
or aseprite
file contains frame tags, their contained frames are
imported as arrays of sprites, under subpaths named the same as the frame tags.
Large, decorative images which do not affect gameplay.
Empty space is automatically trimmed.
Duplicate frames from the same file are eliminated.
The center of the untrimmed source image is used as the origin.
Use isLoaded()
to ensure that every frame of an animation is loaded before
attempting to display any part of it.
If an ase
or aseprite
file contains frame tags, their contained frames are
imported as arrays of backgrounds, under subpaths named the same as the frame
tags.
Short, "fire-and-forget" samples of sound. They are packed into a single file which is loaded as soon as possible (following user interaction).
Leading and trailing silence is automatically trimmed.
A single looping song can be playing at a time. Leading and trailing silence are trimmed.
initial() video
¦ ^
v ¦
.--> state -> render() <- content
¦ ¦ ¦
¦ v v
'- elapsed() audio
^
¦
input
To allow for "hot reload", all game state must be encapsulated in an object
which is JSON-serializable. This is called state
, and its type must be
declared with a name of State
.
Note that no type checks will be performed; if the contents of State
change
between hot reloads, it is possible that the Typescript constraints will no
longer align with the restored state, in which case it will need to be flushed.
Function | Mutate state | Input | Video | Audio | Save/load |
---|---|---|---|---|---|
initialState |
(returned) | ❌ | ❌ | ❌ | ❌ |
elapsed |
✔️ | ✔️ | ❌ | ✔️ | ✔️ |
render |
❌ | ❌ | ✔️ | ❌ | ❌ |
currentSong |
❌ | ❌ | ❌ | (returned) | ❌ |
Returns the State
to use if there is not one available already.
Mutates the given State
to take the given number of elapsed seconds into
account.
A constant string prefixed onto any local storage saves/loads.
The dimensions of the "virtual display". Note that this only defines the "safe area", and if the user's display is wider or taller than this aspect ratio, extra content may be shown at the top/bottom or left/right sides.
Symbol | Meaning |
---|---|
█ |
Safe zone (always visible) |
░ |
Margin (only visible when target and actual aspect ratio do not match) |
0 |
X 0, Y 0 |
X+ |
X targetWidth , Y 0 |
Y+ |
X 0, Y targetHeight |
Where the display is a perfect match for the specified aspect ratio:
O███████████X+
██████████████
██████████████
Y+████████████
Where the display is wider than the specified aspect ratio:
░░░O███████████X+░░░
░░░██████████████░░░
░░░██████████████░░░
░░░Y+████████████░░░
Where the display is taller than the specified aspect ratio:
░░░░░░░░░░░░░░
O███████████X+
██████████████
██████████████
Y+████████████
░░░░░░░░░░░░░░
Produces video/audio for the given State
.
Selects a piece of content to play as a looping music track for the given
State
, or null
if none should be played.
An object which describes all of the available content; see File structure
for
details on its contents.
Inputs are specified by their location rather than their label. For instance, on a keyboard, WASD will transparently become ZQSD on a French keyboard.
Returns true
when the given input is held down, otherwise, false
.
The number of virtual pixels in the left/right margins.
The number of virtual pixels in the top/bottom margins.
Draws the specified piece of content at the specified location in virtual pixels.
Plays the specified piece of content.
Local storage is used for save/load.
The following keys/values will be created:
{localStoragePrefix}-check
{localStoragePrefix}-quicksave
{localStoragePrefix}-settings
{localStoragePrefix}-game-{name}
Saves the given JSON as the given name. Returns true
to indicate successful
create or overwrite, and false
to indicate that no changes were made.
Loads JSON from the given name. Returns undefined
if it could not be loaded.
Note that no type checks will be performed on the returned data.
Deletes previously saved JSON, by name. Returns true
to indicate that it
definitely no longer exists (it may never have existed), and false
to indicate
that no changes were made.
Defined as true
to indicate that save
/load
/drop
should be possible, and
false
to indicate that it is definitely not possible.
In development builds, errors are logged to the console but the game does not stop. In production builds, the game stops with text describing the error.
In development builds, the game does not pause when focus is lost. This is so that development tools can have focus without the game pausing. In production builds, the game will pause and may release resources so that mobile devices do not kill the tab.
In production builds, WebGL shaders are disposed of as soon as possible. This causes stability problems in a number of WebGL debugging tools, so they are kept attached in development builds.
More time is spent packing assets in production builds to reduce artifact size as much as possible. Development builds, on the other hand, optimize for build performance. For example, duplicate sprite frames are eliminated, PNGs are heavily compressed and JavaScript, HTML and CSS are minified.