! В 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 Которую_написал_Я! |
Simple console programКоторую_написал_Я! |
Simple console programКоторую |
Эта приложение консольное, а некоторые оконные среды настроены таким образом, чтобы немедленно закрывать окно консоли, программа в котором завершила свою работу. Чтобы всё-таки успеть прочитать выводимый программой текст можно добавить в конец метода main следующие строки:
try{ System.in.read(); }catch(Exception e){} |
Но можно поступить и ещё проще. Не переписывать программу, а перенаправить её вывод в текстовый файл. А делается это особым вызовом программы, вот так:
ПриглашениеСистемы>java ConsoleApp Которую_написал_Я! > 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);//выводим текст } } |
<applet code=SimpleApplet.class name=SimpleApplet width=320 height=200> <param name=text value="Simple Applet"> </applet> |
Каждый апплет начинается со следующих строк:
import java.awt.*; import java.applet.*; |
Вся необходимая инициализация производится методом init(), который должен быть описан только следующим образом:
public void init() |
this.setBackground(new Color(0x888888)); |
Параметры могут содержать строку тескта и её цвет. Метод 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);//передать событие родителю } |
Интересным нововведением является метод handleEvent(), который должен быть объявлен только следующим образом
public boolean handleEvent(Event event) |
События могут быть любыми, но мы обрабатываем только интересующие нас - а про остальные не заботимся, препоручая их обработчику событий вышестоящего класса (в данном случае это класс Applet). Хотя можно вместо этого просто честно ответить системе, что сообщение не обработано, вот так:
return false;//сигнализировать, что сообщение не обработано |
С этим дело довольно просто: определяем не совпадает ли тип сообщения с нужным, проверяя поле .id на совпадение с соответствующей предопределённой константой из класса Event, и если совпадает, то обрабатываем его (в нашем случае обработка сводится к выборке интересующих нас данных), а в завершение сигнализируем системе, что сообщение обработано нашим методом.
Нам помогает то, что каждое сообщение несёт по возможности полный комплект данных о том как оно произошло. Например, движение мыши вызывает создание системного сообщения, которое среди прочего содержит и координаты данного события.
Но мало выбрать данные из события - надо ещё отобразить их на экране. Это делается следующей строкой:
repaint();//перерисовать изображение апплета |
! Вы может встретить предупреждение насчёт метода handleEvent(), что он де устаревший и вообще так уже не пишут. Пишут. Потому что это удобно (когда все реакции на события собраны в одном месте и их не нужно искать по всей программе). Более того, хотя это и не относится к данной теме, но знайте что в версии Java для мобильных устройств (даже самой распоследней редакции) есть только один единственный метод для обработки всех событий - и это handleEvent(). Правда, обработать прокрутку колеса мыши с его помощью невозможно - но, по-моему, это единственный случай для которого игроделу может потребоваться иной метод.
Теперь уже почти совсем хорошо - апплет реагирует на происходящие события. Но быть может Вы уже заметили проблему - иногда апплет может мерцать при перерисовке. Пока изображение не слишком насыщено это не особенно заметно, но это начнёт раздражать когда экран будет заполнен игровыми объектами.
Дублирующий буфер экрана
Эта тема жизненно важна для создания плавной анимации.
Для плавной смены изображения нужно заранее подготовить кадр полностью где-то вне экрана - а когда он будет готов перебросить его на экран одной операцией. Внеэкранная область для предварительного создания кадра называется дублирующим буфером (намекая, что он дублирует экранное изображение). Сам же этот принцип используется абсолютно везде для плавной смены изображения - вне зависимости от операционной системы или языка программирования. Разумеется, средства для создания внеэкранного буфера есть и в Java.
Начнём с добавления необходимых переменных - членов класса:
Image offScreen;//собственно дублирующий буфер Graphics graph;//графический контекст дублирующего буфера |
Переменные объявлены - но фактически никакое пространство под дублирующий буфер ещё не выделено. Надо завершить инициализацию, вставив следующие строки в метод init() либо в конструктор класса программы (как Вам больше нравиться - но не надо в оба места одновременно!):
offScreen = createImage(320,200);//создание дублирующего буфера размером 320x200 пикселей graph = offScreen.getGraphics();//получить графический контекст для него |
Но мы то заменим весь кадр целиком, а стало быть очистка экранного изображения просто не нужна. И даже вредна - потому что вызовет мерцание (ведь экран сначала будет очищаться, а только затем заполняться новым кадром). А чтобы избавиться от этого, переопределим метод следующим образом:
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 |
Но прежде добавим переменную класса потока Thread и сразу инициализируем её как несуществующую (это можно и не делать, но для самоуспокоения всё же стоит), вот так:
Thread appThread = null; |
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) {}//именно так - // - фактическая обработка исключения нам не нужна, но нужно соблюсти правила } } |
Как видно, фактически всё тело метода run() занимает цикл - вечный, пока не будет уничтожен сам поток. В этом цикле производятся все вычисления, затем даётся команда на обновление изображения (чтобы пользователь видел результат) и поток ненадолго усыпляется.
! Полезный совет: Между прочим, в программах использующих потоки иногда проявляется ленивая реакция на нажатия клавиш. А связано это с чрезмерно малой величиной задержки, установленной в методе sleep(). Малая величина может быть установлена для большей скорости счёта, а в результате получается ситуция, когда программа вечно слишком занята, чтобы "послушать" клавиатуру. Стоит увеличить время задержки и реакция на клавиши станет приемлемой (общая рекомендация: эксперементируйте со значениями от 10 до 50).
Быть может описанный выше способ не слишком правилен с точки зрения "хорошего тона" программирования на Java (например, следовало бы применять так называемые слушатели событий, да и в поток превращать только ту часть, которая отвечает за видеовывод) - но мой способ прост для понимания и быстр. А быстродействие в играх всегда будет актуально.
Спрайт
Это не только газировка или маленький лесной дух со стрекозьими крылышками на спине. Ещё так называют изображение, перемещающиеся по экрану и не портящее при этом перемещении фонового изображения под собой - как герои мультфильмов движутся по заранее отрисованным сценам.
В Java уже включено всё необходимое для использования спрайтов. Вспомните, как воспроизводился на экране кадр из дублирующего буфера, командой:
g.drawImage(offScreen, 0, 0, this);//поместить изображение на экран |
Таким образом задача добавления анимации к нашему апплету сводится к двум шагам: загрузка изображений формата GIF и их воспроизведение в дублирующем буфере - а всё остальное у нас уже сделано.
Добавим новые переменные - члены класса:
Image pic[] = new Image[28];//кадры для анимации, расположенные по порядку int frame = 0;//номер текущего кадра, нужен для анимации |
MediaTracker tracker = new MediaTracker(this); |
Теперь дополним метод собственно операцией загрузки изображений, воспользовавшись тем, что они упорядочены по номерам:
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();//нормальные операционные системы этого не требуют - впрочем и не ругаются } |
Затем две следующих строки производят собственно загрузку изображения, делая это так, чтобы не "подвесить" систему (что актуально, если изображение большое, а закачивать его надо через модем).
Последняя строка производит "очистку связанных с изображением буферов и действительную запись всех произведённых изменений". Не правда ли, странно - оказывается произведённое изменение не обязательно действительно произведено. Но тому, кто серьёзно программировал под DOS, тут же придёт на память аналогия с организацией общения программ с диском - видимо Windows крепко унаследовала такие, не самые лучшие, черты своей предшественницы (настолько крепко, что проще вписать лишнюю строку в программу, чен написать java-среду, избавляющую программиста от вникания в такие наследственные проблемы). Другие же операционные системы свободны от подобных анахронизмов. Или же лучше и честнее документированы, чем детище Билла Гейтса (который уж слишком любит, чтобы только продукты его фирмы могли работать под его операционной системой).
В метод run() впишем - как раз туда, где оставлено место для вычислений - увеличение счётчика кадров. И не забудем обнулять его, когда он достигнет предельного значения:
//здесь производим все необходимые действия if(frame < 27) frame++;//увеличить номер кадра else frame = 0;//обнулить |
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); |
Звук
Изначально базовая Java-среда поддерживала звук только в формате AU. Зато работа с ним облегчена максимально, все необходимые методы уже интегрированы в класс апплета. Вот всё, что надо сделать. Прежде всего объявить переменную - член класса:AudioClip soundTrack = null;//звук |
//загрузить космическую музыку soundTrack = getAudioClip(getCodeBase(), "resource/spacemusic.au"); |
- loop() - запускает бесконечное (зацикленное) воспроизведение звука
- play() - проигрывает звук однократно, при каждом новом вызове начиная с начала
- stop() - останавливает воспроизведение звука
if(soundTrack != null) soundTrack.loop();//запустить воспроизведение звука |
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 полностью, и создавать полноэкранные приложения (а возможно и игровые сревера). Для большинства же игр апплет будет наилучшей формой реализации, лёгкой в разработке и в использовании.