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

Кто бы поспорил с тем, что самые полезные и нужные программы для любой мобильно платформы — это конечно игры! И андроид здесь вовсе не исключение. Мобильное устройство в наше время это центр развлечений в кармане. Можно сказать, что современная гонка за производительностью — прямой результат этой тенденции. Ну так и мы с вами давайте будем в тренде! Разработка любой игры процесс достаточно трудоемкий и кропотливый. А написание игрового движка — пожалуй не менее сложное дело.

Вот и я решил попробовать написать свой простенький игровой движок для Android и поделиться с Вами этим опытом.

Прежде всего необходимо определиться какие возможности будут у нашего движка? Ведь смысл его создания заключается в том, чтобы облегчить нам в будущем разработку игры. Писать трехмерный движок мы с Вами не будем. Слишком это сложно для нас новичков. Поэтому попробуем создать простенький 2D движок для спрайтовой анимаии. Может по ходу дела будем его расширять. В его функционал будет входить следующее:

  • Работа с самой системой, как то: создание поверхности для рисования, слежение за отрисовкой.
  • Работа с графическими примитивами — (точка, линия, прямоугольник, круг, эллипс).
  • Возможность перемещать примитивы.
  • Работа со спрайтами (загрузка, движение, вращение, изменение размера).

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

Ну вот, проект создали, теперь переходим к самому простому и самому базовому классу нашего движка. Так его и назовем mBasic. Все остальные классы для работы с примитивами мы в будующем унаследуем от этого класса. Вот его код:

package ru.davidmd.myengine;

import android.graphics.Canvas;
import android.graphics.Paint;

/**
 * @author davidmd
 * @version 0.1 Базовый класс движка. определяет тип
 */

public abstract class mBasic {

 /**
 * Указывает на тип объекта-наследника класса mBasic присваивается в
 * конструкторах классов потомков. Для каждого класса потомка свой.
 */
 public int type;

 /**
 * Тип точка
 */
 public static final int TYPE_POINT = 1;
 /**
 * Тип отрезок
 */
 public static final int TYPE_LINE = 2;
 /**
 * Тип полилиния
 */
 public static final int TYPE_POLYLINE = 3;
 /**
 * Тип прямоугольник
 */
 public static final int TYPE_RECT = 4;
 /**
 * Тип круг
 */
 public static final int TYPE_CIRCLE = 5;
 /**
 * Тип простой спрайт
 */
 public static final int TYPE_SIMPLESPRITE = 6;

 abstract void update();

 /**
 * Проверяет, попадает ли точка в какой-то из примитивов
 *
 * @param f
 * - координата по оси x
 * @param g
 * - координата по оси y
 * @return озвращает истину, если попадает и ложь, если нет
 */
 abstract boolean isSelected(float f, float g);

 /**
 * Метод отрисовки
 * @param c - канва для отрисовки
 * @param p - кисть для отрисовки
 */
 abstract void draw(Canvas c, Paint p);
}

И так, мы определили абстрактный класс (это значит что в наших программах мы не сможем создавать экземпляры такого класса напрямую), в котором определили в качестве констант несколько различных типов примитивов.  TYPE_POINT — точка,  TYPE_LINE — отрезок,  TYPE_POLYLINE — полилиния,  TYPE_RECT — прямоугольник,  TYPE_CIRCLE — круг,  TYPE_SIMPLESPRITE — простой спрайт. На самом деле не думаю что нам понадобятся в работе над игрой все эти типы примитивов, но на всякий случай пусть будут. Также в этом классе мы определили абстрактный метод update() которым будем пользоваться для обновления параметров наших примитивов и еще один метод boolean isSelected(float f, float g) — им мы будем пользоваться для определения принадлежит ли точка с координатами f и g нашему примитиву. Ну и зададим метод draw(Canvas c, Paint p). Он  понадобится нам для отрисовки наших примитивов. Параметрами метода являются объекты типа Canvas (канва на которой мы хотим рисовать) и Paint (кисть которой мы хотим проводить отрисовку). Оба метода абстрактны, значит реализовывать мы их будем уже в соответствующих классах. 

Теперь давайте приступим к реализации нашей библиотеки. Начнем с класса mPoint — этот класс будет отвечать за работу с точкой. Параметры x и y — это понятно, положение точки на экране (или за его пределами). dx и dy — это скорость точки в направлении каждой из осей. axX и axY — это ускорение точки если оно больше чем единица, то точка будет ускоряться, если меньше чем единица то замедляться. (зачем это надо разберемся потом). сам класс mPoint унаследуем от класса mBasic. И зададим несколько разных конструкторов, чтоб было не скучно :-). В итоге получаем следующий класс:

package ru.davidmd.myengine;

import android.graphics.Canvas;
import android.graphics.Paint;

/**
 * @author davidmd
 * @version 0.1 Точка в двумерном пространстве есть возможность для
 *          автоматического движения с заданной скоростью и ускорением. Класс
 *          унаследован от {@link mBasic}
 */

public class mPoint extends mBasic {

	/**
	 * Координаты по осям
	 */
	float x, y; // Позиция точки
	/**
	 * Ускорения по осям
	 */
	float axX = 1, axY = 1; // ускорение

	/**
	 * Скорости по осям
	 */
	float dx = 0, dy = 0; // скорость

	/**
	 * Конструктор принимает два целочисленных значения
	 *
	 * @param x
	 *            - координата по x
	 * @param y
	 *            - координата по y
	 */
	public mPoint(int x, int y) { // конструктор
		this.x = x; //
		this.y = y;
		this.type = mBasic.TYPE_POINT;
	}

	/**
	 * Конструктор принимает два значения с плавающей запятой
	 *
	 * @param x
	 *            - координата по x
	 * @param y
	 *            - координата по y
	 */
	public mPoint(float x, float y) {// Еще коструктор
		this.x = x;
		this.y = y;
		this.type = mBasic.TYPE_POINT;
	}

	// Дальше все методы для получения и установки значений.
	/**
	 * @return возвращает координату по оси X
	 */
	public float getX() {
		return x;
	}

	/**
	 * Устанавливает координату по оси х
	 *
	 * @param x
	 *            - новая координата
	 */
	public void setX(float x) {
		this.x = x;
	}

	/**
	 * @return возвращает координату по оси Y
	 */
	public float getY() {
		return y;
	}

	/**
	 * Устанавливает координату по оси Y
	 *
	 * @param y
	 *            - новая координата
	 */
	public void setY(float y) {
		this.y = y;
	}

	/**
	 * @return возвращает ускорение по оси X если больше 1 - то объект
	 *         ускоряется если меньше 1 - то замедляется
	 */
	public float getAxX() {
		return axX;
	}

	/**
	 * устанавливает ускорение по оси X
	 *
	 * @param axX
	 *            - новое ускорение. если больше 1 - то объект ускоряется если
	 *            меньше 1 - то замедляется
	 */
	public void setAxX(float axX) {
		this.axX = axX;
	}

	/**
	 * @return возвращает ускорение по оси Y если больше 1 - то объект
	 *         ускоряется если меньше 1 - то замедляется
	 */
	public float getAxY() {
		return axY;
	}

	/**
	 * устанавливает ускорение по оси Y
	 *
	 * @param axY
	 *            - новое ускорение. если больше 1 - то объект ускоряется если
	 *            меньше 1 - то замедляется
	 */
	public void setAxY(float axY) {
		this.axY = axY;
	}

	/**
	 * @return возвращает скорость по оси X
	 */
	public float getDx() {
		return dx;
	}

	/**
	 * Устанавливает скорость по оси X
	 *
	 * @param dx
	 *            - новая скорость
	 */
	public void setDx(float dx) {
		this.dx = dx;
	}

	/**
	 * @return возвращает скорость по оси Y
	 */
	public float getDy() {
		return dy;
	}

