gamedev Tutorials Программирование игр

! В Java имя файла всегда должно совпадать с именем класса. Не забывайте, что заглавные и прописные буквы в Java различаются - и это же верно для имён файлов классов

Консольное приложение

Java ассоциируется с Интернетом и апплетами - хотя на нём можно создавать и приложения.

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

Вот этот очень простой пример:

public class ConsoleApp{
	public static void main(String args[]){
		System.out.println("Simple console program" + args[0]);
	}
}

Пример обладает всеми необходимыми чертами программы на Java. Во-первых, он содержит класс приложения - в нашем случае это ConsoleApp.

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

Во-вторых, внутри главного класса имеется обязательный метод main, который должен быть описан именно вот так:
public static void main(String args[])
и никак иначе. Именно этот метод вызывается средой исполнения, и уже из него Вы можете вызвать сами все прочие собственные методы.

Итак, главный метод программы должен быть объявлен с модификаторами доступа public и static, а так же не возвращающим никакого значения. Но для чего в скобках String args[]?

Дело в том, что любая программа может получать некоторые инструкции при своём вызове. Делается это так: в строке приглашения вводится имя программы, а за ним через пробел необходимые инструкции. В случае Java-программы этому ещё будет предшествовать вызов Java-интерпретатора, вот так:

ПриглашениеСистемы>java ConsoleApp Которую_написал_Я!

После нажатия клавиши Enter должна появиться строка:

Simple console programКоторую_написал_Я!

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

Simple console programКоторую

Эта приложение консольное, а некоторые оконные среды настроены таким образом, чтобы немедленно закрывать окно консоли, программа в котором завершила свою работу. Чтобы всё-таки успеть прочитать выводимый программой текст можно добавить в конец метода main следующие строки:
try{
	System.in.read();
}catch(Exception e){}
Всё что они делают - заставляют программу ждать нажатия на любую клавишу.

Но можно поступить и ещё проще. Не переписывать программу, а перенаправить её вывод в текстовый файл. А делается это особым вызовом программы, вот так:

ПриглашениеСистемы>java ConsoleApp Которую_написал_Я! > result.txt

Всё, результат работы будет сохранён в файле result.txt, который можно прочесть не торопясь, позже открыв через любой редактор.

В общем, всё это хорошо, но мало. Потому оставим консоль и перейдём, наконец, к графике.

Апплет

Можно было бы ограничиться и совсем примитивным примером, вроде нижеследующего:
import java.awt.*;
import java.applet.*;

public class SimpleApplet extends Applet{
	public void paint(Graphics g){
		g.drawString("Simple Web-Applet", 30, 30);
	}
}
А что, вполне работоспособный апплет. Правда и делает он не много - выводит одну строку текста в заданных координатах. Поэтому возьмёт пример посложнее, но и поинтереснее. Вот такой:
import java.awt.*;
import java.applet.*;

public class SimpleApplet extends Applet{
	//апплет выводит заданный текст
	String text;//текст, который надо вывести
	
	public void init(){
		this.setBackground(new Color(0x888888));//установлен фоновый цвет = серый 50%
		//теперь попробуем считать значения параметров
		//заданные апплету при вызове из html-документа
		text = getParameter("text");
		if(text == null)
			text = "Ничего не введено";
	}

	public void paint(Graphics g){
		g.drawString(text, 30, 30);//выводим текст
	}
}
Апплет предназначен для работы в браузере. И для того чтобы посмотреть как работает наш апплет, создайте любой html-файл и включите в его тело (между тегами body) следующий фрагмент:
<applet code=SimpleApplet.class name=SimpleApplet width=320 height=200>
	<param name=text value="Simple Applet">
</applet>
а затем просмотрите этот документ через любой современный web-браузер.

Каждый апплет начинается со следующих строк:
import java.awt.*;
import java.applet.*;
Это включение доступа к стандартным пакетам, хотя мне больше по душе называть их по старинке библиотеками. Библиотека applet содержит всё необходимое для функционирования апплета в браузере, а awt всю графику.

Вся необходимая инициализация производится методом init(), который должен быть описан только следующим образом:
public void init()
и никак иначе (потому что он автоматически вызывается системой при загрузке апплета). В нашем примере он строкой
this.setBackground(new Color(0x888888));
устанавливает фоновый цвет, затем пытается считать переданные апплету параметры. Если их считать не удаётся (а это возможно когда соответствующий тег applet вызывающего html-документа не содержит параметров с требуемым именем) - то устанавливаются значения по умолчанию.

Параметры могут содержать строку тескта и её цвет. Метод init() считает их, а вот за вывод на экран отвечает метод paint(), который должен быть описан только вот так:
public void paint(Graphics g)
и не иначе (потому что его система вызывает при необходимости перерисовать изображение на экране). Вся работа по созданию изображения производится только тут.

Уже можно было бы и закончить - апплет считывает и выводит на экран заданные параметры не хуже консольного приложения. Не хуже?! Но надо лучше. Добавим интерактивности - научим апплет дополнять заданный текст вводимыми с клавиатуры символами. А ещё пусть показывает координаты мыши и нажатия её кнопок.

Всё что для этого потребуется - это добавить тройку переменных (содержащих координаты мыши и состояние её кнопок), одну строку в метод paint() (чтобы научить его выводить на экран состояние мыши) и целый метод называемый handleEvent(). В общем, перепешите метод paint() и вставьте сразу за ним объявление новых переменных и метод handleEvent() как показано здесь:
	public void paint(Graphics g){
		g.setColor(new Color(255, 255, 255));//задаём текущий цвет рисования
		g.drawString(text, 30, 30);//выводим текст
		g.drawString(""+mouseX+","+mouseY+", key="+mouseKey, 50,50);//и состояние мыши
	}

	int mouseX, mouseY, mouseKey;//координаты и кнопки мыши

	public boolean handleEvent(Event event){
		//метод обрабатывает события:
		// 1) получает символ с клавиатуры
		// 2) отслеживает перемещения мыши
		if(event.id == Event.KEY_PRESS){//нажата обычная ASCII клавиша
			char keyChar = ((char)event.key);//выделить из события введённый символ
			text += keyChar;//добавить его к тексту
			repaint();//перерисовать изображение апплета
			return true;//сигнализировать что сообщение уже обработано
		}else if(event.id == Event.MOUSE_MOVE){
			mouseX = event.x;//считать координаты события
			mouseY = event.y;
			repaint();//перерисовать изображение апплета
			return true;//сигнализировать что сообщение уже обработано
		}else if(event.id == Event.MOUSE_DOWN){
			//попытка узнать какая кнопка мыши нажата
			//для однокнопочных мышей эмулируется с помощью спецклавиш клавиатуры
			mouseKey = 1;//все системы поддерживают однокнопочную мышь
			//но в следующих строках мы проверим, не нажата ли
			//или не эмулируется ли нажатие иных кнопок
			if(event.modifiers == Event.META_MASK) mouseKey = 2;
			if(event.modifiers == Event.ALT_MASK) mouseKey = 3;
			repaint();//перерисовать изображение апплета
			return true;//сигнализировать что сообщение уже обработано
		}else if(event.id == Event.MOUSE_UP){
			mouseKey = 0;
			repaint();//перерисовать изображение апплета
			return true;//сигнализировать что сообщение уже обработано
		}else
			return super.handleEvent(event);//передать событие родителю
	}
То, что переменные mouseX, mouseY и mouseKey объявлены позже, чем используются (в методе paint()), не должно иметь никакого значения. Хотя, конечно, лучше объявлять все глобальные для блока переменные в его начале (по крайней мере так их потом будет легче найти в тексте, когда понадобиться вносить исправления).

Интересным нововведением является метод handleEvent(), который должен быть объявлен только следующим образом
public boolean handleEvent(Event event)
и никак иначе (ведь он вызывается системой при возникновении событий - а она будет вызывать только строго определённый метод либо все события остануться необработанными).

События могут быть любыми, но мы обрабатываем только интересующие нас - а про остальные не заботимся, препоручая их обработчику событий вышестоящего класса (в данном случае это класс Applet). Хотя можно вместо этого просто честно ответить системе, что сообщение не обработано, вот так:
return false;//сигнализировать, что сообщение не обработано
Но более важна обработка нужных нам сообщений, не так ли?

С этим дело довольно просто: определяем не совпадает ли тип сообщения с нужным, проверяя поле .id на совпадение с соответствующей предопределённой константой из класса Event, и если совпадает, то обрабатываем его (в нашем случае обработка сводится к выборке интересующих нас данных), а в завершение сигнализируем системе, что сообщение обработано нашим методом.

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

Но мало выбрать данные из события - надо ещё отобразить их на экране. Это делается следующей строкой:
repaint();//перерисовать изображение апплета
причём именно так. Метод paint() никогда не вызывается напрямую - только через вызов repaint() и никак иначе. Таковы правила.

! Вы может встретить предупреждение насчёт метода handleEvent(), что он де устаревший и вообще так уже не пишут. Пишут. Потому что это удобно (когда все реакции на события собраны в одном месте и их не нужно искать по всей программе). Более того, хотя это и не относится к данной теме, но знайте что в версии Java для мобильных устройств (даже самой распоследней редакции) есть только один единственный метод для обработки всех событий - и это handleEvent(). Правда, обработать прокрутку колеса мыши с его помощью невозможно - но, по-моему, это единственный случай для которого игроделу может потребоваться иной метод.

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

Дублирующий буфер экрана

Эта тема жизненно важна для создания плавной анимации.

Для плавной смены изображения нужно заранее подготовить кадр полностью где-то вне экрана - а когда он будет готов перебросить его на экран одной операцией. Внеэкранная область для предварительного создания кадра называется дублирующим буфером (намекая, что он дублирует экранное изображение). Сам же этот принцип используется абсолютно везде для плавной смены изображения - вне зависимости от операционной системы или языка программирования. Разумеется, средства для создания внеэкранного буфера есть и в Java.

Начнём с добавления необходимых переменных - членов класса:
Image offScreen;//собственно дублирующий буфер
Graphics graph;//графический контекст дублирующего буфера
Объект класса Image - это обычное изображение. Дублирующий буфер как раз и является изображением, не так ли?

Переменные объявлены - но фактически никакое пространство под дублирующий буфер ещё не выделено. Надо завершить инициализацию, вставив следующие строки в метод init() либо в конструктор класса программы (как Вам больше нравиться - но не надо в оба места одновременно!):
offScreen = createImage(320,200);//создание дублирующего буфера размером 320x200 пикселей
graph = offScreen.getGraphics();//получить графический контекст для него
И наконец главный секрет - надо переопределить метод update(). На самом деле даже если Вы не включали этот метод в свою программу, он там уже присутствует (неявно) - и всё что он делает, это очистка экрана перед перерисовкой.

Но мы то заменим весь кадр целиком, а стало быть очистка экранного изображения просто не нужна. И даже вредна - потому что вызовет мерцание (ведь экран сначала будет очищаться, а только затем заполняться новым кадром). А чтобы избавиться от этого, переопределим метод следующим образом:
public void update(Graphics g){
	paint(g);
}
Теперь подготовка завершена.

Как использовать дублирующий буфер? Вспомним, что метод paint() получает единственный входной параметр Graphics g. Точно таков же и тип графического контекста дублирующего буфера! Просто замените во всех случаях g. на graph. и рисование произойдёт не на экран, а в дублирующий буфер. Откуда взят контекст - туда и рисуем!

Но надо не забыть очищать дублирующий буфер перед рисованием каждого очередного кадра (это созданное нами изображение и система его не очистит автоматически), да ещё в самом конце метода paint() нужно добавить строку, которая перебросит созданный кадр из дублирующего буфера на экран (иначе мы не увидим результат своих трудов). Это делается точно так же, как помещение на экран любого изображения. В итоге paint() должен принять вот такой вид:
public void paint(Graphics g){
	//прежде всего очистим дублирующий буфер
	Dimension d = this.getSize();//получение текущих размеров окна
	graph.clearRect(0, 0, d.width, d.height);//очистка дублирующего буфера
	//весь вывод осуществляется через графический контекст дублирующего буфера - а значит в дублирующий буфер
	graph.setColor(new Color(255, 255, 255));//задаём текущий цвет рисования
	graph.drawString(text, 30, 30);//выводим текст
	graph.drawString(""+mouseX+","+mouseY+", key="+mouseKey, 50,50);//и состояние мыши
	g.drawImage(offScreen, 0, 0, this);//поместить изображение на экран
}
Вот, собственно и всё!

