Skip to main content

Happy New Year Excalibur!

ยท 16 min read
Erik Onarheim
Maintainer of Excalibur

We've put together a fireworks demo to commemorate the occasion!

Tap, click, or keypress to add more fireworks:

In 2024 a lot happened for Excalibur! We had 3 big releases v0.28.x, v0.29.x, and v0.30.x! Join the newsletter to make sure you don't miss a thing!

2 new core conributers: Matt Jennings & Justin Young

Thriving discord with lots of cool games being built ๐Ÿ˜

Discord

RECORD number of outside code contributions to Excalibur! Everyone that's commented, made an issue, discussion, or commited to a repo!

Adamduehansen AlexSynchronicity Autsider666 Charkui DaVince DrSensor EduardoHidalgo Evgenii190 JumpLink JustLeif LokiMidgard Nexucis SamuelAsherRivello SimplyFriday WimYedema aruru-weed blakearoberts brunocalou cdelstad cosmicarrow djcsdy dpayne5 egodjuks floAr gavriguy hanekoo herobank110 ikudrickiy isokissa ivasilov jaredegan jellewijma jfelsinger jyoung4242 kamranayub lampewebdev mathieuher mattjennings mcavazotti mogoh mosesintech muhajirdev muirch nilskj robbertstevens romaintailhurat rumansaleem sellmark tarsupin volkans80

We are at 1.9k stars on github and growing, give us a star!

github stars

AND We have GitHub Sponsors & Patreons! Many thanks!

  • 2 x Anonymous
  • Ribsom
  • PlanetCraft
  • AdamE
  • Latanya
  • Feremabraz
  • MRJP-Consulting

Highlightsโ€‹

Development Build & EcmaScript Modulesโ€‹

We now ship a excalibur.development.js, which provides additional debug information about issues that you would probably not want to ship to a production game. This provides a better development experience for our devs helping them find bugs faster.

Additionally we ship both an UMD and ESM bundle in Excalibur for modern bunders, the ESM build drastically reduces bundle sizes for folks.

Quick Startโ€‹

A big boost to productivity is the new Excalibur CLI for quickly scaffolding games in your favorite bundler technology or even vanilla JavaScript! This tool pulls all our open source template repos from github.

sh
npx create-excalibur@latest
sh
npx create-excalibur@latest

This tool was built by contributor Manu Hernandez, big thanks to him for donating his time and building this great tool!

Accomplishments & Cool Stuffโ€‹

You might notice a pattern here but Justin (aka Mookie) has been an MVP this year

Goal Oriented Action Planning by Justin Young

Wave Function Collapse by Justin Young

Dev.to Web Game Challenge Winner Justin Young

Demo Reel Games people are making in Excalibur

Performanceโ€‹

We've really turned the screws on performance in excalibur with v0.30.x being roughly 2-3 times faster than v0.29.x, both in the physics and the graphics! A lot of this was achieved through new data structures, and removing allocations from the hot loop using arena style object pools.

Check out the Excalibur Bunnymark for raw draw performance!

In the physics realm we switched to a new spatial data structure "SparseHashGrid" that seems to yield better performance than our "DynamicAABBTree" previously, especially for large numbers of colliders. Autsider666 really helped dig into this collision performance endeavor, check out his Idle Survivors. We can support many 100s of collisions at once!

Debuggingโ€‹

We have new debug drawing API you can use anywhere in your game to debug.

typescript
ex.Debug.drawRay(ray: Ray, options?: { distance?: number, color?: Color })
ex.Debug.drawBounds(boundingBox: BoundingBox, options?: { color?: Color })
ex.Debug.drawCircle(center: Vector, radius: number, options?: ...)
ex.Debug.drawPolygon(points: Vector[], options?: { color?: Color })
ex.Debug.drawText(text: string, pos: Vector)
ex.Debug.drawLine(start: Vector, end: Vector, options?: LineGraphicsOptions)
ex.Debug.drawLines(points: Vector[], options?: LineGraphicsOptions)
ex.Debug.drawPoint(point: Vector, options?: PointGraphicsOptions)
typescript
ex.Debug.drawRay(ray: Ray, options?: { distance?: number, color?: Color })
ex.Debug.drawBounds(boundingBox: BoundingBox, options?: { color?: Color })
ex.Debug.drawCircle(center: Vector, radius: number, options?: ...)
ex.Debug.drawPolygon(points: Vector[], options?: { color?: Color })
ex.Debug.drawText(text: string, pos: Vector)
ex.Debug.drawLine(start: Vector, end: Vector, options?: LineGraphicsOptions)
ex.Debug.drawLines(points: Vector[], options?: LineGraphicsOptions)
ex.Debug.drawPoint(point: Vector, options?: PointGraphicsOptions)
typescript
onPreUpdate(engine: ex.Engine, elapsedMs: number): void {
this.vel = ex.Vector.Zero;
this.graphics.use('down-idle');
if (engine.input.keyboard.isHeld(ex.Keys.ArrowRight)) { ... }
if (engine.input.keyboard.isHeld(ex.Keys.ArrowLeft)) { ... }
if (engine.input.keyboard.isHeld(ex.Keys.ArrowUp)) { ... }
if (engine.input.keyboard.isHeld(ex.Keys.ArrowDown)) { ... }
ex.Debug.drawRay(
new ex.Ray(this.pos, this.vel),
{ distance: 100, color: ex.Color.Red }
);
}
typescript
onPreUpdate(engine: ex.Engine, elapsedMs: number): void {
this.vel = ex.Vector.Zero;
this.graphics.use('down-idle');
if (engine.input.keyboard.isHeld(ex.Keys.ArrowRight)) { ... }
if (engine.input.keyboard.isHeld(ex.Keys.ArrowLeft)) { ... }
if (engine.input.keyboard.isHeld(ex.Keys.ArrowUp)) { ... }
if (engine.input.keyboard.isHeld(ex.Keys.ArrowDown)) { ... }
ex.Debug.drawRay(
new ex.Ray(this.pos, this.vel),
{ distance: 100, color: ex.Color.Red }
);
}

debug draw api

We now have a chrome & firefox extension to help debug your games in the browser. (This is a great place to contribute to open source! We have a LONG wish list for the plugin)

Big thanks to Adam Due Hansen for adding recent fixes that allow debug settings to be saved across browser refresh!

ECS (Entity Component System)โ€‹

Simplified Custom System implementations

  • Systems are passed an ECS world as a default constructor
  • Systems can use any number of queries
typescript
import * as ex from 'excalibur';
export class MySystem extends ex.System {
static priority = ex.SystemPriority.Average;
public readonly systemType = ex.SystemType.Draw;
query: Query<typeof ex.TransformComponent | typeof ex.GraphicsComponent>;
constructor(public world: ex.World) {
super();
this.query = this.world.query([ex.TransformComponent, ex.GraphicsComponent]);
}
public update(elapsed: number): void {
// TODO implement system using query(s)
}
}
typescript
import * as ex from 'excalibur';
export class MySystem extends ex.System {
static priority = ex.SystemPriority.Average;
public readonly systemType = ex.SystemType.Draw;
query: Query<typeof ex.TransformComponent | typeof ex.GraphicsComponent>;
constructor(public world: ex.World) {
super();
this.query = this.world.query([ex.TransformComponent, ex.GraphicsComponent]);
}
public update(elapsed: number): void {
// TODO implement system using query(s)
}
}

New Tag Queries

  • New simplified way to query entities ex.World.query([MyComponentA, MyComponentB])
  • New way to query for tags on entities ex.World.queryTags(['A', 'B'])

Coroutinesโ€‹

These are a powerful tool for doing computation over time, and one of the best examples of that is animation. Coroutines are create for complex behavior and animations over time. They read very linearly for doing complex sequences over time compared to another approach where you might set flags and track timing in a class.

You can do lots of cool things in the body of coroutines in excalibur:

typescript
ex.coroutine(function*() {
...
yield 100; // wait 100ms
yield; // wait to next frame
yield Promsie.resolve(); // wait for promise
yield* ex.coroutine(function* () { ..}); // wait for nested coroutine
});
typescript
ex.coroutine(function*() {
...
yield 100; // wait 100ms
yield; // wait to next frame
yield Promsie.resolve(); // wait for promise
yield* ex.coroutine(function* () { ..}); // wait for nested coroutine
});

coroutine running animations

A more concrete example is in Jelly Jumper, where we use coroutines to handle the complex squash/stretch of player's graphic

typescript
// apply a stretch animation when jumping
if (this.animation.is('jump') && this.oldVel.y >= 0 && this.vel.y < 0) {
ex.coroutine(
this.scene!.engine,
function* (this: Player): ReturnType<ex.CoroutineGenerator> {
const duration = 70
const scaleTo = 1 + 1 * this.FX_SQUISH_AMOUNT
const easing = ex.EasingFunctions.EaseOutCubic
const force = this.vel.y
let elapsed = 0
// stretch player graphic while jumping
while (this.vel.y < force * 0.25) {
elapsed += yield 1
if (elapsed < duration) {
this.squishGraphic(
easing(Math.min(elapsed, duration), 1, scaleTo, duration)
)
}
}
elapsed = 0
// un-stretch player graphic while jumping
while (!this.touching.bottom.size) {
elapsed += yield 1
if (elapsed < duration) {
this.squishGraphic(
easing(Math.min(elapsed, duration), scaleTo, 1, duration * 2)
)
}
}
this.squishGraphic(1)
}.bind(this)
)
}
typescript
// apply a stretch animation when jumping
if (this.animation.is('jump') && this.oldVel.y >= 0 && this.vel.y < 0) {
ex.coroutine(
this.scene!.engine,
function* (this: Player): ReturnType<ex.CoroutineGenerator> {
const duration = 70
const scaleTo = 1 + 1 * this.FX_SQUISH_AMOUNT
const easing = ex.EasingFunctions.EaseOutCubic
const force = this.vel.y
let elapsed = 0
// stretch player graphic while jumping
while (this.vel.y < force * 0.25) {
elapsed += yield 1
if (elapsed < duration) {
this.squishGraphic(
easing(Math.min(elapsed, duration), 1, scaleTo, duration)
)
}
}
elapsed = 0
// un-stretch player graphic while jumping
while (!this.touching.bottom.size) {
elapsed += yield 1
if (elapsed < duration) {
this.squishGraphic(
easing(Math.min(elapsed, duration), scaleTo, 1, duration * 2)
)
}
}
this.squishGraphic(1)
}.bind(this)
)
}

Graphicsโ€‹

Pixel Artโ€‹

First class pixel art art support with custom shaders/settings for the nicest looking pixel art you've ever seen. This removes common shimmering/banding artifacts that are visible when using pixel art with nearest neighbor filtering.

typescript
const engine = new ex.Engine({
pixelArt: true,
...
});
typescript
const engine = new ex.Engine({
pixelArt: true,
...
});

Check out pixelArt: true! Smooth as butter AA but still preserving the pixel art!

Compared with before when using antialiasing: false, you may need to go fullscreen but obvious banding and fat pixels are visible. This is a common visual issue when working with pixel art

The magic is doing subtle subpixel antialiasing along the pixel seams to avoid artifacts by adjusting the UVs and letting bilinear filtering do the hard work for us.

c
// Inigo Quilez pixel art filter
// https://jorenjoestar.github.io/post/pixel_art_filtering/
vec2 uv_iq(in vec2 uv, in vec2 texture_size) {
vec2 pixel = uv * texture_size;
vec2 seam=floor(pixel+.5);
vec2 dudv=fwidth(pixel);
pixel=seam+clamp((pixel-seam)/dudv,-.5,.5);
return pixel/texture_size;
}
c
// Inigo Quilez pixel art filter
// https://jorenjoestar.github.io/post/pixel_art_filtering/
vec2 uv_iq(in vec2 uv, in vec2 texture_size) {
vec2 pixel = uv * texture_size;
vec2 seam=floor(pixel+.5);
vec2 dudv=fwidth(pixel);
pixel=seam+clamp((pixel-seam)/dudv,-.5,.5);
return pixel/texture_size;
}

SVGs as Graphicsโ€‹

Did you know you can use SVG's now as graphics in excalibur?! You can!

You can inline an SVG string if you're really strong in SVG markup, or you want to generate SVG's on the fly!

