БлогNot. Пишем Тетрис на HTML5+CSS+Javascript

Пишем Тетрис на HTML5+CSS+Javascript

в коде игры где-то есть баг, иногда игра завершается преждевременно, надо будет поискать внимательней... ну, или нашедшему - приз!

Современная версия формата разметки HTML5 в связке c CSS и Javascript превращается не просто в массовый инструмент разработки, на котором можно писать игры из... нуля строк кода, но, того и гляди, "HTML-программисты" потеснят классических :) По крайней мере, большинство детских курсов по программированию построены именно на этой связке.

Удобно то, что вся логическая разметка, будь то форма приложения или канва для вывода графики, делается обычным HTML, для оформления и вёрстки идеально подходят стили CSS, а в JS есть средства для удобного доступа к любому узлу документа, высокоуровневые средства программирования и готовые таймеры для реализации анимации. К тому же, в ныне распространённых версиях JS нет в чистом виде обычно непонятных начинающим "классов" с запутанными отношениями между ними, но есть произвольное конструирование объектов.

Шагая в ногу со временем, давайте и мы реализуем на HTML+Javascript что-нибудь законченное, например, классический Тетрис примерно так, как он описан в Википедии.

За разметку будет отвечать обычный HTML-файл с именем index.html, предусмотрим вёрстку в 2 колонки с идентификаторами tetrisleft и tetrisright и 2 графичеких канвы - tetriscanvas для вывода основного игрового поля и figurecanvas для отдельной отрисовки следующей фигуры. Для вывода информационных элементов игры также предусмотрим стилевой класс info.

Вот файлы разметки и стиля, которые можно поместить в одну папку.

Файл index.html
<!DOCTYPE html>
<html>
 <head>
  <title>Tetris</title>
  <link rel='stylesheet' type="text/css" href='tetrisstyle.css'/>
 </head>
 <body>

 <div id="tetris">
  <div id="tetrisleft">
   <canvas id="tetriscanvas" width="300" height="600"></canvas>
   <audio id="clearsound" src="sound/pop.ogg" preload="auto"></audio>
   <audio autoplay loop>
    <source src="sound/music.ogg" type="audio/ogg; codecs=vorbis" preload="auto">
    <small>Извините, тег audio не поддерживается вашим браузером</small>
   </audio>
  </div>
  <div id="tetrisright">
   <div align="center"><b>T E T R I S</b></div>
   <p class="info">&harr;: move left/right;
   <br>&uarr;: rotate;
   <br>&darr; or _: drop;
   <br>Esc: pause
   </p>
   <canvas id="figurecanvas" width="120" height="120"></canvas>
   <p class="info" id="tetriscount"></p>
  </div>
  <div class="clear"></div>
 </div>
 <script src='js/game.js'></script>
 <script src='js/controller.js'></script>
 <script src='js/canvas.js'></script>

 </body>
</html>
Файл tetrisstyle.css
#tetriscanvas {
 display: block;
 border: 1px solid black;
}
#tetris {
 margin: 0 auto;
 width: 480px; 
 height: 600px; 
 text-align: center;
 clear: both;
}
#tetrisleft {
 width: 300px;
 min-width: 300px;
 height: 600px; 
 float: left; 
 margin: 0px; 
}
#tetrisright {
 width: 180px; 
 min-width: 180px;
 height: 600px; 
 float: left; 
 margin: 0px;
}
.clear {
 clear: left; /* Отмена обтекания */
}
.info {
 text-align: left;
 margin-left: 24px;
}

Из HTML-кода видно, что мы подключили целых три яваскрипт-файла. Файл canvas.js будет отвечать за обработку основного игрового поля, controller.js добавит к приложению обработчик событий (в нашем случае - только нажатия клавиш), а game.js займётся основной логикой игры.

Все эти файлы поместим во вложенную папку с именем js.

Файл canvas.js просто получит ссылку на основную канву и займётся отрисовкой игрового поля по данным, содержащимся в массиве board файла game.js. У него будет собственная частота перерисовки канвы, равная 50 миллисекундам.

Файл js/canvas.js
var canvas = document.getElementById ('tetriscanvas');
var ctx = canvas.getContext ('2d');
var width = canvas.width, height = canvas.height;
var blockWidth = width / columns, blockHeight = height / rows;

function drawBlock (x,y) { //Нарисовать фигуру в позиции (x,y)
 ctx.fillRect (blockWidth*x, blockHeight*y, blockWidth-1, blockHeight-1);
 ctx.strokeRect (blockWidth*x, blockHeight*y, blockWidth-1, blockHeight-1);
}

function render() { //Нарисовать стакан и фигуры
 ctx.clearRect( 0, 0, width, height );
 ctx.strokeStyle = 'black';
 for (var x=0; x<columns; x++) {
  for (var y = 0; y < rows; y++ ) {
   if (board[y][x]) {
    ctx.fillStyle = colors[board[y][x]-1];
    drawBlock (x,y);
   }
  }
 }
 ctx.fillStyle = 'red';
 ctx.strokeStyle = 'black';
 for (var y=0; y<4; y++) {
  for (var x=0; x<4; x++) {
   if (current[y][x]) {
    ctx.fillStyle = colors[current[y][x]-1];
    drawBlock (currentX+x,currentY+y);
   }
  }
 }
}

setInterval (render,50); //частота перерисовки, мс

Файл controller.js устроен ещё проще - там прописываются коды нужных клавиш и ставится обработчик события нажатия клавиши в документе.

Стрелки влево и вправо будут двигать фигурку в соответствующих направлениях, стрелка вверх - вращать, а стрелка вниз или пробел - ускорять падение. Паузу можно будет удобно вызвать нажатием клавиши Escape, а возобновить игру - повторным нажатием Escape или кликом по кнопке "OK" в появившемся окне "Pause" (в Javascript уже есть модальное окно window.alert, так что программировать задержки нам не придётся).

Файл js/controller.js
document.body.onkeydown = function (e) {
 var keys = { //Клавиши
  37: 'left',
  39: 'right', //Стрелки влево и вправо
  40: 'down',
  32: 'down', //Вниз - пробелом или стрелкой вниз
  38: 'rotate', //Вращение- стрелкой вверх
  27: 'escape' //Пауза по клавише Esc
 };
 if (typeof(keys[e.keyCode])!='undefined') { //Если код клавиши допустимый,
  keyPress (keys[e.keyCode]); //Передать его обработчику
  render(); //и перерисовать стакан
 }
};

Сама игра, конечно же, будет подлиннее, ровно в 200 строк, но комментарии помогут вам без проблем понять её.

Файл js/game.js
var columns = 10, rows = 20; //Классические размеры стакана
var board = []; //Стакан
var lose; //Конец игры
var lines = 0; //Убрано линий
var count = 0; //Счёт
var maxCount = 0; //Рекорд
var interval; //Скорость игры в мс
var current; //Текущая фигурка
var currentX, currentY; //Позиция текущей фигурки
var shapes = [ //Массив фигур
 [1,1,1,1], //I
 [1,1,1,0, //L
  1],
 [1,1,1,0, //J
  0,0,1],
 [1,1,0,0, //O
  1,1],
 [1,1,0,0, //Z
  0,1,1],
 [0,1,1,0, //S
  1,1],
 [0,1,0,0, //T
  1,1,1 ]
];
var colors = [ //Массив цветов
 'cyan', 'orange', 'blue', 'yellow', 'red', 'lime', 'purple'
];
var shaped = 0; //Есть ли следующая фигурка
var savedShape; //Следующая фигурка

function drawNewShape (current) { //Нарисовать следующую фигуру на отдельной канве
 var canvas = document.getElementById ('figurecanvas');
 var ctx = canvas.getContext ('2d');
 var width = canvas.width, height = canvas.height;
 var blockWidth = width / 4, blockHeight = height / 4;
 ctx.fillStyle = 'red';
 ctx.strokeStyle = 'black';
 ctx.clearRect (0,0,width,height);
 for (var y=0; y<4; y++) {
  for (var x=0; x<4; x++) {
   if (current[y][x]) {
    ctx.fillStyle = colors[current[y][x]-1];
    ctx.fillRect (blockWidth*x, blockHeight*y, blockWidth-1, blockHeight-1);
    ctx.strokeRect (blockWidth*x, blockHeight*y, blockWidth-1, blockHeight-1);
   }
  }
 }
}

function generateShape () { //Сгенерировать следующую фигуру
 var id = Math.floor (Math.random()*shapes.length);
 var shape = shapes[id];
 var current = [];
 for (var y=0; y<4; y++) {
  current[y] = [];
  for (var x=0; x<4; x++) {
   var i = 4*y+x;
   if (typeof(shape[i])!='undefined' && shape[i]) current[y][x] = id+1;
   else current[y][x]=0;
  }
 }
 if (shaped) drawNewShape(current);
 return current;
}

function newShape() { //Создать новую фигурку 4x4 в массиве current
 if (shaped) { //Есть сохранённая
  for (var i=0; i<savedShape.length; i++) current[i] = savedShape[i]; 
 }
 else { //Нет сохранённой
  current = generateShape();
  shaped = 1;
 }
 savedShape = generateShape();
 currentX = Math.floor((columns-4)/2); currentY = 0; //Начальная позиция новой фигурки
}

function init() { //Очистить стакан
 for (var y=0; y<rows; ++y) {
  board[y] = [];
  for (var x=0; x<columns; x++) board[y][x] = 0;
 }
}

function countPlus (lines0) { //Подсчёт очков
 lines += lines0; 
 var bonus = [0, 100, 300, 700, 1500];
 count += bonus[lines0];
 if (count > maxCount) maxCount = count;
 document.getElementById('tetriscount').innerHTML = 
  "Lines: "+lines+"<br>Count: "+count+"<br>Record: "+maxCount;
}

function freeze() { //Остановить фигурку и записать её положение в board
 for (var y=0; y<4; y++) {
  for (var x=0; x<4; x++) {
   if (current[y][x]) board[y+currentY][x+currentX] = current[y][x];
  }
 }
}

function rotate( current ) { //Вращение текущей фигурки current против часовой стрелки
 var newCurrent = [];
 for (var y=0; y<4; y++) {
  newCurrent[y] = [];
  for (var x=0; x<4; x++) newCurrent[y][x]=current[3-x][y];
 }
 return newCurrent;
}

function clearLines() { //Проверить, есть ли заполненные линии и очистить их
 var cleared = 0;
 for (var y=rows-1; y>-1; y--) {
  var rowFilled = true;
  for (var x=0; x<columns; x++) {
   if (board[y][x]==0) {
    rowFilled = false;
    break;
   }
  }
  if (rowFilled) { //Очистить линию
   cleared++;
   document.getElementById ('clearsound').play();
   for (var yy=y; yy>0; yy--) {
    for (var x=0; x<columns; x++) {
     board[yy][x]=board[yy-1][x];
    }
   }
   y++;
  }
 }
 return cleared;
}

function keyPress( key ) { //Обработчик нажатий клавиш
 switch ( key ) {
  case 'escape':    
   window.alert ('paused'); //В JS уже есть модальное окно :)
  break;
  case 'left':
   if (valid(-1)) --currentX;
  break;
  case 'right':
   if (valid(1)) ++currentX;
  break;
  case 'down':
   if (valid(0,1)) ++currentY;
  break;
  case 'rotate':
   var rotated = rotate(current);
   if (valid(0,0,rotated)) current = rotated;
  break;
 }
}

function valid (offsetX,offsetY,newCurrent) { //Проверка допустимости итоговой позиции фигуры current
 offsetX = offsetX || 0;
 offsetY = offsetY || 0;
 offsetX = currentX + offsetX;
 offsetY = currentY + offsetY;
 newCurrent = newCurrent || current;
 for (var y=0; y<4; y++) {
  for (var x=0; x<4; x++) {
   if (newCurrent[y][x]) {
    if (typeof(board[y+offsetY])=='undefined' || typeof(board[y+offsetY][x+offsetX])=='undefined'
     || board[y+offsetY][x+offsetX]
     || x+offsetX<0 || y+offsetY>=rows || x+offsetX>=columns) {
     if (offsetY==1) lose=true; //Конец игры, если текущая фигура - на верхней линии
     return false;
    }
   }
  }
 }
 return true;
}

function playGame() { //Контроль падения фигурки, создание новой и очистка линии
 if (valid(0,1)) currentY++;
 else {
  freeze();
  var cleared = clearLines();
  if (cleared) countPlus(cleared);
  if (lose) {
   newGame();
   return false;
  }
  newShape();
 }
}

function newGame() { //Новая игра
 clearInterval (interval);
 init ();
 shaped = 0; newShape ();
 lose = false; lines = 0; count = 0; countPlus (0); 
 interval = setInterval (playGame,300); //скорость игры, мс
}

newGame();

Разумеется, нетрудно прикрутить прикрутить сюда, например, сохранение рекорда в Cookie-файле, предусмотреть увеличение скорости игры в зависимости от количества убранных линий или набранных очков (уменьшать переменную interval в game.js) и т.д.

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

Конечно, без музыки игр не бывает. Звуки поместим во вложенную папку sound, а разметка для их подключения уже предусмотрена в index.html. Ставить тегу <audio> атрибут controls, выводящий для звукового файла маленький плеер, не будем, чтобы наши нажатия клавиш не "перехватывались" плеером. Во всех современных браузерах можно отключить фоновый звук, просто щёлкнув по значку с динамиком на ярлычке вкладки.

Так как традиционная для тетриса фоновая мелодия "Коробейники" займёт 99% объёма архива, выкладывать сюда никакой архив с проектом не будем, а просто сошлёмся на звуковые файлы. Формат звука .ogg, думаю, удобнее всего, чтобы "шло" и на "Андроиде":

Все остальные файлы проекта - выше по тексту :)

 Тетрис на HTML5+Javascript в работе, ПОИГРАТЬ

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

Ну, чем хуже оригинала? :) Кстати, чёрно-белый оригинал от 1986 года у меня сработал под Windows7, только таймер там, видимо, абсолютный, из-за чего слишком быстро падают фигурки. Но из-под эмулятора DOS должно всё быть идеально.

оригинальный Тетрис, 1986 год
оригинальный Тетрис, 1986 год
оригинальный Тетрис, 1986 год
оригинальный Тетрис, 1986 год

 Скачать оригинальный Тетрис от 1986 года в архиве .zip (14 Кб)

 Сыграть в ещё один "Тетрис" на HTML+Javascript, тоже глючноват, сами увидите, почему (15 Кб)

Папки и файлы "Тетриса" на локальном компе
Папки и файлы "Тетриса" на локальном компе

Пример внесения изменений в код игры (изменённые в файле js/game.js строки выделены красным). В данном случае мы увеличиваем скорость (уменьшая интервал, но не более, чем до 50 мс, так как это - частота перерисовки, заданная в js/canvas.js) после убирания одной линии. А можно, заведя ещё одну переменную, контролировать количество линий, после которого нужно увеличить скорость.

добавляем в игру увеличение скорости
добавляем в игру увеличение скорости

17.06.2017, 15:53 [21936 просмотров]


теги: html программирование учебное javascript картинка игра ретро

показать комментарии (4)