Coding4Fun: создание игры типа «Тетрис» с помощью макета сетки CSS и Blend 5


Сегодня я хочу поделиться с вами одним секретом, который компания Майкрософт долго хранила в тайне. Он связан с историей возникновения концепции макета сетки CSS, предназначавшейся для IE10 и приложений Магазина Windows. Многие считают, что эта спецификация разрабатывалась для упрощения создания приложений и макетов веб-сайтов. На самом деле цель ее была совершенно иной, а именно облегчить разработку игр типа «Тетрис»! Не верите?  Я готов доказать это с помощью приложения Blend 5. Начнем!

Предварительные требования: перед тем как приступить к изучению данного руководства, необходимо выполнить следующие действия:

1. Загрузить (купить) и установить Windows 8 RTM на своем компьютере: http://msdn.microsoft.com/ru-ru/windows/apps/br229516.aspx 
2. Загрузить и установить бесплатный выпуск Visual Studio 2012 Express RTM для Windows 8 http://msdn.microsoft.com/ru-ru/windows/apps/br229516.aspx, в состав которого входит приложение Expression Blend 5 для Visual Studio, либо установить более позднюю версию.

Шаг 1. Раскрываем секрет, связанный с появлением концепции макета сетки CSS, с помощью Blend 5

Запустите Expression Blend 5 и создайте новый HTML-проект (Магазин Windows) типа Blank App (Пустое приложение). Назовите его TheRealCSSGridStory:

image

Замените строку:

<p>Content goes here</p> 

на:

<div class="mainGrid"> </div> 

Создайте сетку размером 10 столбцов на 20 строк, не зависящую от разрешения экрана, с помощью дробных единиц. Для этого добавьте следующее правило CSS:

.mainGrid {
    display: -ms-grid;
    width: 100%;
    height: 100%;
    -ms-grid-columns: (1fr)[10];
    -ms-grid-rows: (1fr)[20];
}

Выберите в Live DOM элемент <div> mainGrid. На экране появится примерно следующее:

image

Теперь нарисуйте фигуру в этой симпатичной сетке. Добавьте в основную сетку следующий фрагмент HTML-кода:

<div class="shape1"> </div> 

И вставьте связанный с ним фрагмент CSS:

.shape1 {
    -ms-grid-column: 4;
    -ms-grid-row: 3;
    -ms-grid-column-span: 3;
    -ms-grid-row-span: 2;
    background-color: red;
}

В окне Blend 5 появится следующее:

image

Отлично, но пока что мало напоминает блоки «Тетриса». Идем дальше. Добавьте два класса DIV в элемент shape1:

<div class="line1shape1"></div> <div class="line2shape1"></div> 

и замените предыдущее правило .shape1 фрагментом CSS:

.shape1 {
    -ms-grid-column: 4;
    -ms-grid-row: 3;
    -ms-grid-column-span: 3;
    -ms-grid-row-span: 2;
    display: -ms-grid;
    -ms-grid-columns: 1fr 1fr 1fr;
    -ms-grid-rows: 1fr 1fr;
    width: 100%;
    height: 100%;
}

.line1shape1 {
    -ms-grid-column-span: 2;
    background-color: red;
    
}

.line2shape1 {
    -ms-grid-column: 2;
    -ms-grid-row: 2;
    -ms-grid-column-span: 2;
    background-color: red;
}

Элемент shape1 занимает три столбца и две строки. Я создаю в этой области новую сетку, ячейки которой совпадают по размеру с ячейками основной области, вне зависимости от разрешения экрана!

После этого я добавляю две строки, имитирующие Z-образную фигуру из «Тетриса». Должно получиться примерно следующее:

image

Наши возможности этим не ограничиваются: поиграйте с различными видами, имеющимися на вкладке Device (Устройство), и убедитесь, что в игре воплощен принцип адаптивного дизайна! Чертовски хорошо, не так ли?  Clignement d'œil 

Далее приведены результаты в прикрепленном и книжном представлениях:

image image

