Сообщений 7    Оценка 93        Оценить  
Система Orphus

Осьмушки

Графика, проектирование и деловой подход

Автор: Олег Михайлик
Источник: RSDN Magazine #2-2004
Опубликовано: 10.10.2004
Исправлено: 10.12.2016
Версия текста: 1.0
Техническое Задание
Пятнашки
Осьмушки
Игра
В защиту GDI+
Архитектура классов
Рисование в манере минимализма
Мистер Вольф
Девичьи страхи
Стратегия в сублимированном виде
Суровая правда жизни
Игра-2
Благодарности

Код к статье

Техническое Задание

В нашем программистском мире Техническое Задание — это судьба. Соберите все Технические Задания, которые вы выполняли в своей жизни, и вы получите краткую аннотацию своей программистской карьеры. Иногда ТЗ бывает излишне скупым, иногда тупым, иногда совершенно казуистическим, но ещё древние греки заметили, что бороться с судьбой бесполезно.

В моём случае ТЗ было забавным.

Пятнашки

Все знают игру «Пятнашки»? В квадратном поле 4x4 расположены 15 шашек, оставлющих «дырку» в одной из позиций. Одна из шашек сдвигается в пустое поле, потом следующая. Постепенно «дырка» передвигается по полю, перепутывая шашки и изменяя игровую позицию. Это какое-то подобие кубика Рубика по мотивам электронно-дырочной проводимости.



Рисунок 1

Задача игрока – восстановить гармонию, передвигая шашки (или «дырку»), и привести "пятнашки" в упорядоченное состояние.

Осьмушки

«Судьба, кажется, обладает чувством юмора», — сказал Морфей. В подтверждение этих слов я получил задание сделать игру «пятнашки» в поле 3x3. Итак, требуется реализовать геймплей, а также добавить функцию решения головоломки.

Задание дано, проект открыт и с самого момента создания получил меткое имя «Осьмушки».


Рисунок 2.

Игра

В защиту GDI+

Принято считать, что средства графики в .NET Framework работают слишком медленно. Честно говоря, я всегда сомневался в этом. А в процессе работы над своей задачей я окончательно убедился - это распространённое мнение ошибочно.

Да, System.Drawing работает медленно. Но этой скорости достаточно для большинства разумных применений!

В осьмушках вся графика выполнена при помощи GDI+, то есть System.Drawing. И это не просто статическая графика, движения шашек анимируются. Причём это делается на прозрачном фоне окна, и вы можете видеть, что происходит «за игровым полем» в самый момент движения шашки.


Рисунок 3.

Ответственность за такой хороший результат я возлагаю на грамотное кеширование объектов GDI+ и на продуманный «конвейер рендеринга».

Архитектура классов

CellPoolControl

Очевидным первым решением является создание некоего выделенного законченного контрола — игрового поля осьмушек. Этот контрол называется CellPoolControl, в нём живут все восемь шашек и одна дырка. Для шашек и их коллекции тоже созданы свои классы (CellPlate и CellTable), но они элементарны, не будем здесь останавливаться.

CellPainter — рисовальщик

Для рисования шашек разумно использовать заранее приготовленные изображения. И точно так же разумно использовать простые функции типа DrawRectangle, FillRectangle, DrawString. Разумных способов много. В конце концов, это непринципиально, как именно будет выглядеть шашка. Но чтобы всё было действительно по-взрослому, я таки выделил и абстрагировал рисование шашки в отдельный класс. Класс CellPainter может нарисовать шашку в любой позиции, с любой надписью «на брюхе». В тот момент, когда контролу нужно нарисовать шашку, он просто вызывает CellPainter с нужными параметрами.

Сейчас шашка рисуется выпуклой, завтра пойдёт мода на плоские шашки. Потом на шашки в стиле Office 2003 — изменения будут предсказуемыми и «точечными». Поучительный класс, не правда ли? Кстати, в .NET Framework 2.0 вы увидите множество подобных «Painter’ов».

AnimationManager — киномеханик

Когда шашка отправляется в полёт, мы можем реально увидеть это движение в нашем контроле. Для отслеживания и всяческой заботы о летающих шашках введён класс AnimationManager. Этот класс в нужный момент включает свой собственный таймер и в нужный момент вызывает перерисовку для всякой летящей шашки (в конечном счёте, это метод Invalidate нашего контрола).

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

Поэтому, когда вы передвигаете шашки быстро одну за другой, AnimationManger следит не за какой-то одной, а за всем летящими шашками.

Рисование в манере минимализма

Как я уже говорил, правильному подходу к рисованию я уделял особое внимание. Отправной точкой был тезис, что механизм отрисовки окон GDI/USER оптимален для поставленной задачи.

Ещё со времён Windows 3.0 рисование управлялось сообщениями Invalidate и Paint. По Invalidate некоторая часть окна (регион) помечалась как требующая перерисовки. Впоследствии, после реакции на более срочные оконные сообщения, вызывалась отрисовка Paint. Функции Paint на этом этапе передавался регион, который необходимо перерисовать, и она занималась отрисовкой нужных частей окна.

Такой подход требует аккуратности и усидчивости. В наше время это не модно, и даже в простейших играх нагнетается гонка FPS, постоянный цикл обновления окна и полная загрузка процессора.

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

Минималистическая анимация

AnimationManager по таймеру сверяет текущее положение всех движущихся шашек и посылает сообщение Invalidate окну игрового поля. Это сообщение вовсе не вызывает отрисовку шашек, оно просто ложится в очередь и ждёт. Когда процессор освободится от действительно важных вещей, окно осьмушек будет обновлено. Торопиться некуда.

Это означает, что AnimationManager вообще абстрагирован от отрисовки.

Минималистическая бережливость

Вторым проявлением минимализма является экономия GDI-объектов. Кисти, линеры — всё, что задействовано в отрисовке, аккуратно учитывается и используется бережно и рачительно.

К примеру, кисть для отрисовки фона создаётся только при первой необходимости. Во время изменения цвета фона (BackColor) кисть не затрагивается. Зачем дёргать объект попусту? Пусть лежит.

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

При этом я не предлагаю заводить глобальные пулы, коллекции, контейнеры. Просто каждый метод, каждый класс следит за своими GDI-объектами. Например, сам контрол заботится о backBrush. Это философия, это подход. Берегите муравейники. Не надо мусорить, да? Да.

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

Мистер Вольф

«Мистер Вольф, специалист по решению проблем». Этот человек запомнился мне исключительно продуктивным и исключительно утилитарным подходом к решению проблем. Решать задачу нужно оптимальным способом, не заботясь о красоте или полноте подхода. На входе мы имеем проблему, на выходе мы должны получить её отсутствие. Все методы разрешаются.

Давайте остановимся. Как бы вы стали решать осьмушки? Головоломка, без сомнения, несложная. Но всё-таки, как именно?

Итак, я выбрал своё решение на основе «Криминального чтива». Конечно же, это полный перебор. Вы знаете готовый алгоритм решения? Я не знаю, и не собираюсь узнавать. У меня есть проблема на входе. Я делаю полный перебор, на выходе проблемы нет. Вопросы?

Девичьи страхи

Кто-то скажет, что полный перебор – это слишком долго. Давайте подсчитаем. Количество вариантов:

9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 = 362 880

Когда-то Билл Гейтс сказал, что 640 Кбайт достаточно для любой программы, и ему до сих пор это припоминают. Пусть сегодня найдётся человек, который назовёт 360 тысяч вариантов слишком большим числом. И пусть этот человек наденет каску, потому что я первым брошу в него камень.

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

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

Наилучшим был бы способ, при котором для каждого отдельного состояния игрового поля указывалось бы нужное движение. Движение, ведущее оптимальным путём к решению головоломки.

Посмотрим: из каждого положения существует максимум четыре возможных направления движения. Дырка перемещается вверх, вниз, вправо или влево. Я для простоты создал массив байт длиной 360 тысяч. Каждая позиция в массиве соответствует уникальному состоянию поля. Значение байта является вектором: куда должна перемещаться дырка для указанного состояния поля.


Рисунок 4.

Конечно, байт может принимать не 4, а 256 возможных значений. Но, как я уже говорил, в наше время эти разряды не стал бы экономить даже Билл Гейтс. Не мне спорить с финансовыми воротилами. Пусть будет 360 Кб.

Для завершения нужно сделать метод сопоставления числа от 1 до 360 000 и уникального состояния игрового поля осьмушек. А также продумать метод обхода, строящий массив-стратегию. Я не стану здесь останавливаться на этих чисто технических деталях.

Суровая правда жизни

Показатели моего компьютера:

Duron 1.3 GHz, 256 Mb DDR, Windows XP SP1, 40 Gb HDD

На этом компьютере приложение «Мистер Вольф» выполнялось меньше 5 секунд. Приложение было скомпилировано в Release и обработано утилитой NGen.

Игра-2

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

В данном случае добавление функции было немногим сложнее пересылки двух байт.

Когда уставший игрок решает воспользоваться функцией автоматического решения, он нажимает кнопку F12. При этом запускается ещё один простой таймер.

Каждые полторы секунды обработчик таймера выбирает следующее нужное движение и запускает его при помощи аниматора AnimationManger. Все.

Благодарности

Человека, давшего имя игре, зовут Юрий Билецкий. Низкий поклон ему за столь ёмкое и сочное название. Не забывайте о качественных идентификаторах!


Эта статья опубликована в журнале RSDN Magazine #2-2004. Информацию о журнале можно найти здесь
    Сообщений 7    Оценка 93        Оценить