typescript
const svgImage = ex.ImageSource.fromSvgString(svg`
<svg version="1.1"
id="svg2"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="resize-full.svg" inkscape:version="0.48.4 r9939"
xmlns="http://www.w3.org/2000/svg"
width="800px" height="800px"
viewBox="0 0 1200 1200" enable-background="new 0 0 1200 1200" xml:space="preserve">
<path id="path18934" fill="#000000ff" inkscape:connector-curvature="0" d="M670.312,0l177.246,177.295L606.348,418.506l175.146,175.146
l241.211-241.211L1200,529.688V0H670.312z M418.506,606.348L177.295,847.559L0,670.312V1200h529.688l-177.246-177.295
l241.211-241.211L418.506,606.348z"/>
</svg>
`);
typescript
const svgImage = ex.ImageSource.fromSvgString(svg`
<svg version="1.1"
id="svg2"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="resize-full.svg" inkscape:version="0.48.4 r9939"
xmlns="http://www.w3.org/2000/svg"
width="800px" height="800px"
viewBox="0 0 1200 1200" enable-background="new 0 0 1200 1200" xml:space="preserve">
<path id="path18934" fill="#000000ff" inkscape:connector-curvature="0" d="M670.312,0l177.246,177.295L606.348,418.506l175.146,175.146
l241.211-241.211L1200,529.688V0H670.312z M418.506,606.348L177.295,847.559L0,670.312V1200h529.688l-177.246-177.295
l241.211-241.211L418.506,606.348z"/>
</svg>
`);

Additionally you can load svgs from files

typescript
const svgExternal = new ex.ImageSource('path/to/my.svg');
typescript
const svgExternal = new ex.ImageSource('path/to/my.svg');

Nine Slice Spritesโ€‹

Excalibur now supports Nine Slice Sprites! Thanks Justin!

Check out the demo app to play around with this feature!

Useful for creating bits of UI, or for tiling areas with a boarder!

typescript
const nineSlice = new ex.NineSlice({
width: 300,
height: 100,
source: inputImageSource,
sourceConfig: {
width: 64,
height: 64,
topMargin: 5,
leftMargin: 7,
bottomMargin: 5,
rightMargin: 7
},
destinationConfig: {
drawCenter: true,
horizontalStretch: ex.NineSliceStretch.Stretch,
verticalStretch: ex.NineSliceStretch.Stretch
}
});
actor.graphics.add(nineSlice);
typescript
const nineSlice = new ex.NineSlice({
width: 300,
height: 100,
source: inputImageSource,
sourceConfig: {
width: 64,
height: 64,
topMargin: 5,
leftMargin: 7,
bottomMargin: 5,
rightMargin: 7
},
destinationConfig: {
drawCenter: true,
horizontalStretch: ex.NineSliceStretch.Stretch,
verticalStretch: ex.NineSliceStretch.Stretch
}
});
actor.graphics.add(nineSlice);

Tiling Sprites & Animationsโ€‹

We now have convenient and performant tiling support for sprites and animations! You can specify you wrapping mode in x/y. If one of the dimensions exceeds the intrinsic width/height of the original image it can be Clamped (which stretches the last pixel), Repeat (which repeats the pixels from left to right), or RepeatMirror (which repeats the pixels from the last edge like a mirror).

typescript
const tiledGroundSprite = new ex.TiledSprite({
image: groundImage,
width: game.screen.width,
height: 200,
wrapping: {
x: ex.ImageWrapping.Repeat,
y: ex.ImageWrapping.Clamp
}
});
typescript
const tiledGroundSprite = new ex.TiledSprite({
image: groundImage,
width: game.screen.width,
height: 200,
wrapping: {
x: ex.ImageWrapping.Repeat,
y: ex.ImageWrapping.Clamp
}
});

This can also be done with animations!

typescript
const tilingAnimation = new ex.TiledAnimation({
animation: cardAnimation,
sourceView: {x: 20, y: 20},
width: 200,
height: 200,
wrapping: ex.ImageWrapping.Repeat
});
typescript
const tilingAnimation = new ex.TiledAnimation({
animation: cardAnimation,
sourceView: {x: 20, y: 20},
width: 200,
height: 200,
wrapping: ex.ImageWrapping.Repeat
});

Bezier Curvesโ€‹

