Я всегда хотел создать 3D-игру. У меня просто никогда не было времени и сил изучать тонкости 3D-программирования. Потом понял, что не надо...

Однажды, возясь, я подумал, что, возможно, я мог бы смоделировать 3D-среду с помощью преобразований CSS. Я наткнулся на старую статью о создании трехмерных миров с помощью HTML и CSS.

Я хотел смоделировать мир Minecraft (или хотя бы маленькую его часть). Minecraft — это игра-песочница, в которой вы можете разбивать и размещать блоки. Я хотел такую ​​же функциональность, но с HTML, JavaScript и CSS.

Приходите, и я расскажу, что я узнал, и как это может помочь вам быть более творческими с преобразованиями CSS!

Примечание. Большую часть кода для этого руководства можно найти на Github. Я проверил это в последней версии Chrome. Я не могу обещать, что в других браузерах он будет выглядеть точно так же, но основные концепции универсальны.

Это только половина приключения. Если вы хотите узнать, как сохранить проекты на реальном сервере, ознакомьтесь с дочерним постом PHP Minecraft Mod. Там мы изучаем способы взаимодействия с сервером Minecraft, чтобы управлять им в режиме реального времени и реагировать на действия пользователя.

Вещи, которые мы уже делаем

Я написал свою долю CSS и достаточно хорошо понял его для создания веб-сайтов. Но это понимание основано на предположении, что я буду работать в 2D-пространстве.

Рассмотрим пример:

.tools {
      position: absolute;
      left: 35px;
      top: 25px;
      width: 200px;
      height: 400px;
      z-index: 3;
    }
    .canvas {
      position: absolute;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      z-index: 2;
    }

Здесь у нас есть элемент холста, начинающийся в верхнем левом углу страницы и простирающийся до правого нижнего угла. Кроме того, мы добавляем элемент инструментов. Он начинается на 25px слева и 35px сверху страницы и имеет размеры 200px в ширину и 400px в высоту.

В зависимости от порядка добавления div.tools и div.canvas в разметку вполне возможно, что div.canvasможет перекрывать div.tools. Это за исключением z-index стилей, примененных к каждому.

Вы, вероятно, привыкли думать об элементах, стилизованных таким образом, как о 2D-поверхностях, которые могут накладываться друг на друга. Но это наложение, по сути, является третьим измерением. left, top и z-index также могут быть переименованы в x, y и z. Пока мы предполагаем, что каждый элемент имеет фиксированную глубину 1px, а z-index имеет неявную единицу измерения px, мы уже думаем в трехмерных терминах.

С чем некоторые из нас склонны бороться, так это с концепциями вращения и перемещения в этом третьем измерении…

Теория трансформаций

Переводы CSS дублируют эту знакомую функциональность в API, который выходит за пределы ограничений, наложенных на нас top, left и z-index. Некоторые из наших предыдущих стилей можно заменить переводами:

.tools {
      position: absolute;
      background: green;
      /*
        left: 35px;
        top: 25px;
      */
      transform-origin: 0 0;
      transform: translate(35px, 25px);
      width: 200px;
      height: 400px;
      z-index: 3;
    }

Вместо определения смещений left и top (с предполагаемым исходным положением 0px слева и 0px сверху) мы можем объявить явное происхождение. Мы можем выполнять все виды преобразований этого элемента, для чего используем 0 0 в качестве центра. translate(35px, 25px) перемещает элемент 35px вправо и 25px вниз. Мы можем использовать отрицательные значения для перемещения элемента влево и/или вверх.

Имея возможность определить источник для наших преобразований, мы можем начать делать и другие интересные вещи. Например, мы можем вращать и масштабировать элементы:

transform-origin: center;
    transform: scale(0.5) rotate(45deg);

Каждый элемент начинается с transform-origin по умолчанию из 50% 50% 0, но значение center устанавливает x, y и z в эквивалент 50%. Мы можем масштабировать наш элемент до значения от 0 до 1 и поворачивать его (по часовой стрелке) на градусы или радианы. И мы можем конвертировать между ними с помощью:

  • 45deg = (45 * Math.PI) / 1800.79rad
  • 0.79rad = (0.79 * 180) / Math.PI45deg

Чтобы повернуть элемент против часовой стрелки, нам просто нужно использовать отрицательное значение deg или rad.

Что еще более интересно в этих преобразованиях, так это то, что мы можем использовать их 3D-версии.

Браузеры Evergreen довольно хорошо поддерживают эти стили, хотя для них могут потребоваться префиксы поставщиков. CodePen имеет удобную опцию «autoprefix», но вы можете добавить библиотеки, такие как PostCSS, в свой локальный код, чтобы добиться того же результата.

Первый блок

Давайте начнем создавать наш 3D-мир. Мы начнем с создания пространства для размещения наших блоков. Создайте новый файл с именем index.html:

<!doctype html>
    <html>
      <head>
        <style>
          html, body {
            padding: 0;
            margin: 0;
            width: 100%;
            height: 100%;
          }
          .scene {
            position: absolute;
            left: 50%;
            top: 50%;
            margin: -192px 0 0 -192px;
            width: 384px;
            height: 384px;
            background: rgba(100, 100, 255, 0.2);
            transform: rotateX(60deg) rotateZ(60deg);
            transform-style: preserve-3d;
            transform-origin: 50% 50% 50%;
          }
        </style>
      </head>
      <body>
        <div class="scene"></div>
        <script src="https://code.jquery.com/jquery-3.1.0.slim.min.js"></script>
        <script src="http://ricostacruz.com/jquery.transit/jquery.transit.min.js"></script>
        <script>
          // TODO
        </script>
      </body>
    </html>

Здесь мы растягиваем тело на полную ширину и высоту, сбрасывая отступы на 0px. Затем мы создаем smallishdiv.scene, который будем использовать для хранения различных блоков. Мы используем 50% left и top, а также отрицательное значение left и topmargin (равное половине width и height) для горизонтального и вертикального центрирования. Затем мы слегка наклоняем его (используя трехмерное вращение), чтобы у нас было перспективное представление о том, где будут находиться блоки.

Обратите внимание, как мы определяем transform-style:preserve-3d. Это сделано для того, чтобы дочерними элементами также можно было манипулировать в 3D-пространстве.

Теперь давайте начнем добавлять на сцену форму блока. Нам нужно создать новый файл JavaScript с именем block.js:

"use strict"
    class Block {
      constructor(x, y, z) {
        this.x = x;
        this.y = y;
        this.z = z;
        this.build();
      }
      build() {
        // TODO: build the block
      }
      createFace(type, x, y, z, rx, ry, rz) {
        // TODO: return a block face
      }
      createTexture(type) {
        // TODO: get the texture
      }
    }

Каждый блок должен иметь 6-стороннюю трехмерную форму. Мы можем разбить различные части построения на методы, чтобы (1) построить весь блок, (2) построить каждую поверхность и (3) получить текстуру каждой поверхности.

Каждое из этих поведений (или методов) содержится в классе ES6. Это удобный способ сгруппировать структуры данных и методы, которые с ними работают. Возможно, вам знакома традиционная форма:

function Block(x, y, z) {
      this.x = x;
      this.y = y;
      this.z = z;
      this.build();
    }
    var proto = Block.prototype;
    proto.build = function() {
      // TODO: build the block
    };
    proto.createFace = function(type, x, y, z, rx, ry, rz) {
      // TODO: return a block face
    }
    proto.createTexture = function(type) {
      // TODO: get the texture
    }

Это может выглядеть немного по-другому, но это почти то же самое. В дополнение к более короткому синтаксису классы ES6 также предоставляют ярлыки для расширения прототипов и вызова переопределенных методов. Но я отвлекся…

Работаем снизу вверх:

