КлассGameCanvas предоставляет часть экрана, которую устройство отводит под нашу игру. Класс javax.microedition.lcdui.game.GameCanvas отличается от своего суперкласса javax.microedition.lcdui.Canvas двумя вещами: буферизацией графики и способностью возвращать состояния клавиш. Обе эти возможности значительно упрощают работу разработчика игр.
Буферизация графики позволяет создавать все графические объекты вне экрана, а затем переносить их на экран все сразу. Это делает анимацию более плавной и устраняет эффект мигания. В методе advance() (см код ниже) я показал как реализовать этот прием. (Метод advance() вызывается из главного цикла объектаGameThread.) Заметте, все что вам нужно сделать - это вызвать метод paint(getGraphics()), а затем flushGraphics(). Чтобы сделать Вашу программу более эффективной, существует версия метода flushGraphics(), который позволяет вам перерисовывать только часть экрана, если известно, что изменилась только эта часть. Я провел экспиремент: попробывал заменить вызовы методов paint(getGraphics()) и flushGraphics() на repaint(), а затем на serviceRepaints() (конечно вместоGameCanvas пришлось использовать Canvas). В моем простом примере значительных отличий не было отмечено, но если ваша игра имеет много различных графических элементов, то использованиеGameCanvas несомненно даст ощутимый прирост скорости.
Если Вы посмотрите на приведенный ниже код, то заметите, что после перерисовки экрана (метод advance()), я устанавливаю задержку в одну миллисекунду. Это необходимо для того, чтобы убедиться в том, что перерисовка завершена, прежде чем начинать следующую перерисовку. Кроме того это позволяет корректно обрабатывать события клавиатуры. Как я говорил выше, возможность обрабатывать состояние клавиш составляет еще одно различие междуGameCanvas и Canvas. При использовании Canvas, если вы хотите знать состояние клавиш, то должны выполнять метод keyPressed(int keyCode), который будет вызывать оболочка Java, когда захочет сообщить программе о нажатой клавише. При использованииGameCanvas, Вы можете вызвать getKeyStates() когда угодно, и узнать какая кнопка была нажата. Чтобы получить объективную информацию о деятельности пользователя в многопоточном приложении, необходимо делать небольшие задержки (достаточно миллисекунды) внутри потоков. (Как-то я написал игру без этой задержки. Игровой объект реагировал на мои действия с ощутимым опозданием.)
Очевидно, что эти два отличияGameCanvas улучшили контролируемость перерисовки и обработки клавиш. Возвращаясь к классуGameThread, заметьте, что главный игровой цикл сначала сообщает моему субклассуGameCanvas (названному JumpCanvas), чтобы опросить состояние клавиш (смотрите метод JumpCanvas.checkKeys()). Затем когда произойдет событие нажатия клавиши, главный цикл класса GameThread вызовет JumpCanvas.advance(), который велит LayerManager сделать соответствующие изменения в графике, а затем выполняет перерисовку экрана и задержку, как было описано выше.
Ниже приведен листинг класса JumpCanvas.java:
******************** Листинг JumpCanvas.java********************** package net.frog_parrot.jump; import javax.microedition.lcdui.*; import javax.microedition.lcdui.game.*; /** * Этот класс обеспечивает вывод игры на экран. * * @автор Carol Hamer */ publicclass JumpCanvasextends javax.microedition.lcdui.game.GameCanvas{ //--------------------------------------------------------- // поля размеров /** * высота зеленой области над экраном. */ static int GROUND_HEIGHT=32; /** * размер экрана. */ static int CORNER_X; static int CORNER_Y; static int DISP_WIDTH; static int DISP_HEIGHT; /** * Размер шрифта. */ static int FONT_HEIGHT; static int SCORE_WIDTH; /** * Шрифт по умолчанию. */ static Font FONT; /** * Ширина строки, отображающей время. */ static int TIME_WIDTH; //--------------------------------------------------------- // поля игровых объектов /** * Указатель экрана. */ Display myDisplay; /** * указатель на объект MIDlet-a (чтобы обработать системные клавиши) */ Jump myJump; /** * LayerManager который указывает игровую графику. */ JumpManager myManager; /** * Закончилась ли игра. */ static boolean myGameOver; /** * Счет игры. */ int myScore=0; /** * Через сколько тактов мы начинаем игру. */ int myInitialGameTicks=950; /** * Эта переменная учтанавливается, если строка времени * нуждается в пересчете */ int myOldGameTicks= myInitialGameTicks; /** * Число прошедших игровых тактов. */ int myGameTicks= myOldGameTicks; /** * Была или нет осуществлена отрисовка. */ boolean myInitialized; /** * Начальное значение строки времени. */ static String myInitialString="1:00"; /** * мы сохранили строку времени, чтобы избежать * ее пересоздание, когда этого не требуется. */ String myTimeString= myInitialString; //----------------------------------------------------- // gets/sets /** * Этот метод вызывается в конце игры. */ static void setGameOver(){ myGameOver=true; GameThread.requestStop(); } /** * Узнаем закончилась ли игра. */ static boolean getGameOver(){ return(myGameOver); } //----------------------------------------------------- // Инициализация и изменение состояния игры /** * Конструктор. */ public JumpCanvas(Jump midlet){ super(false); myDisplay= Display.getDisplay(midlet); myJump= midlet; } /** * Этот метод запускается как только приложение запущено. */ void start(){ myGameOver=false; myDisplay.setCurrent(this); repaint(); } /** * Устанавливаем все переенные. */ voidreset(){ myManager.reset(); myScore=0; myGameOver=false; myGameTicks= myInitialGameTicks; myOldGameTicks= myInitialGameTicks; repaint(); } /** * Очищаем значение кнопок. */ void flushKeys(){ getKeyStates(); } //------------------------------------------------------- // Графические методы /** * Рисуем игровую графику на экране. */ public void paint(Graphics g){ // если требуется, выполняем вычисления if(!myInitialized){ CORNER_X= g.getClipX(); CORNER_Y= g.getClipY(); DISP_WIDTH= g.getClipWidth(); DISP_HEIGHT= g.getClipHeight(); FONT= g.getFont(); FONT_HEIGHT= FONT.getHeight(); SCORE_WIDTH= FONT.stringWidth("Score: 000"); TIME_WIDTH= FONT.stringWidth("Time: "+ myInitialString); myInitialized=true; } // очищаем экран: g.setColor(0xffffff); g.fillRect(CORNER_X, CORNER_Y, DISP_WIDTH, DISP_HEIGHT); g.setColor(0x0000ff00); g.fillRect(CORNER_X, CORNER_Y+ DISP_HEIGHT- GROUND_HEIGHT, DISP_WIDTH, DISP_HEIGHT); // иоздаем и ресуем менеджер слоев: try{ if(myManager==null){ myManager=new JumpManager(CORNER_X, CORNER_Y+ FONT_HEIGHT*2, DISP_WIDTH, DISP_HEIGHT- FONT_HEIGHT*2- GROUND_HEIGHT); } myManager.paint(g); } catch(Exception e){ errorMsg(g, e); } // рисуем время и счет g.setColor(0); g.setFont(FONT); g.drawString("Score: "+ myScore, (DISP_WIDTH- SCORE_WIDTH)/2, DISP_HEIGHT+5- GROUND_HEIGHT, g.TOP|g.LEFT); g.drawString("Time: "+ formatTime(), (DISP_WIDTH- TIME_WIDTH)/2, CORNER_Y+ FONT_HEIGHT, g.TOP|g.LEFT); // Пишем Game Over если игра закончена if(myGameOver){ myJump.setNewCommand(); // очищаем верхнюю область экрана: g.setColor(0xffffff); g.fillRect(CORNER_X, CORNER_Y, DISP_WIDTH, FONT_HEIGHT*2+1); int goWidth= FONT.stringWidth("Game Over"); g.setColor(0); g.setFont(FONT); g.drawString("Game Over",(DISP_WIDTH- goWidth)/2, CORNER_Y+ FONT_HEIGHT, g.TOP|g.LEFT); } } /** * простая утилита, которая делает так, чтобы номер игроваого такта * имел формат времени */ public String formatTime(){ if((myGameTicks/16)+1!= myOldGameTicks){ myTimeString=""; myOldGameTicks=(myGameTicks/16)+1; int smallPart= myOldGameTicks%60; int bigPart= myOldGameTicks/60; myTimeString+= bigPart+":"; if(smallPart/10<1){ myTimeString+="0"; } myTimeString+= smallPart; } return(myTimeString); } //------------------------------------------------------- // движения /** * Говорим менеджеру слоев прдвинуть вперед слой, а затем обновить * экран. */ void advance(){ myGameTicks--; myScore+= myManager.advance(myGameTicks); if(myGameTicks==0){ setGameOver(); } // рисуем экран try{ paint(getGraphics()); flushGraphics(); } catch(Exception e){ errorMsg(e); } // делаем очень короткую задержку, которая позволяет другим // потокам обновить информацию о нажатых клавишах synchronized(this){ try{ wait(1); } catch(Exception e){} } } /** * реагируем на состояния клавиш. */ public void checkKeys(){ if(! myGameOver){ int keyState= getKeyStates(); if((keyState& LEFT_PRESSED)!=0){ myManager.setLeft(true); } if((keyState& RIGHT_PRESSED)!=0){ myManager.setLeft(false); } if((keyState& UP_PRESSED)!=0){ myManager.jump(); } } } //------------------------------------------------------- // обработка ошибок /** * Конвертируем исключительные ситуации в сообщения и выводим * их на экран */ void errorMsg(Exception e){ errorMsg(getGraphics(), e); flushGraphics(); } void errorMsg(Graphics g, Exception e){ if(e.getMessage()==null){ errorMsg(g, e.getClass().getName()); }else{ errorMsg(g, e.getClass().getName()+":"+ e.getMessage()); } } /** * Показать сообщение об ошибке, если что-то пошло не так. */ void errorMsg(Graphics g, String msg){ // очищаем экран g.setColor(0xffffff); g.fillRect(CORNER_X, CORNER_Y, DISP_WIDTH, DISP_HEIGHT); int msgWidth= FONT.stringWidth(msg); // Пишем сообщение красным g.setColor(0x00ff0000); g.setFont(FONT); g.drawString(msg,(DISP_WIDTH- msgWidth)/2, (DISP_HEIGHT- FONT_HEIGHT)/2, g.TOP|g.LEFT); myGameOver=true; } }
Автор Carol Hamer
Перевод: Alex.