Как написать тетрис
Перейти к содержимому

Как написать тетрис

  • автор:

Как написать свой Тетрис на Java за полчаса

В предыдущих статьях этой серии мы уже успели написать сапёра, змейку и десктопный клон игры 2048. Попробуем теперь написать свой Тетрис.

Нам, как обычно, понадобятся:

  • 30 минут свободного времени;
  • Настроенная рабочая среда, т.е. JDK и IDE (например, Eclipse);
  • Библиотека LWJGL (версии 2.x.x) для работы с графикой (опционально). Обратите внимание, что для LWJGL версий выше 3 потребуется написать код, отличающийся от того, что приведён в статье;
  • Спрайты, т.е. картинки плиток всех возможных состояний (пустая, и со степенями двойки до 2048). Можно нарисовать самому, или скачать использовавшиеся при написании статьи.

Прежде чем задавать вопрос в комментариях, не забудьте заглянуть в предыдущие статьи, возможно там на него уже давался ответ. Исходный код готового проекта традиционно можно найти на GitHub.

С чего начать?

Начать стоит с главного управляющего класса, который в нашем проекте находится выше остальных по уровню абстракции. Вообще отличный совет — в начале работы всегда пишите код вида if (getKeyPressed()) doSomething() , так вы быстро определите фронт работ.

public static void main(String[] args) < initFields(); while(!endOfGame)< input(); logic(); graphicsModule.draw(gameField); graphicsModule.sync(FPS); >graphicsModule.destroy(); > 

Это наш main() . Он ничем принципиально не отличается от тех, что мы писали в предыдущих статьях — мы всё так же инициализируем поля и, пока игра не закончится, осуществляем по очереди: ввод пользовательских данных ( input() ), основные игровые действия ( logic() ) и вызов метода отрисовки у графического модуля ( graphicsModule.draw() ), в который передаём текущее игровое поле ( gameField ). Из нового разве что метод sync — метод, который должен будет гарантировать нам определённую частоту выполнения итераций. С его помощью мы сможем задать скорость падения фигуры в клетках-в-секунду.

Вы могли заметить, что в коде использована константа FPS . Все константы удобно определять в классе с public static final полями. Полный список констант, который нам потребуется в ходе разработки, можно посмотреть в классе Constants на GitHub.

Оставим пока инициализацию полей на потом (мы же ещё не знаем, какие нам вообще понадобятся поля). Разберёмся сначала с input() и logic() .

Получение данных от пользователя

Код, честно говоря, достаточно капитанский:

private static void input()< /// Обновляем данные модуля ввода keyboardModule.update(); /// Считываем из модуля ввода направление для сдвига падающей фигурки shiftDirection = keyboardModule.getShiftDirection(); /// Считываем из модуля ввода, хочет ли пользователь повернуть фигурку isRotateRequested = keyboardModule.wasRotateRequested(); /// Считываем из модуля ввода, хочет ли пользователь "уронить" фигурку вниз isBoostRequested = keyboardModule.wasBoostRequested(); /// Если был нажат ESC или "крестик" окна, завершаем игру endOfGame = endOfGame || keyboardModule.wasEscPressed() || graphicsModule.isCloseRequested(); > 

Все данные от ввода мы просто сохраняем в соответствующие поля, действия на основе них будет выполнять метод logic() .

Теперь уже потихоньку становится понятно, что нам необходимо. Во-первых, нам нужны клавиатурный и графический модули. Во-вторых, нужно как-то хранить направление, которое игрок выбрал для сдвига. Вторая задача решается просто — создадим enum с тремя состояниями: AWAITING, LEFT, RIGHT . Зачем нужен AWAITING ? Чтобы хранить информацию о том, что сдвиг не требуется (использования в программе null следует всеми силами избегать). Перейдём к интерфейсам.

Интерфейсы для клавиатурного и графического модулей

Так как многим не нравится, что я пишу эти модули на LWJGL, я решил в статье уделить время только интерфейсам этих классов. Каждый может написать их с помощью той GUI-библиотеки, которая ему нравится (или вообще сделать консольный вариант). Я же по старинке реализовал их на LWJGL, код можно посмотреть здесь в папках graphics/lwjglmodule и keyboard/lwjglmodule.

Интерфейсы же, после добавления в них всех упомянутых выше методов, будут выглядеть следующим образом:

public interface GraphicsModule < /** * Отрисовывает переданное игровое поле * * @param field Игровое поле, которое необходимо отрисовать */ void draw(GameField field); /** * @return Возвращает true, если в окне нажат "крестик" */ boolean isCloseRequested(); /** * Заключительные действия, на случай, если модулю нужно подчистить за собой. */ void destroy(); /** * Заставляет программу немного поспать, если последний раз метод вызывался * менее чем 1/fps секунд назад */ void sync(int fps); > 
public interface KeyboardHandleModule < /** * Считывание последних данных из стека событий, если модулю это необходимо */ void update(); /** * @return Возвращает информацию о том, был ли нажат ESCAPE за последнюю итерацию */ boolean wasEscPressed(); /** * @return Возвращает направление, в котором пользователь хочет сдвинуть фигуру. * Если пользователь не пытался сдвинуть фигуру, возвращает ShiftDirection.AWAITING. */ ShiftDirection getShiftDirection(); /** * @return Возвращает true, если пользователь хочет повернуть фигуру. */ boolean wasRotateRequested(); /** * @return Возвращает true, если пользователь хочет ускорить падение фигуры. */ boolean wasBoostRequested(); > 

Отлично, мы получили от пользователя данные. Что дальше?

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

private static void logic() < if(shiftDirection != ShiftDirection.AWAITING)< // Если есть запрос на сдвиг фигуры /* Пробуем сдвинуть */ gameField.tryShiftFigure(shiftDirection); /* Ожидаем нового запроса */ shiftDirection = ShiftDirection.AWAITING; >if(isRotateRequested) < // Если есть запрос на поворот фигуры /* Пробуем повернуть */ gameField.tryRotateFigure(); /* Ожидаем нового запроса */ isRotateRequested = false; >/* Падение фигуры вниз происходит если loopNumber % FRAMES_PER_MOVE == 0 * Т.е. 1 раз за FRAMES_PER_MOVE итераций. */ if( (loopNumber % (FRAMES_PER_MOVE / (isBoostRequested ? BOOST_MULTIPLIER : 1)) ) == 0) gameField.letFallDown(); /* Увеличение номера итерации (по модулю FPM)*/ loopNumber = (loopNumber+1)% (FRAMES_PER_MOVE); 

Сюда же добавим проверку на переполнение поля (в Тетрисе игра завершается, когда фигурам некуда падать):

/* Если поле переполнено, игра закончена */ endOfGame = endOfGame || gameField.isOverfilled(); > 

Так, а теперь мы напишем класс для того магического gameField, в который мы всё это передаём, да?

Не совсем. Сначала мы пропишем поля класса Main и метод initFields() , чтобы совсем с ним закончить. Вот все поля, которые мы использовали:

/** Флаг для завершения основного цикла программы */ private static boolean endOfGame; /** Графический модуль игры*/ private static GraphicsModule graphicsModule; /** "Клавиатурный" модуль игры, т.е. модуль для чтения запросов с клавиатуры*/ private static KeyboardHandleModule keyboardModule; /** Игровое поле. См. документацию GameField */ private static GameField gameField; /** Направление для сдвига, полученное за последнюю итерацию */ private static ShiftDirection shiftDirection; /** Был ли за последнюю итерацию запрошен поворот фигуры */ private static boolean isRotateRequested; /** Было ли за последнюю итерацию запрошено ускорение падения*/ private static boolean isBoostRequested; /** Номер игровой итерации по модулю FRAMES_PER_MOVE. * Падение фигуры вниз происходит если loopNumber % FRAMES_PER_MOVE == 0 * Т.е. 1 раз за FRAMES_PER_MOVE итераций. */ private static int loopNumber; 

А инициализировать мы их будем так:

private static void initFields()

Если вы решили не использовать LWJGL и написали свои классы, реализующие GraphicsModule и KeyboardHandleModule , то здесь нужно указать их конструкторы вместо, соответственно new LwjglGraphicsModule() и new LwjglKeyboardHandleModule() .

А вот теперь мы переходим к классу, который отвечает за хранение информации об игровом поле и её обновление.

Класс GameField

Этот класс должен, во-первых, хранить информацию о поле и о падающей фигуре, а во-вторых, содержать методы для их обновления, и получения о них информации — кроме тех, которые мы уже использовали, необходимо написать метод, возвращающий цвет ячейки по координатам, чтобы графический модуль мог отрисовать поле.

Начнём по порядку.

Хранить информацию о поле…
/** Цвета ячеек поля. Для пустых ячеек используется константа EMPTINESS_COLOR */ private TpReadableColor[][] theField; /** Количество непустых ячеек строки. * Можно было бы получать динамически из theField, но это дольше. */ private int[] countFilledCellsInLine; 
…и о падающей фигуре
/** Информация о падающей в данный момент фигуре */ private Figure figure; 

TpReadableColor — простой enum, содержащий элементы с говорящими названиями (RED, ORANGE и т.п.) и метод, позволяющий получить случайным образом один из этих элементов. Ничего особенного в нём нет, код можно посмотреть тут.

Это все поля, которые нам понадобятся. Как известно, поля любят быть инициализированными.
Сделать это следует в конструкторе.

Конструктор и инициализация полей
public GameField() < spawnNewFigure(); theField = new TpReadableColor[COUNT_CELLS_X][COUNT_CELLS_Y+OFFSET_TOP]; countFilledCellsInLine = new int[COUNT_CELLS_Y+OFFSET_TOP];

“Что это за OFFSET_TOP ?” — спросите вы. OFFSET_TOP это количество неотображаемых ячеек сверху, в которых создаются падающие фигуры. Если фигуре не сможет “выпасть” из этого пространства, и хоть одна ячеек theField выше уровня COUNT_CELLS_Y будет заполнена, это будет обозначать, что поле переполнено и пользователь проиграл, поэтому OFFSET_TOP должен быть строго больше нуля.

Далее в конструкторе стоит заполнить массив theField значениями константы EMPTINESS_COLOR , а countFilledCellsInLine — нулями (второе в Java не требуется, при инициализации массива все int‘ы равны 0). Или можно создать несколько слоёв уже заполненных ячейкам — на GitHub вы можете увидеть реализацию именно второго варианта.

А что это там за spawnNewFigure()? Почему инициализация фигуры вынесена в отдельный метод?

Вы правильно догадались, spawnNewFigure() действительно инициализирует поле figure . А в отдельный метод это вынесено, потому что нам придётся делать инициализацию каждый раз, когда будет создаваться новая фигура.

/** * Создаёт новую фигуру в невидимой зоне * X-координата для генерации не должна быть ближе к правому краю, * чем максимальная ширина фигуры (MAX_FIGURE_WIDTH), чтобы влезть в экран */ private void spawnNewFigure()

На этом с хранением данных мы закончили. Переходим к методам, которые отдают информацию о поле другим классам.

Методы, передающие информацию об игровом поле

Таких метода всего два. Первый возвращает цвет ячейки (для графического модуля):

public TpReadableColor getColor(int x, int y)

А второй сообщает, переполнено ли поле (как это происходит, мы разобрали выше):

public boolean isOverfilled() < for(int i = 0; i < OFFSET_TOP; i++)< if(countFilledCellsInLine[COUNT_CELLS_Y+i] != 0) return true; >return false; > 
Методы, обновляющие фигуру и игровое поле

Начнём реализовывать методы, которые мы вызывали из Main.logic() .

Сдвиг фигуры

За это отвечает метод tryShiftFigure() . В комментариях к его вызову из Main было сказано, что он “пробует сдвинуть фигуру”. Почему пробует? Потому что если фигура находится вплотную к стене, а пользователь пытается её сдвинуть в направлении этой стены, никакого сдвига в реальности происходить не должно. Так же нельзя сдвинуть фигуру в статические ячейки на поле.

public void tryShiftFigure(ShiftDirection shiftDirection) < Coord[] shiftedCoords = figure.getShiftedCoords(shiftDirection); boolean canShift = true; for(Coord coord: shiftedCoords) < if((coord.y=COUNT_CELLS_Y+OFFSET_TOP) ||(coord.x=COUNT_CELLS_X) || ! isEmpty(coord.x, coord.y)) < canShift = false; >> if(canShift) < figure.shift(shiftDirection); >> 

Что мы сделали в этом методе? Мы запросили у фигуры ячейки, которые бы она заняла в случае сдвига. А затем для каждой из этих ячеек мы проверяем, не выходит ли она за пределы поля, и не находится ли по её координатам в сетке статичный блок. Если хоть одна ячейка фигуры выходит за пределы или пытается встать на место блока — сдвига не происходит. Coord здесь — класс-оболочка с двумя публичными числовыми полями (x и y координаты).

Поворот фигуры

Логика аналогична сдвигу:

Coord[] rotatedCoords = figure.getRotatedCoords(); boolean canRotate = true; for(Coord coord: rotatedCoords) < if((coord.y=COUNT_CELLS_Y+OFFSET_TOP) ||(coord.x=COUNT_CELLS_X) ||! isEmpty(coord.x, coord.y)) < canRotate = false; >> if(canRotate)
Падение фигуры

Сначала код в точности повторяет предыдущие два метода:

public void letFallDown() < Coord[] fallenCoords = figure.getFallenCoords(); boolean canFall = true; for(Coord coord: fallenCoords) < if((coord.y=COUNT_CELLS_Y+OFFSET_TOP) ||(coord.x=COUNT_CELLS_X) ||! isEmpty(coord.x, coord.y)) < canFall = false; >> if(canFall) < figure.fall();

Однако теперь, в случае, если фигура дальше падать не может, нам необходимо перенести её ячейки («кубики») в theField , т.е. в разряд статичных блоков, после чего создать новую фигуру:

