Make a Zelda Fan-Game in 20 Minutes!

Or:

so, first, what's a game look like?

runLoop()

  • Function that runs every 60 times a second
  • In charge of calling update(), which updates the state, draw(), which draws the screen

update(state, dt) -> new state

  • Takes the state of the world from the previous frame, and updates it
  • dt (delta time) - time in milliseconds since the last frame. Used to consistently scale calculations
  • Handle input per frame
  • (redux people: imagine a reducer where the only action creator was a frame tick)

draw(state) -> graphics!

  • Function that takes the state of a frame and calls drawing instructions
  • In our example, uses a canvas context to redraw screen every frame
    • (performance-intensive games may need further optimization, but start simple)

ok, so let's build a game!

our game

npm install -g create-react-app
create-react-app zelda
npm start

make a canvas

<!-- index.html -->
<body>
  <canvas id="game"></canvas>
</body>

// index.js
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');

define state

const initialState = {
  playerHasSword: false,
  ganonIsDead: false,

  player: {
    position: [width / 2, 20],
    facing: 'down',
    width: 16,
    height: 24,
  },

  // ...
};

update, pt 1: handle input

function update(prevState, dt) {
  const newState = {...prevState};

  if (keysDown.has(keyCodes.UP_ARROW)) {
    newState.player.position[1] -= playerSpeed * dt;
    newState.player.facing = 'up';
  }

  if (keysDown.has(keyCodes.DOWN_ARROW)) {
    newState.player.position[1] += playerSpeed * dt;
    newState.player.facing = 'down';
  }

  // (you can probably guess the code for left & right)
  // ...

  return newState;
}

update, pt 2: handle collisions

function update(prevState, dt) {
  // ...

  if (colliding(newState.player, newState.sword)) {
    newState.playerHasSword = true;
  }

  if (colliding(newState.player, newState.ganon)) {
    if (newState.playerHasSword) {
      newState.ganonIsDead = true;
    }
  }
}

update, pt 2: handle collisions

export default function colliding(rect1, rect2) {
  return (
    rect1.position[0] < rect2.position[0] + rect2.width &&
    rect1.position[0] + rect1.width > rect2.position[0] &&
    rect1.position[1] < rect2.position[1] + rect2.height &&
    rect1.height + rect1.position[1] > rect2.position[1]
  );
}

rendering

const images = {
  'zelda-up': require('../sprites/zelda-up.png'),
  // ...
};

function draw(ctx, state) {
  ctx.fillStyle = 'black';
  ctx.fillRect(0, 0, width, height);

  // render player
  const spritePath = images[`zelda-${state.player.facing}`];
  const image = new Image(spritePath);

  ctx.drawImage(
    ctx,
    image,
    state.player.position[0],
    state.player.position[1]
  );

  // ... draw ganon, sword ...
}

a run loop

function runLoop(
  prevState = initialState, prevTime = Date.now()) {

  requestAnimationFrame(() => {
    const currentTime = Date.now();
    const dt = currentTime - prevTime;
    const newState = update(prevState, dt / 1000);
    draw(ctx, newState);
    runLoop(newState, currentTime);
  });

}

runLoop();

and done!

You really don't need a framework

  • Want to make levels? Tiled exports to easy to parse XML
  • Sprites? aseprite or Pixen (both $15) have you covered, TexturePacker can help you efficiently pack & export a JSON atlas for free
  • Physics? `npm install p2.js`

  • Tricky collision detection? `npm install sat`

  • (and so many, many other tools)

...but you should use one if you want...

  • Coquette.js: microframework for beginners that heavily influenced this talk
  • Phaser: popular batteries-included framework (physics, WebGL rendering, animations...)
  • Superpowers: new Unity-inspired TypeScript IDE & framework

...even one without JS

  • Unity: seriously! the HTML5 export is really good now
  • Construct: just launched a beta of a new multiplatform web version
  • Game Maker:  Windows only, but still an extremely powerful tool (and one that goes on sale a lot~)

thanks!

Made with Slides.com