воскресенье, 11 ноября 2012 г.

Рефакторинг кода. Продолжаем наводить порядок.


Я всегда так пишу программы. От частного к общему. Главная причина в том, что я заранее не знаю, куда иду. Ибо зачастую путь и есть цель. (О как!) Но многие вполне справедливо имеют право закидывать меня помидорами и сыпать не менее красивыми цитатами. (Вроде "Не зная куда идешь - придешь в никуда.") Что ж - у каждого своя голова на плечах. Но тот, кто прочитает мой блог и, возможно, будет копировать мой код - вместе со мной придет к моему рефакторингу.

Теперь ближе к делу. Есть у нас в игре пока два вида объектов. Герой и Астероид. И у них много общего. А потом появятся еще объекты, и под каждый пришлось бы писать код с нуля... если бы не рефакторинг, который мы сейчас и проведем! (Кода будет больше чем в предыдущих постах!)



Вот код некоторого базового объекта, от которого мы позже унаследуем наши астероиды, героя и прочие вкусности. Кроме того, обратите внимание на поле mBody. Оно необходимо для физического взаимодействия между объектами. В этом посте мы о нем говорить не будем. Вы можете либо выпилить его из кода, либо прочитать следующий пост на эту тему, либо написать свой класс BodyRectangle.
package ru.sapfil.AETest2;

import org.andengine.engine.camera.Camera;
import org.andengine.entity.sprite.Sprite;
import org.andengine.opengl.texture.region.ITextureRegion;
import org.andengine.opengl.vbo.VertexBufferObjectManager;

public class BasicObject extends Sprite implements GlobalConstants{
 // ===========================================================
 // Constants
 // ===========================================================
 // ===========================================================
 // Fields
 // ===========================================================

 public float vx, vy;
 private float deltaX, deltaY;
 private float health;
 public BodyRectangle mBody;
 
 final Camera mCamera;
   
 // ===========================================================
 // 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;
 };
 // ===========================================================
 // Getter & Setter
 // ===========================================================
 
 public float GetSpeedX(){return vx;} 
 public float GetSpeedY(){return vy;};
 public void ReverseX(){ vx *= -1;};
 public void ReverseY(){ vy *= -1;};
 public void SetSpeed(float vx, float vy){
  this.vx = vx; this.vy = vy;
 }
 // ===========================================================
 // Methods for/from SuperClass/Interfaces
 // ===========================================================
 // ===========================================================
 // Methods
 // ===========================================================
 
 // 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;  
 }
 
 // returns sprite to the screen if it is partially offscreen
 public void StayInCamera(){
  if (this.getX() < 0)
   this.setX(0);
  if (this.getX() > mCamera.getWidth() - this.getWidth())
   this.setX(mCamera.getWidth() - this.getWidth());
  if (this.getY() < 0)
   this.setY(0);
  if (this.getY() > mCamera.getHeight() - this.getHeight())
   this.setY(mCamera.getHeight() - this.getHeight());
  resetBody();
 }
 
 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;  
 };  
 // ===========================================================
 // Inner and Anonymous Classes
 // =========================================================== 
}
Как видим, функция Update() возвращает разные значения.
По традиции возврат значения 0 обозначает, что все ОК. Ну а другие значения мы будем использовать для "нестандартных" состояний объекта.
Если функция вернула 1, значит объект на экране, но его здоровье равно или ниже нуля. Например, наш герой стрелял-стрелял в астероид и наконец его "убил", значит нужно не просто убрать его из программы, но и создать красивый взрыв на его месте. Или вот например дадим мы герою бластер, стреляющий сгустками плазмы. Каждый такой сгусток - такой же объект. При попадании в цель он будет отнимать у цели чуток здоровья, при этом сам он будет полностью терять свое здоровье. Возврат единицы позволит нам создать в месте попадания пули в объект - какой-нибудь красивый эффектик.
2 обозначает, что объект (тот же астероид) улетел за экран и его надо убирать из программы, но при этом совсем не нужно красивых эффектов (к тому же сильно жрущих ФПС). Геройские пули, так же улетающие за экран - не нуждаются в спец-эффектах перед удалением.

Функция StayInCamera() нужна нам в основном для героя, но мало ли что - пусть она будет в базовом классе.