> else < Coord[] figureCoords = figure.getCoords(); /* Флаг, говорящий о том, что после будет необходимо сместить линии вниз * (т.е. какая-то линия была уничтожена) */ boolean haveToShiftLinesDown = false; for(Coord coord: figureCoords) < theField[coord.x][coord.y] = figure.getColor(); /* Увеличиваем информацию о количестве статичных блоков в линии*/ countFilledCellsInLine[coord.y]++; /* Проверяем, полностью ли заполнена строка Y * Если заполнена полностью, устанавливаем haveToShiftLinesDown в true */ haveToShiftLinesDown = tryDestroyLine(coord.y) || haveToShiftLinesDown; >/* Если это необходимо, смещаем линии на образовавшееся пустое место */ if(haveToShiftLinesDown) shiftLinesDown(); /* Создаём новую фигуру взамен той, которую мы перенесли*/ spawnNewFigure(); > 

Так как в результате переноса ячеек какая-то линия может заполниться полностью, после каждого добавления ячейки мы проверяем линию, в которую мы её добавили, на полноту:

private boolean tryDestroyLine(int y) < if(countFilledCellsInLine[y] < COUNT_CELLS_X)< return false; >for(int x = 0; x < COUNT_CELLS_X; x++)< theField[x][y] = EMPTINESS_COLOR; >/* Не забываем обновить мета-информацию! */ countFilledCellsInLine[y] = 0; return true; > 

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

private void shiftLinesDown() < /* Номер обнаруженной пустой линии (-1, если не обнаружена) */ int fallTo = -1; /* Проверяем линии снизу вверх*/ for(int y = 0; y < COUNT_CELLS_Y; y++)< if(fallTo == -1)< //Если пустот ещё не обнаружено if(countFilledCellsInLine[y] == 0) fallTo = y; //. пытаемся обнаружить (._.) >else < //А если обнаружено if(countFilledCellsInLine[y] != 0)< // И текущую линию есть смысл сдвигать. /* Сдвигаем. */ for(int x = 0; x < COUNT_CELLS_X; x++)< theField[x][fallTo] = theField[x][y]; theField[x][y] = EMPTINESS_COLOR; >/* Не забываем обновить мета-информацию*/ countFilledCellsInLine[fallTo] = countFilledCellsInLine[y]; countFilledCellsInLine[y] = 0; /* * В любом случае линия сверху от предыдущей пустоты пустая. * Если раньше она не была пустой, то сейчас мы её сместили вниз. * Если раньше она была пустой, то и сейчас пустая -- мы её ещё не заполняли. */ fallTo++; > > > > 

Теперь GameField реализован почти полностью — за исключением геттера для фигуры. Её ведь графическому модулю тоже придётся отрисовывать:

public Figure getFigure()

Теперь нам нужно написать алгоритмы, по которым фигура определяет свои координаты в разных состояниях. Да и вообще весь класс фигуры.

Класс фигуры

Реализовать это всё я предлагаю следующим образом — хранить для фигуры (1) «мнимую» координату, такую, что все реальные блоки находятся ниже и правее неё, (2) состояние поворота (их всего 4, после 4-х поворотов фигура всегда возвращается в начальное положение) и (3) маску, которая по первым двум параметрам будет определять положение реальных блоков:

/** * Мнимая координата фигуры. По этой координате * через маску генерируются координаты реальных * блоков фигуры. */ private Coord metaPointCoords; /** * Текущее состояние поворота фигуры. */ private RotationMode currentRotation; /** * Форма фигуры. */ private FigureForm form; 

Rotation мод здесь будет выглядеть таким образом:

public enum RotationMode < /** Начальное положение */ NORMAL(0), /** Положение, соответствующее повороту против часовой стрелки*/ FLIP_CCW(1), /** Положение, соответствующее зеркальному отражению*/ INVERT(2), /** Положение, соответствующее повороту по часовой стрелке (или трём поворотам против)*/ FLIP_CW(3); /** Количество поворотов против часовой стрелки, необходимое для принятия положения*/ private int number; /** * Конструктор. * * @param number Количество поворотов против часовой стрелки, необходимое для принятия положения */ RotationMode(int number)< this.number = number; >/** Хранит объекты enum'а. Индекс в массиве соответствует полю number. * Для более удобной работы getNextRotationForm(). */ private static RotationMode[] rotationByNumber = ; /** * Возвращает положение, образованое в результате поворота по часовой стрелке * из положения perviousRotation * * @param perviousRotation Положение из которого был совершён поворот * @return Положение, образованное в результате поворота */ public static RotationMode getNextRotationFrom(RotationMode perviousRotation) < int newRotationIndex = (perviousRotation.number + 1) % rotationByNumber.length; return rotationByNumber[newRotationIndex]; >> 

Соответственно, от самого класса Figure нам нужен только конструктор, инициализирующий поля:

/** * Конструктор. * Состояние поворота по умолчанию: RotationMode.NORMAL * Форма задаётся случайная. * * @param metaPointCoords Мнимая координата фигуры. См. документацию одноимённого поля */ public Figure(Coord metaPointCoords) < this(metaPointCoords, RotationMode.NORMAL, FigureForm.getRandomForm()); >public Figure(Coord metaPointCoords, RotationMode rotation, FigureForm form) < this.metaPointCoords = metaPointCoords; this.currentRotation = rotation; this.form = form; >> 

И методы, которыми мы пользовались в GameField следующего вида:

/** * @return Координаты реальных ячеек фигуры в текущем состоянии */ public Coord[] getCoords() < return form.getMask().generateFigure(metaPointCoords, currentRotation); >/** * @return Координаты ячеек фигуры, как если бы * она была повёрнута проти часовой стрелки от текущего положения */ public Coord[] getRotatedCoords() < return form.getMask().generateFigure(metaPointCoords, RotationMode.getNextRotationFrom(currentRotation)); >/** * Поворачивает фигуру против часовой стрелки */ public void rotate() < this.currentRotation = RotationMode.getNextRotationFrom(currentRotation); >/** * @param direction Направление сдвига * @return Координаты ячеек фигуры, как если бы * она была сдвинута в указано направлении */ public Coord[] getShiftedCoords(ShiftDirection direction) < Coord newFirstCell = null; switch (direction)< case LEFT: newFirstCell = new Coord(metaPointCoords.x - 1, metaPointCoords.y); break; case RIGHT: newFirstCell = new Coord(metaPointCoords.x + 1, metaPointCoords.y); break; default: ErrorCatcher.wrongParameter("direction (for getShiftedCoords)", "Figure"); >return form.getMask().generateFigure(newFirstCell, currentRotation); > /** * Меняет мнимую X-координату фигуры * для сдвига в указаном направлении * * @param direction Направление сдвига */ public void shift(ShiftDirection direction) < switch (direction)< case LEFT: metaPointCoords.x--; break; case RIGHT: metaPointCoords.x++; break; default: ErrorCatcher.wrongParameter("direction (for shift)", "Figure"); >> /** * @return Координаты ячеек фигуры, как если бы * она была сдвинута вниз на одну ячейку */ public Coord[] getFallenCoords() < Coord newFirstCell = new Coord(metaPointCoords.x, metaPointCoords.y - 1); return form.getMask().generateFigure(newFirstCell, currentRotation); >/** * Меняет мнимую Y-координаты фигуры * для сдвига на одну ячейку вниз */ public void fall()

