Advertisement
  1. Game Development
  2. 2D Games

Механика платформера: перемещение платформ

Scroll to top
Read Time: 7 min

() translation by (you can also view the original English article)

Final product imageFinal product imageFinal product image
What You'll Be Creating

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

Что понадобится

Этот урок основан на серии Basic Platformer Physics (Базовая физика платформера). В частности, мы будем использовать код, основанный на 8-й части урока, в качестве отправной точки, с несколькими изменениями. Ознакомьтесь с учебной серией и, в частности, с последней частью. Принципы реализации будут такими же, даже если вы используете другое решение для физики, но код будет совместим с версией, представленной в серии уроков.

Демо

Вы можете загрузить демо из вложений (справа кнопка "Download Attachment") к этому уроку. Используйте клавиши WASD, чтобы перемещать персонаж, Space — чтобы клонировать персонаж, и P, чтобы добавить двигающуюся платформу. Правой кнопкой мыши можно создать плитку. Вы можете использовать колесо прокрутки или клавиши со стрелками, чтобы выбрать плитку, которую вы хотите разместить. Бегунки изменяют размер персонажа игрока.

Демо опубликован из-под Unity 2017.2b4, а исходный код совместим с этой версией Unity.

Реализация

Перемещение платформ

Для начала, давайте создадим скрипт, который двигает платформы.

Инициализация

Давайте начнём с создания класса объекта.

1
public class MovingPlatform : MovingObject
2
{
3
}

Теперь давайте зададим некоторые основные параметры объекта в функции init.

1
public void Init()
2
{
3
    mAABB.HalfSize = new Vector2(32.0f, 8.0f);
4
    mSlopeWallHeight = 0;
5
    mMovingSpeed = 100.0f;
6
    mIsKinematic = true;
7
    mSpeed.x = mMovingSpeed;
8
}

Мы задаём размер и скорость, и создаём кинематику сталкивания, а это означает, что они не будут сдвинуты обычными объектами. Мы также устанавливаем значение mSlopeWallHeight равным 0, а это означает, что платформа не будет подниматься по склонам — она всегда будет рассматривать их как стены.

Поведение

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

1
public void CustomUpdate()
2
{
3
    if (mPS.pushesRightTile && !mPS.pushesBottomTile)
4
        mSpeed.y = -mMovingSpeed;
5
    else if (mPS.pushesBottomTile && !mPS.pushesLeftTile)
6
        mSpeed.x = -mMovingSpeed;
7
    else if (mPS.pushesLeftTile && !mPS.pushesTopTile)
8
        mSpeed.y = mMovingSpeed;
9
    else if (mPS.pushesTopTile && !mPS.pushesRightTile)
10
        mSpeed.x = mMovingSpeed;
11
    
12
    UpdatePhysics();
13
}

Вот визуализированный образец:

The pattern visualizedThe pattern visualizedThe pattern visualized

Приклеим персонажа к платформе

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

The offset of the platformThe offset of the platformThe offset of the platform

Определение родительского объекта

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

1
public MovingObject mMountParent = null;

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

1
public void TryAutoMount(MovingObject platform)
2
{
3
    if (mMountParent == null)
4
    {
5
        mMountParent = platform;
6
    }
7
}

Теперь, давайте используем это в подходящем месте, т.е. где угодно, где наш объект сталкивается с другим объектом под ним.

1
else if (overlap.y == 0.0f)
2
{
3
    if (other.mAABB.Center.y > mAABB.Center.y)
4
    {
5
        mPS.pushesTopObject = true;
6
        mSpeed.y = Mathf.Min(mSpeed.y, 0.0f);
7
    }
8
    else
9
    {
10
        TryAutoMount(other);
11
        mPS.pushesBottomObject = true;
12
        mSpeed.y = Mathf.Max(mSpeed.y, 0.0f);
13
    }
14
    continue;
15
}

Первое место, это где мы проверяем, что объекты соприкасаются.

1
if (overlap.y < 0.0f)
2
{
3
    mPS.pushesTopObject = true;
4
    mSpeed.y = Mathf.Min(mSpeed.y, 0.0f);
5
}
6
else
7
{
8
    TryAutoMount(other);
9
    mPS.pushesBottomObject = true;
10
    mSpeed.y = Mathf.Max(mSpeed.y, 0.0f);
11
}

Второе место — когда они перекрываются.

Теперь, когда мы позаботились об этом, давайте займёмся перемещением нашего объекта. Давайте изменим функцию UpdatePhysics из предыдущего урока.

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

1
public Vector2 mOffset;

Теперь, давайте заменим старое локальное смещение этим классом.

1
mOffset = mSpeed * Time.deltaTime;

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

1
mOffset = mSpeed * Time.deltaTime;
2
3
if (mMountParent != null)
4
{
5
    if (HasCollisionDataFor(mMountParent))
6
        mOffset += mMountParent.mPosition - mMountParent.mOldPosition;
7
    else
8
        mMountParent = null;
9
}

Заметьте, что тут мы проверяем ещë, если мы всё ещë касаемся объекта. Если это не так, тогда мы задаём mMountParent равным null, чтобы обозначить, что этот объект больше не едет на другом.

Далее, давайте переместим наш объект на это смещение (offset). Мы не будем использовать нашу функцию Move, а просто изменим положение. Таким образом, при проверке столкновения объектов, которая идёт сразу после UpdatePhysics, мы получим результаты расположений в этой рамке, вместо предыдущей.

1
mOffset = mSpeed * Time.deltaTime;
2
3
if (mMountParent != null)
4
{
5
    if (HasCollisionDataFor(mMountParent))
6
        mOffset += mMountParent.mPosition - mMountParent.mOldPosition;
7
    else
8
        mMountParent = null;
9
}
10
11
mPosition += RoundVector(mOffset + mReminder);
12
mAABB.Center = mPosition;

Теперь, давайте перейдём к функции UpdatePhysicsP2, которая вызывается сразу после устранения столкновения. Здесь мы отменяем наше предыдущее перемещение, которое не было проверено на правильность.

