Advertisement
  1. Game Development
  2. Platformer

Основы физики 2D платформера, часть 1

Scroll to top
Read Time: 17 min
This post is part of a series called Basic 2D Platformer Physics .
Basic 2D Platformer Physics, Part 2

Russian (Pусский) translation by Evgeniy (you can also view the original English article)

Столкновения персонажа

Хорошо, задача описывается так: мы хотим создать 2D платформер c простой, устойчивой, отзывчивой, точной и предсказуемой физикой. Мы не хотим использовать тяжеловесный 2D физический движок в этой задаче, по нескольким причинам:

  • непредсказыемые последствия столкновений
  • трудно установить точное и устойчивое перемещение персонажа
  • более сложная работа с ним
  • потребление большей вычислительной мощности по сравнению с упрощенной физикой

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

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

Ограничения персонажа

Начнем с определения типа форм которые будут использоваться в нашей физике. Одна из наиболее базовых форм которую мы можем использовать для представления физического объекта в игре это Axis Aligned Bounding Box (AABB). По сути AABB это не повернутый прямоугольник.

Example of an AABBExample of an AABBExample of an AABB

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

Без дополнительных объяснений, создадим структуру нашего AABB.

1
public struct AABB
2
{
3
}

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

1
public struct AABB
2
{
3
    public Vector2 center;
4
  public Vector2 halfSize;
5
}

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

1
public AABB(Vector2 center, Vector2 halfSize)
2
{
3
    this.center = center;
4
    this.halfSize = halfSize;
5
}

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

1
public bool Overlaps(AABB other)
2
{
3
	if ( Mathf.Abs(center.x - other.center.x) > halfSize.x + other.halfSize.x ) return false;
4
	if ( Mathf.Abs(center.y - other.center.y) > halfSize.y + other.halfSize.y ) return false;
5
	return true;
6
}

На картинке показана эта проверка на оси x; ось y проверяется аналогично

Demonstrating a check on the X-AxisDemonstrating a check on the X-AxisDemonstrating a check on the X-Axis

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

Перемещение объекта

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

1
public class MovingObject
2
{
3
}

Теперь заполним этот класс данными. Нам потребуется достаточно много информации для данного объекта:

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

Положение, скорость и масштаб двумерных векторов.

1
public class MovingObject
2
{
3
    public Vector2 mOldPosition;
4
	public Vector2 mPosition;
5
    
6
    public Vector2 mOldSpeed;
7
    public Vector2 mSpeed;
8
    
9
    public Vector2 mScale;
10
}

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

1
public AABB mAABB;
2
public Vector2 mAABBOffset;

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

1
public bool mPushedRightWall;
2
public bool mPushesRightWall;
3
4
public bool mPushedLeftWall;
5
public bool mPushesLeftWall;
6
7
public bool mWasOnGround;
8
public bool mOnGround;
9
10
public bool mWasAtCeiling;
11
public bool mAtCeiling;

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

1
public void UpdatePhysics()
2
{	
3
}

Первое что мы хотим сделать это сохранить данные предыдущего кадра в соответствующие переменные

1
public void UpdatePhysics()
2
{
3
    mOldPosition = mPosition;
4
    mOldSpeed = mSpeed;
5
    
6
    mWasOnGround = mOnGround;
7
	mPushedRightWall = mPushesRightWall;
8
	mPushedLeftWall = mPushesLeftWall;
9
	mWasAtCeiling = mAtCeiling;
10
}

Сейчас обновим положение используя текущую скорость.

1
mPosition += mSpeed*Time.deltaTime;

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

1
if (mPosition.y < 0.0f)
2
{
3
    mPosition.y = 0.0f;
4
    mOnGround = true;
5
}
6
else
7
    mOnGround = false;

После этого, нам также нужно обновить положение центра AABB, как это соответствует новому положению.

1
mAABB.center = mPosition + mAABBOffset;

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

1
mTransform.position = new Vector3(Mathf.Round(mPosition.x), Mathf.Round(mPosition.y),-1.0f);
2
mTransform.localScale = new Vector3(mScale.x, mScale.y, 1.0f);

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

Управление персонажем

Данные

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

Во-первых, создадим класс Character, и отделим его от класса MovingObject.

1
public class Character : MovingObject
2
{
3
}

Тут нам необходимо кое-что разобрать. Прежде всего, это нажатия клавиш - зададим их перечислением которое охватит все управление для персонажа. Создадим его в другом файле, и назовем его KeyInput.