Вдобавок, у фигуры должен быть цвет, чтобы графический модуль мог её отобразить. В тетрисе каждой фигуре соответствует свой цвет, поэтому цвет мы будем запрашивать у формы:

public TpReadableColor getColor()

Форма фигуры и маски координат

Чтобы не занимать лишнее место, здесь я приведу реализацию только для двух форм: I-образной и J-образной. Код для остальных фигур принципиально не отличается и выложен на GitHub.

Храним для каждой фигуры маску координат (которая определяет, насколько каждый реальный блок должен отстоять от “мнимой” координаты фигуры) и цвет:

public enum FigureForm < I_FORM (CoordMask.I_FORM, TpReadableColor.BLUE), J_FORM (CoordMask.J_FORM, TpReadableColor.ORANGE); /** Маска координат (задаёт геометрическую форму) */ private CoordMask mask; /** Цвет, характерный для этой формы */ private TpReadableColor color; FigureForm(CoordMask mask, TpReadableColor color)

Реализуем методы, которые использовали выше:

/** * Массив со всеми объектами этого enum'а (для удобной реализации getRandomForm() ) */ private static final FigureForm[] formByNumber = ; /** * @return Маска координат данной формы */ public CoordMask getMask() < return this.mask; >/** * @return Цвет, специфичный для этой формы */ public TpReadableColor getColor() < return this.color; >/** * @return Случайный объект этого enum'а, т.е. случайная форма */ public static FigureForm getRandomForm()

Ну а сами маски координат я предлагаю просто захардкодить следующим образом:

/** * Каждая маска -- шаблон, который по мнимой координате фигуры и * состоянию её поворота возвращает 4 координаты реальных блоков * фигуры, которые должны отображаться. * Т.е. маска задаёт геометрическую форму фигуры. * * @author DoKel * @version 1.0 */ public enum CoordMask < I_FORM( new GenerationDelegate() < @Override public Coord[] generateFigure(Coord initialCoord, RotationMode rotation) < Coord[] ret = new Coord[4]; switch (rotation)< case NORMAL: case INVERT: ret[0] = initialCoord; ret[1] = new Coord(initialCoord.x , initialCoord.y - 1); ret[2] = new Coord(initialCoord.x, initialCoord.y - 2); ret[3] = new Coord(initialCoord.x, initialCoord.y - 3); break; case FLIP_CCW: case FLIP_CW: ret[0] = initialCoord; ret[1] = new Coord(initialCoord.x + 1, initialCoord.y); ret[2] = new Coord(initialCoord.x + 2, initialCoord.y); ret[3] = new Coord(initialCoord.x + 3, initialCoord.y); break; >return ret; > > ), J_FORM( new GenerationDelegate() < @Override public Coord[] generateFigure(Coord initialCoord, RotationMode rotation) < Coord[] ret = new Coord[4]; switch (rotation)< case NORMAL: ret[0] = new Coord(initialCoord.x + 1 , initialCoord.y); ret[1] = new Coord(initialCoord.x + 1, initialCoord.y - 1); ret[2] = new Coord(initialCoord.x + 1, initialCoord.y - 2); ret[3] = new Coord(initialCoord.x, initialCoord.y - 2); break; case INVERT: ret[0] = new Coord(initialCoord.x + 1 , initialCoord.y); ret[1] = initialCoord; ret[2] = new Coord(initialCoord.x, initialCoord.y - 1); ret[3] = new Coord(initialCoord.x, initialCoord.y - 2); break; case FLIP_CCW: ret[0] = initialCoord; ret[1] = new Coord(initialCoord.x + 1, initialCoord.y); ret[2] = new Coord(initialCoord.x + 2, initialCoord.y); ret[3] = new Coord(initialCoord.x + 2, initialCoord.y - 1); break; case FLIP_CW: ret[0] = initialCoord; ret[1] = new Coord(initialCoord.x, initialCoord.y - 1); ret[2] = new Coord(initialCoord.x + 1, initialCoord.y - 1); ret[3] = new Coord(initialCoord.x + 2, initialCoord.y - 1); break; >return ret; > > ); /** * Делегат, содержащий метод, * который должен определять алгоритм для generateFigure() */ private interface GenerationDelegate < /** * По мнимой координате фигуры и состоянию её поворота * возвращает 4 координаты реальных блоков фигуры, которые должны отображаться * * @param initialCoord Мнимая координата * @param rotation Состояние поворота * @return 4 реальные координаты */ Coord[] generateFigure(Coord initialCoord, RotationMode rotation); >private GenerationDelegate forms; CoordMask(GenerationDelegate forms) < this.forms = forms; >/** * По мнимой координате фигуры и состоянию её поворота * возвращает 4 координаты реальных блоков фигуры, которые должны отображаться. * * Запрос передаётся делегату, спецефичному для каждого объекта enum'а. * * @param initialCoord Мнимая координата * @param rotation Состояние поворота * @return 4 реальные координаты */ public Coord[] generateFigure(Coord initialCoord, RotationMode rotation) < return this.forms.generateFigure(initialCoord, rotation); >> 

Т.е. для каждого объекта enum‘а мы передаём с помощью импровизированных (других в Java нет) делегатов метод, в котором в зависимости от переданного состояния поворота возвращаем разные реальные координаты блоков. В общем-то, можно обойтись и без делегатов, если хранить в каждом элементе отсупы для каждого из режимов поворота.

Наслаждаемся результатом

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

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

Ещё раз напомню, что исходники готового проекта доступны на GitHub.

Следите за новыми постами по любимым темам

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

Тетрис для начинающих 🙂

Что стало причиной для написания этой статьи?
1 Задаваемые вопросы пользователей этого сайта о том как написать тетрис.
2 Недавно написанный мной непонятно по каким причинам и для каких целей тетрис. В детстве у меня небыли попыток писать тетрисы. Немного повзрослев решил реализовать детскую мечту 🙂
3 Банальное желание показать себе и всему миру какой я замечательный программист и так я круто умею писать тетрисы 🙂
Все хорош трепаться перейдем к делу.

Для начала нам нужно представить из каких частей будет состоять наша игра. В тетрисе есть фигурки (class Figure) и поле (class Field). Еще нам нужен мир (class World) он будет содержать поле, 2 фигуры (падающую и следующую), очки, уровень и еще кое какую информацию.

Давайте подумаем из чего будет состоять наша фигурка. Есть несколько вариантов. Мы выберем попроще. Возьмем за основу что наша фигурка это какая то матрица размерности N на M содержащая пустые и заполненные клетки. Теперь нужно определится что из себя представляет клетка?! По сути от нее нам нужно знать заполнена она или нет. Значит для представления ячейки нам достаточно типа «BOOL». Так же фигурка содержит информацию о цвете и позицию на игровом поле. Из всего сказанного вот какая структура у нас получается:

Class Figure < public: bool body_[MAX_FIGURE_LEN][MAX_FIGURE_LEN]; unsigned int color; float posX, posy; >;