createFace(type, x, y, z, rx, ry, rz) {
      return $(`<div class="side side-${type}" ></div>`)
        .css({
          transform: `
            translateX(${x}px)
            translateY(${y}px)
            translateZ(${z}px)
            rotateX(${rx}deg)
            rotateY(${ry}deg)
            rotateZ(${rz}deg)
          `,
          background: this.createTexture(type)
        });
    }
    createTexture(type) {
      return `rgba(100, 100, 255, 0.2)`;
    }

Каждая поверхность (или грань) состоит из повернутого и сдвинутого div. Мы не можем сделать элементы толще, чем 1px, но мы можем имитировать глубину, закрывая все отверстия и используя несколько элементов, параллельных друг другу. Мы можем придать блоку иллюзию глубины, даже если он полый.

С этой целью метод createFace принимает набор координат: x, y и z для положения лица. Мы также предоставляем повороты для каждой оси, так что мы можем вызывать createFace с любой конфигурацией, и он будет перемещать и поворачивать лицо именно так, как мы этого хотим.

Давайте построим базовую форму:

build() {
      const size = 64;
      const x = this.x * size;
      const y = this.y * size;
      const z = this.z * size;
      const block = this.block = $(`<div class="block" ></div>`)
        .css({
          transform: `
            translateX(${x}px)
            translateY(${y}px)
            translateZ(${z}px)
          `
        });
      $(`<div class="x-axis" ></div>`)
        .appendTo(block)
        .css({
          transform: `
            rotateX(90deg)
            rotateY(0deg)
            rotateZ(0deg)
          `
        });
      $(`<div class="y-axis" ></div>`)
        .appendTo(block)
        .css({
          transform: `
            rotateX(0deg)
            rotateY(90deg)
            rotateZ(0deg)
          `
        });
      $(`<div class="z-axis" ></div>`)
        .appendTo(block);
    }

Мы привыкли думать о позициях отдельных пикселей, но такая игра, как Minecraft, работает в большем масштабе. Каждый блок больше, и система координат имеет дело с положением блока, а не отдельными пикселями, из которых он состоит. Я хочу передать ту же мысль здесь…

Когда кто-то создает новый блок с размерами 1 × 2 × 3, я хочу, чтобы это означало 0px × 64px × 128px. Поэтому мы умножаем каждую координату на размер по умолчанию (в данном случае 64px, потому что это размер текстур в пакете текстур, который мы будем использовать).

Затем мы создаем контейнер div (который мы называем div.block). Внутри него размещаем еще 3 div. Они покажут нам ось нашего блока — они как направляющие в программе 3D-рендеринга. Мы также должны добавить новый CSS для нашего блока:

.block {
      position: absolute;
      left: 0;
      top: 0;
      width: 64px;
      height: 64px;
      transform-style: preserve-3d;
      transform-origin: 50% 50% 50%;
    }
    .x-axis,
    .y-axis,
    .z-axis {
      position: absolute;
      left: 0;
      top: 0;
      width: 66px;
      height: 66px;
      transform-origin: 50% 50% 50%;
    }
    .x-axis {
      border: solid 2px rgba(255, 0, 0, 0.3);
    }
    .y-axis {
      border: solid 2px rgba(0, 255, 0, 0.3);
    }
    .z-axis {
      border: solid 2px rgba(0, 0, 255, 0.3);
    }

Этот стиль похож на то, что мы видели раньше. Нам нужно не забыть установить transform-style:preserve-3d на .block, чтобы оси визуализировались в их собственном 3D-пространстве. Мы даем каждому свой цвет и делаем их немного больше, чем блок, в котором они содержатся. Это делается для того, чтобы они были видны, даже если у блока есть стороны.

Давайте создадим новый блок и добавим его в div.scene:

let first = new Block(1, 1, 1);
    $(".scene").append(first.block);

Теперь давайте добавим эти лица:

this
      .createFace("top", 0, 0, size / 2, 0, 0, 0)
      .appendTo(block);
    this
      .createFace("side-1", 0, size / 2, 0, 270, 0, 0)
      .appendTo(block);
    this
      .createFace("side-2", size / 2, 0, 0, 0, 90, 0)
      .appendTo(block);
    this
      .createFace("side-3", 0, size / -2, 0, -270, 0, 0)
      .appendTo(block);
    this
      .createFace("side-4", size / -2, 0, 0, 0, -90, 0)
      .appendTo(block);
    this
      .createFace("bottom", 0, 0, size / -2, 0, 180, 0)
      .appendTo(block);

