Использование обнаружения столкновений, чтобы ваш игровой персонаж прыгал

Использование обнаружения столкновений, чтобы ваш игровой персонаж прыгал

13 марта 2023 г.

Это третья часть серии, в которой я учусь создавать простой платформер с помощью

Пламенная машина.

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

Добавление гравитации

Первое, что нам нужно сделать, это добавить силу, которая постоянно притягивает игрока к земле.

Я должен сказать, что если ваша игра требует сложной физики, возможно, стоит изучить Forge2D — модуль физического движка для Flame. Это заслуживает отдельной серии, поэтому для простоты в этой серии мы воспроизведем гравитацию только с помощью Flame.

Добавьте несколько новых констант в начало компонента TheBoy:

final double _gravity = 15; // How fast The Boy gets pull down
final double _jumpSpeed = 500; // How high The Boy jumps
final double _maxGravitySpeed = 300; // Max speed The Boy can have when falling

Теперь давайте применим его к скорости y игрока, добавив эти две строки в метод update сразу после установки velocity.x:

_velocity.y += _gravity;
_velocity.y = _velocity.y.clamp(-_jumpSpeed, _maxGravitySpeed);

Первая строка увеличивает вертикальную скорость Мальчика в каждом игровом цикле на величину _gravity. Вторая строка ограничивает скорость между _jumpSpeed и _maxGravitySpeed, чтобы избежать неограниченного ускорения.

Если мы сейчас запустим игру, то увидим, что Мальчик проваливается сквозь землю. На данный момент это ожидаемое поведение, потому что игра еще не знает, что земля должна быть твердой. Для движка игры наш уровень — это просто набор спрайтов, и нам нужно сообщить ему, какие тайлы являются платформами. К счастью, это можно было сделать с помощью редактора Tiled, который мы использовали для создания уровня во второй части.

Добавление платформ

Вернуться к плитке. Как вы могли заметить, тайловая карта имеет разные слои, как и в Adobe Photoshop. У нас уже есть слой Tile, который содержит тайлы для нашего уровня. Теперь мы добавим новый слой объекта, который будет представлять границы наших платформ. Назовите его «Платформы»

New Object Layer

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

The objects layer with platforms positions

Что мы сделали, так это добавили «невидимый» слой, который содержит информацию о положении платформ. Затем мы воспользуемся игровым движком, чтобы преобразовать их в реальные компоненты, которые позже мы сможем использовать для обнаружения столкновений. Сохраните файл уровня и вернитесь в IDE.

Давайте добавим компонент, представляющий платформу, в папку objects:

class Platform extends PositionComponent {

  Platform(Vector2 position, Vector2 size) : super(position: position, size: size);

  @override
  Future<void> onLoad() async {
    return super.onLoad();
  }
}

Теперь перейдите в game.dart и добавьте новый метод:

void spawnObjects(RenderableTiledMap tileMap) {
    final platforms = tileMap.getLayer<ObjectGroup>("Platforms");

    for (final platform in platforms!.objects) {
      add(Platform(Vector2(platform.x, platform.y), Vector2(platform.width, platform.height)));
    }
  }

И вызовите его из onLoad после того, как мы добавили наш компонент уровня:

spawnObjects(level.tileMap);

Давайте пройдемся по тому, что здесь произошло. Мы взяли наш объект tilemap и получили только что созданный слой по его имени. Имейте в виду, что ключ здесь должен точно совпадать с именем слоя в Tiled: tileMap.getLayer<ObjectGroup>("Platforms").

Затем мы перебираем все объекты этого слоя и для каждого из них создаем компонент Platform с тем же положением и размером.

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

Обнаружение столкновений

Обнаружение столкновений — очень распространенная проблема в разработке игр. По сути, это означает, что при пересечении двух игровых объектов мы хотим инициировать некоторые события. Например, пуля попадает в цель, две машины врезаются друг в друга или Мальчик приземляется на платформу.

Движок Flame предоставляет полезный API для обработки обнаружения столкновений. Во-первых, давайте проведем некоторые подготовительные работы. Откройте game.dart и добавьте миксин HasCollisionDetection, чтобы указать движку, что мы хотим отслеживать столкновения:

class PlatformerGame extends FlameGame with HasKeyboardHandlerComponents, HasCollisionDetection

Затем перейдите к классу Platform и добавьте хитбокс, область компонента, которую мы хотим использовать для обнаружения столкновений:

@override
  Future<void> onLoad() async {
    add(RectangleHitbox()..collisionType = CollisionType.passive);
    return super.onLoad();
  }

Существует три типа коллизий:

  • Активный — может конфликтовать с другими хитбоксами активного и пассивного типа.
  • Пассив — может конфликтовать только с активными хитбоксами.
  • Неактивно – обнаружение столкновений отключено.

Поскольку платформы будут сталкиваться только с The Boy, их тип пассивен.

Давайте перейдем к theboy.dart и добавим для него хитбокс в конец onLoad:

add(CircleHitbox());

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

Добавьте миксин CollisionCallbacks в TheBoy:

class TheBoyPlayer extends SpriteAnimationComponent
    with KeyboardHandler, CollisionCallbacks, HasGameRef<PlatformerGame>

И переопределите метод onCollision

@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other)

Этот метод будет запускаться каждый раз, когда компонент TheBoy сталкивается с другим компонентом.

Первый параметр – это точки, в которых пересекаются два компонента, а второй – компонент, с которым сталкивается TheBoy.

Метод обработки столкновений, который я буду использовать, описан в руководстве DevKage (ссылка в конце истории) с небольшими улучшениями. Я должен упомянуть, что существует множество способов разрешения коллизии. Я буду использовать довольно простой вариант, который неплохо справляется со своей задачей, но, конечно, его можно улучшить.

Столкновение кругового хитбокса игрока и прямоугольного хитбокса платформ в момент вызова onCollision можно представить следующим образом:

Collision schema

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

Calculation of the penetration depth

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

@override
  void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
    if (other is Platform) {
      if (intersectionPoints.length == 2) {
        final mid = (intersectionPoints.elementAt(0) +
            intersectionPoints.elementAt(1)) / 2;

        final collisionVector = absoluteCenter - mid;
        double penetrationDepth = (size.x / 2) - collisionVector.length;

        collisionVector.normalize();
        position += collisionVector.scaled(penetrationDepth);
      }
    }

    super.onCollision(intersectionPoints, other);
  }

Сначала мы вычисляем среднюю точку между точками пересечения (mid). Затем, используя центр круга и mid, мы вычисляем вектор столкновения — направление движения Мальчика к платформе. Затем, используя радиус (size.x / 2) и длину collisionVector, мы вычисляем, насколько глубоко круговой хитбокс проникает в прямоугольную платформу. Наконец, мы нормализуем вектор так, чтобы он содержал только направление столкновения, и обновляем положение игрока на penetrationDepth, умноженное на нормализованный вектор, чтобы сохранить направление.

Если мы запустим игру, Мальчик больше не провалится сквозь платформу. И если он двинется вперед, стена его остановит.

The Boy no longer falls through the ground

Прыжки

Добавим прыжки. Во-первых, нам нужно определить, была ли нажата клавиша со стрелкой. Добавьте новую переменную в класс TheBoy:

bool _hasJumped = false;

Измените метод onKeyEvent и добавьте следующее внизу:

_hasJumped = keysPressed.contains(LogicalKeyboardKey.keyW) || keysPressed.contains(LogicalKeyboardKey.arrowUp);

Затем отредактируйте метод update и добавьте его сразу после применения гравитации:

if (_hasJumped) {
      _velocity.y = -_jumpSpeed;
      _hasJumped = false;
}

Здесь мы проверяем, была ли нажата клавиша со стрелкой, и обновляем скорость Мальчика по оси Y с помощью _jumpSpeed

.

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

Вернемся к методу onCollision и сохраним компонент, на котором стоит The Boy. Затем, когда столкновение закончится, мы очистим эту ссылку. Наконец, мы собираемся проверить, не является ли ссылка пустой, прежде чем применять нашу логику перехода. Это позволит Мальчику прыгать только при активном столкновении с землей.

Добавьте эти переменные в класс TheBoy:

Component? _standingOn; // The component The Boy is currently standing on
final Vector2 up = Vector2(0, -1); // Up direction vector we're gonna use to determine if The Boy is on the ground
final Vector2 down = Vector2(0, 1); // Down direction vector we're gonna use to determine if The Boy hit the platform above

Перейдите к методу onCollision и добавьте следующее сразу после нормализации collisionVector:

if (up.dot(collisionVector) > 0.9) {
  _standingOn = other;
}

Здесь мы проверяем, сталкивается ли Мальчик с платформой под ним, и сохраняем ссылку на этот компонент.

Затем добавьте реализацию метода onCollisionEnd, который будет срабатывать каждый раз, когда The Boy прекращает сталкиваться с платформой:

@override
  void onCollisionEnd(PositionComponent other) {
    if (other == _standingOn) {
      _standingOn = null;
    }
    super.onCollisionEnd(other);
  }

Наконец, измените логику перехода:

if (_hasJumped) {
  if (_standingOn != null) {
    _velocity.y = -_jumpSpeed;
  }
  _hasJumped = false;
}

Круто, теперь Мальчику можно разрешить прыгать только один раз. Но есть еще одно улучшение, которое мы можем добавить. Если вы прыгнете ниже платформы, вы заметите, что Мальчик на секунду зависает. Чтобы избежать этого, мы собираемся проверить, не сталкивается ли Мальчик с платформой наверху, как мы это делали с землей. Затем мы отскочим от Мальчика, изменив его вертикальную скорость.

Добавьте ветку else к условию, которое мы добавили ранее:

if (up.dot(collisionVector) > 0.9) {
  _standingOn = other;
} else if (down.dot(collisionVector) > 0.9) {
  _velocity.y += _gravity;
}

Молодец! Теперь Мальчик может прыгать и падать.

Обновить анимацию прыжка

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

Добавьте _jumpAnimation и _fallAnimation в метод onLoad, аналогично тому, что мы сделали для анимаций бездействия и запуска:

_jumpAnimation = SpriteAnimation.fromFrameData(
      game.images.fromCache(Assets.THE_BOY),
      SpriteAnimationData.range(
        start: 4,
        end: 4,
        amount: 6,
        textureSize: Vector2.all(20),
        stepTimes: [0.12],
      ),
    );

    _fallAnimation = SpriteAnimation.fromFrameData(
      game.images.fromCache(Assets.THE_BOY),
      SpriteAnimationData.range(
        start: 5,
        end: 5,
        amount: 6,
        textureSize: Vector2.all(20),
        stepTimes: [0.12],
      ),
    );

Затем давайте изменим метод updateAnimation, чтобы включить новые анимации:

void updateAnimation() {
    if (_standingOn != null) {
      if (_horizontalDirection == 0) {
        animation = _idleAnimation;
      } else {
        animation = _runAnimation;
      }
    } else {
      if (_velocity.y > 0) {
        animation = _fallAnimation;
      } else {
        animation = _jumpAnimation;
      }
    }
  }

Здесь мы проверяем, стоит ли Мальчик на земле (_standingOn != null), затем используем анимацию бега или бездействия. В противном случае мы проверяем, прыгает или падает Мальчик, проверяя знак вертикальной скорости, а затем применяем соответствующую анимацию.

Different animations on jump and fall

Отлично! Анимации выглядят намного лучше.

Обзор

В части 3 все готово. Мы узнали, как добавить простую физику и обнаружение столкновений. Мальчик теперь может прыгать, и игра начинает обретать свою окончательную форму. В последней части мы добавим монеты — еще один тип объектов, с которыми может взаимодействовать Мальчик. А также добавим HUD для отображения собранных монет.

Другие истории цикла:

Полный код этой части вы можете найти на моем github

Ресурсы

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

https://www.youtube.com/watch?v=mSPalRqZQS8&feature=youtu .be * Канал Крейга Оды:

https://youtu.be/hwQpBuZoV9s * Учебник по игре Ember Quest * документация по Flame Engine


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE