понедельник, 19 ноября 2012 г.

Обработка столкновений (коллизии).


Перед нами две последовательные задачи. Первая: наделить все активные объекты неким "физическим телом". Пока объекты имели только графическую интерпретацию, но этого не достаточно. Неважно, что там у нас нарисовано - программа все воспринимает как прямоугольники графики (спрайты). И край спрайта далеко не всегда приходится на видимый край объекта. По-этому нам нужно дополнительное "физическое тело", лучше подходящее для будущей обработки столкновений.
Мы будем использовать прямоугольное тело, меньшее по размерам, чем наш спрайт. Для простоты центр его будет совпадать с центром спрайта. Будем использовать встроенные методы (геттеры-сеттеры) от центра вращения (Rotation Center). Мне кажется это более логичным, возможно в будущем придется вращать спрайты вокруг этой точки. Вторая задача: обработка столкновений. Взаимодействие двух "физических тел". О ней позже. Для решения первой задачи внесем в наши классы следующие изменения: Класс BasicObject:
// ...some code...
private float health;
public BodyRectangle mBody;

// ===========================================================
// Constructors
// =========================================================== 
BasicObject(float x, float y, final ITextureRegion mFaceTextureRegion,
   VertexBufferObjectManager pVertexBufferObjectManager, float vx, float vy, 
   float bodyWidth, float bodyHeight, float health, final Camera pCamera) {
  super (x-mFaceTextureRegion.getWidth()/2, y-mFaceTextureRegion.getHeight()/2, 
           mFaceTextureRegion, pVertexBufferObjectManager);
  this.vx = vx;
  this.vy = vy;
  this.health = health;
  this.mBody = new BodyRectangle(x-bodyWidth/2,y-bodyHeight/2,bodyWidth,bodyHeight);
  this.deltaX = (this.getWidth()-bodyWidth)/2;
  this.deltaY = (this.getHeight()-bodyHeight)/2;  
  this.mCamera = pCamera;
};
//... some code...
public float GetAbsSpeed(){
 return (float) Math.sqrt(vx*vx + vy*vy);
}

 // returns 1 - object dead, 2 - object offscreen, 0 - object alive and onscreen
 public byte Update(float dt){ 
  this.setX(this.getX() + vx * dt);
  this.setY(this.getY() + vy * dt);
  resetBody();
  this.resetRotationCenter();
  this.mBody.resetRotationCenter();
  if(!this.mCamera.isRectangularShapeVisible(this))
   return 1;    
  if (this.health <= 0)
   return 2;
  return 0;  
 }

 private void resetBody(){
  this.mBody.setX(this.getX() + deltaX);
  this.mBody.setY(this.getY() + deltaY);  
 }

 public void ReduceHealth(int healthToReduce1) {
  health -= healthToReduce1;
 };
 
 public void Kill(){
  health = 0;  
 };
Строка 2 - "здоровье объекта". Если оно ниже или равно нулю - объект мертв. Строка 3 - собственно тело объекта. Код класса BodyRectangle  приведу полностью чуть ниже. Пока нам надо знать, что это прямоугольник, совпадающий центром со спрайтом. Строка 10 - в конструкторе появились параметры для нашего тела - ширина, высота, здоровье. Строки 11 и 16 - хитрые преобразования и деления на два предназначены вот для чего. Основная координата спрайта, по которой он располагается на экране - его верхний левый угол. Однако, я хочу , чтобы объект создавался с центром именно в той точке, которую я указал.   Ну а дальше пусть его система обрабатывает, как ей удобней. Мне же удобней ориентироваться по центрам объектов, а не по их углу. Строки 17 и 18 - "дельта" - это разница между координатой верхнего левого угла спрайта и верхнего левого угла тела. Соответственно по Х и по У. Используются в функции "сброса координаты тела", то есть выравнивания центра тела и центра спрайта. Сама функция - в строчках 40-43. Строка 22 - нужна будет для обработки столкновений. Возвращает модуль вектора скорости. Строчки 31 и 32 возможно не нужны. Код писался три дня назад, не все помню. Надо бы выяснить и поправить. Строка 33 - проверка "вылетания" объекта за экран. Строка 35 - проверка "жив ли объект" Строки 45 и 49 - убивалки объекта. Хотя... если в строку 45 подать отрицательное значение, то здоровье объекта увеличится. Теперь обещанный код класса BodyRectangle:
package ru.sapfil.AETest2;
import org.andengine.entity.shape.RectangularShape;
import org.andengine.opengl.shader.PositionColorTextureCoordinatesShaderProgram;
import org.andengine.opengl.vbo.IVertexBufferObject;

class BodyRectangle extends RectangularShape{

 public BodyRectangle(float pX, float pY, float pWidth, float pHeight) {
  super(pX, pY, pWidth, pHeight, PositionColorTextureCoordinatesShaderProgram.getInstance());
  // TODO Auto-generated constructor stub
 } 
 public boolean Intersect(BodyRectangle secondBody){
   return this.collidesWith(secondBody); 
 }
 public IVertexBufferObject getVertexBufferObject() {
  // TODO Auto-generated method stub
  return null;
 }
 @Override
 protected void onUpdateVertices() {
  // TODO Auto-generated method stub  
 } 
};
Пришлось унаследовать дочерний класс от RectangularShape, потому что родительский класс - интерфейс. А экземпляр интерфейса создать нельзя. Зачем нужны две последние функции, унаследованные от родителя - я не знаю. А жаль... Теперь самый интересный класс. Назвал я его Intersector, хотя можно было назвать Collider, но уж тут кому как нравится. К тому же моя обработка упрощенная и иногда идет в разрез с простейшими законами физики. Об этом потом, а пока вот код класса:
package ru.sapfil.AETest2;

import android.annotation.SuppressLint;
import android.util.Log;

public class Intersector{
  public void CollWithSingleIntersect (BasicObjectsCollection op1, 
          int healthToReduce1,  BasicObject op2, int healthToReduce2){
   this.CollWithSingleIntersect(op1, healthToReduce1, op2, healthToReduce2, false);
  } 
  
  public void CollWithSingleIntersect (BasicObjectsCollection op1, int healthToReduce1,
            BasicObject op2, int healthToReduce2, boolean repulsion){
   for (int i = 0; i < op1.GetCount(); i++)
    if(op1.mList.get(i).mBody.Intersect(op2.mBody)) {
     HealthReducer(op1.mList.get(i), healthToReduce1);
     HealthReducer(op2, healthToReduce2);
     if (repulsion)
      Repulse(op1.mList.get(i), op2);     
    };
  }
  
  private void HealthReducer(BasicObject patient, int healthToReduce){
   switch (healthToReduce) {
   case -1: {patient.Kill(); break;}
   case 0: break;
   default: patient.ReduceHealth(healthToReduce);
   }
  }  
  
  @SuppressLint("FloatMath")
  private void Repulse(BasicObject op1, BasicObject op2) {
   
   float v = (op1.GetAbsSpeed() + op2.GetAbsSpeed())/2;
   float deltaX = ((op2.getX()+op2.getRotationCenterX()) - (op1.getX()+op1.getRotationCenterX()));
   float deltaY = ((op2.getY()+op2.getRotationCenterY()) - (op1.getY()+op1.getRotationCenterY()));
   
   double angle = Math.atan2(deltaX,deltaY);
   op1.SetSpeed( -v * (float)Math.sin(angle), -v * (float)Math.cos(angle));
   op2.SetSpeed( v * (float)Math.sin(angle), v * (float)Math.cos(angle));   
  }
}
Вот иллюстрация: Постараюсь объяснить все эти математические штуки вместе с объяснением кода. Строка 7,12 - функция пересечения одного объекта с группой объектов. Нужна мне для обработки столкновений героя и астероидов. При этом строка 7 - обработка столкновений без "отталкивания" по умолчанию. А в строке 12 можно последним параметром указать - нужно ли обсчитывать отталкивание. Так же указывается параметр "уменьшения здоровья" для каждого объекта. Строка 14 перебирает все объекты из группы. например все астероиды. Строка 15 проверяет каждый из этих объектов на пересечение с нашим отдельным объектом (в нашем случае корабликом-героем). Строки 16 и 17 уменьшают здоровье обоих объектов. Кстати, в тестовом варианте оба этих параметра у меня равны нулю. Чуть ппозже надо будет делать "взрывы" и тогда уже будем тестировать эти самые параметры. А если подать отрицательный параметр, равный -1, то объект убивается, независимо от того, какое у него было здоровье. За это отвечает функция в строке  23. Строка 18 отправляет нас в функцию Repulsion ("отталкивание"), работа которой показана на рисунке. Вот о ней поговорим максимально подробно. Строка 34 вычисляет берет модули скорости обоих объектов, и делает из них среднюю скорость. То есть объекты будут разлетаться с одинаковыми скоростями,  но их вектора будут направлены в противоположные стороны. На рисунке это синие толстые вектора. Но пока мы нашли только модуль вектора. Строки 35 и 36 вычисляют расстояние между центрами столкнувшихся объектов - по Х и У. На рисунке обозначены оранжевыми стрелками. Строка 38 - вычисление угла между некоторым "нулевым направлением" и линией, на которой лежат искомые толстые синие вектора. В школьной математике это самое нулевое направление обычно совпадало с осью Х. Однако в нашем случае удобно использовать ось У. Ведь наши объекты летят чаще всего снизу-вверх и сверху-вниз. Именно по-этому в арктангенс подается отношение Х/У, а не У/Х, как мы привыкли в школе. На рисунке этот угол обозначен синей "полукруглой" стрелкой. Строки 39,40 - вычисление Х- и У-составляющих скорости через модуль скорости и угол. И передача этих данных в объект. На рисунке это синие стрелочки потоньше. Длины этих векторов мы и передаем объекту. ВАЖНОЕ ЗАМЕЧАНИЕ! Система обработки столкновений упрощенная (я уже говорил об этом). Во-первых она не учитывает направление движения объектов до столкновения. По-этому "касательные" удары будут обрабатываться наименее реалистично. Зато "лобовые" будут выглядеть вполне приемлемо. Во-вторых в нашей физической модели нет понятия "массы" объекта. По-этому в данном варианте не стоит сталкивать объекты, которые визуально сильно разнятся по массе. Например, вы столкнете огромный астероид и маленький камушек. Они разлетятся с одинаковой скоростью, что совершенно не реалистично. Я не знаю, потребуется ли мне улучшать систему столкновений, или нынешний вариант кода будет вполне подходящим. Напоследок выложу код класса Hero. В нем переработана функция движения - теперь он "инерционный". Так же он приведен в соответствие с "физической моделью".
package ru.sapfil.AETest2;
import org.andengine.engine.camera.Camera;
import org.andengine.entity.particle.emitter.IParticleEmitter;
import org.andengine.opengl.texture.region.ITextureRegion;
import org.andengine.opengl.vbo.VertexBufferObjectManager;

class Hero extends BasicObject implements IParticleEmitter{
  // ===========================================================
  // Constants
  // ===========================================================
  // ===========================================================
  // Fields
  // ===========================================================  
  private float maxSpeed = 100;
  // ===========================================================
  // Constructors
  // ===========================================================
 Hero(final ITextureRegion mHeroFaceTextureRegion, 
          VertexBufferObjectManager pVertexBufferObjectManager, Camera mCamera){
  super(mCamera.getWidth()/2, mCamera.getHeight()-2*mHeroFaceTextureRegion.getHeight(),
             mHeroFaceTextureRegion, pVertexBufferObjectManager, 
    0, 0, 52, 52, 100, mCamera);
  } 
  // ===========================================================
  // Getter & Setter
  // =========================================================== 
  // ===========================================================
  // Methods for/from SuperClass/Interfaces
  // ===========================================================
  public void getPositionOffset(float[] pOffset) {
   pOffset[VERTEX_INDEX_X] = this.getX() + 15.5f;
   pOffset[VERTEX_INDEX_Y] = this.getY() + 46f; 
  } 
  // ===========================================================
  // Methods
  // ===========================================================
   
  public void heroUpdate (float dt, float acs_dx, float acs_dy){
    this.SetSpeed(this.GetSpeedX() -acs_dx * maxSpeed *dt ,
                           this.GetSpeedY() + acs_dy * maxSpeed * dt);
    this.Update(dt);
    this.StayInCamera();  
  }
  // ===========================================================
  // Inner and Anonymous Classes
  // ===========================================================  
}
Думаю на этом все. Что будет дальше я пока не знаю. Надо бы изучать класс Level или как-либо еще попытаться создать уровень с заранее заданными параметрами, а не случайно создаваемыми астероидами.

4 комментария:

  1. Здравствуйте!
    Очень хороший блог, побольше бы таких, спасибо.
    Как скоро можно ждать новые уроки?
    И не могли бы вы оставить ICQ/Skype?

    ОтветитьУдалить
    Ответы
    1. Привет.
      Я решил заморозить это проект, раз уж никто не заходил сюда.
      Но если хотя бы одному человеку это надо - я готов продолжить работу. :)
      Скайп Sapfil2.

      Удалить
  2. Этот комментарий был удален автором.

    ОтветитьУдалить
  3. Здравствуйте! Не могли бы вы выложить полный проект именно этого примера? При вызове что-то делаю неправильно и проект постоянно крашится..

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