1
public void UpdatePhysicsP2()
2
    {
3
        mPosition -= RoundVector(mOffset + mReminder);
4
        mAABB.Center = mPosition;

Затем, мы переходим к UpdatePhysicsResponse, чтобы применить сдвиг от пересечения с другими объектами. Тут, ранее, мы изменяли позицию напрямую, но теперь, вместо этого, давайте изменим mOffset, таким образом изменение положения урегулируется позже, когда мы будем использовать нашу функцию Move.

1
if (smallestOverlap == Mathf.Abs(overlap.x))
2
{
3
    float offsetX = overlap.x * speedRatioX;
4
5
    mOffset.x += offsetX;
6
    offsetSum.x += offsetX;
7
8
    if (overlap.x < 0.0f)
9
    {
10
        mPS.pushesRightObject = true;
11
        mSpeed.x = Mathf.Min(mSpeed.x, 0.0f);
12
    }
13
    else
14
    {
15
        mPS.pushesLeftObject = true;
16
        mSpeed.x = Mathf.Max(mSpeed.x, 0.0f);
17
    }
18
}
19
else
20
{
21
    float offsetY = overlap.y * speedRatioY;
22
23
    mOffset.y += offsetY;
24
    offsetSum.y += offsetY;
25
26
    if (overlap.y < 0.0f)
27
    {
28
        mPS.pushesTopObject = true;
29
        mSpeed.y = Mathf.Min(mSpeed.y, 0.0f);
30
    }
31
    else
32
    {
33
        TryAutoMount(other);
34
        mPS.pushesBottomObject = true;
35
        mSpeed.y = Mathf.Max(mSpeed.y, 0.0f);
36
    }
37
}

Теперь мы можем вернуться к UpdatePhysicsP2, где мы просто вызываем функции UpdatePhysicsResponse и Move, как мы делали раньше, чтобы получить правильное положение.

1
mPosition -= RoundVector(mOffset + mReminder);
2
mAABB.Center = mPosition;
3
4
UpdatePhysicsResponse();
5
6
if (mOffset != Vector2.zero)
7
    Move(mOffset, mSpeed, ref mPosition, ref mReminder, mAABB, ref mPS);

Исправляем порядок обновления

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

Traveling up or downTraveling up or downTraveling up or down

Чтобы исправить это, везде, где мы задаём mMountParent, если платформа идёт после дочернего объекта в очереди обновления, мы меняем их местами, так чтобы родительский объект всегда обновляется первым. Давайте сделаем эти изменения в функции TryAutoMount.

1
public void TryAutoMount(MovingObject platform)
2
{
3
    if (mMountParent == null)
4
    {
5
        mMountParent = platform;
6
        if (platform.mUpdateId > mUpdateId)
7
            mGame.SwapUpdateIds(this, platform);
8
    }
9
}

Как видите, если id объекта платформы больше чем дочерний, порядок объектов заменяется, устраняя проблему.

The order of the objectsThe order of the objectsThe order of the objects

Ну, как-то так, когда нам нужно приклеить персонаж к двигающейся платформе.

Определение столкновения

Определить столкновение очень просто. В UpdatePhysicsResponse нам нужно увидеть, если перекрытие кинематического объекта смещает нас в стену.

Давайте, сначала позаботимся об оси X:

1
if (smallestOverlap == Mathf.Abs(overlap.x))
2
{
3
    float offsetX = overlap.x * speedRatioX;
4
5
    mOffset.x += offsetX;
6
    offsetSum.x += offsetX;
7
8
    if (overlap.x < 0.0f)
9
    {
10
        mPS.pushesRightObject = true;
11
        mSpeed.x = Mathf.Min(mSpeed.x, 0.0f);
12
    }
13
    else
14
    {
15
        mPS.pushesLeftObject = true;
16
        mSpeed.x = Mathf.Max(mSpeed.x, 0.0f);
17
    }
18
}

Если объект справа, и мы уже толкаем его в левую сторону, давайте вызовем функцию Crush, которую мы введём позже. Делаем тоже самое и для другой стороны.

1
if (overlap.x < 0.0f)
2
{
3
    if (other.mIsKinematic && mPS.pushesLeftTile)
4
        Crush();
5
6
    mPS.pushesRightObject = true;
7
    mSpeed.x = Mathf.Min(mSpeed.x, 0.0f);
8
}
9
else
10
{
11
    if (other.mIsKinematic && mPS.pushesRightTile)
12
        Crush();
13
14
    mPS.pushesLeftObject = true;
15
    mSpeed.x = Mathf.Max(mSpeed.x, 0.0f);
16
}

Повторим это для оси Y.

1
if (overlap.y < 0.0f)
2
{
3
    if (other.mIsKinematic && mPS.pushesBottomTile)
4
        Crush();
5
6
    mPS.pushesTopObject = true;
7
    mSpeed.y = Mathf.Min(mSpeed.y, 0.0f);
8
}
9
else
10
{
11
    if (other.mIsKinematic && mPS.pushesTopTile)
12
        Crush();
13
14
    TryAutoMount(other);
15
    mPS.pushesBottomObject = true;
16
    mSpeed.y = Mathf.Max(mSpeed.y, 0.0f);
17
}

Для примера, функция Crush просто переместит персонаж к центру карты.

1
public void Crush()
2
{
3
    mPosition = mMap.mPosition + new Vector3(mMap.mWidth / 2 * Map.cTileSize, mMap.mHeight / 2 * Map.cTileSize);
4
}

В результате, персонаж будет телепортирован, если он будет раздавлен платформой.

Being crushed by a platformBeing crushed by a platformBeing crushed by a platform

Подытожим

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

Этот урок запрашивался много раз, поэтому, я надеюсь, вы найдёте его полезным! Спасибо что прочли, увидимся в следующий раз!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Game Development tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.