Хорошо, давайте добавим математику к этому.
Я всегда был сторонником важности и полезности математики в gamedev, и, возможно, я зайду слишком далеко в этомэто ответ, но я действительно думаю, что ваш вопрос вовсе не о кодировании, а о моделировании и решении проблемы алгебры.В любом случае, давайте пойдем.
Параметризация
Если у вас есть высшее образование, вы можете вспомнить кое-что о функциях - операциях, которые принимают параметр и дают результат -и graphs - графическое представление (или график) эволюции функции в зависимости от ее параметра.f(x)
может напомнить вам кое-что: в нем говорится, что функция с именем f
зависит от параметра x
.Таким образом, «to parameterize » грубо означает выражение системы в виде одного или нескольких параметров.
Возможно, вы не ознакомлены с условиями, но вы делаете это постоянно.Например, Track
- это система с 3 параметрами: f(x,y,z)
.
Одна интересная вещь в параметризации заключается в том, что вы можете захватить систему и описать ее в терминах других параметров.Опять же, вы уже делаете это.Когда вы описываете эволюцию вашего трека со временем, вы говорите, что каждая координата является функцией времени, f(x,y,z) = f(x(t),y(t),z(t)) = f(t)
.Другими словами, вы можете использовать время для вычисления каждой координаты и использовать координаты для позиционирования вашего объекта в пространстве в течение данного времени.
Моделирование системы треков
Наконец, я начнуотвечая на ваш вопрос.Чтобы полностью описать нужную вам систему треков, вам понадобятся две вещи:
- Путь;
Вы практически уже решили эту часть.Вы устанавливаете некоторые точки в пространстве сцены и используете сплайн Катмулла-Рома для интерполяции точек и создания пути.Это умно, и с этим ничего не поделаешь.
Кроме того, вы добавили поле time
в каждую точку, чтобы вы хотели убедиться, что движущийся объект пройдет эту проверку именно в это время.,Я вернусь к этому позже.
Движущийся объект.
Одна интересная вещь в вашем решении Path состоит в том, что вы параметризовали расчет пути с помощью параметра percentageThroughSegment
- значение в диапазоне от 0 до 1, представляющее относительную позицию внутрисегмент.В вашем коде вы выполняете итерацию с фиксированными временными шагами, и ваше percentageThroughSegment
будет пропорциональным соотношением между затраченным временем и общим временным интервалом сегмента.Поскольку каждый сегмент имеет определенный промежуток времени, вы эмулируете много постоянных скоростей.
Это довольно стандартно, но есть одна тонкость.Вы игнорируете чрезвычайно важную часть описания движения: пройденное расстояние .
Я предлагаю вам другой подход.Используйте пройденное расстояние для параметризации вашего пути.Тогда движение объекта будет параметризованным по времени пройденным расстоянием.Таким образом, у вас будет две независимые и согласованные системы.Руки на работу!
Пример:
С этого момента я сделаю все 2D для простоты, но позже его изменение на 3D будет тривиальным.
Рассмотрим следующий путь:
Где i
- индекс отрезка, d
- пройденное расстояние и x, y
-координаты в плоскости.Это может быть путь, созданный таким сплайном, как у вас, или с кривыми Безье, или чем-то еще.
Движение, развиваемое объектом с вашим текущим решением, может быть описано как график distance traveled on the path
против time
какэто:
Где t
в таблице - это время, когда объект должен достичь чека, d
- снова пройденное расстояниев этой позиции v
- скорость, а a
- ускорение.
Верхняя часть показывает, как объект движется со временем.Горизонтальная ось - это время, а вертикальная - это пройденное расстояние.Можно представить, что вертикальная ось - это путь, «развернутый» по плоской линии.Нижний график представляет собой эволюцию скорости во времени.
Мы должны вспомнить некоторую физику в этой точке и отметить, что в каждом сегменте график расстояния представляет собой прямую линию, которая соответствует движению впостоянная скорость, без ускорения.Такая система описывается этим уравнением: d = do + v*t
Всякий раз, когда объект достигает контрольных точек, его значение скорости внезапно изменяется (поскольку существуетнет преемственности в своем графике), и это имеет странный эффект в сцене.Да, вы уже знаете это, и именно поэтому вы разместили вопрос.
Хорошо, как мы можем сделать это лучше?Хм ... если бы график скорости был непрерывным, не было бы такого раздражающего скачка скорости.Самое простое описание такого движения может быть равномерно ускоренным.Такая система описывается этим уравнением: d = do + vo*t + a*t^2/2
.Мы также должны будем принять начальную скорость, здесь я выберу ноль (отстранение от отдыха).
Как мы и ожидали, график скоростиявляется непрерывным, движение ускоряется через путь.Это может быть закодировано в Unity, изменяя метиды Start
и GetPosition
следующим образом:
private List<float> lengths = new List<float>();
private List<float> speeds = new List<float>();
private List<float> accels = new List<float>();
public float spdInit = 0;
private void Start()
{
wayPoints.Sort((x, y) => x.time.CompareTo(y.time));
wayPoints.Insert(0, wayPoints[0]);
wayPoints.Add(wayPoints[wayPoints.Count - 1]);
for (int seg = 1; seg < wayPoints.Count - 2; seg++)
{
Vector3 p0 = wayPoints[seg - 1].pos;
Vector3 p1 = wayPoints[seg].pos;
Vector3 p2 = wayPoints[seg + 1].pos;
Vector3 p3 = wayPoints[seg + 2].pos;
float len = 0.0f;
Vector3 prevPos = GetCatmullRomPosition(0.0f, p0, p1, p2, p3, catmullRomAlpha);
for (int i = 1; i <= Mathf.FloorToInt(1f / debugTrackResolution); i++)
{
Vector3 pos = GetCatmullRomPosition(i * debugTrackResolution, p0, p1, p2, p3, catmullRomAlpha);
len += Vector3.Distance(pos, prevPos);
prevPos = pos;
}
float spd0 = seg == 1 ? spdInit : speeds[seg - 2];
float lapse = wayPoints[seg + 1].time - wayPoints[seg].time;
float acc = (len - spd0 * lapse) * 2 / lapse / lapse;
float speed = spd0 + acc * lapse;
lengths.Add(len);
speeds.Add(speed);
accels.Add(acc);
}
}
public Vector3 GetPosition(float time)
{
//Check if before first waypoint
if (time <= wayPoints[0].time)
{
return wayPoints[0].pos;
}
//Check if after last waypoint
else if (time >= wayPoints[wayPoints.Count - 1].time)
{
return wayPoints[wayPoints.Count - 1].pos;
}
//Check time boundaries - Find the nearest WayPoint your object has passed
float minTime = -1;
// float maxTime = -1;
int minIndex = -1;
for (int i = 1; i < wayPoints.Count; i++)
{
if (time > wayPoints[i - 1].time && time <= wayPoints[i].time)
{
// maxTime = wayPoints[i].time;
int index = i - 1;
minTime = wayPoints[index].time;
minIndex = index;
}
}
float spd0 = minIndex == 1 ? spdInit : speeds[minIndex - 2];
float len = lengths[minIndex - 1];
float acc = accels[minIndex - 1];
float t = time - minTime;
float posThroughSegment = spd0 * t + acc * t * t / 2;
float percentageThroughSegment = posThroughSegment / len;
//Define the 4 points required to make a Catmull-Rom spline
Vector3 p0 = wayPoints[ClampListPos(minIndex - 1)].pos;
Vector3 p1 = wayPoints[minIndex].pos;
Vector3 p2 = wayPoints[ClampListPos(minIndex + 1)].pos;
Vector3 p3 = wayPoints[ClampListPos(minIndex + 2)].pos;
return GetCatmullRomPosition(percentageThroughSegment, p0, p1, p2, p3, catmullRomAlpha);
}
Хорошо, давайте посмотрим, как это происходит ...
Э-э-э-э-э.Это выглядело почти хорошо, за исключением того, что в какой-то момент он движется назад, а затем снова продвигается.На самом деле, если мы проверим наши графики, это описано там.Между 12 и 16 секундами скорость равна нулю.Почему это происходит?Поскольку эта функция движения (постоянные ускорения), хотя и проста, имеет некоторые ограничения.При некоторых резких изменениях скорости может не существовать постоянного значения ускорения, которое может гарантировать нашу предпосылку (прохождение контрольных точек в правильное время) без побочных эффектов, подобных этим.
Что нам теперь делать?
У вас есть много вариантов:
- Опишите систему с изменениями линейного ускорения и примените граничные условия (Предупреждение: много уравнений для решения);
- Опишите систему с постоянным ускорением в течение некоторого периода времени, например, ускорение или замедление непосредственно перед / после кривой, затем сохраняйте постоянную скорость для остальной части сегмента (Предупреждение: еще больше уравнений для решения, трудногарантируйте посылку прохождения контрольных точек в правильное время);
- Используйте метод интерполяции для создания графика положения во времени.Я попробовал сам Catmull-Rom, но мне не понравился результат, потому что скорость выглядела не очень плавно.Кривые Безье кажутся более предпочтительным подходом, потому что вы можете управлять склонами (иначе говоря, скоростями) на контрольных точках напрямую и избегать обратных движений;
- И мой любимый: добавить общедоступное поле
AnimationCurve
в классе и настроитьваш график движения в редакторе с потрясающим встроенным ящиком!Вы можете легко добавить контрольные точки с помощью метода AddKey
и выбрать позицию на некоторое время с помощью метода Evaluate
.Вы даже можете использовать метод OnValidate
в своем классе компонентов, чтобы автоматически обновлять точки в Сцене, когда вы редактируете ее на кривой и наоборот.
Не останавливайтесь на этом!Добавьте градиент на линию контура Gizmo, чтобы легко видеть, куда он идет быстрее или медленнее, добавьте маркеры для манипулирования траекторией в режиме редактора ... проявите творческий подход!