! Отнеситесь со всей серьёзностью к следующему предупреждению: иногда синтаксически правильно написанная программа может не работать. Виновен в этом не сам язык Java, а неразумно поспешный прогресс, стремление выбросить на рынок очередную версию операционной системы или компилятора раньше конкурента. Например, невозможно создать дублирующий буфер в программе, компилируемой под JBuilder 8 (а ведь когда-то Borland это была марка!), хотя тот же самый текст будет откомпилирован даже Visual J++ от MicroSoft (фирмы - противника технологии Java).

В общем, если хотите избежать таких ошибок не по своей вине - работайте только с компилятором javac из стандартной поставки (ничего что он консольный - зато эталон!).

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

Поток для создания анимации

Потоки применяются для разных целей, но нам сейчас будет наиболее интересен всего один вид их применения - для создания анимации, независимой от внешних событий.

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

Я использую для этого следующий способ, суть которого сводится к превращению в поток самой программы (а не выделению только какой-то одной её части в независимый от прочих частей поток). На мой взгляд так удобнее синхронизировать отдельные части программы между собой. Практически же это реализуется следующим образом.

В конец объявления класса программы, после указания класса-предка, надо добавить implements Runnable, вот так:
public class SimpleApplet extends Applet implements Runnable
этим объявляется, что созданный нами класс программы наследует ещё и интерфейс Runnable, необходимый для создания потока. К слову сказать, каждый класс может происходить только от одного класса-предка (увы, множественного наследования, как в C++, здесь нет), но зато может при этом наследовать несколько различных интерфейсов. Интерфейс же только указывает какие методы обязан реализовать наследующий его класс - и нам придётся теперь реализовать их все собственноручно.

Но прежде добавим переменную класса потока Thread и сразу инициализируем её как несуществующую (это можно и не делать, но для самоуспокоения всё же стоит), вот так:
Thread appThread = null;
Теперь реализуем обязательные методы start(), stop() и run(), вот так:
public void start(){
	if(appThread == null){
		appThread =  new Thread(this);
		appThread.start();
	}
}

public void stop(){
	appThread = null;
}

public void run(){
	while(appThread != null){
		//здесь производим все необходимые действия

		//всё подсчитали? тогда пора перерисовать изображение
		repaint();
		try{//начинается часть, которая может вызвать исключительную ситуацию
			Thread.sleep(50);//усыпляем поток на 50 миллисекунд
		}catch(InterruptedException e) {}//именно так -
		// - фактическая обработка исключения нам не нужна, но нужно соблюсти правила
	}
}
Тогда как методы start() (создающий поток) и stop() (уничтожающий поток) будут скорее всего идентичными во всех Ваших программах, самое интересное происходит в run() - ведь он ни что иное как главный цикл программы!

Как видно, фактически всё тело метода run() занимает цикл - вечный, пока не будет уничтожен сам поток. В этом цикле производятся все вычисления, затем даётся команда на обновление изображения (чтобы пользователь видел результат) и поток ненадолго усыпляется.

! Полезный совет: Между прочим, в программах использующих потоки иногда проявляется ленивая реакция на нажатия клавиш. А связано это с чрезмерно малой величиной задержки, установленной в методе sleep(). Малая величина может быть установлена для большей скорости счёта, а в результате получается ситуция, когда программа вечно слишком занята, чтобы "послушать" клавиатуру. Стоит увеличить время задержки и реакция на клавиши станет приемлемой (общая рекомендация: эксперементируйте со значениями от 10 до 50).

Быть может описанный выше способ не слишком правилен с точки зрения "хорошего тона" программирования на Java (например, следовало бы применять так называемые слушатели событий, да и в поток превращать только ту часть, которая отвечает за видеовывод) - но мой способ прост для понимания и быстр. А быстродействие в играх всегда будет актуально.

Спрайт

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

В Java уже включено всё необходимое для использования спрайтов. Вспомните, как воспроизводился на экране кадр из дублирующего буфера, командой:
g.drawImage(offScreen, 0, 0, this);//поместить изображение на экран
Тогда же было упомянуто, что это стандартный метод для воспроизведения любого изображения из памяти на экран. И если вместо offScreen (созданного нами изображения, которое используется в качестве дублирующего буфера) подставить имя другого изображения - будет выведено это другое изображение. А оно может быть и загруженным из файла формата GIF. Который имеет очень полезное свойство - поддерживает прозрачные области в изображении. Короче, это готовый спрайт, причём хранящийся в настолько распространённом формате, что даже простейший графический редактор Paint из комплекта Windows может работать с ним.

Таким образом задача добавления анимации к нашему апплету сводится к двум шагам: загрузка изображений формата GIF и их воспроизведение в дублирующем буфере - а всё остальное у нас уже сделано.

Добавим новые переменные - члены класса:
Image pic[] = new Image[28];//кадры для анимации, расположенные по порядку
int frame = 0;//номер текущего кадра, нужен для анимации
Сама загрузка изображений (содержащих кадры) осуществляется в методе инициализации. Прежде всего добавим новую переменную
MediaTracker tracker = new MediaTracker(this);
Этой строкой создаётся объект, очень важный для загрузки медиа-файлов, к которым, в частности, относятся и файлы формата GIF.

Теперь дополним метод собственно операцией загрузки изображений, воспользовавшись тем, что они упорядочены по номерам:
for(int i = 0; i < 28; i++){
	pic[i] = getImage(getCodeBase(),"resource/fregat" + i + ".gif");
	tracker.addImage(pic[i],i);
	try{tracker.waitForID(i);}catch(InterruptedException e){}
	//следующая строка обязательно нужна для Windows, если загружается несколько изображений
	pic[i].flush();//нормальные операционные системы этого не требуют - впрочем и не ругаются
}
Здесь показан важный приём загрузки ресурсов в апплет. Апплет не имеет доступа к файловой системе, поэтому используется операция getCodeBase(), определяющая исходное местонахождение апплета - на сервере в сети или на локальном компьютере. В общем, апплету его конкретное местонахождение безразлично, главное чтобы все ресурсы находились там же. В данном случае они находятся в подкаталоге. И, обратите внимание, что стиль указания пути здесь такой же, как в адресной строке браузера.

Затем две следующих строки производят собственно загрузку изображения, делая это так, чтобы не "подвесить" систему (что актуально, если изображение большое, а закачивать его надо через модем).

Последняя строка производит "очистку связанных с изображением буферов и действительную запись всех произведённых изменений". Не правда ли, странно - оказывается произведённое изменение не обязательно действительно произведено. Но тому, кто серьёзно программировал под DOS, тут же придёт на память аналогия с организацией общения программ с диском - видимо Windows крепко унаследовала такие, не самые лучшие, черты своей предшественницы (настолько крепко, что проще вписать лишнюю строку в программу, чен написать java-среду, избавляющую программиста от вникания в такие наследственные проблемы). Другие же операционные системы свободны от подобных анахронизмов. Или же лучше и честнее документированы, чем детище Билла Гейтса (который уж слишком любит, чтобы только продукты его фирмы могли работать под его операционной системой).

В метод run() впишем - как раз туда, где оставлено место для вычислений - увеличение счётчика кадров. И не забудем обнулять его, когда он достигнет предельного значения:
//здесь производим все необходимые действия
if(frame < 27)
	frame++;//увеличить номер кадра
else
	frame = 0;//обнулить
Наконец, чтобы видеть результат, надо дополнить paint() следующим:
graph.drawImage(pic[frame], 100, 100, this);
И всё - можно компилировать и наблюдать результат. У меня в пространстве апплета (в добавок ко всему прочему, что там уже было) вращается космический корабль.

И снова есть необходимость в улучшении. Корабль вращается заметно "дёрганно". А это оттого, что размеры изображений с отдельными кадрами - разные. Можно, конечно, переделать все изображения, приведя их к одному размеру - но это не лучший вариант, так как увеличит общий объём программы (увеличивая объём необходимых для работы ресурсов), что уж точно нежелательно для web-апплета. Да и работы предстоит не так уж мало - переделать целых 28 изображений! Нет, ленивые выбирают правильный путь - мы будем вычислять положение центра очередного кадра непосредственно перед его прорисовкой. И изменить нам придётся всего одну строку - отвечающую за прорисовку спрайта в методе paint(), вот так:
graph.drawImage(pic[frame],
	100 - pic[frame].getWidth(null)/2,//скорректировать центр спрайта
	100 - pic[frame].getHeight(null)/2,
	this);