Я нашел этот код методом проб и ошибок (из-за моего ограниченного опыта работы с трехмерной перспективой). Каждый элемент начинается точно с той же позиции, что и элемент div.z-axis. То есть в вертикальном центре div.block и лицом вверх.

Итак, для «верхнего» элемента мне пришлось перевести его «вверх» на половину размера блока, но никак не повернуть. Для «нижнего» элемента мне пришлось повернуть его на 180 градусов (по оси x или y) и переместить вниз на половину размера блока.

Используя аналогичные мысли, я повернул и переместил каждую из оставшихся сторон. Мне также пришлось добавить для них соответствующий CSS:

.side {
      position: absolute;
      left: 0;
      top: 0;
      width: 64px;
      height: 64px;
      backface-visibility: hidden;
      outline: 1px solid rgba(0, 0, 0, 0.3);
    }

Добавление backface-visibility:hidden предотвращает визуализацию «нижней» стороны элементов. Обычно они выглядели бы одинаково (только зеркально) независимо от того, как они были повернуты. Со скрытыми задними гранями визуализируется только «верхняя» сторона. Будьте осторожны при включении: ваши поверхности должны быть правильно повернуты, иначе стороны блока просто исчезнут. Вот почему я задал сторонам 90/270/-90/-270 оборотов.

Давайте сделаем этот блок более реалистичным. Нам нужно создать новый файл с именем block.dirt.js и переопределить метод createTexture:

"use strict"
    const DIRT_TEXTURES = {
      "top": [
        "textures/dirt-top-1.png",
        "textures/dirt-top-2.png",
        "textures/dirt-top-3.png"
      ],
      "side": [
        "textures/dirt-side-1.png",
        "textures/dirt-side-2.png",
        "textures/dirt-side-3.png",
        "textures/dirt-side-4.png",
        "textures/dirt-side-5.png"
      ]
    };
    class Dirt extends Block {
      createTexture(type) {
        if (type === "top" || type === "bottom") {
          const texture = DIRT_TEXTURES.top.random();
          return `url(${texture})`;
        }
        const texture = DIRT_TEXTURES.side.random();
        return `url(${texture})`;
      }
    }
    Block.Dirt = Dirt;

Мы будем использовать популярный пакет текстур под названием Sphax PureBDCraft. Его можно бесплатно загрузить и использовать (при условии, что вы не пытаетесь продать его), и он бывает разных размеров. Я использую версию x64.

Начнем с определения справочной таблицы для текстур сторон и верха блока. Пакет текстур не указывает, какие текстуры следует использовать для нижней части, поэтому мы просто повторно используем верхние текстуры.

Если сторона, которой нужна текстура, является «верхней» или «нижней», то мы получаем случайную текстуру из «верхнего» списка. Метод random не существует, пока мы его не определим:

Array.prototype.random = function() {
      return this[Math.floor(Math.random() * this.length)];
    };

Точно так же, если нам нужна текстура для стороны, мы получаем случайную. Эти текстуры бесшовные, поэтому рандомизация работает в нашу пользу.

Создание сцены

Как сделать это интерактивным? Что ж, неплохо начать со сцены. Мы уже размещали блоки на сцене, так что теперь нам просто нужно включить динамическое размещение!

Для начала мы можем отрендерить плоскую поверхность блоков:

const $scene = $(".scene");
    for (var x = 0; x < 6; x++) {
      for (var y = 0; y < 6; y++) {
        let next = new Block.Dirt(x, y, 0);
        next.block.appendTo($scene);
      }
    }

Отлично, это дает нам плоскую поверхность, на которую мы начинаем добавлять блоки. Теперь давайте подсветим поверхности, когда мы наводим на них курсор:

.block:hover .side {
      outline: 1px solid rgba(0, 255, 0, 0.5);
    }

Однако происходит что-то странное:

Это связано с тем, что поверхности пересекаются друг с другом случайным образом. Нет хорошего способа решить эту проблему, но мы можем предотвратить ее, немного увеличив размеры блоков:

const block = this.block = $(`<div class="block" ></div>`)
      .css({
        transform: `
          translateX(${x}px)
          translateY(${y}px)
          translateZ(${z}px)
          scale(0.99)
        `
      });

Хотя это и улучшает внешний вид, это влияет на производительность по мере увеличения количества блоков в сцене. Будьте осторожны при одновременном масштабировании множества элементов…

Давайте пометим каждую поверхность блоком и типом, которые ему принадлежат:

createFace(type, x, y, z, rx, ry, rz) {
      return $(`<div class="side side-${type}" ></div>`)
        .css({
          transform: `
            translateX(${x}px)
            translateY(${y}px)
            translateZ(${z}px)
            rotateX(${rx}deg)
            rotateY(${ry}deg)
            rotateZ(${rz}deg)
          `,
          background: this.createTexture(type)
        })
        .data("block", this)
        .data("type", type);
    }

Затем, когда мы нажимаем на поверхность, мы можем получить новый набор координат и создать новый блок:

function createCoordinatesFrom(side, x, y, z) {
      if (side == "top") {
        z += 1;
      }
      if (side == "side-1") {
        y += 1;
      }
      if (side == "side-2") {
        x += 1;
      }
      if (side == "side-3") {
        y -= 1;
      }
      if (side == "side-4") {
        x -= 1;
      }
      if (side == "bottom") {
        z -= 1;
      }
      return [x, y, z];
    }
    const $body = $("body");
    $body.on("click", ".side", function(e) {
      const $this = $(this);
      const previous = $this.data("block");
      const coordinates = createCoordinatesFrom(
        $this.data("type"),
        previous.x,
        previous.y,
        previous.z
      );
      const next = new Block.Dirt(...coordinates);
      next.block.appendTo($scene);
    });

У createCoordinatesFrom есть простая, но важная задача. Учитывая тип стороны и координаты блока, к которому она принадлежит, createCoordinatesFrom должна возвращать новый набор координат. Здесь будет размещен новый блок.

Затем мы подключили прослушиватель событий. Он будет запускаться для каждого клика по div.side. При этом мы получаем блок, которому принадлежит сторона, и выводим новый набор координат для следующего блока. Получив их, мы создаем блок и добавляем его к сцене.

Призраки

Было бы полезно увидеть контур блока, который мы собираемся разместить, до того, как мы его разместим. Иногда это называют «показом призрака» того, что мы собираемся сделать.

Код для включения этого очень похож на тот, который мы уже видели:

let ghost = null;
    function removeGhost() {
      if (ghost) {
        ghost.block.remove();
        ghost = null;
      }
    }
    function createGhostAt(x, y, z) {
      const next = new Block.Dirt(x, y, z);
      next.block
        .addClass("ghost")
        .appendTo($scene);
      ghost = next;
    }
    $body.on("mouseenter", ".side", function(e) {
      removeGhost();
      const $this = jQuery(this);
      const previous = $this.data("block");
      const coordinates = createCoordinatesFrom(
        $this.data("type"),
        previous.x,
        previous.y,
        previous.z
      );
      createGhostAt(...coordinates);
    });
    $body.on("mouseleave", ".side", function(e) {
      removeGhost();
    });

Основное отличие состоит в том, что мы поддерживаем один экземпляр блока-призрака. При создании каждого нового, старый удаляется. Это может выиграть от нескольких дополнительных стилей:

.ghost {
      pointer-events: none;
    }
    .ghost .side {
      opacity: 0.6;
      pointer-events: none;
      -webkit-filter: brightness(1.5);
    }

Оставленные активными, события указателя, связанные с элементами призрака, будут противодействовать событиям mouseenter и mouseleave стороны внизу. Поскольку нам не нужно взаимодействовать с элементами-призраками, мы можем отключить эти события указателя.

Изменение перспективы

Чем больше интерактивности мы добавляем, тем сложнее увидеть, что происходит. Кажется, самое время что-то с этим сделать. Было бы здорово, если бы мы могли масштабировать и поворачивать окно просмотра, чтобы лучше видеть, что происходит…

Начнем с зума. Многие интерфейсы (и игры) позволяют масштабировать область просмотра, прокручивая колесико мыши. Разные браузеры по-разному обрабатывают события колесика мыши, поэтому имеет смысл использовать библиотеку абстракций.

Как только это будет установлено, мы можем подключиться к событиям:

let sceneTransformScale = 1;
    $body.on("mousewheel", function(event) {
      if (event.originalEvent.deltaY > 0) {
        sceneTransformScale -= 0.05;
      } else {
        sceneTransformScale += 0.05;
      }
      $scene.css({
        "transform": `
          scaleX(${sceneTransformScale})
          scaleY(${sceneTransformScale})
          scaleZ(${sceneTransformScale})
        `
      });
    });

Теперь мы можем управлять масштабом всей сцены, просто прокручивая колесико мыши. К сожалению, в тот момент, когда мы это делаем, вращения переопределяются. Нам нужно учитывать вращение, так как мы позволяем перетаскивать окно просмотра мышью, чтобы настроить его:

let sceneTransformX = 60;
    let sceneTransformY = 0;
    let sceneTransformZ = 60;
    let sceneTransformScale = 1;
    const changeViewport = function() {
      $scene.css({
        "transform": `
          rotateX(${sceneTransformX}deg)
          rotateY(${sceneTransformY}deg)
          rotateZ(${sceneTransformZ}deg)
          scaleX(${sceneTransformScale})
          scaleY(${sceneTransformScale})
          scaleZ(${sceneTransformScale})
        `
      });
    };

Эта функция будет учитывать не только коэффициент масштабирования сцены, но и коэффициенты поворота по осям x, y и z. Нам также нужно изменить наш прослушиватель событий масштабирования:

$body.on("mousewheel", function(event) {
      if (event.originalEvent.deltaY > 0) {
        sceneTransformScale -= 0.05;
      } else {
        sceneTransformScale += 0.05;
      }
      changeViewport();
    });

Теперь мы можем начать вращать сцену. Нам нужно:

  1. Прослушиватель событий, когда начинается действие перетаскивания.
  2. Слушатель событий, когда мышь движется (во время перетаскивания)
  3. Прослушиватель событий, когда действие перетаскивания останавливается.

Что-то вроде этого должно помочь:

Number.prototype.toInt = String.prototype.toInt = function() {
      return parseInt(this, 10);
    };
    let lastMouseX = null;
    let lastMouseY = null;
    $body.on("mousedown", function(e) {
      lastMouseX = e.clientX / 10;
      lastMouseY = e.clientY / 10;
    });
    $body.on("mousemove", function(e) {
      if (!lastMouseX) {
        return;
      }
      let nextMouseX = e.clientX / 10;
      let nextMouseY = e.clientY / 10;
      if (nextMouseX !== lastMouseX) {
        deltaX = nextMouseX.toInt() - lastMouseX.toInt();
        degrees = sceneTransformZ - deltaX;
        if (degrees > 360) {
            degrees -= 360;
        }
        if (degrees < 0) {
            degrees += 360;
        }
        sceneTransformZ = degrees;
        lastMouseX = nextMouseX;
        changeViewport();
      }
      if (nextMouseY !== lastMouseY) {
        deltaY = nextMouseY.toInt() - lastMouseY.toInt();
        degrees = sceneTransformX - deltaY;
        if (degrees > 360) {
            degrees -= 360;
        }
        if (degrees < 0) {
            degrees += 360;
        }
        sceneTransformX = degrees;
        lastMouseY = nextMouseY;
        changeViewport();
      }
    });
    $body.on("mouseup", function(e) {
      lastMouseX = null;
      lastMouseY = null;
    });

