Автор: Антон Овчинников «CoderAX27»

Опубликовано: 24.05.2010

Изменено: 24.05.2010

Постоянная ссылка

Комментарии [14]

Создание простейшего движка, с разработкой собственной физики (Simple Runner)





Страницы: 1 2

6. Физика и обработка столкновений с объектами

Физика и все что с ней связано – это самое главное и интересное в этой статье. Без физики и простейших столкновений ощущение реализма пропадает на 100%, потому что теряется абсолютно все, что может доставить игроку удовольствие от игр: элементарные столкновения, взятие предметов, уничтожение врагов и прочее.

Создание простейшего движка, с разработкой собственной физики (Simple Runner)

Как уже было сказано ранее, обработка столкновений в движке "Simple Runner" осуществляется в зависимости от типа значения переменной modelColl у того или иного объекта. Первым делом следует рассмотреть обновление состояния и положения персонажа в пространстве:

// Обновление состояния персонажа
void UpdatePl(int gp, float timeDelta)
{
  // -------- Изменение высоты персонажа (падение) --------
  if (pl[gp].squat)
  {
    pl[gp].hSquat = pl[gp].hmin;
  }
  else
  {
    if (pl[gp].hSquat == pl[gp].hmin)
       pl[gp].pos.y += pl[gp].hmax*0.5f;      /* Данное преобразование необходимо потому что условный центр персонажа 
       расположен в центре его, а это означает, что масштабирование его высоты (и размеров тоже) может повлиять на его 
       взаимодействие с другими объектами (в том чисте и потом). То есть, если увеличение высоты персонажа (hSquat) 
       больше толщины пола, то центр персонажа окажется под полом, следовательно песонаж после обработки столкновения 
       окажется под полом */

    pl[gp].hSquat = pl[gp].hmax;
  }

  // Плюсуем вектора до их обновления, в целях корректности физики. Например, персонаж не падает если ему 
  // какой-либо объект укорачивает векторы перемещения до нуля
  pl[gp].pos += pl[gp].vec;

  // Падение персонажа
  pl[gp].vec.y -= FLOAT(Sc::g*timeDelta*5.0f);
  if (pl[gp].vec.y<-20.0f)  pl[gp].vec.y = -20.0f;

  // Плавное укорачивание векторов
  vif (!pl[gp].phantom)
  {
    pl[gp].vec*=0.9f;
  }

  // Для следующих действий необходимо, что-бы персонаж был жив:
  if (pl[gp].health>0)
  {
    // Если персонаж оказался ниже предельно допустимой высоты, то он погибает
    if (pl[gp].pos.y<Sc::h_min)
    {
      pl[gp].timeDeath = Sc::currTime;
      pl[gp].health = 0;
      if (gp == agp)
         SUtil_ReCreateAndPlaySound( Sc::pXAudio2, &sn::sBackground, specials::ssLose );
      else
         SUtil_ReCreateAndPlaySound( Sc::pXAudio2, &sn::sPlayerDeath, spl::ssDeath );
    }

    // Регенерация жизни с учетом индивидуальных настроек персонажа
    if ((float)timeGetTime() – pl[gp].timeHealth Regen>= pl[gp].spHealthRegen*1000)
    {
      pl[gp].timeHealthRegen = (float)timeGetTime	

      if (pl[gp].health<pl[gp].healthRegenTo)
           pl[gp].health+=pl[gp].stepHealthRegen;
      if (pl[gp].health>pl[gp].maxHealth)
           pl[gp].health-=pl[gp].stepHealthRegen;
    }
  }
}

Вторым шагом следует обратить внимание на столкновение персонажа с ландшафтом, причем движок поддерживает работу со сценой без создания ландшафта:

// При присутствии ландшафта производятся проверки на столкновения с ним
void CollisionsPltoTerrain(int gp)   //Terraine XYZ max and min ...
{
  const float st = 0.001f;
  if (Sc::terrain)
  {
    if (!pl[gp].phantom)
    {
      // Столкновения с крутыми частями ландшафта
      float heightTerrin1;

      do {
         heightTerrin1 = TheTerrain->getHeight( pl[gp].pos.x+1, pl[gp].pos.z ) + pl[gp].hSquat;
         pl[gp].pos.x+=st;
      } while (TheTerrain->hitInTerrain(pl[gp].pos.x+1,pl[gp].pos.z) && heightTerrain(gp)>heightTerrin1+2.0f);

      do {
         heightTerrin1 = TheTerrain->getHeight( pl[gp].pos.x-1, pl[gp].pos.z ) + pl[gp].hSquat;
         pl[gp].pos.x-=st;
      } while (TheTerrain->hitInTerrain(pl[gp].pos.x-1,pl[gp].pos.z) && heightTerrain(gp)>heightTerrin1+2.0f);

      do {
         heightTerrin1 = TheTerrain->getHeight( pl[gp].pos.x, pl[gp].pos.z+1 ) + pl[gp].hSquat;
         pl[gp].pos.z+=st;
      } while (TheTerrain->hitInTerrain(pl[gp].pos.x,pl[gp].pos.z+1) && heightTerrain(gp)>heightTerrin1+2.0f);

      do {
         heightTerrin1 = TheTerrain->getHeight( pl[gp].pos.x, pl[gp].pos.z-1 ) + pl[gp].hSquat;
         pl[gp].pos.z-=st;
      } while (TheTerrain->hitInTerrain(pl[gp].pos.x,pl[gp].pos.z-1) && heightTerrain(gp)>heightTerrin1+2.0f);
    }

    float lim = 2.0f;
    if (!TheTerrain->hitInTerrain(fabs(pl[gp].pos.x) + lim, fabs(pl[gp].pos.z)+lim))
       // Запрет выхода за пределы ландшавта
       TheTerrain->returnPosInTerrain(&pl[gp].pos.x, &pl[gp].pos.z,lim);

    /* Строение следующего цикла довольно хитрое, дело в том, что в сцене два ландшафта, столкновения 
    высчитываются только с главным, а контролировать высоту позиций персонажей необходимо с учетом двух ландшафтов, 
    поэтому сначала корректируется координата Y персонажа если она меньше высоты главного ландшафта, а потом 
    производятся теже действия, но только относительно второго (дополнительного) ландшафта. */
    float hTerr = heightTerrain(gp);
    do {
      if (pl[gp].pos.y < hTerr)
      {
        pl[gp].pos.y = hTerr;
        pl[gp].fly = false;
        pl[gp].CollisionYup = true; // Персонаж находится на ландшавте, т.е. столкнулся с ним
      }
      hTerr = adjTerrain->getHeight( pl[gp].pos.x, pl[gp].pos.z ) + pl[gp].hSquat;
    } while (pl[gp].pos.y < hTerr);
  }
}

Пришло время рассмотреть главную функцию, отвечающую за физику:

// Главная функция отвечающая за физику
void Phisic(float timeDelta)
{
  float Delta = 0.04f;

  /* Прогоняем алгоритмы столкновений необходимое количество раз, во избежание прохождений сквозь объекты и прочих 
  проблем, которые могли бы быть вызваны, например, в результате зависания, даже на очень короткое время, 
  т.к. физика работает в режиме реального времени, т.е. с учетами интервалов времени после начала предыдущего кадра */
  do
  {
    KeyManaged(Delta);

    for (int i = 0; i<plCount; i++)
      // Физика и интеллект персонажей задействуются, только при расстоянии меньшего 40.0f 
      // по отдельным осям, за исключением привидения
      if (pl[agp].pos.x - pl[i].pos.x<40.0f && 
          pl[agp].pos.y - pl[i].pos.y<40.0f &&
          pl[agp].pos.z - pl[i].pos.z<40.0f || pl[i].phantom)
      {
        // Контроль столкновений персонажа с объектами, находящимися выше и ниже его
        pl[i].CollisionYup = false;
        pl[i].CollisionYdown = false;

        // Предполагаем, что персонаж в полете, но если произойдет столкновение, в частности преземление 
        // на какой-либо из объектов, то этот флаг меняется и тем самым дает нам, понять, что к примеру, 
        // можно воспроизводить стук шагов при передвижении персонажа!
        pl[i].fly = true;

        UpdatePl(i, Delta);

        // CollisionsPltoTerrain(i);
        /* Данная функция здесь чисто для улучшения качества столкновений, когда персонаж зажат между ландшафтом 
        и каким-либо объектом, в принципе её можно убрать, т.к. ниже она еще раз повторяется. Убирать снизу её нельзя, 
        ибо в подобном случае персонаж будет проваливаться под ланжшафт! */

        if (!pl[i].phantom)
        {
          /* В принципе следующую проверку на столкновения между персонажами можно убрать, т.к. данные столкновения 
          проверяются при перемещении того или иного персонажа относительно него-же самого, но если вдруг один 
          персонаж упадет на другого (а это происходит не по его воле), то последний может провалиться под пол, 
          потому что при падении персонажа столкновения не просчитываются, без этой проверки: */
          CollisionsPlwithPl(i);
          CollisionsPlwithObj(i,Delta);
        }
        CollisionsPltoTerrain(i);
    }
    timeDelta -= 0.04f;
  } while (timeDelta > 0.04);

  if ((float)timeGetTime() - pl[agp].lastActivObjTime > 2000.0f)
     pl[agp].activateObj = false;
  // Обновляем положение камеры
  TheCamera.setPosition(&pl[agp].pos);
}

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

// Управление с клавиатуры
void KeyManaged(float Delta)
{
  // Выход в игровое меню, а не главное
  if (DXUtil_getKeyTimeLim(VK_ESCAPE,&Sc::lastKeyDownTime, Sc::DeltaKeyDownTime))
  {
    Sc::gamePause = true;
    SUtil_DestroySound(&sn::sBackMenu);
    SUtil_CreateAndPlaySound( Sc::pXAudio2,&sn::sBackMenu, it::ssBackMenu );
  }

  // Все остальное управление отвечает за персонажа, поэтому оно не должно перемещать его, когда он мертв
  if (pl[agp].health < 1) return;

  // ------- Стрельба и прицеливание -------

  // Если персонаж лег или присел, то происходит замедление его скорости
  if (pl[agp].squat) Delta *= 0.25f;

  // Приближение в случае прицеливания на правую кнопку
  if ( ::GetAsyncKeyState( VK_RBUTTON ) & 0x8000f )
  {
    Sc::perspect = 0.1f;
  }
  else
  {
    Sc::perspect = 0.25f;
  }

  // Стрельба
  if ( DXUtil_getKeyTimeLim(VK_LBUTTON, &pl[agp].lastShootTime, 1) )
  {
    pl[agp].fire = true;
    SUtil_ReCreateAndPlaySound( Sc::pXAudio2,&sn::sPlayerFire, spl::ssFireLeft);
  }
  if ( DXUtil_getKeyTimeLim(VK_RBUTTON, &pl[agp].lastShootTime, 1) )
  {
    SUtil_ReCreateAndPlaySound( Sc::pXAudio2,&sn::sPlayerFire, spl::ssFireRight);
  }

  // Выбор оружия
  if ( DXUtil_getKeyTimeLim(VK_1, &pl[agp].lastShootTime, 1) )
  {
    pl[agp].active_ws = ws::spade;
  }
  if ( DXUtil_getKeyTimeLim(VK_2, &pl[agp].lastShootTime, 1) )
  {
    pl[agp].active_ws = ws::javelin;
  }
  if ( DXUtil_getKeyTimeLim(VK_3, &pl[agp].lastShootTime, 1) )
  {
    pl[agp].active_ws = ws::stake;
  }

  // -------- Изменение состояний персонажа --------

  float speedStep;

  // Увеличение скорости передвижения персонажа при удерживании нажатия заданной клавиши
  if ( ::GetAsyncKeyState( options.kbrAcceleration ) & 0x8000f )
  {
    Delta = Delta*2;
    speedStep = 0.2f;
  }
  else
    speedStep = 0.4f;

  // Изменение состояния персонажа стоя-сидя
  // Один из вариантов, когда приседаешь, только когда нажата соответствующая клавиша
  /* if (::GetAsyncKeyState( options.kbrSquat )) 
  {
    pl[agp].squat = true;
  }
  else
  {
    pl[agp].squat = false;
  } */

  if (DXUtil_getKeyTimeLim(options.kbrSquat,&pl[agp].lastSquatTime,0.5))
  {
    pl[agp].squat = !pl[agp].squat;
    SUtil_ReCreateAndPlaySound( Sc::pXAudio2, &sn::sPlayerFire, spl::ssSquat);
  }

  // Прыжок персонажа
  if ( ::GetAsyncKeyState(options.kbrJump) & 0x8000f )
    if (!pl[agp].fly)
    {
      SUtil_ReCreateAndPlaySound(  Sc::pXAudio2, &sn::sPlayerJump, spl::ssJump);
      pl[agp].vec.y = pl[agp].jump;
    }

  // ---------- Перемещение персонажа ----------

  float step_up = 0;
  float step_right = 0; 
  if ( ::GetAsyncKeyState(options.kbrStepUp) & 0x8000f )
  // Перемещение вперед
  {
    // if (!pl[agp].fly) // как один из вариантов перемещения только в случае приземления, а не состоянии полета
    {
      step_up += 1.0f;
      playStep(true,speedStep);
    }
  }

  if ( ::GetAsyncKeyState(options.kbrStepDown) & 0x8000f )
  // Перемещение назад
  {
    //if (!pl[agp].fly)
    {
      step_up -= 1.0f;
      playStep(true,speedStep);
    }
  }

  if ( ::GetAsyncKeyState(options.kbrStepLeft) & 0x8000f )
  // Перемещение влево
  {
    // if (!pl[agp].fly)
    {
      step_right -= 1.0f;
      playStep(false, speedStep);
    }
  }

  if ( ::GetAsyncKeyState(options.kbrStepRight) & 0x8000f )
  // Перемещение вправо
  {
    // if(!pl[agp].fly)
    {
      step_right += 1.0f;
      playStep(false,speedStep);
    }
  }

  translateXZObject(&pl[agp].vec.x, &pl[agp].vec.z, pl[agp].rot.y, step_up*Delta, step_right*Delta);
  // Проверка на столкновение управляемого персонажа с каким-то другим, для того чтобы персонажи не двигали друг друга
  CollisionsPlwithPl(agp); 

  if (DXUtil_getKeyTimeLim(options.kbrActivateObj, &pl[agp].lastActivObjTime, 0.5))
  {
    pl[agp].lastActivObjTime = (float)timeGetTime();
    pl[agp].activateObj = true;
  }
}

Столкновения между персонажами рассматривать нет смысла, т.к. они работают в упрощенном варианте обработки столкновений с объектами.

