Текст книги "Создание игр для мобильных телефонов"
![](/books_files/covers/thumbs_240/sozdanie-igr-dlya-mobilnyh-telefonov-45436.jpg)
Автор книги: Майкл Моррисон
Жанр: Зарубежная компьютерная литература, Зарубежная литература
сообщить о неприемлемом содержимом
Текущая страница: 29 (всего у книги 35 страниц) [доступный отрывок для чтения: 12 страниц]
Другой тип оптимизации игр – это оптимизация размера, которая подразумевает изменения кода для минимизации размеров файла игры. Оптимизация размера очень важна для мобильных игр, поскольку она определяет необходимый объем памяти. Основа оптимизации размера – это повторное использование кода, что проистекает из наследования классов в Java. К счастью, хорошая объектно-ориентированная разработка способствует минимизации кода, поэтому вам редко потребуется выполнять этот тип оптимизации, по крайней мере, с игровым кодом. Например, с целью уменьшения размера полезно помещать повторяющийся код в отдельный метод. В этом случае некоторая оптимизация выполняется еще на стадии разработки кода игры.
Более важная грань оптимизации размера мобильных игр – это ресурсы, используемые в игре (например, изображения, звуковые эффекты и музыка). По сравнению с небольшим кодом Java ресурсы могут быть очень объемными. Поэтому будьте очень внимательны, когда устанавливаете требования к изображениям и звукам в игре. Вы можете использовать, например, изображения меньшего размера или меньшее число изображений, низкокачественные звуки. Вы также должны отслеживать размер прочих данных, необходимых игре, например, данных, загружаемых из сети или сохраняемых в памяти телефона.
Совет Разработчику
Вы можете быстро уменьшить размер игры, используя звуки низкого качества. Например, в программах, приводимых в книге, используются 8-битовые монофонические звуки с частотой 8 кГц.
Оптимизация по скорости, бесспорно, самый важный тип оптимизации мобильных игр, поскольку он определяет, как быстро будет выполняться приложение. Оптимизация по скорости подразумевает повышение скорости выполнения кода путем отладки. Зная о проблемах производительности в Java, не говоря уже о маломощных процессорах мобильных телефонов, оптимизация по скорости играет важнейшую роль при разработке любых мидлетов, особенно игровых. Компилятор Java говорит последнее слово в том, как работает мидлет, поэтому всю оптимизацию по скорости необходимо выполнить самостоятельно.
Большая часть этой главы сосредоточена на рассмотрении вопросов оптимизации по скорости и на том, как создать наиболее эффективный с точки зрения производительности код. В ряде случаев вам придется пренебречь прочими типами оптимизации, чтобы увеличить скорость выполнения мидлета. В большинстве случаев это вполне приемлемо, поскольку организация кода и его размер не будут иметь значения, если приложение выполняется медленно. Однако вы должны стремиться соблюсти баланс между оптимизацией по размеру и по скорости. Очевидно, что очень быстрый мидлет, но большого размера, который будет долго загружаться по беспроводному соединению, – это не лучшее решение. К счастью, оптимизация по скорости и размеру часто идут рука об руку, поскольку простые алгоритмы намного быстрее и меньше сложных.
Основные приемы оптимизации игрВ копилку Игрока
Для сравнения, чтобы в игре была плавная анимация, необходимо, чтобы частота смены кадров лежала в диапазоне от 15 до 24 кадров в секунду. Чтобы вспомнить, что такое частота смены кадров, посмотрите главу 5.
Прежде чем перейти к вопросам оптимизации Java-кода, чтобы сделать его как можно более быстрым, следует уделить внимание рассмотрению основных стратегий оптимизации, которые вы должны знать, поскольку приступаете к самостоятельной разработке мидлетов. Эти стратегии в основном касаются оптимизации размера, поскольку большинство оптимизаций выполняется через тщательную проработку эффективных мидлетов, а не хитрых изменений кода. Не волнуйтесь, интересные приемы ждут вас далее.
Не секрет, что мобильные телефоны имеют очень маленький объем памяти по сравнению с другими вычислительными устройствами. Во многом ограничение памяти мобильных телефонов является более жестким, чем ограничения вычислительных способностей. Следовательно, важно постараться сократить использование памяти мобильной игрой. К счастью, вы можете использовать для этого несколько подходов:
► по возможности избегать применения объектов;
► если вы все же используете объекты, попробуйте применить их повторно;
► удаляйте объекты по окончании работы с ними.
В следующих нескольких разделах эти методы сокращения используемой мидлетом памяти будут освещены подробно.
Избежание применения объектов
Это может показаться странным, однако в мидлетах по возможности следует избегать использования объектов. Память под объекты выделяется из памяти среды выполнения, а не из стека, как в случае обычными типами данных. Стандартные типы данных, известные как скаляры, – это такие типы языка Java, как int, long, boolean и char. Конечно в CLDC и MIDP API множество классов, да и сами мидлеты – это объекты, следовательно, есть нижняя граница того, насколько вы можете сократить применение объектов. Тем не менее сокращение использования объектов больше касается данных мидлета, которые в большинстве случаев могут храниться в переменных стандартных типов, а не в объектах.
Если вы изучите CLDC и MIDP API, вы обнаружите, что многие вспомогательные классы, используемые в J2SE API, здесь отсутствуют. Например, класс Rectangle в J2SE – это хранилище четырех целочисленных переменных (X, Y, ширина и высота), описывающих прямоугольник. Этот класс отсутствует в MIDP API, а в тех местах, где ранее использовались переменные такого типа, используется непосредственное указание каждой переменной. Четыре переменные целочисленного типа менее требовательны к памяти по сравнению с объектом, хранящим четыре целочисленные переменные, под который нужно выделять память и управлять ей. Следовательно, обратите внимание, что в CLDC и MIDP API объекты используются только тогда, когда это функционально необходимо. В других случаях используются стандартные типы данных.
Вы должны придерживаться концепции CLDC и MIDP API, когда речь идет о ваших игровых данных. Не заключайте данные в класс, если для этого нет значимой причины. Наоборот, намного лучше использовать стандартные типы данных, которые намного эффективнее объектов.
Если используете объекты, то применяйте их повторно
Очевидно, что нельзя полностью избежать применения объектов в мобильных играх. Объекты играют очень важную роль в Java-программировании, и мидлеты не являются исключением. Один из способов минимизировать затраты памяти на объекты – повторно использовать их. При этом отпадает необходимость создавать объекты заново. Конечно, такой подход можно применить только в случае, если необходимо использовать объект одного типа несколько раз, но вы будете удивлены, насколько часто такой метод применим при разработке мидлетов.
Совет Разработчику
Хороший пример повторного использования объектов – это использование одного объекта класса Sprite вместо удаления и создания нового. Изменение объекта можно сравнить с удалением старого и созданием нового. Такой подход широко применялся в мидлете High Seas, разработанном в главах 12 и 13.
Повторное использование объектов позволяет избежать ненужного динамического выделения памяти. Например, если вы создаете объект, а затем прекращаете его использовать, сборщик мусора Java удалит его из памяти. Если впоследствии вам понадобится объект такого же типа, вы создадите новый, и под этот объект будет вновь выделена память. Вместо этого вы можете использовать предыдущий объект, заново проведя инициализацию.
Удаление объектов
Говоря о повторном использовании и уборке мусора, следует упомянуть о последнем приеме оптимизации, связанном с удалением объектов из памяти. В традиционном программировании J2SE или J2EE вы создаете объекты по необходимости, а удаляются они сборщиком мусора Java, когда становятся ненужными. В J2ME все аналогично, но стандартный сборщик мусора – это не очень эффективное средство высвобождения памяти. Сборщик мусора запущен как низкоприоритетный фоновый поток, который определяет и удаляет неиспользуемые объекты. Объект является используемым до тех пор, пока он не выйдет за границы области видимости или не станет равным null.
Один из способов помочь сборщику мусора определить неиспользуемый объект – это по окончании работы с объектом явно присвоить ему значение null, тогда занимаемая память будет освобождена при первой же возможности. Все объекты рано или поздно будут удалены из памяти, однако этот прием позволяет ускорить удаление ненужных объектов сборщиком мусора.
Вы знаете, что мобильные телефоны работают с беспроводной сетью, которая имеет сравнительно низкую скорость передачи данных. Поскольку сетевые мобильные игры должны использовать эту сеть, существуют значительные ограничения на объем передаваемых/получаемых по сети данных. Меньше всего игрок хочет ждать загрузки данных, описывающих ход оппонента, например, в шутере или лабиринте. Это может прозвучать очевидным, однако разработчики игр для персональных компьютеров или игровых консолей не сталкиваются с чрезвычайно ограниченной пропускной способностью сетевых соединений, поэтому минимизация объема передаваемых данных для них не является первостепенной задачей.
Вероятно, вы думаете, что графика очень важна в создаваемых вами играх, но в реальности это может быть не так. Например, если в вашей игре есть графика, отличающаяся углом поворота, то, вероятно, вы зря расходуете память. Класс Sprite позволяет поворачивать спрайтовые изображения (на углы кратные 90 0) или зеркально отображать их. Используя это, вы потенциально можете сократить объем графических ресурсов на 75 %.
Рассмотрим пример High Seas, созданный в главах 12 и 13. В этой игре спрайт пиратского корабля состоит из четырех фреймов (рис. 17.1), которые содержат изображения корабля, повернутого на север, восток, юг и запад. Эти положения соответствуют поворотам одного спрайтового изображения на 90 0, следовательно, можно избежать применения всех изображений, если использовать возможности класса Sprite (рис. 17.2).
![](i_120.png)
Рис. 17.1. Спрайт пиратского корабля из игры High Seas состоит из четырех направленных спрайтов
![](i_121.png)
Рис. 17.2. Все изображения, кроме одного, могут быть исключены, но динамически восстановлены с помощью преобразований
В копилку Игрока
Использование преобразования спрайтов может отрицательно сказаться на реалистичности некоторых элементов графики игры. Например, пиратский корабль в игре High Seas 2 показывает лицевую и обратную стороны паруса, а также тень, отбрасываемую кораблем. Хотя эти детали незначительны, они теряются, если использовать описанную выше методику (рис. 17.2). В результате пиратский корабль выглядит менее реалистично.
Основной недостаток использования преобразований спрайтов – снижение скорости при снижении размера. Для того чтобы преобразовать спрайт, требуется немного больше времени, чем просто вывести его на экран, однако при этом выигрыш в размере значителен.
Приемы оптимизации Java-кодаПока мы обсудили оптимизацию, не рассматривая конкретные примеры. В последующих разделах я продемонстрирую вам методы написания кода, которые вы можете применять в разрабатываемых играх для увеличения быстродействия. Большая часть приводимого кода позволяет увеличить скорость выполнения приложения, а не сократить необходимый объем памяти.
Вы не должны задумываться об оптимизации скорости каждого бита создаваемого вами кода. Некоторые части кода вызываются чаще других. Зная это, вы должны оптимизировать именно такие фрагменты. Помните, что нереально оптимизировать каждую строку медленного кода игры. Ваша цель – оптимизировать те фрагменты, в которых это можно сделать, не прикладывая много усилий. Чуть позже вы узнаете, как выделить фрагменты кода, которым следует уделить особое внимание при оптимизации.
Совет Разработчику
Не думайте об оптимизации игрового кода по скорости или размеру до тех пор, пока игра не заработала. Оптимизированный код зачастую может содержать хитрые алгоритмы, сложные для отладки. Следовательно, всегда следует начинать с работающего кода, когда приходит время внедрять приемы оптимизации.
Возможно, самая простая оптимизация не подразумевает программирования вообще. Я говорю об исключении отладочной информации, которая по умолчанию включается в состав классов при использовании стандартного компилятора (javac). По умолчанию Java-компилятор включает дополнительную отладочную информацию в классы файлов, которая помогает отладчикам анализировать и идентифицировать код. По окончании отладки игры важно отключить отладочную информацию в компиляторе, используя ключ – g: none. Ниже приведен пример использования этого ключа:
javac -g:none MyMIDlet.java
К счастью, все примеры в этой книге, расположенные на прилагаемом компакт-диске, откомпилированы с выключенной отладочной информацией. Вам придется отключить опцию – g: none, если вы планируете отладить какой-либо из примеров.
Следующая методика оптимизации – это простой прием программирования, исключающий ненужные вычисления. Такие вычисления проблематичны, поскольку они занимают время процессора. Ниже приведен пример кода, который выполняет ненужные вычисления:
for (int i = 0; i < size(); i++)
a = (b + c)/i;
Несмотря на то что сложение (b + c) – это весьма эффективный фрагмент кода, лучше вынести его за пределы цикла:
int tmp = b + c;
for (int i = 0; i < size(); i++)
a = tmp/i;
Такое простое изменение может иметь значительный эффект, в зависимости от числа повторов выполнения цикла. Есть еще один прием оптимизации, который вы, вероятно, могли упустить. Обратите внимание на вызов метода size(). С точки зрения производительности, лучше сохранить возвращаемое им значение в отдельную переменную, чтобы избежать вызова метода при каждом повторе цикла:
int s = size();
int tmp = b + c;
for (int i = 0; i < s; i++)
a = tmp/i;
Говоря об оптимизации выражений, рассмотрим еще одну проблему, которая снижает скорость выполнения кода: общие выражения. Вы можете часто использовать выражения в коде, не осознавая последствий. В разгар работы можно использовать одно и то же выражение повторно, вместо того чтобы вычислить его значение однажды и присвоить переменной:
b = Math.abs(a) * c;
d = e / (Math.abs(a) + b);
Повторный вызов метода abs() – трудоемкая операция, вместо этого лучше вызвать метод однократно, а результат сохранить во временной переменной:
int tmp = Math.abs(a);
b = tmp * c;
d = e / (tmp + b);
Возможно, вы не задумывались, но Java-коду требуется больше времени обратиться к переменным класса, чем к локальным переменным. Это связано с тем, как осуществляется доступ к двум различным типам данных. На практике следует использовать локальные переменные, а не переменные класса, если вопрос производительности критичен. Например, если внутри цикла происходит постоянное обращение к переменной класса, то целесообразно присвоить локальной переменной значение переменной класса и внутри цикла работать с локальной переменной. Ниже приведен пример кода:
for (int i = 0; i < 1000; i++)
a = obj.b * i;
Как вы видите, внутри цикла обращение к переменной объекта obj выполняется 1000 раз. Оптимизация этого кода подразумевает замену переменной obj.b локальной переменной, к которой будет выполняться обращение в цикле:
int localb = obj.b;
for (int i = 0; i < 1000; i++)
a = localb * i;
Популярный «лобовой» прием оптимизации известен как раскрытие циклов, в результате которого исключается использование циклов. Даже простой цикл-счетчик перегружает процессор операциями сравнения и инкрементирования. Это может показаться неважным, однако в мобильных играх важен каждый бит оптимизации.
Раскрытие цикла подразумевает его замену «грубым» эквивалентом. Чтобы лучше понять это, давайте рассмотрим пример:
for (int i = 0; i < 1000; i++)
a[i] = 25;
Это, вероятно, выглядит как эффективный фрагмент кода, и на самом деле это так. Но если вы хотите ускорить его выполнение, то раскройте цикл:
int i = 0;
for (int j = 0; j < 100; j++) {
a[i++] = 25;
a[i++] = 25;
a[i++] = 25;
a[i++] = 25;
a[i++] = 25;
a[i++] = 25;
a[i++] = 25;
a[i++] = 25;
a[i++] = 25;
a[i++] = 25;
}
В приведенном примере вы сократили число повторений цикла на порядок (с 1000 до 100), но вы загрузили процессор операцией инкрементирования внутри цикла. В целом, приведенный код работает быстрее исходного, однако не ждите чудес. Раскрытие циклов может быть полезным в ряде случаев, но я не советую вам ставить этот метод оптимизации на первое место. Такой метод следует применять в играх, в которых важна каждая миллисекунда производительности.
Когда ваша игра готова к распространению, для сокращения объема кода можно использовать автоматическое средство. Я говорю об инструментах сжатия и затенения кода, которые сжимают код Java-программы и переименовывают переменные, чтобы усложнить процесс восстановления кода. Даже самый тщательно оптимизированный код наверняка будет содержать несколько неиспользуемых пакетов, классов, методов и переменных – именно для этого и нужна программа сжатия кода (shrinker). Программа затенения кода (obfuscator) предназначена не для повышения эффективности кода, а для его защиты и копирования.
Большинство программ сжатия и затенения кода объединены в один инструмент. Например, открытый инструмент ProGuard, служит как для сжатия, так и для затенения кода. Эту программу можно загрузить с адреса http://proguard.sourcesafe.net/. Такие инструменты, как ProGuard вырезают комментарии из кода и неиспользуемый код, а также переименовывают идентификаторы, используя более короткие криптографические имена. В результате получается класс, который на 20–50 % меньше и более защищенный по сравнению с исходным.
Анализ кода мобильной игрыПрограммисты часто говорят, что 90 % времени выполнения игры тратится на выполнение 10 % игрового кода. Это означает, что лишь малая часть кода действительно отвечает за выполнение игры. Вам необходимо сосредоточить внимание лишь на 10 % кода. Вы можете направить усилия по оптимизации на небольшой фрагмент программы, тогда вероятность создания эффективного мидлета значительно возрастает.
Принципиальная трудность для большинства разработчиков мобильных игр при начале оптимизации заключается не в использовании приемов оптимизации, а в поиске тех 10 % кода, которые будут выполняться 90 % времени. Выявление малой части кода, определяющей быстродействие мобильной игры, – это самая сложная грань процесса оптимизации. К счастью, для решения этого вопроса можно использовать специальный инструмент.
Анализатор (profiler) – это инструмент, которой анализирует программу во время ее выполнения и сообщает, сколько процессорного времени и циклов заняло выполнение определенной части программы. Вы можете изучить данные, собранные анализатором и определить, какая часть вашей программы выполняется чаще всего. Эта информация может указать, где следует приложить усилия и провести оптимизацию, используя приемы и методы, описанные в этой главе.
Пакет J2ME Wireless Toolkit поставляется с анализатором Java, который достаточно прост в использовании. Для начала запустите приложение Preferences (Настройки) стандартной установки J2ME Wireless Toolkit. Перейдите на вкладку Monitor (Монитор), и вы увидите окно как на рис. 17.3.
![](i_122.png)
Рис. 17.3. Вкладка Monitor приложения Preferences позволяет включить анализ мидлетов
Единственное отличие между окном, представленным на рисунке, и окном на экране вашего компьютера может заключаться в том, что у вас, вероятно, не поставлена галочка в окошке метки Enable Profiling (Включить анализ). Поставьте галочку, чтобы разрешить анализ мидлетов. Когда вы щелкнете по кнопке OK, анализатор Java готов, он запустится в следующий раз, когда вы будете использовать эмулятор J2ME.
Следующий шаг – это запустить игру в эмуляторе, например, Henway, разработанную в главе 7. По окончании работы эмулятора приложение анализатора автоматически запускается и показывает вам результаты анализа игры. На рис. 17.4 показан результат анализа игры Henway, проведенный на моем компьютере.
![](i_123.png)
Рис. 17.4. Анализатор J2ME Wireless Toolkit предоставляет детальную информацию о том, в каком месте игрового кода самые большие затраты времени и ресурсов процессора
Совет Разработчику
Перед тем как использовать анализатор, убедитесь, что вы не применяли к мидлету затенитель кода. Иначе применение анализатора станет бессмысленным.
Задача анализатора Java – подсказать вам, какие части Java-программы потребляют больше всего процессорного времени. Он выводит список всех вызовов в вашем мидлете и показывает, сколько времени было потрачено в каждом из них. Список вызовов, или, как его еще называют, «граф вызовов», представлен в левой панели в виде дерева. Каждый узел-потомок соответствует вызовам соответствующего метода из другого метода, представленного узлом-родителем. Это позволяет точно определить, на что же уходит время.
Колонки в правой панели окна анализатора (рис. 17.4) важны для интерпретации полученных данных:
► Name – полное имя метода
► Count – сколько всего раз вызывался метод
► Cycles – время, потраченное на выполнение данного метода (в тактах ЦП)
► %Cycles – процентная доля времени, потраченного на выполнение данного метода, от общего времени работы программы
► Cycles with Children – время выполнения данного метода и всех вызывавшихся из него (а тактах ЦП)
► %Cycles with Children – процентная доля времени в предыдущей колонке от общего времени работы программы.
Если щелкнуть по любому заголовку, то список отсортируется по соответствующей колонке. Чтобы освоиться с анализатором, взгляните на список методов в правой панели (рис. 17.4). Если сложить процентные доли всех методов, то в сумме всегда получится 100 %. Это понятно, ведь анализатор показывает, как общее время работы программы делится между отдельными методами. Если щелкнуть по знаку «+» слева от имени метода, то соответствующий узел раскроется, и вы увидите как время, проведенное в данном методе, распределяется между вложенными вызовами методов. Так, на рис. 17.4 мы видим, что метод HCanvas.run() потребляет 98 % времени ЦП. А на рис. 17.5 показано, как распределено это время между вложенными вызовами.
![](i_124.png)
Рис. 17.5. Раскрытие узла метода в левой панели анализатора показывает, какие методы вызываются из него
Ага, вот это уже интереснее – выясняется, что в HCanvas.draw() тратится 25 % времени, а в HCanvas.update() – свыше 72 %. Вряд ли это это повергнет вас в шок, но тем не менее анализатор показал, что при работе игры Henway примерно три четверти времени процессор тратит в методе HCanvas.update(). Имея такую информацию, вы сможете понять, куда направить усилия по оптимизации.
Совет Разработчику
Анализатор Java не предназначен для моделирования выполнения кода на конкретном телефоне, поэтому очень может статься, что на одних телефонах программа будет вести себя иначе, чем на других. Иными словами, оптимизация, сотворившая чудо в эмуляторе, может не дать столь же ощутимого эффекта при работе на конкретном телефоне. Поэтому так важно тестировать любую проведенную оптимизацию не только в эмуляторе, но и на реальных устройствах.
Помните, что в большинстве случаев анализатор указывает непосредственно на фрагмент кода, который следует оптимизировать. Но даже в этом случае вы должны вникнуть в суть происходящего и вычислить, какие методы занимают большую часть процессорного времени. Затем к выделенному фрагменту кода следует применить методы оптимизации, описанные выше.
Правообладателям!
Данное произведение размещено по согласованию с ООО "ЛитРес" (20% исходного текста). Если размещение книги нарушает чьи-либо права, то сообщите об этом.Читателям!
Оплатили, но не знаете что делать дальше?