Что здесь нам не нравится? Во-первых сама матрица. Поскольку при таком подходе нам нужно делать ее такого размера, что бы в нее поместилась самая большая фигурка. Самой большой фигуркой у нас будет состоять в длину из 4 клеточек и в ширину из 1. В тетрисе есть возможность переворачивает фигурки отсюда мы получаем квадратную матрицу размерностью 4 на 4. Недостатком такой матрицы является «сложность» проверки при вертикальных и горизонтальных перемещениях. Это связано с тем что часть фигуры будет выходить за пределы нашего поля. Исходя из этого переделаем нашу матрицу из статической в динамическую.

tepedef std::vector cell_v; Class Figure < public: cell_v body_; int width_; int height_; float posX, posy; >;

В этом варианте мы заменили тип BOOL на UNSIGNED INT это сделано было потому что STL специфически реализовывает STD::VECTOR (об этом вы почитаете где ни будь в другом месте). Наша матрица стала одномерной. Так как конструкция STD::VECTOR < STD::VECTOR> выглядит немного «угрожающе» у нас будет она одномерной. Создавшиеся неудобства с получением нужного нам элемента матрицы решаются очень просто всего лишь добавлением одной маленькой функции:

int Figure::XYToIdx(const int x, const int y) < int idx = y*width_+x; return idx >= width_*height_ ? -1 : idx; >

Все. Мы снова можем работать с нашей матрицей как с двумерным массивом.
В классе Figure произошли еще кое какие изменения. Пропал цвет нашей фигуры. Об этом мы поговорим в другом разделе.

И так! Наше поле имеет размерность N на M собственно как и фигура. Как и фигура оно должно содержать информацию о том пуста клетка или нет. А так же цвет этой клетки. Цвет клетки мы будем кодировать в unsigned int (0xAARRGGBB). Теперь ниша клетка хранить в себе и цвет и информацию о том пустая они или нет. Если в клетке не ноль значит она заполненная если ноль то пустая. Вот так выглядит наше поле:

Class Field < public: cell_v body_; int width_; int height_; int XYToIdx(const int x, const int y); >;

Вы скажете что оно не многим отличается от фигуры. И будете совершенно правы. Предлагаю все общее из наших 2 классов объединить в одну сущность:

class GameObj < public: GameObj(const int w, const int h); virtual ~GameObj() <>int Width() const < return width_; >int Height() const < return height_; >int XYToIdx(const int x, const int y); bool IsCellEmpty(const int x, const int y); protected: cell_v body_; int width_; int height_; >;
class Figure : public GameObj < public: Figure(Field* field, int w, int h); void Draw(const hgeVector& offset); void Update(float dt); bool IsFly() const < return fly_; >const cell_v& Body() const < return body_; >const hgeVector& Position() const < return pos_; >void SetPosition(const hgeVector& pos) < pos_ = pos; >void RotateLeft(); void SetSpeedHorizontal(const float speed) < speed_.x = speed; >float SpeedHorizontal() const < return speed_.x; >void SetSpeedVertical(const float speed) < speed_.y = speed; >float SpeedVertical() const < return speed_.y; >void MoveTo(int delta); private: Field* field_; hgeVector pos_; bool fly_; hgeVector speed_; int destX_; bool needMove_; >;
class Field : public GameObj < public: Field(World* world, const int w, const int h); void DrawBkg(); void DrawField(); void DrawWell(); void Update(float dt); void Clear(); bool Hit(Figure* figure); bool Collision(Figure* figure); void SetFigure(Figure* figure); World* GetWorld() const < return world_; >void KillLine(); private: World* world_; >;

Теперь мы видим что наше поле и фигура наследуются от GameObj. А в классах Figure и Field остались только те вещи которые их от кардинально отличают друг от друга. Из последнего листинга класса Figure, Field видно что в них «волшебным» образом мы добавили больше деталей. Давайте поговорим теперь о них.

Подробный разбор Figure

В этот класс мы добавили скорость. Причем не одну а сразу 2 (горизонтальную и вертикальную). Почему 2. Потому что наша фигурка при нажатии (в классике это кнопка пробел) волшебной кнопки фигурка ускоряет свое падение. А горизонтальные перемещения у нас происходят с постоянной скоростью. Но на всякий случай мы реализуем возможность изменять и их (тем более что это не тяжело). Флажок «fly_» находясь в значении «true» говорит нам что фигурка еще падает (ей управляет игрок) и не заняла свае место на поле. «needMove_» говорит нам о том что фигурка в данный момент перемещается в лево либо в право. Переменная «destX_» указывает горизонтальную позицию куда должна переместится фигура на нашем поле. Из функций в этом классе самыми интересными являются MoveTo, RotateLeft и Update. MoveTo получает на вход направление в котором нам нужно переместится по горизонтали на одну клетку и делает проверки возможно ли это перемещение. Если да то выставляет значение переменной «destX_» и включает флажок «needMove_». Реализация:

void Figure::MoveTo(int delta) < if(needMove_) return; const int x = pos_.x + delta; if(x < 0 || x+width_ >field_->Width()) return; Figure temp(field_, width_, height_); temp.body_ = body_; temp.pos_ = pos_ + hgeVector(delta, 0); if(field_->Collision(&temp)) return; if(field_->Hit(&temp)) return; destX_ = x; needMove_ = true; >

Думаю здесь объяснять особо нечего и так все понятно. Хочу лишь заметить, что класс фигурке содержит указатель на класс поля. Этот указатель он получает в конструкторе. При таком подходе мы легко можем вызывать нужные нам функции класса Field что собственно мы с успехом и делаем.
RotateLeft функция поворачивает нашу фигурку в лево.