Теперь попробуем решить другую проблему. Игровое поле «Тетриса» состоит из квадратов. При использовании концепции адаптивного дизайна ширина увеличивается на 100 %. Приложения для Магазина Windows предназначены для широкоэкранных мониторов (современные планшеты имеют разрешение 1366x768 или 1920x1080, а в большинстве мониторов для настольных ПК используется формат 16:9). Следовательно, ориентация на широкоэкранный формат подойдет практически для любого случая. Для вычисления необходимой ширины следует умножить 9/16 на 10/20 (формат основного игрового поля), в итоге получаем 28,125 %.

Добавим это правило, чтобы основная сетка выводилась в полноэкранном режиме и альбомной ориентации:

@media screen and (-ms-view-state: fullscreen-landscape) {
    .mainGrid {
        width: 28.125%;
        }
}

Отцентрируем игровую сетку –– и вновь с помощью макета сетки CSS! (Теперь вы верите, что данная концепция изначально разрабатывалась для «Тетриса»?)

Изменим элемент body на –ms-grid, состоящий из одного столбца и одной строки:

body {
    display: -ms-grid;
    -ms-grid-columns: 1fr;
    -ms-grid-rows: 1fr;
    width: 100%;
    height: 100%;
}

Теперь просто добавим этот атрибут к коду CSS, относящемуся к основной сетке:

-ms-grid-column-align: center;

Сетка располагается по центру:

image

Ве��оятно, вы шокированы? И спрашиваете себя: «Как можно было пропустить такую очевидную вещь?» Расслабьтесь. Теперь вы знаете СЕКРЕТ, и мы продолжим знакомиться с другими замечательными возможностями, которые дают нам комбинации спецификаций CSS.

Шаг 2. Перемещение и вращение фигуры

Вначале я не хотел прибегать к JS и стремился применять по возможности CSS. Моей первой идеей стало использование CSS3 Animations для перемещения и анимации фигур в различных строках и столбцах. Но меня постигло разочарование: инструмент CSS3 Animations не позволяет изменять значения –ms-grid-column или –ms-grid-row. Пришлось добавить немного кода JavaScript.

Затем я начал думать, как вращать фигуры. Инструмент CSS Transforms показался мне идеальным для решения этой задачи. Проверим на практике. Действительно, Blend 5 отлично подходит, поскольку я сразу же вижу результаты изменений.

Добавим вращение на 90 градусов для элемента shape1, вставив соответствующий класс в DIV-элемент:

.shape1rotated {
    transform: rotate(90deg);
}

Уверен, такого результата вы не ждали:

image

Проблема: фигура не совмещена с игровой сеткой. Нужно немного поправить код:

.shape1rotated {
    transform-origin: 33% 50%;
    transform: rotate(90deg) translateX(-33%);
}

Теперь вращение осуществляется так же, как в играх типа «Тетрис». Ниже приведены два снимка экрана: до и после вращения.

image  image

Можно пойти дальше, применив к элементу shape1 набор переходов:

transition: all 1s ease-out;

После этого при удалении или добавлении класса .shape1rotated для элемента shape1 DIV будет активировать плавную анимацию вращения!

В этом коротком видеоролике показан результат, полученный в Blend 5:

Poster Image

Download Video: MP4, WebM, HTML5 Video Player by VideoJS

 
Вероятно, вы думаете, что такой подход вполне применим для нашей игры. К сожалению, это не так, и вот почему. Попробуйте переместить фигуру, просто изменив ее свойство –ms-grid-column. В Blend 5 все изменения отображаются сразу же. Если фигура не вращается, ее можно переместить до восьмого ряда:

image

Пока вроде бы все в порядке. Но если попробовать повернуть фигуру, добавив класс .shape1rotated к DIV, получится вот что:

image

Как видите, справа от фигуры осталась одна свободная колонка. Если вы думаете, что можно просто переместить сюда фигуру, то ошибаетесь! На самом деле мы получим в девятом ряду вот что:

image

Возможно, вы забыли, что сейчас мы перемещаем DIV-элемент, отображающий макет сетки из трех столбцов и двух строк, который в точности совпадает с соответствующим участком игровой сетки. Когда мы перемещаем элемент, нам кажется, что смещается участок главной сетки. Но для того чтобы этот фокус сработал, требуется не менее трех свободных столбцов, которые могли бы вместить нашу фигуру. Если попытаться втиснуть ее в два столбца (при смещении к девятому столбцу) и меньше, она сожмется, как показано на снимке экрана.

Есть два варианта решения проблемы.

  1. Не применять CSS Transforms и нарисовать повернутую фигуру с помощью другого определения сетки. Например, в фигуре можно использовать три элемента div вместо двух, однако в этом случае мы не сможем применить CSS Animations. 
  2. Задать в основной сетке 12 столбцов вместо 10, но использовать только столбцы со второго по одиннадцатый (своего рода выделенную область). Это позволит решить проблему «переполнения».

Попробуем реализовать второе решение.

Переопределите основную сетку:

.mainGrid {
    display: -ms-grid;
    -ms-grid-columns: (1fr)[12];
    -ms-grid-rows: (1fr)[20];
    -ms-grid-column-align: center;    
    width: 100%;
    height: 100%;
}

Чтобы задать соотношение сторон, обновим запрос мультимедиа:

@media screen and (-ms-view-state: fullscreen-landscape) {
    .mainGrid {
        width: 33.75%;
        }
}

33,75 % = 9/16 *12/20

И заодно добавим «виртуальную сетку», ограничивающую пространство для перемещения фигур. В основную сетку DIV добавьте следующее:

<div class="virtualGrid"> </div> 

Соответствующий этой сетке блок CSS:

.virtualGrid {
    -ms-grid-column: 2;
    -ms-grid-column-span: 10;
    -ms-grid-row-span: 20;
    border-right-style: dashed;
    border-left-style: dashed;
    background-color: #505A5A;
}

Теперь игровая область имеет серый фон и пунктирные линии границ.

Если передвинуть Z-образную фигуру в строку два столбца девять, то результат будет выглядеть так:

image

Вращая фигуру с помощью CSS Transforms, можно переместить ее точно в столбец 10:

image

 

Бонус — работа в режиме книжной ориентации:

 

Если необходима поддержка книжной ориентации (она лучше подходит играм типа «Тетрис»), добавьте следующее определение CSS Media Query:

@media screen and (-ms-view-state: fullscreen-portrait) {
        .mainGrid {
        width: 106.66%;
        }
}

Требуемое соотношение сторон в данном случае равняется 16/9 * 12/20 = 106,66 %.

image

Шаг 3. Добавление кода для обработки игровой логики

Мы реализовали графическую часть игры с помощью только CSS и HTML, и теперь нужно обратиться к JavaScript, чтобы перемещать и вращать фигуру в игровой области. Мы собираемся повторно внедрить логику CSS посредством объекта JS, определяемого с помощью WinJS.Class.

Откройте TheRealCSSGridStory в Visual Studio 2012.

Создайте файл TetrisShapeZ.js в каталоге JS и вставьте следующий фрагмент кода:

(function () {
    "use strict";

    var ShapeZ = WinJS.Class.define(
    /// Constructor - columnIndex is optional. If provided, will define in which column the shape will start function (columnIndex) {
            // We're creating the equivalent of this HTML block : // <div class="shape1 "> // <div class="line1shape1"></div> // <div class="line2shape1"></div> // </div> this._shape1 = document.createElement("div");
            var line1 = document.createElement("div");
            var line2 = document.createElement("div");
            this._shape1.className = "shape1";
            line1.className = "line1shape1";
            line2.className = "line2shape1";
            this._shape1.appendChild(line1);
            this._shape1.appendChild(line2);
            // Boolean to indicate if the shape is in its default orientation mode or not // True means not rotated, false means rotated this._defaultOrientation = true;
            // Setting the column position in the main grid if (columnIndex) {
                this._currentColPos = columnIndex;
                this._shape1.style.msGridColumn = this._currentColPos;
            }
            else {
                this._currentColPos = 1;
            }
            // We always start at line 1 this._currentLinePos = 1;
            // Boolean to know if the shape can be move/rotate or not // If true, this means we've reached the last line possible this._fixed = false;
        },
        {
            /// Specify in which HTML element displayed in CSS Grid you'd like to work with /// width is the number of columns of the grid & height is the number of lines insertIntoGrid: function (element, width, height) {
                element.appendChild(this._shape1);
                this._gridWidth = width;
                this._gridHeight = height;
                // These are the left & bottom max limit for this shape // when displayed in default orientation mode this._maxLeft = width - 3;
                this._maxBottom = height - 1;
            },
            /// Rotate the Z shape 90 degrees anti/clockwise using CSS Transforms /// by simply removing/adding the shape1rotated class rotate: function () {
                if (!this._fixed && this._defaultOrientation) {
                    // rotating 90 degrees clockwise, it will trigger also the CSS Transition WinJS.Utilities.addClass(this._shape1, "shape1rotated");
                    this._defaultOrientation = false;
                    // the left limit is now +1 vs the default orientation this._maxLeft = this._gridWidth - 2;
                }
                else {
                    if (!this._fixed && this._currentColPos < this._maxLeft) {
                        // removing the shape1rotated will automatically reset the shape // to its initial matrix and again the CSS Transition will do the animation for you WinJS.Utilities.removeClass(this._shape1, "shape1rotated");
                        this._defaultOrientation = true;
                        this._maxLeft = this._gridWidth - 3;
                    }
                }
            },
            // Internal function called by public moveLeft/moveRight _moveHorizontally: function (direction) {
                if (!this._fixed && (this._currentColPos < this._maxLeft || direction === -1) && (this._currentColPos > 2 || direction === 1)) {
                    this._currentColPos = this._currentColPos + direction;
                    this._shape1.style.msGridColumn = this._currentColPos;
                }
            },
            /// Move the shape on the immediate left column /// Test if you've reached the left limit or not moveLeft: function () {
                this._moveHorizontally(-1);
            },
            /// Move the shape on the immediate right column /// Test if you've reached the right limit or not moveRight: function () {
                this._moveHorizontally(1);
            },
            /// Move the shape down on the immediate below line /// Test if you've reached the bottom limit or not moveDown: function () {
                if (!this._fixed) {
                    this._currentLinePos = this._currentLinePos + 1;
                    this._shape1.style.msGridRow = this._currentLinePos;
                    if (this._currentLinePos === this._maxBottom) {
                        this._fixed = true;
                    }
                }
            }
        }
    );

    WinJS.Namespace.define("CSSTetris", { ShapeZ: ShapeZ });
} ());

Просто прочитайте код, чтобы разобраться, как он работает. В нем содержится достаточно поясняющих комментариев.

Добавьте ссылку к данному файлу сценария в default.html и сохраните в теле кода только данный фрагмент HTML:

<div class="mainGrid"> <div class="virtualGrid"> </div> </div> 

Перейдите в default.js.

Преимуществом хорошо задокументированного кода является то, что мы теперь получили интересные данные IntelliSense для функции constructor:

image

или rotate:

image

Чтобы код работал правильно, добавьте следующий блок JS сразу после вызова processAll:

document.addEventListener("keydown", OnKeyDown, false);
mainGrid = document.getElementsByClassName("mainGrid")[0];
myShape = new CSSTetris.ShapeZ(4);
myShape.insertIntoGrid(mainGrid, 12, 20);
init();

Затем добавьте две функции:

function init() {
    setInterval(function () {
        myShape.moveDown();
    }, 1000);
}

function OnKeyDown(event) {
    switch (event.keyCode) {
        case KEYCODE_X:
            myShape.rotate();
            break;
        case KEYCODE_LEFT:
            myShape.moveLeft();
            break;
        case KEYCODE_RIGHT:
            myShape.moveRight();
            break;
        case KEYCODE_DOWN:
            myShape.moveDown();
            break;
    }
}

Готово! Теперь у нас есть простенькая игра, графическая часть которой создана с помощью макета сетки CSS, инструментов CSS Transforms и CSS Animations, а обработка простейших действий, характерных для игр типа «Тетрис», выполняется посредством пары строк кода JS.

Ниже представлен короткий видеоролик, демонстрирующий конечный результат:

Poster Image

Download Video: MP4, WebM, HTML5 Video Player by VideoJS

 
Финальное решение для Visual Studio, в котором содержатся три шага данного руководства, можно загрузить здесь: http://david.blob.core.windows.net/win8/TheRealCSSGridStory.zip 

Skip to main content