Добавил призрака лучшего времени, чтобы было проще побить рекорд в таблице лидеров

Как делал

Сразу решил, что записывать каждый кадр движение игрока излишне (может быть 120 кадров в секунду, а может быть и 240 - зависит от настроек игры и устройства игрока). Буду записывать каждые 0.1 секунду новый кадр (итого 10 кадров в секунду) = неплохая оптимизация.

Сделал отдельные копии машин для игрока, в которых меньше "лишних" объектов. Только необходимые части машины для повтора призрака и прозрачный материал

Вот они слева направо
Вот они слева направо

Сам материал супер простой, Unlit шейдер, с Transparent отрисовкой:

Цветом задаю оттенок и альфу для прозрачности
Цветом задаю оттенок и альфу для прозрачности

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

Немного кода по записи и чтению кадров

Данные

Создал класс для записи кадров в файл PlayerGhostFrameData.cs:

[Serializable] public struct PlayerGhostFrameData { public float Time; public bool SkipInterpolation; public Vector2 ChassisPosition; public Quaternion ChassisRotation; // остальное убрал, чтобы было легче читать }

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

А SkipInterpolation в структуре кадра нужен, чтобы прервать интерполяцию между этим кадром и следующим (в моей игре машина может за 1 кадр телепортировался в другую точку). Иначе машина за 0.1сек летела к новой точке.

Работа с данными

Сделал отдельный менеджер PlayerGhostManager.cs, он хранит ссылку на игрока и записывает кадры + воспроизводит призрака лучшего времени. Сохраняет лучшее время в файл и читает его перед началом уровня (если существует).

В Update менеджера уровня вызываю менеджер призрака, передаю ему gameplayTimer (игровое время уровня):

public void Update(float gameplayTimer) { // запись кадров текущего забега if (gameplayTimer > _lastRecordFrameTime + _recordFramesDelay) { RecordNewFrame(gameplayTimer); } // воспроизведение призрака лучшего времени if (_bestGhostDataExists) { // ниже в "Поиск нужных кадров" будет про этот метод UpdateGhost(gameplayTimer); } } private void RecordNewFrame(float gameplayTimer) { // ссылка на игрока, в RefreshGhostData идет запись // данных шасси / колеса и других в структуру кадра _player.RefreshGhostData(ref _recordFrameData); // запоминаю время кадра _recordFrameData.Time = gameplayTimer; _recordFrames.Add(_recordFrameData); _lastRecordFrameTime = gameplayTimer; }

Сохраняется 10 кадров в секунду (0.1 задержка между кадрами), что достаточно мало - движение призрака будет дерганным. Но при помощи интерполяции кадров я смогу сделать движение плавным. Идея простая: нужно искать 2 ближайших кадра к текущему игровому времени и считать прогресс между кадрами (мы знаем Time каждого):

public void UpdateState(PlayerGhostFrameData from, PlayerGhostFrameData to, float curTime) { // считаем время между кадрами float framesDelta = to.Time - from.Time; // считаем прогресс интерполяции (от 0 до 1), curTime это игровое время float progress = Mathf.Clamp01((curTime - from.Time) / framesDelta); // считаем позицию и поворот шасси через Lerp соответственно Vector2 chassisPosition = Vector2.LerpUnclamped(from.ChassisPosition, to.ChassisPosition, progress); Quaternion chassisRotation = Quaternion.LerpUnclamped(from.ChassisRotation, to.ChassisRotation, progress); // применяем данные к отображению шасси (_chassis это Transform) _chassis.SetPositionAndRotation((Vector3)chassisPosition, chassisRotation); }

Поиск нужных кадров

Считал из файла список кадров, которые записал уже с помощью менеджера (переменная _bestFrames), работа с ними:

private void UpdateGhost(float gameplayTimer) { if (_bestFramesCount < 2) return; // поиск нужного кадра // учитываем, что реальный следующий кадр может быть через 1 секунду // (если игра пролагала / зафризила / ну вы поняли) while (_currentGhostFrame < _bestFramesCount - 2 && gameplayTimer > _bestFrames[_currentGhostFrame + 1].Time) { _currentGhostFrame++; } // получаю 2 кадра для интерполяции между ними var from = _bestFrames[_currentGhostFrame]; var to = _bestFrames[_currentGhostFrame + 1]; // передаю призраку эти данные _playerGhostCar.UpdateState(from, to, gameplayTimer); }

В целом все.

Делаю это для моей игры Wheel Balance в Steam. Игра уже вышла, доступна демо, также заходите на мой ТГ, если хотите следить за разработкой.

Надеюсь этот материал будет кому-то полезен!

14
3
1