Статьи

3D Tetris с учебником Three.js — Часть 3

Третья часть урока посвящена движущимся блокам. Мы создадим блок, введем некоторые движения и рулевого управления. Пока нас не волнует обнаружение столкновений.

Давайте начнем с резюме. Каков жизненный цикл блока Tetris? Он создается в некоторой фиксированной точке, перемещается и поворачивается игроком, с течением времени он также падает сам по себе, и в самом конце он падает на землю и превращается в статический элемент. Затем создается следующий блок и цикл повторяется. Это описание является более или менее планом методов, которые мы должны иметь в нашем блочном объекте.

подготовка

Сначала создайте новый файл для хранения нашего блочного объекта и включите его в index.html. Файл должен начинаться с:

window.Tetris = window.Tetris  || {}; // equivalent to if(!window.Tetris) window.Tetris = {};

Таким образом, даже если порядок синтаксического анализа файла каким-либо образом нарушен (что очень маловероятно, кстати), вы никогда не будете перезаписывать существующие объекты или использовать неопределенные переменные. На этом этапе вы можете заменить «var Tetris = {};» объявление в нашем главном файле.

Нам нужна одна служебная функция, прежде чем мы продолжим.

 

Tetris.Utils = {};
 
Tetris.Utils.cloneVector = function (v) {
  return {x: v.x, y: v.y, z: v.z};
};

 

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

var a = 5;
var b = a;

 поместит число 5 в b , но это никак не будет связано с a . Однако при использовании объектов:

var a = (x: 5};
var b = a;

 

б является ссылкой на объект. Используя bx = 6; будет писать в тот же объект, на который ссылается a .

Вот почему нам нужен метод для создания копии вектора. Простое v1 = v2 будет означать, что в нашей памяти есть только один вектор. Однако если мы получим прямой доступ к числовым частям вектора и создадим клон, у нас будет два вектора, и манипулирование ими будет независимым.

Последняя подготовка — определение форм.

Tetris.Block = {};
 
Tetris.Block.shapes = [
    [
        {x: 0, y: 0, z: 0},
        {x: 1, y: 0, z: 0},
        {x: 1, y: 1, z: 0},
        {x: 1, y: 2, z: 0}
    ],
    [
        {x: 0, y: 0, z: 0},
        {x: 0, y: 1, z: 0},
        {x: 0, y: 2, z: 0},
    ],
    [
        {x: 0, y: 0, z: 0},
        {x: 0, y: 1, z: 0},
        {x: 1, y: 0, z: 0},
        {x: 1, y: 1, z: 0}
    ],
    [
        {x: 0, y: 0, z: 0},
        {x: 0, y: 1, z: 0},
        {x: 0, y: 2, z: 0},
        {x: 1, y: 1, z: 0}
    ],
    [
        {x: 0, y: 0, z: 0},
        {x: 0, y: 1, z: 0},
        {x: 1, y: 1, z: 0},
        {x: 1, y: 2, z: 0}
    ]
];

 

Обратите внимание, что первый куб каждой фигуры (0,0,0). Это очень важно и будет объяснено в следующем разделе.

Генерация формы

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

По своему опыту я могу сказать, что обнаружение столкновений в играх всегда более или менее поддельно . И я говорю не только о глупых играх JS. Все дело в производительности — геометрии упрощены, в первую очередь исключаются коллизии для определенных ситуаций, некоторые коллизии вообще не рассматриваются, а реакция на коллизию почти всегда не точна. Это не имеет значения — если это выглядит естественно, никто никогда не заметит, и мы сэкономим много драгоценных циклов процессора.

Итак, что такое простейшее обнаружение столкновений для тетриса? Все фигуры представляют собой выровненные по оси кубы с центрами в одной из указанных групп точек. Я на 99% уверен, что хранение массива значений [FREE, MOVING, STATIC] для каждой позиции на доске — лучший способ справиться с этим. Таким образом, если мы хотим переместить фигуру и пространство, в котором она нуждается, уже занято — у нас есть столкновение. Сложность: O (количество кубиков в форме) <=> O (1). Бу-Ях!

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

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

Сначала мы произвольно принимаем форму и создаем копию. Вот почему нам нужна была функция cloneVector.

Tetris.Block.position = {};
 
Tetris.Block.generate = function() {
  var geometry, tmpGeometry;
 
  var type = Math.floor(Math.random()*(Tetris.Block.shapes.length));
  this.blockType = type;
 
  Tetris.Block.shape = [];
  for(var i = 0; i < Tetris.Block.shapes[type].length; i++) {
    Tetris.Block.shape[i] = Tetris.Utils.cloneVector(Tetris.Block.shapes[type][i]);
  }
  // to be continued...

 

Теперь нам нужно соединить все кубы, чтобы они действовали как одна форма.

Для этого есть функция Three.js — она ​​берет геометрию и сетку и объединяет их. То, что на самом деле происходит здесь, это слияние массива внутренних вершин. Он учитывает положение объединенной геометрии. Это причина, почему нам нужен был первый куб (0,0,0). У сетки есть позиция, а у геометрии нет — всегда считается (0,0,0). Можно было бы написать функцию слияния для двух сеток, но это сложнее, чем сохранять формы, как мы, не так ли?

geometry = new THREE.CubeGeometry(Tetris.blockSize, Tetris.blockSize, Tetris.blockSize);
for(var i = 1 ; i < Tetris.Block.shape.length; i++) {
  tmpGeometry = new THREE.Mesh(new THREE.CubeGeometry(Tetris.blockSize, Tetris.blockSize, Tetris.blockSize));
  tmpGeometry.position.x = Tetris.blockSize * Tetris.Block.shape[i].x;
  tmpGeometry.position.y = Tetris.blockSize * Tetris.Block.shape[i].y;
  THREE.GeometryUtils.merge(geometry, tmpGeometry);
}
  // to be continued...

 С объединенной геометрией мы можем использовать трюк с двойными материалами из первой части урока.

Tetris.Block.mesh = THREE.SceneUtils.createMultiMaterialObject(geometry, [
  new THREE.MeshBasicMaterial({color: 0x000000, shading: THREE.FlatShading, wireframe: true, transparent: true}),
  new THREE.MeshBasicMaterial({color: 0xff0000})
]);
  // to be continued...

 Мы должны установить начальную позицию и вращение для нашего блока (центр доски для x, y и некоторое произвольное число для z).

// initial position
  Tetris.Block.position = {x: Math.floor(Tetris.boundingBoxConfig.splitX/2)-1, y: Math.floor(Tetris.boundingBoxConfig.splitY/2)-1, z: 15};
 
  Tetris.Block.mesh.position.x = (Tetris.Block.position.x - Tetris.boundingBoxConfig.splitX/2)*Tetris.blockSize/2;
  Tetris.Block.mesh.position.y = (Tetris.Block.position.y - Tetris.boundingBoxConfig.splitY/2)*Tetris.blockSize/2;
  Tetris.Block.mesh.position.z = (Tetris.Block.position.z - Tetris.boundingBoxConfig.splitZ/2)*Tetris.blockSize + Tetris.blockSize/2;
  Tetris.Block.mesh.rotation = {x: 0, y: 0, z: 0};
  Tetris.Block.mesh.overdraw = true;
 
  Tetris.scene.add(Tetris.Block.mesh);
}; // end of Tetris.Block.generate()

 

Если вы хотите, вы можете вызвать Tetris.Block.generate () из вашей консоли.

перемещение

Переместить блок на самом деле очень просто. Для вращения мы используем внутренние компоненты Three.js и должны преобразовывать углы в радианы.

Tetris.Block.rotate = function(x,y,z) {
  Tetris.Block.mesh.rotation.x += x * Math.PI / 180;
  Tetris.Block.mesh.rotation.y += y * Math.PI / 180;
  Tetris.Block.mesh.rotation.z += z * Math.PI / 180;
};

 Позиция также проста — Three.js нужна позиция с учетом размера блока, а наша копия — нет. Для нашего развлечения есть простая проверка попадания в пол — она ​​будет удалена позже.

Tetris.Block.move = function(x,y,z) {
  Tetris.Block.mesh.position.x += x*Tetris.blockSize;
  Tetris.Block.position.x += x;
 
  Tetris.Block.mesh.position.y += y*Tetris.blockSize;
  Tetris.Block.position.y += y;
 
  Tetris.Block.mesh.position.z += z*Tetris.blockSize;
  Tetris.Block.position.z += z;
  if(Tetris.Block.position.z == 0) Tetris.Block.hitBottom();
};

ударить и создать снова

Для чего нужен hitBottom? Помнить? Если жизненный цикл блока закончился, мы должны преобразовать его в статические кубы, удалить его со сцены и сгенерировать новый.