1
public enum KeyInput
2
{
3
    GoLeft = 0,
4
	GoRight,
5
	GoDown,
6
	Jump,
7
	Count
8
}

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

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

1
protected bool[] mInputs;
2
protected bool[] mPrevInputs;

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

1
protected bool Released(KeyInput key)
2
{
3
    return (!mInputs[(int)key] && mPrevInputs[(int)key]);
4
}
5
6
protected bool KeyState(KeyInput key)
7
{
8
    return (mInputs[(int)key]);
9
}
10
11
protected bool Pressed(KeyInput key)
12
{
13
    return (mInputs[(int)key] && !mPrevInputs[(int)key]);
14
}

Тут нет ничего особенного - мы хотим видеть что клавиша была нажата, отпущена, или она включена или выключена.

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

1
public enum CharacterState
2
{
3
    Stand,
4
    Walk,
5
    Jump,
6
    GrabLedge,
7
};

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

1
public CharacterState mCurrentState = CharacterState.Stand;
2
public float mJumpSpeed;
3
public float mWalkSpeed;

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

Цикл обновления

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

1
public void CharacterUpdate()
2
{
3
    switch (mCurrentState)
4
    {
5
        case CharacterState.Stand:
6
            break;
7
        case CharacterState.Walk:
8
            break;
9
        case CharacterState.Jump:
10
            break;
11
        case CharacterState.GrabLedge:
12
            break;
13
    }
14
}

Персонаж стоит на месте

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

1
case CharacterState.Stand:
2
    mSpeed = Vector2.zero;
3
    break;

Мы также хотим показать соответствующий спрайт для состояния.

1
case CharacterState.Stand:
2
    mSpeed = Vector2.zero;
3
    mAnimator.Play("Stand");
4
    break;

Если же персонаж находится не на земле, то он не может больше стоять, поэтому нам необходимо изменить состояние на прыжок.

1
case CharacterState.Stand:
2
    mSpeed = Vector2.zero;
3
    mAnimator.Play("Stand");
4
    
5
    if (!mOnGround)
6
    {
7
        mCurrentState = CharacterState.Jump;
8
        break;
9
    }
10
    break;

Если клавиши GoLeft или GoRight нажаты, тогда нам нужно изменить наше состояние на ходьбу.

1
case CharacterState.Stand:
2
    mSpeed = Vector2.zero;
3
    mAnimator.Play("Stand");
4
    
5
    if (!mOnGround)
6
    {
7
        mCurrentState = CharacterState.Jump;
8
        break;
9
    }
10
    
11
    if (KeyState(KeyInput.GoRight) != KeyState(KeyInput.GoLeft))
12
    {
13
        mCurrentState = CharacterState.Walk;
14
        break
15
    }
16
    break;

В случае нажатия клавиши Jump, мы хотим установить вертикальную скорость на скорость прыжка и изменить состояние на прыжок.

1
if (KeyState(KeyInput.GoRight) != KeyState(KeyInput.GoLeft))
2
{
3
    mCurrentState = CharacterState.Walk;
4
    break;
5
}
6
else if (KeyState(KeyInput.Jump))
7
{
8
    mSpeed.y = mJumpSpeed;
9
    mCurrentState = CharacterState.Jump;
10
    break;
11
}

Это происходит именно для этого состояния, по крайней мере сейчас.

Состояние ходьбы

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

1
case CharacterState.Walk:
2
    mAnimator.Play("Walk");
3
    break;

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

1
if (KeyState(KeyInput.GoRight) == KeyState(KeyInput.GoLeft))
2
{
3
    mCurrentState = CharacterState.Stand;
4
    mSpeed = Vector2.zero;
5
    break;
6
}

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

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

1
if (KeyState(KeyInput.GoRight) == KeyState(KeyInput.GoLeft))
2
{
3
    mCurrentState = CharacterState.Stand;
4
    mSpeed = Vector2.zero;
5
    break;
6
}
7
else if (KeyState(KeyInput.GoRight))
8
{
9
    if (mPushesRightWall)
10
        mSpeed.x = 0.0f;
11
    else
12
        mSpeed.x = mWalkSpeed;
13
        
14
    mScale.x = Mathf.Abs(mScale.x);
15
}
16
else if (KeyState(KeyInput.GoLeft))
17
{
18
    if (mPushesLeftWall)
19
        mSpeed.x = 0.0f;
20
    else
21
        mSpeed.x = -mWalkSpeed;
22
23
    mScale.x = -Mathf.Abs(mScale.x);
24
}

Нам также нужно разобраться с движением влево аналогичным образом.

Также как мы сделали для состояния стояния персонажа на месте, нам нужно знать нажата ли кнопка прыжка, и задать вертикальную скорость если это так.

1
if (KeyState(KeyInput.Jump))
2
{
3
    mSpeed.y = mJumpSpeed;
4
    mAudioSource.PlayOneShot(mJumpSfx, 1.0f);
5
    mCurrentState = CharacterState.Jump;
6
    break;
7
}

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

1
if (KeyState(KeyInput.Jump))
2
{
3
    mSpeed.y = mJumpSpeed;
4
    mAudioSource.PlayOneShot(mJumpSfx, 1.0f);
5
    mCurrentState = CharacterState.Jump;
6
    break;
7
}
8
else if (!mOnGround)
9
{
10
    mCurrentState = CharacterState.Jump;
11
    break;
12
}

Это для ходьбы. Перейдем к состоянию прыжка.

Состояние прыжка

Начнем с настройки соответствующей анимации для спрайта.

1
mAnimator.Play("Jump");

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

1
mSpeed.y += Constants.cGravity * Time.deltaTime;

Но будет разумным установить предел, так персонаж не сможет падать слишком быстро.

1
mSpeed.y = Mathf.Max(mSpeed.y, Constants.cMaxFallingSpeed);

Во многих играх, когда персонаж в воздухе, его управляемость снижается, но мы идем к более простому и точному управлению, которое даст полную управляемость в воздухе. Так если мы нажали клавишу GoLeft или GoRight, персонаж двигается в соответствующем направлении в прыжке так же быстро, как это должно было быть на земле. В данном случае мы можем просто скопировать логику движения из состояния ходьбы.

1
if (KeyState(KeyInput.GoRight) == KeyState(KeyInput.GoLeft))
2
{
3
    mSpeed.x = 0.0f;
4
}
5
else if (KeyState(KeyInput.GoRight))
6
{
7
    if (mPushesRightWall)
8
        mSpeed.x = 0.0f;
9
    else
10
        mSpeed.x = mWalkSpeed;
11
    mScale.x = Mathf.Abs(mScale.x);
12
}
13
else if (KeyState(KeyInput.GoLeft))
14
{
15
    if (mPushesLeftWall)
16
        mSpeed.x = 0.0f;
17
    else
18
        mSpeed.x = -mWalkSpeed;
19
    mScale.x = -Mathf.Abs(mScale.x);
20
}

В конце, мы собираемся сделать прыжок выше, если кнопка прыжка нажата долшье. Сделав это, мы сделаем прыжок ниже, если кнопка прыжка не зажата.

1
if (!KeyState(KeyInput.Jump) && mSpeed.y > 0.0f)
2
    mSpeed.y = Mathf.Min(mSpeed.y, Constants.cMinJumpSpeed);

Как вы можете видеть, если кнопка прыжка не зажата и вертикальная скорость положительна, тогда мы можем установить скорость на максимальное значение cMinJumpSpeed (200 пикселей  в секунду). Это значит что если мы выполним кратковременное нажатие кнопки прыжка, вместо принятой скорости mJumpSpeed (410 по умолчанию), мы получим сниженную до 200, и следовательно прыжок будет короче.

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

Обновление предыдущих нажатий клавиш.

Как только кадр весь  закончен, мы можем обновить предшествующие данные нажатий клавиш. Создадим новую функцию для этого. Все что нам нужно сделать это переместить значения состояний клавиш из массива mInputs в массив mPrevInputs.

1
public void UpdatePrevInputs()
2
{
3
    var count = (byte)KeyInput.Count;
4
5
    for (byte i = 0; i < count; ++i)
6
        mPrevInputs[i] = mInputs[i];
7
}

В самом конце функции CharacterUpdate, нам нужно будет сделать пару вещей.  Во-первых это обновить физику.

1
UpdatePhysics();

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

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

1
if (mOnGround && !mWasOnGround)
2
    mAudioSource.PlayOneShot(mHitWallSfx, 0.5f);

В конце, обновим предыдущие данные нажатий клавиш.

1
UpdatePrevInputs();

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

