Tracking User Input

Tracking and updating the stars was fairly simple—every frame required the same update. Let’s try something more challenging. Our game needs a player, otherwise it’ll be pretty boring. Instead of the same update every time, our player update function needs to figure out what keys are currently being pressed and move the player’s ship around. First, we need to track keyboard state. Open gameState.ts and add the following:

 // Keep track of the state of all movement keys
 let​ keyStatus = {};
 fromEvent(document, ​'keydown'​)
 .subscribe((e: any) => {
  keyStatus[e.keyCode] = ​true​;
 });
 
 fromEvent(document, ​'keyup'​)
 .subscribe((e: any) => {
  keyStatus[e.keyCode] = ​false​;
 });

This keyStatus object tracks the state of the entire keyboard. We create it outside of the GameState class, so that it only needs to be initialized once. Now that we know what keys the player is pressing, it’s time to update the state. Open player.ts and start filling it out with the following:

Joe asks:
Joe asks:
What if the player releases the key halfway through our update step?

The update and render steps still execute as one synchronous unit. There is a gap between them for any other updates to happen. If the user releases the spacebar, the browser makes a note to send an event down the keyup observable chain whenever the event loop clears up. This means that when the update/render steps start executing, we can be sure that the key state stays the same throughout.

 function​ updatePlayerState(gameState: GameState): GameState {
 if​ (gameState.keyStatus[config.controls.left]) {
  gameState.player.x -= config.ship.speed;
  }
 if​ (gameState.keyStatus[config.controls.right]) {
  gameState.player.x += config.ship.speed;
  }

 

 if​ (gameState.player.x < 0) {
  gameState.player.x = 0;
  }
 if​ (gameState.player.x > (config.canvas.width - config.ship.width)) {
  gameState.player.x = (config.canvas.width - config.ship.width);
  }
 return​ gameState;
 }

So far this is pretty simple—if the left key is pressed, move left; if the right key is pressed, move right. If both keys are pressed, move the player to the left and then back to center again. This is slightly inefficient, but harmless in the grand scheme of things. Following that are two checks to make sure the player doesn’t slide off the edge of the screen. Now that the player can move about, let’s make sure they can see the results of their actions. Fill out renderPlayer with a simple image display (but only if they haven’t been hit yet):

 export​ ​function​ renderPlayer(state: GameState) {
 if​ (!state.player.alive) { ​return​; }
  ctx.drawImage(playerImg, state.player.x, state.player.y);
 }

You’ll notice that updatePlayerStatus isn’t exported. That’s because there’s a bit more to do in this file. You need to write a third function, updatePlayer, in player.ts, that just takes an observable stream and maps it past the updatePlayerStatus function we just wrote. This is the actual operator that this file will export.

 export​ ​const​ updatePlayer = (obs: Observable<GameState>) => {
 return​ obs
  .pipe(
  map(updatePlayerState)
  );
 };

You’ll see why this function is separate in the next section. For now, import updatePlayer and renderPlayer into index.ts and add them to the observable backbone. At this point, you should see your ship flying across a starfield and be able to move left and right. Unfortunately, this tranquil spaceflight is about to be interrupted by some aggressive Space Pirates! We need to equip our player with some weapons before they become a rapidly-expanding cloud of vapor.

Building a Complex Operator

Now it’s time to put on our game designer hats and figure out what sort of weapons we should give the player. If we give the player a laser cannon that can fire on every frame, then they’re practically invulnerable. That’s no fun at all. We’ll need to limit how often the player’s laser can fire. We’ll need an observable operator that can take the game state, check to see whether a given condition is true (spacebar is pressed) and whether a given amount of time has passed since the last time it fired. Alas, RxJS doesn’t have this built in—but it does contain the tools for us to build such an operator ourselves. Open util.ts and add the following function:

 export​ ​function​ triggerEvery(
  mapper,
  timeInterval: () => number,
  condition?: (gameState$: GameState) => ​boolean
 ) {
 let​ nextValidTrigger;
 return​ ​function​ (obs$: Observable<GameState>) {
 return​ obs$.pipe(map((gameState: GameState): GameState => {
 if​ (condition && !condition(gameState)) {
 return​ gameState;
  }
 if​ (nextValidTrigger > performance.now()) {
 return​ gameState;
  }
  nextValidTrigger = performance.now() + timeInterval();
 return​ mapper(gameState);
  }));
  };
 }

You’ll notice that you’ve just written another operator! The outer function allows us to customize how and when the lasers fire, while the inner encapsulates the logic and tracks how long it’s been since the last fire. Let’s write some values for the three methods this requires. Open player.ts and add this code:

 let​ playerFire = (gameState: GameState) => {
 let​ availableLaser = gameState.player.lasers.find(l => l.y
  - config.laser.height < 0);
 if​ (!availableLaser) { ​return​ gameState; }
  availableLaser.x = gameState.player.x + (config.ship.width / 2)
  - (config.laser.width / 2);
  availableLaser.y = gameState.player.y;
 return​ gameState;
 };
 let​ fiveHundredMs = () => 500;
 let​ isSpacebar = (gameState: GameState) =>
  gameState.keyStatus[config.controls.fireLaser];

playerFire is our runIfTrue. It finds a laser attached to the player object that isn’t currently on screen (remember, we’re reusing objects instead of constantly creating new ones). If it finds an available laser object, it sets the laser’s position to just in front of the player’s current position. fivehundredms simply returns the number 500. This function will get more exciting when the space pirates come into play. Finally, we have a filtering function that checks to ensure the spacebar is currently pressed.

We’re almost there—the lasers will appear, but nothing’s in charge of updating them. Add a function to handle the laser state updating:

 function​ updatePlayerLasers(gameState: GameState): GameState {
 // Lasers actually move
  gameState.player.lasers
  .forEach(l => {
  l.y -= config.laser.speed;
  });
 return​ gameState;
 }

Now we need to add the firing and updating of the player’s lasers to the updatePlayer method:

 export​ ​const​ updatePlayer = (obs: Observable<GameState>) => {
 return​ obs
  .pipe(
  map(updatePlayerState),
  map(updatePlayerLasers),
  triggerEvery(playerFire, fiveHundredMs, isSpacebar)
  );
 };

Add a new render function in lasers.ts and import the function into the backbone in index.ts:

 export​ ​function​ renderLasers(state: GameState) {
  state.player.lasers
  .filter(l => l.y + config.laser.height > 0)
  .forEach(laser => {
  ctx.fillStyle = ​'#6f4'​;
  ctx.fillRect(laser.x, laser.y, config.laser.width, config.laser.height);
  });
  state.enemy.lasers
  .filter(l => l.y + config.laser.height > 0)
  .forEach(laser => {
  ctx.fillStyle = ​'#f64'​;
  ctx.fillRect(laser.x, laser.y, config.laser.width, config.laser.height);
  });
 }

Now the player can dodge left and right while simultaneously firing their laser. Time to bring out the space pirates.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.212.71