Lili's Game Engine

Getting Started

To get started find a demo that you would like to use, then click "View Source Code" to see the corresponding directory name. Then use the following command in the command line (with the command line located in the directory you would like to create your project in), replacing "example-name" with the name of the example directory you'd like to use as your basis:

npx lilis-engine create example-name destination-dirSo for example if we want to make a flappy bird type game in the "my-first-flappy-game" directory we'd use:npx lilis-engine create spike-vs-space my-first-flappy-game

There are also more example projects available that are not listed in the demo page, you can find them by 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

Flowchart Overview:

Game Core(Holds Everything) Start: Game Loop(also a plugin) EntityList EntityA EntityB EntityC EntityD Plugin List PluginA PluginB PluginC PluginD Game loop Waitsfor next frame(via window.requestAnimationFrame)

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.jsx
import { 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 below

So 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.

Core Exports

Entity

Jabr Type: Store

An 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: Signal

The 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 inside

As 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:

NamePurposePlugin DocsImport PathLibrary Home PageNPM Dependencies
Pixi2D RendererWIPlilis-engine/pixipixijs.compixi.js
p52D RendererWIPlilis-engine/p5p5js.orgp5
SolidInteractive HTMLWIPlilis-engine/solidsolidjs.comsolid-js
MatterPhysics SimulationWIPlilis-engine/matterbrm.io/matter-js/matter-js
Pixi-TiledTiled Support for Pixi PluginWIPlilis-engine/pixi-tiledmapeditor.orgpixi.js, pixi-tiledmap, lilis-engine/pixi
Pixi-Tiled-MatterPhysics support for the Pixi-Tiled pluginWIPlilis-engine/pixi-tiled-matterSee Abovepixi.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.