Рассмотрим главную функцию определения и обработки столкновений конкретного персонажа с объектами:

// Функция взаимодействия выбранного персонажа со всеми объектами в сцене (gp - game player)
void CollisionsPlwithObj (int gp, float timeDelta)
{
  // Текущие перпендикулярные расстояния от игрока до граней куба (а не предельно допустимое как rminx!)
  float rx, ry, rz;

  // Расстояния от центра игрока до центра объекта по осям X,Y,Z
  float dx,dy,dz;

  // Предельно допустимое расстояние до объекта (а дальше идет проникновение)
  float rminx,rminy,rminz;

  for (int i = 0; i < objCount ; i++)
    /* Вычисляем столкновение персонажа с данным объектом (i-ый по счету в массиве) в том случае, если у объекта 
    положительные размеры (в принципе работать с размерами, можно взяв их по модулю), он обладает свойсnвом 
    видимости (т.е. не скрыт, например, как аптечки на некоторое время) и свойством физики */
    if (objects[i].sc.x>=0 && objects[i].sc.y>=0 && objects[i].sc.z>=0 
               && objects[i].show && objects[i].phisic)
    {
      rminx = objects[i].sc.x + pl[gp].sc.x;
      rminy = objects[i].sc.y + pl[gp].hSquat;
      rminz = objects[i].sc.z + pl[gp].sc.z;

      dx = pl[gp].pos.x - objects[i].pos.x;
      dy = pl[gp].pos.y - objects[i].pos.y;
      dz = pl[gp].pos.z - objects[i].pos.z;

      // Вычисляем расстояния между игроком и объектами по осям X, Y, Z
      // Необходимо в двух случаях: если столкновения просчитываются по кубу или объект прозрачный (для сортировки)
      if (dx>0)
        rx = pl[gp].pos.x - objects[i].pos.x-rminx;
        //= pl[gp].pos.x - (objects[i].pos.x+rminx);
      else
        rx = pl[gp].pos.x - objects[i].pos.x+rminx;
        //= pl[gp].pos.x - (objects[i].pos.x-rminx);

      if (dy>0)
        ry = pl[gp].pos.y - objects[i].pos.y-rminy;
        //= pl[gp].pos.y - (objects[i].pos.y+rminy);
      else
        ry = pl[gp].pos.y - objects[i].pos.y+rminy;
        //= pl[gp].pos.y - (objects[i].pos.y-rminy);

      if (dz>0)
        rz = pl[gp].pos.z - objects[i].pos.z-rminz;
        //= pl[gp].pos.z - (objects[i].pos.z+rminz);
      else
        rz = pl[gp].pos.z - objects[i].pos.z+rminz;
        //= pl[gp].pos.z - (objects[i].pos.z-rminz);

      // Для корректной сортировки объектов в функции вывода
      if (gp == agp)
      {	
        objects[i].rx = rx;
        objects[i].ry = ry;
        objects[i].rz = rz;
      }

      // Проверка на наличие столкновения:
      if (fabs(dx)<rminx && fabs(dy)<rminy && fabs(dz)<rminz)
      {
        // Обрабатываем столкновения, в зависимости от типа объекта
        switch (objects[i].modelColl)
        {
        case objType::vb_kit: // Аптечка
           CollisionsPlwithKit(gp,i);
           break;
        case objType::vb_armour: // Броня
           CollisionsPlwithArmour(gp,i);
           break;
        case objType::vb_armour_mini: // Малая броня
           CollisionsPlwithArmourMini(gp,i);
           break;
        case objType::vb_artefact: // Артефакт
           CollisionsPlwithArtefact(gp,i);
           break;
        case objType::vb_soul: // Монета
           CollisionsPlwithSoul(gp,i);
           break;
        case objType::vb_end_lavel: // Метка завершения уровня
           CollisionsPlwithEndLavel(gp,i);
           break;
        case objType::vb_teleport: // Телепорт
           CollisionsPlwithTeleport(gp,i);
           break;
        case objType::vb_javelin: // Дротик
           CollisionsPlwithJavelin(gp,i);
           break;
        case objType::vb_stake: // Осиновый кол
           CollisionsPlwithStake(gp,i);
           break;
        case objType::vb_spade: // Лопата
           CollisionsPlwithSpade(gp,i);
           break;
        case objType::vb_secret_place: // Секретное место
           CollisionsPlwithSecretPlace(gp,i);
           break;
        case objType::vb_door: // Дверь
           CollisionsPlwithDoor(gp,i);
           CollisionsPlwithCube(gp,i,dx,dy,dz,rminx,rminy,rminz,rx,ry,rz);
           break;
        case objType::vb_door_cupe: // Дверь-купэ
           CollisionsPlwithDoorCupe(gp,i);
           CollisionsPlwithCube(gp,i,dx,dy,dz,rminx,rminy,rminz,rx,ry,rz);
           break;
        case objType::vb_stairs: // Лестница
           CollisionsPlwithStairs(gp,i,timeDelta);
           CollisionsPlwithCube(gp,i,dx,dy,dz,rminx,rminy,rminz,rx,ry,rz);
           break;
        case objType::vb_lift: // Лифт
           CollisionsPlwithLift(gp,i);
           CollisionsPlwithCube(gp,i,dx,dy,dz,rminx,rminy,rminz,rx,ry,rz);
           break;
        case objType::vb_jump_rope: // "Прыгалка"
           CollisionsPlwithJumpPore(gp,i);
           break;
        // Обрабатываем столкновения по кубу, в случае если объект - куб 
        // или у него отсутствует персональная модель обработки столкновений
        default:
           // Обработка столкновений с обычными объектами гораздо сложнее обработки взятия обычной аптечки и т.п.,
           // поэтому требуются параметры описывающие сам процесс столкновения (положение тел относительно друг-друга), 
           // которые были вычисленны только-что
           CollisionsPlwithCube(gp,i,dx,dy,dz,rminx,rminy,rminz,rx,ry,rz);
           break;
      }
    }
  }
}

Из приведенной выше функции обработки столкновений видно, что она всего лишь проверяет наличие столкновения персонажа с тем или иным объектом, а потом выбирает метод обработки столкновения для этого объекта.

В качестве примера обработки столкновения с объектами, следует привести несколько примеров:

// Обработка процесса столкновения персонажа с аптечкой (gp - game player, i - objects)
void CollisionsPlwithKit(int gp, int i)
{
  // Защита от взятия объекта чужим персонажем и своим, если у него уже хватает здоровья
  if (pl[gp].health<pl[gp].maxHealth && agp == gp)
  {
    objects[i].timeNotShow = (float)timeGetTime(); // Время выключения отбражения объекта
    objects[i].show = false;
    pl[gp].health = pl[gp].maxHealth;
    profile.opt[opt::last_lavel].kitCount++;
    SUtil_ReCreateAndPlaySound( Sc::pXAudio2,&sn::sKit, specials::ssHealth);
    AddMsg(L"Подобрана аптечка"); // Уведомление о поднятии объекта
  }
}
// Обработка столкновений с дверью (gp - game player, i - objects)
void CollisionsPlwithDoor(int gp, int i)
{
  // Защита от изменения состояния объекта чужим персонажем
  if (agp == gp)
  {
    if (objects[i].activate)
       Message(L"Открыть дверь", Sc::Width - 150, Sc::Height - 150,2);
    else
       Message(L"Закрыть дверь", Sc::Width - 150, Sc::Height - 150,2);

    if (pl[gp].activateObj)
    {
      if (objects[i].activate)
      {
        objects[i].pos.x -=objects[i].sc.x;
        objects[i].pos.z -=objects[i].sc.x;
      }
      else
      {
        objects[i].pos.x +=objects[i].sc.z;
        objects[i].pos.z +=objects[i].sc.z;
      }

      float d = objects[i].sc.x;
      objects[i].sc.x = objects[i].sc.z;
      objects[i].sc.z = d;

      SetMatrixObj(i);

      objects[i].activate = !objects[i].activate;
      pl[gp].activateObj = false;
    }
  }
}
// Обработка столкновений с дверью-купэ
void CollisionsPlwithDoorCupe(int gp, int i)
{
  // Защита от изменения состояния объекта чужим персонажем
  if (agp == gp)
  {
    if (!objects[i].activate)
    {
      objects[i].vec.z = objects[i].vec.x;
      objects[i].vec.x = objects[i].vec.y;
      objects[i].vec.y = objects[i].vec.z;

      objects[i].timeReShow = 0.0f;
      objects[i].activate = true;
      pl[gp].activateObj = false;
    }
  }
}
// Обработка столкновений с лифтом
void CollisionsPlwithLift(int gp, int i)
{
  // Защита от изменения состояния объекта чужим персонажем
  if (agp == gp)
  {
    if (!objects[i].activate)
       Message(L"Активировать лифт", Sc::Width - 200, Sc::Height - 150, 2);

    if (pl[gp].activateObj && !objects[i].activate)
    {
      objects[i].vec.z = objects[i].vec.x;   // Смысл этого в том, что лифт изменяет свою высоту от текущей
      objects[i].vec.x = objects[i].vec.y;   // до vec.y, a значения vec.x и vec.y меняются местами в 
      objects[i].vec.y = objects[i].vec.z;   // результате опускания-поднимания лифта

      objects[i].activate = true;
      pl[gp].activateObj = false;
    }
  }
}
// Обработка столкновений с "прыгалкой"
void CollisionsPlwithJumpPore(int gp, int i)
{
  pl[gp].vec.x += objects[i].vec.x;
  pl[gp].vec.y += objects[i].vec.y;
  pl[gp].vec.z += objects[i].vec.z;
}
// Обработка процесса столкновения выбранного персонажа (gp - game player)
// с параллелепипедом или масштабированным кубом
void CollisionsPlwithCube(int gp, int i, float dx, float dy, float dz, float rminx, 
                          float rminy, float rminz, float rx, float ry, float rz)
{
  // Минимальное проникновение в объект по 3-м возможным осям (x,y,z)
  float mmin = min(min(fabs(rx),fabs(ry)),fabs(rz));

  float h; 
  /* Для нормального передвижения в местах состыковки объектов для этого предполагаем, что персонаж немного 
  выше, чем стоит на объекте и тем самым изолируем его от микро-столкновений с объектами по осям X и Z 
  (добавляем мнимое (ложное) увеличение высоты персонажа), более того, если смоделировать лестницу с 
  небольшими ступеньками, то игрок не будет и ними сталкиваться, а встанет на нее (аналогично это работает 
  в алгоритме столкновения в другими персонажами и, например, если один из них относительно небольшой 
  (или труп), то другой на него встанет! */
		
  if (pl[gp].hSquat<2) h = 1; else h = pl[gp].hSquat*0.5f;
						
  // Если персонаж может прыгать, вданный не обнаружено столкновений с объектами по Y оси и 
  // если персонаж немного выше объекта (но не сталкивается с ним)
  if (!pl[gp].CollisionYup && !pl[gp].CollisionYdown && pl[gp].pos.y+h >= objects[i].pos.y+rminy)
  {
    for (int j = 0; j <=1; j++) // Дважды прогоним физику столкновения "сверху и снизу"
    {
      if (pl[gp].pos.y+h >= objects[i].pos.y + rminy) // Персонаж стоит на объекте
      {
        pl[gp].fly = false;
        pl[gp].CollisionYup = true; // Помечаем, что персонаж над объектом

        // Восстановление персонажа на предельно допустимую высоту над объектом
        pl[gp].pos.y = objects[i].pos.y+rminy;
        pl[gp].vec.y = 0.0f; 
      }
      else   // Персонаж под объектом (например, задел головой потолок)
      if (objects[i].pos.y+h < rminy⁄2)
      {
        pl[gp].CollisionYdown = true; // Помечаем, что персонаж под объектом
        pl[gp].vec.y = 0.0f;

        // Восстановление персонажа на предельно допустимую высоту под объектом
        pl[gp].pos.y = objects[i].pos.y - rminy⁄2;
        pl[gp].vec.y = 0.0f; 
      }

      /* Если вдруг получится что персонаж над одним и под другим объектом одновременно, и более того с обеими 
      сталкивается, то он вынужден присесть (обработка приседания в конце функции), т.к. он физически не может 
      находиться в подобном положении (такое может произойти, в том случае, если персонаж присел и оказался в 
      таком положении, а после захотел встать, но расстояние между объетами по высоте оказалось меньше его 
      полного роста) */
    }
  }
  else // Если столкновение произошло (но не микро-столкновение, а значительное), то обрабатываем
  {
  /* По стандартной схеме - объект восстанавливается у той грани объекта, в которую он проникнул меньше всего.
  Это можно отлично представить, взяв в руки какой-нибуть куб или параллелепипед, например из пластилина и 
  воткнуть в него другой предмет, в результате чего будет хорошо видно, куда по отношению к большому объекту 
  должен будет восстановиться маленький */
    if (fabs(rx) == mmin)
    {
      if (dx > 0)
         pl[gp].pos.x = objects[i].pos.x + rminx;
      else
         pl[gp].pos.x = objects[i].pos.x - rminx;
    }

    if (fabs(ry) == mmin)
    {
      if (dy > 0) // Столкновение с полом
      {
        pl[gp].fly = false;
        pl[gp].CollisionYup = true;
        pl[gp].pos.y = objects[i].pos.y + rminy;
        pl[gp].vec.y = 0.0f; 
      }
      else // Столкновение с потолком
      if (pl[gp].pos.y + pl[gp].sc.y > objects[i].pos.y - objects[i].sc.y)
      {
        pl[gp].CollisionYdown = true;
        pl[gp].vec.y = 0.0f; 
        pl[gp].pos.y = objects[i].pos.y - objects[i].sc.y - pl[gp].sc.y;
      }
    }

    if (fabs(rz) == mmin)
    {
      if (dz > 0)
         pl[gp].pos.z = objects[i].pos.z + rminz;
      else
         pl[gp].pos.z = objects[i].pos.z - rminz;
    }
  }

  // Если "подпирает" с низу и сверху одновременно
  if (pl[gp].CollisionYdown && pl[gp].CollisionYup)
  {
    pl[gp].squat = true;
    pl[gp].hSquat = pl[gp].hmin;
  }
}

Также не менее важными являются столкновения персонажа с ландшафтом, а если быть точнее, то это не просто поднятие камеры на определенную высоту над ландшафтом, а еще и высчитывание столкновений с крутыми его частями:

// Получение высоты персонажа над ландшафтом
float heightTerrain( int gp )
{
  // Проверка на минимальную высоту над ландшафтом, и если персонаж ниже её, то он восстанавливается 
  // именно найденным допустимым минимумом
  return TheTerrain->getHeight( pl[gp].pos.x, pl[gp].pos.z ) + pl[gp].hSquat;
}
// При присутствии ландшафта производятся проверки на столкновения с ним
void CollisionsPltoTerrain(int gp)
{
  if (Sc::terrain)
  {
    const float st = 0.001f;
    if (!pl[gp].phantom)
    {
      // Столкновения с крутыми частями ландшафта
      float heightTerrin1;
      do {
        heightTerrin1 = TheTerrain->getHeight( pl[gp].pos.x+1, pl[gp].pos.z ) + pl[gp].hSquat;
        pl[gp].pos.x+=st;
      } while (TheTerrain->hitInTerrain(pl[gp].pos.x+1, pl[gp].pos.z) && heightTerrain(gp)>heightTerrin1+2.0f);

      do {
        heightTerrin1 = TheTerrain->getHeight( pl[gp].pos.x-1, pl[gp].pos.z ) + pl[gp].hSquat;
        pl[gp].pos.x-=st;
      } while (TheTerrain->hitInTerrain(pl[gp].pos.x-1,pl[gp].pos.z) && heightTerrain(gp)>heightTerrin1+2.0f);

      do {
        heightTerrin1 = TheTerrain->getHeight( pl[gp].pos.x, pl[gp].pos.z+1 ) + pl[gp].hSquat;
        pl[gp].pos.z+=st;
      } while (TheTerrain->hitInTerrain(pl[gp].pos.x,pl[gp].pos.z+1) && heightTerrain(gp)>heightTerrin1+2.0f);

      do {
        heightTerrin1 = TheTerrain->getHeight( pl[gp].pos.x, pl[gp].pos.z-1 ) + pl[gp].hSquat;
        pl[gp].pos.z-=st;
      } while (TheTerrain->hitInTerrain(pl[gp].pos.x, pl[gp].pos.z-1) && heightTerrain(gp)>heightTerrin1+2.0f);
    }

    float lim = 2.0f;
    if (!TheTerrain->hitInTerrain(fabs(pl[gp].pos.x)+lim, fabs(pl[gp].pos.z)+lim))
    {
      // Запрет выхода за пределы ландшафта
      TheTerrain->returnPosInTerrain(&pl[gp].pos.x, &pl[gp].pos.z,lim);
    }

    // Строение слудующего цикла довольно хитрое, дело в том, что в сцене два ландшафта. Столкновения высчитываются 
    // только с главным, а контролировать высоту позиций персонажей необходимо с учетом двух ландшафтов, поэтому 
    // сначала корректируется координата Y персонажа если она меньше высоты главного ландшафта, а потом производятся 
    // теже действия, но только относительно второго (дополнительного) ландшафта
    float hTerr = heightTerrain(gp);
    do {
      if (pl[gp].pos.y < hTerr)
      {
        pl[gp].pos.y = hTerr;
        pl[gp].fly = false;
        pl[gp].CollisionYup = true; // Персонаж находится на ландшафте, т.е. столкнулся с ним
      }
      hTerr = adjTerrain->getHeight( pl[gp].pos.x, pl[gp].pos.z ) + pl[gp].hSquat;
    } while (pl[gp].pos.y < hTerr);
  }
}

На данном этапе описание алгоритмов физики можно закончить, но не нужно, потому что есть еще одна замечательная функция, которая отвечает на анимацию двери-купэ и лифтов:

void UpdateObj(int i)     // i – номер объекта
{
  if (objects[i].show)    // Действия для отображаемых объектов
  {
    // Обработка работы лифта
    if (objects[i].modelColl == objType::vb_lift )
    {
      if (objects[i].activate)
      {
        // Выбор между поднятием и опусканием лифта в зависимости от его текущего положения
        if (objects[i].pos.y > objects[i].vec.y)
        {
          objects[i].pos.y -= objects[i].speedAnimated*Sc::timeDelta;
          // Если лифт достиг "места назначения"
          if (objects[i].pos.y > objects[i].vec.x)
               objects[i].activate = false;
        }
        else
        {
          objects[i].pos.y += objects[i].speedAnimated*Sc::timeDelta;
          // Если лифт достиг "места назначения"
          if (objects[i].pos.y < objects[i].vec.x)
              objects[i].activate = false;
        }
      }
      if (!objects[i].activate)   // Для точной анимации
           objects[i].pos.y = objects[i].vec.y;

      SetMatrixObj(i);
    }

    // Обработка работы двери
    if (objects[i].modelColl == objType::vb_door_cupe)
    {
      if (objects[i].activate)
      {
        float j;  // Положение двери по оси перемещения

        // Выбор оси перемещения двери
        if (objects[i].sc.x > objects[i].sc.z)
           j = objects[i].pos.x;
        else
           j = objects[i].pos.z;

        if (j > objects[i].vec.x)
        {
          j -= objects[i].speedAnimated*Sc::timeDelta;

          if (j >= objects[i].vec.y)   // Если дверь открылась или закрылась, то закрываем ее, меняя значения векторов
            if (objects[i].timeNotShow > 0) // Дверь открыта, можно закрывать
            {
              if (objects[i].timeReShow == 0.0f) // timeReShow используется для определения времени начала закрытия двери
                 objects[i].timeReShow = (float)timeGetTime() + 4000.0f;

              if (objects[i].timeReShow < (float)timeGetTime())
              {
                 objects[i].vec.z = objects[i].vec.x;
                 objects[i].vec.x = objects[i].vec.y;
                 objects[i].vec.y = objects[i].vec.z;
                 objects[i].timeReShow = -1.0f;
              }
              else
                 j += objects[i].speedAnimated * Sc::timeDelta;
            }
            else 
               objects[i].activate = false;
        }
        else
        {
          j += objects[i].speedAnimated * Sc::timeDelta;

          if (j <= objects[i].vec.y) // Если дверь закрылась или открылась
            if (objects[i].timeNotShow < 0) // Дверь открыта, можно закрывать
            {
              if (objects[i].timeReShow == 0.0f)
              // timeReShow используется для определения времени начала закрытия двери
                objects[i].timeReShow = (float)timeGetTime() + 4000.0f;

              if (objects[i].timeReShow < (float)timeGetTime())
              {
                 objects[i].vec.z = objects[i].vec.x;
                 objects[i].vec.x = objects[i].vec.y;
                 objects[i].vec.y = objects[i].vec.z;
                 objects[i].timeReShow = -1.0f;
              }
              else
                 j -= objects[i].speedAnimated * Sc::timeDelta;
            }
            else
              objects[i].activate = false;
        }
        if (!objects[i].activate)  // Для точной анимации
           j = objects[i].vec.x;

        // Обновление положения двери
        if (objects[i].sc.x > objects[i].sc.z)
           objects[i].pos.x = j;
        else
           objects[i].pos.z = j;

        SetMatrixObj(i);
      }
    }

    if ( objects[i].animated && objects[i].speedAnimated > 0)
    {
      objects[i].rot.y += objects[i].speedAnimated * Sc::timeDelta;
      SetMatrixObj(i);
    }
  }
  else  // Действия для скрытых объектов
  {
    if (objects[i].timeReShow > 0)
      if (((float)timeGetTime()-objects[i].timeNotShow) > objects[i].timeReShow*1000)
        objects[i].show = true;
        // Если объект появился после скрытия - это еще не означает, что это новый объект, поэтому и в 
        // результатах может быть что-то вроде следующего: Найдено аптечек: 7/3
  }
}

