Пишем движок игры под Android – tutorial часть 5 (Surface)

Наш игровой движок для 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;
	}

}

Мы просто создаем в конструкторе экземпляр mDrawerTaskdrawer, запускаем его когда создается поврехность:  t.scheduleAtFixedRate(drawer, 0, mSettings.frameInterval), и останавливаем таймер, когда поверхность уничтожается:  t.cancel(). Все остальное — просто обвязка для этого действа.

На этом работу над первой альфа версией движка mEngine для Android можно считать оконченной,  всем спасибо, все свободны!

Собсно весь мой проект из Eclipse можно взять здесь:mEngine. Если есть предложения по доработке этой штуки с удовольствием буду ждать. Коменты пишите ниже, буду рад пообщаться с теми кто в теме намного глубже меня и ткнет мне носом во все мои ляпы.

Здесь можно посмотреть первый опыт написания игры на этом движке.

Вам понравилось? Было полезно? Поделитесь!

Опубликовать в Facebook
Опубликовать в Google Buzz
Опубликовать в Google Plus
Опубликовать в LiveJournal
Опубликовать в Мой Мир
Опубликовать в Одноклассники
Опубликовать в Яндекс
Запись опубликована в рубрике Android, Программирование с метками , , , , , , , . Добавьте в закладки постоянную ссылку.

17 Responses to Пишем движок игры под Android – tutorial часть 5 (Surface)

  1. Сапфил говорит:

    …Мысленно стучусь головой о стену…
    А все потому, что эклипс ругается на строчки @override перед методами surfaceChanged
    surfaceCrested
    surfacedestroyed

    И настойчиво предлагает их убрать. Такое впечателние, что он думает, что это просто мои собственные методы, взятые чисто из моей головы. И никакого отношения к интерфейсу SurfaceHolder.Callback они не имеют.
    ОДНАКО! Если полностью удалить из кода любую из функций — эклипс начинает тут же ругаться в строчке конструктора и требовать реализации удаленной функции,ю ссылаясь опять же на SurfaceHolder.Callback. Возможно, если функция выше была объявлена как абстрактная, до для ее конкретной реализации не требуется @override. Если так, то почему у автора все работает с этими злосчастными строчками?
    Я чувствую, что понимание всех этих штуковин «extends», «implements», «@ override» обязательно необходимо для кодинга на Яве. Потому и паникую 🙁

  2. Alexander говорит:

    Для Сапфил.
    Когда напишешь public class mSurfaceView extends SurfaceView implements SurfaceHolder.Callback на mSurfaceView появится красная линия, нажимаешь на mSurfaceView и на выпадающем списке появится Add implements, выбери его и тут автоматом тебе построит методы @override surfaceChanged
    surfaceCrested
    surfacedestroyed. Возможно ты этого хотел спросить?

  3. Евген говорит:

    Если еще актуально.
    Автор просто зря там влепил @override оно там не нужно,
    потому что @override указывает на то что данная процедура перекрывает процедуру суперкласса (SurfaceView) с таким же названием.
    а у SurfaceView таких процедур нет, мы их сами придумали — поэтому ничего перекрывать не нужно, и @override не требуется.

    В ранних версиях Eclipse это не считалось ошибкой, а в текущей пишет ошибку. просто уберите @override — это правильно.

    • Anatolij говорит:

      «а у SurfaceView таких процедур нет, мы их сами придумали — поэтому ничего перекрывать не нужно, и @override не требуется»
      Мы их не придумали! mSurfaceView реализует интерфейс SurfaceHolder.Callback. Это значит, что он ДОЛЖЕН реализовать методы этого интерфейса: surfaceChanged, surfaceCreated и surfaceDestroyed. Если их убрать — то эклипс будет ругаться на то что мы их не определили, если пометить их как override, то будет ругаться что в класса-предка нет таких методов

  4. Серг говорит:

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

    • davidmd говорит:

      Хм… У меня счас со временм ооочень не хорошо… Я все никак не могу даже готовые статьи выложить…

  5. Serg_HIS говорит:

    Большое спасибо за статьи.
    За очень простой и доходчивый код.
    Буквально за пару-тройку дней «наваял» на основе ваших пятнашек, свои (более презентабельные, на мой взгляд).
    С учётом, что я до этого под Android не программировал вообще, то очень даже годные у вас примеры.

    Пришлось чуток поковырять движок. Не без ошибок он. Но для старта самое оно! Подправил несколько ошибок, уже внёс несколько дополнений и в планах есть ещё чего подправить\дополнить для моих других разработок. Если прикрутить к OpenGL — будет вообще красотища.

    • davidmd говорит:

      Рад что вам понравилось 🙂

      • Serg_HIS говорит:

        Если Вы ещё не забросили свой движок, можем обменяться идеями.

        Я существенно расширил возможности добавив риалтайм обработку передвижений и вращений, сейчас доделываю деформации, эвенты на нажатия, т.п.

        Хоть от Вашего движка остались только базовые классы элементов, но всё равно, сподвигли меня на глобальные разработки Ваши туториалы. Очень нуждаются в планах переработки — примитивы. Физику соударений начал продумывать. Позже наверное таки переведу на OpenGL (наверное) и добавлю 3Д (наверное).

        Распараллеливание процессов в движке… Ну конечно же нужно будет сделать… Потом…

      • Serg_HIS говорит:

        Наконец дошли руки задействовать Hardware acceleration.
        Получаю кучу наслаждения от 60 fps.
        К тому же, стал заметно экономичнее работать движок в плане потребления электричества (в разы), при этом можно на порядки больше рисовать элементов в кадре не теряя fps.

        Завтра для тестов нарисую сотни элементов с заданием правил движения, вращения и изменения цвета/прозрачности с текстурой.

        Позже в планах сделать адаптирование фреймрейта под разные вариации состояния сцены (для экономии электроэнергии).

      • Serg_HIS говорит:

        Уже делаю псевдо 3D для технологии VR

  6. Serg_HIS говорит:

    Вполне так получается VR версия. Немного доработок с меню и с сенсорами положения устройства.

    Если хозяин сайта разрешит, Расположу ссылку.

    • davidmd говорит:

      Конечно!

      • Serg_HIS говорит:

        Спасибо большое за разрешение.
        Вот доделываю конструктор меню, который закладывается как часть движка.
        Надеюсь скоро будет релиз.
        VR будет чуть позже, продумываю удобное управление для этого варианта, а изображение вполне так объёмное.

      • Serg_HIS говорит:

        Вот сделал релиз своих Новогодних Гипер Пятнашек.
        https://play.google.com/store/apps/details?id=com.his.photofifteen

        Всех с наступающим Новым Годом.

        Автору сайта спасибо за уроки по Андроид системам.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *