|
В этой статье мы попытаемся сделать тетрис онлайн. Естественно, javascript позволяет делать не только тетрисы: сделать можно любую игру, главное, что бы хватило знаний и ресурсов процессора на выполнение такой игры. Создадим файл с именем titris.html, откроем его в любом текстовом редакторе, желательно Блокноте Windows, а лучше чем-то с подсветкой кода HTML/javascript, например Dreamweaver. Вставим туда следующий код HTML: Code <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Tetris</title>
<style type="text/css"> * {border:0; margin:0; padding:0; font-size:20px;} body {background-color:#000000; padding:10px;} td {vertical-align:top; text-align:center; border:1px solid #666666;} #htmlgame {position:relative; width:320px; height:576px;} #nextelm {position:relative; width:129px; height:129px;} #score {color:#33ff00; font-family:'Comic Sans MS'; border-bottom: 1px dashed #666666; border-top: 1px dashed #666666; padding:4px;} .dashed {color:#999; font-family:'Comic Sans MS'; border-bottom: 1px dashed #666666; border-top: 1px dashed #666666; padding:4px;} .dashed a {font-size:20px; font-family:'Comic Sans MS'; color:#096; text-decoration:none;} .dashed a:hover {color:#0C6; text-decoration:underline;} .physx {border:1px solid #cccccc; background-color:green; position:absolute; font-size:9px; height:31px; width:31px;} </style>
</head>
<body>
<table> <tr> <td><div id="htmlgame"></div></td> <td> <div id="nextelm"></div><div id="score">0</div> <div class="dashed">Level: <span id="level">1</span></div> <div class="dashed"><a id="restart" href="javascript:;">New game</a></div> <div class="dashed"><a id="pause" href="javascript:;">Pause</a></div> </td> </tr> </table>
</body>
</html> Открыв tetris.html, вот что мы увидим в браузере: - 0 - отображает кол-во набранных очков.
- level 1 - уровень игрока (начинаем с первого).
- New game - кнопка, нажав на которую начинается новая игра.
- Pause - пауза в игре.
- Внутри дива с id="htmlgame" и будут происходить основные игровые действия.
- А внутри дива с id="nextelm" будет показываться следующий в очереди элемент.
- Класс .physx будет задавать нужные параметры блокам, которые образуют фигурку элемента для тетриса; высота и ширина такого блока будет одинаковой, 32 пикселя.
Впечатляет? Пока сказать сложно... Сам по себе код HTML статичен и неинтерактивен (будущую его версию - HTML5 - в расчёт не берём, да и ждать его выхода и полной поддержки всеми браузерами мы не хотим). Вот тут-то и придёт на помощь javascript. Javascript — объектно-ориентированный скриптовый язык программирования. Обычно используется в браузерах как язык сценариев для придания интерактивности веб-страницам. Дабы свести объём кода к минимуму мы воспользуемся библиотекой jQuery, в которой уже есть все часто используемые функции javascript. Писать их ещё раз нам не придётся - просто берём и используем их. Для начала подключим эту библиотеку к себе на страницу. Скачивать её с официального сайта не обязательно: достаточно будет использовать ссылку до неё, которую, кстати, может предоставить Google. Вставляем этот код Code <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.5.2/jquery.min.js"></script> между тегами head нашей страницы, после тега </style>. Всё, теперь мы можем использовать функции jQuery, к примеру, для того, что бы получить какой-то элемент, вместо длинного document.getElementById(eid) достаточно писать просто $(eid). А обращаться к элементам нам придётся часто. Очень часто. Как известно, любая игра на компьютере имеет частоту кадров. Т.е. каждый кадр (обычно 60-100 раз в секунду) компьютер выполняет некоторый код игры и выводит результат на экран. Значит, в нашем коде должна быть функция, которая выполняется с некоторым интервалом несколько раз в секунду. Сделаем это. Сразу после кода подключения jQuery добавьте этот код: Code <script type="text/javascript">
$(document).ready(function() {
var cubeW = 32; var cubeH = 32; var tetW = 320; var tetH = 576; var lower = 100;
var pause = true; var gOver = false; var tm = 0; var e = []; var ste = []; var scr = 0; var lvl = 1;
/////////////////// var i = 0; var ii = 0; var iii = 0; var im = 0; //////////////////
setInterval(function (){
// если пауза или игра окончена - пропускаем действия нашей функции // игра как бы будет висеть if(pause || gOver) return; // чем меньше параметр lower, тем быстрее будет идти игра // а чем больше будет уровень (lvl) игрока, тем сложнее будет играть, за счёт этого «ускорения» if(tm++ < lower-lvl) return; else tm = 0;
//////// код игры....
}, 10);
});
</script> - Каждый элемент тетриса (фигуру) образуют 4 квадрата, переменные cubeW и cubeH задают их размеры (ширину и высоту, соответственно) в пикселях.
- Переменные tetW и tetH определяют размеры непосредственно игрового окна (меню справа не в счёт!), в пикселях. Если будете менять эти значения, то не забудьте их изменить и в стилях вот в этой части:
Code #htmlgame {position:relative; width:320px; height:576px;} - pause и gOver - эти переменные определяют паузу в игре и конец игры.
- lower - эта переменная определяет скорость игры, чем она больше, тем больше будет пропущено кадров и, следовательно, тем медленнее будут движения в игре (в тетрисе - это движение элемента вниз).
- e - эта переменная будет указывать на элемент тетриса, с которым мы в данный момент работаем, т.е. его мы будем перемещать стрелочками, вращать и вставлять куда захотим.
- ste - здесь будет список всех «складированных» на дне элементов тетриса, которые в данный момент ещё существуют и «ждут» своего уничтожения.
- lvl - переменная содержит текущий уровень игрока.
- scr - переменная содержит текущее кол-во очков игрока (точнее, с некоторым запозданием - оно будет меняться только при переходе на следующий уровень, как получить текущее настоящее кол-во очков игрока - узнаете ниже).
Теперь перейдём непосредственно к элементам тетриса. У них может быть разные фигуры. Зададим различные варианты через этот массив: Code // массив возможных фигур для тетриса var arr = [ [ [1,1], [1,1] ], [ [0,1], [1,1], [0,1] ], [ [1,1,1], [0,0,1] ], [ [0,0,1], [1,1,1] ], [ [1,1,1,1] ], [ [1,1,0], [0,1,1] ], [ [0,1,1], [1,1,0] ] ]; Вставьте его перед setInterval. Думаю, понять массив несложно: то место, где имеется единица, будет «замещено» «кубиком». И сразу же, ниже, вставим функцию создания элемента (т.е. фигуры) для тетриса: Code // выбираем случайный элемент из arr var ri = Math.floor(Math.random()*arr.length); var size; // функция для создания элемента тетриса function createElm(ret){ ///////////////////// var x = (ret===true?6*cubeW/2:0); var a = 0; var b = 0; var c = x; var d = 0; var e; var f = []; var s = []; //////////////////////
if($.isArray(ret)){ s = ret; size = ret; }else{ s = arr[ri]; if(ret){ ri = Math.floor(Math.random()*arr.length); size = s; }else{ $("#nextelm").html(""); } } // согласно варианту создаём элемент для тетриса for(a=0; s[a]!==undefined; a++){ for(b=0; s[a][b]!==undefined; b++){ if(s[a][b] == 1){ e = document.createElement("div"); $(e).addClass("physx").html(" ").css("left", c + "px").css("top", d + "px"); f.push(e); // взависимости от параметра ret, мы либо вставляем элемент в игру if(ret) $("#htmlgame").append(e); // либо в окошко очереди («следующий элемент») else $("#nextelm").append(e); } c += cubeW; } c = x; d += cubeH; } // ну и возвращаем его if(ret) return f; } - Перед вызовом функции createElm выбирается случайный элемент и записывается в переменную ri (точнее, записывается ИНДЕКС этого элемента в массиве arr).
- После каждого вызова функции createElm (кроме некоторых, см. ниже) в переменную ri каждый раз записывается новый случайно выбранный индекс элемента в массиве arr.
- Функция может принимать параметр ret, имеющий одно из трёх значений: true, false, array (массив).
Если true, то созданный элемент будет помещён в игровое поле, а указатель на него будет возвращён (return); согласно ранее сгенерированному ri будет выбран элемент из массива arr и записан в переменную size, после чего ri будет сгенерирован ещё раз на будущий вызов функции создания элемента (это нужно, что бы заранее уже знать, каким будет следующий элемент). Если false, то новый созданный элемент будет помещён в ячейку следующего элемента в очереди, при этом переменные ri и size НЕ будeт изменены. Если array, то параметр будет воспринят как указание на форму для нового элемента. Будут совершены почти те же действия, что и при true. Зачем это нужно - узнаете ниже, при рассмотрении функции вращения фигуры. - Переменная x содержит смещение от левой границы тетриса до центра игрового поля. Нужно для того, чтобы размещать созданый элемент «красиво» и по центру в игровом поле.
Элемент тетриса должен реагировать на столкновения со стенами и другими элементами, которые лежат внизу. Для этого создадим функцию collisionDetect. Вставьте перед функций createElm этот код: Code function collisionDetect(a){ var m = 0; var n = 0; for(m=0; e[m]!==undefined; m++){
if((a == 'bottom') && (parseInt($(e[m]).css("top")) >= tetH-cubeH)) return true; if((a == 'left') && (parseInt($(e[m]).css("left")) == 0)) return true; if((a == 'right') && (parseInt($(e[m]).css("left")) == tetW-cubeW)) return true; for(n=0; ste[n]!==undefined; n++){ if(($(e[m]).css("top") == $(ste[n]).css("top")) && ($(e[m]).css("left") == $(ste[n]).css("left"))){ gameOver(); return true; } if((a == 'left') && (parseInt($(e[m]).css("left")) - cubeW == parseInt($(ste[n]).css("left"))) && ($(e[m]).css("top") == $(ste[n]).css("top"))) return true; if((a == 'right') && (parseInt($(e[m]).css("left")) + cubeW == parseInt($(ste[n]).css("left"))) && ($(e[m]).css("top") == $(ste[n]).css("top"))) return true; if((a == 'bottom') && (parseInt($(e[m]).css("top")) + cubeH == parseInt($(ste[n]).css("top"))) && ($(e[m]).css("left") == $(ste[n]).css("left"))) return true;
if((a == 'top') && (parseInt($(e[m]).css("top")) - cubeH == parseInt($(ste[n]).css("top"))) && ($(e[m]).css("left") == $(ste[n]).css("left"))) return true; } } return false; } - Функция принимает параметр a, который определяет, как проверять столкновения.
Если left, то функция проверяет, свободно ли пространство слева. Если right, то функция проверяет, свободно ли пространство ссправа. Если bottom, то функция проверяет, свободно ли пространство внизу. Если top, то функция проверяет, свободно ли пространство сверху. P.S. Все столкновения проверяются в рамках размеров одного блока, составляющего фигурку для тетриса. - Учитываются границы тетриса, за них элемент двигать нельзя.
- Если произошло столкновение, возвращается true. В иных случаях - false.
- gameOver() происходит в тот самый момент, когда элементов уже так много, что новый созданный элемент сразу застревает в них.
В javascript нет методов, позволяющий как-то вращать элементы HTML. Для этих целей нам придётся создать свою функцию. Вставьте ещё ниже этот код: Code function transElm(){ var m = 0; var n = 0; // "переворачиваем" массив size по часовой стрелке на 90 градусов var a = size.reverse(); var b = []; for(m=0; a[m]!==undefined; m++){ for(n=0; a[m][n]!==undefined; n++){ if(b[n]===undefined) b[n] = []; b[n].push(a[m][n]); } } // в сооветствии с новым массивом (т.е. b) создаём и новый элемент var z = createElm(b);
var dx = 0; var dy = 0; for(m=0; e[m]!==undefined; m++){ if(m==1){ // в качестве центра (он же - центр вращения) выбираем второй блок в элементе // получаем разницу (dx, dy) между положением центра нового элемента и центра старого элемента, которого мы хотим перевернуть dx = parseInt($(e[1]).css("left")) - parseInt($(z[1]).css("left")); dy = parseInt($(e[1]).css("top")) - parseInt($(z[1]).css("top")); } } // получив разницу (dx, dy), мы должны переместить все блоки нового элемента for(m=0; z[m]!==undefined; m++) $(z[m]).css("left", parseInt($(z[m]).css("left"))+dx+"px").css("top", parseInt($(z[m]).css("top"))+dy+"px");
// проверим, не застрял ли какой-нибудь элемент внутри другого // или, может, вышел за границы игрового поля var cl = false; for(m=0; z[m]!==undefined; m++){ for(n=0; ste[n]!==undefined; n++){ if( (($(z[m]).css("top")==$(ste[n]).css("top")) && ($(z[m]).css("left")==$(ste[n]).css("left"))) || parseInt($(z[m]).css("left"))<0 || parseInt($(z[m]).css("left"))>tetW-cubeW || parseInt($(z[m]).css("top"))>tetH-cubeH ){ cl = true; break; } if(cl) break; } }
if(cl){ // вращение невозможно for(m=0; z[m]!==undefined; m++) $(z[m]).remove(); }else{ // удаляем старый (не перевёрнутый) элемент for(m=0; e[m]!==undefined; m++) $(e[m]).remove(); // присваиваем переменной e указатель на новый элемент e = z; }
} - Каждый раз при создании элемента (createElm()) в глобальную переменную size заносится элемент массива arr, описывающий форму этого элемента; в функции transElm мы её используем, чтобы знать, как перевернуть текущий элемент.
- Код переворачивает size следующим образом:
было: стало: Ещё ниже добавим события на нужные кнопки для управления элементом тетриса: Code $(document).keydown(function(event){ var notleft = false; var notright = false; switch(event.keyCode){ case 40: //down lower = 3; break; case 37: //left if(!collisionDetect('left')) for(var i=0; e[i]!==undefined; i++) $(e[i]).css("left", parseInt($(e[i]).css("left")) - cubeW + "px"); break; case 39: //right if(!collisionDetect('right')) for(var i=0; e[i]!==undefined; i++) $(e[i]).css("left", parseInt($(e[i]).css("left")) + cubeW + "px"); break; case 32: //space transElm(); break; } }); Код выше работает так: пока мы не «врезались» в препятствие, делать движение. Так же надо не забыть повесить событие и на отпускание клавиши «стрелка вниз» (её код - 40), дабы вернуть значение переменной lower в нормальное: Code $(document).keyup(function(event){ switch(event.keyCode){ case 40: lower = 100-lvl; break; } }); Повесим события на клики по двум ссылкам справа, в меню: Code $("#restart").click(function(){ // начать игру заново $("#gameover").slideUp("fast"); pause = true; for(var i=0; ste[i]!==undefined; i++) $(ste[i]).remove(); for(var i=0; e[i]!==undefined; i++) $(e[i]).remove(); ste = []; e = []; $(this).html("Restart"); $("#pause").html("Pause"); $("#score").html("0"); $("#level").html("1"); $("#nextelm").html(""); gOver = false; pause = false; }); $("#pause").click(function(){ // пауза в игре // клик по паузе имеет смысл только если игра начата if($('#restart').html() == "Restart"){ // а начатой её можно считать, когда кнопка новой игры именуется как "Restart" pause = !pause; if(pause) $(this).html("Continue"); else $(this).html("Pause"); } }); Добавим события при конце игры. Вставьте в див с id="htmlgame" этот код: Code <div id="gameover" align="center">GAME OVER!<br>You gained <span id="scored">0</span> scored!</div> А в стиль добавьте ещё это: Code #gameover {color:#999; font-family:'Comic Sans MS'; display:none; position:absolute; top:100px; left:20px; width:280px; background-color:#000000; border: 1px dashed #666666; z-index:1; padding-top:8px; padding-bottom:8px;} #scored {color:#33ff00;} Перед функцией collisionDetect добавьте этот код: Code function gameOver(){ $("#restart").html("New game"); $("#scored").html($("#score").html()); $("#gameover").slideDown("slow"); gOver = true; pause = false; } Теперь при конце игры у нас будет плавно появляться сообщение вида Quote GAME OVER! You gained 9000 scored! Перейдём к нашей основной функции. Помните, та самая, которая была задана при setInterval? Внутри неё заменяем это: Code //////// код игры.... на следующий код: Code // если элемента нет, то мы его создаём if(e.length == 0){ // не забываем, что e - это массив, список из 4 блоков, образующих фигурку тетриса e = createElm(true); createElm(false); }else{ // проверяем наш элемент, не столкнулся ли он с другим элементом if(collisionDetect('bottom')){ // если да, то добавляем текущий элемент в список всех "лежащих" элементов for(i=0; e[i]!==undefined; i++) ste.push(e[i]); // затем строим список по их положению по горизонтали, т.е. w[32] - содержит список всех элементов на высоте 32px var w = []; for(i=0; ste[i]!==undefined; i++){ im = parseInt(ste[i].style.top); if(w[im] === undefined) w[im] = []; w[im].push(i); } // теперь пробегаемся по полученному списку for(var key in w){ if(!w[key]) continue; // ищем те элементы, которые на одной высоте заполнили всю ширину // в нашем случае там должно быть ровно tetW/cubeW элементов, т.е. 320/32 = 10 элементов iii = Math.floor(tetW/cubeW); if(w[key].length==iii){ scr = 1 * $("#score").html() + iii; lvl = 1 * $("#level").html(); // если набрали ещё 400 очков, то переходим на следующий уровень if(Math.ceil(scr/400) > lvl) lvl += 1; $("#score").html(scr); $("#level").html(lvl); for(i=0; w[key][i]!==undefined; i++){ $(ste[w[key][i]]).remove(); // приравниваем к нулю, потом уберём это из массива ste[w[key][i]] = 0; } } } // теперь надо перестроить массив эелементов, не включая туда те, которые равны нулю var ste1 = ste; ste = [] for(i=0; ste1[i]!==undefined; i++) if(ste1[i]!==0) ste.push(ste1[i]); e = []; // смотрим, нет ли у нас пустых строк под элементами // если есть - то опускаем вышестоящие элементы до тех пор, пока они не упруться в существующие элементы var frees = []; var toper = tetH; // ищем самый верхний элемент // он будет иметь самый маленький style.top for(i=0; ste[i]!==undefined; i++) if(parseInt($(ste[i]).css("top")) < toper) toper = parseInt($(ste[i]).css("top")); for(var p=tetH-cubeH; p>0; p-=cubeH){ // выше самого верхнего элемента продолжать нельзя if(p == toper) break; // ищем пустые строки // массив frees будет представлять те строки (style.top), в которых нет ни одного элемента for(i=0; ste[i]!==undefined; i++){ // предположим, что строка p - пустая, т.е. добавим её в массив frees[p] = p; // пробегаемся по списку существующих элементов if(parseInt($(ste[i]).css("top")) == p){ // если хоть один есть на строке p // то мы присваем ей 0, что означает, что строка далеко не пустая и содержит хотя бы один элемент тетриса frees[p] = 0; break; } } } // сортируем массив по возрастанию // это нужно для того, что бы пробегаться по пустым строкам, начиная с самой верхней // ибо самая верхняя пустая строка будет иметь наименьшее значение в массиве var j = frees.sort(); for(var top in j){ // как уже было отмечено выше, строка, которой присвоено 0 - НЕ пустая, её наличие в массиве мы пропускаем if(j[top] == 0) continue; // опускаем все элементы, которые расположены над пустой строкой for(i=0; ste[i]!==undefined; i++) if(parseInt($(ste[i]).css("top")) < j[top]) $(ste[i]).css("top", parseInt($(ste[i]).css("top")) + cubeH + "px"); } }else{ // элемент тетриса движется вниз, пока это возможно for(i=0; e[i]!==undefined; i++){ $(e[i]).css("top", parseInt($(e[i]).css("top")) + cubeH + "px"); } } } - Код хорошо прокомментирован. Думаю, объяснения будут лишними.
Вот что у нас получилось (можете начать играть прямо в браузере) Продолжение статьи: Браузерная игра на HTML и Javascript. Часть II.
|