четверг, 21 марта 2013 г.

Сборные объекты.


Идея сборных объектов появилась по нескольким причинам.
1. Если нет отдельного художника в команде, то рисовать огромное разнообразие врагов - тяжко. Проще сделать несколько "запчастей" и собирать врагов из этих частей.
2. Каждой запчасти можно назначить свои свойства. То есть мы однажды закодили пушку, стреляющую определенными пулями. И в дальнейшем мы просто добавляем ее в конструкцию очередного врага и больше не паримся над кодом для ее стрельбы.
3. Нам нужно меньше текстур для игры.


Проект полностью в текущем состоянии. (DropBox)


ВАЖНОЕ ЗАМЕЧАНИЕ!То, что я выложу сегодня - далеко не окончательный вариант и нуждается в некотором рефакторинге. Однако, я давно обещал выложить этот пост и больше не могу заставлять читателя ждать. Теперь к делу. Начнем с описания врагов в XML.
В предыдущем своем игростройском опыте я использовал Техно-сэт. Враги состояли из железок и потому стыковать их можно было как угодно. В этот раз я решил усложнить для себя задачу. И сделал живых пришельцев, соединяемых по гексагональной схеме (шестиугольниками). Это требует точной стыковки между частями врага.На картинке вы можете видеть, что швы между частями практически отсутствуют. Если будете делать что-то подобное - очень советую ничего не анимировать на стыке спрайтов, иначе сложность графической работы вырастет для вас в разы.


     
       
     
     
     
     
     
     
     
     
          
     
     
     
          
     
     
     
     
     
     
     
   
Поле too (type of object) будет у нас использоваться для идентификации объекта и выбора способа его поведения. Хотя, можно для этой цели использовать ID. См "важное замечание" выше. Однако, учитывая мой предыдущий опыт, могу сказать, что это поле в будущем не будет лишним. А, возможно, понадобятся еще дополнительные поля с параметрами.Поля X и Y отвечают за размещение региона в текстуре. Поля BX, BY - верхний левый угол тела объекта, BH и BW - высота и ширина тела. Напомню, что тело нужно для расчета коллизий. А проще говоря, чтобы в это тело могла попасть геройская пуля. Заметьте, что у объектов с ID больше 250 нет параметров тела. А значит и нет самого тела, а значит в коллизиях они участвовать не будут. На практике в данном примере эти объекты являются щупальцами пришельцев. Я решил, что геройские патроны будут пролетать сквозь них. На видео это иногда видно. Однако, объекты, не имеющие тела, все равно имеют параметр "здоровье". Можно указать любое число больше нуля. Здоровье объекта проверяется при еже кадровом апдэйте. Поле tob (type of blast) говорит само за себя. Используется при уничтожении объекта. Передается как параметр в процедуру создания взрыва. Поля fps, fh, fw - отвечают за анимацию. fh и fw - это количество кадров по ширине и высоте в исходном png-файле. А вот fps указан отрицательным. Позже, в коде, он конечно меняет знак. Но давайте я сразу объясню, для чего я это сделал. Если ФПС положительный, то объект анимируется обычно. Если отрицательный, то его знак меняется в коде и к нему добавляется случайное целое число в промежутке от -5 до +5. Таким образом, все части пришельца двигаются рассинхронизировано, что придает ему более живой вид. Но это ИМХО. :) Как видите, у последних объектов нет полей, отвечающих за анимацию. Делаем вывод, что это неанимированные объекты. Так. Теперь ниже в том же XML файле надо бы описать конструкцию самого пришельца. Для краткости возьму самого маленького - просто глазик с щупальцами по краям (на видео он есть).




Поле ID. Советую во всей базе данных никогда не дублировать id. Например, в моем XML хранятся как данные частей врагов, так и данные о конструкции врага, что по сути является совсем разными штуками. Однако, позже, при создании объектов, я сортирую их по id. И в зависимости от него - вновь создаваемый объект попадает в тот или иной список. Задники к задникам, Астероиды к астероидам, враги к врагам. Поле total - используется и при чтении из базы данных, и при создании объекта в списке. Поля p0, p1, p2... - это ID тех "запчастей", из которых будет состоять враг. Поля с1, с2... - это зависимость запчастей. Например, если мы увидим запись с7="4", то это будет означать, что 7й объект зависим от 4го. И при смерти 4го объекта 7й объект тоже уничтожится. На примере все объекты зависимы от нулевого (с глазиком). В более сложных конструкциях могут быть многоуровневые зависимости, но в итоге все части зависят от центрального объекта, обозначенного р0. В игре это всегда будет некий мозговой центр. И если убить его - помрет весь враг Поля х1 и у1, х2 и у2, ... - это расположение "запчасти" относительно центрального объекта р0.  Далее. Куда и как "читается" база данных. В файле DataBaseFromXML есть такой класс:
 partsList    = new ArrayList();
    public ArrayList dependList  = new ArrayList();
    public ArrayList connectionX = new ArrayList();
    public ArrayList connectionY = new ArrayList();
    public EnemyConstructionSample (int id, int total){
     this.id = id;
     this.total = total;
    }
   }
Думаю, что здесь комментарии особо не нужны. А ниже описана та часть кода, которая заполняет карту HashMap.
if (total != 0){
      EnemyConstructionSample mSample = new EnemyConstructionSample(id, total);
      for (int i= 0; i < total; i++){
       int partID   = 201;
          int dependanse  = 0;
          float connectionX = 0;
          float connectionY = 0;
          
       try {partID = SAXUtils.getIntAttributeOrThrow(atts,"p" + i);}catch (Exception ex){ };    
       try {dependanse = SAXUtils.getIntAttributeOrThrow(atts,"c" + i);}catch (Exception ex){ };    
       try {connectionX = SAXUtils.getFloatAttributeOrThrow(atts,"x" + i);}catch (Exception ex){ };    
       try {connectionY = SAXUtils.getFloatAttributeOrThrow(atts,"y" + i);}catch (Exception ex){ };    
       mSample.partsList.add(i, partID);
       mSample.dependList.add(i, dependanse);
       mSample.connectionX.add(i, connectionX);
       mSample.connectionY.add(i, connectionY);       
      }
      this.EnemyConstructionMap.put(id, mSample);
     }
Здесь комментарий только один. Так как мы используем SAX-подход в парсинге, то нам самим приходится определять - какой элемент мы сейчас читаем. Поле total присутствует только в элементах EnemyConstruction. И, соответственно, проверка (total != 0) нужна, чтобы понять, что мы читаем действительно очередную вражескую конструкцию. Дефолтное значение id=201 сделано для удобства, хотя можно его тоже обнулить.  Далее. Записываем в level.xml наших глазастиков:

 
  
 
Напомню, что пока все объекты летят равномерно-прямолинейно. Начальная координата задана полями Х и У, а скорость VX, VY. Если какое-то поле не задано, то в коде оно берется равным нулю. Далее. Вот часть MainActivity, отвечающая за создание нового объекта:
if (!endOfLevel){              
             if (mLevel.TimeList.get(counterDB).time + startTime < mEngine.getSecondsElapsedTotal()){                  
              if (mLevel.TimeList.get(counterDB).id < 200)
               mAsteroids.AddNewObject(
                mLevel.TimeList.get(counterDB).id,
                mLevel.TimeList.get(counterDB).x,
                mLevel.TimeList.get(counterDB).y,
                mLevel.TimeList.get(counterDB).vx,
                mLevel.TimeList.get(counterDB).vy);
              else 
               mEnemies.AddNewObject(
                mLevel.TimeList.get(counterDB).id,
                mLevel.TimeList.get(counterDB).x,
                mLevel.TimeList.get(counterDB).y,
                mLevel.TimeList.get(counterDB).vx,
                mLevel.TimeList.get(counterDB).vy);
              counterDB++;
              if(counterDB >= mLevel.TimeList.size())
               endOfLevel = true;      
              }}                 
Как видите, в зависимости от ID команда AddNewObject подается в разные списки. Далее. Процедура создания нового объекта в списке врагов - тривиальна. Не буду ее тут приводить. Интереснее уже сам файл EnemyObject.java. Дело в том, что сборный враг имеет признаки как объекта, так и списка объектов. Однако, в будущем я уберу их него признаки объекта, такие как глобальная координата, скорость и прочее. Вот конструктор:
public EnemyObject(int id, float pX, float pY, float pVX, float pVY) {
  this.vx = pVX; this.vy = pVY;
  this.setX(0); this.setY(0);
  this.total = mDB.EnemyConstructionMap.get(id).total;
  this.totalAlive = this.total;
  
  for (int i = 0; i < mDB.EnemyConstructionMap.get(id).total; i++){   
   this.dependList.add(mDB.EnemyConstructionMap.get(id).dependList.get(i));
   HealthObject tmp = new HealthObject(mDB.EnemyConstructionMap.get(id).partsList.get(i),
     pX + mDB.EnemyConstructionMap.get(id).connectionX.get(i),
     pY + mDB.EnemyConstructionMap.get(id).connectionY.get(i),
     pVX, pVY);
      
   this.attachChild(tmp);   
   this.mList.add(tmp);
   }  
  };