Здесь применён интересный и небесполезный практический приём. Дело в том, что многие графические да и не только графические методы требуют в качестве одного из входных параметров ссылки на некий объект-обозреватель (observer). В частности метод drawImage() требует его - это последний из параметров, который у нас всегда указывал на сам апплет, через зарезервированное слово this. Но нередко вместо фактического указания, можно просто подставить null и тем не менее всё будет работать. Попробуйте сами - поменяйте в приведённой выше строке программы все null на this и наоборот и посмотрите на результат. Данный приём особенно полезен в глубоко вложенных объектах, где получение ссылки на подходящий обозревать становиться громоздкой конструкцией.

Звук

Изначально базовая Java-среда поддерживала звук только в формате AU. Зато работа с ним облегчена максимально, все необходимые методы уже интегрированы в класс апплета. Вот всё, что надо сделать. Прежде всего объявить переменную - член класса:
AudioClip soundTrack = null;//звук
Теперь загрузим звуковой файл, и естественно самое подходящее место для этого - это метод init():
//загрузить космическую музыку
soundTrack = getAudioClip(getCodeBase(), "resource/spacemusic.au");
В отличие от изображений - с которыми работают методы других классов (обычно Graphics) - звук сам всё делает собственными методами, которых всего 3: В данном случае загруженный звук используется как фоновая музыка - наиболее подходящее место для запуска его воспроизведения - это метод start(). Впишите следующие строки сразу после создания экземпляра потока:
if(soundTrack != null)
	soundTrack.loop();//запустить воспроизведение звука
И при завершении работы апплета нужно не забыть выключить звук - конечно же в методе stop(), сразу после уничтожения экземпляра потока:
if(soundTrack != null)
    soundTrack.stop();//остановить воспроизведение звука

Полный текст получившегося апплета

import java.awt.*;
import java.applet.*;

public class SimpleApplet extends Applet implements Runnable{
	//апплет выводит текст заданным цветом
	//далее текст может быть дополнен с клавиатуры
	//дополнительно выводится положение мыши в окне апплета
	//так же присутствует постоянная анимация вращающегося фрегата
	//и фоновая космическая музыка
	String text;//текст, который надо вывести
	Image offScreen;//собственно дублирующий буфер
	Graphics graph;//графический контекст дублирующего буфера
	Thread appThread = null;
	Image pic[] = new Image[28];//кадры для анимации, расположенные по порядку
	int frame = 0;//номер текущего кадра, нужен для анимации
	AudioClip soundTrack = null;//звук

 	public void init(){
		MediaTracker tracker = new MediaTracker(this);
		
		this.setBackground(new Color(0x888888));//установлен фоновый цвет = серый 50%
		//теперь попробуем считать значения параметров
		//заданные апплету при вызове из html-документа
		text = getParameter("text");
		if(text == null)
			text = "Ничего не введено";
		//создадим дублирующий буфер
		offScreen = createImage(320,200);//создание дублирующего буфера размером 320x200 пикселей
		graph = offScreen.getGraphics();//получить графический контекст для него
		//загрузим спрайты
		for(int i = 0; i < 28; i++){
			pic[i] = getImage(getCodeBase(),"resource/fregat" + i + ".gif");
			tracker.addImage(pic[i],i);
			try{
				tracker.waitForID(i);
			}catch(InterruptedException e){}
			//следующая строка обязательно нужна для Windows, если загружается несколько изображений
			pic[i].flush();//нормальные операционные системы этого не требуют - впрочем и не ругаются
		}
		//загрузить космическую музыку
		soundTrack = getAudioClip(getCodeBase(), "resource/spacemusic.au");
	}

	public void start(){
		if(appThread == null){
			appThread =  new Thread(this);
			appThread.start();
		}
		if(soundTrack != null)
			soundTrack.loop();//запустить воспроизведение звука
	}

	public void stop(){
		appThread = null;
		if(soundTrack != null)
		    soundTrack.stop();//остановить воспроизведение звука
	}

	public void run(){
		while(appThread != null){
			//здесь производим все необходимые действия
			if(frame < 27)
				frame++;//увеличить номер кадра
			else
				frame = 0;//обнулить
			//всё подсчитали? тогда пора перерисовать изображение
			repaint();
			try{//начинается часть, которая может вызвать исключительную ситуацию
				Thread.sleep(50);//усыпляем поток на 50 миллисекунд
			}catch(InterruptedException e) {}//именно так -
			// - фактическая обработка исключений нам не нужна, но нужно соблюсти правила
		}
	}

	public void update(Graphics g){
		paint(g);
	}

	public void paint(Graphics g){
		Dimension d = this.getSize();//получение размеров игрового окна
		graph.clearRect(0, 0, d.width, d.height);//очистка дублирующего буфера
		graph.setColor(new Color(255, 255, 255));//задаём цвет
		graph.drawString(text, 30, 30);//выводим текст
		graph.drawString(""+mouseX+","+mouseY+", key="+mouseKey, 50,50);//и состояние мыши
		graph.drawImage(pic[frame],
			100 - pic[frame].getWidth(null)/2,//скорректировать центр спрайта
			100 - pic[frame].getHeight(null)/2,
			this);
		g.drawImage(offScreen, 0, 0, this);//поместить изображение на экран
	}

	int mouseX, mouseY, mouseKey;//координаты и кнопки мыши

	public boolean handleEvent(Event event){
		//метод обрабатывает события:
		// 1) получает символ с клавиатуры
		// 2) отслеживает перемещения мыши
		if(event.id == Event.KEY_PRESS){//нажата обычная ASCII клавиша
			char keyChar = ((char)event.key);//выделить из события введённый символ
			text += keyChar;//добавить его к тексту
			repaint();//перерисовать изображение апплета
			return true;//сигнализировать что сообщение уже обработано
		}else if(event.id == Event.MOUSE_MOVE){
			mouseX = event.x;//считать координаты события
			mouseY = event.y;
			repaint();//перерисовать изображение апплета
			return true;//сигнализировать что сообщение уже обработано
		}else if(event.id == Event.MOUSE_DOWN){
			//попытка узнать какая кнопка мыши нажата
			//для однокнопочных мышей эмулируется с помощью спецклавиш клавиатуры
			mouseKey = 1;//все системы поддерживают однокнопочную мышь
			//но в следующих строках мы проверим, не нажата ли
			//или не эмулируется ли нажатие иных кнопок
			if(event.modifiers == Event.META_MASK) mouseKey = 2;
			if(event.modifiers == Event.ALT_MASK) mouseKey = 3;
			repaint();//перерисовать изображение апплета
			return true;//сигнализировать что сообщение уже обработано
		}else if(event.id == Event.MOUSE_UP){
			mouseKey = 0;
			repaint();//перерисовать изображение апплета
			return true;//сигнализировать что сообщение уже обработано
		}else
			return super.handleEvent(event);//передать событие родителю
	}
}

Подведём итог: Апплеты придуманы как украшение HTML-документов. При этом они не требуют какого-либо участия от сервера, а обрабатываются браузером. В отличие от JavaScript (который является языком скриптов) апплеты исполняются быстрее (потому что откомпилированы заранее) и не зависят от особенностей работы конкретного браузера (к которым нередко оказываются привязаны скрипты). В отличие от flash Java-апплет может занимать намного меньше места (вплоть до того же объёма что JPG-изображение той же площади - согласитесь, неплохой результат, что оказывается важным при модемном соединении) и одновременно обладать большей интерактивностью. Конечно, конкретный результат всегда зависит от таланта программиста (но это же верно и для flash-анимации, где простор для приложения таланта существенно уже).

Апплет - это программа, которая может, кажется, всё, на что хватит Вашей фантазии. Спрайты, векторная графика, настоящее трёхмерное пространство - да что там, хоть многомерные фракталы - всё это возможно на Java.

Но есть и ограничения. Объём занимаемого апплетом экранного пространства устанавливает браузер (а не сам апплет). Файловая система, по соображениям безопасности, недоступна апплету - и для сохранения игры придётся обращаться к услугам JavaScript и cookie-файлов.

С другой стороны апплет не требует специальной процедуры инсталяции - он уже, немедленно готов к использованию. Только если есть абсолютная уверенность, что для реализации Вашего замысла действительно необходимо большее - имеет смысл погрузиться в Java полностью, и создавать полноэкранные приложения (а возможно и игровые сревера). Для большинства же игр апплет будет наилучшей формой реализации, лёгкой в разработке и в использовании.

© Rex
Игры и Java

PMG  14 ноября 2006 (c)  Rex