Изучите объектно-ориентированное программирование на JavaScript, создав тетрис. (5)
В этом раунде мы представляем новый объект, который определяет, ударился ли Мино о стену или нет.
Вот ссылка на статьи из этой серии: Предыдущая статья / Следующая статья
Мино может проходить сквозь стены на текущем этапе, который мы создали. Как бы вы подошли, чтобы улучшить эту ситуацию?
Ну, если говорить о чем-то немного другом, предположим, вы играете в футбол, баскетбол и так далее. Как вы определяете, соблюдаются ли правила в игре в это время? Вам понадобится судья игры. Таким же образом решается и первый вопрос. При использовании ООП программные компоненты создаются при воображении реальных ролей в человеческом обществе. Давайте наймем объект gFieldJdg
, чтобы определить, попал Мино в стену или нет.
Мы создадим объект gFieldJdg
, который имеет массив, в котором хранятся истинные или ложные значения. Общее количество элементов в массиве равно 252, учитывая, что в нем 12 столбцов и 21 строка. Его элементам присваивается значение true, если есть блок, или false, если он пуст. Например, если 14-й элемент ложный, это означает, что место, выделенное красным на рисунке ниже, пусто.
Добавьте следующий код под gFieldGfx
. Я понимаю, что это потребует некоторой работы, но я надеюсь, что вам понравится создавать программу Tetris.
const gFieldJdg = new function() { const _field = []; for (let y = 0; y < 20; y++) { _field.push(true); for (let x = 0; x < 10; x++) { _field.push(false); } _field.push(true); } for (let x = 0; x < 12; x++) { _field.push(true); } this.ChkToPut = (fposLeftTop, fpos4) => { for (let i = 0; i < 4; i++) { if (_field[fposLeftTop + fpos4[i]]) { return false; } } return true; } }
В первой половине программы устанавливается значение true там, где расположены левая и правая торцевые стенки и нижняя стенка.
ChkToPut
ожидает получить массив fpos4
с четырьмя числами. Четыре числа представляют, где расположены блоки Мино. Например, для T-Mino это будет [1, 12, 13, 14]. Другое число fposLeftTop
указывает, куда вы хотите переместить мино. Если fposLeftTop
было 24, это означает, что вы хотите переместить T-мино на [25, 36, 37, 38]. ChkToPut
возвращает true, если мино можно поместить в указанное место, иначе false.
Массивы JavaScript уникальны тем, что не требуют указания длины. Обычное использование состоит в том, чтобы объявить, что это массив, и добавить элементы по push
, как показано ниже. Также возможен доступ к любому элементу по вашему выбору без предварительной подготовки.
const a = []; a.push(10); a.push(20); // a == [10, 20] const b = []; b[2] = 10; // b == [undefined, undefined, 10]
Затем создайте Mino
, как показано ниже, который управляет информацией, передаваемой в gFieldJdg.ChkToPut
. Добавьте следующий код под gFieldJdg
.
function Mino(blkpos8) { let _fposLeftTop = 0; const _fpos4 = []; for (let idx = 0; idx < 8; idx += 2) { _fpos4.push(blkpos8[idx] + blkpos8[idx + 1] * g.PCS_FIELD_COL); } this.move = (dx, dy) => { const posUpdating = _fposLeftTop + dx + dy * g.PCS_FIELD_COL; if (gFieldJdg.ChkToPut(posUpdating, _fpos4) == false) { return false; } _fposLeftTop = posUpdating; return true; } }
blkpos8
в 1-й строке совпадает с переданным в MinoGfx
. _fpos4
в 3-й строке содержит четыре значения для передачи в gFieldJdg.ChkToPut
, например [25, 36, 37, 38]. Mino.move
возвращает true, если Mino можно переместить в указанном направлении, иначе false.
Давайте проверим два добавленных объекта, чтобы увидеть, правильно ли они работают. Добавьте 5 строк L141, 142, 148, 160, 172 к gGame
, как показано в следующем списке. Если вы повернете Мино, они больше не будут работать должным образом, но если вы переместите его, не вращая, вы обнаружите, что они работают правильно.
Рассмотрим список выше. _curMino
управляет местоположением Мино, а _curMinoGfx
отвечает за его отрисовку. Итак, мы считаем _curMino
главным объектом, а _curMinoGfx
его членом. Пересобрав программу таким образом, мы получаем следующее.
function Mino(color, blkpos8) { const _minoGfx = new MinoGfx(color, blkpos8); // omitted this.move = ... function MinoGfx(color, blkpos8) { // omitted this.move = ... this.draw = ... this.erase = ... this.rotateR = ... this.rotateL = ... } }
По мере того, как вы привыкаете к ООП-мышлению, становится более естественным ограничивать информацию как можно больше внутри. Таким образом, мы ограничиваем MinoGfx
значением Mino
. Это обычная ситуация в повседневной жизни. Например, вы почувствуете, что канцелярские принадлежности лежат в ящике письменного стола. Если вы сделаете это, у вас, естественно, будет меньше проблем с канцелярскими принадлежностями. То же самое верно и для программ. Если вы примете указанную выше программу, у вас будет меньше проблем с MinoGfx
.
Есть еще одно преимущество, которое можно получить, перестроив программу, как описано выше. Мы не можем получить доступ к чему-либо, определенному с помощью const
, поэтому мы не можем получить доступ к функциям MinoGfx
. Таким образом, только функция move
доступна извне для объекта Mino
, что снижает нагрузку на программиста. Помните, что одна из целей ООП — уменьшить нагрузку на программиста за счет ограничения информации.
Чтобы четко указать, как это было изменено, я покажу список, выделив внесенные изменения. Всего было изменено 14 строк. Для удобства я добавил две функции-члена: drawAtStartPos
из Mino
и setToStartPos
из MinoGfx
.
Из-за перестроения Mino
gGame
упрощается следующим образом.
const gGame = new function() { let _curMino = new Mino('magenta', [1, 0, 0, 1, 1, 1, 2, 1]); _curMino.drawAtStartPos(); document.onkeydown = (e) => { switch (e.key) { case 'ArrowLeft': _curMino.move(-1, 0); break; case 'ArrowRight': _curMino.move(1, 0); break; case 'ArrowDown': _curMino.move(0, 1); break; } } }
Я покажу полный список ниже. Спасибо, что нашли время прочитать эту статью.
// tetris.js 'use strict'; const divTitle = document.createElement('div'); divTitle.textContent = "TETRIS"; document.body.appendChild(divTitle); const g = { Px_BLOCK: 30, Px_BLOCK_INNER: 28, PCS_COL: 10, PCS_ROW: 20, PCS_FIELD_COL: 12, } const gFieldGfx = new function() { const pxWidthField = g.Px_BLOCK * g.PCS_FIELD_COL; const pxHeightField = g.Px_BLOCK * (g.PCS_ROW + 1); const canvas = document.createElement('canvas'); canvas.width = pxWidthField; canvas.height = pxHeightField; document.body.appendChild(canvas); const _ctx = canvas.getContext('2d'); _ctx.fillStyle = "black"; _ctx.fillRect(0, 0, pxWidthField, pxHeightField); const yBtmBlk = g.Px_BLOCK * g.PCS_ROW; const xRightBlk = pxWidthField - g.Px_BLOCK + 1; _ctx.fillStyle = 'gray'; for (let y = 1; y < yBtmBlk; y += g.Px_BLOCK) { _ctx.fillRect(1, y, g.Px_BLOCK_INNER, g.Px_BLOCK_INNER); _ctx.fillRect(xRightBlk, y, g.Px_BLOCK_INNER, g.Px_BLOCK_INNER); } for (let x = 1; x < pxWidthField; x += g.Px_BLOCK) { _ctx.fillRect(x, yBtmBlk + 1, g.Px_BLOCK_INNER, g.Px_BLOCK_INNER); } this.context2d = _ctx; } const gFieldJdg = new function() { const _field = []; for (let y = 0; y < 20; y++) { _field.push(true); for (let x = 0; x < 10; x++) { _field.push(false); } _field.push(true); } for (let x = 0; x < 12; x++) { _field.push(true); } this.ChkToPut = (fposLeftTop, fpos4) => { for (let i = 0; i < 4; i++) { if (_field[fposLeftTop + fpos4[i]]) { return false; } } return true; } } function Mino(color, blkpos8) { const _minoGfx = new MinoGfx(color, blkpos8); let _fposLeftTop = 0; const _fpos4 = []; for (let idx = 0; idx < 8; idx += 2) { _fpos4.push(blkpos8[idx] + blkpos8[idx + 1] * g.PCS_FIELD_COL); } this.drawAtStartPos = () => { _fposLeftTop = 4; _minoGfx.setToStartPos(); _minoGfx.draw(); } this.move = (dx, dy) => { const posUpdating = _fposLeftTop + dx + dy * g.PCS_FIELD_COL; if (gFieldJdg.ChkToPut(posUpdating, _fpos4) == false) { return false; } _fposLeftTop = posUpdating; _minoGfx.erase(); _minoGfx.move(dx, dy); _minoGfx.draw(); return true; } function MinoGfx(color, blkpos8) { const _ctx = gFieldGfx.context2d; const _color = color; const _pxpos8 = []; let _x =0, _y = 0; for (let idx = 0; idx < 8; idx += 2) { _pxpos8[idx] = blkpos8[idx] * g.Px_BLOCK; _pxpos8[idx + 1] = blkpos8[idx + 1] * g.Px_BLOCK; } this.setToStartPos = () => { _x = 4 * g.Px_BLOCK; _y = 0; } this.move = (dx, dy) => { _x += dx * g.Px_BLOCK; _y += dy * g.Px_BLOCK; } this.draw = () => { _ctx.fillStyle = _color; for (let idx = 0; idx < 8; idx += 2) { _ctx.fillRect(_x + _pxpos8[idx] + 1, _y + _pxpos8[idx + 1] + 1 , g.Px_BLOCK_INNER, g.Px_BLOCK_INNER); } } this.erase = () => { _ctx.fillStyle = 'black'; for (let idx = 0; idx < 8; idx += 2) { _ctx.fillRect(_x + _pxpos8[idx] + 1, _y + _pxpos8[idx + 1] + 1 , g.Px_BLOCK_INNER, g.Px_BLOCK_INNER); } } this.rotateR = () => { for (let idx = 0; idx < 8; idx += 2) { const old_x = _pxpos8[idx]; _pxpos8[idx] = 2 * g.Px_BLOCK - _pxpos8[idx + 1]; _pxpos8[idx + 1] = old_x; } } this.rotateL = () => { for (let idx = 0; idx < 8; idx += 2) { const old_x = _pxpos8[idx]; _pxpos8[idx] = _pxpos8[idx + 1]; _pxpos8[idx + 1] = 2 * g.Px_BLOCK - old_x; } } } } const gGame = new function() { let _curMino = new Mino('magenta', [1, 0, 0, 1, 1, 1, 2, 1]); _curMino.drawAtStartPos(); document.onkeydown = (e) => { switch (e.key) { case 'ArrowLeft': _curMino.move(-1, 0); break; case 'ArrowRight': _curMino.move(1, 0); break; case 'ArrowDown': _curMino.move(0, 1); break; } } }