Немного о создании своей игры. Часть первая.
В качестве предисловия
Я очень много времени провожу за компьютером. Работаю, читаю, смотрю... Но не играю. Точнее я не фанат игр. Так уж исторически сложилось, что мне интереснее создавать, чем пользоваться созданным. А создавать, в силу профессии, приходится много и весьма разнообразные вещи. Но это работа.
В какой-то момент получилось так, что я и мои друзья (дизайнер и иллюстратор / аниматор) решили создать свою первую игру. Для меня причин этому несколько. Во-первых, мне не хватает практики с as3. Во-вторых — уж очень заманчивыми кажутся новые возможности платформы, их хочется "пощупать" в боевых условиях. В-третьих, постоянно делая "вкусные" штуки для клиентов, в какой-то момент начинаешь задумываться над своим проектом. Тем более, есть поддержка профессиональных дизайнера и аниматора.
Идея
Как-таковой идеи почти не было. Были некоторые соображения относительно геймпэя — 2д, вид сбоку, платформер, с физикой. Дальше — по ходу разработки.
Конкретнее
На данный момент я собираю прототип игрушки-платформера, показывающей (это, наверное, банально) приключения воина-викинга по пути из далеких от его родины мест в сторону дома (семьи, счастья). Ему будут противостоять всякого рода агрессивно настроенные мистические существа, с разными игровыми поведениями, динамикой действий и размерами (от сравнимых с персонажем до тех, которые крупнее его в несколько раз). Весь "путь" будет представлен в виде нескольких уровней, графика которых будет соответствовать "этапу" прохождения. Например, в самом начале, персонаж окажется у разбитого корабля, скалистого побережья, плавно перетекающего в густой лес, населенный всякими монстрами. В качестве оружия — нечто среднее между самострелом и дробовиком.
Возможно когда-нибудь я буду дополнять игрушку возможностью мультиплеерного режима. Но об этом пока рано говорить.
Предварительный дизайн. Рисовал очень классный иллюстратор oneappleinbox
Программное устройство
Я переписывал весь код игры восемь раз. Каждый новый раз я писал код до тех пор, пока не понимал, что структура классов (архитектура), их зависимости и последовательности выполнения не становились настолько неприменимы к новым задачам, что было бы проще переписать большую часть заново, освежить все в голове, еще раз продумать, постараться разрисовать на бумаге и так далее. Думаю, это знакомо тому, кто один старательно делал большой проект и не имел при этому достаточного опыта.
Так как, я не знаю, в каком виде игра будет работать (в браузере, десктопно или на девайсах), я решил использовать Starling для рендеринга, Nape для физики и не фиксировать экранные размеры игры. Таким образом, я могу спокойно растягивать окно игры, которая, в свою очередь, адекватно смасштабируется. Естественно, это работает для разумных размеров.
Технически игра представляет собой основную флэшку и несколько подгружаемых. Подгружаемые флэшки — это уровни. Основная флэшка — это игровой движок. Далее я постараюсь расписать работу программных частей игры в последовательности их использования в игре.
Если не брать в расчет стандартный для FD класс Preloader, то стартовой точкой в игре являются два класса — Main и GameSettings.
GameSettings
Как я написал ранее, я не знаю, как именно будет запускаться игра, поэтому я не могу заранее просчитать оптимальные параметры для движка и игровых настроек. Поэтому я вынес все значения в отдельный класс с соответствующими статическими полями. Думаю, тут все понятно.
Вот небольшой кусочек кода для наглядности:
static public const PHYSICS_GRAVITY:Vec2 = new Vec2(0, 1200); static public const PHYSICS_VELOCITY_ITERATIONS:int = 5; static public const PHYSICS_POSITION_ITERATIONS:int = 5; static public const PHYSICS_BROADPHASE:Broadphase = Broadphase.DYNAMIC_AABB_TREE static public const RENDER_LANDSCAPE_BLOCKS:Boolean = false; static public const RENDER_ANIMATED_ELEMENTS_FRAMERATE:int = 20; static public const RENDER_ANIMATED_ELEMENTS_FROM_RANDOM_FRAME:Boolean = false; static public const RENDER_TEXTURE_SMOOTHING:String = TextureSmoothing.NONE; static public const RENDER_ANTIALIASING:int = 0; static public const RENDER_ATLAS_SIZE:int = 2048; static public const RENDER_SUBTEXTURE_SUBSIZE:int = 1; static public const RENDER_SUBTEXTURE_SIZE:int = 256; static public const RENDER_SUBTEXTURE_PACK_MARGIN:int = 1; static public const RENDER_USE_BLENDMODE:Boolean = true; static public const TEST_ELEMENTS_VISIBILITY:Boolean = true; static public const TEST_BACKGROUND_PARTS_VISIBILITY:Boolean = true; static public const STOP_INVISIBLE_PARTICLES:Boolean = true;
Main
Начну издалека: если вдуматься, сама программа игры — это набор взаимосвязанных программных частей, каждая из которых выполняет только свою однозначно определенную задачу. В моей игре такими частями являются, например, классы для обработки нажатий на клавиши, класс для рендеринга с помощью старлинга и класс для загрузки уровней.
В общем, вот те самые классы, которые инициализируются в мэйне (их назначение очевидно из названия, как мне кажется):
- KeyInputManager
- SoundManager
- GameController
- LevelLoader
- ScreenManager
Так вот на старте игры, в мэйне, я инициализирую все эти классы. Некоторым я передаю ссылку на стэйдж, некоторым передаю в параметры ссылку на метод, который должен быть запущен после того, как класс инициализируется (callback). Последний случай — это класс GameController.
GameController
Это основной класс игры. Он контролирует запуск игрового процесса, паузу при неактивности, выход из режима игры (когда переходим к стартовому экрану) и так далее. На старте он инициализирует и "связывает" четыре важнейших компонента — GameRenderer, GamePhysics, PlayerController и EnemiesController. О них чуть позже.
GameRenderer
Этот класс обеспечивает отображение игры на экране и предоставляет доступ до всяких эффектов, например эффект вспышки или эффект "встряхивания" экрана. На старте он создает игровые слои — несколько старлинговых спрайтов, в которые будут добавляться игровые элементы. Фон, например, на самый нижний слой. Эффекты (партиклы) — на самый высший. Для рендеринга с помощью старлинга, этот класс инициализирует класс StarlingRenderer.
StarlingRenderer
Вопреки тому, как рекомендуют использовать старлинг, когда старлинг инициализирует главный класс приложения ( new Starling(StarlingMain, ...) ), я его использую только для экрана с игрой. Все остальные экраны (меню, помощь, загрузка и так далее) сделаны нативными дисплэйобджектами.
StarlingRenderer — это класс-оболочка, который инициализирует старлинг и имеет статические методы типа addChild, addChildAt, start, stop, show, hide и так далее. Так же класс автоматически подписывается на ресайз стэйджа и подгоняет стэйдж старлинга до необходимых размеров.
Я посчитал такое решение наиболее удачным для моих целей, так как я максимально изолировал старлинг (который мне во многих моментах не нравится, хоть его и преподносят, как, чуть ли, не промышленный стандарт) от других частей программы и мне достаточно только "кинуть" старлинговый дисплэйобджект в статичный метод StarlingRenderer`а addChild и насладиться его изображением на экране. Если кодом, то это выглядело бы так:
StarlingRenderer.init( stage, onStarlingReady ); ... private function onStarlingReady():void{ StarlingRenderer.addChild(new Image(Texture.fromColor(100, 100, 0xFFFF0000)) }
В общих чертах, думаю, устройство понятно — последовательно инициализирую основные компоненты игры, которые, в свою очередь, инициализируют следующие и т.д. Теперь об уровнях и их отображении в игре.
Игровой уровень — это .swf-файл с экспортной графикой и несколькими строчками кода. Фактически, это флэшка, скомпилированная из Flash IDE, у которой в библиотеке лежат несколько десятков мувиклипов (с проставленными "Export for ActionScript") и есть код в первом кадре основной временной шкалы.
Итак, по-порядку.
Ранние версии прототипов я пытался сделать либо с рассчетом на плиточный мир, когда вся графика игры состояла из квадратно-ограниченых областей, собраных по сетке, либо полностью из произвольных графических элементов, раскиданных по уровню и физической карты.
В первом случае было бы довольно просто работать с физикой (вообще обойтись без нэйпа, как вариант). Для второго случая — необходимо хранить большую картинку для графики уровня плюс отдельный мувиклип с набором прямоугольников (тоже мувиклипов), которые интерпретировались бы движком, как физические блоки. Иными словами — показываем картинку, а в физическом представлении — работаем с блоками, которые заботливо расставлены по контуру изображения, чтобы можно было перемещаться персонажем в соответствии с изображением ландшафта.
Мне довольно сложно описать это словами, поэтому чуть ниже прилагаю поясняющие картинки.
Первый вариант — используем блоки:
Кстати я рассматривал вариант, когда вся карта сохраняется в виде картинки (png), у которой один пиксель соответствует одному блоку, а цвет этого пикселя — свойствам блока. В таком случае, можно было бы делать карты гигантского размера. Но пришлось бы сделать и редактор ко всему этому. Для разнообразия внешнего вида игры, можно было бы выбирать произвольную картинку для каждого типа блоков. Например, если показываем землю — прорисовать с полсотни совмещающихся изображений земли и получили вполне приемлемый результат.
Из минусов — очевидно, что будут отсутствовать наклонные поверхности, да и произвольную графику (по размерам) было бы проблематично внедрять.
Второй вариант — используем большую картинку для уровня и физическую модель (карту):
В принципе, то, что нужно. Но я пошел дальше и попробовал совместить два подхода: я одновременно использую и физическую карту и блочные элементы с рандомной графикой.
Так, как я хочу получить относительно простыми средствами довольно разнообразную картинку, да и, к тому же, с различными физическими поведениями элементов, я использовал такой способ: создаю в исходнике уровня новый мувиклип, рисую в нем несколько кадров примерно одинакового размера и задаю этому мувиклипу экспортное имя класса, например, "gfx_dynamic_transparent_box_2". Приставка "gfx_" в данном случае нужна мне для "фильтрации" реальной игровой графики от возможного "мусора" на карте уровня. После приставки идет часть названия класса, которая точно характеризует физическое поведение объекта, который будет создан движком. "dynamic_transparent" означает, что этот объект будет динамическим и прозрачным для игрока и противников. Последняя часть названия — произвольная и ассоциативная. Если коробка — так и назову, "box".
Движок, после того, как загрузит уровень, первым делом "полезет" в код первого кадра. Так как флэш обернет этот код в автоматически создаваемый класс, то все переменные, которые в этом коде прописаны, автоматически станут public-свойствами класса. Одно из таких свойств — "levelMapClass". В коде это прописано таким образом:
Значение этой переменной, соответственно, ссылается на экспортное название класса мувиклипа, в котором лежит графика уровня именно в тех местах, в которых она должна находиться уже в игре.
Далее движок, точнее та его часть, которая отвечает за парсинг уровня, создает несколько "экстракторов". Так я назвал классы, чья задача сводится к тому, чтобы распарсить отдельный тип данных карты и создать элементы уже в игровом пространстве. Из-за того, что моментальный парсинг был бы слишком ресурсоемким и мог бы просто "подвесить" флэшку, я написал вспомогательный класс для отложенного выполнения действий, TaskChainResolver. Его работа довольно простая и, думаю, станет понятна после описания методов:
addTask(method:Function, repeatsPerFrame:int = 1, methodArguments:Array = null):void start(allCompleteCallBack:Function = null):void abort():void
static private const TASKS_COUNTER_COEFF:int = 5; static public const HARD_TASKS_COUNTER:int = 1 * TASKS_COUNTER_COEFF; static public const NORMAL_TASKS_COUNTER:int = 5 * TASKS_COUNTER_COEFF; static public const EASY_TASKS_COUNTER:int = 10 * TASKS_COUNTER_COEFF;
Распределение по сложности задач (HARD, NORMAL и EASY) весьма условно и я толком еще не тестировал оптимальность выбранных значений.
В общей сложности я использую пять экстракторов:
- BackgroundExtractor
- ElementsExtractor
- ParallaxExtractor
- ForegroundExtractor
- SettingsExtractor
Работа каждого из них в коде выглядит аналогичным образом:
_elementsExtractor = new ElementsExtractor(); _elementsExtractor.setLevelData(_levelData, _levelInstance); _elementsExtractor.setCallbacks(_nextTaskCallback, _onExtractElement, _onExtractProgress); _elementsExtractor.startExtract();
GameElement — это основополагающий класс любого игрового элемента, будь то фон или коробка на земле или сам персонаж. Из всего разнообразия возможных элементов я выделил ключевые и общие свойства (сеттеры в данном случае представлены как паблик-свойства):
public var isDead:Boolean; public var body:Body; public var image:DisplayObject; public var type:String; public var atlasId:int; public var speed:Number; public var usePhysics:Boolean; public var useGraphics:Boolean; public var useJuggler:Boolean; public var lifes:Number;
В прототипе, например, при клике я создаю "взрыв" с последующим замедлением времени (восстанавливающимся):
— в физическом мире "раздаю" импульсов динамическим объектам, которые находятся близко к эпицентру взрыва (сила импульса квадратично убывает в зависимости от расстояния от центра)
— происходит вспышка экрана (накладывается starling.display.Quad с BlendMode.ADD у которого снижается прозрачность)
— встряска экрана
— уменьшение параметра deltaTime у метода step (nape)
— торможение фоновой музыки
— торможение анимаций старлинг-мувиклипов и партиклов
Что касается atlasId — для каждого отображаемого элемента это номер текстурного атласа, из которого он использует свою текстуру. Этот параметр создан для того, чтобы снизить количество обращений на отрисовку к видеокарте (drawCall), сортируя элементы по глубине. Эта возможность так же может быть отключена из GameSettings. Для меня это было неочевидно,так как я не сразу нашел необходимую информацию о том, что "extra draw calls" бывают, если перемешать по глубине элементы с разными текстурными атласами.
Экстракторы
SettingsExtractor
— этот экстрактор запускается первым и не создает каких-либо игровых элементов. Он занимается тем, что "достает" все настройки уровня и записывает их в экземпляр актуального класса с описанием уровня (LevelData).
BackgroundExtractor
— экстрактор, который создает на уровне статическую картинку фона, которая масштабируется аналогично с флэшовым стэйджем с настройкой StageScaleMode.NO_BORDER
ElementsExtractor
— экстрактор, который создает элементы уровня. Упрощенная последовательность его работы следующая:
1. Временно создает экземпляр мувиклипа с элементами игры.
2. Создает список разных элементов, которые есть на уровне.
3. Каждому элементу обнуляет матрицу трансформаций и отрисовывает каждый его кадр в битмапдату.
4. Анализирует элемент. Если в нем есть вложенный прямоугольник gfx_bounds_clip, то использует этот элемента как маску для графики.
5. Анализирует прозрачные области полученных битмапдат и, по необходимости, обрезает ненужные прозрачные участки.
6. Собирает все битмапдаты и упаковывает их в большие битмапдаты с помощью алгоритма max rects (добавляя пробелы исходя из настроек).
7. Параллельно с предыдущим пунктом, заполняет хмл-ки для текстурных атласов.
8. Создает текстурные атласы с соответствующими хмл-ками из больших битмапдат.
9. Заново проходит по всем элементам уровня и для каждого создает игровой элемент, сохраняя матрицы трансформаций и свойства наложения (для сопоставления флэшового блендмода и старлинг-блендмада написан отдельный класс). Если элемент на карте уровня имеет несколько кадров, то выбирается произвильный кадр. Физическое представление элемента, то есть, прямоугольник, подгоняется под размеры изображения. Класс игрового элемента, который создается, выбирается из соображений максимального вхождения названия класса в название класса элемента. Таким образом, имея классы StaticBlock и StaticTransparentBlock и элемент с названием класса "gfx_static_transparent_box_2", будет использован класс StaticTranspanretBlock, так, как он наиболее полно соответствует названию.
10. Обнуляет ссылки и уничтожает промежуточные битмапдаты.
Пожалуй, самый сложный в плане алгоритмов класс, который есть в игре. Пришлось изрядно помучаться, чтобы исправить ошибки, связанные с позиционированием, масштабированем и т.д. Для обратного преобразования значений матриц в свойства мувиклипа использовал небольшую хитрость: применяю матрицу к трансформации шэйпа и спокойно беру его значения ширины, положения, вращения.
Все размеры для текстурных атласов, а так же размеры отступов и параметры из серии "делать или не делать" (ну или "проверять или нет") вынесены в GameSettings.
Собственно благодаря работе этого экстрактора, ранее я написал, что совместил два подхода, плиточный и не плиточный. Это связано с тем, что я могу составить весь ландшафт уровня исключительно из статических блоков, которые выстроены ровно по сетке. В итоге и получится плиточный игровой мир.
Упрощенный принцип его работы:
ParallaxExtractor
— экстрактор для создания параллакс-фонов в игрушке. Они находятся выше, чем статичный фон и ниже, чем сам уровень. В принципе, я не делал ограничений на их количество, поэтому в коде флэшки-уровня они описываются таким образом:
this.levelParallaxClasses = [ gfx_level_bg_4, gfx_level_bg_3, gfx_level_bg_2, gfx_level_bg_1 ]; this.levelParallaxSettings = [ 2.5, 2.15, 1.75, 1.5 ];
Так же я не делал каких-либо условий для размера и пропорций фона. Эти аспекты я компенсировал предварительными расчетами, благодаря которым фон даже самой неподходящей пропорции для уровня будет подправлен таким образом, чтобы и двигаться с необходимым замедлением и не показывать пустых полей.
Принцип работы такого экстрактора схож с предыдущим. Он создает экземпляр фона, условно делит его на множество "кусочков", проверяет каждый кусочек на прозрачности, создает текстурный атлас и элементы для каждого из кусочков. Создаваемые элементы являются частью игрового элемента Parallax. Во время игры, при перемещении, происходит проверка видимости "кусочков", исходя из результатов которой, те "кусочки", которые находятся вне игрового вьюпорта, исчезают. Относительно производительности такого подхода — я пока не проводил тесты.
ForegroundExtractor
— экстрактор, работа которого полностью аналогична работе ParallaxExtractor`а, за исключением того, что обрабатывается один фон, а его элемент создается поверх слоя с элементами игры (динамические блоки, противники, персонаж и т.д.).
Чтобы было понятнее, как выглядит уровень, приведу иллюстрацию (здесь два параллакс-фона):
Заключение
На этом моменте, пожалуй, я закончу первую часть и постараюсь в ближайшее время написать вторую, про парсинг покадровой анимации персонажа в старлинг-мувиклип с плавно-изменяемой скоростью, некоторых оптимизациях и о других вещах, с которыми приходится разбираться.
И да, я прекрасно понимаю, что у меня не идеальные знания, во многих вещах я могу ошибаться (в том числе в терминологии, именовании классов и грамматике), так что если у вас есть замечания — буду только рад. Ибо пока только учусь делать игрушки.
P.S. Прототип пока не буду показывать.
Всего комментариев 21
Комментарии
20.04.2013 01:27 | |
Отличная статья! И очень радует оформление, прямо образцово-показательное
|
20.04.2013 14:28 | |
Спасибо, вождь! Было бы очень здорово wmode='direct' сделать для встраиваемых флэшек.
|
20.04.2013 14:49 | |
Выбор Stage3D для Flash-игры (не для девайсов) все еще является ошибкой, которая скорее всего всплывет на этапе продажи.
|
20.04.2013 15:13 | |
Zebestov, я не для продажи делаю. Можно описать подробнее, почему так?
|
20.04.2013 15:37 | |
Спасибо за статью! Прочитал с удовольствием!
|
20.04.2013 16:11 | |
А статья хорошая.
|
20.04.2013 17:25 | |
Zebestov,
wmode direct. А у каких из крупных порталов проблемы с этим? |
20.04.2013 19:52 | |
Можно, наверное, попробовать вариант с автоподменой wmode с помощью ExternalInterface.
|
20.04.2013 22:45 | |
Спасибо за статью, понравилась. Жду продолжения, ну и результат конечно тоже.
|
22.04.2013 19:34 | |
PainKiller, спасибо. Я пока не думал над тем, что с игрой делать дальше, после того, как я ее доделаю. Да и доделаю ли я ее — тот еще вопрос.
|
22.04.2013 22:32 | |
Как минимум половину логики ElementsExtractor имеет смысл вынести в редактор уровней или отдельной утилитой сделать, чем каждый раз запускать генерацию атласов на клиенте.
|
22.04.2013 22:59 | |
Верно. Так и планировал. Только не на данном этапе, сейчас мне главное доделать, а подменить модуль уже не проблема будет, как мне кажется.
А вообще утилиту для генерации атласов я пишу для главного персонажа и противников. Только из-за того, что у меня планируется изменение скорости течения времени в игрушке, я не буду сохранять каждый кадр анимации в атлас, а буду хранить "кусочки" персонажа + матрицы их трансформации для каждого кадра. В рантайме используется интерполяция значений матриц, поэтому, например, я могу запускать бег главного героя с отрицательным временем, анимация пойдет задом-наперед. Про это планирую продолжение писать. А относительно уровней и генерации атласов на клиенте — мне, в какой-то момент, показалось это весьма удобным. Например, я могу хранить уровень на своем сервере, периодически обновлять его. Так же могу, например, добавлять новые уровни (ведь список доступных уровней так же подгружаем). Если использовать какую-либо утилиту для "упаковки" уровней — у меня получится еще один технологический процесс между исходником во флэше и самим игровым движком. Мне это кажется избыточным на данном этапе. [ флэш — сфв уровня — утилита — тестирование ] — и так пока не получится хороший уровень. А сечас у меня получается так: [ флэш — сфв уровня — тестирование ] Ну и, тем более, что я получу в итоге? Сразу готовые пнг для атласов, хмл-ки для них же и... всё. Просто, получается, пропускаю отрисовку разных элементов в их битмапдаты и упаковку этих битмапдат в атласы. Меня эти действия не сильно расстраивают, дело в нескольких секундах. Утечек памяти я стараюсь избегать, очень старательно следя за всеми ссылками и объектами во время распаковки и парсинга, ну да, потребление памяти подскакивает под 100Мб в этот момент, но потом до ~25 спадает, как игра начинается. Да, это заманчиво, будет шустро работать на клиенте (подготовлено же всё будет). Но и значительно усложнит мою разработку, как мне кажется, и по времени и по сложности. А это для меня, одни из самых решающих факторов, которые постоянно пытаются одержать верх над моим желанием и умением. |
|
Обновил(-а) Hauts 23.04.2013 в 08:54
|
23.04.2013 20:51 | |
alatar, я вас понял, спасибо за разъяснение!
Да, хорошая штука была бы. Но мне пока не до нее |
Последние записи от Hauts
- Немного о создании своей игры. Часть первая. (19.04.2013)