close

Вход

Забыли?

вход по аккаунту

?

11111

код для вставкиСкачать
МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИ
Федеральное государственное бюджетное образовательное учреждение
высшего профессионального образования
САНКТ-ПЕТЕРБУРГСКИЙ ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ
АЭРОКОСМИЧЕСКОГО ПРИБОРОСТРОЕНИЯ
С. Г. Марковский, Н. В. Марковская
МУЛЬТИМЕДИАТЕХНОЛОГИИ
В МОБИЛЬНЫХ СИСТЕМАХ
Лабораторный практикум
Санкт-Петербург
2011
УДК 004.032.6
ББК 32.973.26-04
М27
Рецензенты:
кафедра комплексной защиты информации ГУАП;
доцент кафедры вычислительных систем и сетей ГУАП В. П. Попов
Утверждено
редакционно-издательским советом университета
в качестве лабораторного практикума
Марковский, С. Г.
М27
Мультимедиатехнологии в мобильных системах: лабораторный
практикум / С. Г. Марковский, Н. В. Марковская. – СПб.: ГУАП,
2011. – 90 с.: ил.
Приведены теоретические и методические указания к выполнению лабораторных работ по дисциплине «Мультимедиатехнологии
в мобильных системах». Лабораторный практикум предназначен
для магистров, обучающихся по магистерской образовательной программе 230211 «Мультимедиатехнологии».
УДК 004.032.6
ББК 32.973.26-04
Учебное издание
Марковский Станислав Георгиевич
Марковская Наталья Владимировна
МУЛЬТИМЕДИАТЕХНОЛОГИИ
В МОБИЛЬНЫХ СИСТЕМАХ
Лабораторный практикум
Редактор Г. Д. Бакастова
Верстальщик С. Б. Мацапура
Сдано в набор 10.10.11. Подписано к печати 9.11.11.
Формат 60×84 1/16. Бумага офсетная. Усл. печ. л. 5,23.
Уч.-изд. л. 5,35. Тираж 100 экз. Заказ № 493.
Редакционно-издательский центр ГУАП
190000, Санкт-Петербург, Б. Морская ул., 67
© Санкт-Петербургский государственный
университет аэрокосмического
приборостроения (ГУАП), 2011
© С. Г. Марковский,
Н. В. Марковская, 2011
ПРЕДИСЛОВИЕ
Лабораторный практикум по дисциплине «Мультимедиатехнологии в мобильных системах» является продолжением лабораторного практикума дисциплины «Программирование приложений
для мобильных устройств» [1]. Целью лабораторного практикума
является обучение магистров процессу создания мультимедийных
мобильных приложений на платформе Java ME и закрепление навыков программирования на языке высокого уровня Java, приобретенных при изучении других дисциплин.
Практикум включает в себя методические указания к четырем
лабораторным работам, посвященным изображениям в формате
ARGB, обработке звуковой информации, вопросам постоянного
хранения данных в мобильных устройствах и анимации. Четвертая
лабораторная работа посвящена разработке игрового приложения,
использующего современные мультимедиатехнологии.
Содержится большое число примеров мобильных приложений
(мидлетов) для мобильных устройств. Предполагается, что разработанные студентами мидлеты должны работать не только на программных эмуляторах, но и на реальных мобильных телефонах.
Студенты знакомятся и осваивают методы интерфейсов и классов
пакетов javax.microedition.media, javax.microedition.rms, javax.
microedition.media.control, javax.microedition.lcdui.game профиля
MIDP 2.0 платформы Java ME[2].
Выполнение лабораторных работ, а также приведенные примеры программ рассчитаны на использование среды программирования Java ME SDK 3.0 [1] или среды программирования NetBeans
6.9.1 [3].
По лабораторным работам № 1, 2 и 4 имеются дополнительные
задания, помеченные в тексте лабораторного практикума символом
«*». Выполнение этих заданий позволяет получить максимальный
рейтинг за лабораторную работу при использовании модульнорейтинговой системы оценки знаний студентов [4].
3
Лабораторная работа № 1
ARGB-изображения
Цель работы: изучение формата изображения ARGB, работа с
точками изображения напрямую, разработка мидлета, модифицирующего точки изображения в процессе работы.
1.1. Методические указания
Формат ARGB
MIDP 2.0 позволяет работать с изображениями в формате ARGB.
При этом изображение представляется в виде массива целых чисел.
Каждый элемент массива – четырехбайтное число, описывающее
один пиксель изображения. Формат ARGB приведен на рис. 1.1.
Старшие 8 бит числа представляют собой альфа-компоненту, отвечающую за прозрачность пикселей изображения. Следующие три байта соответствуют формату RGB (красный, зеленый, синий). Альфакомпонента может принимать значения от 0 до 255, причем 0 соответствует прозрачной точке изображения, а 255 – непрозрачной.
Хранение ARGB-изображения в виде массива точек разрешает
пользователю работать с пикселями изображения напрямую и модифицировать изображение в процессе работы мидлета.
Методы для работы с ARGB-изображениями
Для вывода изображения в формате ARGB на экран существует
метод класса Graphics: void drawRGB (int[] ARGBdata, int offset, int
scanlength, int x, int y, int width, int height, boolean processAlpha),
где:
− ARGBdata – массив данных в формате ARGB;
− offset – смещение от начала массива до первой точки изображения;
− scanlength – число точек в строке изображения;
− x и y – координаты изображения;
"MQIB
3FE
(SFFO
Рис. 1.1. Формат ARGB-изображения
4
#MVF
ºÁË
− width и height – ширина и высота изображения;
− processAlpha – флаг использования альфа-компоненты.
Если processAlpha имеет значение false, то альфа-компоненту и,
соответственно, прозрачность использовать нельзя.
Метод класса Image createRGBImage позволяет создавать из массива точек объект Image и работать с ним как с обычным рисунком:
static Image createRGBImage(int[] ARGBdata, int width, int height,
boolean processAlpha), где:
− ARGBdata – массив данных в формате ARGB;
− width и height – ширина и высота изображения;
− processAlpha – флаг использования альфа-компоненты.
Метод класса Image getRGB позволяет сохранить изображение в
виде массива точек: void getRGB (int[] ARGBdata, int offset, int
scanlength, int x, int y, int width, int height).
Назначение параметров такое же, как и в методе drawRGB.
В листинге 1.1 представлен код мидлета, демонстрирующий работу с ARGB-изображением. Мидлет состоит из двух файлов. Рассмотрим подробно работу метода paint класса Draw. Сначала на экран
выводится изображение из файла «/wolf200_156.png» шириной
200 пикселей и высотой 156 пикселей. Можно было бы использовать для вывода этого изображения обычный метод drawImage. Но,
чтобы показать применение метода getRGB, изображение выводится на экран за два шага. Первый шаг – изображение сохраняется в
массиве точек rgbData при помощи метода getRGB. Второй шаг – отображение изображения на экране с использованием метода drawRGB.
Далее, будем выводить на экран три ARGB-изображения квадрата
стороной sqSide, равной 25 пикселей. Для первого изображения заполняем массив пикселей pixArray значением 0xFF0000FF. Значение альфа-компоненты – 255, в формате RGB задаем синий цвет.
Используя метод createRGBImage, создаем объект класса Image. При
помощи обычного метода drawImage выводим непрозрачное изображение синего квадрата в левом верхнем углу экрана поверх основного изображения. Последний параметр в методе createRGBImage
устанавливаем в false, что запрещает использование альфакомпоненты. Для второго изображения заполним теперь массив
значением 0x7F0000FF. Значение альфа-компоненты – 127 позволяет выводить полупрозрачное изображение синего квадрата. Выводим изображение, задавая координаты (50,0), используя метод
drawRGB. Третье ARGB-изображение с координатами (100,0) – полностью прозрачное. Чтобы показать его границы, рисуем рамку белого цвета. Результат работы мидлета показан на рис. 1.2.
5
Рис. 1.2. Результат работы мидлета
Листинг 1.1
Файл ARGBMIDlet.java:
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
public class ARGBMIDlet extends MIDlet
{
private Draw dr;
private Display d;
}
public ARGBMIDlet()
{
dr = new Draw();
d = Display.getDisplay( this );
d.setCurrent( dr );
}
public void startApp() { }
public void pauseApp() { }
public void destroyApp(boolean destroy) { }
Файл Draw.java:
import javax.microedition.lcdui.*;
import java.io.IOException;
public class Draw extends Canvas
{
private Image im, imARGB;
private int[] pixArray = null;
private int[] rgbData = null;
public Draw()
6
{
}
super();
try
{
im = Image.createImage(«/wolf200 _ 156.png»);
}
catch( IOException ioe ) { System.out.println( ioe.getMessage() );
}
public void paint( Graphics g )
{
int sqSide = 25;
int width = 200;
int height = 156;
g.setColor( 0xFFFFFF ); // белый цвет
g.fillRect( 0, 0, g.getClipWidth(), g.getClipHeight() );
if(rgbData == null) rgbData = new int[width * height];
im.getRGB(rgbData, 0, width, 0, 0, width, height );
g.drawRGB(rgbData, 0, width, 0, 0, width, height, false);
if(pixArray == null) pixArray = new int[sqSide * sqSide];
// непрозрачный синий квадрат
for(int i = 0; i < pixArray.length; i++) pixArray[i] = 0xFF0000FF;
imARGB = Image.createRGBImage(pixArray, sqSide, sqSide, false);
g.drawImage( imARGB, 0, 0, Graphics.LEFT | Graphics.TOP );
// полупрозрачный синий квадрат
for(int i = 0; i < pixArray.length; i++) pixArray[i] = 0x7F0000FF;
g.drawRGB(pixArray, 0, sqSide, 50, 0, sqSide, sqSide, true);
}
}
// прозрачный синий квадрат
for(int i = 0; i < pixArray.length; i++) pixArray[i] = 0x000000FF;
g.drawRGB(pixArray, 0, sqSide, 100, 0, sqSide, sqSide, true);
g.drawRect(100, 0, sqSide, sqSide); // рамка
Теперь поменяем в методе paint строку кода g.drawRGB(rgbData,
0, width, 0, 0, width, height, false); на строку g.drawRGB(rgbData,
10000, width, 0, 0, width, height – 50, false), чтобы показать использование параметра offset (смещение от начала массива на первую точку изображения). Зададим значение offset, равное 10 000.
Так как ширина строки – 200 пикселей, то это означает, что изображение будет выводиться на экран, начиная с 51-й строки. При этом
параметр height в методе drawRGB уменьшаем на 50. Экранная форма
для случая вывода изображения с заданным значением параметра
offset показана на рис. 1.3.
7
Рис. 1.3. Вывод изображения
с ненулевым значением параметра offset
1.2. Задание к лабораторной работе
1. Получить у преподавателя файл изображения в формате JPG.
2. Создать файл в формате PNG. Размер изображения не должен
превышать 240 пикселей по ширине и 320 пикселей по высоте.
3. Разработать мидлет, в котором на экран выводится изображение из файла. Поверх этого изображения выводится непрозрачное
ARGB-изображение, цвет которого задается согласно табл. 1.1. Используя поток, постепенно сделать все точки ARGB-изображения
прозрачными. Порядок модификации точек ARGB-изображения
приведен в табл. 1.2.
Таблица 1.1
Цвет непрозрачного ARGB-изображения
Номер варианта
1, 9, 17
2,10,18
3,11,19
4,12,20
5,13,21
6,14,22
7,15,23
8,16,24
Цвет изображения
Красный
Зеленый
Синий
Желтый
Фиолетовый
Малиновый
Голубой
Серый
4*. Добавить в мидлет, указанный в п. 3, обработку событий с
клавиатуры мобильного телефона. Используемые клавиши: «1» –
все точки ARGB-изображения прозрачные, «2» – все точки ARGB8
а)
б)
в)
Рис. 1.4. Экранные формы работы мидлета
изображения непрозрачные, «3» – точки ARGB-изображения становятся прозрачными; «4» – точки ARGB-изображения становятся
непрозрачными.
Таблица 1.2
Порядок модификации точек ARGB изображения
Номер варианта
Порядок модификации точек изображения
1, 9, 17
2,10,18
3,11,19
4,12,20
5,13,21
6,14,22
7,15,23
8,16,24
Начиная с верхней строки (справа налево)
Начиная с верхней строки (слева направо)
Начиная с нижней строки (справа налево)
Начиная с нижней строки (слева направо)
Начиная с крайнего левого столбца (сверху вниз)
Начиная с крайнего левого столбца (снизу вверх)
Начиная с крайнего правого столбца (снизу вверх)
Случайным образом
5. Проверить работу мобильного приложения на телефоне.
9
Экранные формы, приведенные на рис. 1.4, демонстрируют работу мидлета (1.4, а – на экране ARGB-изображение, 1.4, б – постепенное появление нижнего изображения, 1.4, в – на экране изображение из файла).
1.3. Содержание отчета
1. Титульный лист.
2. Цель работы.
3. Задание к лабораторной работе.
4. Изображение в формате PNG.
5. Листинг с кодом мидлета.
6*. Метод обработки событий с клавиатуры, дополняющий листинг п. 5.
7. Экранные формы с результатами работы мидлета.
8. Выводы по лабораторной работе.
10
Лабораторная работа № 2
Работа со звуком в Java ME
Цель работы: изучение библиотеки MIDP 2.0 Media API, воспроизведение тоновой последовательности, воспроизведение звука
из аудиофайла, знакомство с форматом RTTTL, разработка мидлетов для работы со звуком.
2.1. Методические указания
В профиле MIDP 2.0 имеется возможность работы со звуком.
Существует MIDP 2.0 Media API библиотека для мобильных
устройств [5], которая содержит набор классов и интерфейсов, поддерживающих различные средства мультимедиа. Классы и интерфейсы, входящие в состав библиотеки, размещены в пакетах javax.
microedition.media и javax.microedition.media.control.
Класс Manager позволяет опрашивать телефон о поддерживаемых средствах мультимедиа, а также создавать проигрыватели для
воспроизведения различных типов мультимедийных данных.
Методы класса Manager:
− static Player createPlayer(InputStream stream, String type) – создает проигрыватель для воспроизведения мультимедиа из потока;
− static Player createPlayer(String locator) – создает проигрыватель для воспроизведения мелодии, созданной программным путем;
− static String[] getSupportedContentTypes(String protocol) – возвращает список мультимедийных средств, поддерживаемых телефоном;
− static String[] getSupportedProtocols(String contentType) – возвращает список, поддерживаемых протоколов передачи.
Запрос аудиовозможностей телефона
Чтобы узнать, какие средства мультимедиа может поддерживать телефон, необходимо воспользоваться методом getSupportedContentTypes(). Ниже приведен пример вызова этого метода:
String[] contentTypes = Manager.getSupportedContentTypes(null).
В листинге 2.1 приведен фрагмент кода мидлета для вывода на
экран аудиовозможностей мобильного устройства. Метод paint отображает информацию на объект класса Canvas. Так как нас будет
11
интересовать возможность устройства воспроизводить звуковые
файлы из jar-архива, то метод getSupportedContentTypes() вызывается с параметром «file». Далее на экран отображаются строки контента, относящиеся только к типу «audio», так как тип «video» в
данной работе не рассматривается.
Листинг 2.1
public void paint(Graphics g) {
// очистить холст
g.setColor(255, 255, 255);
g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(0, 0, 0);
// черный цвет шрифта
// получить поддерживаемые средства мультимедиа
String[] contentTypes = Manager.getSupportedContentTypes(“file”);
}
// высветить поддерживаемые звуковые типы файлов
int y = 0;
for (int i = 0; i < contentTypes.length; i++)
{
if(contentTypes[i].substring(0,5).equals(“audio”))
{
g.drawString(contentTypes[i],0, y, Graphics.TOP|Graphics.LEFT);
y += 20;
}
}
Результат работы мидлета для эмулятора Java(TM) ME Platform
SDK 3.0 представлен на рис. 2.1.
Рис. 2.1. Типы файлов, поддерживаемых
эмулятором Java(TM) ME Platform SDK 3.0
12
Если, например, запустить мобильное приложение на телефоне Samsung SGH-E950, то последовательно высветятся следующие
строки:
− audio/amr;
− audio/midi;
− audio/x-tone-seq;
− audio/aac;
− audio/mpeg.
В табл. 2.1 приведен список типов MIME аудиофайлов для пояснения полученных звуковых возможностей эмулятора и телефона в результате работы мидлета. MIME (Multipurpose Internet
Mail Extensions – многоцелевые расширения почты интернета) –
Интернет-стандарт, описывающий передачу различных типов данных по электронной почте.
Таблица 2.1
Список типов MIME, поддерживаемых в MIDP 2.0
Тип MIME
Файл
audio/x-tone_seq, audio/tone
audio/x-wav, audio/wav
audio/mpeg
audio/aac
audio/amr;
audio/midi, audio/mid, audio/x-midi
audio/sp-midi
Файл тоновой последовательности
wav-файл
mp3-файл
aac-файл
amr-файл
midi-файл
midi-файл для телефонов
третьего поколения
Воспроизведение тоновой последовательности
Тоновая последовательность – это набор звуков, воспроизводимых в определенном порядке, позволяющая программировать музыку. Тоновая последовательность хранится внутри массива типа
byte. Каждый байт массива имеет свое особое значение. Чтобы задать тоновую последовательность, необходимо использовать ряд
констант, определенных в интерфейсе ToneControl (табл. 2.2).
Версия тоновой последовательности – указывает версию
ToneControl и всегда равна 1.
Темп – определяет скорость воспроизведения мелодии и определяется количеством ударов в минуту. Может принимать значения
от 20 до 508. Значение по умолчанию – 120. Так как для указания
темпа в тоновой последовательности используется один байт, то не13
обходимо разделить это значение на 4. Например, если темп равен
120, то в последовательности значение параметра записывается
как 30.
Таблица 2.2
Константы интерфейса ToneControl
Константа
Значение
константы
ToneControl.VERSION
ToneControl.TEMPO
–2
–3
ToneControl.RESOLUTION
–4
ToneControl.SET_VOLUME
ToneControl.SILENCE
ToneControl.C4
ToneControl.BLOCK_START
ToneControl.BLOCK_END
ToneControl.PLAY_BLOCK
–8
–1
60
–5
–6
–7
Описание
Версия тоновой последовательности
Темп или скорость воспроизведения
Разрешение тоновой последовательности
Установить громкость
Пауза в звучании (тишина)
Нота «до»
Начало блока тонов
Конец блока тонов
Воспроизведение блока тонов
Разрешение – определяет, на сколько частей будет делиться целая нота. Диапазон возможных значений – 1/1 … 1/127. Значение
по умолчанию – 1/64.
Громкость – определяет громкость звучания. Эффективные значения от 0 до 100% громкости. Значение по умолчанию – 100%.
Блок – повторяющиеся фрагменты мелодий можно записывать в
виде блоков. Начало блока имеет метку BLOCK_START, а конец блока –
BLOCK_END. После меток должен следовать целочисленный идентификатор блока от 0 до 127. Идентификаторы начала и конца блока
обязательно должны совпадать.
Воспроизведение блока – параметр имеет метку PLAY_BLOCK, после которой указывается идентификатор блока.
Нота – параметр не имеет метки. Каждая нота в последовательности определяется парой значений, которые задают высоту
(табл. 2.3) и длительность звука.
Таблица 2.3
Названия, частоты и значения основных нот четвертой октавы
14
Музыкальная нота
Частота, Гц
Значение (высота тона)
C (до)
C#
D (ре)
262
277
294
60
61
62
Окончание табл. 2.3
Музыкальная нота
Частота, Гц
Значение (высота тона)
D#
E (ми)
F (фа)
F#
G (соль)
G#
A (ля)
A#
B (си)
311
330
349
370
392
416
440
466
493
63
64
65
66
67
68
69
70
71
Длительность (табл. 2.4) – определяет, сколько частей ноты будет звучать. Диапазон значений от 0 до 127.
Таблица 2.4
Длительности нот и соответствующие им значения
Длительность ноты
Значение
1/1
1/2
1/4
1/8
64
32
16
8
Константа ToneControl.SILENCE используется для установки паузы звучания в тоновой последовательности.
Спецификация RTTTL
Самым популярным форматом записи монофонических мелодий (рингтонов) является формат RTTTL (рис. 2.2) фирмы Nokia.
Различные Web-сайты в Internet распространяют мелодии, представленные в формате RTTTL.
Название мелодии не должно превышать 11 латинских символов.
¦¹À»¹ÆÁ¾
ÉÁƼËÇƹ
ªÄÌ¿¾ºÆ¹Ø
ÁÆÍÇÉŹÏÁØ
£Ç½Å¾ÄǽÁÁ
Рис. 2.2. Формат RTTTL
15
Служебная информация, отвечающая за воспроизведение рингтонов состоит из символов (d,o,b,v,s), знака = и численного значения. Параметры разделяются запятыми (без пробелов).
Перечислим основные параметры формата:
− d(duration) – длительность, принимаемая в коде мелодии по
умолчанию. Например, если d = 16, то в коде мелодии можно писать не 16c2, а записать просто c2. Если не указывать значение параметра d, то по умолчанию оно равно 4;
− o(octave) – октава, принимаемая по умолчанию. Например,
если в коде мелодии много нот октавы 2, то можно задать o = 2 и
тогда вместо 16c2 можно записать 16c. Если не указывать значение
параметра o, то по умолчанию оно равно 5;
− b(beats per minute) – темп мелодии (число ударов в минуту).
По умолчанию b = 63. Диапазон возможных значений изменяется
от 25 до 900.
Сокращение кода мелодии необходимо для уменьшения объема
передаваемой информации, например, при использовании SMS.
Для наглядности кода мелодии лучше эти сокращения не использовать;
Параметры v(volume) – громкость звучания и s(style) – стиль исполнения указываются в формате RTTTL не так часто и здесь рассмотрены не будут.
В коде мелодии записываются ноты в следующем формате:
[длительность] нота [дополнительные значки] [октава] разделитель. В квадратных скобках указаны необязательные параметры.
Длительность определяет, какая часть ноты будет звучать. Если
длительность равна x, то будет звучать 1/x ноты. Возможные значения x = 1, 2, 4, 8, 16, 32. Например, если длительность равна 8,
то будет звучать 1/8 ноты.
Ноты принято писать маленькими буквами. Возможные ноты и
их названия приведены в табл. 2.5.
Таблица 2.5
Ноты в RTTTL
16
Нота
Название
C
c#
d
d#
E
F
До
До-диез
Ре
Ре-диез
Ми
Фа
Окончание табл. 2.5
Нота
Название
f#
g
g#
a
a#
b
p
Фа-диез
Соль
Соль-диез
Ля
Ля-диез
Си
Пауза
Дополнительные значки, представленные в табл. 2.6, позволяют изменить длительность звучания ноты. Например, для записи
4c. . Длительность ноты будет не 1/4, а 3/8.
Таблица 2.6
Дополнительные значки
Значок
Действие
.
;
&
Увеличение длительности ноты в 1,5 раза
Увеличение длительности ноты в 2 раза
Увеличение длительности ноты в 2,5 раза
Октава. В формате RTTTL предполагается, что в секвенсоре мобильного телефона присутствуют только четыре октавы: 4, 5, 6 и 7.
Эти октавы дублируют октавы 0, 1, 2 и 3. Поэтому, например, ноты
0-й и 4-й октав будут воспроизводиться одинаково, т. е. записи 8a0
и 8a4 – идентичны.
В качестве разделителя в коде мелодии используется запятая.
Для воспроизведения мелодии, представленной в формате
RTTTL ее необходимо конвертировать в формат тоновой последовательность MIDP 2.0 Media API.
П р и м е р. Задана мелодия в формате RTTTL (канкан композитора Оффенбаха из оперетты «Орфей в аду»):
CanCan:d=4,o=5,b=160:2c,8d,8f,8e,8d,8g,8p,8g,8p,8g,8a,8e,8f,
8d,8p,8d,16p,32p,8d,8f,8e,8d,8c,8c6,8b,8a,8g,8f,8e,8d.
Из записи мелодии видно, что практически все ноты, кроме 8c6,
относятся к 5-й октаве, значение которой задано по умолчанию.
Значение темпа примем равным 40, т. е. разделим параметр b на 4.
Определим все ноты, необходимые для воспроизведения последовательности через константу ToneControl.C4:
byte C4 = ToneControl.C4;
byte C5 = (byte) (C4 + 12);
17
byte
byte
byte
byte
byte
byte
byte
byte
D5 = (byte) (C5 + 2);
E5 = (byte) (C5 + 4);
F5 = (byte) (C5 + 5);
G5 = (byte) (C5 + 7);
A5 = (byte) (C5 + 9);
B5 = (byte) (C5 + 11);
C6 = (byte) (C5 + 12);
r = ToneControl.SILENCE.
Сформируем байтовый массив тоновой последовательности.
byte [] Cancan = {
ToneControl.VERSION, 1,
ToneControl.TEMPO, 40,
C5, 32, D5, 8, F5, 8, E5, 8, D5, 8, G5, 8, r, 8,
G5, 8, r, 8, G5, 8, A5, 8, E5, 8, F5, 8, D5, 8,
r, 8, D5, 8, r, 4, r, 2, D5, 8, F5, 8, E5, 8, D5, 8,
C5, 8, C6, 8, B5, 8, A5, 8, G5, 8, F5, 8, E5, 8, D5, 8
};
Выделим повторяющиеся фрагменты в мелодии и оформим их
в виде блока. Тогда массив тоновой последовательности можно записать следующим образом:
byte [] Melody = {
ToneControl.VERSION, 1,
ToneControl.TEMPO, 40,
ToneControl.BLOCK _ START, 0,
F5, 8, E5, 8, D5, 8,
ToneControl.BLOCK _ END, 0,
C5, 32, D5, 8,
ToneControl.PLAY _ BLOCK, 0,
G5, 8, r, 8, G5, 8, r, 8, G5, 8, A5, 8, E5, 8, F5, 8, D5, 8,
r, 8, D5, 8, r, 4, r, 2, D5, 8, ToneControl.PLAY _ BLOCK, 0,
C5, 8, C6, 8, B5, 8, A5, 8, G5, 8, ToneControl.PLAY _ BLOCK, 0
};
В листинге 2.2 приведен фрагмент кода мидлета для воспроизведения созданной тоновой последовательности. Этот код необходимо вставить или в конструктор мидлета, или в метод startApp()
мидлета.
Листинг 2.2
try
{
Player pl = Manager.createPlayer(Manager.TONE_DEVICE_LOCATOR);
pl.realize();
ToneControl c = (ToneControl) pl.getControl(«ToneControl»);
c.setSequence(Melody);
pl.start();
18
}
catch( IOException ioe ) { }
catch( MediaException me ) { }
Созданный массив тоновой последовательности можно сохранить в файле с расширением.jts (Java Tone Sequence). Для этого
напишем Java-приложение в среде NetBeans (листинг 2.3). Параметры ToneControl.VERSION и ToneControl.TEMPO указываем в виде
байтовых значений в шестнадцатеричной системе счисления. Имя
файла (параметр args[0]) задается при запуске программы в командной строке. В данном случае было использовано имя «cancan».
Содержимое файла с расширением «cancan.jts» представлено на
рис. 2.3.
Листинг 2.3
package writetojts;
import java.io.*;
public class Main {
public static void main(String[] args) {
ByteArrayOutputStream bos = null;
FileOutputStream fos = null;
byte
byte
byte
byte
byte
byte
byte
byte
byte
C4
C5
D5
E5
F5
G5
A5
B5
C6
=
=
=
=
=
=
=
=
=
60;
(byte)
(byte)
(byte)
(byte)
(byte)
(byte)
(byte)
(byte)
(C4
(C5
(C5
(C5
(C5
(C5
(C5
(C5
+
+
+
+
+
+
+
+
12);
2);
4);
5);
7);
9);
11);
12);
byte r = (byte)0xFF;
byte [] Melody = {
(byte)0xFE, 1,
(byte)0xFD, 40,
C5, 32, D5, 8, F5, 8, E5, 8, D5, 8, G5, 8, r, 8,
G5, 8, r, 8, G5, 8, A5, 8, E5, 8, F5, 8, D5, 8, r, 8,
D5, 8, r, 4, r, 2, D5, 8, F5, 8, E5, 8, D5, 8,
Рис. 2.3. Содержимое бинарного файла «cancan.jts»
19
};
try
{
}
}
C5, 8, C6, 8, B5, 8, A5, 8, G5, 8, F5, 8, E5, 8, D5, 8
bos = new ByteArrayOutputStream();
bos.write(Melody, 0, Melody.length);
fos = new FileOutputStream (new File( args[0] + «.jts»));
bos.writeTo(fos);
} catch(Exception e) {System.err.println(e);}
finally{
try {
if( fos != null ) fos.close();
} catch(Exception e) {System.err.println(e);}
} // end finally
Воспроизведение звука из файла
Для воспроизведения звука из файла необходимо сначала создать проигрыватель:
Pl = Manager.createPlayer(getClass().getResourceAsStream(
«/cancan.jts»),»audio/x-tone-seq»);
Метод getClass() возвращает экземпляр класса, в котором он находится. Метод getResourceAtStream() создает поток звуковых данных и связывает его с файлом cancan.jts, который должен храниться в каталоге res проекта. Второй параметр в методе createPlayer()
указывает тип потока данных MIME (см. табл. 2.1). В данном случае
мы воспроизводим файл тоновой последовательности «cancan.jts»,
поэтому указывается тип «audio/x-tone-seq». Далее необходимо
последовательно перевести проигрыватель в состояния REALIZED,
PREFETCHED и STARTED, вызывая соответствующие методы.
В листинге 2.4 представлен фрагмент кода мидлета для воспроизведения файла «cancan.jts», который необходимо вставить в конструктор мидлета или в метод startApp() мидлета.
Листинг 2.4
try
{
pl = Manager.createPlayer(getClass().getResourceAsStream(
“/cancan.jts”),”audio/x-tone-seq”);
pl.realize();
pl.prefetch();
pl.start();
} catch (Exception e) { }
20
Теперь составим мобильное приложение PlayerMIDlet для воспроизведения звукового файла, записанного с микрофона. Предполагается, что звук с микрофона был записан в файл «speech.wav».
Для запуска мидлета на телефоне Samsung SGH-E950 файл был
конвертирован в формат mp3, так как этот телефон не поддерживает воспроизведение wav-файлов. Кроме того, необходимо реализовать возможность остановки воспроизведения с последующим продолжением с места остановки. Для этого класс PlayerMIDlet должен
реализовывать интерфейс PlayerListener:
public class PlayerMIDlet extends MIDlet implements PlayerListener,
CommandListener. Этот интерфейс имеет специальный метод для
управления проигрывателем: void playerUpdate(Player player,
String event, Object eventData). Метод принимает три параметра:
проигрыватель, от которого пришло событие, само событие и дополнительная информация о событии.
Приложение будет содержать три команды:
− Command cmdExit = new Command(«Exit»,Command.EXIT, 1) – команда
выхода из приложения;
− Command cmdPlay = new Command(«Play»,Command.SCREEN, 1) – команда начала воспроизведения;
− Command cmdPause = new Command(«Pause»,Command.SCREEN, 1) – команда остановки воспроизведения.
Добавляем в приложение объекты Player pl; Form f;
Объект класса Form необходим только для добавления команд.
Ниже приведен конструктор класса PlayerMIDlet, в котором реализуются следующие действия (листинг 2.5):
1. Создается объект проигрывателя (в качестве типа файла указывается «audio/mpeg»).
2. К проигрывателю добавляется обработчик событий.
3. Проигрыватель переводится в состояние REALIZED.
4. Проигрыватель переводится в состояние PREFETCHED.
5. Создается объект класса Form.
6. К форме добавляются команды cmdExit, cmdPlay и обработчик
команд.
Листинг 2.5
public PlayerMIDlet(){
try
{
pl = Manager.createPlayer(getClass().getResourceAsStream(
“/speech.mp3”),”audio/mpeg”);
21
pl.addPlayerListener(this);
pl.realize();
pl.prefetch();
}
} catch (Exception e) { }
}
f = new Form( «Player» );
f.setCommandListener(this);
f.addCommand( cmdExit );
f.addCommand( cmdPlay );
Display.getDisplay(this).setCurrent(f);
В методе CommandAction() (листинг 2.6):
− по команде cmdExit завершается работа приложения и проигрыватель переводится в состояние CLOSED;
− по команде cmdPlay производится запуск проигрывателя, и
проигрыватель переводится в состояние STARTED;
− по команде cmdPause выполняется остановка проигрывателя, и
проигрыватель переводится в состояние PREFETCHED;
Листинг 2.6
public void commandAction(Command c, Displayable d)
{
if( c == cmdExit ){
pl.close();
notifyDestroyed();
} else if( c == cmdPlay ){
try{
pl.start();
} catch(Exception e){}
} else if( c == cmdPause ){
try{
pl.stop();
} catch(Exception e){}
}
}
В методе playerUpdate() (листинг 2.7) производится обработка событий проигрывателя. Если проигрыватель находится в состоянии
STARTED, то из формы удаляется команда cmdPlay и добавляется
команда cmdPause. Если же проигрыватель находится в состояниях
STOPPED или CLOSED или закончились данные для воспроизведения (константа END_OF_MEDIA) удаляется команда cmdPause и добавляется команда cmdPlay.
Листинг 2.7
public void playerUpdate(Player pl, String event, Object obj)
22
{
}
if( event == STARTED ){
f.removeCommand( cmdPlay );
f.addCommand( cmdPause );
} else if( ( event == STOPPED ) | ( event == CLOSED ) |
( event == END_OF_MEDIA ))
{
f.removeCommand( cmdPause );
f.addCommand( cmdPlay );
}
Следует отметить, что в списке, приведенном на рис. 2.1, указано, что эмулятор Java(TM) ME Platform SDK 3.0 воспроизводит
mp3-файлы. Однако попытка запустить файл «speech.mp3» на эмуляторе закончилась неудачей. Поэтому для реализации мобильного приложения на эмуляторе пришлось заменить строку создания
проигрывателя на следующую строку:
pl = Manager.createPlayer(getClass().getResourceAsStream(
«/speech.wav»),«audio/x-wav») и использовать файл «speech.
wav». Экранные формы, иллюстрирующие работу приложения, показаны на рис. 2.4.
Реализованный проигрыватель не имеет управление громкостью
воспроизведения. Если необходимо к проигрывателю добавить
Рис. 2.4. Экранные формы плеера
в эмуляторе Java(TM) ME Platform SDK 3.0
23
управление громкостью, то сначала следует получить управление
громкостью при помощи метода getControl() интерфейса Player:
− VolumeControl vc;
− vc = (VolumeControl)pl.getControl(«VolumeControl»).
Затем добавить на форму команды увеличения и уменьшения
громкости и реализовать их обработку в методе commandAction().
Ддя управления громкостью используются следующие методы интерфейса VolumeControl:
− int getLevel() – возвращает текущий уровень громкости;
− int setLevel(int level) – устанавливает уровень громкости.
Значение может находиться в пределах от 0 до 100.
2.2. Задание к лабораторной работе
1. Написать мидлет 1 для получения аудиовозможностей эмулятора (мобильного телефона).
2. Получить у преподавателя последовательность в формате
RTTTL.
3. Конвертировать последовательность в формате RTTTL в тоновую последовательность MIDP 2.0 Media API при возможности,
объединяя повторяющиеся фрагменты мелодии в блоки.
4. Составить мидлет 2 для воспроизведения тоновой последовательности.
5. Записать тоновую последовательность в файл с расширением.
jts.
6. Разработать мидлет 3 для воспроизведения звука из файла с
расширением.jts.
7. Записать с микрофона wav-файл, содержащий следующий
текст:
Меня зовут _______________________. Я студент(ка) группы
__________.
(Фамилия, Имя, Отчество)
Я учусь в магистратуре. Мы изучаем дисциплину «Мультимедиатехнологии для мобильных систем».
8. Если мобильный телефон не воспроизводит wav-файл, то конвертировать его в тип файла, поддерживаемого мобильным телефоном, например, mp3-файл.
9. Написать мидлет 4, воспроизводящий звуковой файл (п. 7,8).
10. Проверить работу созданных мидлетов на мобильных телефонах.
24
11*. Написать мидлет 5, воспроизводящий звуковой .mid или
.wav файл не из JAR-архива, а через URL.
12*. Проверить работу мидлета 5 на эмуляторе Java(TM) ME
Platform SDK 3.0.
2.3. Содержание отчета
1. Титульный лист.
2. Цель работы.
3. Задание.
4. Листинг с кодом мидлета 1.
5. Экранные формы с результатами работы мидлета 1.
6. Последовательность в формате RTTTL.
7. Листинг с кодом мидлета 2.
8. Содержимое файла с расширением.jts.
9. Листинг с кодом мидлета 3.
10. Листинг с кодом мидлета 4.
11*. Листинг с кодом мидлета 5.
12. Выводы по лабораторной работе.
25
Лабораторная работа № 3
Хранение данных в мобильных устройствах
Цель работы: изучение организации постоянного хранения
данных в MIDP 2.0, знакомство с системой управления записями,
работа с пакетом javax.microedition.rms, разработка мидлета для
работы с базой данных.
3.1. Методические указания
Постоянного хранения данных обычно требует любое приложение, разработанное на платформах Java SE, Java EE, Java ME.
Однако, если платформы Java SE и Java EE, поддерживают файловую систему и позволяют сохранять информацию в виде файлов на
дисках, серверах, в сетевых базах данных, то платформа Java ME
ориентирована на компьютерные устройства с ограниченными возможностями. Большинство мобильных устройств не поддерживают
файловую систему. Хранение данных в Java ME организовано при
помощи системы управления записями RMS (Record Management
System). Классы и интерфейсы для работы с RMS размещены в пакете javax.microedition.rms. Хранилище записей – это база данных,
которая может быть представлена следующей схемой (рис. 3.1).
На рисунке представлены три хранилища с именами ”Book Store»,
«Food Market» и «Department Store». Данные представляются записями, которые записываются в хранилище и постоянно там сохраняются, в том числе и при закрытии приложения, перезагрузке
телефона и замене аккумулятора.
Хранилище записи создается мидлетом и представляется объектом класса RecordStore. Запись является массивом байтов типа
byte[]. RMS не поддерживает описание или форматирование полей
записи, поэтому приложения должны преобразовывать данные из
произвольных типов в байты при создании записей, а при чтении,
наоборот, преобразовывать их из байтов в соответствующие типы
данных.
Основные методы класса RecordStore приведены в табл. 3.1.
Чтобы получить доступ к записи в хранилище необходимо знать
ее уникальный номер recordID. Другой способ – извлечение списка
записей из хранилища, а затем выбор из списка одной или нескольких необходимых записей. Класс RecordStore определяет метод
RecordEnumeration enumerateRecords(RecordFilter, RecordComparator,
26
®É¹ÆÁÄÁÒ¹À¹ÈÁʾÂ
„#PPL4UPSF”
3FDPSE
„#PPL4UPSF”
3FDPSE
„'PPE.BSLFU”
3FDPSE/
„%FQBSUNFOU4UPSF”
„'PPE.BSLFU”
3FDPSE
„%FQBSUNFOU4UPSF”
3FDPSE
3FDPSE
3FDPSE/
3FDPSE
3FDPSE/
Рис. 3.1. Хранилище записей
boolean keepUpdated), который выдает список записей в хранилище. Отметим, что для того, чтобы извлекать записи, номера записей указывать не нужно. Интерфейс RecordEnumeration является
нумератором записей, выполняя перечисление всех записей хранилища. Основные методы интерфейса RecordEnumeration приведены в табл. 3.2. Навигация по списку записей осуществляется при
помощи методов nextRecordId и previousRecordId, или nextRecord и
previousRecord. Если в методе enumerateRecords в качестве параметров RecordFilter и RecordComparator передавать значения null, то
можно получить список всех записей хранилища.
Интерфейс RecordFilter позволяет извлекать из хранилища только те записи, которые соответствуют заданным критериям поиска.
Для этого необходимо реализовать интерфейс RecordFilter и переписать его метод boolean matches(byte[]candidate), который должен
возвратить значение true, если запись соответствует критерию поиска и false в противоположном случае.
27
Таблица 3.1
Методы класса RecordStore
Метод
static RecordStore
openRecordStore
(String recordStoreName)
static RecordStore
openRecordStore
(String recordStoreName,
boolean createIfNecessary)
Описание
Открывает хранилище записей с именем
recordStoreName
Открывает хранилище записей с именем
recordStoreName. Если createIfNecessary =
true, то создает новое хранилище записей,
если хранилище с именем recordStoreName
не существует, в противном случае
(createIfNecessary = false) – не создает
Закрывает хранилище записей
Удаляет хранилище записей с именем
recordStoreName
void closeRecordStore()
static void
deleteRecordStore(String
recordStoreName)
int addRecord(byte[] data, Добавляет запись, представленную масint offset, int numBytes) сивом байтов data в хранилище, начиная с
позиции offset в массиве байтов размером
numBytes. Возвращает номер записи
void deteteRecord(int
Удаляет из хранилища запись с номером
recordId)
recordId
byte[] getRecord(int
Возвращает запись с номером recordId из
recordId)
хранилища
int getRecord(int recordId, Возвращает запись с номером recordId из
byte[] buffer, int offset) хранилища в массив buffer, начиная с позиции offset
int getNumRecords()
Возвращает число записей в хранилище
int getNextRecordID()
Возвращает номер, который будет присвоен следующей добавленной записи
Таблица 3.2
Методы интерфейса RecordEnumeration
Метод
int numRecords()
byte[] nextRecord()
byte[] previousRecord()
int nextRecordId()
28
Описание
Возвращает количество записей, содержащихся в списке
Возвращает копию следующей записи в
списке
Возвращает копию предыдущей записи в
списке
Возвращает номер следующей записи в
списке
Окончание табл. 3.2
Метод
Описание
int previousRecordId()
Возвращает номер предыдущей записи в
списке
boolean hasNextElement()
Возвращает значение true, если список содержит следующую запись, т. е. возможно продвижение с использованием метода
nextRecord()
boolean hasPreviousElement() Возвращает значение true, если список содержит предыдущую запись, т. е. возможно продвижение с использованием метода
previousRecord()
void reset()
Возвращает нумератор списка в исходное
положение
void rebuild()
Перестраивает список после выполнения
операций добавления или удаления записей в хранилище
Интерфейс RecordComparator позволяет отсортировать список в
заданном порядке. Для этого необходимо реализовать этот интерфейс и переписать его метод int compare(byte[] rec1, byte[] rec2),
который возвращает следующие константы:
− RecordComparator.PRECEDES – если запись rec1 предшествует записи rec2;
− RecordComparator.FOLLOWS – если запись rec1 следует за записью
rec2;
− RecordComparator.EQUIVALENT – если записи с точки зрения упорядочивания эквивалентны. Объекты классов фильтра и компаратора передаются в качестве параметров в метод
enumerateRecords(RecordFilter, RecordComparator, boolean keepUpdated).
Третий параметр этого метода автоматически перестраивает список
записей хранилища после добавления или удаления записей, если
его значение равно true. В противном случае (keepUpdated=false),
для этой цели необходимо вызывать метод rebuild().
П р и м е р. Разработать базу данных для магазина «Овощи –
фрукты».
1. Определим структуру записи для хранилища (рис. 3.2).
2. Определим перечень действий для работы с базой данных
и с каждым действием свяжем соответствующую команду меню
(рис. 3.3):
− добавление новой записи в хранилище («Add»);
− удаление записи из хранилища («Delete»);
29
¦¹À»¹ÆÁ¾
ÈÉǽÌÃ˹
¯¾Æ¹À¹Ã¼
›Á½ÈÉǽÌÃ˹
Ç»ÇÒÁÄÁÍÉÌÃË
¨Çľ
¨Çľ
¨Çľ
Рис. 3.2. Структура записи
Рис. 3.3. Команды меню
− получение информации о каждой записи («Info»);
− вывод
информации
обо
всех
записях
хранилища
(«Fruit+veget»);
− вывод информации только о записях с овощами
(«Vegetables»);
− вывод информации только о записях с фруктами («Fruit»).
Естественно, в меню также будет команда завершения работы
приложения «Exit». Создание новых команд выделим в отдельный
метод createCommands() (листинг 3.1).
Листинг 3.1
private void createCommands()
{
info = new Command( “Info”, Command.SCREEN, 1 );
add = new Command( “Add”, Command.SCREEN, 1 );
delete = new Command( “Delete”, Command.SCREEN, 1 );
fruit = new Command( “Fruit”, Command.SCREEN, 1 );
veget = new Command( “Vegetables”, Command.SCREEN, 1 );
fr_veg = new Command( “Fruit+Veget”, Command.SCREEN, 1 );
OK = new Command( “OK”, Command.OK, 1 );
back = new Command( “Back”, Command.BACK, 1 );
exit = new Command( “Exit”, Command.EXIT, 0 );
}
3. Определим тип переменной для каждого поля записи:
− поле 1 – String;
30
− поле 2 – int;
− поле 3 – boolean (так как переменная может принимать только
два значения).
4. Определим форму для заполнения полей новой записи
(рис. 3.4). Поля «Название» и «Цена» – объекты класса TextField,
а «Вид» – объект класса ChoiceGroup. Добавляем две команды:
«Back» – возврат к предыдущему экрану, «OK» – подтверждение
ввода данных. За создание формы будет отвечать метод createForm()
(листинг 3.2).
Листинг 3.2
private void createForm()
{
String[] stringType = { “Овощи», “Фрукты» };
productForm = new Form(“Products”);
productName = new TextField(“Название :”,””,15, TextField.ANY);
productForm.append( productName );
productPrice = new TextField(“Цена :”,””,15, TextField.NUMERIC);
productForm.append( productPrice );
productType = new ChoiceGroup(“Вид :”,ChoiceGroup.EXCLUSIVE,
stringType, null);
productForm.append( productType );
productForm.addCommand(OK);
productForm.addCommand(back);
productForm.setCommandListener(this);
}
Рис. 3.4. Форма
для заполнения полей новой записи
31
5. Определим вид первоначального экрана приложения. Если
в базе данных нет никаких записей (рис. 3.5, а), то пользователю
доступна только команда «Add». Добавим в базу 4 записи. Тогда
на экране отображаются названия продуктов (поле 1) всех записей
хранилища (рис. 3.5, б).
Добавим в базу четыре записи с информацией об апельсинах,
грушах, огурцах и томатах.
Для этого создается список «FoodMarket» типа IMPLICIT. Создание списка реализуется в методе createProductsNameList() (листинг
3.3). Сначала создается пустой список, к которому добавляются команды и обработчик команд. Далее получаем информацию о числе
записей в хранилище. Создаем нумератор записей, в который передаем объекты filter и comparator. Классы этих объектов реализованы в отдельных файлах и будут приведены ниже в листингах 3.6 и
3.7 соответственно. Фильтр будет позволять выводить информацию
или только об овощах, или только о фруктах. Компаратор будет выполнять сортировку записей о продуктах в хранилище в алфавитном порядке. С каждым элементом списка связываем идентификатор записи и запоминаем его в массиве recIndexes. Это необходимо
для удаления конкретной записи из хранилища (команда «Delete»)
или получения информации о записи хранилища (команда «Info»).
Далее в цикле читаются записи и их первые поля добавляются в
список.
а)
б)
Рис. 3.5. Первоначальный экран приложения
32
Листинг 3.3
private void createProductsNameList()
{
nameList = new List( «FoodMarket», List.IMPLICIT );
nameList.setCommandListener(this);
nameList.addCommand( add );
nameList.addCommand( exit );
try
{
int size = rs.getNumRecords();
if( size > 0 )
{
nameList.addCommand( delete );
nameList.addCommand( info );
nameList.addCommand( fruit );
nameList.addCommand( veget );
nameList.addCommand( fr _ veg );
}
recIndexes = new int[size];
ProductsSort comparator = new ProductsSort();
ProductsFilter filter = new ProductsFilter( mType );
RecordEnumeration re = rs.enumerateRecords( filter, comparator,
false );
int i = 0;
while( re.hasNextElement() )
{
int id = re.nextRecordId();
recIndexes[i++] = id;
byte[] record = rs.getRecord( id );
ByteArrayInputStream bais = new ByteArrayInputStream( record );
DataInputStream dis = new DataInputStream( bais );
nameList.append( dis.readUTF(), null );
}
} catch(RecordStoreException rse) {}
catch(IOException ioe) {}
}
6. Создаем основной класс мидлета FoodMarket, добавляем необходимые пакеты, объявляем переменные класса. Переменная mType
определяет вид выводимой информации. По умолчанию, в конструкторе мидлета устанавливаем значение переменной, равное
2 (выводится информация обо всех продуктах). Если mType = 0, то
выводится информация о фруктах, а если mType = 1, то выводится
информация об овощах. В конструкторе класса FoodMarket открываем хранилище с именем «FoodMarket», вызываем методы для создания необходимых команд, формы заполнения параметров и списка продуктов. Список продуктов отображаем на экране. Основные
фрагменты файла FoodMarket.java представлены в листинге 3.4.
33
Листинг 3.4
import
import
import
import
javax.microedition.midlet.*;
javax.microedition.lcdui.*;
javax.microedition.rms.*;
java.io.*;
public class FoodMarket extends MIDlet implements CommandListener
{
private
Display d;
private
RecordStore rs;
private
List nameList;
private
int recIndexes[];
private
Command add, info, delete, OK, back, exit;
private
Command fruit, veget, fr_veg;
private
TextField productName, productPrice;
private
ChoiceGroup productType;
private
Form productForm;
private
int mType;
public FoodMarket()
{
super();
mType = 2;
try
{
rs = RecordStore.openRecordStore(«FoodMarket», true);
} catch(RecordStoreException rse) {}
d = Display.getDisplay( this );
createCommands();
createForm();
createProductsNameList();
d.setCurrent(nameList);
}
private void createCommands() {} // текст приведен выше
private void createForm() {} // текст приведен выше
private void createProductsNameList() {} // текст приведен выше
}
public void startApp() { }
public void pauseApp() { }
public void destroyApp(boolean destroy) { }
public void commandAction( Command c, Displayable display )
// текст приведен ниже
7. Реализуем метод commandAction для обработки команд меню
(листинг 3.5).
34
Листинг 3.5
public void commandAction( Command c, Displayable display )
{
if( c == exit )
{
try
{
rs.closeRecordStore();
}
catch(RecordStoreException rse) {}
destroyApp( false );
notifyDestroyed();
}
if( c == fruit )
{
mType = 0;
createProductsNameList();
d.setCurrent( nameList );
}
if( c == veget )
{
mType = 1;
createProductsNameList();
d.setCurrent( nameList );
}
if( c == fr_veg )
{
mType = 2;
createProductsNameList();
d.setCurrent( nameList );
}
if( c == add )
{
createForm();
d.setCurrent( productForm );
}
if( c == delete )
{
try
{
int id = recIndexes[nameList.getSelectedIndex()];
rs.deleteRecord(id);
}
catch(RecordStoreException rse) {}
createProductsNameList();
d.setCurrent( nameList );
}
if( c == info )
35
{
try
{
int id = recIndexes[nameList.getSelectedIndex()];
boolean type;
byte[] record = rs.getRecord( id );
ByteArrayInputStream bais = new ByteArrayInputStream( record );
DataInputStream dis = new DataInputStream( bais );
Form f = new Form( ““ );
f.append( dis.readUTF()+”\n” );
f.append( dis.readInt()+”\n” );
type = dis.readBoolean();
if( type == false )
f.append( “овощи» +”\n” );
else
f.append( “фрукты» +”\n” );
f.addCommand( back );
f.setCommandListener(this);
d.setCurrent( f );
}
catch(RecordStoreException rse) {}
catch(IOException ioe) {}
}
if( c == OK )
{
if(( productName.size()!= 0 ) && ( productPrice.size()!=0 ))
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream( baos );
try
{
dos.writeUTF( productName.getString() );
dos.writeInt( Integer.parseInt(productPrice.getString()) );
int iList = productType.getSelectedIndex();
boolean bType;
if( iList == 0 )
bType = false; // овощи
else
bType = true; // фрукты
dos.writeBoolean( bType );
rs.addRecord( baos.toByteArray(), 0, baos.size() );
}
catch(IOException ioe) {}
catch(RecordStoreException rse) {}
createProductsNameList();
d.setCurrent( nameList );
}
else
{
36
Alert al = new Alert(«Предупреждение!»,
«Не заполнены поля ввода», null,
AlertType.WARNING);
al.setTimeout( 2000 );
d.setCurrent( al );
}
}
}
if( c == back )
{
d.setCurrent( nameList );
}
}
По команде «exit» закрывается хранилище и завершается приложение.
Рассмотрим подробно действия по каждой команде списка продуктов.
По командам «fruit», «veget», «fr_veg» устанавливается соответствующее значение переменной m_Type (0, 1 или 2) и выбирается перечень продуктов для отображения на экране (рис. 3.6, а–3.6, в).
По команде «add» на экране отображается форма для ввода информации о новом продукте. Заполним информацию о новом продукте – свекле (рис. 3.7). При выполнении команды «OK» выполняется возврат к первоначальному экрану, название свекла в алфавитном порядке добавляется к списку продуктов (рис. 3.8). Если заполнены не все поля новой записи и выполняется команда «OK», то
а)
б)
в)
Рис. 3.6. Действия по командам «fruit», «veget» и «fr_veg»:
а – вывод информации о фруктах; б – вывод информации об овощах;
в – вывод информации обо всех продуктах
37
Рис. 3.7. Действие по команде «add»
Рис. 3.8. Список после добавления
продукта «свекла»
на экран на короткое время выводится предупреждение (рис. 3.9) и
выполняется возврат к форме ввода.
По команде «delete» по выбранному элементу списка получаем
индекс записи в хранилище, и далее запись удаляется из хранилища. Например, удаляем из хранилища запись о свекле (рис. 3.10).
Рис. 3.9. Предупреждение о том,
что не все поля формы заполнены
38
Содержимое экрана после удаления записи представлено на
рис. 3.6, в.
По команде «info» по выбранному элементу списка получаем
индекс записи в хранилище, создаем форму, информацию о записи
добавляем на форму, а форму выводим на экран. Например, выведем на экран информацию о томатах (рис. 3.11).
Рис. 3.10. Удаление
из хранилища записи о свекле
Рис. 3.11. Вывод информации о томатах
39
Листинг 3.6
Файл ProductsFilter.java:
import javax.microedition.rms.*;
import java.io.*;
public class ProductsFilter implements RecordFilter
{
int mType;
public ProductsFilter( int type )
{
mType = type;
}
);
}
public boolean matches( byte[] candidate )
{
ByteArrayInputStream bais = new ByteArrayInputStream( candidate
DataInputStream
dis = new DataInputStream( bais );
boolean type = false;
try
{
dis.readUTF();
dis.readInt();
type = dis.readBoolean();
}
catch( IOException ioe ) {}
if( mType == 2 ) return true;
if( ( mType == 1 ) && ( type == false ) ) return true;
if( ( mType == 0 ) && ( type == true ) ) return true;
return false;
}
Листинг 3.7
Файл ProductsSort.java:
import javax.microedition.rms.*;
import java.io.*;
public class ProductsSort implements RecordComparator
{
public int compare( byte[] rec1, byte[] rec2 )
{
ByteArrayInputStream bais1 = new ByteArrayInputStream( rec1
);
ByteArrayInputStream bais2 = new ByteArrayInputStream( rec2
);
DataInputStream dis1 = new DataInputStream( bais1 );
DataInputStream dis2 = new DataInputStream( bais2 );
40
}
}
String name1 = null;
String name2 = null;
try
{
name1 = dis1.readUTF();
name2 = dis2.readUTF();
}
catch( IOException ioe ) {}
int res = name1.compareTo( name2 );
if( res < 0 ) return RecordComparator.PRECEDES;
else if( res == 0 ) return RecordComparator.EQUIVALENT;
else
return RecordComparator.FOLLOWS;
3.2. Задание к лабораторной работе
1. Получить у преподавателя вариант задания.
2. Составить мидлет базы данных для выполнения соответствующих действий.
3. Проверить работу разработанного мидлета на эмуляторе и мобильном телефоне.
3.3. Содержание отчета
1. Титульный лист.
2. Цель работы.
3. Задание.
4. Описание структуры записи и команд работы с базой данных.
5. Описание фильтра и компаратора.
6. Листинг с кодом мидлета.
7. Экранные формы с результатами работы мидлета.
8. Выводы по лабораторной работе.
41
Лабораторная работа № 4
Разработка игрового приложения
Цель работы: изучение классов игрового интерфейса MIDP 2.0,
работа с пакетом javax.microedition.gamecanvas, разработка игрового мидлета.
4.1. Методические указания
Классы игрового интерфейса [5, 6] размещены в пакете javax.
microedition.gamecanvas. Это пакет содержит 5 классов для создания
мобильных игр:
− GameCanvas – абстрактный класс, содержащий основные элементы игрового интерфейса;
− Layer – абстрактный класс, отвечающий за слои, представляемые в игре;
− LayerManager – менеджер слоев;
− Sprite – класс для создания спрайтового изображения;
− TiledLayer – класс для создания фонового изображения.
Класс GameCanvas
GameCanvas – абстрактный класс. Создать объект этого класса
нельзя, но можно создать объект класса, производного от класса GameCanvas. Созданный объект можно рассматривать как игровой холст, на котором будут выполняться все игровые действия.
При этом в производном классе для правильной компиляции обязательно надо вызвать конструктор класса GameCanvas: protected
GameCanvas(boolean suppressKeyEvents), где параметр suppressKeyEvents
определяет, будет ли GameCanvas получать события при нажатии
обычных клавиш. Если этот параметр равен true, то GameCanvas не
будет получать события при нажатии цифровых клавиш клавиатуры (KEY_NUM0, …, KEY_NUM9, KEY_STAR, KEY_POUND).
В отличие от класса Canvas класс GameCanvas предоставляет пользователю возможность использовать двойную буферизацию и высокоэффективную обработку пользовательского ввода.
Двойная буферизация в классе GameCanvas
В классе Canvas рисование выполняется непосредственно на
экране мобильного устройства (рис. 4.1). При рисовании большого
42
числа движущихся объектов анимация выполняется следующим
образом. При каждом перемещении экран сначала очищается, а
затем перерисовываются объекты согласно вычисленным координатам. Как следствие, возникает эффект прерывистой анимации и
мерцания экрана.
Для решения проблемы в классе GameCanvas используется методика двойной буферизации. При двойной буферизации выполняется стирание и рисование на невидимом для пользователя экране
ªË¾É¾ËÕ
¨¾É¾ÉÁÊÇ»¹ËÕ
¶ÃɹÆ
Рис. 4.1. Рисование в классе Canvas
ªË¾É¾ËÕ
¶ÃɹÆ
¨¾É¾ÉÁÊÇ»¹ËÕ
šÌ;É
Рис. 4.2. Рисование в классе GameCanvas
43
(буфере в памяти). По окончании рисования содержание буфера
отображается на реальном экране (рис. 4.2). Для этого используется метод flushGraphics(). Это позволяет достичь плавной анимации.
Для получения буферного экрана используется метод getGraphics(),
который вызывает объект класса Graphics для рисования.
Обработка пользовательского ввода
Пользовательский ввод – это средство взаимодействия пользователя с мобильной игрой. Класс GameCanvas реализует высокоэффективную обработку ввода, предназначенную для мобильных
устройств. Для этого используется метод getKeyStates(). Метод возвращает целочисленное значение, которое можно использовать для
проверки нажатой клавиши. Чтобы проверить нажатие клавиши,
необходимо вызвать метод getKeyStates() и сравнить возвращенное
целочисленное значение с одной из констант класса GameCanvas (см.
табл. 4.1). Клавиши A, B, C и D – дополнительные клавиши, которые могут отсутствовать на мобильном телефоне. Поэтому эти клавиши можно использовать, если создается игра для особой модели
телефона. Пример использования метода представлен в листинге
4.1. При нажатии игровой клавиши влево на консоль выводится
соответствующее сообщение.
Таблица 4.1
Константы класса GameCanvas
Константа
UP_PRESSED
DOWN_PRESSED
LEFT_PRESSED
RIGHT_PRESSED
FIRE_PRESSED
GAME_A_PRESSED
GAME_B_PRESSED
GAME_C_PRESSED
GAME_D_PRESSED
Описание
Игровая клавиша вверх
Игровая клавиша вниз
Игровая клавиша влево
Игровая клавиша вправо
Игровая клавиша выстрела
Дополнительная клавиша A
Дополнительная клавиша A
Дополнительная клавиша B
Дополнительная клавиша C
Листинг 4.1
int pattern = getKeyStates();
if( (pattern & GameCanvas.LEFT_PRESSED) != 0)
{
System.out.println(“LEFT_PRESSED”);
}
44
Отметим преимущества использования метода getKeyStates().
1. События, говорящие о нажатии клавиш, помещаются в специальную очередь, что позволяет отрабатывать многочисленные
нажатия клавиш. Например, если пользователь нажимает клавиши быстрее, чем срабатывает игровой цикл, то все равно ни
одно нажатие клавиши не будет потеряно. Таким образом, метод
getKeyStates() просто возвращает значение из очереди, которое не
обязательно является текущим состоянием клавиши.
2. Метод getKeyStates() не возвращает никакой информации до
тех пор пока, игровой холст невидим. Если игра поддерживает несколько экранов, то клавиши для игрового холста будут активны
только тогда, когда холст будет выбран как текущий экран.
Игровой цикл
Класс GameCanvas создает основной цикл игрового процесса в
одном потоке. Весь игровой процесс сосредоточен в одном цикле
метода run() интерфейса Runnable (листинг 4.2).
Листинг 4.2
public void run()
{
Graphics g = getGraphics();
while(true)
{
inputKey(); // метод, обрабатывающий нажатия клавиш телефона
init(g); // метод, прорисовывающий графику
flushGraphics(); // двойная буферизация
}
}
Класс Layer
Для работы со слоями предназначен класс Layer. Слой – это базовый графический элемент игрового интерфейса. Он представляет
собой растровое изображение, которое можно отобразить на экране, сделать видимым или невидимым и перемещать по экрану.
Каждый слой имеет уникальный индекс, который определяет,
насколько далеко он расположен от пользователя. Слой, имеющий
меньший индекс, будет перекрывать слой, имеющий больший индекс. Предположим, у нас есть эллипс белого цвета, принадлежащий слою с индексом 0 и прямоугольник черного цвета, принадлежащий слою с индексом 1. Если объекты не перекрываются, то
проблем не возникает (рис. 4.3). Если объекты перекрываются, то
45
белый эллипс находится на переднем плане (рис. 4.4), так как слой
имеет меньший индекс. Чтобы черный прямоугольник перекрывал
белый эллипс, необходимо поменять индексы слоев (рис. 4.5). Это
можно сделать с использованием метода класса LayerManager, который будет рассмотрен ниже.
Рис. 4.3. Объекты не перекрываются
Рис. 4.4. Объекты перекрываются
(белый эллипс на переднем плане)
Рис. 4.5. Объекты перекрываются
(черный прямоугольник на переднем плане)
Абстрактный класс Layer имеет следующие методы:
− int getX() – возвращает координату X верхнего левого угла
слоя;
− int getY() – возвращает координату Y верхнего левого угла
слоя;
− int getWidth() – возвращает значение ширины слоя;
− int getHeight() – возвращает значение высоты слоя;
− void setPosition(int x, int y) – устанавливает координаты
левого верхнего угла слоя;
− void move(int dx, int dy) – перемещает слой на координаты dx
и dy;
− boolean isVisible() – получает видимость слоя;
− void setVisible(boolean visible) – устанавливает видимость
слоя;
− abstract void paint(Graphics g) – рисует слой.
46
9
:
TFU1PTJUJPO
NPWF Рис. 4.6. Пример использования методов setPosition и move
Начальное положение слоя – (0,0) задается в системе координат
объекта Graphics, передаваемого в метод paint(), и соответствует левому верхнему углу экрана. Метод setPosition устанавливает позицию слоя по отношению к левому верхнему углу экрана. Например,
вызов метода setPosition(20,20) устанавливает новые координаты
левого верхнего угла слоя – (20,20). Метод move описывает размещение слоя относительно текущей позиции. Если мы хотим переместить слой в точку с координатами (80,60), мы должны вызвать
метод move(60,40)(рис. 4.6).
Слой может быть сделан невидимым на экране, если вызвать метод setVisible c параметром false. Тогда метод paint не отображает
слой на экране. Для отображения слоя на экране надо использовать
параметр true в методе setVisible.
Класс Layer имеет два производных класса: Sprite и TiledLayer.
Класс Sprite
Класс Sprite отвечает за рисование анимированных объектов
приложения. Монстр-призрак в игре Pac-Man, ядра в игре Asteroid,
неопознанные летающие объекты в игре UFO, лягушка в игре
Frogger – все это примеры спрайтов.
47
Рис. 4.7. Размещение кадров спрайта горизонтально
Рис. 4.8. Размещение кадров спрайта
прямоугольным блоком
Спрайт – слой, состоящий из некоторого количества кадров.
Если спрайт состоит из одного кадра, то это графический объект, не
использующий анимацию. Анимированный спрайт состоит из нескольких кадров. Спрайты могут перемещаться по экрану, меняя
свой внешний вид (анимированные объекты в играх).
Кадры спрайта являются частями одного изображения. Каждый кадр имеет свой индекс. Нумерация индексов начинается с
нуля. Внутри изображения кадры спрайта могут быть расположены горизонтально в ряд – индексы кадров возрастают слева направо (рис. 4.7), вертикально в ряд – индексы возрастают сверху
вниз и прямоугольным блоком (рис. 4.8). Блок кадров обязательно
должен быть полным, т. е. нельзя 3 кадра расположить блоком 2 на
2, так как одна ячейка блока останется незаполненной. Кадр может
быть любого размера по ширине и высоте, но все кадры должны
быть одинакового размера.
Создание неанимированного спрайта
Для создания простого спрайта используется конструктор public
Sprite(Image image), где image – растровое изображение спрайта.
Спрайт размещается в левом верхнем углу экрана – в точке с координатами (0,0). Так как спрайт – это слой, то его можно отображать
на экране, скрывать, перемещать, вращать. Кроме того, можно
определять столкновение спрайтов.
48
Трансформация спрайтов
Кадры спрайта можно поворачивать на 90, 180 и 270 градусов.
Также возможно зеркальное отображение кадра по горизонтали и
вертикали. Пусть у нас есть однокадровый спрайт с изображением оленя (рис. 4.9). Все операции трансформации осуществляются
относительно базового пикселя. Базовым будем называть пиксель,
относительно которого происходит вращение изображения.
Если не определить координаты базового пикселя, то по умолчанию он будет иметь координаты (0,0) и совпадать с левым верхним
углом изображения (рис. 4.10).
Метод defineReferencePixel(int x, int y) используется для определения базового символа. Координаты базового пикселя определяются относительно левого верхнего угла изображения. Для примера на рис. 4.11 базовым является пиксель с координатами (20,20).
Эти координаты и надо передать в метод в качестве параметров.
Метод setRefPixelPosition(int x, int y) определяет координаты x и y базового пикселя в общей системе координат, фактически
определяя местоположение самого спрайта. На рис. 4.11 представлен результат использования метода setRefPixelPosition(40,40), который устанавливает координаты (40,40) базового пикселя в общей
системе координат.
Рис. 4.9. Однокадровый спрайт
– Базовый пиксель
Рис. 4.10. Базовый пиксель
в левом верхнем углу изображения
49
0
20
40
X
20
40
Y
– Базовый пиксель
Рис. 4.11. Базовый пиксель
с координатами (20,20)
Методы getRefPixelX() и getRefPixelY() возвращают координаты
X и Y базового пикселя в общей системе координат соответственно.
Положение спрайта в общей системе координат можно задать,
используя метод setPosition класса Layer, который определяет положение левого верхнего угла спрайта. Для примера на рис. 4.11
координаты левого верхнего угла спрайта – (20,20).
Метод setTransform(int transform) выполняет трансформацию
спрайта. Он имеет единственный параметр – тип трансформации,
который задается константами класса Sprite. В табл. 4.2 приведены возможные значения параметров типа трансформации и описание трансформации спрайта. При вращении изображения с оленем
(см. рис. 4.9) предполагалось, что базовый пиксель расположен по
центру изображения.
Для примера создадим мобильное приложение, выполняющее
зеркальное отражение изображения относительно вертикальной
оси. В качестве изображения выберем изображения оленя из файла
«deer.png» (см. рис. 4.9). Файл должен храниться в каталоге resпроекта.
Текст мидлета с комментариями приведен в листинге 4.3.
В основном классе мидлета SpriteMIDlet создаем игровой холст
spr класса SpriteGameCanvas, запускаем поток, добавляем команду
выхода из приложения и блок прослушивания событий, в методе
destroyApp останавливаем поток.
В классе SpriteGameCanvas создаем однокадровый спрайт, загружая изображение из файла. Используя метод setPosition, размещаем изображение по центру экрана.
50
Таблица 4.2
Трансформация спрайта
Параметр
Описание
TRANS_NONE
Изображение не подвергается
трансформации
TRANS_ROT90
Поворот изображения
на 90 градусов
TRANS_ROT180
Поворот изображения
на 180 градусов
TRANS_ROT270
Поворот изображения
на 270 градусов
TRANS_MIRROR
Зеркальное отражение
изображения относительно
вертикальной оси
TRANS_MIRROR_
ROT90
Поворот зеркального отражения
изображения на 90 градусов
TRANS_MIRROR_
ROT180
Поворот зеркального отражения
изображения на 180 градусов
TRANS_MIRROR_
ROT270
Поворот зеркального отражения
изображения на 270 градусов
Изображение
51
Метод run() реализует игровой цикл. Внутри игрового цикла
выполняется цикл while до тех пор, пока не остановлен поток. Рабочему состоянию потока соответствует значение true логической
переменной working. Внутри цикла while проверяется значение логической переменной mirror. Если значение этой переменной равно
false, то выполняется зеркальное отражение спрайта и переменной
mirror присваивается значение true, в противном случае трансформация не выполняется и в переменную mirror записывается false.
Далее в цикле вызывается метод init для рисования спрайта.
В методе init содержимое буфера очищается, вызывается метод
paint класса Sprite, который рисует спрайт в буфере, и содержимое
буфера отображается на экране (метод flushGraphics). В конце цикла while останавливаем игровой цикл на полсекунды. В результате
создается анимация, и мы видим в секунду два разных изображения: одно – без трансформации, а другое – зеркально отраженное
(результат работы приложения см. на рис. 4.12). Заметим, что
трансформация спрайта выполняется относительно верхнего левого угла изображения, и поэтому мы видим на экране только часть
зеркального отраженного изображения. Чтобы увидеть это изображение полностью установим базовый пиксель по центру изображения. Для этого снимем комментарий со строки
// deer.defineReferencePixel(deer.getWidth()/2,deer.getHeight()/2); в
конструкторе класса SpriteGameCanvas (экранные формы с результатом работы мидлета для этого случая см. на рис. 4.13).
Рис. 4.12. Результат трансформации
(базовый пиксель в левом верхнем углу экрана)
52
Рис. 4.13. Результат трансформации
(базовый пиксель по центру экрана)
Листинг 4.3
/* класс SpriteMIDlet */
import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;
public class SpriteMIDlet extends MIDlet implements CommandListener
{
// команда выхода из приложения
private Command exit = new Command(“Exit”, Command.EXIT, 0);
// объект класса SpriteGameCanvas
private SpriteGameCanvas spr;
public SpriteMIDlet()
{
try{ // обрабатываем исключительную ситуацию
// инициализируем объект класса SpriteGameCanvas
spr = new SpriteGameCanvas();
// запускаем поток
spr.start();// запускаем поток
spr.addCommand(exit); // добавляем команду выхода
spr.setCommandListener(this);
// отражаем текущий дисплей
Display.getDisplay(this).setCurrent(spr);
} catch (java.io.IOException ioe) {};
}
public void startApp() {}
public void pauseApp() {}
public void destroyApp(boolean unconditional)
53
{
}
}
if(spr != null) spr.stop();// останавливаем поток
public void commandAction(Command c, Displayable d)
{
if ( c == exit )
{
destroyApp(false);
notifyDestroyed();
}
}
/* класс SpriteGameCanvas */
import java.io.IOException; // подключаем исключение IOException
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
public class SpriteGameCanvas extends GameCanvas implements Runnable
{
boolean working; // логическая переменная для выхода из цикла
boolean mirror; // логическая переменная для зеркального отражения
// создаем объект класса Sprite
private Sprite deer;
public SpriteGameCanvas() throws IOException
{
// вызываем конструктор суперкласса GameCanvas
super(true);
mirror = true;
// загружаем изображение оленя
Image deerImage = Image.createImage(“/deer.png”);
deer = new Sprite(deerImage); // инициализируем объект
deer.setPosition((getWidth() – deer.getWidth())/2,
(getHeight() – deer.getHeight())/2);
// deer.defineReferencePixel(deer.getWidth()/2,deer.getHeight()/2);
}
public void start()
{
working = true;
Thread t = new Thread(this); // создаем и запускаем поток
t.start();
}
public void stop() { working = false; }
public void run()
{
Graphics g = getGraphics();// получаем графический контекст
while(working)
{
if(mirror == false)
54
{
}
}
}
deer.setTransform(Sprite.TRANS _ MIRROR); mirror = true;
}
else
{
deer.setTransform(Sprite.TRANS _ NONE); mirror = false;
}
init(g); // рисуем спрайт
try {Thread.sleep(300);} //останавливаем цикл на 300 мс
catch (java.lang.InterruptedException zxz) {};
private void init(Graphics g)
{
g.setColor(0xFFFFFF); // белый цвет фона для перерисовки экрана
g.fillRect(0, 0, getWidth(), getHeight());// залить экран
deer.paint(g);
// рисуем спрайт
flushGraphics(); // двойная буферизация
}
Определение столкновений
Детектирование столкновений используется для определения
физического взаимодействия объектов друг с другом в спрайтовой
анимации. Чтобы определить столкновение двух спрайтов, необходимо знать их местоположения на экране и проверить, совпадают
ли они. При большом числе движущихся спрайтов задача становится сложной.
Зададим два объекта размером 6 × 6 пикселей (рис. 4.14) и покажем возможные методы определения столкновений.
Определение столкновений
методом ограничивающих прямоугольников
Метод не требует больших вычислительных затрат и сводится
к простой проверке перекрытия спрайтов. Однако если спрайты
имеют непрямоугольную форму, то их столкновения будут опреде-
Рис. 4.14. Два спрайта
55
ляться с погрешностями. Например, на рис. 4.15 показан вариант,
когда спрайты не пересекаются, но, тем не менее, их столкновение
детектируется. На рисунке перекрывающиеся области заштрихованы.
Чтобы уменьшить ошибку, можно уменьшить размеры ограничивающих прямоугольников спрайтов. Для этого используется
метод void defineCollisionRectangle(int x, int y, int width, int
height), где x и y – координаты левого верхнего угла нового прямоугольника, width и height – ширина и высота прямоугольника.
Например, строка кода defineCollisionRectangle(1, 1, 4, 4), выполненная для каждого спрайта, определяет новые прямоугольни-
Рис. 4.15. Определение столкновения
на уровне ограничивающих прямоугольников
(спрайты не пересекаются, но столкновение обнаруживается)
Рис. 4.16. Определение столкновения
методом уменьшенных ограничивающих прямоугольников
(спрайты не пересекаются, столкновение не обнаруживается)
56
ки определения столкновений шириной и высотой 4 пикселя. На
рис. 4.16 рассмотрен предыдущий вариант столкновения спрайтов,
но с использованием уменьшенных ограничивающих прямоугольников. Теперь столкновение спрайтов не обнаруживается. С другой
стороны, метод уменьшенных ограниченных прямоугольников может вызвать другую ошибку, не определяя истинного столкновения спрайтов.
Определение столкновений на уровне пикселей
В данном методе результат столкновения будет положительным
только в том случае, если пересекаются пиксели, имеющие цвет,
отличный от белого. Этот метод позволяет определять столкновения спрайтов произвольной формы без ошибок. В вариантах столкновения, приведенных на рис. 4.15 и 4.16, столкновения не будут
обнаружены. Недостатком метода является большой объем вычислений по сравнению с методами определения столкновений при помощи ограничивающих прямоугольников. Это может существенно
понизить скорость работы мобильного приложения.
Существуют три метода определения столкновений в классе
Sprite:
− boolean collidesWith(Sprite s, boolean pixelLevel) – определяет
столкновение между спрайтами;
− boolean collidesWith(TiledLayer t, boolean pixelLevel) – определяет столкновение между спрайтом и препятствием, нарисованным с использованием класса TiledLayer (класс TiledLayer будет
рассмотрен позднее);
− boolean collidesWith(Image im, int x, int y, boolean pixelLevel)
– определяет столкновение спрайта с изображением.
Параметр pixelLevel определяет метод, используемый при обнаружении столкновений. Если pixelLevel равен true, то используется метод на основе пикселей, в противном случае (pixelLevel равен
false) – метод ограничивающих прямоугольников.
Создание анимированных спрайтов
Конструктор Sprite(Image image, int frameWidth, int frameHeight)
создает анимационный спрайт, где frameWidth – ширина кадра, а
frameHeight – высота кадра.
Для рисования анимационного спрайта создается последовательность отображения кадров. Для спрайта с изображением яблока (см. рис. 4.7 или 4.8) создается очередь индексов кадров {0, 1, 2,
57
3}. Для перехода к следующему кадру очереди используется метод
void nextFrame(). Очередь является циклической, т. е. при достижении конца очереди использование метода nextFrame() приведет к
перемещению в начало очереди. Чтобы вернуться к предыдущему
кадру очереди, применяется метод void prevFrame(). Для перехода к
определенному кадру очереди с индексом sequenceIndex существует
метод void setFrame(int sequenceIndex). Метод int getFrame() возвращает индекс текущего элемента очереди.
Пользователь может организовать свою последовательность отображения кадров. Очередь может иметь любую длину, единственное требование – чтобы содержащиеся в ней индексы кадров были
действительными. Для этого надо организовать массив индексов
кадров. Например, int sequence[] = {3,2,1,0}. После этого необходимо вызвать метод void setFrameSequence(int[] sequence), чтобы установить последовательность индексов кадров из массива
sequence. Метод int getFrameSequenceLength() позволяет определить
длину очереди.
Теперь создадим мидлет, иллюстрирующий работу с анимационным спрайтом. В качестве изображения спрайта выберем изображение из файла apples.png (см. рис. 4.7). Файл должен храниться в
каталоге res-проекта.
Текст мидлета с комментариями приведен в листинге 4.4.
Основной класс мидлета SpriteMIDlet, в котором создается игровой холст spr класса SpriteGameCanvas, не отличается от подобного
класса, приведенного в листинге 4.3. Поэтому он здесь не рассматривается.
В классе SpriteGameCanvas создаем анимированный спрайт apple,
загружая изображение из файла. Спрайт состоит из четырех кадров
размером 160 × 160 пикселей. Используя метод setPosition, размещаем кадр по центру экрана.
В методе run() реализуется игровой цикл. Для навигации по кадрам спрайта используется метод nextFrame, который вызывается
два раза в секунду. При этом мы наблюдаем вращение изображения с яблоком по часовой стрелке. Экранные формы с результатами
работы приложения приведены на рис. 4.17. Чтобы изменить направление движения изображения (против часовой стрелки), необходимо вместо метода nextFrame воспользоваться методом prevFrame.
Другой способ поменять направление вращения – создать свою очередь последовательности индексов в обратном порядке. Для этого
надо снять комментарии со строк, помеченных в тексте мидлета
жирным начертанием.
58
Рис. 4.17. Экранные формы с результатами работы мидлета
Отметим также, что подобное вращение изображения можно
получить, используя однокадровый спрайт яблока, с которым необходимо последовательно выполнять трансформацию на 90, 180 и
270 градусов.
Листинг 4.4
/* класс SpriteGameCanvas */
import java.io.IOException; // подключаем исключение IOException
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
public class SpriteGameCanvas extends GameCanvas implements
Runnable
{
boolean working; // логическая переменная для выхода из цикла
// static int[] frSequence = { 0, 3, 2, 1 };
private Sprite apple; // создаем объект класса Sprite
public SpriteGameCanvas() throws IOException
{
super(true); // вызываем конструктор суперкласса GameCanvas
// загружаем изображение яблока
Image appleImage = Image.createImage(“/apples.png”);
apple = new Sprite(appleImage,160,160); // инициализируем объект
apple.setPosition((getWidth() – apple.getWidth())/2,
(getHeight() – apple.getHeight())/2);
//
apple.setFrameSequence( frSequence );
//
apple.setFrame( 0 );
}
public void start()
{
working = true;
59
Thread t = new Thread(this); // создаем и запускаем поток
t.start();
}
public void stop() { working = false; }
public void run()
{
Graphics g = getGraphics(); // получаем графический контекст
while (working)
{
apple.nextFrame(); // переход к следующему кадру
init(g); // рисуем спрайт
try { Thread.sleep(500);} // останавливаем цикл на полсекунды
catch (java.lang.InterruptedException zxz) {};
}
}
private void init(Graphics g)
{
g.setColor(0xFFFFFF);// белый цвет фона для перерисовки экрана
g.fillRect(0, 0, getWidth(), getHeight()); // очистить экран
apple.paint(g); // рисуем спрайт
flushGraphics(); // двойная буферизация
}
}
Класс TiledLayer
Так же как и класс Sprite, класс TiledLayer является производным от класса Layer и наследует все его свойства. Он предназначен
для создания фоновых изображений. Предположим, нам необходимо создать картинку с изображением теннисного стола (рис. 4.18).
Если сохранить это изображение целиком, то оно будет занимать
Рис. 4.18. Изображение
теннисного стола
60
много места в памяти. А ведь на самом деле рассмотренное изображение содержит всего девять различных фрагментов, показанных
на рис. 4.19.
Для решения проблемы экономии места, используются плиточные слои (от англ. tile – плитка, черепица). Плиточный слой представляет собой таблицу (графическую карту), заполненную графическими фрагментами так, что в результате получается целостное
изображение. Графические фрагменты хранятся одним сплошным
изображением. Размеры фрагмента по ширине и высоте могут быть
разными, но все фрагменты должны быть одинаковыми по размеру.
При загрузке изображения необходимо указать высоту и ширину
фрагмента. В результате изображение разбивается на фрагменты.
Каждому фрагменту присваивается уникальный индекс. В отличие
от спрайта первый фрагмент имеет индекс 1. Нумерация фрагментов начинается с 1, так как нулями обозначаются пустые ячейки,
т. е. ячейки, не содержащие никаких фрагментов.
Для создания фрагментов изображения удобно использовать редактор игровых карт Tile Studio. Программу Tile Studio и примеры
работы с ней можно загрузить с сайта http://tilestudio.sourceforge.
net.
Пример заполнения таблицы для приведенного изображения
показан на рис. 4.20. При создании плиточного слоя по умолчанию
все ячейки заполнены нулями.
Объект класса TiledLayer создается при помощи конструктора:
TiledLayer(int columns, int rows, Image image, int tileWidth, int
tileHeight), где:
− columns – число столбцов;
− rows – число строк;
Рис. 4.19. Фрагменты изображения
61
Рис. 4.20. Таблица
фрагментов изображения
− image – исходное изображение;
− tileWidth – ширина фрагмента в пикселях;
− tileHeight – высота фрагмента в пикселях.
В качестве примера разработаем мидлет с изображением теннисного стола. Для создания фонового изображения необходимо:
1. Загрузить изображение, содержащее фрагменты из файларесурса. Изображение (рис. 4.21) хранится в файле с именем
«tt.png» и разбито на фрагменты (см. рис. 4.19).
2. Создать объект класса TiledLayer.
3. Заполнить ячейки таблицы фрагментами (см. рис. 4.20). Для
этого можно воспользоваться методом void setCell(int col, int
row, int tileIndex), который записывает в ячейку таблицы, расположенную в столбце col и строке row, фрагмент изображения с
индексом tileIndex.
4. Установить позицию для отображения фонового изображения
на экране, используя метод setPosition класса Layer.
Текст мидлета приведен в листинге 4.5. Основной класс мидлета не отличается от класса SpriteMIDlet в листинге 4.3, за исклю-
Рис. 4.21. Изображение в файле-ресурсе
62
чением того, что в нем создается объект класса TTGameCanvas, а не
SpriteGameCanvas. Поэтому основной класс мидлета в листинге 4.5
не приведен. Главное отличие класса TTGameCanvas от предыдущих
игровых классов заключается в использовании метода Background().
Данный метод создает объект tl класса TiledLayer и заполняет ячейки таблицы индексами фрагментов изображения, размещенных в
массиве map. В цикле for проводится разметка игрового поля. Номер столбца таблицы вычисляется как остаток от деления индекса
элемента массива на 6. Результат целочисленного деления индекса
элемента массива на 6 определяет нужную строку таблицы. Объект
tl размещается по центру экрана. Экранная форма для мобильного
приложения приведена на рис. 4.22.
Листинг 4.5
/* класс TTGameCanvas */
import java.io.IOException; // // подключаем исключение IOException
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
public class TTGameCanvas extends GameCanvas implements Runnable
{
private TiledLayer tl; // создаем объект класса TiledLayer
boolean working; // логическая переменная для выхода из цикла
public TTGameCanvas() throws IOException
{
super(true); // вызываем конструктор суперкласса Canvas
tl = Background(); // инициализируем tl
}
Рис. 4.22. Фоновое изображение
теннисного стола
63
public void start()
{
working = true;
Thread t = new Thread(this); // создаем и запускаем поток
t.start();
}
/* метод создающий объект класса TiledLayer
и загружающий фоновое изображение */
public TiledLayer Background() throws IOException
{
// загрузка изображения из файла-ресурса
Image im = Image.createImage(“/tt.png”);
// создаем объект класса TiledLayer
tl = new TiledLayer( /*столбцы*/6,/*строки*/10, im,
/*ширина*/32,/*высота*/32 );
int[] map = // разметка игрового поля
{ 6, 3, 7, 6, 3, 7,
4, 1, 5, 4, 1, 5,
4, 1, 5, 4, 1, 5,
4, 1, 5, 4, 1, 5,
9, 2, 8, 9, 2, 8,
6, 3, 7, 6, 3, 7,
4, 1, 5, 4, 1, 5,
4, 1, 5, 4, 1, 5,
4, 1, 5, 4, 1, 5,
9, 2, 8, 9, 2, 8};
// цикл считывающий разметку поля
for(int i = 0; i < map.length; i++)
{ /* присваиваем каждому элементу игрового поля
определенную ячейку изображения im*/
tl.setCell( i%6, i/6, map[i] );
}
tl.setPosition( ( getWidth() – tl.getWidth() ) / 2,
( getHeight() – tl.getHeight() ) / 2 );
}
return tl;
public void stop() { working = false; }
public void run()
{
Graphics g = getGraphics(); // получаем графический контекст
while (working)
{
init(g); // рисуем графические элементы
try { Thread.sleep(30); }// останавливаем цикл на 30 миллисекунд
catch (java.lang.InterruptedException zxz) {};
}
}
64
}
private void init(Graphics g)
{
g.setColor(0x000000); // черный цвет фона для перерисовки экрана
g.fillRect(0, 0, getWidth(), getHeight());
tl.paint(g); // рисуем фоновый слой
flushGraphics(); // двойная буферизация
}
Перечислим также другие методы класса:
− int getCellHeight() – возвращает высоту фрагмента изображения в пикселях;
− int getCellWidth() – возвращает ширину фрагмента изображения в пикселях;
− int getColumns() – возвращает количество колонок в фоновом
изображении;
− int getRows() – возвращает количество строк в фоновом изображении;
− void fillCells(int col, int row, int numCols, int numRows, int
tileIndex) – заполняет область (numCols-столбцов и numRows-строк таблицы), начиная с ячейки, размещенной в столбце col и строке row,
фрагментом изображения с индексом tileIndex;
− void paint(Graphics g) – рисует изображение фона.
Создание анимированной ячейки
Ячейки могут быть не только статическими, но и динамическими. Например, для создания эффекта анимации добавим к фоновому изображению (см. рис. 4.18) фрагмент 10 (рис. 4.23), который
содержит надпись фирмы производителя инвентаря для настольного тенниса «Andro». Если в затемненных ячейках таблицы (см.
рис. 4.20) поочередно менять кадры с индексами 9 и 10, то будет
создаваться эффект анимации.
Рис. 4.23. Дополнительный
фрагмент
65
Для работы с анимированными ячейками используют следующие методы:
− int createAnimatedTile(int staticTileIndex) – создает анимированную ячейку с индексом фрагмента staticTileIndex, который
устанавливается в качестве начального содержимого ячейки и возвращает отрицательное число – индекс анимированной ячейки;
− int getAnimatedTile(int animatedTileIndex) – возвращает индекс
фрагмента, который в данный момент отображается в анимированной ячейке с индексом animatedTileIndex;
− int setAnimatedTile(int animatedTileIndex, int staticTileIndex) –
помещает фрагмент с индексом staticTileIndex в анимированную
ячейку с индексом animatedTileIndex (меняет содержимое анимированной ячейки).
Для использования анимированных ячеек в предыдущее мобильное приложение необходимо внести следующие изменения:
1. Необходимо создать анимированную ячейку: animIndex =
tl.createAnimatedTile(9). Здесь 9 – индекс фрагмента, который должен быть установлен в качестве начального содержимого ячейки.
Метод возвращает отрицательное число – индекс анимированной
ячейки.
2. Поместить анимированную ячейку в нужные ячейки таблицы:
tl.setCell(0,9,animIndex);
tl.setCell(0,4,animIndex);
tl.setCell(3,9,animIndex);
tl.setCell(3,4,animIndex).
Ячейки будут работать синхронно.
3. Поменять содержимое массива map. В отличие от статических
ячеек, в анимированные ячейки ставятся отрицательные числа, начиная с –1. Поэтому в затемненные ячейки таблицы (см. рис. 4.20)
устанавливаем индекс –1.
4. Для смены содержимого анимированной ячейки использовать
метод tl.setAnimatedTile(animIndex,tileIndex), где второй параметр
может принимать значения 9 или 10.
Первые три пункта изменений необходимо выполнить в методе Background(), в котором создается объект tl. Смену содержимого анимированной ячейки необходимо выполнять в методе run().
Для работы с анимированной ячейкой создаем переменную cnt.
Код внутри цикла while(working) будет выполняться, если остаток
от деления будет равен нулю, т. е. каждый пятнадцатый раз будет
происходить смена содержимого анимированной ячейки. В методе
66
Рис. 4.24. Результат работы мидлета
с анимированными ячейками
tl.setAnimatedTile(animIndex ( cnt % 2 ) + 9 ) переменная cnt используется для выбора одного из двух фрагментов изображения.
Код метода run() приведен в листинге 4.6. Результат работы мидлета с анимированными ячейками приведен на рис. 4.24.
Листинг 4.6
public void run()
{
Graphics g = getGraphics(); // получаем графический контекст
int cnt = 0;
while (working)
{
if( (cnt % 15) == 0 )
tl.setAnimatedTile(animIndex, (cnt % 2) + 9 );
init(g); // рисуем графические элементы
cnt++;
try {Thread.sleep(30);} // останавливаем цикл на 30 миллисекунд
catch (java.lang.InterruptedException zxz) {};
}
}
Класс LayerManager
Итак, мы рассмотрели две разновидности слоев: спрайты и плиточные слои. Теперь надо разобраться, как эти слои помещаются
на экран и взаимодействуют между собой. Для этого используется
67
класс LayerManager (менеджер слоев). После того как создан слой, его
необходимо передать менеджеру слоев. Менеджер слоев отвечает за
рисование слоя, очередность отображения и создание окна просмотра. Для создания менеджера слоев используется конструктор без
параметров: LayerManager(). Для управления слоями используются
следующие методы класса LayerManager:
− void append(Layer l) – добавляет слой l в менеджер слоев;
− void insert(Layer l, int index) – вставляет слой l по индексу
index в менеджер слоев;
− void remove(Layer l) – удалить слой l из менеджера слоев;
− int getSize() – возвращает число управляемых слоев;
− Layer getLayer(int index) – получить слой с индексом index;
− void paint(Craphics g, int x, int y) – вывести все слои в заданных координатах;
− void setViewWindow(int x, int y, int width, int height) – установить окно просмотра.
Пример работы с классом LayerManager приведен в листинге 4.7.
Листинг 4.7
LayerManager lm = new LayerManager();
lm.append(spr1);
lm.append(background);
lm.insert(spr2, 0);
lm.remove(spr1);
lm.setViewWindow(0, 0, getWidth(), getHeight());
lm.paint(g,0,0);
Создаем объект класса менеджера слоев – lm. Добавляем фоновый слой background и спрайт spr1 в менеджер слоев. При этом
спрайт spr1 будет ближе к пользователю, чем фоновый слой (верхние слои добавляются в первую очередь). Далее, вставляем слой
спрайта spr2 на ближнюю к пользователю позицию. Потом удаляем
слой спрайта spr1 из менеджера слоев. Остались два слоя. Позади –
фоновый слой, ближе к пользователю – спрайт слоя spr2. В случае,
если слои не помещаются на экран телефона, необходимо создать
окно просмотра, используя метод setViewWindow. В этом методе координаты определяются в системе координат менеджера слоев.
В примере окно просмотра создается в левом верхнем углу экрана
размером с экран. Если необходимо переместить окно просмотра,
то необходимо снова вызвать метод setViewWindow, изменив первые
два параметра. И последнее, отображаем менеджер слоев на игровой холст GameCanvas.
68
4.2. Пример разработки игры
Теперь приступим к разработке игры. При этом можно использовать весь пройденный до этого материал. В игре можно использовать ARGB-изображения, которые были рассмотрены в лабораторной работе № 1. Пользователь может использовать в игре звуковое
сопровождение (см. лабораторную работу № 2). Если игра предполагает наличие численных результатов, то можно организовать таблицу рекордов в виде базы данных для хранения лучших результатов с использованием RMS-системы (см. лабораторную работу
№ 3). И наконец, необходимо использовать весь материал лабораторной работы № 4.
Попробуем создать игру на базе известной задачи о Ханойских
башнях. Для разработки игры необходимо определить ее сценарий,
сцену и все необходимые персонажи (объекты) игры.
Сценарий. Имеются три колышка A, B, C и n дисков разного размера. Для простоты будем считать, что n равняется трем. Самый
большой диск имеет зеленый цвет, диск среднего размера – синий,
а самый маленький – красный. Сначала все диски надеты на колышек A (левый колышек), как показано на рис. 4.25.
Требуется перенести все диски с колышка A на колышек C (правый колышек), соблюдая при этом следующие условия: диски можно переносить только по одному, больший диск нельзя ставить на
меньший. Для трех дисков задача решается за 7 шагов (рис. 4.26 и
табл. 4.3). Цвета дисков на рис. 4.26 можно определить по их размеру. После седьмого шага игра окончена.
Объекты. Необходимо создать сцену или фоновое изображение
игры (рис. 4.27). Плиточный слой фонового изображения можно
составить из 5 различных фрагментов изображения (рис. 4.28).
Каждый фрагмент будет иметь размер 40 × 40 пикселей. Полный
Рис. 4.25. Начальное состояние игры
69
Рис. 4.26. Решение задачи
70
размер изображения: ширина – 240 пикселей и высота также 240
пикселей. Таблица с индексами фрагментов изображения представлена на рис. 4.29.
Рис. 4.27. Фоновое изображение игры
1
2
3
4
5
Рис. 4.28. Фрагменты изображения
s
s
s
Рис. 4.29. Таблица с индексами
фрагментов изображения
71
Таблица 4.3
Последовательность шагов решения задачи
Номер Перемещаемый
шага
диск
1
2
3
4
5
6
7
Красный
Cиний
Красный
Зелений
Красный
Синий
Красный
Положение диска
до перемещения
Положение диска
после перемещения
Колышек A (верхний)
Колышек A (средний)
Колышек C (нижний)
Колышек A (нижний)
Колышек B (средний)
Колышек B (нижний)
Колышек A (нижний)
Колышек C (нижний)
Колышек B (нижний)
Колышек B (средний)
Колышек C (нижний)
Колышек A (нижний)
Колышек C (средний)
Колышек C (верхний)
Затемненные ячейки таблицы будут анимированными. Им присвоим индексы –1, –2 и –3. Для этого добавим в изображение фрагменты 6, 7 и 8 с названиями колышков. Изображение хранится в
файле bgr5.png (рис. 4.30). Размер изображения – 320 × 40 пикселей. При этом периодически на экране будут появляться названия
колышков – A, B и C.
Персонажи игры. Красный, синий и зеленый диски – три спрайта (рис. 4.31). Изображение каждого спрайта будет состоять из
двух кадров. Первый кадр – просто диск определенного цвета – соответствует неактивному состоянию диска, т. е. на данном шаге
игры диск не принимает участия. Если диск является активным,
то используется второй кадр спрайта – диск, обрамленный белой
рамкой и с наличием белого кружка посередине.
В игре надо пересмотреть перемещение спрайтов. Каждый диск
может перемещаться по своему колышку вверх, далее двигаться
влево или вправо. Опуститься вниз диск может только на определенный колышек.
1
2
3
4
5
6
8
Рис. 4.30. Изображение с дополнительными
фрагментами
0
1
Красный
0
Синий
1
0
1
Зеленый
Рис. 4.31. Спрайты с изображениями дисков
72
Чтобы усложнить игру, а именно добавить в игру
столкновение спрайтов, введем еще один однокадровый
спрайт с изображением в виде стрелки и назовем его
маркером (рис. 4.32). Маркер постоянно движется по Рис. 4.32.
тому колышку, на который на данном шаге игры необ- Спрайт
ходимо перенести диск. На определенное время маркер с изобрауходит вверх экрана, а потом возвращается вниз. За это жением
время необходимо успеть опустить диск на нужный ко- маркера
лышек. При столкновении маркера с диском игра будет
завершена.
Размеры используемых в игре спрайтов приведены в табл. 4.4.
Таблица 4.4
Размеры спрайтов
Спрайт
Красный диск
Cиний диск
Зелений диск
Маркер
Число кадров
Ширина кадра
Высота кадра
2
2
2
1
30
50
70
15
40
40
40
15
Какая игра без музыкального сопровождения? Можно назначить
мелодию для игры в целом, а можно каждому спрайту отдельно.
В нашей игре музыка будет звучать с момента запуска приложения (начала игры) до завершения игры. Игра может завершиться,
когда пользователь успешно пройдет все шаги, или досрочно – при
столкновении спрайтов диска и маркера. В обоих случаях плеер,
воспроизводящий мелодию, остановится. В качестве мелодии будем использовать созданную в лабораторной работе № 2 тоновую
последовательность, записанную в файл cancan.jts. Данный файл
должен быть размещен в каталоге res-проекта.
И наконец, в нижней части игрового экрана будем выводить строку состояния игры. В этой строке будет отражаться информация о
завершении очередного шага игры и высвечиваться прямоугольник, окрашенный в цвет диска, активного на данном шаге игры.
Программирование игры
Мобильное приложение будет состоять из четырех классов.
Каждый класс разместим в отдельном файле. Код основного класса мидлета Hanoi_towers с комментариями представлен в листинге
4.8. В конструкторе основного класса мидлета создается объект ht
игрового холста класса HTCanvas(this). В качестве параметра здесь
73
передается ссылка на основной класс мидлета this. В целом, текст
основного класса мидлета похож на текст мидлета в листинге 4.3.
Отличия между ними связаны с добавлением звука в приложение.
В методе startApp() создается проигрыватель pl, связанный с входным потоком файла тоновой последовательности cancan jts. В метод
setLoopCount() передается значение –1, которое говорит о том, что
мелодия будет воспроизводиться бесконечно. Этот метод вызывается перед методом start(). После этого начинается воспроизведение
мелодии до тех пор, пока не будет вызван метод stop() или закрыт
проигрыватель. В классе присутствует также метод stopPlayer(),
который останавливает воспроизведение и вызывается из класса
HTCanvas в случае окончания игры. При закрытии приложения по
команде exit вызывается метод close(), который закрывает проигрыватель.
Листинг 4.8
/* класс Hanoi_towers */
import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;
import javax.microedition.media.*;
import java.io.*;
public class Hanoi_towers extends MIDlet implements CommandListener
{
// команда выхода из приложения
private Command exit = new Command(“Exit”, Command.EXIT, 0);
private HTCanvas ht; // объект класса HTCanvas
private Player pl;
public Hanoi_towers()
{
try { // обрабатываем исключительную ситуацию
ht = new HTCanvas(this); // инициализируем объект класса HTCanvas
ht.start(); // запускаем поток
ht.addCommand(exit); // добавляем команду выхода
ht.setCommandListener(this); // добавляем блок прослушивания
Display.getDisplay(this).setCurrent( ht ); // вывод на дисплей
} catch( IOException ioe ) {};
}
public void startApp()
{
try {
// создать проигрыватель
pl = Manager.createPlayer
(getClass().getResourceAsStream(“/cancan.jts”),”audio/x-tone-seq”);
pl.realize();
pl.prefetch();
74
pl.setLoopCount(-1); // бесконечное воспроизведение мелодии
pl.start(); // старт воспроизведения
} catch(Exception e) { }
}
}
public void pauseApp() {}
public void destroyApp(boolean unconditional)
{
if( ht != null ) ht.stop(); // останавливаем поток
}
public void commandAction(Command c, Displayable d)
{
if ( c == exit )
{
pl.close();
destroyApp(false);
notifyDestroyed();
}
}
public void stopPlayer() // остановить воспроизведение
{
try {
pl.stop(); }
catch (Exception e) { }
}
Класс HTCanvas – производный от класса GameCanves. Он отвечает
за реализацию всей логики игры. Его код внушителен по размерам и размещен в листинге 4.9. Рассмотрим его подробнее. Сначала
разберемся с переменными. Переменные screenWidth и screenHeight
определяют высоту и ширину экрана. Игра создана для эмулятора
Java ME, размер которого 320 × 240. При создании спрайтов указывались конкретные координаты их положения. Для запуска игры на
различных моделях телефонов необходимо вычислять положение
спрайтов и изображений. Переменная upBound определяет верхнюю
границу фонового изображения и контролирует, чтобы диски при
движении вверх не выходили за границы изображения. Константы
GREEN _ DISK, BLUE _ DISK и RED _ DISK предназначены для определения
активного диска и выбора нужного кадра спрайта для диска. Константа NO _ DISK устанавливается при завершении игры, когда уже
нет активного диска. Константы PEG _ A, PEG _ B и PEG _ C определяют
координаты колышков по оси X для размещения на них маркера.
Переменная markerX определяет положение маркера по оси X и может
принимать значение одной из трех вышеперечисленных констант.
Переменная step определяет шаг алгоритма. Алгоритм начинается
75
с первого шага. Состоянию окончания игры соответствует нулевое
значение переменной step. Переменная focus определяет активный
диск. Нажатия клавиш перемещения диска всегда относятся к активному диску. На первом шаге игры активным является красный
диск. Следующие три логические переменные g _ disk _ down, b _
disk _ down и r _ disk _ down проверяют возможность надеть диск на
тот или иной колышек. Первоначально им присваивается значение
false. Дальше в процессе игры их новые значения устанавливаются
в методах putGreenDiskToPeg(), putBlueDiskToPeg()и putRedDiskToPeg().
Значение true означает, что на данном шаге игры диск можно надеть на колышек. Дальше описываются три объекта g _ disk, b _ disk
и r _ disk класса Disk, объект marker класса Marker, объект tl класса
TiledLayer и объект lm класса LayerManager. Назначение логической
переменной working известно по предыдущим программам. Переменные animIndex1, animIndex2 и animIndex3 служат для хранения индексов анимированных ячеек. Переменная midlet используется для
остановки проигрывателя (вызов метода stopPlayer()).
В конструкторе класса HTCanvas определяются размеры экрана,
задается начальное размещение маркера на колышке C, вызывается метод Background() создания фонового изображения, загружаются изображения и создаются объекты трех дисков и маркера, определяется положение созданных объектов на экране. Далее объекты
добавляются к менеджеру слоев, причем объект фона tl добавляется последним.
Методы start() и stop() – такие же как и в ранее рассмотренных
мидлетах.
В методе Background()загружается изображение фонового слоя,
создаются объект tl и три анимированные ячейки, выполняется
заполнение ячеек таблицы фрагментами изображения согласно
массиву map, устанавливается содержимое анимированных ячеек,
определяется значение переменной upBound и положение объекта
tl на экране.
Главный код игрового цикла заключен в методе run(). Рассмотрим подробнее содержимое цикла while. Метод chooseDisk()
присваивает фокус диску, используемому на данном шаге игры.
Метод nextStep() определяет возможность перехода к следующему шагу игры. Методы putGreenDiskToPeg(), putBlueDiskToPeg()и
putRedDiskToPeg() проверяют, можно ли диск данного цвета надеть
на определенный колышек. Далее вызываются два метода класса
Marker для запуска перемещения маркера и установки нижней границы перемещения маркера. Проверяется столкновение маркера с
76
дисками разных цветов на уровне ограничивающих прямоугольников. Если столкновение происходит, то игра заканчивается. Переменная cnt разрешает обновлять содержимое анимированных ячеек на каждом 15-м проходе цикла и менять индексы фрагмента изображения в анимированных ячейках. Затем обрабатываем события
с клавиш телефона и рисуем изображения на экране.
В методе inputKey() в зависимости от нажатой клавиши активный диск может перемещаться влево, вправо, вверх или вниз. Методы DrawInfoText и DrawInfoRect формируют содержимое строки
состояния.
Листинг 4.9
/* класс HTCanvas */
import java.io.IOException; // подключаем исключение IOException
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
public class HTCanvas extends GameCanvas implements Runnable
{
int screenWidth, screenHeight;
int upBound;
int markerX; // координата маркера по оси X
// константы для определения активного диска
public final static byte NO_DISK = 0;
public final static byte GREEN_DISK = 1;
public final static byte BLUE_DISK = 2;
public final static byte RED_DISK = 3;
// координаты X для колышков A,B и C
public final static int PEG_A = 37;
public final static int PEG_B = 107;
public final static int PEG_C = 177;
byte step = 1; // шаг алгоритма
byte focus = RED_DISK; // активный диск
boolean g_disk_down = false;
boolean b_disk_down = false;
boolean r_disk_down = false;
private Disk g_disk; // создаем объект класса Green_disk
private Disk b_disk; // создаем объект класса Blue_disk
private Disk r_disk; // создаем объект класса Red_disk
private Marker marker; // создаем объект класса Marker
private TiledLayer tl; // создаем объект класса TiledLayer
private LayerManager lm; // создаем объект класса LayerManager
boolean working; // логическая переменная для выхода из цикла
// индексы анимационных ячеек
private int animIndex1, animIndex2, animIndex3;
private Hanoi_towers midlet; //ссылка на основной класс мидлета
public HTCanvas(Hanoi_towers imidlet) throws IOException
77
{
super(true); // вызываем конструктор суперкласса
midlet = imidlet;
screenWidth = getWidth();
screenHeight = getHeight();
markerX = PEG_C; // маркер на колышке C
tl = Background(); // инициализируем tl
// загружаем изображение зеленого диска
Image g_diskImage = Image.createImage(«/green_disk1.png»);
// инициализируем объект g_disk
g_disk = new Disk(g_diskImage, 70, 40, upBound );
// выбираем позицию зеленого диска
g_disk.setPosition( 10, upBound + 160 );
// загружаем изображение синего диска
Image b_diskImage = Image.createImage(«/blue_disk1.png»);
// инициализируем объект b_disk
b_disk = new Disk(b_diskImage, 50, 40, upBound );
// выбираем позицию синего диска
b_disk.setPosition( 20, upBound + 120 );
// загружаем изображение синего диска
Image r_diskImage = Image.createImage(«/red_disk1.png»);
// инициализируем объект b_disk
r_disk = new Disk(r_diskImage, 30, 40, upBound );
// выбираем позицию красного диска
r_disk.setPosition( 30, upBound + 80 );
Image imMarker = Image.createImage(«/marker.png»);
marker = new Marker(imMarker, 15, 15 );
lm = new LayerManager();
lm.append(g_disk); // добавляем зеленый диск в менеджер слоев
lm.append(b_disk); // добавляем синий диск в менеджер слоев
lm.append(r_disk); // добавляем красный диск в менеджер слоев
lm.append(marker); // добавляем маркер в менеджер слоев
lm.append(tl); // добавляем фон в менеджер слоев
}
public void start()
{
working = true;
Thread t = new Thread(this); // создаем и запускаем поток
t.start();
}
/* метод создающий объект класса TiledLayer
и загружающий фоновое изображение */
public TiledLayer Background() throws IOException
{
// загрузка изображения из файла ресурса
Image im = Image.createImage(«/bgr5.png»);
// создаем объект класса TiledLayer
tl = new TiledLayer( /*столбцы*/6,/*строки*/6, im,
/*ширина*/40,/*высота*/40 );
78
// создаем анимированные ячейкм
animIndex1 = tl.createAnimatedTile(5);
animIndex2 = tl.createAnimatedTile(5);
animIndex3 = tl.createAnimatedTile(5);
int[] map =
{ // разметка игрового поля
1, 1, 1, 1, 1, 1,
1, 2, 3, 1, 4, 1,
1, 2, 3, 1, 4, 1,
1, 2, 3, 1, 4, 1,
1, 2, 3, 1, 4, 1,
-1, 5, -2, 5, -3, 5 };
tl.setCell(1,5,animIndex1);
tl.setCell(3,5,animIndex2);
tl.setCell(5,5,animIndex3);
// цикл считывающий разметку поля
for(int i = 0; i < map.length; i++)
{
/* присваиваем каждому элементу игрового поля
определенную ячейку изображения im*/
tl.setCell( i % 6, i / 6, map[i] );
}
upBound = ( getHeight() – tl.getHeight() ) / 2 – 10;
if( upBound < 0 ) upBound = 0;
tl.setPosition((screenWidth – tl.getWidth())/2, upBound );
return tl;
}
public void stop() { working = false; }
public void run()
{
Graphics g = getGraphics(); // получаем графический контекст
int cnt = 0;
marker.setPosition( markerX, 180 );
while( working ) // бесконечный цикл
{
chooseDisk();
nextStep();
putRedDiskToPeg();
putBlueDiskToPeg();
putGreenDiskToPeg();
marker.setDownBound(step);
marker.move( markerX );
boolean r_diskCollision = marker.collidesWith(r_disk,false);
boolean b_diskCollision = marker.collidesWith(b_disk,false);
boolean g_diskCollision = marker.collidesWith(g_disk,false);
if( (cnt % 15) == 0 )
{
tl.setAnimatedTile(animIndex1, (cnt % 2) + 5 );
tl.setAnimatedTile(animIndex2, (cnt % 2) * 2 + 5 );
79
tl.setAnimatedTile(animIndex3, (cnt % 2) * 3 + 5 );
}
if((r_diskCollision||b_diskCollision||g_diskCollision)==true )
{
step = 0;
midlet.stopPlayer();
working = false;
}
inputKey(); // обрабатываем события с клавиш телефона
init( g ); // рисуем графические элементы
cnt++;
try { Thread.sleep( 60 ); } // останавливаем цикл на 60 мс
catch (java.lang.InterruptedException zxz) {};
}
}
private void inputKey()
{
int keyStates = getKeyStates(); // определяем нажатую клавишу
// код обработки для левой нажатой клавиши
if ((keyStates & LEFT_PRESSED) != 0)
{
switch( focus )
{
case GREEN_DISK: g_disk.moveLeft(); break;
case BLUE_DISK: b_disk.moveLeft(); break;
case RED_DISK: r_disk.moveLeft(); break;
}
}
// код обработки для правой нажатой клавиши
if ((keyStates & RIGHT_PRESSED) != 0)
{
switch( focus )
{
case GREEN_DISK: g_disk.moveRight(); break;
case BLUE_DISK: b_disk.moveRight(); break;
case RED_DISK: r_disk.moveRight(); break;
}
}
// код обработки для клавиши вверх
if ((keyStates & UP_PRESSED) != 0)
{
switch( focus )
{
case GREEN_DISK: g_disk.moveUp(); break;
case BLUE_DISK: b_disk.moveUp(); break;
case RED_DISK: r_disk.moveUp(); break;
}
}
80
// код обработки для клавиши вниз
if ((keyStates & DOWN_PRESSED) != 0)
{
switch( focus )
{
case GREEN_DISK: g_disk.moveDown( g_disk_down ); break;
case BLUE_DISK: b_disk.moveDown( b_disk_down ); break;
case RED_DISK: r_disk.moveDown( r_disk_down ); break;
}
}
}
private void putRedDiskToPeg()
{ // проверка возможности надеть красный диск на колышек
if( ((step == 1) || (step == 7)) && (r_disk.getX() == 170) )
r_disk_down = true;
else if( ( step == 3 ) && ( r_disk.getX() == 100 ) )
r_disk_down = true;
else if( ( step == 5 ) && ( r_disk.getX() == 30 ) )
r_disk_down = true;
else r_disk_down = false;
}
private void putBlueDiskToPeg()
{ // проверка возможности надеть синий диск на колышек
if( ( step == 2 ) && ( b_disk.getX() == 90 ) )
b_disk_down = true;
else if( ( step == 6 ) && ( b_disk.getX() == 160 ) )
b_disk_down = true;
else b_disk_down = false;
}
private void putGreenDiskToPeg ()
{ // проверка возможности надеть зеленый диск на колышек
if( ( step == 4 ) && ( g_disk.getX() == 150 ) )
g_disk_down = true;
else g_disk_down = false;
}
private void chooseDisk()
{ // выбор активного диска
if( ( step == 2 ) || ( step == 6 ) )
{
focus = BLUE_DISK;
b_disk.setFocus( true );
r_disk.setFocus( false );
g_disk.setFocus( false );
}
else if( step == 4 )
{
focus = GREEN_DISK;
g_disk.setFocus( true );
b_disk.setFocus( false);
81
r_disk.setFocus( false );
}
else
{
focus = RED_DISK;
g_disk.setFocus( false );
b_disk.setFocus( false);
r_disk.setFocus( true );
}
}
private void nextStep()
{
switch( step )
{
case 0: r_disk.setFocus( false );
focus = NO_DISK;
break;
case 1:
if((r_disk.getX() == 170)&&(r_disk.getY() == upBound + 160))
{
step = 2; markerX = PEG_B;
marker.setPosition( markerX, 180 );
}
break;
case 2:
if((b_disk.getX() == 90)&&(b_disk.getY() == upBound + 160))
step = 3;
break;
case 3:
if((r_disk.getX() == 100)&&(r_disk.getY() == upBound + 120))
{
step = 4; markerX = PEG_C;
marker.setPosition( markerX, 180 );
}
break;
case 4:
if((g_disk.getX() == 150)&&(g_disk.getY() == upBound + 160))
{
step = 5; markerX = PEG_A;
marker.setPosition( markerX, 180 );
}
break;
case 5:
if((r_disk.getX() == 30)&&(r_disk.getY() == upBound + 160))
{
step = 6; markerX = PEG_C;
marker.setPosition( markerX, 156 );
}
break;
82
case 6:
if((b_disk.getX() == 160)&&(b_disk.getY() == upBound + 120))
step = 7;
break;
case 7:
if((r_disk.getX() == 170)&&(r_disk.getY() == upBound + 80))
{
step = 0;
marker.setPosition( -50, -50 );
midlet.stopPlayer();
}
break;
}
}
private void DrawInfoText( Graphics g )
{ // вывод сообщения о числе пройденных шагов
int layout = Graphics.TOP|Graphics.LEFT;
if( step == 1 )
g.drawString(«Game start»,screenWidth/2,screenHeight-40,layout);
else
if( step == 0 )
g.drawString(«Game over»,screenWidth/2,screenHeight-40,layout);
else
g.drawString(«Step « + ( step – 1 ) + « completed»,
screenWidth/2 – 40, layout);
}
private void DrawInfoRect( Graphics g )
{
if( ( step & 1 ) == 1 )
{
g.setColor( 255, 0, 0 ); // красный цвет
g.fillRect( 5, screenHeight-40, 25, 15 );
}
else
if( ( step == 2 ) || ( step == 6 ) )
{
g.setColor( 0, 0, 255 ); // синий цвет
g.fillRect( 5, screenHeight-40, 45, 15 );
}
else if( step == 4 )
{
g.setColor( 0, 128, 64 ); // зеленый цвет
g.fillRect( 5, screenHeight-40, 65, 15 );
}
}
private void init( Graphics g )
{
g.setColor( 0xFFFFFF ); // белый цвет фона
g.fillRect( 0, 0, screenWidth, screenHeight );
83
g.setColor(255, 0, 0); // красный цвет информационной строки
DrawInfoText( g ); // рисуем информационную строку
DrawInfoRect(g);//прямоугольник, окрашенный в цвет активного диска
lm.paint(g, 0, 0); // рисуем слой в точке (0,0)
flushGraphics(); // двойная буферизация
}
}
Класс Disk – производный от класса Sprite. Он отвечает за диски,
используемые в игре, и имеет методы для перемещения дисков в
различных направлениях и установки кадра последовательности
в зависимости от того, является ли диск активным или нет. Код
класса Disk представлен в листинге 4.10.
Листинг 4.10
/* класс Disk */
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
public class Disk extends Sprite
{
int upBound;
static int[] frSequence = { 0, 1 };
// конструктор
public Disk(Image image, int fw, int fh, int Bound )
{
// обращаемся к конструктору суперкласса
super(image, fw, fh);
setFrameSequence( frSequence );
setFrame( 0 );
upBound = Bound;
}
);
);
84
public void moveLeft()// метод для левой нажатой клавиши
{
// передвигаем спрайт
if( ( getY() == upBound ) && ( getX() > 0 ) ) move( -10, 0
}
public void moveRight()// метод для правой нажатой клавиши
{
// передвигаем спрайт
if( (getY() == upBound ) && ( getX() < 190 ) ) move( 10, 0
}
public void moveUp()// метод для клавиши вверх
{
// передвигаем спрайт
if( getY() > upBound ) move( 0, -40 );
}
public void moveDown( boolean go ) // метод для клавиши вверх
{
// передвигаем спрайт
if( go ) move( 0, 40 );
}
}
public void setFocus( boolean focus )
{
if( focus == true ) setFrame( 1 );
else
setFrame( 0 );
}
Класс Marker, представленный в листинге 4.11, реализует логику работы маркера. Он имеет методы перемещения маркера и
установки нижней границы перемещения. Логическая переменная
direction определяет направление движения маркера. При значении, равном true, маркер перемещается вверх, а при значении
false – вниз. При достижении верхней или нижней границы маркер меняет направление движения на противоположное. Логика
перемещения маркера реализована в методе move(). Верхняя граница имеет координату –20 по оси Y, т. е. находится вне экрана. А положение нижней границы может меняться в зависимости от шага
игры и от того, сколько дисков надето на колышек. Для определения положения нижней границы служит метод setDownBound().
Листинг 4.11
/* класс Marker */
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
public class Marker extends Sprite
{
boolean direction;
int downBound;
// конструктор
public Marker(Image image, int fw, int fh )
{
// обращаемся к конструктору суперкласса
super(image, fw, fh);
direction = true;
downBound = 200;
setPosition(177,200);
}
85
// метод перемещения маркера
public void move( int X)
{
// передвигаем спрайт
if( direction == true )
{
if( getY() > -20 )
move( 0, -2 );
else direction = false;
}
if( direction == false )
{
if( getY() < downBound )
move( 0, 2 );
else direction = true;
}
}
}
public void setDownBound( int step )
{
if( step == 1 || step == 2 || step == 4 || step == 5 )
downBound = 200;
else
if( step == 3 || step == 6)
downBound = 156;
else
if( step == 7 )
downBound = 116;
}
Рис. 4.33. Экранные формы работы мобильного приложения
86
Рис. 4.34. Экранная форма завершения игры
в случае столкновения спрайтов маркера и диска
Экранные формы, демонстрирующие работу мобильного приложения, показаны на рис. 4.33 и 4.34.
4.3. Задание к лабораторной работе
1. Составить игровое мобильное приложение, содержащее не менее двух анимированных спрайтов и фоновое изображение, состоящее не менее чем из четырех фрагментов. В игре следует предусмотреть возможность столкновения спрайтов и использования хотя
бы одной анимированной ячейки в фоновом изображении. На экране должна выводиться некоторая информация о состоянии игры,
например количество набранных очков, количество жизней и т. п.
2*. Добавить в мидлет возможность трансформации одного из
спрайтов и определения столкновения одного из спрайтов с препятствием в фоновом изображении.
3. Проверить работу разработанного мидлета на эмуляторе и мобильном телефоне.
4.4. Содержание отчета
1. Титульный лист.
2. Цель работы.
3. Задание.
87
4. Сценарий игры.
5. Используемые изображения спрайтов и фонового слоя.
6*. Дополнительные возможности мидлета.
7. Листинги с кодами классов мидлета.
8. Экранные формы с результатами работы мидлета.
9. Выводы по лабораторной работе.
88
Библиографический список
1. Марковский С. Г., Марковская Н. В. Программирование приложений для мобильных устройств: лабораторный практикум.
СПб.: СПбГУАП, 2011.
2. Горнаков С. Г. Программирование мобильных телефонов на
Java 2 Micro Edition. М.: ДМК Пресс, 2008. 512 с.
3. Монахов В. Язык программирования Java и среда NetBeans.
СПб.: БХВ-Петербург, 2011. 704 с.
4. Болонский процесс: оценка качества образования. Организационно-метод. матер. по реализации модульно-рейтинговой системы оценки качества учебной работы студентов ГУАП/В. И. Хименко, А. П. Ястребов, А. В. Павлова, Е. Г. Семенова, О. И. Красильникова. СПб.: СПбГУАП, 2009. 34 с.
5. Barbagallo R. Wireless Game Development in Java with MIDP
2.0. Wordware Publishing, 2004. 500 с.
6. Bloch C., Wagner A. MIDP Style Guide for the Java™ 2 Platform,
Micro Edition. Addison Wesley, 2003. 288 с.
Интернет-ресурсы
http://www.java.sun.com
http://www.juga.ru
http://www.mobilab.ru
89
СОДЕРЖАНИЕ
Предисловие ....................................................................
Лабораторная работа № 1. ARGB-изображения .....................
Лабораторная работа № 2. Работа со звуком в Java ME ...........
Лабораторная работа № 3. Хранение данных в мобильных
устройствах......................................................................
Лабораторная работа № 4. Разработка игрового приложения...
Библиографический список ................................................
90
3
4
11
26
42
89
Документ
Категория
Без категории
Просмотров
0
Размер файла
897 Кб
Теги
11111
1/--страниц
Пожаловаться на содержимое документа