Tetris.Block.hitBottom = function() {
  Tetris.Block.petrify();
  Tetris.scene.removeObject(Tetris.Block.mesh);
  Tetris.Block.generate();
};

 У нас уже есть generate (), а removeObject () — это функция Three.js для удаления неиспользуемых сеток. К счастью, во второй части этого урока мы написали функцию для статических кубов и теперь будем использовать ее в petrify ().

Tetris.Block.petrify = function() {
  var shape = Tetris.Block.shape;
  for(var i = 0 ; i < shape.length; i++) {
    Tetris.addStaticBlock(Tetris.Block.position.x + shape[i].x, Tetris.Block.position.y + shape[i].y, Tetris.Block.position.z + shape[i].z);
  }
};

 

Для Tetris.Block.shape используется сокращение — оно улучшает как ясность кода, так и производительность, поэтому используйте эту технику всякий раз, когда она подходит. В этой функции вы можете увидеть, почему держать повернутую форму и разделенное положение было хорошей идеей. Благодаря этому наш код будет приятным для чтения, а с обнаружением столкновений это будет еще важнее.

Соедините точки

Хорошо, теперь у нас есть все функции, которые нам нужны для блоков, давайте подключим их там, где это необходимо. Нам нужно сгенерировать один блок при запуске, поэтому измените Tetris.start () на

Tetris.start = function() {
  document.getElementById("menu").style.display = "none";
  Tetris.pointsDOM = document.getElementById("points");
  Tetris.pointsDOM.style.display = "block";
  Tetris.Block.generate(); // add this line
  Tetris.animate();
};

 С каждым шагом игры мы должны перемещать блок на один шаг вперед, поэтому найдите место в Tetris.animate (), где мы делаем ход, и измените его на:

while(Tetris.cumulatedFrameTime > Tetris.gameStepTime) {
   Tetris.cumulatedFrameTime -= Tetris.gameStepTime;
   Tetris.Block.move(0,0,-1); // add this line

 

клавиатура

Я должен быть честным — я ненавижу события клавиатуры. Коды клавиш не имеют смысла и различаются для нажатия клавиш и нажатия клавиш . Нет хорошего способа опроса состояния клавиатуры, после того как второе нажатие клавиши повторяется в 10 раз быстрее, чем для первых двух и т. Д. Если вы думаете о серьезной игре с большим количеством взаимодействий с клавиатурой, вы почти наверняка создадите какую-то оболочку для все это фигня. Вы можете попробовать KeyboardJS , это выглядит хорошо. Я буду использовать Vanilla JS, чтобы показать общую идею. Для отладки я использовал console.log (keycode) — он очень помогает найти правильные коды:)

window.addEventListener('keydown', function (event) {
  var key = event.which ? event.which : event.keyCode;
 
  switch(key) {
    case 38: // up (arrow)
      Tetris.Block.move(0, 1, 0);
      break;
    case 40: // down (arrow)
      Tetris.Block.move(0, -1, 0);
      break;
    case 37: // left(arrow)
      Tetris.Block.move(-1, 0, 0);
      break;
    case 39: // right (arrow)
      Tetris.Block.move(1, 0, 0);
      break;
    case 32: // space
      Tetris.Block.move(0, 0, -1);
      break;
 
    case 87: // up (w)
      Tetris.Block.rotate(90, 0, 0);
      break;
    case 83: // down (s)
      Tetris.Block.rotate(-90, 0, 0);
      break;
 
    case 65: // left(a)
      Tetris.Block.rotate(0, 0, 90);
      break;
    case 68: // right (d)
      Tetris.Block.rotate(0, 0, -90);
      break;   
 
    case 81: // (q)
      Tetris.Block.rotate(0, 90, 0);
      break;
    case 69: // (e)
      Tetris.Block.rotate(0, -90, 0);
      break;
  }
}, false);

 

Если вы попытаетесь играть в игру сейчас, вы сможете перемещать и вращать блок. Не будет никакого обнаружения столкновений, но когда он упадет на землю, он будет удален и на борту появится новый блок. Поскольку мы не применяем вращение к сохраненной фигуре, статическая версия может вращаться по-разному.

После этого урока вы должны:

  • Знайте, что числа передаются по значению, а объекты по ссылке.
  • Понять жизненный цикл блока тетриса.
  • Знать, как объединить геометрию и разницу между сеткой и геометрией.
  • Знать, как связать события keyoboard.

Захватить источник из GitHub

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