На этом разбор физики окончен, но необходимо отметить, что в ней не хватает очень и очень многого, например:
• Столкновения между самими объектами
• Столкновения с объектами, с учетом углов поворота
• Расширение моделей столкновений (например, цилиндрические, сферические и т.п.)
• Комбинирование моделей столкновений (например, сфера с конусом)
• Задание типов столкновений (например, упругое)
• Отсутствие элементарных параметров, таких как масса объекта

7. Демонстрационный код

Скачать демонстрационную программу вместе с исходным кодом: Simple_Runner.zip (3.3 Мб)
(Обновлено 27 мая 2010 г.)

По мере прочтения статьи у читателя, наверняка начало складываться какое-либо мнение о данном движке, однако, оно связано лишь с тем кодом, что был представлен выше. Что же касается использования сторонних разработок, то здесь следует отметить, следующее:

1) DXUT (с небольшими поправками, т.к. он не очень удобен)

2) xAudio, dxutil (из DirectX SDK, кстати, dxutil с добавлением дополнительных функций, которые, все же было бы лучше представить отдельно, нежели здесь)

3) Классы Camera, Terrain (смотрите раздел Литература)

Классы Camera и Terrain, были использованы, потому что стандартные классы в DXUT реализованы слишком громоздко, правда, ландшафт намного функциональнее, чем в используемой литературе, но на этот случай лучше было бы найти третий вариант...

Создание простейшего движка, с разработкой собственной физики (Simple Runner)

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

Создание простейшего движка, с разработкой собственной физики (Simple Runner)
Главное меню

 

Создание простейшего движка, с разработкой собственной физики (Simple Runner)
Меню настроек

Главными недостатками программы, является отсутствие:
1) Шейдеров
2) ИИ у персонажей
3) Собственного редактора уровней
4) Поддержки скриптов, плагинов
5) Сетевых возможностей

8. Литература

При разработке движка "Simple Runner" использовалась следующая книга:
1) Frank D. Luna. Introduction to 3D GAME Programming with DirectX 9.0.

Рекомендуемая литература:
1) Конгер. Физика для разработчиков компьютерных игр.
2) Шампандар. Искусственный интеллект в компьютерных играх.
3) Грег Снук. Создание 3D-ландшафтов в реальном времени с использованием C++ и DirectX 9

 

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

 


 

Статья участвовала в конкурсе статей по программированию #4 (2009).

 

Страницы: 1 2