1
public void CharacterUpdate()
2
{
3
    switch (mCurrentState)
4
    {
5
        case CharacterState.Stand:
6
7
            mWalkSfxTimer = cWalkSfxTime;
8
            mAnimator.Play("Stand");
9
10
            mSpeed = Vector2.zero;
11
12
            if (!mOnGround)
13
            {
14
                mCurrentState = CharacterState.Jump;
15
                break;
16
            }
17
18
            //if left or right key is pressed, but not both

19
            if (KeyState(KeyInput.GoRight) != KeyState(KeyInput.GoLeft))
20
            {
21
                mCurrentState = CharacterState.Walk;
22
                break;
23
            }
24
            else if (KeyState(KeyInput.Jump))
25
            {
26
                mSpeed.y = mJumpSpeed;
27
                mAudioSource.PlayOneShot(mJumpSfx);
28
                mCurrentState = CharacterState.Jump;
29
                break;
30
            }
31
32
            break;
33
        case CharacterState.Walk:
34
            mAnimator.Play("Walk");
35
36
            mWalkSfxTimer += Time.deltaTime;
37
38
            if (mWalkSfxTimer > cWalkSfxTime)
39
            {
40
                mWalkSfxTimer = 0.0f;
41
                mAudioSource.PlayOneShot(mWalkSfx);
42
            }
43
44
            //if both or neither left nor right keys are pressed then stop walking and stand

45
46
            if (KeyState(KeyInput.GoRight) == KeyState(KeyInput.GoLeft))
47
            {
48
                mCurrentState = CharacterState.Stand;
49
                mSpeed = Vector2.zero;
50
                break;
51
            }
52
            else if (KeyState(KeyInput.GoRight))
53
            {
54
                if (mPushesRightWall)
55
                    mSpeed.x = 0.0f;
56
                else
57
                    mSpeed.x = mWalkSpeed;
58
                mScale.x = -Mathf.Abs(mScale.x);
59
            }
60
            else if (KeyState(KeyInput.GoLeft))
61
            {
62
                if (mPushesLeftWall)
63
                    mSpeed.x = 0.0f;
64
                else
65
                    mSpeed.x = -mWalkSpeed;
66
                mScale.x = Mathf.Abs(mScale.x);
67
            }
68
69
            //if there's no tile to walk on, fall

70
            if (KeyState(KeyInput.Jump))
71
            {
72
                mSpeed.y = mJumpSpeed;
73
                mAudioSource.PlayOneShot(mJumpSfx, 1.0f);
74
                mCurrentState = CharacterState.Jump;
75
                break;
76
            }
77
            else if (!mOnGround)
78
            {
79
                mCurrentState = CharacterState.Jump;
80
                break;
81
            }
82
83
            break;
84
        case CharacterState.Jump:
85
86
            mWalkSfxTimer = cWalkSfxTime;
87
88
            mAnimator.Play("Jump");
89
90
            mSpeed.y += Constants.cGravity * Time.deltaTime;
91
92
            mSpeed.y = Mathf.Max(mSpeed.y, Constants.cMaxFallingSpeed);
93
94
            if (!KeyState(KeyInput.Jump) && mSpeed.y > 0.0f)
95
            {
96
                mSpeed.y = Mathf.Min(mSpeed.y, 200.0f);
97
            }
98
99
            if (KeyState(KeyInput.GoRight) == KeyState(KeyInput.GoLeft))
100
            {
101
                mSpeed.x = 0.0f;
102
            }
103
            else if (KeyState(KeyInput.GoRight))
104
            {
105
                if (mPushesRightWall)
106
                    mSpeed.x = 0.0f;
107
                else
108
                    mSpeed.x = mWalkSpeed;
109
                mScale.x = -Mathf.Abs(mScale.x);
110
            }
111
            else if (KeyState(KeyInput.GoLeft))
112
            {
113
                if (mPushesLeftWall)
114
                    mSpeed.x = 0.0f;
115
                else
116
                    mSpeed.x = -mWalkSpeed;
117
                mScale.x = Mathf.Abs(mScale.x);
118
            }
119
120
            //if we hit the ground

121
            if (mOnGround)
122
            {
123
                //if there's no movement change state to standing

124
                if (mInputs[(int)KeyInput.GoRight] == mInputs[(int)KeyInput.GoLeft])
125
                {
126
                    mCurrentState = CharacterState.Stand;
127
                    mSpeed = Vector2.zero;
128
                    mAudioSource.PlayOneShot(mHitWallSfx, 0.5f);
129
                }
130
                else    //either go right or go left are pressed so we change the state to walk

131
                {
132
                    mCurrentState = CharacterState.Walk;
133
                    mSpeed.y = 0.0f;
134
                    mAudioSource.PlayOneShot(mHitWallSfx, 0.5f);
135
                }
136
            }
137
            break;
138
139
        case CharacterState.GrabLedge:
140
            break;
141
    }
142
143
    UpdatePhysics();
144
145
    if ((!mWasOnGround && mOnGround)
146
        || (!mWasAtCeiling && mAtCeiling)
147
        || (!mPushedLeftWall && mPushesLeftWall)
148
        || (!mPushedRightWall && mPushesRightWall))
149
        mAudioSource.PlayOneShot(mHitWallSfx, 0.5f);
150
151
    UpdatePrevInputs();
152
}

Инициализация персонажа

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

  • задать масштаб
  • задать скорость прыжка
  • задать скорость ходьбы
  • задать начальное положение
  • установить AABB
1
public void CharacterInit(bool[] inputs, bool[] prevInputs)
2
{
3
}

Тут мы будем использовать несколько из заданных констант.

1
public const float cWalkSpeed = 160.0f;
2
public const float cJumpSpeed = 410.0f;
3
public const float cMinJumpSpeed = 200.0f;
4
public const float cHalfSizeY = 20.0f;
5
public const float cHalfSizeX = 6.0f;

Для демонстрации, мы можем установить начальное положение в редакторе.

1
public void CharacterInit(bool[] inputs, bool[] prevInputs)
2
{
3
    mPosition = transform.position;
4
}

Для  AABB нам нужно установить смещение и половинный размер. Для демонстрации смещение спрайта должно быть просто половинным размером.

1
public void CharacterInit(bool[] inputs, bool[] prevInputs)
2
{
3
    mPosition = transform.position;
4
    mAABB.halfSize = new Vector2(Constants.cHalfSizeX, Constants.cHalfSizeY);
5
    mAABBOffset.y = mAABB.halfSize.y;
6
}

Сейчас мы можем заняться остальными переменными.

1
public void CharacterInit(bool[] inputs, bool[] prevInputs)
2
{
3
    mPosition = transform.position;
4
    mAABB.halfSize = new Vector2(Constants.cHalfSizeX, Constants.cHalfSizeY);
5
    mAABBOffset.y = mAABB.halfSize.y;
6
    
7
    mInputs = inputs;
8
    mPrevInputs = prevInputs;
9
    
10
    mJumpSpeed = Constants.cJumpSpeed;
11
    mWalkSpeed = Constants.cWalkSpeed;
12
    
13
    mScale = Vector2.one;
14
}

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

1
public class Game
2
{
3
    public Character mPlayer;
4
    bool[] mInputs;
5
    bool[] mPrevInputs;
6
    
7
    void Start ()
8
    {
9
        inputs = new bool[(int)KeyInput.Count];
10
        prevInputs = new bool[(int)KeyInput.Count];
11
12
        player.CharacterInit(inputs, prevInputs);
13
    }
14
}

Дополнительно, при обновлении менеджера, нам нужно обновить игрока, и его вводимые данные.

1
void Update()
2
{
3
    inputs[(int)KeyInput.GoRight] = Input.GetKey(goRightKey);
4
    inputs[(int)KeyInput.GoLeft] = Input.GetKey(goLeftKey);
5
    inputs[(int)KeyInput.GoDown] = Input.GetKey(goDownKey);
6
    inputs[(int)KeyInput.Jump] = Input.GetKey(goJumpKey);
7
}
8
9
void FixedUpdate()
10
{
11
    player.CharacterUpdate();
12
}

Заметьте, что мы обновляем физику персонажа в функции FixedUpdate(). Это дает нам возможность убедиться что прыжки всегда будут на одинаковую высоту, независимо от того, с какой частотой кадров игра работает. Тут отличная статья Гленна Фидлера о том как исправить шаг времени, в случае если вы не используете Unity.

Тестирование управления персонажем

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

An animation of the character controllerAn animation of the character controllerAn animation of the character controller

Выводы

Управление персонажем может выглядеть слишком отзывчивым (персонаж ощущается невесомым), и не таким приятным как при задании движения основе импульса, но все зависит от того какой тип управления подойдет лучше для вышей игры. К счастью, изменение способа движения персонажа весьма просто; достаточно определить как меняется значение скорости в состояниях ходьбы и прыжка.

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

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.