На mousedown мы фиксируем начальные координаты мыши x и y. По мере движения мыши (если кнопка все еще нажата) мы корректируем sceneTransformZ и sceneTransformX в масштабе. Нет ничего плохого в том, чтобы значения превышали 360 градусов или были ниже 0 градусов, но это выглядело бы ужасно, если бы мы хотели отобразить их на экране.

Вычисления внутри прослушивателя событий mousemove могут быть дорогостоящими в вычислительном отношении из-за того, насколько часто может запускаться этот даже прослушиватель. На экране потенциально могут быть миллионы пикселей, и этот прослушиватель может срабатывать, когда мышь перемещается к каждому из них. Вот почему мы выходим раньше, если кнопка мыши не удерживается.

Когда кнопка мыши отпущена, мы сбрасываем lastMouseX и lastMouseY, так что слушатель mousemove прекращает вычисления. Мы могли бы просто очистить lastMouseX, но мне кажется чище, если очистить оба.

К сожалению, событие mousedown может мешать событию click на сторонах блока. Мы можем обойти это, предотвратив всплытие событий:

$scene.on("mousedown", function(e) {
      e.stopPropagation();
    });

Дайте ему вихрь…

Удаление блоков

Давайте завершим эксперимент, добавив возможность удалять блоки. Нам нужно сделать пару тонких, но важных вещей:

  1. Изменить цвет рамки при наведении с зеленого на красный
  2. Отключить призраки блоков

Это будет проще сделать с помощью CSS, если у нас есть класс body, чтобы указать, находимся ли мы в режиме сложения (обычном) или в режиме вычитания:

$body.on("keydown", function(e) {
      if (e.altKey || e.controlKey || e.metaKey) {
        $body.addClass("subtraction");
      }
    });
    $body.on("keyup", function(e) {
      $body.removeClass("subtraction");
    });

При нажатии клавиши-модификатора (alt, control или command) этот код гарантирует, что body имеет subtractionкласс. Это упрощает нацеливание на различные элементы с помощью этого класса:

.subtraction .block:hover .side {
      outline: 1px solid rgba(255, 0, 0, 0.5);
    }
    .subtraction .ghost {
      display: none;
    }

Мы проверяем несколько ключей-модификаторов, поскольку разные операционные системы перехватывают разные модификаторы. Например, altKey и metaKey работают в macOS, а controlKey — в Ubuntu.

Если мы нажмем на блок, когда мы находимся в режиме вычитания, мы должны удалить его:

$body.on("click", ".side", function(e) {
      const $this = $(this);
      const previous = $this.data("block");
      if ($body.hasClass("subtraction")) {
        previous.block.remove();
        previous = null;
      } else {
        const coordinates = createCoordinatesFrom(
          $this.data("type"),
          previous.x,
          previous.y,
          previous.z
        );
        const next = new Block.Dirt(...coordinates);
        next.block.appendTo($scene);
      }
    });

Это тот же прослушиватель событий клика .side, который у нас был раньше, но вместо того, чтобы просто добавлять новые блоки при нажатии на сторону, мы сначала проверяем, находимся ли мы в режиме вычитания. Если да, блок, по которому мы только что щелкнули, удаляется со сцены.

Нам предстоит пройти долгий путь, прежде чем мы поддержим столько блоков и взаимодействий, сколько Minecraft, но это хорошее начало. Более того, нам удалось добиться этого без необходимости изучения передовых 3D-методов. Это нетрадиционное (и творческое) использование преобразований CSS!

Если вы хотите сделать больше с этим кодом, переходите к другой половине этого приключения. Вам не нужно быть экспертом по PHP, чтобы взаимодействовать с серверами Minecraft. И только представьте, какие удивительные вещи вы можете делать с этими знаниями…

Предложить

Unity 5 разместите свой игровой сервер онлайн как профессионал

Освоение 3D-анимации в Unity

Учебный курс JavaScript — 2016

Обновите свой JavaScript до ES6

Учебники по JavaScript: странные детали