void Figure::RotateLeft() < if(pos_.y+width_ >field_->Height()) return; Figure newFigure(field_, height_, width_); for(int y=0; y > newFigure.pos_ = pos_; if(newFigure.pos_.x + newFigure.Width() >= field_->Width()) newFigure.pos_.x = field_->Width() - newFigure.Width() - 1; if(field_->Hit(&newFigure)) return; body_ = newFigure.body_; pos_ = newFigure.pos_; height_ = newFigure.Height(); width_ = newFigure.Width(); >

Как это работает?! Создаем новую фигурку Figure newFigure(field_, height_, width_). Заносим в новую фигуру данные с нашей матрицы с учетом поворота. Устанавливаем позицию и проверяем на коллизию с полем. Если произошла коллизия, то повернуть фигурку не возможно. Если нет, то с чистой совестью меняем данные нашей фигурки на данные повернутой.
Update тут мы двигаем нашу фигурку по горизонтали и вертикали а так же проверяем на возможность ее дальнейшего падения. Если падать больше некуда, то данные с нашей фигурки переносятся на поле и фигурка помечается как упавшая «fly_ = false;»

void Figure::Update(float dt) < if(!fly_) return; if(needMove_) < const float dist = destX_ - pos_.x; if(abs(dist) < dt * SpeedHorizontal()) < pos_.x = destX_; needMove_ = false; >else pos_.x += sign(dist) * dt * SpeedHorizontal(); > int y = pos_.y; pos_.y += dt*SpeedVertical(); if(y != static_cast(pos_.y) && (field_->Hit(this) || pos_.y+Height()+1 >= field_->Height())) < field_->SetFigure(this); fly_ = false; field_->KillLine(); > >

Подробный разбор Field

В этом классе рассказывать особо не о чем. Просто перечислю самые интересные функции и расскажу что они делают.
Hit – проверяет есть ли куда падать фигурке или под ней уже занятое пространства
Collision – проверка на пересечение с уже занятыми клетками поля и фигурки.
SetFigure – переносит данные фигурки на поле.
KillLine – проверяет на возможность снятия линии. Если токовая имеется снимает линию, осыпает верхние клеточки вниз, прибавляет очки и увеличивает счетчик снятых линий.

Собственно мы до сих пор не коснулись класса World. Вот он:

class World < public: static void DrawCell(World* world, const cell_t cell, const int x, const int y, const hgeVector& offset); World(HGE* renderer); virtual ~World(); int CellWidth() const; int CellHeight() const; int ScoreForCell() const; float SpeedHorizontal(); float NormalSpeedDown(); float FastSpeedDown(); void Draw(); void Update(float dt); HGE* Renderer() const < return renderer_; >const hgeVector& Position() const < return pos_; >void GenerateNext(); void MoveLeftFigure(); void MoveRightFigure(); void MoveDownFigure(); void RotateLeftFigure(); void AddScore(int score); int GetScores() const < return scores_; >void IncKillLine(); bool IsGameOver() const < return gameOver_; >private: HGE* renderer_; hgeFont* fnt_; hgeVector pos_; Field* field_; Figure* figure_; Figure* next_; int scores_; int level_; int killLines_; bool gameOver_; >;

Не чего интересного в этом классе нет. Исключение функция GenerateNext.

void World::GenerateNext() < if(next_) return; int colors[] = ; int selectColor = colors[renderer_->Random_Int(0, 3)]; int rnd = renderer_->Random_Int(0, 4); switch(rnd) < case 0: next_ = new Figure0(field_, selectColor); break; case 1: next_ = new Figure1(field_, selectColor); break; case 2: next_ = new Figure2(field_, selectColor); break; case 3: next_ = new Figure3(field_, selectColor); break; case 4: next_ = new Figure4(field_, selectColor); break; >next_->SetSpeedVertical(NORMAL_SPEED_DOWN); >

Изучив эту функцию можно заметить что здесь случайно создаются и инициализируются 5 классов у поминания о которых негде не было. Давайте рассмотрим что это за классы. Вот один из них:

class Figure3 : public Figure < public: Figure3(Field* field, cell_t cell); >;

Мы видим, что он наследуется от класса Figure с которым мы уже хорошо знакомы и состоит из одного конструктора. Вот что происходит в конструкторе:

Figure3::Figure3(Field* field, cell_t cell) : Figure(field, 3, 2) < for(int i=0; ibody_[2] = body_[3] = 0; >

В этом конструкторе и создается наша фигурка. Те клетки которые заполнены нулем будут пустыми остальные будут, отображается на экране. Создавая такие классы мы создаем различные фигуры. Вот и все ребята!

Написанный тут тетрис является не законченным продуктом. Он содержит баги и некоторые недоработки. Я конечно же мог все исправить но подумал что бы читателю не было так скучно и он мог развлечь себя возложить на него эту задачу если конечно ему это надо 🙂 Так что дерзайте!
Сложно сказать понятен ли процесс создания игры из этой стати но если не понятен на этот случай прилагаются исходники.
Всем спасибо к то дочитал до сюда и удачи на нелегком поприще 😉

Как написать тетрис

Этот урок возник в связи с тем, что мы делали обзор на другие источники и выяснили, что большинство из них напрямую завязаны на Unity. Мы же хотели бы сделать универсальное и максимально понятное, модульное решение, котороe не зависело от движка и могло быть использовано в любом языке программирования будь то Java, С++

Необходимые знания: двумерные массивы, циклы, создание объектов из кода в Unity

1. Создадим скрипт Tetris и повесим его на Main Camera.

2. Игровое поле зададим 2мерным массивом размером 16x8. Все действия будут происходить именно в массиве, а мы лишь будем визуализировать результат средствами Unity. Необходимо инициализировать его заполнив конкретными значениями, для того, чтобы было проще тестировать наш алгоритм.

3. Объявим двумерный массив из int'ов как глобальную переменную.

void Start() >
void Update() >
>
Это не размер оригинального поля, но вполне достаточно, чтобы тестировать основные моменты. В таком массиве
0 - пустое место,
1 - фигура, которая падает,
2 - блоки, которые уже упали.

4. Следующим шагом будет визуализация нашего массива. Для этого понадобится картинка блока тетриса размером 100x100 пикселей, чтобы в мире размер одного блока составлял 1x1 метр и нам не нужно было масштабировать наши блоки.

5. Из этого sprite создадим prefab. Можно пометить его static для частичной оптимизации.

6. В нашем тетрисе получим к нему доступ объявив публичную переменную
public GameObject pfbBlock;

7. Идея визуализации такова: мы изначально создадим все поле из квадратов, а потом будем включать и выключать их в зависимости от содержимого массива pole.

8. Нам очевидно потребуется получать доступ к этим блока и где-то их хранить. Будем их хранить в двумерном массиве из GameObject

9. Создадим ф-ю Fill, котороя и создаст все игровые блоки и поместит их в двумерный массив allBlocks.

10. Внутри Fill
allBlocks = new GameObjec t[ 16,8 ];
for ( int y= 0 ;y < 16 ;y++)for ( int x= 0 ;x < 8 ;x++)allBlocks[y,x] = GameObject .Instanciate( blockPfb ); //создаем и помещаем кубик в массив
allBlocks[y,x].transform.position = new Vector3 ( x, 15 - y, 0 ); //мы специально отнимаем от 15 y иначе наше поле нарисуется отраженным сверху вниз, потому что координаты в массиве идут вниз и вправо, а в Unity вверх и вправо.
>
>

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

12. Создадим ф-ю Draw без параметров ничего не возвращающую

13. Вызываем ф-ю Draw в Update;

14. Внутри Draw будем анилизировать наш массив pole и в зависимости от того какое в нем число включать или выключать нужный кубик на экране. Для того чтобы пройтить по двумерному массиву нам понадобится for внутри for сначала по строкам, потом по x.
for ( int y = 0 ; y < 16 ; y++) for ( int x = 0 ; x < 8 ; x++) if ( pole[y,x] > 0 ) blocks[y,x].SetActive( true );
>
else blocks[y,x].SetActive( false );
>
>
>

15. После запуска игры вы должны увидеть ваше игровое поле на экране.

Как написать «Тетрис»

Создатель «Тетриса» Алексей Пажитнов о том, как эта компьютерная игра стала популярной, и о преимуществах игр перед остальными медиа

Detailed_picture

© Кирилл Гатаван / Colta.ru

В этом году исполнилось 30 лет «Тетрису» — одной из самых известных компьютерных игр в мире, придуманной российским программистом Алексеем Пажитновым. Илья Воронин поговорил для COLTA.RU с создателем «Тетриса», который рассказал историю появления этой игры, почему она стала популярной, как он акклиматизировался в Америке и что будет с компьютерными играми в будущем.

В России всегда было очень плохо с развлечениями. Я не знаю, с чем это связано, тут, мне кажется, лучше спросить философов. Быть может, причина в тяге к духовной жизни, которая была довольно ревнивой и легкомысленных развлечений не допускала. Быть может, в излишней религиозности. Я не знаю почему, но с развлечениями было плохо, а в советское время плохо было особенно, поскольку все было забито идеологией. Было такое ощущение, что люди жили ради серьезных свершений, а заниматься различными пустяками в лучшем случае считалось слегка простительной слабостью, в худшем — расценивалось как непростительное легкомыслие. Однако в мире отношение к развлечениям куда более серьезное. Они считаются частью культуры, частью самовыражения людей.

После четвертого курса МАИ я попал на практику в Вычислительный центр АН СССР, в лабораторию технической кибернетики. Мой руководитель, Валериан Николаевич Трунин-Донской, был очень крупным специалистом в области автоматического распознавания речи. Я ужасно любил программировать, а что именно программировать, мне тогда было неважно. Но мне понравилось в ВЦ, мне понравились люди, которые там работали, — настоящая русская интеллигенция. Советскую власть там терпеть не могли, и все старались слушать «голоса». Академия наук тогда была чем-то вроде островка свободы, поскольку не была засекречена, как большинство технических учреждений, и люди там себя чувствовали чуть более вольготно. В итоге я там прижился, просидел весь пятый курс, делал курсовые работы, дипломную работу. Из МАИ обычно распределяли по закрытым предприятиям, но поскольку я довольно неплохо учился, то меня взяли на кафедру, а уже оттуда я при первой возможности убежал в ВЦ. Должен был там засесть за написание диссертации, но моя лень оказалась сильнее меня.

В то время распознавание речи все-таки было больше про акустику на эвристическом уровне. Ведь речь сама по себе — это довольно сложное физическое явление, поскольку человеческая носоглотка — очень сложная акустическая система с постоянно меняющимися параметрами. Кажется, что стройной теории о том, что такое речь, не существует до сих пор. Подобного рода исследования и работы требуют от ученого железной задницы, то есть невероятной усидчивости. Конечно, я занимался этой работой, но сердце мое к ней не лежало, и как только появлялся повод заняться чем-нибудь другим, я сразу же за это брался. Начальник нашей лаборатории был этакий Самоделкин — ужасно любил сам паять, придумывать какие-то маленькие штучки. А поскольку ВЦ был открытым учреждением, то нам постоянно присылали какие-то новые компьютеры, процессоры и прочее. Для того чтобы понять, как устроено то или иное новое «железо», для него сначала нужно было написать программу, поэтому я обычно был на подхвате. И со временем я стал полноценным программистом, но на более глубоком уровне — стал разбираться в принципах устройства микросхем, как работают осциллографы и тому подобные вещи. Надо понимать, что работал я тогда часов 60 в неделю, не меньше, — работал с утра до ночи. Но мне это нравилось, поскольку я обожал компьютеры, я считал эту работу большим счастьем.

Я прикинул, где счастье на компьютере ближе всего, и понял, что это игры.

Мой опыт в программировании в итоге склонил меня к играм. Как только на «Электронике-60» появилась техническая возможность (монитор, принтер, флоппи-дисководы и прочее) запустить операционную систему, я начал делать небольшие программки. «Тетрис» не был первой моей игрой — до него я сделал штук пять или шесть игрушек. Какие-то игры я придумал самостоятельно, какие-то воссоздал по рассказам очевидцев.

Дизайн «Тетриса» прошел довольно легко и удачно. Решения, которые я принимал на дизайнерском уровне, были более-менее правильными. Вся разработка «Тетриса» у меня прошла очень быстро, недели две-три, не больше. Сегодня что-то подобное любой программист может сделать за день. Эту игру я делал в 1984 году.

В самой первой версии «Тетриса» графики не было вообще, поскольку мониторы все-таки тогда были не так широко распространены. С графикой работать не имело никакого смысла, поскольку мои труды мне было бы просто некому показать. Мониторы того времени выглядели совсем смешно — 24 строки по 80 символов в строке. «Тетрис» был создан на компьютере «Электроника-60», клоне американского компьютера PDP-11. Для России это была вполне продвинутая система, поскольку компьютеры семейства PDP (и более мощные, и менее) в то время были широко распространены по всему миру и совместимы между собой. Поэтому я бы не сказал, что этот советский компьютер выглядел слишком сильно морально устаревшим. Скорее это можно было сказать по поводу технической оснащенности — очень плохого качества была советская периферия. Но с точки зрения софта эта система была неплохо оснащена: и матобеспечение было доступно, и первые крошечные операционные системы были.

После создания «Тетриса» я его начал раздавать своим коллегам-инженерам. У нас было нечто вроде клуба, внутри которого мы обменивались не только знаниями, но и «железом», и софтом. Все-таки никаких программ тогда практически не существовало — все делалось самостоятельно. Поэтому «Тетрис» было раздать очень легко. Эта игра настолько широко разошлась, что через какое-то время мне стали давать мою же игру.

«Тетрисом» я занимался в свободное от работы время. А так мне нужно было делать и свою работу, мой начальник постоянно меня дергал, и диссертацию все-таки как-то надо было закончить. Плюс я еще заинтересовался психологией и стал двигаться в этом направлении. Я хочу сказать, что «Тетрис» тогда не стоял у меня в высшем приоритете.

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

В какой-то момент, подустав от акустики, я отправился к психологам и сказал, что у меня есть компьютер, я хороший программист и давайте что-нибудь сделаем вместе. Я устал делать инструменты, нужные для того, чтобы делать другие инструменты, нужные для создания программ, которые принесут деньги, на которые можно будет купить счастье. Я хотел получать счастье прямо с экрана компьютера. Я прикинул, где счастье на компьютере ближе всего, понял, что это игры, какое-то компьютерное творчество (хотя тогда об этом было рано говорить — слишком медленным было техническое оснащение) и разного рода психологические тесты и методики. То, что помогает человеку себя хоть как-то усовершенствовать. «Биограф» стал продуктом, на который я положил ну очень много сил, создавая его вместе с автором собственной методики Александром Александровичем Кроником. «Биограф» — это своеобразное исследование по психологическому времени личности. Кроник считал, что ваше ощущение психологического возраста определяется не временем, которое прошло, или тем, как вы его прожили, а степенью насыщенности этого времени разного рода событиями. И он заставлял человека размышлять о своей жизни в виде последовательности разных событий и анализировать их взаимосвязь, поскольку значимость того или иного события зависит от того, как оно связано с теми или иными событиями вашей жизни. «Биограф» представлял собой некий тест-опросник, над которым мне пришлось очень много поработать. В то время я вообще делал большое количество компьютерных тестов. Дело в том, что компьютерный тест — это не только сама программа, там важную роль играет статистическое обеспечение: нужно сравнивать с другими аналогичными тестами, собирать собственную информацию для ее валидизации. «Биограф» использовался в разного рода научных работах, и отчасти поэтому я и прекратил заниматься этой программой, поскольку, может быть, она полезна человечеству, но лично для себя я не видел никакой пользы.

В России очень высокий пиетет перед авторами, перед создателями, перед режиссерами. На Западе, тем более в США, все несколько не так. Персональное авторство в Америке, как и в Японии, не очень ценится, а поскольку именно эти страны являются родиной видеоигр, то культ автора здесь значительно принижен, особенно по сравнению с кино, театром или книгами. Но мне повезло, я довольно известный человек — во многом потому, что со мной связана довольно курьезная история: родом я из СССР, и это привлекало дополнительное внимание к моей персоне. Но если вдуматься — в год выходят сотни, если не тысячи, игр. А известных создателей можно пересчитать по пальцам — Сид Мейер, Уилл Райт, Сигэру Миямото да я. Это дополнительно свидетельствует в пользу того мнения, что индустрия компьютерных игр находится еще в пеленках.

Сегодня что-то подобное «Тетрису» любой программист может сделать за день.

После «Тетриса» я сделал еще несколько игр. Некоторые из них я очень люблю и ценю, но они и в подметки «Тетрису» не годятся. Я не знаю, почему «Тетрис» так выстрелил, но поделюсь своими соображениями на эту тему. Это и время было такое — начиналась перестройка, страна открывалась миру. Компьютер производил на людей пугающее впечатление — казалось, что с ним могут работать только ученые и математики. Страх перед компьютерами действительно был нешуточный. А тут появляется очень простая и дружелюбная к пользователю программа, которая как раз и снимала это ощущение страха перед компьютером. Плюс по цветам «Тетрис» напоминал кубик Рубика и подсознательно воспринимался как кубик Рубика — те же самые семь цветов, квадратики, поэтому «Тетрис» несколько унаследовал популярность кубика Рубика. Дополнительно на популярность «Тетриса» оказали свое влияние и простота, и хороший маркетинг, и многое другое.

Я довольно быстро акклиматизировался в Штатах — единственной сложностью поначалу был язык. Там мы с друзьями создали фирму Animatek, где выпустили Elfish — «электронный аквариум» для 386-х компьютеров. А вот кризис 1995 года наша компания не пережила. Тогда в игровой индустрии возник самый настоящий кризис перепроизводства, и интерес к играм довольно ощутимо упал. После этого я устроился на работу в Microsoft, где создал Mind Aerobics, одну из первых онлайн-игр, но в то время в интернете играть было не принято, поэтому об этой работе сегодня мало кто знает. Затем я сделал сборник головоломок Pandora's Box, который, как мне кажется, несколько опередил свое время. После этого у меня был довольно непростой период, поскольку к выходу готовился Xbox и у него были свои сложности роста. У меня в работе было несколько проектов, которые то открывались, то закрывались. Связано это было прежде всего с тем, что в Microsoft постоянно меняли политику — то они делают консоль для детей, то для всех, то для продвинутой аудитории. То есть Microsoft с точки зрения игр была не очень опытной компанией, а опытных людей со стороны они почему-то не нанимали. В итоге в какой-то момент я перешел в подразделение, занимавшееся онлайн-проектами. Там я сделал игру Hexic HD. Мне ужасно понравилась игра Bejeweled, но сделана она была из рук вон плохо, и я решил показать, как эту идею нужно было правильно реализовать. Я взял другое поле, другие принципы, но оставил главный принцип — разрешать только успешные ходы. И эта игра, насколько я знаю, и сейчас живет довольно неплохо.

Я обожаю играть в игры. Я играю в них постоянно и трачу на них часа по три-четыре каждый день. Больше всего я люблю головоломки и стараюсь не пропускать самые интересные. Долгое время, лет пять, я играл в World of WarCraft. А вот к играм более активного толка я спокоен. Не выделяется у меня на них адреналин.

Компьютерные игры обладают колоссальными преимуществами перед всеми медиа, которые были до них.

Мне кажется, что все, что сделала компания Nintendo, чрезвычайно важно не только для индустрии компьютерных игр, но и вообще для мировой культуры. Игры про Марио и Зельду на всех платформах стали колоссальным культурным явлением, которое сформировало понимание жизни у целого поколения. Для профессиональных игровых дизайнеров ярким примером высочайшего качества являются такие игры, как SimCity и Civilization. Нельзя не вспомнить Pac-Man, Space Invaders и Centipede, игры, которые в свое время оказали на весь игровой ландшафт мощнейшее влияние и определили его дальнейшее развитие.

Очень серьезное влияние оказали и первые ролевые игры, типа Ultima. Но я с ними плохо знаком, поэтому боюсь здесь сказать что-то лишнее. В какой-то момент большую роль стали играть стратегии в реальном времени вроде Age of Empires, игры, которая у Microsoft получилась ну очень хорошо. Конечно же, и World of WarCraft повлиял на игровую индустрию.

Компьютерные игры обладают колоссальными преимуществами перед всеми медиа, которые были до них. Потому что все, что было до этого, — и театр, и кино, и балет, и многое другое — было развлечением пассивным. Был автор, который писал, сочинял, создавал, и был читатель или зритель, чьей задачей было пассивно потреблять этот авторский труд. В играх же присутствует колоссальный инновационный момент — потребитель игр активен. Интеллектуально, а теперь даже и физически. Но главное — он активен. Он не спит. Это очень интересно, поскольку игры что-то пробуждают в человеке. И про это многие люди забывают или же просто не видят. А не видят они потому, что культура компьютерных игр еще находится в пеленках.

Активное участие зрителя, как мне кажется, препятствует возникновению авторского, высокого, если хотите, искусства. Поскольку, чтобы создать драму, все-таки нужно быть профессионалом. Если вы делаете драму и хотите, чтобы в нее играли, то активная часть вашей игры просто испортит вам все драматическое начало. В этом кроется некоторое противоречие — скажем, если миллионы людей должны быть активны в вашей игре, то, значит, эти миллионы будут заниматься разного рода чушью и ничего серьезного или духовного сделать просто не получится. Да, люди пытаются разрешить это противоречие, найти какие-то компромиссы, и мне кажется, что рано или поздно их найдут. Но сам факт того, что твое участие как игрока снижает планку качества продукта, вызывает иллюзию, что игры — что-то совсем несерьезное. Ничего подобного! Эта культура за довольно короткое время развилась совершенно потрясающе. Начавшись с каких-то совсем примитивных игр, она доросла до мощнейших и сложнейших систем вроде Xbox. В этой среде работают десятки тысяч талантливых программистов, сценаристов, дизайнеров, и рано или поздно здесь будет настоящий прорыв. Тот же Kinect — это же технический прорыв, когда ты включаешься в процесс не только интеллектуально, но и физически. А еще остается слово за искусственным интеллектом, который все никак не может пробиться в игры, но я верю в то, что скоро игры перестанут быть настолько глупыми, что любому школьнику сразу понятно, что и как нужно делать.

Я уверен, что в будущем компьютерные игры разовьются в мощнейшую художественную среду. Индустрия растет, интерес к ней — тоже, так что все с ней будет хорошо.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *