Цель: разработка компьютерной игры
Целевая аудитория: начинающие, интересующиеся, зеваки
Инструменты: ActionScript 3.0**
Замечание 0:
Постараюсь достичь относительной автономности для каждой части, что позволит читать их факультативно и не по порядку.
Замечание 1:
Текст статьи может содержать личное мнение автора по определённым вопросам. Вся ответственность за разжигание жарких бессмысленных споров возлагается на читателя (18+).
- Часть 1: введение, утилиты
- Будет дополнено по мере доступности...
Введение
Замечание 2:
В процессе чтения вы не раз встретите слово «инструмент» по отношению к некоторым вещам. Это сделано специально. Считаю, что разработчик для решения определённых задач способен выбирать наиболее подходящий для этого инструмент и использовать его с умом.
Замечание 3:
В процессе чтения вы также не раз встретите слово «процесс». Оно будет употребляться как в широком смысле этого слова, так и в суженном техническом понимании. Без процесса нет творчества. Без процесса нет разработки.
Возможно, вы обратили внимание, что я пишу «разработка компьютерной игры», явно не указывая, какой именно. Это не просто так. С одной стороны, можно сказать: «не важно, что ты пишешь, главное как»*. С другой стороны, как вы, наверное, опять же, заметили, статья подразумевает несколько частей, потому что это, кроме всего прочего, целая история. Так что пускай конечный продукт будет некоторой интригой всего цикла статей. На самом деле, то, что я излагаю материал через историю, подчёркивает, что без процесса нет разработки.
Для разработчика компьютерная игра** — это не просто некоторая программа, но и некоторый процесс. Во время этого процесса (творческого, или творческого местами, или не творческого местами) происходит масса событий, влияющих на конечный продукт, иные продукты и непосредственно сам процесс, на разработчика и окружающих людей.
Если не брать в расчёт модель «выжать максимум прибыли», то для начала важен некоторый начальный импульс — идея, которая увлечёт вас в той или иной степени. Вы рисуете, оживляете процесс (в воображении, на бумаге, в графическом редакторе)… и т.д.
Вся история началась, когда я в очередной раз, между делом, посмотрел на логотип одной известной iкомпании…
Я взялся за свою идею (она увлекла меня настолько, что я не стал проверять её на «всё уже украдено до нас»*) и начал рисовать что-то на бумаге. Создавал текстовые файлы с краткими заметками по игровой механике. Это были не только технические аспекты, но и просто тексты, названия музыкальных композиций и игр. Схватил первую попавшуюся под руку среду (Flash?!) и начал создавать графику для игры. Flash** позволил мне удивительно легко переводить мои рисунки из воображения в конкретные графические элементы, вызывая желание работать дальше. Я знал, что позже я смогу использовать их непосредственно в своей игре, что добавляло несколько очков к моему энтузиазму. Читал много статей (в т.ч. и здесь, на habrahabr.ru**) по разработке игр. Про то, что нужно бросить идею создать игру в одиночку. Смотрел видео, как некто notch создавал игру в реальном времени, что усиливало моё нежелание согласиться с «N причин бросить всё, принять триста капель эфирной валерьянки и забыться сном»*.
Накопив достаточное количество (критерий достаточности не определён) графических материалов, имея в воображении всё уже работающим как надо, я принялся кодировать. «Поначалу советское вторжение было успешным»*, но вскоре процесс застопорился. «А потом он ещё застопорился. И ещё застопорился. И ещё...»*
Но мне повезло, стартовый импульс был на столько силён, что не позволил мне окончательно забросить проект. Ура!..
Итак, в процессе разработки мне захотелось, чтобы во время паузы игрок мог себя развлечь другой игрой. Чем-то очень простым и тёплым. Мне понадобилась игра, которая смогла бы выполнить эту роль. Процесс кипел, и вариант пришёл довольно быстро. Я переключился на новую игру, с тем же энтузиазмом. Процесс повторился в миниатюре, но т.к. игра была простая, её создание не застопорилось. Напротив, завершение маленькой игры дало дополнительный толчок для разработки основной. В следующей части мы разработаем её вместе. А пока…
Во время работы много что менялось в программном коде, потому что он вечно не устраивал разработчика (и не устраивает). Я желал, чтобы мои изменения не портили уже работающую игру. Засим я обратился к TDD**. Предстояло понять, что же это такое: Kent Beck. Test Driven Development: By Example**.
Замечание 3:
TDD — это «не авада кедавра** для решения всех проблем разработки»*. Всем нужно пользоваться с умом. В частности, это удобный инструмент для внесения изменений и исправления ошибок. Хотя я пользовался этой методикой и для разработки, не могу сказать, что всегда полностью придерживался обозначенного в ней алгоритма «красный/зеленый/рефакторинг».
Итак, я с ActionScript**, и мне нужен простой инструмент для тестирования. Звучит как задача.
Вероятно, есть какие-то готовые инструменты (обзор, для массовки)** для этих целей. Однако, желая пропитаться духом тестирования поглубже, я решил изобрести велосипед.
Замечание 4:
Уважаемый читатель, по моему мнению, изобретение велосипеда — это не плохо. Делая что-то своё, получаешь бесценный опыт и базовое понимание того, как это что-то работает. Сталкиваешься с теми проблемами, с которыми сталкивались те, кто уже решил эту задачу. Это как изучение основ программирования (изучить, попробовать реализовать сортировку нужно, хотя в большинстве случаев вы будете просто вызывать метод «sort()» или тому подобное). Однако, вряд ли ваш велосипед, написанный за пару часов, сможет конкурировать с существующими решениями (поздравляю, если может). Поэтому в будущем стоит обратить внимание на какой-то конкретный инструмент от «людей, которые поймали не один баг на этом»*.
Утилиты
Будем документировать код в стиле javadoc**.
Замечание 5:
Считаю, что код можно и нужно писать так, чтобы комментарии были не нужны. Однако, если мы пишем библиотеку «на экспорт», то возникает вопрос о предоставлении потребителю достойной документации. Для таких целей комментарии javadoc** являются хорошим инструментом для простого создания такой документации.
Замечание 6:
ActionScript** поддерживает отсутствие проверки типов, однако он предоставляет и возможность следить за ними. Считаю, что если язык предоставляет инструмент для слежения за типами на уровне синтаксиса, им нужно пользоваться. Нужно использовать проверку типов везде, а отсутствие проверки использовать с умом там, где благодаря этому достигается определённая функциональность. Благодаря слежению за типами, компилятор обнаружит множество ошибок ещё на этапе компиляции, сохранив в конечном счёте ваше время на нечто более интересное, чем отладка.
Инструмент тестирования
Git: github.com/v-alx-v/as3-unit-tests**
Задача: необходим максимально простой инструмент, который можно было бы без мучений использовать при последующей разработке игр. Оформить как подключаемую библиотеку с минимальным порогом вхождения.
Вдохновляясь JUnit**, учитывая ограничения ActionScript**, желая создать максимально простой инструмент, я решил сделать, пускай, возможно, не очень удобный, зато очень прозрачный набор инструментов (без явного привлечения специалистов Хогвартса**).
Итого: класс СTester, абстрактный класс СUnitTest, небольшой менеджер СUnitTests для организации прозрачной среды запуска тестов, скрипт конфигурации запуска тестов, «запускной файл».
На рисунке 2 можно изучить простую UML** диаграмму разработанных классов.
Для того, чтобы протестировать новый класс, нужно создать для него новый класс, использующий СUnitTest как базовый, добавить новый класс в конфигурацию запуска и запустить «запускной файл».
класс СTester
Его можно использовать отдельно во время отладки как инструмент для проверки ожидаемых значений (а можно и не использовать).
Здесь нет ничего сложного, однако полностью избавлять СTester от зависимости (СUnitTests) мы не будем, т.к. он интересен больше в совокупности, а не отдельно:
/**
* Invokes error in test process.
* @param strTitle String
* @param strComment String
*/
public function error( strTitle:String, strComment:String = ''):void
{
this.m_bError = true;
CUnitTests.error( strTitle, strComment);
}
класс CUnitTest
«Есть желание объявить абстрактный класс, но в ActionScript** нет такой возможности. Есть возможность объявить интерфейс, но у нас есть некоторая общая реализация, так что нет такого желания. Так выпьем за то...»*.
Будем считать обычный класс абстрактным, а благодаря системе исключений, не позволим безнаказанно использовать нашу абстракцию:
/**
* Gets list of functions to run.
* @return Array of Function
*/
protected function testList():Array
{
throw new Error( 'testList must be implemented');
}
Описанный выше метод предполагает, что его определит класс-потомок. Суть метода в том, чтобы вернуть список функций класса, которые нужно запустить (в JUnit** вы бы пометили такие функции аннотацией @ Test).
класс CUnitTests
Это среда выполнения тестов (набор статических данных и функций). Ниже вы узнаете, что фактически работать с этим классом не нужно, хотя он явно фигурирует в скрипте конфигурации запуска тестов.
Из интересных особенностей — мы можем попросить среду использовать наш CMyTester extends CTester для проведения тестирования. Он будет предоставлен всем заинтересованным в этом объектам:
/**
* Tester fabric.
* @param strLabel String
* @return {@link alx.common.test.CTester}
*/
public static function createTester( strLabel:String):CTester
{
return new CUnitTests.s_testerClass( strLabel);
}
Итак, при использовании среды фактический интерес представляет класс CUnitTest (расширяя его можно создать все пользовательские тесты) и скрипт конфигурации запуска:
import alx.common.test.*;
CUnitTests.init( CTester, CUnitTests.SIMPLE_MODE, true);
CUnitTests.run();
CUnitTests.printResult( root);
Для того, чтобы добавить новый тест, нам нужно импортировать его класс и добавить его в список для запуска.
Так для теста, например, test.alx.common.test.CExampleUnitTest extends CUnitTest нужно изменить конфигурацию следующим образом:
import alx.common.test.*;
import test.alx.common.test.CExampleUtitTest;
CUnitTests.init( CTester, CUnitTests.SIMPLE_MODE, true);
CUnitTests.run( CExampleUtitTest);
CUnitTests.printResult( root);
После запуска «запускного файла» пользователь получит окно, залитое красным или зелёным цветом в зависимости от общего результата тестирования (см. рисунок 1). В консоли можно ознакомиться с конкретным ходом тестирования: с версией фреймворка и настройками, с которыми он запущен, а также с тем, какие конкретно тесты (классы) (EXTENDED_MODE) и какие конкретно функции в них (FULL_MODE) были запущены с результатом тестирования по каждому аспекту отдельно. Если произошёл сбой, то обязательно выведется отладочная информация и стек вызовов (если вы попросили об этом специальной настройкой).
- Применён шаблон проектирования** Фабричный метод** public static function createTester( strLabel:String):CTester, реализованный через своего рода Прототип** CUnitTests.s_testerClass;
Генератор случайных чисел
Git: github.com/v-alx-v/as3-random**
Задача: необходим генератор случайных чисел, который поддерживал бы повторение псевдослучайной последовательности.
ActionScript** на столько странен, что не включает в себя базового класса генерации случайных чисел с поддержкой возможности повторения псевдослучайной последовательности.
Бегло посмотрев решения проблемы в интернете, полностью устраивающего меня генератора я не обнаружил. То при генерации творится какая-то «ксоросдвиговая» магия, то проблема перекладывается на генератор шума в bitmap, то ещё что-то.
Открыв исходные тексты Random для Java**, решил портировать решение на ActionScript**. К сожалению, за пятнадцать минут ничего не вышло. В итоге была реализована ещё одна странная реализация. Но, настроившись решительно, я всё-таки переделал решение в более близкое к решению на Java**.
Генератор случайных чисел рассматривался мной ещё и с позиции тестирования. Как тестировать программы, которые используют случайные числа?
Кажется, что ответ вот он — «Люк, у тебя теперь есть возможность повторения псевдослучайной последовательности»*. Выглядит убедительно. Однако приняв во внимание свой опыт разработки генератора случайных чисел, я понял, что в будущем, возможно, захочется поменять реализацию генератора (сделать распределение псевдослучайных чисел ближе к непрерывному распределению** или предусмотреть возможность задания требуемого распределения** ***). Новое решение тоже сможет повторять псевдослучайные последовательности, но уже не такие, какие были до этого. Это означает, что нужно:
- Реализовать новый класс CNewRandom extends CRandom. Тогда «старый» код будет использовать «старую» версию генератора без простой возможности поправить дело (вы тут можете поспорить, что если писать программы правильно, то вообще-то просто поменять конфигурацию. Да, но не все такие правильные). Кроме того, CRandom остаётся как мусор, который вынести на помойку нельзя (назовём это эффектом бабушки);
- Обновить существующий класс. Тогда «старый» код обновляется элементарной заменой. Мусора нет. Однако тесты, которые полагаются на определённые случайные последовательности перестают работать;
- Подумать и всё переделать.
- 0% (0);
- 0% (0);
- 100% (1).
После определённых скитаний я всё-таки пришёл к приемлемому варианту. Для начала был создан интерфейс для генератора случайных чисел, а общая его функциональность локализована в абстрактном классе. Теперь можно создать и сам генератор:
- Рекомендуется везде оперировать генератором случайных чисел только через его интерфейс;
- Можно создать «улучшенный генератор случайных чисел» CNewRandom extends CRandom так, что «старый» код при необходимости сможет подменить «старый» генератор новым и избавиться от мусора;
Кроме обычного генератора случайных чисел я создал «генератор случайных чисел для тестирования» CFakeRandom extends CRandom:
- Гарантируется, что у CFakeRandom все псевдослучайные последовательности постоянны;
- Для большего удобства у CFakeRandom появилась дополнительная возможность «попросить сгенерировать определённую последовательность псевдослучайных чисел»:
/** * Asks generator to generate certain sequence. * @return {@link CFakeRandom this} * @throws ArgumentError if arguments are not valid */ public function ask( ...arValue):CFakeRandom { for ( var i:int = 0; i < arValue.length; i++) { var nValue:Number = arValue[ i]; if (( nValue < 0) || ( nValue >= 1)) throw ArgumentError( "Values should be in [0;1)"); } this.m_arRandomSequence = arValue; return this; }
Ведь иногда так этого хочется.
Замечание 7:
Возможно, читатель напишет, что эту функцию можно перенести в IRandom, рекомендовать её использование в тестах, а CFakeRandom удалить. Да, но я считаю, что данная функция может использоваться исключительно при тестировании. Использование такой функции в коде приложения неправильно и вредно. Поэтому IRandom просто не должен предоставлять пользователю обозначенной возможности, чтобы исключить её неправильное использование. - Рекомендуется использовать его во всех тестах. Тогда можно поменять СRandom без особых опасений (внимательный читатель всё равно отметит, что в этом случае изменение СRandom может негативно повлиять на «старый» код).
Итак, есть класс СRandom (чем-то напоминает Java Random**), который нужно использовать через интерфейс IRandom. При тестировании вместо СRandom следует использовать СFakeRandom. Описывать здесь больше нечего, разве что привести простую UML диаграмму разработанных классов на рисунке 3.
- Применён шаблон проектирования** Мост** IRandom — CAbsctractRandom.
Коллекции
Git: github.com/v-alx-v/as3-collections**
Задача: необходимы удобные коллекции объектов.
И вновь в интернете можно найти множество готовых решений: github.com/danschultz/as3-collections**, www.as3commons.org/as3-commons-collections**.
Я не стал использовать их, но при этом мне кажется, меня бы они полностью устроили. Мы будем до конца упёртыми и реализуем что-нибудь своё. Пускай оно, возможно, будет кривым, но мы не можем упустить возможность попрактиковаться.
В целом, описывать здесь нечего, т.к. это просто воспроизведение готового решения. Можно отметить только тот факт, что в процессе реализации ActionScript** вновь оказался довольно странным. Я долго пытался найти адекватный способ посчитать хеш код для произвольного объекта, но, провозившись не один час над этой проблемой, плюнул на всё и реализовал HashMap хоть как-нибудь. Ну а что, вся сила в том, что можно потом при необходимости вернуться к этой проблеме и поменять реализацию без опаски сломать существующий код (например, если создатели ActionScript** одумаются).
За основу, естественно, были взяты коллекции Java**. Тут я бы хотел сделать лирическое отступление…
Дело в том, что это не первый раз, когда я брался реализовывать коллекции на ActionScript**, но это тот раз, когда я пришёл хоть к какому-то результату, а не бросил всё в начале пути. В процессе реализации я до сих пор не понимаю некоторых вещей, с которыми я столкнулся при изучении исходных кодов openjdk**.
Проблема:
Допустим, есть интерфейс Collection** и интерфейс List extends Collection**.
Вопрос: авторы не верят в наследование и определяют методы в List, которые уже определены в Collection?
Проблема:
Допустим, есть абстрактный класс AbstractCollection** и есть ArrayList extends AbstractList extends AbstractCollection**.
Вопрос: я ещё понимаю переопределение некоторых методов на делегирование задачи другому объекту, что, вероятно, связано с улучшением производительности этих методов. Но зачем переопределять метод isEmpty, я отказываюсь понимать. Замена обобщённого public boolean isEmpty() { return size() == 0;} на частный public boolean isEmpty() { return size == 0;} вызывает сожаление. На основании остального кода я не верю, что это только ради того, чтобы «сэкономить» на вызове метода.
Впрочем, всё это всего лишь некие мелочи, которые могут свидетельствовать о недостаточной дальновидности автора сей статьи.
Заключение
«Не стану утомлять вас, почтеннейший читатель, дальнейшими подробностями...»*.
В следующем выпуске:
- Знакомимся с рекурсией на примере разработки мини-игры для основной игры для игры во время паузы во время игры в основную игру.
Замечание 8:
Текст проверен автором, программой MS Word** и лицом, сведующим в правописании. По вопросам ошибок обращайтесь с письменной претензией непосредственно к автору.
Замечание 9:
Это мой первый пост на habrahabr.ru**. Будьте предельно жестоки.
- Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software**;
- Martin Fowler. Refactoring: Improving the Design of Existing Code**;
- Kent Beck. Test Driven Development: By Example**;
- Javadoc Tool**;
- Справочник по ActionScript 3.0 для платформы Adobe Flash**.
** — Некоторые (не будем называть имена и названия) могли бы и доплачивать за рекламу.
*** — Как мне кажется, можно создать ещё более элегантное решение описанной проблемы, пересмотрев сам подход генерации случайных чисел в сторону законов распределения случайной величины. Но и для моего случая этого не нужно, и утомлять читателя дополнительным текстом не хочется. Если эта тема интересна, можно будет рассмотреть этот вопрос отдельно.
- Почему тег acronym не воспринимает атрибут title?
- Как в статью вставить текст вида [собака][текст] и не получить ссылку на пользователя?
- Автор рассматривает свой код с желанием открыть его для публики с единственным ограничением: сохранение и указание авторства. Одна из задач сейчас — это изучение доступных открытых лицензий на предмет того, какую из них выбрать или на основе какой написать собственную. Консультации приветствуются. Хотя я здесь явно указываю, что собираюсь подарить код общественности, хочу обратить внимание, что пока соответствующие проекты не получили соответствующего файла лицензии, их лучше не использовать.
Автор: pqbd