Hello Excalibur
Introduction
Excalibur uses a theater-style metaphor to organize your games. There are Actor
's which can move around and do things in the currently active Scene
. All of that lives in the Engine
container.
Global Namespace vs. Imports
Imports
In this tutorial, we are using "ES Style" imports, but the global ex.
namespace can also be used if you are using a standalone script tag.
ts
// ES Style Importsimport { Actor } from 'excalibur';const actor = new Actor();
ts
// ES Style Importsimport { Actor } from 'excalibur';const actor = new Actor();
Script Tag
If you are using a standalone script, the excalibur types will have a ex.
in front of them.
Excalibur can be used as a script reference, see standalone script for more info.
html
<html><head> </head><body><!-- Include your script at the end of the body tag --><script src="./path/to/my/excalibur-version.js"></script><script src="game.js"></script></body></html>
html
<html><head> </head><body><!-- Include your script at the end of the body tag --><script src="./path/to/my/excalibur-version.js"></script><script src="game.js"></script></body></html>
ts
// game.js// Standalone style 'ex' global exists from an included script.const actor = new ex.Actor();
ts
// game.js// Standalone style 'ex' global exists from an included script.const actor = new ex.Actor();
Hello Excalibur: Building Breakout!
In this example we'll build a simple version of the popular game Breakout. Breakout is a game where you break bricks at the top of the screen using a ball that bounces off a player paddle. You must break all the bricks to win and avoid the ball falling off the bottom of the screen.
The whole code example in this guide is on GitHub if you want to skip to the code.
This example creates the game in a single file for simplicity, we recommend splitting your game into separate files to keep your project more manageable.
Basic Engine Start
Create a script in your project, for example, game.ts
. Excalibur games are built with the Engine container.
It is important to call game.start()
once you are done configuring your game otherwise it won't start!
typescript
// ES style import from Excaliburimport { Engine } from 'excalibur';
typescript
// ES style import from Excaliburimport { Engine } from 'excalibur';
ts
// Create an instance of the engine.// I'm specifying that the game be 800 pixels wide by 600 pixels tall.// If no dimensions are specified the game will fit to the screen.constgame = newEngine ({width : 800,height : 600,});
ts
// Create an instance of the engine.// I'm specifying that the game be 800 pixels wide by 600 pixels tall.// If no dimensions are specified the game will fit to the screen.constgame = newEngine ({width : 800,height : 600,});
ts
// Start the engine to begin the game.game .start ();
ts
// Start the engine to begin the game.game .start ();
Open a browser and view the blank blue screen of goodness.
Creating the paddle with an Actor
Game elements that have a position and are drawn to the screen are called an Actor. Actors are the primary way to draw things to the screen.
Think of actors like you would the actors in a play. If you change scenes different actors might be on stage.
Actors must be added to a scene to be drawn or updated! game.add(actor)
will add an actor to the current scene.
Below we are going to create the paddle Actor for our Breakout game. Actors can be given many parameters such as position, width, and height.
ts
// Create an actor with x position of 150px,// y position of 40px from the bottom of the screen,// width of 200px and a height of 20pxconstpaddle = newActor ({x : 150,y :game .drawHeight - 40,width : 200,height : 20,// Let's give it some color with one of the predefined// color constantscolor :Color .Chartreuse ,});// Make sure the paddle can participate in collisions, by default excalibur actors do not collide with each other// CollisionType.Fixed is like an object with infinite mass, and cannot be moved, but does participate in collision.paddle .body .collisionType =CollisionType .Fixed ;// `game.add` is the same as calling// `game.currentScene.add`game .add (paddle );
ts
// Create an actor with x position of 150px,// y position of 40px from the bottom of the screen,// width of 200px and a height of 20pxconstpaddle = newActor ({x : 150,y :game .drawHeight - 40,width : 200,height : 20,// Let's give it some color with one of the predefined// color constantscolor :Color .Chartreuse ,});// Make sure the paddle can participate in collisions, by default excalibur actors do not collide with each other// CollisionType.Fixed is like an object with infinite mass, and cannot be moved, but does participate in collision.paddle .body .collisionType =CollisionType .Fixed ;// `game.add` is the same as calling// `game.currentScene.add`game .add (paddle );
Open up your favorite browser and you should see something like this:
That’s neat, but this game is way more fun if things move around. Let’s make the paddle follow the mouse around in the x direction. The paddle will be centered on the mouse cursor.
ts
// Add a mouse move listenergame .input .pointers .primary .on ("move", (evt ) => {paddle .pos .x =evt .worldPos .x ;});
ts
// Add a mouse move listenergame .input .pointers .primary .on ("move", (evt ) => {paddle .pos .x =evt .worldPos .x ;});
Important! Actors have a default anchor of (0.5, 0.5) which means their graphics are positioned in their center (not top-left).
Creating the ball with Actors
What’s Breakout without the ball? Excalibur comes pre-built with a physics collision system to help you out with things like balls bouncing off other objects.
To make the ball, we switch the collider to a circle with the useCircleCollider(radius)
helper.
In this case we want to handle the resolution ourselves to emulate the the way Breakout works. We use the ex.CollisionType.Passive
which will send an event that there has been an intersection but will not resolve the positions.
Read more about the different CollisionTypes that Excalibur supports.
ts
// Create a ball at pos (100, 300) to startconstball = newActor ({x : 100,y : 300,// Use a circle collider with radius 10radius : 10,// Set the colorcolor :Color .Red ,});// Start the serve after a secondconstballSpeed =vec (100, 100);setTimeout (() => {// Set the velocity in pixels per secondball .vel =ballSpeed ;}, 1000);// Set the collision Type to passive// This means "tell me when I collide with an emitted event, but don't let excalibur do anything automatically"ball .body .collisionType =CollisionType .Passive ;// Other possible collision types:// "ex.CollisionType.PreventCollision - this means do not participate in any collision notification at all"// "ex.CollisionType.Active - this means participate and let excalibur resolve the positions/velocities of actors after collision"// "ex.CollisionType.Fixed - this means participate, but this object is unmovable"// Add the ball to the current scenegame .add (ball );
ts
// Create a ball at pos (100, 300) to startconstball = newActor ({x : 100,y : 300,// Use a circle collider with radius 10radius : 10,// Set the colorcolor :Color .Red ,});// Start the serve after a secondconstballSpeed =vec (100, 100);setTimeout (() => {// Set the velocity in pixels per secondball .vel =ballSpeed ;}, 1000);// Set the collision Type to passive// This means "tell me when I collide with an emitted event, but don't let excalibur do anything automatically"ball .body .collisionType =CollisionType .Passive ;// Other possible collision types:// "ex.CollisionType.PreventCollision - this means do not participate in any collision notification at all"// "ex.CollisionType.Active - this means participate and let excalibur resolve the positions/velocities of actors after collision"// "ex.CollisionType.Fixed - this means participate, but this object is unmovable"// Add the ball to the current scenegame .add (ball );
The ball is now setup to move at 100 pixels per second down and right. Next we will make the ball bounce off the side of the screen. Let’s take advantage of the postupdate
event.
ts
// Wire up to the postupdate eventball .on ("postupdate", () => {// If the ball collides with the left side// of the screen reverse the x velocityif (ball .pos .x <ball .width / 2) {ball .vel .x =ballSpeed .x ;}// If the ball collides with the right side// of the screen reverse the x velocityif (ball .pos .x +ball .width / 2 >game .drawWidth ) {ball .vel .x =ballSpeed .x * -1;}// If the ball collides with the top// of the screen reverse the y velocityif (ball .pos .y <ball .height / 2) {ball .vel .y =ballSpeed .y ;}});
ts
// Wire up to the postupdate eventball .on ("postupdate", () => {// If the ball collides with the left side// of the screen reverse the x velocityif (ball .pos .x <ball .width / 2) {ball .vel .x =ballSpeed .x ;}// If the ball collides with the right side// of the screen reverse the x velocityif (ball .pos .x +ball .width / 2 >game .drawWidth ) {ball .vel .x =ballSpeed .x * -1;}// If the ball collides with the top// of the screen reverse the y velocityif (ball .pos .y <ball .height / 2) {ball .vel .y =ballSpeed .y ;}});
Creating the bricks with Actors
Breakout needs some bricks to break. To do this we calculate our brick layout and add them to the current scene.
ts
// Build Bricks// Padding between bricksconstpadding = 20; // pxconstxoffset = 65; // x-offsetconstyoffset = 20; // y-offsetconstcolumns = 5;constrows = 3;constbrickColor = [Color .Violet ,Color .Orange ,Color .Yellow ];// Individual brick width with padding factored inconstbrickWidth =game .drawWidth /columns -padding -padding /columns ; // pxconstbrickHeight = 30; // pxconstbricks :Actor [] = [];for (letj = 0;j <rows ;j ++) {for (leti = 0;i <columns ;i ++) {bricks .push (newActor ({x :xoffset +i * (brickWidth +padding ) +padding ,y :yoffset +j * (brickHeight +padding ) +padding ,width :brickWidth ,height :brickHeight ,color :brickColor [j %brickColor .length ],}));}}bricks .forEach (function (brick ) {// Make sure that bricks can participate in collisionsbrick .body .collisionType =CollisionType .Active ;// Add the brick to the current scene to be drawngame .add (brick );});
ts
// Build Bricks// Padding between bricksconstpadding = 20; // pxconstxoffset = 65; // x-offsetconstyoffset = 20; // y-offsetconstcolumns = 5;constrows = 3;constbrickColor = [Color .Violet ,Color .Orange ,Color .Yellow ];// Individual brick width with padding factored inconstbrickWidth =game .drawWidth /columns -padding -padding /columns ; // pxconstbrickHeight = 30; // pxconstbricks :Actor [] = [];for (letj = 0;j <rows ;j ++) {for (leti = 0;i <columns ;i ++) {bricks .push (newActor ({x :xoffset +i * (brickWidth +padding ) +padding ,y :yoffset +j * (brickHeight +padding ) +padding ,width :brickWidth ,height :brickHeight ,color :brickColor [j %brickColor .length ],}));}}bricks .forEach (function (brick ) {// Make sure that bricks can participate in collisionsbrick .body .collisionType =CollisionType .Active ;// Add the brick to the current scene to be drawngame .add (brick );});
When the ball collides with bricks, we want to remove them from the scene. We use the collisionstart
handler to accomplish this. This handler fires when objects first touch, if you want to know every time resolution is completed use postcollision
.
ts
// On collision remove the brick, bounce the ballletcolliding = false;ball .on ("collisionstart", function (ev ) {if (bricks .indexOf (ev .other .owner ) > -1) {// kill removes an actor from the current scene// therefore it will no longer be drawn or updatedev .other .owner .kill ();}// reverse course after any collision// intersections are the direction body A has to move to not be clipping body B// `ev.content.mtv` "minimum translation vector" is a vector `normalize()` will make the length of it 1// `negate()` flips the direction of the vectorvarintersection =ev .contact .mtv .normalize ();// Only reverse direction when the collision starts// Object could be colliding for multiple framesif (!colliding ) {colliding = true;// The largest component of intersection is our axis to flipif (Math .abs (intersection .x ) >Math .abs (intersection .y )) {ball .vel .x *= -1;} else {ball .vel .y *= -1;}}});ball .on ("collisionend", () => {// ball has separated from whatever object it was colliding withcolliding = false;});
ts
// On collision remove the brick, bounce the ballletcolliding = false;ball .on ("collisionstart", function (ev ) {if (bricks .indexOf (ev .other .owner ) > -1) {// kill removes an actor from the current scene// therefore it will no longer be drawn or updatedev .other .owner .kill ();}// reverse course after any collision// intersections are the direction body A has to move to not be clipping body B// `ev.content.mtv` "minimum translation vector" is a vector `normalize()` will make the length of it 1// `negate()` flips the direction of the vectorvarintersection =ev .contact .mtv .normalize ();// Only reverse direction when the collision starts// Object could be colliding for multiple framesif (!colliding ) {colliding = true;// The largest component of intersection is our axis to flipif (Math .abs (intersection .x ) >Math .abs (intersection .y )) {ball .vel .x *= -1;} else {ball .vel .y *= -1;}}});ball .on ("collisionend", () => {// ball has separated from whatever object it was colliding withcolliding = false;});
Finally, if the ball leaves the screen by getting past the paddle, the player loses!
ts
// Loss conditionball .on ("exitviewport", () => {alert ("You lose!");});
ts
// Loss conditionball .on ("exitviewport", () => {alert ("You lose!");});
Congratulations! You have just created your first game in Excalibur! You can download this example here.
Some things you could do on your own to take this sample further
- Add sprite graphics to the paddle, ball, and bricks
- Add a fun background instead of the blue
- Interpolate the paddle position between move events for a smoother look
- Make the ball ricochet differently depending where the paddle hits it
It's time to get introduced to the Engine for more examples. Once you're ready, you can browse the API Reference.