Поиск по этому блогу

воскресенье, 1 января 2012 г.

Пишем игру под Android: Часть 1 - Рисуем картинки на SurfaceView

Я прочитал много разных туториалов по разработке игр под эту платформу, читал и создание на основе движков и с нуля но толком разобрать некоторые детали так и не смог. Сегодня я хочу Вам рассказать о примитивах использования класса SurfaceView.
Статьи по теме:

  1. Пишем игру под Android: Часть 1 — Рисуем картинки на SurfaceView
  2. Пишем игру под Android: Часть 2 — Создаем первый спрайт
  3. Пишем игру под Android: Часть 3 — Спрайтовая анимация, работа с несколькими спрайтами
  4. Пишем игру под Android: Часть 4 — onTouchEvent и определение столкновений
  5. Пишем игру под Android: Часть 5 — Создание полноценной 2D игры
  6. Пишем игру под Android: Часть 6: Добавление звука
  7. Пишем игру под Android: Часть 7: Меню для игры и окно приветствия
  8. Пишем игру под Android: Часть 8: Фоновая музыка в игре


И так, задача:
Для начала просто нарисуем ic_launcher.png на нашем Surface.

Для этого — создаем проект — Eclipse — New — Android project — MyFirstGame — Main.java.

Заходим в res/layout/main.xml и пишем вот такое:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <package.GameView
        android:id="@+id/game"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />
</LinearLayout>


Вместо package пишите тот пак который у вас в проекте.

Открываем создавшийся файл Main.java и вносим в него одну строку, между super.onCreate() и setContentView().

//эта строка позволяет спрятать верхнее меню с экрана телефона
requestWindowFeature(Window.FEATURE_NO_TITLE);


Вот как оно будет выглядеть полностью:

Main.java
import android.app.Activity;
import android.os.Bundle;
import android.view.Window;
 public class Main extends Activity {
    public void onCreate(Bundle savedInstanceState) 
    {
        super.onCreate(savedInstanceState);

        requestWindowFeature(Window.FEATURE_NO_TITLE);

        setContentView(new GameView(this));
    }
}


Дальше нам нужно создать класс GameView.java который будет производить отрисовку нашей графики на сцене и унаследуем его от класса SurfaceView, для того что бы производить 

GameView.java
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.view.View;
public class GameView extends SurfaceView {
       /**Загружаемая картинка*/
       private Bitmap bmp;
       
       /**Наше поле рисования*/
       private SurfaceHolder holder;
 
       //конструктор
       public GameView(Context context) 
       {
             super(context);
             holder = getHolder();
             holder.addCallback(new SurfaceHolder.Callback() 
             {
                    public void surfaceDestroyed(SurfaceHolder holder) 
                    {
                    }
 
                    @Override
                    public void surfaceCreated(SurfaceHolder holder) 
                    {
                           Canvas c = holder.lockCanvas(null);
                           onDraw(c);
                           holder.unlockCanvasAndPost(c);
                    }
 
                    @Override
                    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) 
                    {
                    }
             });
             bmp = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
       }
 
       //Рисуем нашу картинку на черном фоне
       protected void onDraw(Canvas canvas) 
       {
             canvas.drawColor(Color.BLACK);
             canvas.drawBitmap(bmp, 10, 10, null);
       }
}


Для отрисовки мы вызываем метод OnDraw() который реализуется непосредственно программистом в коде игры. Что бы представить как работает Canvas — можете представить его как доску на которой можно рисовать то, что Вам вздумается. Для того что бы рисовать на сцене (Canvas) — получаем команду рисовать, при помощи функции OnDraw(). 

Рисование будет происходить в отдельном потоке для того что бы при отрисовке в нужный момент мы могли получить картинку для этого мы блокируем Canvas, а для продолжения рисования разблокируем его.

Мы должны принять во внимание, что во время отрисовки у нас есть ресурс который заблокирован и мы теряем производительность и очень важно, свести к минимуму время блокировки. Пока что у Вас должно получиться только вот такое:



Добавляем анимацию
Теперь можно добавить немного анимации, пусть наша картинка движется в каком-то направлении, я хочу что бы она медленно передвигалась вправо. Вот сейчас мы это и сделаем.

Игра это цикл повторений. Основные направления деятельности:

1. Физика обновления, это обновление данных игры, как, например х и у координаты позиции для маленьких символов.
2. Рисование, это отрисовка картинки, которую вы видите на экране. При вызове этого метода оно дает вам восприятие анимации.

Мы собираемся выполнить цикл игры в отдельном потоке. В одном потоке мы создаем обновления и рисование, а в основном потоке мы обрабатываем события. Для этого создаем еще один файл GameManager.java и вставляем в него следующий код:

GameManager.java
import android.graphics.Canvas;
 public class GameManager extends Thread {
       /**Объект класса*/
       private GameView view;
      
       /**Переменная задавания состояния потока рисования*/
       private boolean running = false;
      
       /**Конструктор класса*/
       public GameManager(GameView view) 
       {
             this.view = view;
       }
 
        /**Задание состояния потока*/
       public void setRunning(boolean run) 
       {
             running = run;
       }
 
       /** Действия, выполняемые в потоке */
       public void run() {
             while (running) {
                    Canvas c = null;
                    try {
                           c = view.getHolder().lockCanvas();
                           synchronized (view.getHolder()) {
                                  view.onDraw(c);
                           }
                    } finally {
                           if (c != null) {
                                  view.getHolder().unlockCanvasAndPost(c);
                           }
                    }
             }
       }
}  


Работает поле флаг, позволяющий остановить цикл игры. Внутри цикла мы вызываем метод OnDraw() о котором мы узнали в прошлом уроке. В этом случае для простоты мы делаем обновление и рисование в OnDraw() методе. Мы используем синхронизации, чтобы избежать других потоков, что бы потоки не конфликтовали.
В SurfaceView мы просто добавим интовое поле int х, координата по которой будет двигаться наша картинка Кроме того, в методе OnDraw() мы увеличиваем х на 1, если он не достиг правой границы, конечно же мы это будет делать на нашей сцене, а значит в файле GameView.java. Вот какой вид будет иметь GameView.java 

GameView.java
public class GameView extends SurfaceView {
       /**Загружаемая картинка*/
       private Bitmap bmp;
       
       /**Наше поле рисования*/
       private SurfaceHolder holder;

       /**Объект класса GameManager*/
       private GameManager gameLoopThread;

       /** Координата движения по Х=0*/
       private int x = 0; 
      
       /**Скорость изображения = 1*/
       private int xSpeed = 1;

       public GameView(Context context) 
       {
             super(context);
             gameLoopThread = new GameManager(this);
             holder = getHolder();
             holder.addCallback(new SurfaceHolder.Callback() 
             {
 
                    /** Уничтожение области рисования */
                    public void surfaceDestroyed(SurfaceHolder holder) 
                    {
                           boolean retry = true;
                           gameLoopThread.setRunning(false);
                           while (retry) {
                                  try {
                                        gameLoopThread.join();
                                        retry = false;
                                  } catch (InterruptedException e) {
                                  }
                           }
                    }
 
                    /** Создание области рисования */
                    public void surfaceCreated(SurfaceHolder holder) 
                    {
                           gameLoopThread.setRunning(true);
                           gameLoopThread.start();
                    }
 
                    /** Изменение области рисования */
                    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) 
                    {
                    }
             });
             bmp = BitmapFactory.decodeResource(getResources(), R.drawable.icon);
       }
 
       @Override
       protected void onDraw(Canvas canvas) 
       {
             if (x == getWidth() - bmp.getWidth()) 
             {
                    xSpeed = -1;
             }
             if (x == 0) 
             {
                    xSpeed = 1;
             }
             x = x + xSpeed;
             canvas.drawColor(Color.BLACK);
             canvas.drawBitmap(bmp, x , 10, null);
       }
}


Мы ограничили отрисовку до 10 кадров в секунду, что составляет 100 мс (миллисекунды). Мы используем метод sleep() за оставшееся время, чтобы получить 100 мс. Если цикл занимает больше, чем 100 мс мы спим 10 мс в любом случае, наше приложение будет требовать слишком много памяти процессора. Немного усложним код, в GameManager.java заменяем старый код на этот:

 GameManager.java
public class GameManager extends Thread
{
       /**Наша скорость в мс = 10*/
       static final long FPS = 10;
      
       /**Объект класса GameView*/
       private GameView view; 

       /**Задаем состояние потока*/
       private boolean running = false;
      
       /**Конструктор класса*/
       public GameManager(GameView view) 
       {
             this.view = view;
       }
 
        /**Задание состояния потока*/
       public void setRunning(boolean run) 
       {
             running = run;
       }
 
       /** Действия, выполняемые в потоке */
 
       @Override
       public void run() 
       {
             long ticksPS = 1000 / FPS;
             long startTime;
             long sleepTime;
             while (running) {
                    Canvas c = null;
                    startTime = System.currentTimeMillis();
                    try {
                           c = view.getHolder().lockCanvas();
                           synchronized (view.getHolder()) {
                                  view.onDraw(c);
                           }
                    } finally {
                           if (c != null) {
                                  view.getHolder().unlockCanvasAndPost(c);
                           }
                    }
                    sleepTime = ticksPS-(System.currentTimeMillis() - startTime);
                    try {
                           if (sleepTime > 0)
                                  sleep(sleepTime);
                           else
                                  sleep(10);
                    } catch (Exception e) {}
             }
       }
}


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

Исходные коды

38 комментариев:

  1. В последнем GameView.java gameLoopThread = new GameView(this); конструктор не тот должен от GameManager быть. А за статью спасибо )

    ОтветитьУдалить
  2. Помогите пожалуйста! Сделал вроде все как у Вас. Пока картинка была неподвижна она выводилась, а после того, как переделал дальше стал выводиться только черный экран.

    ОтветитьУдалить
    Ответы
    1. Возможно удалил отрисовку в onDraw() или не прописал её в run() методе

      Удалить
  3. "Вместо package пишите тот пак который у вас в проекте." я не знаю что такое "пак" - поэтому скачал исходники, а там все по другому вообще в этом файле.
    "Дальше нам нужно создать класс GameView.java который будет производить отрисовку нашей графики на сцене и унаследуем его от класса SurfaceView, для того что бы производить "
    как его создать что то я не понял, в папке layouts?
    пока завис на этом))

    ОтветитьУдалить
    Ответы
    1. Создаешь файл GameView.java и он наследует класс SurfaceView. В исходниках все ж понятно вро де бы расписывал.

      Удалить
  4. скачал исходники, вставил себе. эклипс ругается что надо убрать @Override, когда убираю то запускается но просто черный экран. в чем дело?

    ОтветитьУдалить
    Ответы
    1. Анонимный21 июня 2012 г., 15:19

      Чтобы Эклипс не ругался на @Override зайдите в настройки проекта (Project->Properties) в левой панели открывшегося окна выберите Java Compiler , справа выберите уровень компилятора (Compiler compliance level) 1.6 или выше.

      Удалить
  5. перенесите все действовать с холдером в другой конструктор, у менять используется конструктор с двуия параметрами, а конструктор их примера не вызввается вообще.

    ОтветитьУдалить
    Ответы
    1. Анонимный21 июня 2012 г., 15:47

      В начале статьи автор предлагает создать разметку в xml ресурсах, но далее он её не использует, а создает экран непосредственно из объекта класса GameView (setContentView(new GameView(this));)
      при этом действительно будет вызываться конструктор с одним параметром принимающим только контекст (в данном случае указатель this).
      Но если ваш GameView создается действительно из xml ресурса (setContentView(R.layout.main)), то обязательно будет вызываться конструктор с двумя параметрами, во втором параметре передаются атрибуты заданные в xml разметке. А если в разметке используются стиль, то будет вызван конструктор с тремя параметрами (последним будет стиль).

      Поэтому проверьте свой setContentView.

      Удалить
  6. Спасибо за статью, получилось с первого раза! =)

    ОтветитьУдалить
  7. Анонимный1 мая 2012 г., 21:05

    Очень много лишнего для столь простых результатов, а не существует более простых сред разработак под андроид ?

    ОтветитьУдалить
  8. Для джавы вложенные функции - это нормальная практика?

    ОтветитьУдалить
  9. "автор" (если его можно так сказать) просто взял нормально описывающий весь процесс текст (http://megadarja.blogspot.com/2009/03/android-1-surface.html), переписал его, испоганил и запутал и выложил под своим авторством. Молодей, нуб!

    ОтветитьУдалить
    Ответы
    1. Код писал я свой, то что написано у дарьи совсем другое. Если посмотреть внимательно то похожи только названия файлов, но это во всех туториалах так, сперва разберитесь в себе уважаемый, а потом говорите за других.

      Удалить
    2. Точно, толком по коду ничего не описывается, что где. Плагиат, переменные только разные.

      Удалить
    3. Да даже если автор и заимствовал, в чем я сомневаюсь, то ничего плохого не вижу, загуглил инфу, выпал этот блог. Какие проблемы?

      Удалить
    4. Не утруждайтесь) Код я писал свой, то что он похож на код Дарьи не значит что я скопипастил его себе, попробуйте написать по другому и тогда я скажу что вы написали его сами.

      Удалить
  10. Я сделал чуть сложнее: на MainActivity кнопка, которая запускает GameActivity. Может, проблема из-за этого? Не знаю...
    Все работает на эмуляторе, но при нажатии кнопки "Назад" (иногда) или "Home" (всегда), программа завершается аварийно:

    09-22 18:51:16.586: E/AndroidRuntime(948): FATAL EXCEPTION: Thread-99
    09-22 18:51:16.586: E/AndroidRuntime(948): java.lang.NullPointerException
    09-22 18:51:16.586: E/AndroidRuntime(948): at com.var.bubbles.drawing.GameView.onDraw(GameView.java:71)
    09-22 18:51:16.586: E/AndroidRuntime(948): at com.var.bubbles.drawing.GameManager.run(GameManager.java:38)

    Проясните, плз., где происходит обработка этих событий.

    ОтветитьУдалить
  11. Как сделать,так чтобы картинка передвигалась верх и вниз?

    ОтветитьУдалить
    Ответы
    1. В следующем уроке посмотрите, там нужно вместо нуля в drawBitmap поставить координату y.

      Удалить
  12. Сложновато конечно сразу для понимания, тк не все объяснено(код комментирован блоками). То есть статья рассчитана не на новичка, а на человека знающего этот метод и работавшего с ним. Но я все равно рад этой статье, самая понятная среди тех, которые мне попадались. Месяц назад я ее не осилил, купил книгу по андроид сдк, но не полную. Методов отрисовки там не было. Спасибо за статью, очень полезная:)

    ОтветитьУдалить
    Ответы
    1. Честно скажу что с андроидом работаю давно, и не могу определить какая сложность описанного, но стараюсь писать как можно понятней для человека который хотя бы имеет понятие как писать код и имеет общие понятия про разработку под андроид.

      Удалить
  13. >Вместо package пишите тот пак который у вас в проекте.
    Не очень понял что за package и где его найти.
    Заранее спасибо.

    ОтветитьУдалить
  14. Не могли бы вы посоветовать литературу по Android в частности в области графики?

    ОтветитьУдалить
    Ответы
    1. http://www.amazon.com/dp/1430230428/?tag=stackoverfl08-20

      От создателей proAndroid. ОЧень хорошая книга. Где скачать не знаю, так что гуглите)

      Удалить
    2. Спасибо большое, давно за статьями вашими слежу. Думаю нагуглю :)

      Удалить
  15. Как я полагаю имеет значение в каком порядке в методе onDraw мы рисуем объекты (при наложение одного на другой объект который нарисован ранее будет перекрываться объектом который рисуется после него).
    Но вот у меня возникла проблема, когда канвасов становится больше чем 5-6 при добавлении нового, некоторые из старых канвасов просто не отрисовываются =\

    ОтветитьУдалить
  16. Анонимный21 мая 2013 г., 21:05

    В данном уроке XML файл вообще не нужен. Может автор объяснит, с какой целью он тут и как он используется???
    Аналогично и в "исходниках"

    ОтветитьУдалить
    Ответы
    1. Анонимный11 июня 2013 г., 00:05

      Указанный XML отвечает за разметку окна игры.
      P.S. Как аноним анониму - додумались, что в коде примера XML не задействован, так включите голову погуглить, зачем он нужен.

      Удалить
  17. Я начинающий. Мне интересно как можно из меню изменить например переменные X и Y в GameView? Я только изменил с "private int x" на "public int x", а дальше пока не знаю как получить к нему доступ.

    ОтветитьУдалить
    Ответы
    1. Вам нужно менять не тип переменной, а сам функционал в коде, и тогда будет результат

      Удалить
  18. Анонимный8 июля 2013 г., 10:18

    у меня вообще не запускаеться. все как ошибку выделяет

    ОтветитьУдалить
  19. Не открываются исходники в 2019( Что делать?

    ОтветитьУдалить
    Ответы
    1. Остается вариант только повторить все что было написанно самому. У меня к сожалению исходников не осталось что бы из перезалить, это было очень давно...

      Удалить