typescript
player.actions.rotateTo({angleRadians: angle, duration: 1000, rotationType});
player.actions.moveTo({pos: ex.vec(100, 100), duration: 1000});
player.actions.scaleTo({scale: ex.vec(2, 2), duration: 1000});
player.actions.repeatForever(ctx => {
ctx.curveTo({
controlPoints: [cp1, cp2, dest],
duration: 5000,
mode: 'uniform'
});
ctx.curveTo({
controlPoints: [cp2, cp1, start1],
duration: 5000,
mode: 'uniform'
});
});
typescript
player.actions.rotateTo({angleRadians: angle, duration: 1000, rotationType});
player.actions.moveTo({pos: ex.vec(100, 100), duration: 1000});
player.actions.scaleTo({scale: ex.vec(2, 2), duration: 1000});
player.actions.repeatForever(ctx => {
ctx.curveTo({
controlPoints: [cp1, cp2, dest],
duration: 5000,
mode: 'uniform'
});
ctx.curveTo({
controlPoints: [cp2, cp1, start1],
duration: 5000,
mode: 'uniform'
});
});

bezier curves

HTML UI with Pixel Conversionโ€‹

Convert from Excalibur "world" pixles to CSS pixels, this is very useful when you want your game HTML to scale with the excalibur canvas and match up.

css
.ui-container {
pointer-events: none;
position: absolute;
transform-origin: 0 0;
transform: scale(
calc(var(--ex-pixel-ratio)),
calc(var(--ex-pixel-ratio)));
}
css
.ui-container {
pointer-events: none;
position: absolute;
transform-origin: 0 0;
transform: scale(
calc(var(--ex-pixel-ratio)),
calc(var(--ex-pixel-ratio)));
}

Here are some example repos of this being employed with HTML UI

GPU Particlesโ€‹

You already saw the fireworks! With GPU particles you can run hundreds of thousands of particles no sweat using instanced rendering with transform feedback under the hood. This means that the entire particle simulation runs on the GPU for maximum speed.

typescript
export class Firework extends Actor {
random: Random;
trail: GpuParticleEmitter;
explosion: GpuParticleEmitter;
explosion2: GpuParticleEmitter;
life: number;
body: BodyComponent;
originalPos: Vector;
inProgress: boolean = false;
constructor(pos: Vector, life: number, random: Random) {
super({ name: "Firework" })
this.random = random;
this.originalPos = pos.clone();
this.pos = pos;
this.acc = vec(0, 800);
this.body = new BodyComponent();
this.life = life;
this.trail = new GpuParticleEmitter({
isEmitting: false,
emitRate: 70,
particle: {
life: 1000,
endColor: Color.White,
beginColor: Color.White,
minSpeed: 10,
maxSpeed: 30,
startSize: 3,
endSize: 0,
fade: true,
acc: vec(0, 50),
}
});
this.explosion = new GpuParticleEmitter({
isEmitting: false,
particle: {
life: 2000,
fade: true,
startSize: 5,
endSize: 2,
minSpeed: 10,
maxSpeed: 200,
acc: vec(0, 100),
beginColor: this.randomColor(),
endColor: this.randomColor()
}
});
this.explosion2 = new GpuParticleEmitter({
isEmitting: false,
particle: {
life: 1000,
fade: true,
startSize: 5,
endSize: 2,
minSpeed: 10,
maxSpeed: 200,
acc: vec(0, 100),
beginColor: this.randomColor(),
endColor: this.randomColor()
}
});
this.addChild(this.trail);
this.addChild(this.explosion);
this.addChild(this.explosion2);
}
private _colors = [
Color.fromHex("#ff0000"),
Color.fromHex("#0078ff"),
Color.fromHex("#ffffff"),
Color.fromHex("#d059c5"),
Color.fromHex("#dff241"),
Color.fromHex("#05ff1c"),
Color.fromHex("#ffdf00"),
Color.fromHex("#3e00f9"),
Color.fromHex("#ff5fc0"),
Color.fromHex("#ff3f3f"),
Color.fromHex("#f66706"),
]
private randomColor(): Color {
return this.random.pickOne(this._colors);
}
launch() {
if (this.inProgress) return;
this.inProgress = true;
this.pos = this.originalPos;
coroutine(this.scene!.engine, (function*(this: Firework) {
this.vel = vec(this.random.floating(-200, 200), this.random.floating(-800, -1000));
this.trail.isEmitting = true;
let hasExploded = false;
let life = this.life;
while (life > 0) {
const elapsed = yield;
life -= elapsed;
if (this.vel.y >= 0 && !hasExploded) {
hasExploded = true;
this.trail.isEmitting = false;
this.explosion.emitParticles(500);
this.explosion2.emitParticles(500);
}
}
this.trail.clearParticles();
this.explosion.clearParticles();
this.explosion2.clearParticles();
this.inProgress = false;
} as CoroutineGenerator).bind(this))
}
}
typescript
export class Firework extends Actor {
random: Random;
trail: GpuParticleEmitter;
explosion: GpuParticleEmitter;
explosion2: GpuParticleEmitter;
life: number;
body: BodyComponent;
originalPos: Vector;
inProgress: boolean = false;
constructor(pos: Vector, life: number, random: Random) {
super({ name: "Firework" })
this.random = random;
this.originalPos = pos.clone();
this.pos = pos;
this.acc = vec(0, 800);
this.body = new BodyComponent();
this.life = life;
this.trail = new GpuParticleEmitter({
isEmitting: false,
emitRate: 70,
particle: {
life: 1000,
endColor: Color.White,
beginColor: Color.White,
minSpeed: 10,
maxSpeed: 30,
startSize: 3,
endSize: 0,
fade: true,
acc: vec(0, 50),
}
});
this.explosion = new GpuParticleEmitter({
isEmitting: false,
particle: {
life: 2000,
fade: true,
startSize: 5,
endSize: 2,
minSpeed: 10,
maxSpeed: 200,
acc: vec(0, 100),
beginColor: this.randomColor(),
endColor: this.randomColor()
}
});
this.explosion2 = new GpuParticleEmitter({
isEmitting: false,
particle: {
life: 1000,
fade: true,
startSize: 5,
endSize: 2,
minSpeed: 10,
maxSpeed: 200,
acc: vec(0, 100),
beginColor: this.randomColor(),
endColor: this.randomColor()
}
});
this.addChild(this.trail);
this.addChild(this.explosion);
this.addChild(this.explosion2);
}
private _colors = [
Color.fromHex("#ff0000"),
Color.fromHex("#0078ff"),
Color.fromHex("#ffffff"),
Color.fromHex("#d059c5"),
Color.fromHex("#dff241"),
Color.fromHex("#05ff1c"),
Color.fromHex("#ffdf00"),
Color.fromHex("#3e00f9"),
Color.fromHex("#ff5fc0"),
Color.fromHex("#ff3f3f"),
Color.fromHex("#f66706"),
]
private randomColor(): Color {
return this.random.pickOne(this._colors);
}
launch() {
if (this.inProgress) return;
this.inProgress = true;
this.pos = this.originalPos;
coroutine(this.scene!.engine, (function*(this: Firework) {
this.vel = vec(this.random.floating(-200, 200), this.random.floating(-800, -1000));
this.trail.isEmitting = true;
let hasExploded = false;
let life = this.life;
while (life > 0) {
const elapsed = yield;
life -= elapsed;
if (this.vel.y >= 0 && !hasExploded) {
hasExploded = true;
this.trail.isEmitting = false;
this.explosion.emitParticles(500);
this.explosion2.emitParticles(500);
}
}
this.trail.clearParticles();
this.explosion.clearParticles();
this.explosion2.clearParticles();
this.inProgress = false;
} as CoroutineGenerator).bind(this))
}
}

Materialsโ€‹

Use custom shader code to do neat effects in excalibur, like doing Star Marbles.

You can use the screen texture to do cool things like reflections in the water.

Scene Ergonomicsโ€‹

You can now define scenes up front and get strong typing in addition to addScene(...)

