Разработка игр на JavaScript

Мирошник Сергей

Разработка игры. Зачем?

С чего начать разработку игры?

  1. Разобраться с game loop и отрисовкой
  2. Научиться обрабатывать пользовательский ввод
  3. Создать прототип основной сцены игры
  4. Добавить остальные сцены игры

Что такое game loop?

          // game loop
          setInterval(() => {
            update();
            render();
          }, 1000 / 60);
        

Особенности браузерного окружения:

Способы реализации game loop в браузере:

Простой и ненадёжный способ:

          requestAnimationFrame(() => {
            angle++;
            render();
            ...
          });
        
Cкорость игры напрямую зависит от количества FPS

Подстраиваемся под частоту кадров:

          let last = performance.now();

          requestAnimationFrame(() => {
            let now = performance.now(),
                dt = now - last;

            angle += dt * 60 / 1000;
            last = now;
            render();
            ...
          });
        
Скорость уже не зависит от производительности, но можно лучше...

Фиксированный шаг между кадрами:

          let dt   = 0,
              step = 1 / 60,
              last = performance.now();

          requestAnimationFrame(() => {
            let now = performance.now();
            dt += (now - last) / 1000;
            while(dt > step) {
              dt -= step;
              angle++;
            }
            last = now;
            render(dt);
            ...
          });
        
Гарантирует постоянный интервал для update()

Почему именно фиксированный timestep?

Зависимость физических движков от FPS:

От частоты вызовов update() зависит итоговый результат симуляции

Проблемы округления:

Округления или большая скорость объектов могут привести к багам

Проблемы округления:

Ничего не меняется...

Реализация fixed timestep:

          let last = performance.now(),
              step = 1 / 60,
              dt = 0,
              now;

          let frame = () => {
            now = performance.now();
            dt = dt + Math.min(1, (now - last) / 1000);
            while(dt > step) {
              dt = dt - step;
              update(step);
            }
            last = now;

            render(dt);
            requestAnimationFrame(frame);
          }

          requestAnimationFrame(frame);
        

Различный FPS для update() и render():

(используется LERP для отрисовки промежуточных кадров)

Линейная интерполяция

what is lerp
          let lerp = (start, finish, time) => {
            return start + (finish - start) * time;
          };
        

Добавляем поддержку slow motion:

          let last = performance.now(),
              fps = 60,
              slomo = 1, // slow motion multiplier
              step = 1 / fps,
              slowStep = slomo * step,
              dt = 0,
              now;

          let frame = () => {
            now = performance.now();
            dt = dt + Math.min(1, (now - last) / 1000);
            while(dt > slowStep) {
              dt = dt - slowStep;
              update(step);
            }
            last = now;

            render(dt / slomo * fps);
            requestAnimationFrame(frame);
          }

          requestAnimationFrame(frame);
        

Slow motion эффект:

(предыдущий пример с поддержкой slow motion)

Производительность кода

Преждевременная оптимизация - зло!

Производительность кода

Обработка пользовательского ввода:

          let inputState = {
            UP: false,
            DOWN: false,
            LEFT: false,
            RIGHT: false,
            ROTATE: false
          };
          
          // ...
          
          let update = (step) => {
            if (inputState.LEFT)   posX--;
            if (inputState.RIGHT)  posX++;
            if (inputState.UP)     posY--;
            if (inputState.DOWN)   posY++;
            if (inputState.ROTATE) angle++;
          };
        

Отличается от web-приложений — cохраняем состояние нажатых клавиш, а учитываем ввод позже в момент вызова update()

Обработка пользовательского ввода:

(W, S, A, D, SPACE)

Внимание!

Не используйте пиксели в логике игры -
логика не должна быть связана с единицами рендеринга

Дополнительные устройства ввода:

Структура игры. Cцены

Каждой сцене свой update() и render()

Код основной сцены:

          class GameScene {
            constructor(game) {
              this.game = game;
              this.angle = 0;
              this.posX = game.canvas.width / 2;
              this.posY = game.canvas.height / 2;
            }
            update(dt) {
              if (this.game.keys['87']) this.posY--; // W
              if (this.game.keys['83']) this.posY++; // S
              if (this.game.keys['65']) this.posX--; // A
              if (this.game.keys['68']) this.posX++; // D
              if (this.game.keys['32']) this.angle++; // SPACE
              if (this.game.keys['27']) this.game.setScene(MenuScene); // Back to menu
            }
            render(dt, ctx, canvas) {
              ...
              ctx.fillStyle = '#0d0';
              ctx.fillRect(posX, posY, rectSize, rectSize);
            }
          }
        

Код переключения сцен:

          class Game {
            constructor() {
              this.setScene(IntroScene);
              this.initInput();
              this.startLoop();
            }
            initInput() {
              this.keys = {};
              document.addEventListener('keydown', e => { this.keys[e.which] = true; });
              document.addEventListener('keyup', e => { this.keys[e.which] = false; });
            }
            setScene(Scene) {
              this.activeScene = new Scene(this);
            }
            update(dt) {
              this.activeScene.update(dt);
            }
            render(dt) {
              this.activeScene.render(dt, this.ctx, this.canvas);
            }
          }
        

Пример реализации нескольких сцен:

(Реализованы 4 сцены: Intro, Menu, Game, Exit)

А как же звук?


          let context = new AudioContext();

          fetch('sounds/music.mp3').then(response => {
            response.arrayBuffer().then(arrayBuffer => {
              context.decodeAudioData(arrayBuffer, buffer => {
                let source = context.createBufferSource();
                source.buffer = buffer;
                source.connect(context.destination);
                source.start(0);
              });
            });
          });
        

Пример работы WebAudio:

Игровые фреймворки

JS Powered:
Compiled:
Нужны ли они для вашей цели?

Соберём все вместе:

Исходники

Итого:

Спасибо!

Вопросы?
Skype:
miroshnik.sergei
GitHub:
inferpse