Класс полностью соответствует нашим пожеланиям к астеройдам, по-этому Asteroid.java удален из моего пакета. Hero.java теперь имеет вот такой вид:
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, 40, 100, mCamera);
  } 
  // ===========================================================
  // Getter & Setter
  // =========================================================== 
  // ===========================================================
  // Methods for/from SuperClass/Interfaces
  // ===========================================================
  public void getPositionOffset(float[] pOffset) {
   pOffset[VERTEX_INDEX_X] = this.getX() + 13f;
   pOffset[VERTEX_INDEX_Y] = this.getY() + 40f; 
  }
 
  // ===========================================================
  // Methods
  // ===========================================================
   
  public void heroUpdate (float dt, float acs_dx, float acs_dy){
    this.SetSpeed(-acs_dx * maxSpeed, acs_dy * maxSpeed);
    this.Update(dt);
    this.StayInCamera();  
  }
  // ===========================================================
  // Inner and Anonymous Classes
  // ===========================================================  
}
Теперь нам нужен класс, занимающийся обработкой группы базовых объектов. Пока я написал простой класс. Он будет создавать идентичные объекты. Позже, конечно, нужно будет переписать его так, чтобы можно было создавать разные, но подобные объекты. Например, те же астероиды разных размеров и форм. Но пока есть вот такой класс:
package ru.sapfil.AETest2;

import java.util.ArrayList;
import org.andengine.engine.camera.Camera;
import org.andengine.entity.Entity;
import org.andengine.opengl.texture.region.ITextureRegion;
import org.andengine.opengl.vbo.VertexBufferObjectManager;

public class BasicObjectsCollection extends Entity implements GlobalConstants{
 // ===========================================================
 // Constants
 // ===========================================================   
 // ===========================================================
 // Fields
 // ===========================================================
   
 public final ArrayList mList = new ArrayList();
 private int count;
 private ITextureRegion mTextureRegion;
 private VertexBufferObjectManager mVertexBufferObjectManager;
 private Camera mCamera;
 
 // ===========================================================
 // Constructors
 // ===========================================================
 
 BasicObjectsCollection(ITextureRegion mTextureRegion, 
            VertexBufferObjectManager pVertexBufferObjectManager, Camera camera){
  this.mCamera = camera;
  this.mTextureRegion = mTextureRegion;
  this.mVertexBufferObjectManager = pVertexBufferObjectManager;
  count = 0;
 }
 // ===========================================================
 // Getter & Setter
 // ===========================================================
 
 public int GetCount(){ return count;}
  
 // ===========================================================
 // Methods for/from SuperClass/Interfaces
 // =========================================================== 
 // ===========================================================
 // Methods
 // ===========================================================
 
 public void AddNewObject (float x, float y, float vx, float vy, float w, float h, float health){
  mList.add(new BasicObject(x, y, mTextureRegion, mVertexBufferObjectManager, 
            vx, vy, w, h, health, mCamera));
  this.attachChild(mList.get(count));
  count++;
 };
  
 public byte Update (float dt){
  for (int i = 0; i < count; i++) {
   switch (mList.get(i).Update(dt)){
   case 2: 
   case 1: {this.detachChild(mList.get(i)); mList.remove(i); count--; break;}
   case 0: {};
   default: {};
   };
  }
  return 0;
 };
};
     Тут хотелось бы сказать вот о чем. Весь AndEnginre сильно завязан на свой класс Entity. Практически все объекты (Scene, Sprite и пр.) - его наследники. У Entity есть 2 важных метода - AttachChild(Entity) и DetachChild(Entity). Во-первых, добавлять детишек можно сколько угодно. Во вторых, каждое дитё так же может иметь своих детишек. В третьих, все детишки всех поколений отправляются на рэндер. (Конечно, в том случае, если их прародитель добавлен к сцене методом AttachChild() ). В нашем классе есть список ArrayList. Я где-то уже писал про него. Так вот при создании нового объекта нам надо не только добавить его в контейнер ArrayList, но еще и "прогнать" его через метод AttachChild(). С другой стороны, при удалении объекта надо провернуть все в обратном порядке - сначала detachChild(), а потом mList.remove(i).
      Можно еще вот что сделать. Сейчас в моем коде в сцену сваливается все вперемешку. Однако можно создать класс Layer, унаследованный от Entity. И добавить в сцену слои, а уже в слои добавлять детишек. Я это не придумывал, а подсмотрел в примерах к тому же AndEngine (Конкретно в игрушке Snake).
       Забыл сказать - в функции Update() пока что нет различных решений для объектов, вернувших 1 или 2 в своем Update(). И те и те просто убираются со сцены.

      Пока все. Чуть позже будет пост про то, как заставить объекты взаимодействовать. Так же я еще не разбирался со SpriteBatch и SpriteGroup. Эти две волшебные штуки должны помочь улучшить производительность при обработке одинаковых спрайтов.

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

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