Найти тему
ZDG

Разработка игры Pengu5: Архитектурные решения и последствия

Оглавление

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

Мудрые принципы Flash-приложений

В эпоху расцвета Flash появлялись весьма увесистые приложения, которые долго загружались.

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

Поэтому толковые разработчики в первом кадре делали всё по минимуму, чтобы загрузилось только самое необходимое и уже начало работать. А чтобы пользователь не скучал, на экран выводился индикатор прогресса. Но для этого нужна графика, которая тоже должна сначала подгрузиться, либо же рисовать процедурно средствами самого Flash. Самое простое – растущая полоска прогресса.

Когда я сделал воду в игре, она оказалась отличным кандидатом на индикатор прогресса: во-первых, сразу задаёт сеттинг игры, во-вторых, выглядит нескучно, в-третьих, рисуется процедурно и не требует загрузки ресурсов.

Я сделал, чтобы уровень воды поднимался снизу вверх. Честно говоря, ресурсы подгружались так быстро, что пришлось прогресс искусственно замедлить :)

Архитектура, которую заслужил

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

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

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

Войти в одну воду дважды

Одна из проблем, которую пришлось решать, это помещение объектов в воду. Дело в том, что вода в сцене рисуется самой первой, а потом добавляются другие объекты. Но вода состоит из двух слоёв. Один для фона и другой, полупрозрачный, для переднего плана. Чтобы поместить объект в воду, надо вставить его между этими двумя слоями.

-2

Но так как вода была отдельным компонентом, который свои оба слоя сам рисовал перед всеми остальными объектами, пришлось настраивать довольно сложное взаимодействие со сценой.

В результате компонент воды находился на сцене и прикидывался обычным графическим объектом, но отрисовывал он сам себя. А чтобы поместить объект в воду, нужно было помещать его не на сцену, а непосредственно в компонент воды.

Это порождало пару жёстких зависимостей, но ими можно было пренебречь, я делал всё абсолютно осознанно, и они не разрастались. За исключением воды, остальное работало штатно. На тот момент мне так нравилось. Сейчас я другой человек, а не тот, которым был. Теперь мне нравится по-другому. Поэтому в одну воду нельзя войти дважды.

Рефакторинг

Первым делом я решил отвязать графическое представление воды от её компонента. Компонент должен просто рассчитывать параметры волн, но не рисовать их.

Попутно переделал рендеринг сцены. Сцена была устроена так:

-3

Она имела единый метод отрисовки, который получал от всех объектов на сцене их картинки через метод getGraphics() и отрисовывал эти картинки на экране, включая прозрачность, масштабирование и поворот. Как помним, сцена не умела рисовать воду, поэтому компонент воды размещал на сцене два своих слоя по отдельности как обычные DisplayObject, но когда у них запрашивался метод getGraphics(), они пользовались этим, чтобы нарисовать самих себя, а вместо картинки возвращали null, чтобы сцена ничего не рисовала.

В принципе ни о чём не жалею. Но теперь сделал так:

-4

Появился компонент Renderer, который занимается своим прямым делом – рисованием. Метод draw() есть теперь в каждом объекте сцены, но объект не рисует сам себя. Вместо этого в метод передаётся ссылка на рендерер, и объект может попросить у рендерера "нарисуй меня", вызвав метод renderer.drawImage(), к примеру. Рендерер всегда знает, как рисовать то, о чём его попросят, в том числе и воду.

Выгоды очевидны. Вся работа с графикой теперь находится ТОЛЬКО в рендерере, который всегда можно заменить на другой. Например, на OpenGL. Нет ограничений на то, какую именно графику может содержать объект. Хоть картинку, хоть процедурную. Всё, что может быть нарисовано – нарисует рендерер, надо только его попросить.

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

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

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

Вся подборка:

Игра Pengu5 | ZDG | Дзен