RPG Maker - критика статей
Мои посты по разработке на RPG Maker набирают примерно столько же минусов, сколько и плюсов. Приглашаю всех поделиться критикой моих публикаций. Хочу повысить качество контента!
Мои посты по разработке на RPG Maker набирают примерно столько же минусов, сколько и плюсов. Приглашаю всех поделиться критикой моих публикаций. Хочу повысить качество контента!
Это можно сделать с помощью команды Выполнить скрипт в событии. Ниже скрипт для MZ с комментариями.
// Для красоты вынес перевод строки времени в кол-во секунд в отдельную функцию.
function convertStringToSeconds(time) {
const array = time.split(":");
return (parseInt(array[0], 10) * 60 * 60) + (parseInt(array[1], 10) * 60) + parseInt(array[2], 10)
}
// Устанавливаем номер переменной, в которую хотим записать значение.
const varId = 1;
// Фиксируем текущее игровое время.
const currentSeconds = $gameSystem.playtime();
// Проверяем, есть ли сохранения.
if (!DataManager.isAnySavefileExists()) {
$gameVariables.setValue(varId, currentSeconds);
} else {
// Определяем индекс последнего сохранения.
const lastSaveFileId = DataManager.latestSavefileId();
// Получаем данные об этом сохранении (нам даже не нужно грузить всё сохранение).
const saveInfo = DataManager._globalInfo[lastSaveFileId];
// Вычисляем, сколько секунд игры было в последнем сохранении.
const lastSeconds = convertStringToSeconds(saveInfo.playtime);
// Вычисляем разницу.
const deltaSeconds = currentSeconds - lastSeconds;
// Записываем в переменную.
$gameVariables.setValue(varId, deltaSeconds);
}
Учтите, что так мы можем получить отрицательное число. К примеру, если поиграем час, запишем сохранение, а потом начнём новую игру, текущее игровое время будет меньше времени в последнем сохранении.
Версия для MV:
function convertStringToSeconds(time) { const array = time.split(":"); return (parseInt(array[0], 10) * 60 * 60) + (parseInt(array[1], 10) * 60) + parseInt(array[2], 10) }
const varId = 1; const currentSeconds = $gameSystem.playtime();
if (!DataManager.isAnySavefileExists()) { $gameVariables.setValue(varId, currentSeconds); }
else {
const lastSaveFileId = DataManager.latestSavefileId();
const saveInfo = DataManager.loadGlobalInfo()[lastSaveFileId];
const lastSeconds = convertStringToSeconds(saveInfo.playtime);
const deltaSeconds = currentSeconds - lastSeconds;
$gameVariables.setValue(varId, deltaSeconds);
}
Если нужны какие-то данные, которых нет в информации о сохранении, придётся грузить файл сохранения. Это можно сделать так:
// Устанавливаем номер переменной, в которую хотим записать значение.
const varId = 1;
// Проверяем, есть ли сохранения.
if (!DataManager.isAnySavefileExists()) {
$gameVariables.setValue(varId, 0);
} else {
// Определяем индекс последнего сохранения.
const lastSaveFileId = DataManager.latestSavefileId();
// Получаем имя файла этого сохранения.
const saveName = DataManager.makeSavename(lastSaveFileId);
// Загружаем данные из файла.
StorageManager.loadObject(saveName).then(contents => {
// Данные в contents. Обрабатываем их, как нужно.
return 0;
});
}
На смежные и схожие вопросы могу ответить в этом треде.
Я разработал плагин для языковых локализаций, здесь описание поподробнее. Постепенно буду добавлять в него функционал. Если вам нужна какая-то конкретная функция, напишите идею в теме плагина. Также обращу внимание, что в плагине есть функционал по экспорту языковых данных в Excel-таблицу и импорту из неё.
Важный совет. Если вы допускаете, что в вашей игре будет более одного языка (возможно, не сразу, но в будущем), лучше сразу разрабатывать с учётом локализаций. То есть нигде в редакторе не используйте захардкоженный текст, только теги из файлов локализации. Сначала можно прописать только один язык, потом по надобности добавлять другие. Тогда вам не придётся мучаться с экспортом текста из игры.
Тем не менее, иногда может потребоваться внедрить локализации в уже готовую игру.
Как-то раз я выполнил такой заказ для игры на RPG Maker MV. Вероятно, решение прокатит и на MZ. Если не сработает, напишите в этой теме.
Экспортировать текст из игры в JSON.
Сконвертировать JSON в XLSX и отправить получившуюся таблицу переводчику.
Дождаться готового перевода в XLSX формате.
Конвертировать новую таблицу в JSON и внедрить новый язык в проект.
Формат JSON поддерживает произвольную структуру данных. Объекты, массивы, любой уровень вложенности. При этом переводчик с JSON работать не хотел, нужно было ему обеспечить таблицу. Поэтому я решил для хранения данных локализации использовать один JSON-файл с плоским объектом. То есть был только один объект в корне файла, и каждое его свойство было текстовым. Никаких вложенных объектов.
Сначала нужно экспортировать текст из проекта. Я написал скрипт, который находит все диалоги (команда "Показать сообщение"), извлекает из них текст и сразу генерирует JSON-файл с нужной структурой. Вот он.
Этот скрипт написан на JS. Чтобы его исполнить, можно установить Node.js и запустить скрипт командой node export-localization.js
Можно также переписать скрипт на python, bash или другой язык и исполнить удобными вам инструментами. В принципе, скрипт несложный (даже примитивный).
В результате мы получим файл с таким содержимым:
{
"<тег>": "<реплика>"
}
Теперь забиваем в поисковик: "json to xlsx". Берём любой понравившийся сервис. Например, этот.
Скорее всего, мы получим таблицу с 2 строками и большим количеством столбцов. Переводчику будет неудобно работать с такой. Он ожидает 2 столбца: в одном теги, в другой - текст, который нужно перевести (переводчик либо будет редактировать этот столбец, либо добавит новый).
Если у вас получилось так, то просто транспонируйте получившуюся таблицу. Это можно сделать и в Esxcel, и в LibreOffice, и в Google Sheets. Не знаете, как? Поищите в интернете, это несложно (конкретная кнопка зависит от инструмента, поэтому тут не пишу, но она точно есть).
Отправляем таблицу переводчику, ждём перевода.
Теперь новый XLSX нужно конвертировать в JSON. Сначала транспонируем таблицу, чтобы у нас было 2 строки и множество столбцов.
Вбиваем в поисковик: "xlsx to json". Опять берём любой подходящий сервис, вроде этого. Конвертируем, получаем новый json-файл.
Переименовываем новый JSON-файл, как нам нужно ("en.json", "ru.json" и т.д.).
Вставляем в проект, добавляем язык в параметры плагина локализации (здесь конкретные действия зависят от используемого плагина).
Возможно, это не лучший способ: придумывал его в спешке на коленке. Если у вас есть альтернативные идеи, прошу написать в этом треде.
Самое ценное в этой статье - скрипт на экспорт текста, обратите на него внимание. У DK есть инструмент, который извлекает весь текст из игры, но он не генерирует JSON-файл с нужной структурой и уникальными тегами (очень важно, чтобы теги были уникальными).
Надеюсь, мне удалось достаточно раскрыть тему, остались ли вопросы?
Иногда при разработке механик или мини-игр хочется нестандартно обрабатывать нажатия на клавиши клавиатуры или мыши. Например, совершать игровое действие по нажатию на произвольную клавишу (стрельба, прыжок, лечение), управлять интерфейсом (открывать игровую карту, ещё какое-то меню) и много что ещё.
Существует несколько способов это делать.
Возможно, вам достаточно будет назначить стандартные действие на нестандартные клавиши. Например, сделать ходьбу по WASD. Для подобного достаточно расширить словарь Input.keyMapper, об этом подробнее в другом гайде.
Небольшая справка: событие - это сообщение, которое возникает при выполнении определённых условий. Например, если пользователь пошевелил мышь или тыкнул по клавише - в системе происходит соответствующее событие.
С помощью функции document.addEventListener(<событие, <функция>) можно назначить функции-обработчики на различные события.
Можно обрабатывать нажатие на клавишу:
document.addEventListener("keydown", keyDownHandler);
function keyDownHandler(event) {
if (Input.keyMapper[event.keyCode] == "ok") {
// Ваш код
}
};
"keydown" - это название события нажатия на клавишу клавиатуры. Внутри функции обработчика мы проверяем, не помечена ли нажатая клавиша, как "ok" в Input.keyMapper.
Однако мы можем действовать свободнее и сверять напрямую с числовым кодом клавиши:
document.addEventListener("keydown", keyDownHandler);
function keyDownHandler(event) {
// 70 - это код клавиши F
if (event.keyCode == 70) {
// Ваш код
}
};
Существуют различные события, которые можно обрабатывать в играх. Вот некоторые:
// Нажатие на клавишу клавиатуры:
document.addEventListener("keydown", keyDownHandler);
// Нажатие на клавишу мыши:
document.addEventListener("click", mouseClickHandler);
// Двойное нажатие на клавишу мыши:
document.addEventListener("dblclick", mouseDoubleClickHandler);
// Игрок пошевелил мышкой:
document.addEventListener("mousemove", keyDownHandler);
Давайте немного подробнее про использование мыши. Возможно, вам хочется различать клавиши мыши (да, можно тело функции-обработчика писать прямо при добавлении слушателя событию).
document.body.addEventListener("mousedown", event => {
if (event.button == 0) {
// Нажата левая кнопка
}
else if (event.button == 2) {
// Правая кнопка
}
else {
// Другая кнопка
}
}
В коде движка есть функции, которые обрабатывают нажатия на клавиши мыши (сюда же относится обработка сенсорного интерфейса).
Во-первых, в rmmz_core.js можно найти функции, которые делают примерно то, о чём я писал выше:
TouchInput._setupEventHandlers = function() {
const pf = { passive: false };
document.addEventListener("mousedown", this._onMouseDown.bind(this));
document.addEventListener("mousemove", this._onMouseMove.bind(this));
document.addEventListener("mouseup", this._onMouseUp.bind(this));
document.addEventListener("wheel", this._onWheel.bind(this), pf);
document.addEventListener("touchstart", this._onTouchStart.bind(this), pf);
document.addEventListener("touchmove", this._onTouchMove.bind(this), pf);
document.addEventListener("touchend", this._onTouchEnd.bind(this));
document.addEventListener("touchcancel", this._onTouchCancel.bind(this));
window.addEventListener("blur", this._onLostFocus.bind(this));
};
TouchInput._onMouseDown = function(event) {
if (event.button === 0) {
this._onLeftButtonDown(event);
} else if (event.button === 1) {
this._onMiddleButtonDown(event);
} else if (event.button === 2) {
this._onRightButtonDown(event);
}
};
Во вторых, мы можем расширить функционал функций, которые обрабатывают различные события. Для примера возьмём событие отмены с помощью сенсорного интерфейса (сюда же относится нажатие на правую клавишу мыши, если у вас мышь для праворуких):
const Origin_TouchInput_onCancel = TouchInput._onCancel;
TouchInput._onCancel = function(x, y) {
// Ваш код
Origin_TouchInput_onCancel.apply(this, [x, y])
};
Сначала вы выполняем код, после (что важно!) вызываем оригинальный метод. Можно ваш код выполнять после кода движка. Но обязательно вызывайте Origin_TouchInput_onCancel, иначе стандартная обработка нажатия движком не выполнится.
Ещё момент. Напрямую это не относится к тему, но может быть полезно.
$gameTemp.reserveCommonEvent(N);
С помощью этой функции можно вызвать общее событие. N - номер события в Базе данных проекта. К примеру, вы можете по нажатию на какую-то клавишу вызывать общее событие, где и описана ваша уникальная игровая логика.
Вот здесь демо-проект с реализацией такой идеи.
После запуска новой игры попробуйте нажать на F и ПКМ мыши.
Буду дополнять этот гайд по необходимости, пишите ваши предложения и замечания.
Добавить поддержку клавиш W, A, S, D клавиатуры легко в MV и MZ.
Для этого достаточно добавить в проект такой код:
(function () {
var replacedKeyMapper = {
87: 'up',
65: 'left',
83: 'down',
68: 'right',
69: 'pagedown',
};
for (code in replacedKeyMapper) {
Input.keyMapper[code] = replacedKeyMapper[code];
}
})();
Здесь мы также "перекинули" стандартный функционал клавиши W на E.
Вы можете скачать файл тут и добавить его в проект, как плагин.
У каждой клавиши есть числовой код. Представим, что в движке они помечаются маркерами. Например, стрелка вверх помечена маркером 'up'. Если игрок нажимает на эту стрелку, движок видит срабатывание маркера 'up' и обрабатывает это событие.
Маркеры задаются через словарь. Эта структура данных состоит из пар ключ-значение. Здесь ключ - числовой код клавиши, а значение - маркер. Все ключи уникальные, они не могут повторятся. Значения - могут.
Таким образом, несколько клавиш могут быть помечены одинаковым маркером. Это позволяет нам одновременно поддерживать движение как по стрелкам, так и по WASD.
При желании, вы можете добавлять свои пары ключ-значение в объект replacedKeyMapper из кода выше.
Вот код из файла движка rmmz_core.js:
Input.keyMapper = {
9: "tab", // tab
13: "ok", // enter
16: "shift", // shift
17: "control", // control
18: "control", // alt
27: "escape", // escape
32: "ok", // space
33: "pageup", // pageup
34: "pagedown", // pagedown
37: "left", // left arrow
38: "up", // up arrow
39: "right", // right arrow
40: "down", // down arrow
45: "escape", // insert
81: "pageup", // Q
87: "pagedown", // W
88: "escape", // X
90: "ok", // Z
96: "escape", // numpad 0
98: "down", // numpad 2
100: "left", // numpad 4
102: "right", // numpad 6
104: "up", // numpad 8
120: "debug" // F9
};
Здесь вы можете увидеть все доступные по умолчанию маркеры. Это полезно знать, если вы хотите добавить какой-то клавише маркер.
Есть аналогичный словарь для геймпада:
Input.gamepadMapper = {
0: "ok", // A
1: "cancel", // B
2: "shift", // X
3: "menu", // Y
4: "pageup", // LB
5: "pagedown", // RB
12: "up", // D-pad up
13: "down", // D-pad down
14: "left", // D-pad left
15: "right" // D-pad right
};
Можно также создать обработчик нажатий на клавиатуру и там смотреть на непосредственно код нажатой клавиши. Полезно, если у движка нет подходящего стандартного маркера. Но это уже для продвинутых пользователей ;) Пишите, если нужен гайд по этому.
Коды клавиш легко находятся в интернете. Можно смотреть их, к примеру, тут. Нужны DEC-коды (то есть десятичные).
Ещё важный совет. Не меняйте код движка, не редактируйте стандартные файлы. Мало того, что это запрещено правообладателем, это также неудобно и небезопасно. Что, если вы ошибётесь или захотите откатить изменения? JavaScript позволяет переопределять всё, что удобно. Поэтому для изменения кода движка используйте плагины.
Пишите ваши вопросы в этом треде, если остались.
Введение
Видя как передвигается персонаж в таких играх как Blade And Sorcery, BoneWorks, Battle Talent, Contractors, хотелось разобраться, как и какие варианты реализации locomotion system можно реализовать в VR? Видеоуроки направленные конкретно для VR, не раскрывают как подробно реализовать продвинутое передвижение персонажа, какие сложности есть в этом.
Однажды, один пользователь на форуме unreal engine сказал примерно такие слова: "нет разницы VR это или игра от третьего лица или от первого лица, большинство механик реализуются одинаково. Разница лишь в восприятии функционала и небольших особенностях реализации." В целом, я согласен. что так оно и есть, абсолютно все механики можно адаптировать под VR, поэтому я решил адаптировать Locomotion System из проекта Lyra Unreal Engine.
Вероятно, можно просто взять и перенести логику нижней части тела персонажа из проекта Lyra и заставить это работать в VR. Но я не знал как работают анимационные слои, как разделять игровой и анимационный поток в анимационном инстансе. Поэтому я нашел вполне годные уроки.
Так же никто не запрещает взять проект ALS или GameAnimationSample от Unreal Engine (но с нуля там сложно разобраться будет).
Небольшая предыстория в коротком видео: https://www.youtube.com/shorts/phE0y7TSpLE
Из того что я нашел полезным, могу выделить два плейлиста на ютубе. Всю логику расчетов я переносил в С++.
1. Канал Outcast DevSchool имеет плейлист как с нуля воссоздать передвижение персонажа из Lyra, но он использует свой пак анимаций, который расширяет вариативность движения бедер в разных направлениях.
2. Канал Unreal Shinobi имеет похожий плейлист, использует бесплатные анимации из проекта Lyra. Но в нем было излишество для меня, автор тутора использует GamePlayTag для обозначения состояний передвижения игрока, вещь полезная но в моем случае была излишняя.
Я смотрел оба плейлиста и взял все подходящее для моего проекта.
Стоит обратить внимание на эти каналы:
LocoDev - имеет интересные туториалы по передвижению https://www.youtube.com/@LocoDev
Quinn Kuslich - Advanced Thumbstick Locomotion Tutorial конкретно на VR примере (хочется перенести отсюда VFX для телепорта) https://www.youtube.com/watch?v=KTPx8PeKyQM&t=2030s&ab_channel=QuinnKuslich
На каждое отличие я записал короткий ролик, перезаписывать видеоуроки и делать копию системы передвижения, когда там не такие большие отличия - смысла нет.
1. Используем направление камеры, а не Actor Location/Rotation. В VR в основном используют направление камеры для движения персонажа вперед. Это значит, что если ты нажал стик бежать вперед, то куда смотрит камера, туда и будем бежать. В VR камера HMD и капсула должны быть синхронизированы 1 к 1 (об этом вы должны позаботиться чтобы не было рассинхронизации). https://www.youtube.com/shorts/l9l_Nl8uBB4
2. При использовании Orientation Warping (коротко о Orientation Warping), учитываем оба случая - Движение на стик и движение физически от камеры. https://www.youtube.com/shorts/tX__fngNLZA
3. При использовании Stride Warping учитываем скорость при движении от стика и скорость получаемую от физического движения HMD (когда ходим по комнате). https://www.youtube.com/shorts/CxPJ3lQ03-w
Stride Warping позволяет динамически изменять длину шага, потому что анимации бега обычно рассчитаны на скорость 400 см/с, а для анимаций шага 200 см/с. Физический же шаг может быть индивидуальным 40-160 см/с. Демонстрация работы Stride Warping при физическом передвижении по комнате: https://www.youtube.com/shorts/Pxmgqdd03No
4. Используем Dead Blending Node, вместо INERTIALIZATION Node. Я столкнулся с проблемой, что при резкой смене анимации, происходил прыжок ошибочной позы в 1 кадр и это отстреливало в IK верхней части тела. Подробнее о проблеме в видео - https://www.youtube.com/shorts/QGK9naoeV0A
Надеюсь эта информация тебе поможет построить свою систему передвижения, под потребности проекта.
Изучая VR Expansion Plugin, я столкнулся с тем, что реализация самой Grab System написана в блюпринте персонажа в отдельных функциях, а так как у меня проект на С++ я решил просто переписать этот код на С++. Так как функций около 35 и они довольно объемные по коду, я решил поискать способ как ускорить конвертацию блюпринт кода на С++, поэтому расскажу сегодня про плагин Node to Code.
Сам репозиторий плагина находится здесь https://github.com/protospatial/NodeToCode?tab=readme-ov-file
Плагин поддерживает версии UE 5.3-5.5 скачиваем бинарник для своей версии UE - https://github.com/protospatial/NodeToCode/releases/tag/v1.2.3
Анализ Blueprint: фиксирует всю структуру графа Blueprint, включая потоки выполнения, соединения данных, ссылки на переменные и комментарии.
Несколько вариантов LLM: используйте облачные провайдеры (OpenAI, Anthropic Claude, Google Gemini, DeepSeek) или запускайте полностью локально через Ollama для полной конфиденциальности.
Эффективная сериализация: преобразует блюпринт чертежи в специальную схему JSON, которая сокращает использование токенов на 60–90 % по сравнению с подробным текстовым форматом чертежей UE.
Интегрированный редактор: просматривайте переводы в закрепляемом окне редактора Unreal с подсветкой синтаксиса, примечаниями по реализации и темами
1. Качаем плагин 2. Добываем Api ключ LLM OpenAI, Anthropic Claude, Google Gemini, DeepSeek) 3. Заходим в Project Settings>Plugins>NodeToCode> вставляем Api ключ 4. Настраиваем глубину конвертации блюпринта в С++.
Я использую глубину =1 (это значит переписать то что видно в графе блюпринта и описать функции с параметрами которые вызываются, но без их внутренней реализации).
Плагин ускорил перенос BP в С++, код требует правок, но 80% работы он выполняет отлично.
Краткий обзор от меня в коротком shorts - https://youtube.com/shorts/QJTYkJHtg74?si=5inyYPte2GyveR_Y
Телеграм канал Unreal Engine VR
🤝 Поддержать канал
В феврале, я проходил курс по С++ для Unreal Engine в контексте VR. В это же время я начал параллельно писать основу для VR проекта с нуля, но в какой то момент, понял насколько много вещей нужно учесть и протестировать, прежде чем двигаться дальше.
Я закомментировал весь написанный код и перешел на VR Expansion Plugin. В этом плагине уже решено множество проблем, как синхронизация камеры HMD и капсулы персонажа, поведение капсулы когда ты движешься физически упираешься в стену и так далее. Я до сих пор разбираю код этого плагина. Плагин поддерживает разные версии Unreal Engine. Всё доступно в репозитории бесплатно: https://github.com/mordentral/VRExpansionPlugin
Я полностью перенес код VR Expansion Plugin в свой проект, чтобы в последующем модифицировать его. Так как я базируюсь на коде этого плагина, я не буду описывать туториалы "как с нуля построить VR проект", вроде бы звучит логичным "покажи как перенести код из VR Expansion Plugin и модифицировать его", но у меня столько времени нет.
Статьи будут выходить, в статьях я буду стараться передавать модульный функционал. Показывая пример как его можно применить на простом примере. Так как контекст всех проектов разный, основываясь на моем примере вы можете применить его у себя (нейронки в помощь, грузите ей "свой контекст" и "мой пример", смотрите как можно интегрировать и надо ли оно вам вобще)
А сегодня поговорим про перевод персонажа в спринт с помощью махов рук. Такая механика применяется например в Blade and Sorcery или Battle Talent.
Функции вероятнее всего будут приложены скриншотами, а скопировать код можно будет из https://gist.github.com/8bitsage
Код этой статьи: https://gist.github.com/8bitsage/0e17f196d2114ec7d663e08a29204c5b
Идея проста: когда игрок двигается вперед и активно машет руками (как при беге), персонаж автоматически переходит в режим спринта. Это позволяет:
Освободить кнопки контроллера для других действий.
Сделать передвижение более иммерсивным.
Связать физическую активность игрока со скоростью персонажа в игре.
В заголовочном файле (h файл) класса персонажа (например, AVRPlayerCharacter.h) добавляем необходимые переменные:
// === Настраиваемые параметры ===
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement|ArmSwing",
meta = (ClampMin = "3.0", ClampMax = "15.0"))
float ArmSwingThreshold = 7.0f;
// Порог активации спринта в сантиметрах
// Определяет минимальное суммарное расстояние, которое должны пройти
// оба контроллера за один цикл проверки для активации спринта
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement|ArmSwing",
meta = (ClampMin = "0.01", ClampMax = "0.2"))
float CheckFrequency = 0.02f;
// Частота проверки движения контроллеров в секундах
// Меньшее значение = более отзывчивая механика, но больше нагрузка
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float SprintSpeed = 600.0f;
// Скорость персонажа при спринте (см/с)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float RunSpeed = 400.0f;
// Скорость персонажа при беге (см/с)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float WalkSpeed = 200.0f;
// Скорость персонажа при ходьбе (см/с)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement",
meta = (ClampMin = "0.1", ClampMax = "0.5"))
float MinStickMagnitudeForRun = 0.3f;
// Минимальное отклонение стика для перехода от ходьбы к бегу
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement",
meta = (ClampMin = "0.5", ClampMax = "0.9"))
float MinForwardRatioForSprint = 0.7f;
// Минимальная доля движения вперед для возможности спринта
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement",
meta = (ClampMin = "0.3", ClampMax = "0.7"))
float MaxLateralRatioForSprint = 0.5f;
// Максимальная доля бокового движения для возможности спринта
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement",
meta = (ClampMin = "0.1", ClampMax = "1.0"))
float BackwardSpeedMultiplier = 0.7f;
// Множитель скорости при движении назад
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement",
meta = (ClampMin = "0.1", ClampMax = "1.0"))
float SidewaysSpeedMultiplier = 0.8f;
// Множитель скорости при движении вбок
// === Внутренние переменные ===
FVector LastLeftControllerPos;
// Предыдущая позиция левого контроллера для расчета дельты
FVector LastRightControllerPos;
// Предыдущая позиция правого контроллера для расчета дельты
FTimerHandle ArmSwingTimerHandle;
// Хэндл таймера для периодической проверки движения
bool bIsArmSwinging = false;
// Флаг текущего состояния: true если игрок машет руками достаточно интенсивно
void CheckArmSwing();
void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent);
void MoveCharacter(const FInputActionValue& Value);
void HandleMovementInput(const FVector2D& InputValue);
Слабое отклонение стика (< MinStickMagnitudeForRun) — всегда ходьба, независимо от махов руками
Сильное отклонение стика (≥ MinStickMagnitudeForRun): Движение вперед (ForwardRatio > MinForwardRatioForSprint и боковое отклонение < MaxLateralRatioForSprint):С махами руками → Спринт Без махов руками → Бег Движение назад или вбок → Только бег (спринт невозможен)
Такой подход предотвращает нереалистичный спринт при движении назад или боком, делая механику более естественной. Все пороговые значения можно настроить в редакторе для точной подгонки под ваш проект.
В файле реализации (AVRPlayerCharacter.cpp) создаем функцию проверки:
Функция вызывается каждые CheckFrequency секунд (по умолчанию 0.02с = 50 раз в секунду) Настройте для оптимальной производительности.
Измеряется расстояние, пройденное каждым контроллером с последнего вызова
Если суммарное расстояние превышает ArmSwingThreshold, считается что игрок "бежит"
В методе BeginPlay() запускаем периодическую проверку:
Теперь нужно связать механику с системой ввода. Сначала настраиваем привязку action'а:
Затем реализуем функцию обработки ввода:
Важно понимать! В моем примере применение скорости персонажа в зависимости от направления взгляда камеры. Например, стик отодвинут вперед (мы бежим вперед), повернули голову на 90 градусов, мы по прежнему бежим вперед, только уже в ту сторону, когда повернулась голова.
Иногда делают реализации, когда вектор движения применяется от направления капсулы. Направление "вперед" для ввода с джойстика определяется направлением "вперед" капсулы персонажа (или его тела), а не тем, куда смотрит HMD (голова). Игрок может свободно осматриваться головой, не меняя направления движения. При этом повороты самой капсулы (тела персонажа) обычно управляются отдельно, например, правым стиком (snap turn/smooth turn) или физическим поворотом игрока, если включено bUseControllerRotationYaw для персонажа и соответствующая логика в PlayerController.
Вернемся к моему примеру. Основная логика применения скорости находится в HandleMovementInput:
Создайте новый Input Action IA_MoveCharacter
Установите Value Type: Axis2D
В Input Mapping Context привяжите к левому стику VR контроллера
Укажите персонажу Input Mapping Context
В Blueprint персонажа или в Details панели вы можете настроить все параметры:Параметры махов руками:
Arm Swing Threshold — чувствительность активации спринта (см)
Check Frequency — частота проверки движения контроллеров
Скорости движения:
Sprint Speed — скорость при спринте
Run Speed — скорость при беге
Walk Speed — скорость при ходьбе
Пороги направления:
Min Stick Magnitude For Run — минимальное отклонение стика для бега
Min Forward Ratio For Sprint — доля движения вперед для спринта
Max Lateral Ratio For Sprint — максимальная доля бокового движения для спринта
Множители скорости:
Backward Speed Multiplier — коэффициент замедления при движении назад (0.7 = 70% от обычной скорости)
Sideways Speed Multiplier — коэффициент замедления при движении вбок (0.8 = 80% от обычной скорости)
Эта механика делает передвижение в VR более естественным и погружающим. Игроку не нужно думать о кнопках — достаточно начать активнее двигать руками, как при реальном беге. Демонстрация примера показана упрощенно, модифицируйте код под конкретные нужды проекта, все ключевые параметры доступны для настройки прямо в редакторе.