Гардарики

Вы никогда не задумывались почему Новгород называют Новгородом? То есть «Новый Город»? А старый тогда где?

Не поленился, полез в википедию читать про Новгород и нашел такую информацию:
На роль такого «Старого города», иногда отождествляемого с эпическим Словенском, предполагают несколько поселений, среди которых самые вероятные — Рюриково городище и поселение на месте будущего Славенского конца.

А «Гардарики» — это в переводе со скандинавского «страна городов». Википедия говорит, что, возможно, так называли Новгородскую землю.

Вы знали об этом? Мне как-то ни разу в голову не пришло, почему именно «Нов-город», а ведь слышал много раз. Даже бывал там полтора-два года назад проездом. Вот такие дела, век живи — век учись.

Создание правильного полигона для Box2D

Движок Box2D используется для симуляции физики в двумерном пространстве. Из фигур он поддерживает сферу (круг), бокс (прямоугольник) и выпуклый полигон (с внутренними углами больше 1-10 градусов, иначе будет падение с ошибкой).

Что же делать, если надо нарисовать этот полигон от руки и скормить Box2D, чтоб он не рухнул?

Можно нарисовать составной полигон в виде множества сфер и боксов. Так, например, сделана фигура танка для демки движка NeoAxis Game Engine (этот движок трехмерный, но проблемы с физикой такие же)

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

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

Третий пункт — это просто оптимизация. Делать его необязательно, реализуется простым перебором всех треугольников.

  • находим два треугольника со смежной стороной (две точки должны быть общими)
  • сливаем их в один полигон
  • проверяем на выпуклость (внутренние углы должны быть меньше 180)

И еще один момент: если угол между сторонами полигона равен 180 градусам, можно удалить точку, общую для обеих сторон.

Управляем скоростью полета камеры

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

Итак, обозначения:

  • currCameraPos — текущая позиция камеры
  • nextCameraPos — будущая позиция камеры (например на n метров позади объекта наблюдения)
  • speed — скорость камеры в м/с
  • maxDist — максимально допустимая дистанция между currCameraPos и nextCameraPos

Алгоритм:

  • вычисляем направление и длину от текущей до будущей позиций камеры
  • корректируем длину в зависимости от speed и maxDist
  • смещаем камеру из текущей в будущую позицию камеры с учетом длины

А вот, собственно, код этого безобразия:

void updateCameraPosition( float dt )
{
    // Скорость камеры в метрах в секунду
    const float speed = 0.5f;
    // Максимальная дистанция от текущей позиции камеры до будущей
    const float maxDist = 1.0f;
 
    // Текущая позиция камеры
    const Ogre::Vector3 currCameraPos = camera->getPosition();
    // Будущая позиция камеры
    const Ogre::Vector3 nextCameraPos = calculateCameraPosition();
 
    // Направление от текущей позиции камеры до будущей
    Ogre::Vector3 direction = nextCameraPos - currCameraPos;
    // Длина смещения камеры
    float dist = direction.length();
    // Нормализуем направление
    direction.normalise();
 
    // Проверим дистанцию
    if (dist > maxDist)
    {
        camera->setPosition(nextCameraPos - direction * maxDist);
    }
    // Иначе применим скорость
    else
    {
        // Ограничим максимальное смещение, 
        // чтобы не улететь дальше nextCameraPos;
        const float realSpeed = Ogre::Math::Clamp(dt * speed, 0.0f, dist);
        camera->setPosition(currCameraPos + direction * realSpeed);
    }
}

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

Ogre + Bullet: проверка столкновения камеры с землей

Хочу предложить более совершенный алгоритм проверки коллизии камеры с землей, чем опубликованный ранее (ссылка). Теперь все как «у взрослых» (так сделано, например, в Perfect World). Задача: камера не должна проходить сквозь стены.

Оговорим обозначения:

  • cameraPos — позиция камеры
  • targetPos — позиция цели (за которой следит камера)
  • leftTop, rightTop, leftBottom, rightBottom — позиции крайних точек экрана в мировых координатах
  • hitPoint — точка коллизии луча с землей

В общих чертах, алгоритм такой:

  • вычисляем координаты leftTop, rightTop, rightBottom, leftBottom
  • для каждой крайней точки p создаем луч, начало которого в targetPos, а окончание в p
  • для каждого луча проверяем столкновение с землей, назовем точку столкновения hitPoint
  • вычисляем минимальную длину проекции вектора (hitPointtargetPos) на вектор (cameraPostargetPos)
  • новая позиция камеры — это targetPos смещенная в направлении cameraPos на минимальную длину проекции

А теперь выразим это в коде (код писался прямо в блокноте, поэтому не обессудьте)

bool checkCameraCollides( const Ogre::Vector3 & cameraPos, 
    const Ogre::Vector3 & targetPos, Ogre::Vector3 & newCameraPos )
{
    // Сохраняем позицию и направление камеры
    const Ogre::Vector3 lastCameraPos = camera->getPosition();
    const Ogre::Vector3 lastCameraDir = camera->getDirection();
 
    // Установим новую позицию и направление камеры для корректной 
    // работы метода Ogre::Camera::getCameraToViewportRay()
    camera->setPosition(cameraPos);
    camera->setDirection(targetPos - cameraPos);
 
    // Вычисляем крайние точки
    std::vector<Ogre::Vector3> points;
    {
        Ogre::Ray ray;
        // Верхний левый угол
        camera->getCameraToViewportRay(0, 0, &ray);
        points.push_back(ray.getOrigin());
        // Нижний левый угол
        camera->getCameraToViewportRay(0, 1, &ray);
        points.push_back(ray.getOrigin());
        // Верхний правый угол
        camera->getCameraToViewportRay(1, 0, &ray);
        points.push_back(ray.getOrigin());
        // Нижний правый угол
        camera->getCameraToViewportRay(1, 1, &ray);
        points.push_back(ray.getOrigin());
    }
 
    // Вернем камере исходные позицию и направление
    camera->setPosition(lastCameraPos);
    camera->setDirection(lastCameraDir);
 
    // Минимальная длина проекции
    float minProj = 0.0f;
    // Есть столкновение
    bool collideDetected = false;
    // Луч от цели до камеры
    const Ogre::Vector3 targetToCameraDir = 
        (cameraPos - targetPos).normalisedCopy();
 
    for (size_t = 0; i < points.size(); ++i)
    {
        // Начало луча в позиции цели
        const btVector3 from = OgreBtConverter::to(targetPos);
        // Окончание луча крайней точке
        const btVector3 to = OgreBtConverter::to(points[i]);
 
        // Настроим стуктуру для обнаружение столкновения
        btDynamicsWorld::ClosestRayResultCallback rayCallback(from, to);
        // Сталкиваться только с землей
        rayCallback.m_collisionFilterGroup = CG_GROUND;
        rayCallback.m_collisionFilterMask = CM_GROUND;
        // Пускаем луч
        dynamicsWorld->rayTest(from, to, rayCallback);
 
        // Есть контакт
        if (rayCallback.hasHit())
        {
            // Точка столкновения
            const Ogre::Vector3 hitPoint = 
                BtOgreConverter::to(rayCallback.m_hitPointWorld);
 
            // Длина проекции на луч от targetPos до cameraPos
            float proj = (hitPoint - targetPos).dotProduct(targetToCameraDir);
 
            // Сохраним минимальную длину проекции
            if (!collideDetected || minProj > proj)
            {
                minProj = proj;
                collideDetected = true;
            }
        }
    }
 
    // Если столкновение произошло
    if (collideDetected)
    {
        // Новая позиция камеры - это targetPos смещенная в сторону cameraPos на minProj
        newCameraPos = targetPos + targetToCameraDir * minProj;
        return true;
    }
 
    return false;
}

И для «самых маленьких» пример использования:

void updateCameraPosition()
{
    Ogre::Vector3 cameraPos, targetPos, newCameraPos;
    // Вычисляем позицию камеры и цели (решение о том, как это делается, 
    // остается за вами, у меня позиция и цель камеры зависит от режима камеры)
    calculateCameraPosition(cameraPos, targetPos);
    // Проверим столкновение
    if (checkCameraCollides(cameraPos, targetPos, newCameraPos))
    {
        camera->setPosition(newCameraPos);
    }
    else
    {
        camera->setPosition(cameraPos);
    }
    camera->setDirection(targetPos - camera->getPosition());
}
Блог Евгения Жирнова