вторник, 25 декабря 2012 г.

Продолжаем обрабатывать столкновения. Плавим астероиды.

Всем привет. Огромное спасибо Евгению Shiler за то, что вернул меня в работу. В первой части данного поста выложу то, что уже сделано, а затем займусь оптимизацией.

За видео прошу прощения - снимал, чем мог. Чуть позже, возможно, переделаю.
А сделано вот что - добавлена некоторая контрольная точка впереди кораблика. И если эта точка пересекается с астероидом - у кораблика включается "плавильный лучик". Выглядит это, как на картинке. Лучики сделаны обычной кадровой анимацией в 3D-MAXе.
// Для начала прикрутим анимированный спрайт луча в нашему герою.
// И добавим парочку управляющих функций.
Hero(final ITextureRegion mHeroFaceTextureRegion, ITiledTextureRegion mHeroRayTextureRegion, 
   VertexBufferObjectManager pVertexBufferObjectManager, Camera mCamera){
  super(mCamera.getWidth()/2, mCamera.getHeight()-2*mHeroFaceTextureRegion.getHeight(),
            mHeroFaceTextureRegion, pVertexBufferObjectManager, 
         0, 0, HERO_BODY_WIDTH, HERO_BODY_HEIGHT, HERO_BODY_HEALTH, mCamera);
  this.mHeroRaySprite = new AnimatedSprite(HERO_RAY_OFFSET_X, HERO_RAY_OFFSET_Y, 
            mHeroRayTextureRegion, pVertexBufferObjectManager);
  this.attachChild(this.mHeroRaySprite);
  this.mHeroRaySprite.animate(100);  
  }
// луч сразу включаем и анимируем, но его видимостью мы будем управлять.
 
// Эти функции возвращают  координаты точки, на которую направлен луч. 
// Константы определим выше, чтобы было удобнее тестить проект.
  public float GetRayX(){
   return (this.getX() +  HERO_RAY_OFFSET_X);  }
  public float GetRayY(){
   return (this.getY() + HERO_RAY_OFFSET_Y);  }
// "Включение/выключение луча" На самом деле луч есть всегда, меняется только его видимость. 
  public void SetRayOn(){
   this.mHeroRaySprite.setVisible(true);  }
  public void SetRayOff(){
   this.mHeroRaySprite.setVisible(false);  }
// ---------------------------------------------------------------------------
// Вот такая функция нам понадобится в нашем Intersector
public int CollContainsPoint(BasicObjectsCollection op, float pX, float pY){
   for (int i = 0; i < op.GetCount(); i++)
    if (op.mList.get(i).mBody.contains(pX, pY))
     return i;
   return -1;
  };
// ---------------------------------------------------------------------------
// И вот так мы будем использовать это в MainActivity:
// ...some code...
private ITiledTextureRegion mHeroRayTextureRegion;
// ...some code...
 protected void onCreateResources()
    {
this.mHeroRayTextureRegion = BitmapTextureAtlasTextureRegionFactory.createTiledFromAsset(
     this.mHeroTexture, this, "Hero_ray2.png", 0, 64, 5, 3);
// ...some code...
}
// А этот кусочек кода добавим в нашу еже-кадровую процедуру, сразу за функцией столкновения.
// ...some code...
int i = mIntersector.CollContainsPoint(mAsteroids, hero.GetRayX(), hero.GetRayY());
    if (i > -1) {
     hero.SetRayOn();
     mAsteroids.ModifySpeedByID(i, 0.8f, pSecondsElapsed);
     hero.ModifySpeed(0.8f, pSecondsElapsed);
     }
    else hero.SetRayOff();
// ...some code...
// ---------------------------------------------------------------------------
// Вот эту функцию добавим в BasicObject
public void ModifySpeed(float procent, float dt){
  this.vx *= (procent - dt)/ procent;
  this.vy *= (procent - dt)/ procent;
 }
// ---------------------------------------------------------------------------
// Если что-то забыл - в коментах укажите.
Вообще-то нет ничего трудного. К объекту героя аттачим дочерний объект с анимированым спрайтом лучика. Спрайт видимый или невидимый в зависимости от того - если у лучка цель. Наличие цели определяется Intersector-ом. Там кстати есть еще функция ModifySpeed - она понадобилась просто для удобства тестинга. Хотя, возможно она останется в итоговом варианте.  ((( прошло несколько дней )))  Я думаю, что не стоит засорять пост множеством кода. Дело в том, что чего-то принципиально нового в нем мало. Сделаем так. Я сначала опишу все на словах, а ниже приведу коды для тех, кому вдруг это будет интересно.

1. Спрайт луча разделен на 2 части - левую и правую. Для удобства "растягивания". Теперь если луч включен - он всегда направлен в центр цели.

2. К спрайту луча добавлена партикл-система, с ней все выглядит более симпатично. Тут возникла одна важная особенность. Оказалось более удобным подключать партикл-систему напрямую в сцену, а не делать систему "дочкой" какого-то промежуточного "родителя". Если она становится дочкой во втором или третьем поколении, то трудно отслеживать ее координаты. Ведь у каждого родителя своя система координат и координаты любого "ребенка" - локальные, относительно спрайта родителя. И вот тут начинаются костыли, "добавки" и прочие некрасивости. По-этому эмиттером мы назначаем какой-нибудь объект, а вот attachChicd() делаем не к эмиттеру, а к сцене. ВНИМАНИЕ! В этом месте я бы хотел услышать фидбэк от тех, кто согласен\не согласен.

3. Вообще вся система луча вынесена в отдельный Java-файл HeroGun. Причем он сам по себе получился довольно громоздкий (150 строк). В нем реализована система "слежения" за целью. То есть пушка сама проверяет - есть у нее цель или нет. При этом выдает об этом сообщения наружу. А снаружи, в основном игровом цикле - эти сообщения получают. И если у пушки уже есть цель, то в этом прогоне не тратится время на новый поиск цели. кроме того наконец-то сделана и опробована система нанесения урона пушкой цели.

  4. Создание пушки вынесено в отдельный метод в файле героя. Дело в том, что пока приходится передавать все эти текстурные регионы "в глубину", чтобы там, "на глубине" создавались спрайты. Мне это не нравится и я все еще лелею надежду что-нибудь с этим сделать. Но до тех пор, чтобы хоть как-то разгрузить громоздкий конструктор - я решил разделить его на два - собственно конструктор героя и метод создания пушки, вызывающий конструктор пушки. Думаю все. Теперь код. Вот код "пушки"
package ru.sapfil.AETest2;

class HeroGun extends Entity /*implements IParticleEmitter*/{
  //===========================================================
  // Constants
  // ===========================================================
 
  // ray sprites offset relative to parent sprite
  private final static float HERO_LEFT_RAY_OFFSET_X = 10;
  private final static float HERO_RIGHT_RAY_OFFSET_X = 32;
  private final static float HERO_SPRITE_RAY_OFFSET_Y = -30; 
  // ray "fusion point" offset
  private final static float HERO_RAY_OFFSET_Y = 6;
  // ray "active area" size - if target intersect it - ray attacks target  
  private final static float HERO_RAY_AREA_HALF_SIZE_Y = 2.5f;
  private final static float HERO_RAY_AREA_HALF_SIZE_X = 5.0f;
  // ray power per second
  private final static int   RAY_POWER = 50;

  // ===========================================================
  // Fields
  // ===========================================================
 
  // ray "active area"
  private BodyRectangle RayArea;
  // sprites for left and right parts of the ray
  private AnimatedSprite mHeroLeftRaySprite;
  private AnimatedSprite mHeroRightRaySprite;
  // "fusion" particle system
  public SpriteParticleSystem gunPS;
  // emitter for particle system
  private PointParticleEmitter mPointParticleEmitter;
  // ray has "lock-on-target" system
  public BasicObject mTarget;

  // ===========================================================
  // Constructors
  // ===========================================================
  
  HeroGun(float pX, float pY, ITiledTextureRegion mHeroLeftRayTextureRegion, 
    ITiledTextureRegion mHeroRightRayTextureRegion,
    ITextureRegion mParticlesTextureRegion,
    VertexBufferObjectManager pVertexBufferObjectManager, Camera mCamera) {
    // Creating RayArea 
    RayArea = new BodyRectangle (pX-HERO_RAY_AREA_HALF_SIZE_X, 
     pY - HERO_RAY_AREA_HALF_SIZE_Y + HERO_SPRITE_RAY_OFFSET_Y, 
     HERO_RAY_AREA_HALF_SIZE_X * 2, HERO_RAY_AREA_HALF_SIZE_Y * 2);
    // Creating sprites
    this.mHeroLeftRaySprite = new AnimatedSprite(HERO_LEFT_RAY_OFFSET_X, HERO_SPRITE_RAY_OFFSET_Y, 
     mHeroLeftRayTextureRegion, pVertexBufferObjectManager);
    this.mHeroRightRaySprite = new AnimatedSprite(HERO_RIGHT_RAY_OFFSET_X, HERO_SPRITE_RAY_OFFSET_Y, 
     mHeroRightRayTextureRegion, pVertexBufferObjectManager);
    this.attachChild(this.mHeroLeftRaySprite);
    this.attachChild(this.mHeroRightRaySprite);
    this.mHeroLeftRaySprite.animate(100);
    this.mHeroRightRaySprite.animate(100);
    this.mHeroLeftRaySprite.setScaleCenter(0, this.mHeroLeftRaySprite.getHeight());
    this.mHeroRightRaySprite.setScaleCenter(this.mHeroRightRaySprite.getWidth(), 
                                      this.mHeroRightRaySprite.getHeight());
    // Creating particle system
    mPointParticleEmitter = new PointParticleEmitter(pX, pY);    
    gunPS = new SpriteParticleSystem(mPointParticleEmitter, 20, 40, 50, mParticlesTextureRegion , pVertexBufferObjectManager);
    gunPS.addParticleInitializer(new VelocityParticleInitializer(-100.0f, 100.0f, -100.0f, 100.0f));
    gunPS.addParticleInitializer(new ExpireParticleInitializer(0.3f, 0.8f));
    gunPS.addParticleInitializer(new BlendFunctionParticleInitializer(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE));
    gunPS.addParticleModifier(new ColorParticleModifier(0.0f, 0.3f, 1.0f, 0.0f, 1.0f, 0.3f, 1.0f, 1.0f));
    gunPS.addParticleModifier(new OffCameraExpireParticleModifier(mCamera));
    gunPS.addParticleModifier(new ScaleParticleModifier(0.0f, 0.3f, 1.0f, 0.1f, 1.0f, 0.1f));
    gunPS.addParticleModifier(new AlphaParticleModifier(0.3f, 0.8f, 1.0f, 0.0f));
    // particle system will be attached to the scene 
    // to have simple access to global coords
    // if we attach it here - we will be compelled to use 
    // big constructions like this: GetParent().GetParent().GetX()    
    this.SetRayOff();
    Log.d("Sapfil_Log", "HeroGun created");
  }
  // ===========================================================
  // Getter & Setter
  // ===========================================================
  
  public void SetTarget(IEntity pTarget){
   mTarget = (BasicObject) pTarget;
   if (pTarget != null) 
    Log.d("Sapfil_Log", "HeroGun: Target set");
  }
  
  public BodyRectangle GetRayArea(){
   return RayArea;
  };
   
  // ===========================================================
  // Methods for/from SuperClass/Interfaces
  // ===========================================================
 
  // ===========================================================
  // Methods
  // ===========================================================  
  
  // main updating function
  public int Update(float dt){
   this.UpdateRayArea(this.getParent().getX() + this.getParent().getScaleCenterX(), this.getParent().getY());
   // check - if target is out of "ray_active_area" 
   if (mTarget != null && !mTarget.mBody.Intersect(RayArea)){
     mTarget = null;
     Log.d("Sapfil_Log", "HeroGun: Target lost");
    }
   // check - if target is dead - we must release it
   if (mTarget != null && mTarget.GetHealth() <= 0){
     mTarget = null;
     Log.d("Sapfil_Log", "HeroGun: Target killed");
    }
   // if target is alive and is "locked_on"
   if (mTarget != null)
   {
    mPointParticleEmitter.setCenter(mTarget.getX(), mTarget.getY());
    this.SetRayOn();
    this.SetRayScale(mTarget.getX() + mTarget.getScaleCenterX() - this.getParent().getX(),
               mTarget.getY() + mTarget.getScaleCenterY() - this.getParent().getY());
    mTarget.ReduceHealth((int)(RAY_POWER * dt));
    return 1;
   }
   else{
    this.SetRayOff();
    return 0;
   }
  }
  
  private void SetRayOn(){
   this.mHeroLeftRaySprite.setVisible(true);
   this.mHeroRightRaySprite.setVisible(true);
   this.gunPS.setParticlesSpawnEnabled(true);}
  private void SetRayOff(){
   this.mHeroLeftRaySprite.setVisible(false);
   this.mHeroRightRaySprite.setVisible(false);
   this.gunPS.setParticlesSpawnEnabled(false);}
  
  // Ray scaling - visual stretching of ray sprites
  private void SetRayScale(float target_X, float target_Y){
   this.mHeroLeftRaySprite.setScale((target_X-HERO_LEFT_RAY_OFFSET_X)/mHeroLeftRaySprite.getWidth(),
                -(target_Y/(-HERO_SPRITE_RAY_OFFSET_Y - HERO_RAY_OFFSET_Y)));
   this.mHeroRightRaySprite.setScale(
     (HERO_RIGHT_RAY_OFFSET_X + mHeroRightRaySprite.getWidth() - target_X)/mHeroLeftRaySprite.getWidth(),
        -(target_Y/(-HERO_SPRITE_RAY_OFFSET_Y - HERO_RAY_OFFSET_Y)));
  };
  
  private void UpdateRayArea(float pX, float pY){
   RayArea.setX(pX - HERO_RAY_AREA_HALF_SIZE_X); 
   RayArea.setY(pY + HERO_SPRITE_RAY_OFFSET_Y + HERO_RAY_OFFSET_Y - HERO_RAY_AREA_HALF_SIZE_Y);
  };
  // ===========================================================
  // Inner and Anonymous Classes
  // ===========================================================
};
Вот код героя:
package ru.sapfil.AETest2;

class Hero extends BasicObject implements IParticleEmitter{
  // ===========================================================
  // Constants
  // ===========================================================
  private final static float HERO_BODY_WIDTH = 52;
  private final static float HERO_BODY_HEIGHT = 52;
  private final static float HERO_BODY_HEALTH = 100;
   
  private final static float HERO_ENGINE_PARTICLES_OFFSET_X = 15.5f;
  private final static float HERO_ENGINE_PARTICLES_OFFSET_Y = 46f;  
  
  // ===========================================================
  // Fields
  // ===========================================================
  
  private float maxSpeed = 100;
  public HeroGun mHeroGun;  
   
  // ===========================================================
  // Constructors
  // ===========================================================

 Hero(final ITextureRegion mHeroFaceTextureRegion, 
    
   VertexBufferObjectManager pVertexBufferObjectManager, Camera mCamera){
  super(mCamera.getWidth()/2, mCamera.getHeight()-2*mHeroFaceTextureRegion.getHeight(), 
    mHeroFaceTextureRegion, pVertexBufferObjectManager, 
    0, 0, HERO_BODY_WIDTH, HERO_BODY_HEIGHT, HERO_BODY_HEALTH, mCamera);
  Log.d("Sapfil_Log", "Hero created");
  }
 
  // ===========================================================
  // Getter & Setter
  // ===========================================================
  
  public void CreateGun(ITiledTextureRegion mHeroLeftRayTextureRegion, 
    ITiledTextureRegion mHeroRightRayTextureRegion,
    ITextureRegion mParticlesTextureRegion,
    VertexBufferObjectManager pVertexBufferObjectManager, Camera mCamera)
  {mHeroGun = new HeroGun(this.getX() + this.getWidth()/2, this.getY(), 
                    mHeroLeftRayTextureRegion, mHeroRightRayTextureRegion,
                    mParticlesTextureRegion,
                    pVertexBufferObjectManager, mCamera);
  this.attachChild(mHeroGun);
  Log.d("Sapfil_Log", "HeroGun attached to hero");
  }
  
     public BodyRectangle GetRayArea(){
      return  mHeroGun.GetRayArea();
     }
   
  // ===========================================================
  // Methods for/from SuperClass/Interfaces
  // ===========================================================
  
  public void getPositionOffset(float[] pOffset) {
   pOffset[VERTEX_INDEX_X] = this.getX() + HERO_ENGINE_PARTICLES_OFFSET_X;
   pOffset[VERTEX_INDEX_Y] = this.getY() + HERO_ENGINE_PARTICLES_OFFSET_Y + 5; 
  }
 
  // ===========================================================
  // Methods
  // ===========================================================
   
  public int Update (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();
    return this.mHeroGun.Update(dt);
  }
}
Вот код основного цикла игры:
scene.registerUpdateHandler(new IUpdateHandler() {
            public void onUpdate(float pSecondsElapsed) {              
             // updating asteroids
             switch (mAsteroids.Update(pSecondsElapsed)) {
          case 0: {};           
             }     // creating new asteriod                      
              if (mAsteroids.GetCount() < 10){
               mAsteroids.AddNewObject(mRandom.nextInt((int)sapCamera.getWidth()), -15, mRandom.nextInt(20)-10, mRandom.nextInt(30), 26, 26, 100);
             };
             int i = 0; //target number
             switch (hero.Update(pSecondsElapsed, accellerometerSpeedY, accellerometerSpeedX))
             {
             case 0:{ //if heroGun has no target
              i = mIntersector.CollWithAreaIntersect(mAsteroids, hero.GetRayArea());
                 if (i > -1) { //if intersection is found
                   hero.mHeroGun.SetTarget(mAsteroids.getChildByIndex(i));                                  
                  }
                 else { hero.mHeroGun.SetTarget(null); }
             break;}
                case 1: { //if heroGun has target
                 mAsteroids.ModifySpeedByID(i, 0.8f, pSecondsElapsed);
        hero.ModifySpeed(0.8f, pSecondsElapsed);
        break;}
             }
             // hero with asteroids collision
             mIntersector.CollWithSingleIntersect(mAsteroids, 0, hero, 0, true);             
            }
            public void reset() {
                // TODO Auto-generated method stub
            }
        });
п.с. В последнем коде ошибка - она нашлась уже после публикации поста. И я решил ее не исправлять тут :) Кто найдет - тому бублик!

Комментариев нет:

Отправить комментарий