Наш игровой движок для Android все ширится и ширится. Последнее что мы реализовали — это слои и сцену. Ну а сегодня на мой взгляд самая сложная и самая интересная его часть — собственно отрисовка.
Начать надо с того что в Android для отрисовки есть специальный компонент интерфейса, который предоставляет огромный набор возможностей для рисования. Этот компонент SurfaceView. Он заботится о создании поверхности для рисования, у него же мы можем получить канву для рисования и т.д. Поэтому отрисовку мы с Вами будем производить именно на основе такого компонента.
Ну давайте приступим! Создадим класс mSurfaceView и в качетсве его родителя укажем SurfaceView. Выглядит как-то так:
package ru.davidmd.myengine; import android.content.Context; import android.view.SurfaceView; public class mSurfaceView extends SurfaceView { public mSurfaceView(Context context) { super(context); } }
Однако наш mSurfaceView должен быть связан со сценой которую мы хотим на нем отрисовывать, поэтому добавим в конструктор второй параметр — собственно саму сцену:
mScene scene; public mSurfaceView(Context context, mScene s) { super(context); scene = s; }
Теперь давайте разберемся каким образом происходит управление нашим mSurfaceView. Для этого есть SurfaceHolder — это как раз тот интерфейс, который нужен нам для рисования. У этого интерфейса есть метод lockCanvas() — который возвращает объект Canvas (канву для рисования). Ну а как же контролировать состояние самого Surface? А вот для этих целей есть интерфейс SurfaceHolder.Callback. Любой объект который его реализует будет получать возможность контролировать наш Surface. Поэтому мы реализуем этот интерфейс в самам классе mSurfaceView. Выглядит это вот так:
public class mSurfaceView extends SurfaceView implements SurfaceHolder.Callback{ mScene scene; public mSurfaceView(Context context, mScene s) { super(context); scene = s; } @Override public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) { // TODO Auto-generated method stub } @Override public void surfaceCreated(SurfaceHolder arg0) { // TODO Auto-generated method stub } @Override public void surfaceDestroyed(SurfaceHolder arg0) { // TODO Auto-generated method stub } }
Теперь у нас есть методы, которые позволяют контролировать Surface: surfaceCreated вызывается при создании поверхности, surfaceDestroyed — при ее уничтожении, а surfaceChanged — при ее изменении. Однако они никак не связаны с конкретным Surface (несмотря на то что описаны в одном классе). Поэтому в конструкторе добавляем следующую строку, которая добавляет Callback холдеру нашей поверхности:
this.getHolder().addCallback(this);
Profit! Теперь каждый метод будет вызываться в нужный момент! :-). В общем основу мы подготовили.
Давайте попробуем разобраться с тем, как именно мы будем отрисовавывать нашу сцену. Тут есть несколько вариантов. Для совсем простых игр можно перерисовывать сцену только когда в ней произошли какие-то изменения. То есть например пользователь провел пальцем по экрану и мы перерисовали сцену, а все остальное время на экране статическая картинка. Но мы ведь еще не знаем какие игры будем писать на нашем движке, поэтому такой вариант не подходит. Поэтому сначала я попробовал создать поток, который отрисовывает всю сцену непрерывно. То есть просто бесконечный цикл в котором повторяется отрисовка сцены. Все бы здорово, но у такого варианта есть один большой минус — непрерывная отрисовка постоянно загружает процессор. У меня имеется два устройства на андроиде (Acer Liquid и Galaxy Tab) и оба этих устройства накалялись до предела уже через пять — десять минут такого беспредела. Ну еще минус: в такой ситуации мы вообще никак не можем контролировать framerate (частоту кадров). Поэтому как раз и было решено воспользоваться объектом Timer (о нем мы упоминали вот здесь). Это обычный таймер, который в нужное время или просто с определенным интервалом запускает какие-то задание. Само задание описывается классом TimerTask. Поэтому мы создадим новый класс mDrawerTask — который является наследником TimerTask и будет отвечать у нас за перерисовку сцены. Для этого в конструкторе мы передадим ему саму сцену, и переопределим метод run() (именно он и выполняется когда мы запускаем задание в таймере). Вот код этого класса:
package ru.davidmd.myengine; import java.util.TimerTask; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.Log; import android.view.SurfaceHolder; public class mDrawerTask extends TimerTask { SurfaceHolder holder; Paint mainPaint; mScene scene; mLayer layer; Canvas canvas; int k; mDrawerTask(SurfaceHolder h, mScene s) { this.holder = h; mainPaint = new Paint(); this.scene = s; } @Override public void run() { try { canvas = null; canvas = holder.lockCanvas(); canvas.drawRGB(0, 0, 0); for (int l = 0; l < scene.getLayCount(); l++) { layer = scene.getLayerByNum(l); if (layer != null) { mainPaint = layer.p; for (mBasic tmp : layer.data) { tmp.draw(canvas, mainPaint); } } } scene.update(); mSettings.newFrame(); } catch (Exception e) { } finally { if (canvas != null) { holder.unlockCanvasAndPost(canvas); } } } }
И что же мы здесь делаем? В конструктор мы получаем сцену и сохраняем ее в переменную scene, а так же SurfaceHolder. А в методе run() с помощью холдера блокируем канву (чтоб на ней никто не мог рисовать пока мы сами ее не разблокируем) и отрисовываем всю сцену, последовательно для каждого ее объекта вызывая метод draw(Canvas с Paint p) при этом Paint мы каждый раз берем из настроек слоя. Ну а canvas.drawRGB(0, 0, 0) — вызываемый перед отрисовкой предварительно закрашивает всю сцену черным цветом.
Теперь, кстати, вернемся к таймеру, который мы описали в классе mSettings в третьем туториале. Чем занимается он? А он считает реальное количество кадров в секунду. Для этого в коде выше мы вызываем метод mSettings.newFrame() — который увеличивает счетчик кадров. А потом каждую секунду (как раз по тому самому загадочному таймеру 🙂 ) мы получаем готовый фреймрейт! Ура, работает! Ну то есть пока что ничего не работает, но я то уже все протестил, так что все прекрасно работает.
Теперь возвращаемся к нашему классу mSurfaceView собственно вот он весь целиком:
package ru.davidmd.myengine; import java.util.Timer; import android.content.Context; import android.graphics.Canvas; import android.view.SurfaceHolder; import android.view.SurfaceView; public class mSurfaceView extends SurfaceView implements SurfaceHolder.Callback { /** * сцена */ mScene scene; /** * отрисовщик */ mDrawerTask drawer; /** * канва */ Canvas canv; Timer t=new Timer(); /** * конструктор * * @param context * - ну как обычно контекст * @param s * - сцена ассоциированная с поверхностью */ public mSurfaceView(Context context, mScene s) { super(context); scene = s; drawer = new mDrawerTask(this.getHolder(), this.scene); this.getHolder().addCallback(this); } @Override public void surfaceChanged(SurfaceHolder arg0, int arg1, int w, int h) { scene.setWH(w, h); mSettings.GenerateSettings(w, h); } @Override public void surfaceCreated(SurfaceHolder arg0) { t.scheduleAtFixedRate(drawer, 0, mSettings.frameInterval); canv = this.getHolder().lockCanvas(); scene.setWH(canv.getWidth(), canv.getHeight()); this.getHolder().unlockCanvasAndPost(canv); } @Override public void surfaceDestroyed(SurfaceHolder arg0) { t.cancel(); } /** * устанавливает для поверхности новую сцену * * @param scene * новая сцена */ public void setScene(mScene scene) { this.scene = scene; } /** * Возвращает текущую сцену * * @return возвращает текущую сцену */ public mScene getScene() { return scene; } }
Мы просто создаем в конструкторе экземпляр mDrawerTask — drawer, запускаем его когда создается поврехность: t.scheduleAtFixedRate(drawer, 0, mSettings.frameInterval), и останавливаем таймер, когда поверхность уничтожается: t.cancel(). Все остальное — просто обвязка для этого действа.
На этом работу над первой альфа версией движка mEngine для Android можно считать оконченной, всем спасибо, все свободны!
Собсно весь мой проект из Eclipse можно взять здесь:mEngine. Если есть предложения по доработке этой штуки с удовольствием буду ждать. Коменты пишите ниже, буду рад пообщаться с теми кто в теме намного глубже меня и ткнет мне носом во все мои ляпы.
Здесь можно посмотреть первый опыт написания игры на этом движке.
…Мысленно стучусь головой о стену…
А все потому, что эклипс ругается на строчки @override перед методами surfaceChanged
surfaceCrested
surfacedestroyed
И настойчиво предлагает их убрать. Такое впечателние, что он думает, что это просто мои собственные методы, взятые чисто из моей головы. И никакого отношения к интерфейсу SurfaceHolder.Callback они не имеют.
ОДНАКО! Если полностью удалить из кода любую из функций — эклипс начинает тут же ругаться в строчке конструктора и требовать реализации удаленной функции,ю ссылаясь опять же на SurfaceHolder.Callback. Возможно, если функция выше была объявлена как абстрактная, до для ее конкретной реализации не требуется @override. Если так, то почему у автора все работает с этими злосчастными строчками?
Я чувствую, что понимание всех этих штуковин «extends», «implements», «@ override» обязательно необходимо для кодинга на Яве. Потому и паникую 🙁
Хм… Что самое забавное — я тоже с этим бился, когда перенес код из под линуха в винду… В итоге просто удалил директивы @override и все заработало… Хотя это не нормально…
Я снова столкнулся с этой проблемой уже в другом месте. И нашел-таки как ее решить. Вот что я нашел в интернетах на эту тему:
Go to your project/ide preferences and set the java compiler level to 1.6 and also make sure you select JRE 1.6 to execute your program from eclipse.
Взято отсюда: http://stackoverflow.com/questions/1678122/must-override-a-superclass-method-errors-after-importing-a-project-into-eclips
Для Сапфил.
Когда напишешь public class mSurfaceView extends SurfaceView implements SurfaceHolder.Callback на mSurfaceView появится красная линия, нажимаешь на mSurfaceView и на выпадающем списке появится Add implements, выбери его и тут автоматом тебе построит методы @override surfaceChanged
surfaceCrested
surfacedestroyed. Возможно ты этого хотел спросить?
Если еще актуально.
Автор просто зря там влепил @override оно там не нужно,
потому что @override указывает на то что данная процедура перекрывает процедуру суперкласса (SurfaceView) с таким же названием.
а у SurfaceView таких процедур нет, мы их сами придумали — поэтому ничего перекрывать не нужно, и @override не требуется.
В ранних версиях Eclipse это не считалось ошибкой, а в текущей пишет ошибку. просто уберите @override — это правильно.
«а у SurfaceView таких процедур нет, мы их сами придумали — поэтому ничего перекрывать не нужно, и @override не требуется»
Мы их не придумали! mSurfaceView реализует интерфейс SurfaceHolder.Callback. Это значит, что он ДОЛЖЕН реализовать методы этого интерфейса: surfaceChanged, surfaceCreated и surfaceDestroyed. Если их убрать — то эклипс будет ругаться на то что мы их не определили, если пометить их как override, то будет ругаться что в класса-предка нет таких методов
Когда начал читать то понял что слишком сложно для новичка понять,как происходит отрисовка и т.д.Хотелось бы попросить отдельную статью с маленьким кодом,где происходит какая нибудь совсем маленькая штучка,например,при нажатии остается точка на экране,либо линия.И как сделать что бы программно вывести точку или линию,что то вроде человечка нарисовать или много человечков сделав анимацию,и что бы он мог перемещаться по экрану,как бы ходить куда тыкнешь.Но хотелось бы отдельно понять как происходит прорисовка и как происходит отработка действия,заставить его что то сделать.Вот был бы очень признателен за такую информацию,так сказать без лишнего кода,так как сразу ткой движок не осилить начинающему.человечек может состоять из кружка и палочек
Хм… У меня счас со временм ооочень не хорошо… Я все никак не могу даже готовые статьи выложить…
Большое спасибо за статьи.
За очень простой и доходчивый код.
Буквально за пару-тройку дней «наваял» на основе ваших пятнашек, свои (более презентабельные, на мой взгляд).
С учётом, что я до этого под Android не программировал вообще, то очень даже годные у вас примеры.
Пришлось чуток поковырять движок. Не без ошибок он. Но для старта самое оно! Подправил несколько ошибок, уже внёс несколько дополнений и в планах есть ещё чего подправить\дополнить для моих других разработок. Если прикрутить к OpenGL — будет вообще красотища.
Рад что вам понравилось 🙂
Если Вы ещё не забросили свой движок, можем обменяться идеями.
Я существенно расширил возможности добавив риалтайм обработку передвижений и вращений, сейчас доделываю деформации, эвенты на нажатия, т.п.
Хоть от Вашего движка остались только базовые классы элементов, но всё равно, сподвигли меня на глобальные разработки Ваши туториалы. Очень нуждаются в планах переработки — примитивы. Физику соударений начал продумывать. Позже наверное таки переведу на OpenGL (наверное) и добавлю 3Д (наверное).
Распараллеливание процессов в движке… Ну конечно же нужно будет сделать… Потом…
Наконец дошли руки задействовать Hardware acceleration.
Получаю кучу наслаждения от 60 fps.
К тому же, стал заметно экономичнее работать движок в плане потребления электричества (в разы), при этом можно на порядки больше рисовать элементов в кадре не теряя fps.
Завтра для тестов нарисую сотни элементов с заданием правил движения, вращения и изменения цвета/прозрачности с текстурой.
Позже в планах сделать адаптирование фреймрейта под разные вариации состояния сцены (для экономии электроэнергии).
Уже делаю псевдо 3D для технологии VR
Вполне так получается VR версия. Немного доработок с меню и с сенсорами положения устройства.
Если хозяин сайта разрешит, Расположу ссылку.
Конечно!
Спасибо большое за разрешение.
Вот доделываю конструктор меню, который закладывается как часть движка.
Надеюсь скоро будет релиз.
VR будет чуть позже, продумываю удобное управление для этого варианта, а изображение вполне так объёмное.
Вот сделал релиз своих Новогодних Гипер Пятнашек.
https://play.google.com/store/apps/details?id=com.his.photofifteen
Всех с наступающим Новым Годом.
Автору сайта спасибо за уроки по Андроид системам.