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

В прошлый раз мы с Вами занимались всякими простенькими геометрическими объектами. Конечно они тоже нужны, но красивую игру с ними не сделаешь. Так что думаю самое время перейти к более серьезным вещам и заняться спрайтами. Что такое спрайт? ну это некоторое растровое изображение, которое можно перемещать по экрану из кода нашей программы. На самом деле грамотно используя спрайтовую графику можно создавать очень даже приятные игры. Работать со спрайтами в Andriod на самом деле очень легко. Для нас в SDK есть целая куча классов, которые спешат прийти на помощь. Например класс Bitmap. Вот уж воистину замечательное подспорье! Посмотрим чем он может нам помочь?Начнем описывать класс mSimpleSprite с того, что для разнообразия унаследуем его от mPoint. Таким образом каждый спрайт у нас уже имеет некоторую позицию на экране. И не только позицию, но еще и скорость движения и ускорение!

 

package ru.davidmd.myengine;

import java.io.IOException;

import android.content.res.AssetManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.util.Log;

/**
 * @author davidmd
 * @version 0.1 Класс для работы со спрайтами унаследован от {@link mPoint}
 */
public class mSimpleSprite extends mPoint {

	/**
	 * Bitmap спрайта
	 */
	Bitmap bmp;
	/**
	 * положение оси вращения спрайта пока не реализовано
	 */
	float rpx = 0, rpy = 0;
	/**
	 * Ширина (width) и высота (height) спрайта
	 */
	int width = 0, height = 0;
	/**
	 * Является ли центр спрайта его осью вращения пока не реализовано
	 */
	boolean centralAxis = true;
	/**
	 * Матрица для отрисовки спрайта на канве
	 */
	Matrix matrix=new Matrix();

	boolean selected=false;

	private void AutoSize()
	{
		if (mSettings.AutoScale)
		{
			this.resize(mSettings.ScaleFactorX, mSettings.ScaleFactorY);
		}
		this.refreshAll();
	}

	/**
	 * Конструктор
	 *
	 * @param x
	 *            положение спрайта по оси x
	 * @param y
	 *            положение спрайта по оси y
	 * @param bmp
	 *            битмап спрайта
	 */
	public mSimpleSprite(float x, float y, Bitmap bmp) {
		super(x, y);
		this.bmp = bmp;
		this.type = mBasic.TYPE_SIMPLESPRITE;
		AutoSize();
	}

	/**
	 * Конструктор загружает спрайт из файла
	 *
	 * @param x
	 *            положение спрайта по оси x
	 * @param y
	 *            положение спрайта по оси y
	 * @param bmp
	 *            битмап спрайта
	 */
	public mSimpleSprite(int x, int y, Bitmap bmp) {
		super(x, y);
		this.bmp = bmp;
		this.type = mBasic.TYPE_SIMPLESPRITE;
		AutoSize();
	}

	/**
	 * Конструктор загружает спрайт из файла
	 *
	 * @param s
	 *            путь к файлу из которого будет загружен спрайт
	 */
	public mSimpleSprite(String s) {
		super(0, 0);
		this.bmp = BitmapFactory.decodeFile(s);
		this.type = mBasic.TYPE_SIMPLESPRITE;
		AutoSize();
	}

	/**
	 * конструктор агружает спрайт из ассетов
	 *
	 * @param s
	 *            путь к файлу в папке assets откуда должен быть загружен спрайт
	 * @param am
	 *            - экземпляр {@link AssetManager}
	 */
	public mSimpleSprite(String s, AssetManager am) {

		super(0, 0);
		try {
			this.bmp = BitmapFactory.decodeStream(am.open(s));
		} catch (IOException e) {
			bmp = null;
		}
		this.type = mBasic.TYPE_SIMPLESPRITE;
		AutoSize();
		Log.d("mSimpleSprite", "SRITE "+s+" LOADED");
	}

	/**
	 * Констрктор загружает спрайт из ресурсов
	 *
	 * @param Res
	 *            собственно сами ресурсы
	 * @param ID
	 *            - указатель на сам спрайт
	 */
	public mSimpleSprite(Resources Res, int ID) {
		super(0, 0);
		this.bmp = BitmapFactory.decodeResource(Res, ID);
		this.type = mBasic.TYPE_SIMPLESPRITE;
	}

	/**
	 * Изменяет размер битмапа в спрайте. например для поддержки разных
	 * разрешений экрана с одними и теми же ресурсами
	 *
	 * @param newx
	 *            новая ширина в пикселах
	 * @param newy
	 *            новая высота в пикселах
	 */
	public void resize(int newx, int newy) {
		Bitmap tmp = Bitmap.createScaledBitmap(bmp, newx, newy, true);
		bmp = tmp;
		this.refreshAll();
	}

	/**
	 * Изменяет размер битмапа в спрайте. например для поддержки разных
	 * разрешений экрана с одними и теми же ресурсами
	 * @param newx - множитель по ширине
	 * @param newy - множитель по высоте
	 */
	public void resize(float newx, float newy) {
		int nx;
		int ny;
		this.refreshAll();
		nx = (int)(this.width*newx);
		ny = (int)(this.height*newy);
		Bitmap tmp = Bitmap.createScaledBitmap(bmp, nx, ny, true);
		bmp = tmp;
		this.refreshAll();
	}

	/**
	 * метод обновляет ширину, и высоту изображения вызывается после загрузки
	 * спрайта и после изменения его размера. Также обновляет матрицу отрисовки
	 */
	public void refreshAll() {
		if (bmp != null) {
			this.width = bmp.getWidth();
			this.height = bmp.getHeight();
			matrix.setTranslate(this.x, this.y);
		}
	}

	/**
	 * @return Возвращает матрицу
	 */
	public Matrix getMatrix() {
		return matrix;
	}

	/**
	 * @param matrix
	 *            устанавливает матрицу
	 */
	public void setMatrix(Matrix matrix) {
		this.matrix = matrix;
	}

	/**
	 * @return Возвращает ширину изображения
	 * загруженного в спрайт
	 */
	public int getWidth() {
		return width;
	}

	/**
	 * @return Возвращает высоту изображения
	 * загруженного в спрайт
	 */
	public int getHeight() {
		return height;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see ru.davidmd.myengine.mPoint#setX(float) перегруженный метод установки
	 * нового положения по x c обновлением матрицы
	 */
	@Override
	public void setX(float x) {
		super.setX(x);
		refreshAll();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see ru.davidmd.myengine.mPoint#setY(float) перегруженный метод установки
	 * нового положения по y c обновлением матрицы
	 */
	@Override
	public void setY(float y) {
		super.setY(y);
		refreshAll();
	}

	/**
	 * Устанавливает новое положение спрайта
	 *
	 * @param x
	 * @param y
	 */
	public void setXY(float x, float y) {
		super.setY(y);
		super.setX(x);
		refreshAll();
	}

	/* (non-Javadoc)
	 * @see ru.davidmd.myengine.mPoint#isSelected(float, float)
	 */
	@Override
	public boolean isSelected(float x, float y) {
		selected = false;
		if (x > this.x && x < (this.x + this.width) && y > this.y
				&& y < (this.y + this.height)) {
			selected = true;
		}
		return selected;
	}

	/**
	 * @return озвращает истину если спрайт выбран и ложь если нет
	 */
	public boolean isSelected() {
		return selected;
	}

	/* (non-Javadoc)
	 * @see ru.davidmd.myengine.mPoint#draw(android.graphics.Canvas, android.graphics.Paint)
	 */
	@Override
	public void draw(Canvas c, Paint p)
	{
		c.drawBitmap(bmp, matrix, p);
	}
}

Собственно вот весь код, и давайте теперь разбираться что здесь к чему и почему. Как же работает наш спрайт и какие у него есть возможности?

Начнем как обычно с полей класса. Ну самое главное — это сама картинка которая будет храниться в поле bmp, которое есть не что иное как экземпляр класса Bitmap. Также здесь у нас есть поля width и height — в которых хранится ширина и высота изображения в пикселах. На всякий случай я добавил поля rpx, rpy и centralAxis — которые когданить обеспечат вращение спрайта вокруг точки (правда пока что я этого функционала не реализовал). Зато вместе с изображением в спрайте хранится матрица преобразований matrix. А еще у нашего спрайта получлась целая куча конструкторов. Давайте разберемся зачем их так много.

Дело в том что изображение в Bitmap можно загрузить из разных типов ресурсов. Возможно Вы заметили, что в Вашем проекте есть папка res. Это папка в которой наше приложение хранит ресурсы. При чем для каждого типа ресурсов в этой папке есть отдельный каталог. Для всего содержимого этой папки в классе R.java (находится в папке gen) генерируются дескрипторы. С помощью такого дескриптора изображение легко загрузить в Bitmap статическим методом класса BitmapFactory.decodeResource(Res, ID). Также есть папка assets, в которой хранятся так называемые raw ресурсы, которые тоже легко можно загрузить, представив их в виде потока. Ну и конечно можно загружать изображения хранящиеся на диске.

Собственно все эти методы и представлены в конструкторах. Так что использовать класс mSimpleSprite должно быть легко и приятно :-).

На всякий случай написал еще метод resize(). Он позволяет изменять размер изображения, хранящегося в нашем спрайте.

Метод refreshAll() обновляет дополнительную информацию о картинке, такую как ее ширина и высота. Вызывается в коде конструкторов, а так же при изменении размеров. Можно вызывать и в других случаях. Ну а все остальные методы думаю вообще не вызовут никаких затруднений. Единственное, что в методе autoSize() используются некоторые статичные члены класса mSettings, который я пока что здесь не привел. Это класс с глобальными настройками нашего приложения. После его инициализации в нем хранятся наиболее основные настройки движка. То есть если в классе mSettings переменная AutoScale равна истине, то все загружаемые спрайты будут автоматически изменять размер под любое разрешение экрана.

А вот код класса mSettings:

 

package ru.davidmd.myengine;

import java.util.Timer;

import android.view.Display;

public class mSettings {

	private static Timer t;
	private static mFrameCounter tmp = new mFrameCounter();

	public static boolean AutoScale=false;
	public static boolean FullScreen = false;
	public static int DefaultXRes = 480;
	public static int DefaultYRes = 800;
	public static int CurrentXRes;
	public static int CurrentYRes;
	public static float ScaleFactorX =1;
	public static float ScaleFactorY =1;
	public static int targetFrameRate = 25;
	public static int frameInterval = 1000/targetFrameRate;
	public static int frameCounter=0;
	public static int realFrameRate=0;

	public static void GenerateSettings(Display D)
	{
		mSettings.CurrentXRes = D.getWidth();
		mSettings.CurrentYRes = D.getHeight();
		mSettings.ScaleFactorX = mSettings.CurrentXRes/(float)mSettings.DefaultXRes;
		mSettings.ScaleFactorY = mSettings.CurrentYRes/(float)mSettings.DefaultYRes;
		if (mSettings.ScaleFactorX!=1||mSettings.ScaleFactorY!=1)
		{
			mSettings.AutoScale=true;
		}
	}

	public static void GenerateSettings(int w, int h)
	{
		mSettings.CurrentXRes =w;
		mSettings.CurrentYRes = h;
		mSettings.ScaleFactorX = mSettings.CurrentXRes/(float)mSettings.DefaultXRes;
		mSettings.ScaleFactorY = mSettings.CurrentYRes/(float)mSettings.DefaultYRes;
		if (mSettings.ScaleFactorX!=1||mSettings.ScaleFactorY!=1)
		{
			mSettings.AutoScale=true;
		}
	}

	public static void setTargetFrameRate(int fr)
	{
		mSettings.targetFrameRate=fr;
		frameInterval = 1000/targetFrameRate;
		t = new Timer();
		t.scheduleAtFixedRate(tmp, 0, 1000);
	}

	public static void setDefaultRes(int x, int y)
	{
		mSettings.DefaultXRes = x;
		mSettings.DefaultYRes = y;
	}

	public static int getFrameRate()
	{
		return mSettings.realFrameRate;
	}

	public static void newFrame()
	{
		mSettings.frameCounter++;
	}
}

Как видите, ничего сложного! Названия переменных говорят сами за себя. Работает это примерно так. Во время запуска программы мы инициализируем этот класс методом  GenerateSettings(). Этот метод в качестве параметров может принимать объект типа Display или просто ширину и высоту экрана в пикселах. Это — ширина и высота экрана конкретного устройства. А в переменных  DefaultXRes и  DefaultYRes хранится ширина и высота экрана для которого вы подготовили графику. Исходя их этих значений высчитываются значения переменных  ScaleFactorX и  ScaleFactorY — то есть фактически множители масштабирования. Именно они и используются в методе AutoScale() класса mSimpleBitmap.
Правда есть еще какой-то там загадочный таймер, но что он такое и какую роль здесь играет, оставим на потом.

Код классов можно скачать здесь: androidTutor3. Если есть вопросы или комментарии милости просим

|
V

Тут их можно оставить 🙂 Или написать мне на mail

продолжение обитает вот здесь.

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

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

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

  1. Уведомление: Пишем движок игры под Android – tutorial часть 2 (Полилиния, прямоугольники круг) | Программирование и разработка, android, java – с самых первых шаго

  2. дядя Ваня говорит:

    Давай дельше!
    Сам только начал читать джаву, получаю массу эстетического удовольствия (пока :)) Очень просто и доступно, дает базовое представление о системе.

    С учетом того, что многие начинающие не знакомы с особенностями самого языка, то было бы неплохо дать ссылку на хороший самоучитель.

    И творческих узбеков! 🙂

    • davidmd говорит:

      Спасибо. Стараюсь 🙂 К сожалению нормального самоучителя толком нет… Сам пользуюсь гуглом 🙂 Или вы имеете в виду самоучитель по java?

  3. Василий говорит:

    Редкий говнокод. Сразу видно, что человек не знаком с основами языка Java. Стиль то ли «писателя» на VB то ли PHP.

    • davidmd говорит:

      Да не вопрос! Тогда действительно небыл знаком 🙂
      А вы уважаемый покажите примеры своего кода. Чтоб мне было на чем учиться 🙂

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

        В этом месте можно поучиться у автора легкому и непринужденному отпиныванию троллей). Так держать! 🙂

  4. Alexander говорит:

    Все приходит с опытом. Смотрите в сторону OpenGL ES. По Java из книг советую Java 2 (том 1-2) К.Хорстманн*Г.Корнелл. Мыло не проверяется.

  5. Bimer говорит:

    А откуда берётся класс mFrameCounter в mSettings?

    • davidmd говорит:

      Почитайте остальные статьи из этого цикла про движок, там все должно быть расписано 🙂 Я честно говоря уже не очень хорошо помню что и откуда там растет 🙂

      • Евгений говорит:

        Здравствуйте. Я хочу поблагодарить Вас. Вы мне и не только очень помогли в первых шагах по разработке на Android. Большое Вам спасибо. Но также у меня есть к вам вопрос 🙂 Я дочитал до заключительной статьи, но так и не нашел класс mFrameCounter. Прошу Вас помогите разобраться…

        • davidmd говорит:

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

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

            Вот что я нашел по этому поводу:
            @Override
            public void run() {
            sgeSettings.realFrameRate = sgeSettings.frameCounter;
            sgeSettings.frameCounter = 0;
            }

            При этом в самом mSettings есть такие строчки:

            public static int frameCounter=0;
            public static int realFrameRate=0;

            При данной реализации класса mSettings класс FrameCounter ничего не делает полезного и может быть временно удален.
            Однако есть такое предположение. При выполнении любого приложения неизбежен переменный ФПС. И обязательно нужно как-то вычислять разницу между кадрами в миллисекундах, которую потом использовать для перемещения объектов. Может быть FrameCounter задумывался для этого. Ну или для другого способа решения этой же задачи.

            Сам класс я нашел в полном проекте — ссылка в последнем туториале.

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

              А вообще как то не хватает комментов в коде mSettings. В 1м туторе на каждый чих было 6-7 строчек комментов, не всегда нужных, ибо например очевидно что конструктор точки с параметрами Х и У создает точку в (Х,У). А вот тут хотелось бы реально получить инфу на тему — чт же такое делает setTargetFrameRate и для чего нужны переменные. Хотя, может это я такой бестолковый и не понимаю чего-то очевидного 🙁

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

    Ух.
    Частично разобрался.
    Вот как я понял смысл всех этих переменных:
    targetFrameRate — устанавливаемое значение ФПС (кадры в секунду). В данном примере от автора переменная по умолчанию равна 25. К слову сказать — на наших телевизорах обычный аналоговый сигнал передается именно с такой частотой. И именно по-этому автор и сделал ее частотой по умолчанию.
    realFrameRate — реальный ФПС, которое выдает устройство. на котором запущена наша программа. Очень зависит от конкретного устройства и его загруженности в данный момент. Крайне рекомендуется чтобы она была больше нашей targetFrameRate. Идеально, если она более чем вдвое больше (55-60-и выше). Постоянно меняется в процессе выполнения программы. Именно по-этому и введена наша собственная частота кадров, от которой мы и пляшем — для равномерности и плавности движений всех объектов на экране.
    frameInterval — интервал времени между кадрами, меняющимися с частотой targetFrameRate. В миллисекундах. Нужен для расчета координат объектов в следующем кадре.
    frameCounter — счетчик реальных кадров (если можно так выразится). Считает количество реальных кадров, выдаваемых устройством и служит для получения актуального значения realFrameRate, которое, напомню, имеет свойство постоянно меняться.

    Вот код хитрой функции, в которой это все юзается.

    public static void setTargetFrameRate(int fr)
    {
    mSettings.targetFrameRate=fr;
    frameInterval = 1000/targetFrameRate;
    t = new Timer();
    t.scheduleAtFixedRate(tmp, 0, 1000);
    }

    Здесь мы устанавливаем нужный нам ФПС. Затем мы вычисляем интервал между нашими кадрами в миллисекундах. Затем мы создаем норвый таймер. И этот таймер запускает задание tmp. В первый раз он запускает его немедленно (второй параметр), а все последующие разы он запускает его через 1000 миллисекунд (третий параметр), то есть 1 раз в 1 секунду запускается задание tmp.

    А что же это за задание такое то?
    А вот в начале того же mSettigs есть строчка:
    private static mFrameCounter tmp = new mFrameCounter();
    Которая посылает нас в класс mFrameCounter. А, зайдя туда, мы обнаруживаем следующую функцию:
    @Override
    public void run() {
    mSettings.realFrameRate=mSettings.frameCounter;
    mSettings.frameCounter=0;
    }

    На время вызова данной функции в переменной mSettings.frameCounter хранится количество реальных кадров, отработанных устройством за прошедшую секунду. Это и есть наш реальный ФПС в данную секунду. После этого счетчик кадров обнуляется, начинается новая секунда и счетчик снова считает кадры. И это действо происходит каждую секунду.

    Так же хотелось бы вернуться в mSettings и отметить следующую функцию:
    public static void newFrame()
    {
    mSettings.frameCounter++;
    }

    Именно в ней наш счетчик реальных кадров «тикает». Однако, кто вызывает эту функцию — мне пока не известно. Может быть мы узнаем это в следующем туториале.

    Напоследок. frameInterval лучше сделать float и хранить в нем именно секунды, то есть ,например, он будет равен 0,04 при ФПС 25. Позже мы берем скорость, которая у нас стандартно в метрах в секунду или пикселах в секунду и умножаем на этот float. И получаем правильное движение объекта без лишней путаницы. Однако для этого ее надо передавать в объект. Пока у нас в объекте используется стандартное dx и dy. А должно бы dx*dt и dy*dt.

    • davidmd говорит:

      Хм! В общем все правильно за исключением того что frameInterval — собственно нигде не используется при расчете движения объектов. Идея такая была, однако до реализации не дошел 🙂 Честно говоря тогда у меня была куча свободного времени, а теперь его все меньше и меньше, поэтому буду рад, если вы действительно возьмете эту штуку за основу и доведете ее до ума. Хотя для моих целей она вполне годилась и так, но вообще-то я счас сам смотрю и понимаю что по сути автор поста про говнокод действительно прав 🙂 Уж чересчур много там всякой лишней фигни и чересчур все не стандартизировано…

    • davidmd говорит:

      О, да вы собственно это тоже заметили 🙂 я по поводу FrameInterval 🙂
      А время в милисек. ничего не мешает использовать в системе СИ.

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

        Ну если брать миллисекуннды, то надо не забывать гдето умножать на 1000 а где-то делить. Секунды все же проще.
        Ак же в пользу float говорит то, что мы вычисляем это значение делением. А при делении часто получаются дробные значения. А если их округлять, то… Короче ну его нафиг — лучше поточнее хранить во float, чем «плюс-минус лапоть». Но это имхо.

        И это… самое… я там накомментил в первых двух туториалах — удалите лишнее пожалуйста. Удалил бы сам, да не могу. Ну а если ответите там хотя бы на часть вопросов — буду оооочень благодарен.

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

          Хм. А возможно, миллисекунды не так уж и плохи. Метод scheduleAtFixedRate в таймерах явы использует именно миллисекунды.

        • davidmd говорит:

          Сорри, удалять не буду, а то я же не знаю что именно вы хотите удалить? К тому же ничего страшного в комментах нет… Хай будут 🙂

  7. Леван говорит:

    if (mSettings.AutoScale) {
    this.resize(mSettings.ScaleFactorX, mSettings.ScaleFactorY);
    }

    Ругается на эти строчки, как быть. Выделяет mSetting красным

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

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