typescript
const game = new ex.Engine({
scenes: {
scene1: {
scene: scene1,
transitions: {
out: new ex.FadeInOut({duration: 1000, direction: 'out', color: ex.Color.Black}),
in: new ex.FadeInOut({duration: 1000, direction: 'in'})
}
},
scene2: {
scene: scene2,
loader: ex.DefaultLoader, // Constructor only option!
transitions: {
out: new ex.FadeInOut({duration: 1000, direction: 'out'}),
in: new ex.FadeInOut({duration: 1000, direction: 'in', color: ex.Color.Black })
}
},
scene3: ex.Scene // Constructor only option!
}
})
// Specify the boot loader & first scene transition from loader
game.start('scene1',
{
inTransition: new ex.FadeInOut({duration: 500, direction: 'in', color: ex.Color.ExcaliburBlue})
loader: boot,
});
typescript
const game = new ex.Engine({
scenes: {
scene1: {
scene: scene1,
transitions: {
out: new ex.FadeInOut({duration: 1000, direction: 'out', color: ex.Color.Black}),
in: new ex.FadeInOut({duration: 1000, direction: 'in'})
}
},
scene2: {
scene: scene2,
loader: ex.DefaultLoader, // Constructor only option!
transitions: {
out: new ex.FadeInOut({duration: 1000, direction: 'out'}),
in: new ex.FadeInOut({duration: 1000, direction: 'in', color: ex.Color.Black })
}
},
scene3: ex.Scene // Constructor only option!
}
})
// Specify the boot loader & first scene transition from loader
game.start('scene1',
{
inTransition: new ex.FadeInOut({duration: 500, direction: 'in', color: ex.Color.ExcaliburBlue})
loader: boot,
});

Tile Map Pluginsโ€‹

Tiled plugin was completely rewritten and is SO MUCH better, we support nearly every feature of Tiled now!

New LDtk Plugin check out the sample game using the plugin here

New Spritefusion Plugin check out the sample game using the plugin here

Aseprite Pluginโ€‹

Native .aseprite support iterate on your assets whild you build your game.

typescript
import { AsepriteResource } from "@excaliburjs/plugin-aseprite";
const game = new Engine({
width: 600,
height: 400,
displayMode: DisplayMode.FitScreen
});
// Native
const asepriteSpriteSheet = new AsepriteResource('./beetle.aseprite');
// Or JSON export
// const asepriteSpriteSheet = new AsepriteResource('./beetle.json');
const loader = new Loader([asepriteSpriteSheet]);
game.start(loader).then(() => {
const anim = asepriteSpriteSheet.getAnimation('Loop');
const actor = new Actor({pos: vec(100, 100)});
actor.graphics.use(anim);
game.currentScene.add(actor);
});
typescript
import { AsepriteResource } from "@excaliburjs/plugin-aseprite";
const game = new Engine({
width: 600,
height: 400,
displayMode: DisplayMode.FitScreen
});
// Native
const asepriteSpriteSheet = new AsepriteResource('./beetle.aseprite');
// Or JSON export
// const asepriteSpriteSheet = new AsepriteResource('./beetle.json');
const loader = new Loader([asepriteSpriteSheet]);
game.start(loader).then(() => {
const anim = asepriteSpriteSheet.getAnimation('Loop');
const actor = new Actor({pos: vec(100, 100)});
actor.graphics.use(anim);
game.currentScene.add(actor);
});

aseprite

If you're curious about how this was built, we live streamed the process!

Path finding Pluginโ€‹

Justin put together an official path finding plugin that supports both Djikstra & A*!

You can see the plugin in action in this sample, click to move the little guy around.

JSFXR Pluginโ€‹

The JSFXR plugin is super useful for creating extra small sounds programmatically instead of downloading large wav/mp3 files!

shell
npm i @excaliburjs/plugin-jsfxr
shell
npm i @excaliburjs/plugin-jsfxr

This allows you to sepcify the config for the audio, which could be changed on the fly as well.

typescript
import { SoundConfig } from "@excaliburjs/plugin-jsfxr";
export const sounds: { [key: string]: SoundConfig } = {};
// Generate configs https://excaliburjs.com/sample-jsfxr/
sounds["pickup"] = {
oldParams: true,
wave_type: 1,
p_env_attack: 0,
p_env_sustain: 0.02376922019231107,
p_env_punch: 0.552088780864157,
p_env_decay: 0.44573175628456596,
p_base_freq: 0.6823818961421457,
p_freq_limit: 0,
p_freq_ramp: 0,
p_freq_dramp: 0,
p_vib_strength: 0,
p_vib_speed: 0,
p_arp_mod: 0,
p_arp_speed: 0,
p_duty: 0,
p_duty_ramp: 0,
p_repeat_speed: 0,
p_pha_offset: 0,
p_pha_ramp: 0,
p_lpf_freq: 1,
p_lpf_ramp: 0,
p_lpf_resonance: 0,
p_hpf_freq: 0,
p_hpf_ramp: 0,
sound_vol: 0.25,
sample_rate: 44100,
sample_size: 16,
};
let sndPlugin = new JsfxrResource();
sndPlugin.init(); //initializes the JSFXR library
for (const sound in sounds) {
sndPlugin.loadSoundConfig(sound, sounds[sound]);
}
// play
sndPlugin.playSound("pickup");
typescript
import { SoundConfig } from "@excaliburjs/plugin-jsfxr";
export const sounds: { [key: string]: SoundConfig } = {};
// Generate configs https://excaliburjs.com/sample-jsfxr/
sounds["pickup"] = {
oldParams: true,
wave_type: 1,
p_env_attack: 0,
p_env_sustain: 0.02376922019231107,
p_env_punch: 0.552088780864157,
p_env_decay: 0.44573175628456596,
p_base_freq: 0.6823818961421457,
p_freq_limit: 0,
p_freq_ramp: 0,
p_freq_dramp: 0,
p_vib_strength: 0,
p_vib_speed: 0,
p_arp_mod: 0,
p_arp_speed: 0,
p_duty: 0,
p_duty_ramp: 0,
p_repeat_speed: 0,
p_pha_offset: 0,
p_pha_ramp: 0,
p_lpf_freq: 1,
p_lpf_ramp: 0,
p_lpf_resonance: 0,
p_hpf_freq: 0,
p_hpf_ramp: 0,
sound_vol: 0.25,
sample_rate: 44100,
sample_size: 16,
};
let sndPlugin = new JsfxrResource();
sndPlugin.init(); //initializes the JSFXR library
for (const sound in sounds) {
sndPlugin.loadSoundConfig(sound, sounds[sound]);
}
// play
sndPlugin.playSound("pickup");

Example of how to integrate this into your game

New Tutorialโ€‹

We have a new tutorial! It's a new flappy bird clone called "Excalibird".

New High Fidelity Samplesโ€‹

Caliburn Gamesโ€‹

The core contributors around Excalibur have started a business! We are modeling ourselves off of w4games, providing: educational content, support, project development, and console porting services. Excalibur.js and Excalibur Studio will always be open source.

Our plan for 2025 is to release 2 commercial games on various platforms AND offer Nintendo Switch Console publishing!!!

Visit caliburn.games today and start working with us.

Excaliburjs.TVโ€‹

We are planning on releasing a number of free and paid courses through Caliburn Games.

Sign up for more details when they become available https://excaliburjs.tv

Near Futureโ€‹

Sound Managerโ€‹

We are planning on adding a "Sound Manager" to organize sounds into different "tracks" that can be mixed and muted independently. For example sound effects and background music.

Folks have implemented this in almost every game made, so we plan on adding a first party implementation to make this easy.

Collision and Physicsโ€‹

We are planning a number of highly requested improvements to physics and collisions.

  • Continuous Collision Improvements
    • Swept AABB TOI
    • ShapeCasting
  • Surface Velocity on bodies (think conveyor belts)
  • Arcade Friction

Lightingโ€‹

The future versions of Excalibur will have lighting features to really enhance your games. Big thanks to Justin for digging in and figuring out our approach here!

The plan is to add spot lights and point lights, under the hood this uses a technique called raymarching.

Excalibur Studioโ€‹

We are working on an open source visual editor for Excalibur, so that folks can drag and drop, design, code, configure games, and export to various platforms!

We'll be opening up the source code as soon as we have something that works for a vertical slice of game dev.

excalibur studio

v1.0โ€‹

The majority of APIs are now stable and not much will change between now and the v1.0 announcement. Right now we are working on adding a couple more features, light refactoring, and cleaning up some edge case bugs.

We expect to be making a v1.0 announcement soon!