Теперь, когда я рассмотрел специальные функции GameCanvas, я хочу затронуть основную функцииCanvas: отрисовку экрана. Эта функция возложена на метод paint(Grapics q), который Вы можете переопределить. ОбъектGraphics может потребовать определения резмера экрана. Обычно он испльзуется для рисования строк, рисунков, простых геометрических объектов(таких как прямоугольники). Этот объект имеет богатый набор методов для рисования различных объектов на экране. (Пакет javax.microedition.lcdui.Graphics очень похож на java.awt.graphics. Различие заключается в том, что для экономии памяти вместо того чтобы создавать индивидуальный объект для каждого отображаемого графического элемента, вызывается простой метод отрисовки.) Поскольку описание того, что вы можете нарисовать с помощью объектаGraphics, заняло бы у меня несколько страниц, я решил ограничиться рассмотрением лишь тех частей класса, которые я использовал в своей игре. Вам несомненно стоит заглянуть в документацию, поставляемую вместе с пакетом.
В своей игре я должен нарисовать ковбоя, который бежит по прерии и прыгает через колючки. Игровой экран должен выглядеть так:
Как видите, я рисую счет внизу экрана, а время сверху. когда ковбой бежит, экран двигается влево или вправо (поскольку экран маленький, то нереально разместить всю игровую зону на одном экране), но мне хочется, чтобы счет и таймер все время оставались на одном месте. Чтобы сделать это, в классе JumpCanvas рисуются стабильные полосы сверху и снизу и я делегирую интересуюцую графику в LayerManager (более подробно об этом написано в следующей секции).
Посмотрите на метод paint(Graphics q). Первым делом он использует объектGraphics чтобы получить размер экрана, а затем использует эту информацию чтобы вычислить где должны размещаться графические объекты. Следуя основополагающему принципуJava программирования: "напиши один раз и запускай на чем угодно" мы не привязываемся к фиксированному размеру экрана, а используем динамическую подстройку приложения под каждый конкретный экран. Таким образом ваша игра будет одинаково хорошо смотреться как на больших, так и на маленьких экранах. Вы можете также спровоцировать исключительную ситуацию (Exception) если размер экрана превышает разумные максимальные или минимальные размеры.
Обратите внимание, в методе paint после определения размера верхней и нижний области, я рисую белый и зеленый квадраты методом g.fillrect, а затем методом g.DrawString вывожу время и счет. После этого я вычисляю рамер области между этими квадратами и передаю полученное значение моему подклассу LayerManager.
Интересующие нас графические объекты в J2ME обычно представляются подклассом класса javax.mictoedition.lcdui.game.Layer. Слой заднего плана может потребовать javax.mictoedition.lcdui.game.TiledLayer, а игрок (и его противники) потребуют javax.mictoedition.lcdui.game.Sprite, оба эти класса являются подклассом Layer-а. КлассLayerManager поможет Вам организовать все эти графические слои. Порядок, в котором вы добавляете слои к LayerManager определяет очередность их отрисовки. (Первый добавленный слой рисуется в последнюю очередь. Верхние слои будут перекрывать нижние. Если Вы будете использовать рисунки с прозрачными регионами, то сможете увидеть часть нижних слоев.)
Вероятно наиболее полезный аспект классаLayerManager это то, что Вы можете создавать графические рисунки, которые намного больше, чем экран, а затем выбирать какую ее часть показать на экране. Представьте, что у Вас есть большой рисунок. Вы берете чистый лист бумаги, врезаете в нем прямоугольное окошко, накладываете этот лист на рисунок, и двигаете его. Большой рисунок это то, что вы можете передать LayerManager-у, а дырка в листе-экран мобильного телефона. Таким образом реализован виртуальный экран, который намного больше реального размера телефонного. Это означает, что вам придется иметь дело с двумя системами координат: виртуального экрана и экрана телефона. Описанный принцип очень часто используется при написании игр для мобильников и может сохранить вам огромное количество времени.
В своем примере я использую очень простой задний план, но я хочу чтобы ковбой все время находился в середине экрана не зависимо от того идет ли он вправо или влево, поэтому я должен непрерывно изменять видимую часть грайической площади LayerManager-а. Я делаю это, вызывая метод setViewWindow(int x, int y, int width, int height) из метода paint(Graphics g) моего подклассаLayerManager (называемый JumpManager). Рассмотрим более подробно, что же происходит: главный цикл в GameThread вызывает JumpCanvas.checkKeys(), который опрашивает состояние клавиатурыи говорит классу JumpManager должен ли ковбой идти вправо, влево или прыгать. JumpCanvas передает эту информацию JumpManager-у с помощью вызова метода setLeft(boolean left) или jump(). Если сообщение прыгать, JumpManager вызывает jump() для спрайта ковбоя (Sprite). Если сообщение идти влево (или вправо), то когда GameThread вызовет JumpCanvas, чтобы сообщить JumpManager-у произвести изменения (на следуещем витке цикла), JumpManager сообщает спрайту ковбоя подвинуться на один пиксель влево и выполняет компенсацию этого перемещения смещением видимой области виртуального экрана вправо. Таким образом, ковбой остается в центре экрана телефона. Эти два действия завершаются увелечением переменной myCurrentLeftX (которая представляет собой x координату, передаваемою методу setViewWindow(int x, int y, int width, int height)), а затем происходит вызов myCowboy.advance(gameTicks, myLeft). Конечно я мог удерживать ковбоя в центре экрана не двигая его и не прилагать его LayerManager-у, но лучше рисовать его отдельно, поскольку проще следить за всеми объектами, помещая все перемещения графики на один слой, а затем удерживать окно сфокусированным на спрайте ковбоя. Когда ковбою приходит приказ изменить свое положение, я также меняю положение спрайтов кустов и обновляю TiledLayer для травы, осуществляя таким образом ее анимацию. Затем я проверяю не столкнулся ли ковбой с кустом. Более подробно об этом я расскажу в следующем разделе. После осуществления перемещения игровых объектов, JumpManager вызывает метод wrap(), чтобы определить не достигло ли выводимое окно(видовое окно) края заднего плана. Если край достигнут, двигаем все отображаемые игровые объекты в центр игрового экрана и продолжаем задний план в обе стороны от центра. Затем JumpCanvas все перерисовывает и игровой цикл начинается снова.
Я скажу несколько слов о методе wrap(). Класс LayerManager к сожалению не имеет механизма закольцовки для случая в котором у Вас есть простой циклический задний план, который можно двигать бесконечно. LayerManager прибегает к услугам wrap, когда посланные в setViewWindow(int x, int y, int width, int height) координаты превышают значение Integer.MAX_VALUE. Нам придется написать собственную функцию, чтобы предупредить sprite игрока о том, что он зашел за край заднего плана. В моем преиере трава на заднем плане перерисовывается после нескольких пикселей, полученных от Grass.TILE_WIDTH*Grass.CYCLE. Так что когда координата x видового окна (myCurrentLeftX) достигает значения, равного ширене заднего плана, я перемещаю видовое окно назад в центр виртуального экрана и двигаю все спрайты в том же направлении.
Привожу код JumpManager.java:
package net.frog_parrot.jump; import javax.microedition.lcdui.*; import javax.microedition.lcdui.game.*; publicclass JumpManagerextends javax.microedition.lcdui.game.LayerManager{ //--------------------------------------------------------- // определение переменных // (после инициализации) /** * Координать места игрового холста, где должно появиться окно * LayerManager - а. */ static int CANVAS_X; static int CANVAS_Y; /** * Ширина экрана. */ static int DISP_WIDTH; /** * Высота объекта графического региона. В данном случае она * соответствует высоте видимой части, поскольку прокрутка * экрана производится только по горизонтали. */ static int DISP_HEIGHT; //--------------------------------------------------------- // переменные игровых объектов /** * Объект игрока. */ Cowboy myCowboy; /** * кустики, которые выкатывается слева. */ Tumbleweed[] myLeftTumbleweeds; /** * кустики, которые выкатываются справа. */ Tumbleweed[] myRightTumbleweeds; /** * Объект, представляющий траву на заднем плане. */ Grass myGrass; /** * Показывает, идет ли игрок влево. */ boolean myLeft; /** * Самая левая координата, которая должна быть показана. */ int myCurrentLeftX; /** * Говорим пользователю повернуться налево или направа. */ void setLeft(boolean left){ myLeft= left; } //----------------------------------------------------- // Инициализация и изменение состояния игры /** * Конструктор задает данные. * x, y - координаты места на игровом холсте, где должно появиться * окно LayerManager-а * width, height - ширина и высота региона, доступного * LayerManager-у */ public JumpManager(int x, int y, int width, int height){ CANVAS_X= x; CANVAS_Y= y; DISP_WIDTH= width; DISP_HEIGHT= height; myCurrentLeftX= Grass.CYCLE*Grass.TILE_WIDTH; setViewWindow(0,0, DISP_WIDTH, DISP_HEIGHT); } /** * Устанавливаем все переменные обратно в значения по умолчанию. */ voidreset(){ if(myGrass!=null){ myGrass.reset(); } if(myCowboy!=null){ myCowboy.reset(); } if(myLeftTumbleweeds!=null){ for(int i=0; i< myLeftTumbleweeds.length; i++){ myLeftTumbleweeds[i].reset(); } } if(myRightTumbleweeds!=null){ for(int i=0; i< myRightTumbleweeds.length; i++){ myRightTumbleweeds[i].reset(); } } myLeft=false; myCurrentLeftX= Grass.CYCLE*Grass.TILE_WIDTH; } //------------------------------------------------------- // графические методы /** * Рисуем игровую графику на экране. * Сюда же включен код инициализации, поскольку размер * экрана требует ининциализации. */ public void paint(Graphics g) throws Exception{ // создаем игрока: if(myCowboy==null){ myCowboy=new Cowboy(myCurrentLeftX+ DISP_WIDTH/2, DISP_HEIGHT- Cowboy.HEIGHT-2); append(myCowboy); } // создаем кустик: if(myLeftTumbleweeds==null){ myLeftTumbleweeds=new Tumbleweed[2]; for(int i=0; i< myLeftTumbleweeds.length; i++){ myLeftTumbleweeds[i]=new Tumbleweed(true); append(myLeftTumbleweeds[i]); } } if(myRightTumbleweeds==null){ myRightTumbleweeds=new Tumbleweed[2]; for(int i=0; i< myRightTumbleweeds.length; i++){ myRightTumbleweeds[i]=new Tumbleweed(false); append(myRightTumbleweeds[i]); } } // создаем объект заднего плана: if(myGrass==null){ myGrass=new Grass(); append(myGrass); } // это главная часть метода: // Мы показываем, какая часть площади LayerManager-а должна быть // выведена на экран и затем рисуем ее. Вызов метода paint() // обеспечивает перерисовку всех видимых слоев. setViewWindow(myCurrentLeftX,0, DISP_WIDTH, DISP_HEIGHT); paint(g, CANVAS_X, CANVAS_Y); } /** * Если ковбой добрался до края графического региона, двигаем все * части, чтобы можно было сделать закольцовку экрана. */ void wrap(){ if(myCurrentLeftX%(Grass.TILE_WIDTH*Grass.CYCLE)==0){ if(myLeft){ myCowboy.move(Grass.TILE_WIDTH*Grass.CYCLE,0); myCurrentLeftX+=(Grass.TILE_WIDTH*Grass.CYCLE); for(int i=0; i< myLeftTumbleweeds.length; i++){ myLeftTumbleweeds[i].move(Grass.TILE_WIDTH*Grass.CYCLE,0); } for(int i=0; i< myRightTumbleweeds.length; i++){ myRightTumbleweeds[i].move(Grass.TILE_WIDTH*Grass.CYCLE,0); } }else{ myCowboy.move(-(Grass.TILE_WIDTH*Grass.CYCLE),0); myCurrentLeftX-=(Grass.TILE_WIDTH*Grass.CYCLE); for(int i=0; i< myLeftTumbleweeds.length; i++){ myLeftTumbleweeds[i].move(-Grass.TILE_WIDTH*Grass.CYCLE,0); } for(int i=0; i< myRightTumbleweeds.length; i++){ myRightTumbleweeds[i].move(-Grass.TILE_WIDTH*Grass.CYCLE,0); } } } } //------------------------------------------------------- // Игровые движения /** * Приказать всем движущимся компанентам обновить свои состояния. * gameTicks - определяет время которое главный цикл будет * выполняться, пока игра не закончится. * Возвращает изменения в счете после обновления состояний. */ int advance(int gameTicks){ int retVal=0; // двигаем видовое окно if(myLeft){ myCurrentLeftX--; }else{ myCurrentLeftX++; } // Заставляем игровые объекты двигаться согласно перемещению //видового окна. myGrass.advance(gameTicks); myCowboy.advance(gameTicks, myLeft); for(int i=0; i< myLeftTumbleweeds.length; i++){ retVal+= myLeftTumbleweeds[i].advance(myCowboy, gameTicks, myLeft, myCurrentLeftX, myCurrentLeftX+ DISP_WIDTH); retVal-= myCowboy.checkCollision(myLeftTumbleweeds[i]); } for(int i=0; i< myLeftTumbleweeds.length; i++){ retVal+= myRightTumbleweeds[i].advance(myCowboy, gameTicks, myLeft, myCurrentLeftX, myCurrentLeftX+ DISP_WIDTH); retVal-= myCowboy.checkCollision(myRightTumbleweeds[i]); } // теперь мы проверяем не достигли ли мы края видимой области // и если это так, перемещаем эту область и все объекты, // обеспечивая закольцовку экрана. wrap(); return(retVal); } /** * Просим ковбоя прыгнуть.. */ void jump(){ myCowboy.jump(); } }
Автор Carol Hamer
Перевод: Alex.