Да, кстати. У многих классов теперь появились статические поля. например, у класса EnemyObject есть такое:
public static DataBaseFromXML mDB; 
Введение статических полей значительно облегчило мне кодинг. Теперь не надо передавать ссылки на базу данных или на контроллер частиц. Достаточно один раз заранее прописать статическое поле. Ну а что касается конструктора - думаю, что тут все просто. Берем из базы данных все параметры для каждой "запчасти" и создаем ее, аттачим и все.  Далее. Самая "хитровыделанная" функция - это апдэйт. Причем в будущем эта функция распухнет, обзаведется множеством ответвлений внутри себя. Ведь для каждого отдельного типа объекта надо будет отдельно просчитывать апдэйт. Пушки должны стрелять, двигатели должны менять курс и т.п. Как следствие. функция станет тормозной. И потребует оптимизации. Например, проверку смерти "родителя" можно делать не каждый раз. А за один проход проверять только один объект из конструкции. Код, как он есть сейчас:
public byte Update(float dt){
  if (mList.get(0).getY() >= CAMERA_WIDTH + 200)
   return 1;
  counter +=dt;
  
  for (int i = 0; i < total; i++) { 
   
    switch (mList.get(i).Update(dt)){
     case 2: {
      if (mList.get(i).GetTypeOfObject() == -220 || mList.get(i).GetTypeOfObject() == -221)
      for (int k = 0; k < 6; k++){
       float angle = mRandom.nextFloat() * 2 * 3.1415f;
       mEnemiesShots.AddNewObject(300, mList.get(i).getX()+mList.get(i).getRotationCenterX(), 
        mList.get(i).getY()+mList.get(i).getRotationCenterY(), 
        200 * (float)Math.sin(angle), 200 * (float)Math.cos(angle) );       
      }
     totalAlive -=1;
     this.getChildByIndex(i).setVisible(false);      
     break;}
    default: {};
    }
   if (mList.get(i).GetHealth() > 0 && mList.get(dependList.get(i)).GetHealth() <= 0){ // if connected parent is dead   
    mList.get(i).ReduceHealth(1000 * dt);  
   }
   switch (mList.get(i).GetTypeOfObject()){
   case 213 :{ if (counter > fireRate)mEnemiesShots.AddNewObject(300, mList.get(i).getX()+mList.get(i).getRotationCenterX(), 
                 mList.get(i).getY()+mList.get(i).getRotationCenterY(), 0, 200); break;}   
   case 212 :{ if (counter > fireRate)mEnemiesShots.AddNewObject(300, mList.get(i).getX()+mList.get(i).getRotationCenterX(), 
     mList.get(i).getY()+mList.get(i).getRotationCenterY(), -100, 172); break;}
   case 214 :{ if (counter > fireRate)mEnemiesShots.AddNewObject(300, mList.get(i).getX()+mList.get(i).getRotationCenterX(), 
     mList.get(i).getY()+mList.get(i).getRotationCenterY(), 100, 172); break; }      
   default: {}
   }
  }
  if (counter > fireRate)
   counter = 0;
  if (totalAlive == 0)
   return 2;  
  return 0;}
Вначале делаем проверку того, что объект улетел за экран. Здесь пока примитивнейшая проверка, в будущем придется делать ее более умной. Ведь враги научатся улетать не только вниз. К тому же их размеры сильно отличаются друг от друга, а здесь взят просто какой-то размер 200. Взял я его с запасом для проверок.  Далее. Для каждого объекта делаем Update(dt). Если процедура Апдэйта вернула 2, значит объект убит. Однако, у меня есть особенные объекты - некие "бомбы", которые при смерти должны разбросать несколько вражеских снарядов по случайным направлениям. Я долго думал. как это сделать. И остановился на том, чтобы инвертировать id мертвого объекта. Такой объект больше не подается на рэндер и не апдейтится, не участвует в коллизиях. Но его отрицательный id еще может сыграть свою роль. Ниже. Проверяем родительский объект. Если он умер, то надо убить и дочерний объект. Однако, просто обнулить - не интересно и не оптимально. Если просто обнулить здоровье, то в этот единственный кадр погибнет вся ветка из связанных элементов. И если ветка большая, то данный проход сильно просадит ФПС. Ниже. Объекты с ID 212, 213, 214 - это вражеские пушки. И, конечно, они добавляют объекты в список вражеских снарядов. Кстати, список вражеских снарядов, естественно тоже добавлен в класс, как статическое поле. Кстати, тот кто будет смотреть полную версию кода - увидит в классе EnemyObject после процедуры Update некий закомментированный кусок кода. Этот кусок использовался ранее для прицельной стрельбы по герою. Снаряд генерировался в центре вражеской конструкции. Можете использовать его по своему усмотрению. Все. Моя технология создания сборных объектов описана. Далее ее надо просто развивать, дорисовывая новые запчасти, дописывая их в базу данных и допиливая для них код в процедуре Апдейта сборного врага.

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

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