ExcaliburJS 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.
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.
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
};
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?
Read more about Game I/O here.