четверг, 12 января 2012 г.

Пишем игру под Android: Часть 4 - onTouchEvent и определение столкновений

Добрый день/вечер, в этой части мы будем рассматривать работу с сенсорным экраном и проверка столкновений координат касания пальца к экрану с координатами человечков (будем убивать наших славных ботов:)).

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

  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: Фоновая музыка в игре


Если Вы уже прочитали эти статьи то прошу под кат, там Вы узнаете много нового (надеюсь).


onTouchEvent() — событие касания к экрану


Первое, что нужно сделать, это добавить метод onTouchEvent() в класс GameView, в целях обработки каждого касания по экрану. Для каждого спрайта в списке мы узнаем, существует ли событие нажатия на экран по координатам (х, у) и имеют ли они столкновения со спрайтами. Если столкновения не существует, мы удаляем спрайт из списка спрайтов и возвращаем назад список чтобы избежать ошибок в следующей итерации, когда мы удаляем спрайт.

GameView.java
/**Обработка косания по экрану*/
public boolean onTouchEvent(MotionEvent event) {
     for (int i = sprites.size()-1; i >= 0; i--) 
     {
         Sprite sprite = sprites.get(i);
          if (sprite.isCollition(event.getX(),event.getY())) 
          {
                      sprites.remove(sprite);
          }
      }
    return super.onTouchEvent(event);
}


Теперь метод onDraw() рисует спрайт из списка, удаленные спрайты рисоваться не будут. Результат обработки события onTouchEvent() является как бы раздавливае персонажа своим пальцем :) Мы очень жестоки…

Определение столкновений


Чтобы заставить его работать, мы должны реализовать метод isCollition() в классе Sprite. Этот метод должен вернуть true, если (х, у) координаты указывают на нашего человечка.

Sprite.java
/**Проверка на столкновения*/
public boolean isCollition(float x2, float y2) {
      return x2 > x && x2 < x + width && y2 > y && y2 < y + height;
}


Если х2 не больше х, это означает, что касание было слева от спрайта. если х2 больше, чем х + width, это означает, что касание было — справа от спрайта. 

Если х2 > х & & 2 <х + width, что означает, что прикосновение в том же столбце, но все же мы должны проверить, если он находится в той же строке. у2> у & & 2 <у + height проверяет ряд столкновений таким же образом.

Запустим и глянем что происходит:


Как вы можете видеть при нажатии на человечка, которому соответствует координата касания на экране — удаляется . 

Ограничение по количеству кликов


Удаляются все человечки какие находятся по этим координатам, а мы хотим что бы удалялся только один из допустим трех. Для этого нам нужно добавить прерывании в нашу функцию определения нажатия на экран onTouchEvent().

GameView.java
for(int i = sprites.size()-1; i >= 0; i--) {
           Sprite sprite = sprites.get(i);
           if (sprite.isCollition(event.getX(),event.getY())) 
           {
                   sprites.remove(sprite);
                   break;
           }
}


Я забыл о синхронизации. События обрабатываются в потоке, не похожем на поток рисования и обновления. Это может привести к конфликту, если мы обновляем данные, которые были разработаны в одно и то же время. Решение довольно простое — обернуть весь код внутри onTouchEvent() с тем же объектом SurfaceHolder который мы использовали в классе GameManager. 

Вспомним что мы использовали для синхронизации цикла:

GameManager.java
canv = view.getHolder().lockCanvas();
synchronized (view.getHolder()) {
      view.onDraw(canv);
}


А теперь мы добавляем вместо старого кода в onTouchEvent():

GameView.java
synchronized (getHolder()) {
        for (int i = sprites.size()-1; i > 0; i--) 
        {
                Sprite sprite = sprites.get(i);
                if (sprite.isCollision(event.getX(),event.getY())) 
                {
                     sprites.remove(sprite);
                     break;
                }
        }
}

при помощи этого мы можем избежать ошибки, которая появляется и очень раздражает. :)

Если мы запустим его снова, и увидим что проблема все еще не устранена. На этот раз проблема не в потоке и не синхронизации, проблема в том что событие вызывается сильно быстро и нам его нужно ограничить. Создаем метод который будет ограничивать количество кликов в 300 миллисекунд, тогда эта ошибка исчезнет и человечки будут убиваться по одному раз в 300 мс. Добавляем чуть выше кода в onTouchEvent().

GameView.java
if (System.currentTimeMillis() - lastClick > 300) {
      lastClick = System.currentTimeMillis()
//скобочка закрывается перед return true; иф - охватывает весь метод


Теперь запустите его снова, и вы увидите разницу.

Исходные коды & Скачать с Git

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

  1. Очень классный мануал! Только он почему то не запускается у меня в Эклипсе...
    Ты не могу бы выложить полный код?

    ОтветитьУдалить
    Ответы
    1. Так ссылка ниже - исходные кода для чего? :)

      http://get.keeg.ru/OnTouchEventAndCollision.rar.html

      Удалить
  2. Отличная статья, спасибо! :)

    ОтветитьУдалить
  3. Статья помогла, спасибо! Есть небольшое конструктивное замечание :)
    public boolean isCollition(float x2, float y2) {
    return x2 > x && x2 < x + width && y2 > y && y2 < y + height;
    }
    Не кажется ли Вам, что данный метод будет отслеживать лишь правый нижний угол спрайта? По-моему, лучше бы возвращаемое условие сделать таким:
    return x2 > x - width && x2 < x + width && y2 > y - height && y2 < y + height;
    Делал такую же штуку только с простым drawCircle(). Не сразу понял, почему кружочки убираются не всегда.

    ОтветитьУдалить
  4. Спасибо за урок. Но не могли бы вы подсказать почему при проверке event.getAction() всегда выдает только ACTION_DOWN.

    ОтветитьУдалить
  5. Картинка отвалилась

    ОтветитьУдалить