Getting Started
To begin using the game engine it is recommended you install the command line tool (though not mandatory):
npm install -g lilis-engineOnce that's done there are a variety of demo projects for you to toy around with. To view the list of available demos use the "create" command without additional arguments:
lilis-engine createTo learn more about any of the available demos use the "info" command with the demo name as the first argument, like this:
lilis-engine info topdownOnce you've decided on which demo you'd like to try out you can use the "create" command, with the demo name as the first argument and your project name (no spaces) as the second command:
lilis-engine create topdown my-zelda-gameIf you'd like to view the full list of demos in your web browser as well as view their source code try visiting the examples directory in the engine's source code.
Please note that all of the example projects are made using SolidJS for interactive HTML and Astro as the website framework. While these tools are not mandatory for the game engine to run learning the basic of using them will help you greatly both in understanding the example projects' source code and in building web based games & apps going forwards.
Game Engine Overview
Architecture Overview:
The above flowchart shows how the game engine works. The green lines represent the order that the program executes (a.k.a. the "control flow), and the red lines represent the plugins reading and writing data from the Entity objects in the EntityList that represents the main scene. The plugins typically don't just do static reads from the Entity objects, usually they leverage the abilities of the underlying Jabr objects to provide change listeners so that they can run code only exactly when needed (as opposed to doing static reads every frame which is very slow because they require constant read operations).
We can see a basic example of this in practice by looking at the source code for the level loader demo:
examples/level-loader-demo/src/components/Game.jsximport { onMount, onCleanup, createSignal} from "solid-js"
import { isServer } from 'solid-js/web'
import {createGameCore, Entity, EntityList, RenderSettings, createGameLoop} from 'lilis-engine'
import createPixiRenderer from 'lilis-engine/pixi'
import { LevelLoader } from "lilis-engine"
import createSolidRenderer from 'lilis-engine/solid'
// ...
// ... inside our SolidJS component mount
const renderSettings = RenderSettings({canvas})
const entities = EntityList([])
window.entities = entities
const levelLoader = LevelLoader(entities, {
levelA: {
mount: (_, {entityList})=>{
entityList.addChild(Entity({imageURL: 'chicken by Diarandor.png', x: 0, y: 0, width: 50, height: 50}))
}
},
levelB: {
mount: (_, {entityList})=>{
entityList.addChild(Entity({imageURL: 'warrior.png', x: 0, y: 0, width: 50, height: 50}))
}
}
}, {
defaultLevel: 'levelA'
})
const levelSwitcher = Entity({solid: function LevelSwitcher(){
return <button onClick={()=>{
levelLoader.loadLevel(levelLoader.activeLevel.get().name === "levelA" ? 'levelB' : 'levelA')
}}>Switch Levels</button>
}})
entities.addChild(levelSwitcher)
const pixiRenderer = createPixiRenderer(entities, renderSettings)
renderSettings.solidSetter = setSolidGameContents
const solidRenderer = createSolidRenderer(entities, renderSettings)
const gameCore = createGameCore({plugins:[createGameLoop(), pixiRenderer, levelLoader, solidRenderer]})
await gameCore.mount()
// SolidJS component boilerplate continues belowSo what's happening here? Basically we are assembling our game and our game engine by creating each of our objects and providing them with the context that they need. Entity objects are the basic units of our game scene, and EntityList is our container to hold them. "entities" is our main game scene, so we pass it our plugins so that they can do their jobs. The gameCore just needs to know the list of plugins including at least one plugin to run our game loop. For more about plugins see the "Plugin System" tab below.
Finally we call gameCore.mount() which automatically tells the gameLoop to start running (and the game loop calls the rest of our plugins each frame). Plugins usually need other context on a case-by-case basis, like renderers will usually need a canvas or some other way to render to the screen. To learn more about the basic building blocks of the game engine see the "Core Exports" section below.
Virtual Coordinate Space
The game engine uses a virtual coordinate space that isn't dependant on the screen size. The negative directions are the top and the left, and the positive directions are down and to the right. The center of the screen (a.k.a. the origin) is always at (0,0). Entity object's position and sizing properties (x, y, width, height, etc) use this space to easily position themselves in your world without needing to adjust themselves to the screen dimensions. The virtual screen coordinates typically range from -50 to +50 on each axis for on-screen content though that can vary on a game-by-game basis. Entity objects coordinates also represent their center.
Core Exports
Entity
Jabr Type: StoreAn Entity is an object (a Jabr store specifically) that is usually part of a scene graph. Basically your game is made up of all kinds of objects like characters, bushes, interactive elements like switches. These types of things are each represented by an entity, and the thing that holds them is the EntityList (a.k.a. this game's version of a scene graph).
An Entity may have any number of properties, however it tends to have a few standard set of properties which plugins tend to expect. These include position and sizing properties like x, y (and z for 3d games), and width and height, (and depth for 3d games). Other properties may include renderPriority (which determines the order that things are drawn) or plugin specific methods.
It may also have the .children property. This property contains an array that functions identically to the EntityList, meaning you can have Entity objects nested inside of each other (allowing it to function as both an Entity and an EntityList).
EntityList
Jabr Type: SignalThe EntityList is a Jabr Signal which contains an array of Entity objects. We can read and write the current list of entities by using the .get() and .set() methods. Here is a basic example of using an EntityList
import {EntityList, Entity} from 'lilis-engine'
const entities = EntityList() // Defaults to an empty array
const character = Entity({x: 0, y: 0, width: 5, height: 5, imageURL: '/player.png'})
entities.set([character]) // Add the character to our entity list
console.log(entities.get()) // Now returns an array with a single Entity insideAs you can see, our core library exports really aren't very complicated. Also, because Jabr provides methods to listen to changes in Signal values plugins can automatically listen to our EntityList to know when Entity objects have been added or removed. We can even use it ourselves if we wish, for example:
import {EntityList, Entity} from 'lilis-engine'
const entityList = EntityList()
entityList.addListener(newEntities =>{
console.log(newEntities)
}) // Add a debug listener so we can listen for changes
entityList.set([Entity({x: 12, y: 12})]) // Ta-daa, our debug listener is immediately called with our new entity array.Constantly assigning a new array each time our EntityList's value changes can get annoying. That's why the game engine adds a few helper methods to the EntityList, specifically .addChild, .removeChild, and .hasChild. They abstract away the need to manually do array manipulation. If you've used other game engines this might look familiar:
import {Entity, EntityList} from 'lilis-engine'
const entityList = EntityList()
const character = entityList.addChild(Entity({x: 0, y: 0, width: 5, height: 5, imageURL: '/player.png'}))
console.log(entityList.get())We now have the same entityList value as doing this:
entityList.set([character])Plus if there were other entities on there already we wouldn't have to do this (because setting a new array as the EntityList value overwrites the old array entirely):entityList.set(entityList.get().concat(entity))Plugin System
Adding functionality to the game engine is done through the plugin system. While the game engine has a few core plugins (like the game loop) most plugins are integrations for third party game development libraries.
Official Plugins & Integrations:
| Name | Purpose | Plugin Docs | Import Path | Library Home Page | NPM Dependencies |
|---|---|---|---|---|---|
| Pixi | 2D Renderer | WIP | lilis-engine/pixi | pixijs.com | pixi.js |
| p5 | 2D Renderer | WIP | lilis-engine/p5 | p5js.org | p5 |
| Solid | Interactive HTML | WIP | lilis-engine/solid | solidjs.com | solid-js |
| Matter | Physics Simulation | WIP | lilis-engine/matter | brm.io/matter-js/ | matter-js |
| Pixi-Tiled | Tiled Support for Pixi Plugin | WIP | lilis-engine/pixi-tiled | mapeditor.org | pixi.js, pixi-tiledmap, lilis-engine/pixi |
| Pixi-Tiled-Matter | Physics support for the Pixi-Tiled plugin | WIP | lilis-engine/pixi-tiled-matter | See Above | pixi.js, pixi-tiledmap, lilis-engine/pixi, lilis-engine/pixi-tiled, matter.js |
It's recommended that you learn how to use these plugins by cloning an example project rather than trying to implement them from scratch. The source code for all official plugins can be found here. The game engine is designed to support you making your own plugins.
All instantiated plugins return an object that defines it's methods and properties. The primary methods are "tick" and "render", with tick always being called first. You can change the order that plugins execute by modifying their tickPriority and renderPriority properties which correspond to the similarly named methods. Plugins are sorted by their priorities when being executed, and plugins with the same priority will be executed simultaneously. The default priority value is 0.
