Научете обектно-ориентирано програмиране в JavaScript, като създадете Tetris. (6)

В този кръг ще се опитаме да използваме асинхронни функции и да накараме Mino да падне автоматично.

Ето връзка към статии от тази серия: Предишна статия / Следваща статия

Съвременните езици днес имат асинхронни инструкции като async и await. Ако сте потребител на C++, трябва да имате предвид, че async, който споменах, е доста различен от C++11.

Ето една добра практика за aync и await. Бихте ли могли да добавите около 15 реда по-долу към tetris.js и да го стартирате, моля? Date.now() в списъка по-долу връща броя милисекунди, изминали от 0:00 на 1 януари 1970 г., UTC.

// tetris.js

const g = {
  // omitted

  MSEC_GAME_INTERVAL: 1000
}

const gGame = {
  // omitted

  let _timeNextDown;

  this.run = async ()  => {
    _timeNextDown = Date.now() + g.MSEC_GAME_INTERVAL;
    
    for (;;) {
      await new Promise(r => setTimeout(r, _timeNextDown - Date.now()));

      if (_curMino.move(0, 1) == false) {
        break;
      }
      _timeNextDown += g.MSEC_GAME_INTERVAL;
    }
  }
}

gGame.run();

В JavaScript има метод, наречен setInterval, който изпълнява кодове на редовни интервали, които сте посочили. Но използването на async улеснява регулирането на времето, като например осигуряване на достатъчно време за движение на кацналия Мино.

Основното използване на функцията async е както следва. promise_object е обект на Promise, който държи състоянието на асинхронна операция. След като тази операция приключи, се изпълнява следващият ред от awiat.

const asyncFunc = async () => {
  await promise_object;
}

new Promise(r => setTimeout(r, ms) в tetris.js по-горе създава обект от Promise, който чака ms милисекунди. Стилът на писане на JavaScript с Promise и setTimeout е малко объркващ, но C# предоставя лесен за разбиране стил на писане като Task.Delay(millisec), а Python предоставя като asyncio.sleep(sec).

Когато Mino падне на дъното, ние ще поставим Mino отново на горния ред, както е показано в следващия списък. И въвеждаме _isQuit, за да може играта да бъде прекратена. Тъй като оттук нататък ще има още промени, ще предоставя пълния списък в края на статията.

Когато щракнем върху полето за игра, задаваме _isQuit на true.

const gFieldGfx = new function() {
  // omitted

  this.canvas = canvas;
}

const gGame = new function() {
  // omitted

  this.quit = () => {
    _isQuit = true;
  }
}

gFieldGfx.canvas.onclick = gGame.quit;

След това, когато сте приземили Mino чрез натискане на клавиша със стрелка надолу, нека направим кратко време, в което можете да го преместите. Това може лесно да се постигне, тъй като използваме async.

Днес трябва да имате умението да използвате async и await, когато създавате програми за работа. Затова ще ви покажа няколко интересни примера.

const asyncFunc1 = async () => {
  console.log("--- start asyncFunc1");
  await new Promise(r => setTimeout(r, 1000));
  console.log("--- end asyncFunc1");
}

const asyncFunc2 = async () => {
  console.log("+++ start asyncFunc2");
  await new Promise(r => setTimeout(r, 500));
  console.log("+++ end asyncFunc2");
}

const asyncTest = async () => {
  await asyncFunc1();
  await asyncFunc2();
  console.log("### end asyncTest");
}

asyncTest();

Можете да видите резултатите от console.log, като натиснете клавиша F12 в браузъра си и изберете раздела Console. Резултатите от изпълнението на горното са показани на следващата фигура.

Ако промените asyncTest както в Списък 1, резултатът се променя както на фиг.1. Тъй като await е премахнат, процесът на изчакване вече не се изпълнява и първо се появява ### end .

// List 1

const asyncTest = async () => {
  asyncFunc1();
  asyncFunc2();
  console.log("### end asyncTest");
}

Ако промените asyncTest както в Списък 2, резултатът се променя както на фиг.2. Поради дългото време на изчакване на asyncFunc1, +++ end, ### end и ---end се показват в този ред.

// List 2

const asyncTest = async () => {
  asyncFunc1();
  await asyncFunc2();
  console.log("### end asyncTest");
}

Ако промените asyncTest както в Списък 3, резултатът се променя както на фиг.3. Имайте предвид, че чакането за asyncFunc1 се изпълнява преди извеждане на ### end.

// List 3

const asyncTest = async () => {
  const promiseObj = asyncFunc1();
  await asyncFunc2();
  await promiseObj
  console.log("### end asyncTest");
}

Ако промените asyncTest както в Списък 4, резултатът се променя както на Фиг.4. Поради краткото време на изчакване на asyncFunc2, резултатът е същият като списък 3.

// List 4

const asyncTest = async () => {
  const promiseObj = asyncFunc1();
  asyncFunc2();
  await promiseObj
  console.log("### end asyncTest");
}

Надявам се, че знанията за асинхронните функции ще ви бъдат полезни. Благодарим ви, че отделихте време да прочетете тази статия.

// 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,
  
  MSEC_GAME_INTERVAL: 1000,
}

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;
  this.canvas = canvas;
}

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':
        if (_curMino.move(0, 1)) {
          _timeNextDown = Date.now() + g.MSEC_GAME_INTERVAL;
        }
        break;
    }
  }
  
  let _timeNextDown;
  let _isQuit = false;

  this.run = async ()  => {
    _timeNextDown = Date.now() + g.MSEC_GAME_INTERVAL;
    
    for (;;) {
      await new Promise(r => setTimeout(r, _timeNextDown - Date.now()));
      
      if (_isQuit) { break; }      
      if (Date.now() < _timeNextDown) { continue; }

      if (_curMino.move(0, 1) == false) {
        _curMino.drawAtStartPos();
      }
      _timeNextDown += g.MSEC_GAME_INTERVAL;
    }
  }
    
  this.quit = () => {
    _isQuit = true;
  }
}

gFieldGfx.canvas.onclick = gGame.quit;
gGame.run();