	/**
	 * Устанавливает скорость по оси y
	 *
	 * @param dy
	 *            - новая скорость
	 */
	public void setDy(float dy) {
		this.dy = dy;
	}

	// метод для обновления положения точки на плоскости
	// изходя из ее скорости и ускорения
	@Override
	public void update() {
		this.x = this.dx + this.x;
		this.y = this.dy + this.y;
		this.dx = this.dx * this.axX;
		this.dy = this.dy * this.axY;
	}

	// Метод для определения соответствуют ли координаты точки
	// (x,y)координатам нашей точки
	@Override
	boolean isSelected(float x, float y) {
		if (this.x == x && this.y == y)
			return true;
		else
			return false;
	}

	@Override
	void draw(Canvas c, Paint p) {
		c.drawPoint(x, y, p);

	}

}

А что же делает метод update() который мы унаследовали от родительского класса? Он пересчитывает новые координаты точки, учитывая ее скорость и ускорение. Заметьте, в каждом конструкторе мы строго указываем тип создаваемого объекта:

this.type = mBasic.TYPE_POINT;

Так мы избавим себя в будущем от некоторого количества проблем. Еще мы реализоавли метод darw(Canvas c, Paint p) в его реализиции использован стандартный метод класса Canvas: Canvas.drawPoint(float x, floaty) . Помимо прочего мы задали еще несколько методов аксессоров. В основном думаю весь код достаточно прозрачен и трудностей не вызовет.

Теперь реализуем класс который будет отвечать за работу с отрезками. Вот его код:

package ru.davidmd.myengine;

import android.graphics.Canvas;
import android.graphics.Paint;

/**
 * @author davidmd
 * @version 0.1 Класс для работы с отрезками унаследован от {@link mBasic}
 */
public class mLine extends mBasic {

 private mPoint p1, p2; // концы отрезка

 // конструкторы
 /**
 * Конструктор отрезка принимает в качестве параметров координаты двух точек
 *
 * @param x1
 * - координата первой точки по x
 * @param y1
 * - координата первой точки по y
 * @param x2
 * - координата второй точки по x
 * @param y2
 * - координата второй точки по y
 */
 public mLine(int x1, int y1, int x2, int y2) {
 p1 = new mPoint(x1, y1);
 p2 = new mPoint(x2, y2);
 this.type = mBasic.TYPE_LINE;
 }

 /**
 * Конструктор отрезка принимает в качестве параметров координаты двух точек
 *
 * @param x1
 * - координата первой точки по x
 * @param y1
 * - координата первой точки по y
 * @param x2
 * - координата второй точки по x
 * @param y2
 * - координата второй точки по y
 */
 public mLine(float x1, float y1, float x2, float y2) {
 p1 = new mPoint(x1, y1);
 p2 = new mPoint(x2, y2);
 this.type = mBasic.TYPE_LINE;
 }

 /**
 * Конструктор отрезка принимает в качестве параметров две точки типа mPoint
 *
 * @param p1
 * @param p2
 */
 public mLine(mPoint p1, mPoint p2) {
 this.p1 = p1;
 this.p2 = p2;
 this.type = mBasic.TYPE_LINE;
 }

 /**
 * @return Возвращает первую точку отрезка
 */
 public mPoint getP1() {
 return p1;
 }

 /**
 * устанавливает перую точку отрезка (его начало)
 *
 * @param p1
 * - новое начало отрезка
 */
 public void setP1(mPoint p1) {
 this.p1 = p1;
 }

 /**
 * @return Возвращает вторую точку отрезка
 */
 public mPoint getP2() {
 return p2;
 }

 /**
 * устанавливает вторую точку отрезка (его конец)
 *
 * @param p1
 * - новый конец отрезка
 */
 public void setP2(mPoint p2) {
 this.p2 = p2;
 }

 // метод обновляет положения обоих концов отрезка
 @Override
 void update() {
 p1.update();
 p2.update();
 }

 // просто метод заглушка 🙂
 @Override
 boolean isSelected(float x, float y) {
 return false;
 }

 @Override
 void draw(Canvas c, Paint p) {
 c.drawLine(p1.x, p1.y, p2.x, p2.y, p);

 }

}

Ну здесь вообще ничего сложного нет! Линия в нашем понимании задается концами отрезка. Собственно здесь есть две точки p1 и p2 — которые и являются концами нашего отрезка. Несколько разных конструкторов в которых задаются эти точки и метод для обновления положения точек (поскольку класс mLine унаследован от mBasic то здесь тоже обязательно надо реализовать этот метод). Точно так же как и при отрисовке точки мы воспользовались стандартным методом канвы darwLine().

Ну пожалуй хватит на сегодня. Продолжение эпопеи читать тут. А вот коды классов classes. Если у Вас возникли какие-то вопросы или предложения, оставляйте комментарии или пишите мне davidmd@davidmd.ru.

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

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

44 комментария на «Пишем движок игры под Android — tutorial часть 1 (примитивы)»

  1. Сергей говорит:

    Ссылки на mBasic, mPoint и mLine не скачиваются.

    • davidmd говорит:

      Спасибо! Счас поправим 🙂 В принцпе в пятом уроке есть полностью весь движок.

  2. Николай говорит:

    Спасибо, отличная серия статей. Возник вопрос, почему при создании проекта выбирается android 1.6 а не 2.2? И как пожелание, хотелось бы увидеть уроки по созданию чего-то посложнее чем крестики нолики, простенькой аркады например. Еще раз, спасибо.

    • davidmd говорит:

      Спасибо за комментарий!
      Android 1.6 выбирается в целях совместимости. Счас вот готовлю урок про то как писалась вот эта игра 🙂

      • Николай говорит:

        Круто. А почему игру в маркет не выложил?
        P.S. Не увидел кнопку подписки по RSS, я бы подписался.

        • davidmd говорит:

          Ну чтоб выложить в маркет надо платить 25 баксов, а у меня их счас просто нет и черт знает когда будут (кстати, поэтому если потыркаешь тутта рекламу на сайте буду очень признателен 🙂 )
          А кнопку RSS очень даже можно сделать. Как-то я на эту тему не подумал. Это мы быстро добавим 🙂
          UPD: Уже добавил 🙂

  3. Богдан Кириченко говорит:

    во втором листинге (class mPoint) у вас дублируется метод public mPoint(float x, float y) и комментарий к нему

  4. Богдан Кириченко говорит:

    и в третьем листинге почему то тоже… 🙂

  5. Богдан Кириченко говорит:

    прошу прощения, разобрался… осваиваю Android:)

    Спасибо, очень инетесный материал

  6. Александр говорит:

    Рискну покритиковать, не исключаю, что подобный вопрос уже задавался, но зачем в нотации имен классов ставить префикс «m». Разработчики Android рекомендуют называть с этим префиксом члены класса (members) неужели не путаетесь?

    • davidmd говорит:

      Ну я то не путаюсь, но Вы абсолютно правы. Разработчики на java вообще рекомендуют так делать. Думаю дальше придерживаться этих рекомендаций. Спасибо за критику.

  7. Дима говорит:

    Вместо TYPE_RECT можно предусмотреть TYPE_POLYGON — многоугольник с любым кол-вом вершин.
    Так же в класс можно добавить массу для будущего модуля физики.

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

      Логичнее для начала сделать TYPE_TRIANGLE. Для него будет проще делать проверку попадания точки в форму.
      А многоугольник — как несколько треугольников представить.

      • davidmd говорит:

        Вполне себе логично! Если реализуете код пришлёте?

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

          Думаю дойти до конца всех туториалов. А потом начать писать свой движок, взяв за основу ваш и перекопав до самых основ.

  8. Дима говорит:

    На будущее, наверное проще оперировать с вектором ускорения, а не хранить ускорение по 2-м осям.
    Можно ввести его (ускорение) в класс Basic — и тогда можно будет выстраивать очень сложные движения объектов, например вся ломаная линия движется по собственной траектории с одним ускорением, и плюс еще каждая точка движется со своим собственным. Что-то подобное можно увидеть в World of Goo.
    Если сразу реализовать вектора, на верхнем уровне гораздо проще будет работать.
    Сугубо мое IMHO, конечно.

    • davidmd говорит:

      Да, идея хороша! Я тоже сначала так думал сделать, потом что-то поленился 🙂 Если Вы реализуете, будет здорово, а если еще и исходники мне отправите вообще здорово будет!

  9. Gon4arowww говорит:

    darw — помоему ошибся) И 2 раза) draw.
    А статься интересная, спасибо

  10. win95cih говорит:

    Касательно расчетов скорости и ускорения в методе update класс mPoint — мне кажется, что скорость нужно не умножать на ускорение, а прибавлять его. Представьте подающий предмет в реальном мире, ускорение составляет 10 м/c^2 (9.8). В данный момент тело падает со скоростью 10 м/с, через секунду 20 м/с, потом 30 и т.д. Если мы будем умножать скорость на ускорение то получим через секунду 100, потом 1000 и т.д. Не слишком ли круто? К тому же если, исходя из Ваших формул, взять начальную скорость объекта = 0, то сколько бы мы ни умножали ее на ускорение, скорость останется равной нулю. В общем, школьный курс физики)

    • davidmd говорит:

      Да уж 🙂 Ну в принципе логично 🙂 Я чет не особо там заморачивался как писалось так и писал 🙂 Учился 🙂

    • BychkovDG говорит:

      Осмелюсь поправить: не ускорение к скорости нужно прибавлять, а ускорение умноженное на момент времени, т.к. у скорости и ускорения разные единицы измерения. Получается скорость в определенный момент времени Vt=V+a*t. Ну а для кодинга Вы правы используем «this.dx + this.axX», т.к. считаем, что обновление будет происходить каждую секунду.

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

    Привет.
    На старости лет решил заняться программированием. Опыт в С есть, а в Java пока дуб-дубом.
    Самый нубский вопрос:
    Создали проект, потом ты сразу пишешь код. А куда его вставлять-то? Я так понял что надо в зайти в проект -> в папочку scr -> в свой namespace -> там создать новый файл с расширением java и в него вставлять код. И каждый файл обзывать соответсно. Я прав или не прав? И надо ли потом где-то использовать что-то типа #include как в С++? Или же Eclipse соберет все в одну большую кучу? Непонятненько.

    Теперь чуть менее нубский вопрос. В первом листинге можно ли использовать enum для значительного сокращения размера кода при полном сохранении смысла?

    Ну и в довершение — во всех статейках неплохо было бы в начале и в конце статьи сделать ссылку на «оглавление».

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

      Со своим нубским вопросом частично разобрался — скачал с последнего тутора ваш проект и посмотрел как там сделано. Так, как я и думал 🙂

      Так же нашел в шапке блога пункт «уроки по андройд», так что вопрос с поиском оглавления так же решен )

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

    Так все-таки — можно ли использовать Enum?

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

    Просьба удалить мои предыдущие комментарии.
    Тщательно копаю код. Вот тут собрал все вопросы:
    1. Можно ли использовать «enum»? (22-42 строка листинг 1)
    2. Зачем в 55й строке 1го листинга использованы имена «f» и «g» вместо «x» и «y»
    3. (Рекомендация) В листинге 2 если мы называем скорость dx (по сути это первый дифференциал), соответственно ускорение логично назвать d2x (второй дифференциал). Так же из эстетических соображений можно переставить куски кода так, чтобы везде друг за другом они следовали так: координата-скорость-ускорение. Логичнее вроде, хотя это уже придирка.
    4. В листинге 2 в строке 167 в функции Update — будет ли более логичным сначала апнуть скорость по ускорению, а потом уже по актуальному значению скорости — апнуть координату?
    5. Листинг 2. Строчку 180 можно выбросить. Если строчка 178 верна, то выполнится строчка 179, метод вернул значение и дальше не выполняется. Если же 178 ложна, то метод продолжает выполняться и следующей строчкой можно уже ставить возврат false. Я прав?
    6. Совсем не понравился метод-заглушка в третьем листинге в 106 строчке. Можно было бы использовать упрощенные варианты: попадание точки в концы отрезка. Или в центр отрезка. Но самый сложный метод — проверка попадания точки в любую часть отрезка. Попробую сейчас сюда вставить свой код, занимающийся такой проверкой.
    @Override
    boolean isSelected(float x, float y) {
    if (((x-this.p1.x)/(y-this.p1.y) == (this.p2.x-this.p1.x)/(this.p2.y-this.p1.y)) &&
    ((x >= this.p1.x && x <= this.p2.x) || (x = this.p2.x)))
    return true;
    return false;
    }
    А вообще что с точкой что с отрезком - малоэффективно проверять идеальное попадание тестовой точки в нашу точку. Результат будет false в почти 100% случаев. Логичнее проверять с некоторой погрешностью. Вот пример:
    // точная проверка
    х1 == х2
    // проверка с погрешностью
    x1 - x2 < delta

    погрешность можно определить где-нибудь 1 раз в базовом классе и потом при тюнинге движка - изменять и проверять как оно работает.

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

      Да. Сам косякнул. Где написал про метод с погрешностью — там естесссссно надо брать модуль от разности и сравнивать его с дельтой.

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

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

  14. Роман говорит:

    Все три листинга надо впихивать в отдельные файлы? и как эти файлы назвать?как и имена классов? где эти файлы будут находится? вообще что то я не понимаю…

  15. Валек говорит:

    А вы не могли бы написать еще один класс, который бы просто выводил на экран примитивы реализованные в движке? Я думаю у вас это времени сильно много не займет, зато буде неплохой пример работы классов.

    • davidmd говорит:

      Я сейчас доделываю в свободное от работы время вторую версию движка, буду выкладывать сдлаю примеры 🙂

  16. Иван говорит:

    А как вообще проводить отрисовку? как задействуется метод draw()? и какие параметры ему передавать, не подскажете?

    • davidmd говорит:

      Вообще вам не надо самостоятельно задействовать метод draw() вам надо просто правильно инициализировать движок. ТОгда все будет ОК, он автоматом будет пытаться вытянуть указанный вами фреймрейт и сам бцдет вызывать метод draw(). Вым надо только добавлять объекты для отрисовки на сцену и следить за их перемещением.

  17. Валерчик говорит:

    Здравствуйте. Хотел разобраться с примитивами, но что-то как видно с отрисовкой не ладится. написал класс вот такого содержания:
    package ru.davidmd.main;

    import ru.davidmd.myengine.mLine;
    import ru.davidmd.myengine.mPoint;
    import android.graphics.Paint;
    import android.view.View;
    import android.content.Context;

    public class Tocj extends View {
    mLine Li;
    int x1 = 30;
    int y1 = 170;
    int x2 = 300;
    int y2 = 180;
    mPoint p1;
    mPoint p2;
    Paint dp;

    public Tocj(Context context) {
    super(context);
    p1 = new mPoint(x1, y1);
    p2 = new mPoint(x2, y2);
    this.dp.setStrokeWidth(5.0F);
    this.dp.setColor(-16777216);
    Li = new mLine(p1, p2);
    }
    public void onDraw() {
    Li.setPaint(this.dp);
    }
    }
    Может код и выглядит дико, но я только учусь=) не подскажете что я неправильно написал? Заранее спасибо.

  18. Иван говорит:

    А не подскажете что представляет собой метод-заглушка?

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

    Здравствуйте,а в коммерческих целях ваш движок можно использовать?

    • davidmd говорит:

      Пожалуйста, только будет очень приятно если вы укажете гденить что вы его используете 🙂 Но в принципе — ваше дело! 🙂

  20. Алексей говорит:

    С какую программы надо иметь для написание кода?

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

    Довольно интересно , но мне намного проще использовать libGdx 🙂

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

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