const physicsTickRate = 1 / 60; // Частота физического шага (сек)
let physicsAccumulator = 0; // Накопитель времени для плавности физики
let localPlayer; // Переменная для данных локального игрока
let lastInputId = 0; // Счетчик ID для каждого ввода (инпута)
let pendingInputs = []; // Список вводов, еще не подтвержденных сервером
const inputTickRate = 1 / 60; // Частота опроса ввода (сек)
function getInput() { // Функция захвата управления
let x = 0; // Горизонтальная ось
let y = 0; // Вертикальная ось
if (keyIsDown(87)) y -= 1; // Если нажата W — вверх
if (keyIsDown(83)) y += 1; // Если нажата S — вниз
return {id: lastInputId++, x, y}; // Возврат объекта ввода с новым ID
}
function physicsTick(player, input) { // Расчет физики шага
const playerInput = input || player.input; // Берем переданный ввод или текущий у игрока
if (player.pos.y <= 100) { // Если игрок выше "пола"
player.pos.x += player.speed * playerInput.x * physicsTickRate; // Двигаем по X
player.pos.y += player.speed * playerInput.y * physicsTickRate; // Двигаем по Y
} else { // Если упал ниже пола
player.pos.y = 100; // Возвращаем на уровень пола
if (player.vel.y > 0) player.vel.y = 0; // Сбрасываем вертикальную скорость
}
}
function mockServer(ping, updateTickRate, physicsTickRate, onUpdateTick) { // Эмуляция сервера
let isRunning = false; // Состояние работы сервера
let physicsTimeout; // Ссылка на таймер физики
let updateTimeout; // Ссылка на таймер рассылки данных
const speed = 300; // Базовая скорость игрока
const createInitialRemotePlayer = () => ({ // Функция создания чистого состояния
id: 1, // ID игрока
pos: {x: 0, y: 0}, // Начальная позиция
vel: {x: 0, y: 0}, // Начальная скорость
speed, // Скорость из константы
input: {id: 0, x: 0, y: 0}, // Начальный ввод
});
let remotePlayer = createInitialRemotePlayer(); // Инициализация серверного игрока
return { // Публичное API сервера
async start() { // Запуск сервера
if(isRunning) return; // Игнорируем, если уже запущен
isRunning = true; // Ставим флаг работы
const runPhysics = async () => { // Цикл серверной физики
if(!isRunning) return; // Выход, если сервер остановлен
physicsTick(remotePlayer); // Считаем физику
physicsTimeout = setTimeout(runPhysics, physicsTickRate * 1000); // Планируем следующий шаг
};
const runUpdate = async () => { // Цикл рассылки обновлений
if(!isRunning) return; // Выход, если сервер остановлен
onUpdateTick(remotePlayer); // Отправляем данные клиенту
updateTimeout = setTimeout(runUpdate, updateTickRate * 1000); // Планируем следующую рассылку
};
runPhysics(); // Запуск цикла физики
runUpdate(); // Запуск цикла обновлений
},
stop(){ // Остановка сервера
isRunning = false; // Снимаем флаг работы
clearTimeout(physicsTimeout); // Удаляем таймер физики
clearTimeout(updateTimeout); // Удаляем таймер обновлений
remotePlayer = createInitialRemotePlayer(); // Полный сброс состояния игрока
},
restart() { // Перезапуск
this.stop(); // Сначала стоп
this.start(); // Потом старт
},
sendInput: input => remotePlayer.input = input, // Прием ввода от клиента
};
}
function onUpdateTick(remotePlayer) { // Обработка данных от сервера на клиенте
if(!localPlayer) { // Если локальный игрок еще не создан
localPlayer = { // Создаем объект с методами p5.Vector
id: remotePlayer.id, // Копируем ID
pos: createVector(remotePlayer.pos.x, remotePlayer.pos.y), // Вектор позиции
lastPos: createVector(remotePlayer.pos.x, remotePlayer.pos.y), // Предыдущая позиция
renderPos: createVector(remotePlayer.pos.x, remotePlayer.pos.y), // Позиция для отрисовки
vel: createVector(remotePlayer.vel.x, remotePlayer.vel.y), // Вектор скорости
speed: remotePlayer.speed, // Скорость
input: remotePlayer.input, // Последний обработанный ввод
};
} else { // Если игрок уже есть (Client-side Reconciliation)
localPlayer.speed = remotePlayer.speed; // Обновляем скорость от сервера
localPlayer.vel.set(remotePlayer.vel.x, remotePlayer.vel.y); // Обновляем скорость сервера
localPlayer.pos.set(remotePlayer.pos.x, remotePlayer.pos.y); // Сбрасываем позицию на серверную
pendingInputs = pendingInputs.filter( // Убираем из очереди вводы,
input => input.id > remotePlayer.input.id // которые сервер уже учел
);
for (const input of pendingInputs) { // Прогоняем оставшиеся вводы заново
physicsTick(localPlayer, input); // Чтобы предсказать текущую позицию
}
}
}
const server = mockServer(0, 1 / 60, physicsTickRate, onUpdateTick); // Инициализация сервера
function setup() { // Настройка p5.js
frameRate(240); // Высокий FPS для плавности отрисовки
createCanvas(windowWidth, windowHeight); // Холст на всё окно
const buttons = [ // Массив кнопок управления
createButton('Start Server'), // Кнопка старта
createButton('Restart Server'), // Кнопка рестарта
createButton('Stop Server') // Кнопка стопа
];
for(const [i, btn] of buttons.entries()) { // Стилизация кнопок
btn.style('width', '100px'); // Ширина
btn.style('height', '50px'); // Высота
btn.position(width - 125, 75 * (i + 1)); // Позиция в углу
}
buttons[0].mousePressed(server.start); // Привязка старта
buttons[1].mousePressed(server.restart.bind(server)); // Привязка рестарта с контекстом
buttons[2].mousePressed(server.stop); // Привязка стопа
server.start(); // Автозапуск сервера при старте страницы
setInterval(() => { // Цикл отправки ввода (каждые 16мс)
if (localPlayer) { // Если игрок существует
localPlayer.input = getInput(); // Читаем клавиши
server.sendInput(localPlayer.input); // Шлем на сервер
pendingInputs.push(localPlayer.input); // Запоминаем для предсказания
}
}, inputTickRate * 1000); // Интервал в миллисекундах
}
function draw() { // Цикл отрисовки p5.js
background(220); // Очистка фона
if (!localPlayer) return; // Ждем появления игрока
physicsAccumulator += deltaTime / 1000; // Добавляем время кадра в накопитель
while (physicsAccumulator >= physicsTickRate) { // Если накопилось на целый тик
localPlayer.lastPos = localPlayer.pos.copy(); // Сохраняем позу для интерполяции
physicsTick(localPlayer); // Считаем предсказание физики
physicsAccumulator -= physicsTickRate; // Вычитаем потраченное время
}
const alpha = physicsAccumulator / physicsTickRate; // Коэффициент смещения между тиками
// Сглаживание позиции между физическим и рендер-кадром
localPlayer.renderPos.lerp(p5.Vector.lerp(localPlayer.lastPos, localPlayer.pos, alpha), 0.1);
translate(width / 2, height / 2); // Центрируем камеру
line(-width / 2, 100, width / 2, 100); // Рисуем линию пола
push(); // Сохраняем состояние трансформации
translate(localPlayer.renderPos); // Переходим в позицию игрока
rectMode(CENTER); // Рисуем квадрат из центра
line(-width / 2, 0, width / 2, 0); // Рисуем локальную линию (ось)
rect(0, 0, 100, 50); // Рисуем тело игрока
pop(); // Восстанавливаем состояние трансформации
}
window.setup = setup; // Экспорт функций для p5.js
window.draw = draw; // Экспорт функций для p5.js
window.onresize = () => resizeCanvas(windowWidth, windowHeight); // Реакция на изменение окна