avatar

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

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



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

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

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

// если хотим, чтобы приложение постоянно имело портретную ориентацию
//setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);

// если хотим, чтобы приложение было полноэкранным
//getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);

// и без заголовка
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);

        //или эту строку не пишем, а в androidManifest пишем
        //android:theme="@android:style/Theme.NoTitleBar.Fullscreen"

        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 canvas = holder.lockCanvas(null);
                           onDraw(canvas);
                           holder.unlockCanvasAndPost(canvas);
                    }
 
                    @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 и вставляем в него следующий код:

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 canvas = null;
                    try {
                           canvas = view.getHolder().lockCanvas();
                           synchronized (view.getHolder()) {
                                  view.onDraw(canvas);
                           }
                    } finally {
                           if (canvas  != null) {
                                  view.getHolder().unlockCanvasAndPost(canvas);
                           }
                    }
             }
       }
}  


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

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 canvas = null;
                    startTime = System.currentTimeMillis();
                    try {
                           canvas = view.getHolder().lockCanvas();
                           synchronized (view.getHolder()) {
                                  view.onDraw(canvas);
                           }
                    } finally {
                           if (c != null) {
                                  view.getHolder().unlockCanvasAndPost(canvas);
                           }
                    }
                    sleepTime = ticksPS-(System.currentTimeMillis() - startTime);
                    try {
                           if (sleepTime > 0)
                                  sleep(sleepTime);
                           else
                                  sleep(10);
                    } catch (Exception e) {}
             }
       }
}


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


Будут вопросы — задавайте постараюсь ответить.

Немного пропиарю свой блог, создал недавно, читайте мои статьи и там =) вот он родимый :)
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.