Skip to main content

excaliburjs logoExcaliburJS Intro

xelly.games games are built using the ExcaliburJS framework. The ExcaliburJS docs are highly recommended, but here's a quick detour:

Engine

As we saw previously, you will not create your own ExcaliburJS Engine instance.

The xelly.games platform will provide you with an Engine instance that is already mounted to an HTML canvas element, in a sandboxed iframe, and given a fixed size appropriate for the user's mobile device and xelly.games feed.

export const install: XellyInstallFunction 
= (context: XellyContext, engine: Engine) => {

// use the provided `engine` to create your game here

}

Users will play your games from differently sized mobile devices. You can find out the dimensions of your game screen via the engine.drawWidth and engine.drawHeight properties.

info

An iPhone XR, for example, may result in a game resolution width of 379px, an Samsung Galaxy S20 Ultra, 394px, and an iPad or Desktop browser, limited at 412px ("CSS pixels").

Using the utility tool and your browser's device simulator, you can test your game on different device sizes. (Alternatively, npm run watch:live provides options to run your game with different game resolution widths.)

When you upload and/or test your game you can also select an aspect ratio of default (4:3) or tall (≈ 4:5.2, or 1.75x taller than default). Some games, like tetris, for example, benefit from more vertical real estate.

Read more about this topic at Device Dimensions.

tip

It is common to use engine.drawWidth and engine.drawHeight to layout your game dynamically based on the user's game screen resolution.

Actors

The easiest and most common way to get something onto the screen is to use an Actor.

export const install: XellyInstallFunction 
= (context: XellyContext, engine: Engine) => {

engine.add(new Actor({
anchor: Vector.Half,
radius: 50,
color: Color.Purple,
pos: vec(engine.drawWidth / 2, engine.drawHeight / 2)
}));

}

This gets us a purple ball in the center of our game screen.

The ExcaliburJS Actor type represents more than just the visual thing to draw on the screen — it also encapsulates position, motion (linear and angular velocity and acceleration), collision detection, and input/output — all the cool capabilities you need to make a game.

Graphics

An Actor's graphic determines what is drawn to the screen for that Actor/entity.

We used a nice little "cheat" above to get the Actor's graphic (and collider!) for free — i.e., by specifying a radius and color when we construct the actor, ExcaliburJS automatically creates a Circle graphic (and collider) and sets it on the Actor for us.

When developing games it can be helpful to use this cheat to quickly get something rendering and moving on the screen (as a placeholder, say), but typically we will want to set the Actor's graphic explicitly to something like a Sprite, Animation, or other:

// add some *named* graphics
myActor.graphics.add(myDefaultGraphic);
myActor.graphics.add('open', myOpenGraphic);
myActor.graphics.add('squashed', mySquashedGraphic);
...
// set the current graphics displayed for the actor
myActor.graphics.use('squashed');
...
// set the current graphics to the default graphic
myActor.graphics.use('default');

Motion

It's pretty easy to get our Actor in motion:

export const install: XellyInstallFunction
= (context: XellyContext, engine: Engine) => {

let actor = new Actor({
anchor: Vector.Half,
radius: 50,
color: Color.Purple,
pos: vec(engine.drawWidth / 2, engine.drawHeight / 2)
});

actor.vel = vec(100, 0); // 100 pixels per second to the right

engine.add(actor);
}

This will send our purple ball moving to the right and off the screen.

Collisions

ExcaliburJS has a built-in collision detection system.

To participate in collision detection (and get collision events), our Actor must have a Collider.

The default CollisionType is Passive, which means collision events will be raised on the Actor but the Actor will not be pushed or moved automatically by other Actors.

The geometry that is used for collision detection is determined by the Actor's Collider.

Simple collider geometrics like circles and rectangles are easily established via Actor constructor properties — radius for a circle collider and width and height for a rectangular collider:

let actor = new Actor({
radius: 50,
// if we set a `color`, as well,
// we would automatically get a circle
// *graphic* (as mentioned above)
// color: Color.Purple
});

More complex colliders can be created explicitly, however. See the Colliders documentation.

Input

To have our ball stop when the user taps (or clicks) on it, we can add an event listener:

/** Install. */
export const install: XellyInstallFunction
= (context: XellyContext, engine: Engine) => {

let actor = new Actor({
anchor: Vector.Half,
radius: 50,
color: Color.Purple,
pos: vec(engine.drawWidth / 2, engine.drawHeight / 2)
});

actor.vel = vec(100, 0);

actor.on('pointerdown', (e) => {
actor.vel = Vector.Zero;
});

engine.add(actor);
};

But wait!

Now that we've introduced user input, we have a non-passive game. We now need to indicate that our game is a XellyGameType.Realtime game (via our game's exported metadata):

export const metadata: XellyMetadata = {
type: XellyGameType.Realtime
};
warning

If you don't update your metadata export to set type to XellyGameType.Realtime, you will find that the tap handling doesn't work!

Hey, now, would you look at that?

We've built our first real game!

Let's call it "Stop the Purple Ball Before it Runs Off the Screen". It might not ready to go viral yet, but it's a start, ain't it?

tip

Read more about Game I/O here.