Третья часть урока посвящена движущимся блокам. Мы создадим блок, введем некоторые движения и рулевого управления. Пока нас не волнует обнаружение столкновений.
- Часть 1: Введение и игровой цикл
- Часть 2: Статические блоки и ведение счета
- Часть 3: Добавление и перемещение блока
- Часть 4: обнаружение столкновений
- Часть 5: Аудио и оценка
Давайте начнем с резюме. Каков жизненный цикл блока 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.
Если у вас возникли проблемы с любым из них, проверьте учебник еще раз или задайте вопрос в комментариях ниже.