npm run dev, then navigate to
localhost:8080. If you see a blank white screen with no console errors, you're all set up!
index.tsalready has the scaffolding for a game, so we'll fill it out further:
createPearl. In addition to setting the canvas to use, and its width and height, we define a root component. This component is instantiated when the game starts, and is generally used as an "entry point" into the game. It's attached to a root entity, which can be accessed at
Physicalcomponent, giving it a position, a
BoxCollidercomponent, which creates a rectangular collider, and a
BoxRenderercomponent, which renders the box defined by the
Player, which will be attached to the entity along with the previously-shown components. In a new file, a new component is created:
move(), which gets called on every frame through the
move()reads the currently-pressed keys via the
pearl.inputterAPI, which is available in any component. The x and y velocities are just set to
-1to indicate direction.
translate()method, which moves the entity by a given x and y distance. To give the actual distance to move, the velocities are multiplied by
dt, or delta-time. This is the amount of time, in ms, that have passed since the last frame. This is what allows objects to move smoothly over a variable framerate - e.g., whether your game runs at 30 frames a second or 60 frames a second, as long as you use delta-time as a factor in movement calculations, players will move the same distance over time. This is then multiplied by a
playerSpeedfactor that can be thought of as "pixels per millisecond." Our entity will move at
0.1pixels per millisecond in the direction pushed, or
100pixels a second.
Gamecomponent, we add another entity:
tags- these are strings that can be used to identify types of entities. In a traditional OOP game, you might use
instanceofto determine what kind of object you're looking at - say,
entity instanceof Enemy- but since here, all entities are merely instances of
Entity, we use
tagsto distinguish them. You'll see this in use in the next section.
Collidercomponents for various shapes, it doesn't automatically do anything with them, unlike some fancier frameworks. This is partially so that you have control over handling and resolving collisions - since the way Pac-Man handles collisions is a heck of a lot different than how Mario would - but is also because I haven't come up with a good, magical collision API yet. It might get there eventually!
Playercomponent. We need to check to see if the player has collided with the enemy, and if so, set the player to dead. Back in our player component, we add a new field to the player, and a new placeholder function for checking collisions:
update()to prevent the player from moving, and to skip unnecessary collision detection.
checkCollisions(), we just want to see if the player has collided with the enemy, and then set
isAliveto false if they have:
enemyas a field on
Player, and then set up the reference when creating our entities:
entities.all()becoming a bottleneck, you might want to add some level of caching - especially if you need to do some complex filtering beyond just looking at tags, such as "only get entities in a certain area of the world" - but using
entities.all()is the easiest way to get started.
BoxCollider, which can check against another
BoxCollider, to see if the entities are colliding. If they are, we just set the player to dead. Now, if you refresh the game, you should see the player rendered helplessly immobile after touching the enemy, presumedly because the enemy has eaten or stabbed or done something equally horrendous.
BoxRenderer, Pearl currently doesn't have a drop-in component for displaying text content. That's okay, though, as it's very easy to add.
render()function on it. Traditionally, you'd probably make a new UI component that would probably live in a UI entity, or maybe be a sibling component of your main
Gamecomponent. For simplicity's sake, we'll just add a
render()method to our root
BoxRenderer, and text using canvas drawing instructions. Now, for our sword, let's add a proper sword sprite, drawn by
SpriteRenderercomponent simply renders a single sprite, while the
AnimationManagercan be used to add timed animations and multiple animation states to a component.
assets/sword.png, we'll use Webpack's
url-loader(already pre-configured) and Pearl's built-in assets loader. To start, we add the assets we want to preload to a new
HTMLImageElement) using the
Sprite, which can be passed to a
Player, we can add logic to check collision with the sword, and set a flag to indicate we picked it up:
swordentity would likely have just represented a sword pickup, and once you've collected it, the entity would be removed from the world. However, since this is a tutorial and not a real game, this is a good time to show off one last feature of Pearl. We want to render the player holding the sword, that is, the sword sprite moving along with the player. So let's add the sword as a child entity of the player, and then set its position relative to the player's position:
hit.wav, meant to be played when the player hits the enemy with their sword. Use
pearl.audioAPI to play it at the correct time.