На самом деле их два. Первый - выпустить игру строго под, например, 60 гц и принудительно переключать режим монитора игрока (и надеяться, что такой режим поддерживается).
И второй, который скорее всего придумали и до меня, но мне за всё это время он не попадался.
Итак, в чём соль. Что же такое етот ваш статтеринг?
Представим, что у вас есть спрайт персонажа, который движется по экрану. Суть в том, что экран поделен на пиксели, и спрайт как бы прыгает от одного пикселя к другому. допустим, персонаж движется 1 пиксель за кадр. Значит на 60гц мониторе персонаж продвинется за секунду на 60 пикселей, а не 120гц мониторе - на 120. Но мы ведь хотим, чтоб на любой частоте персонаж двигался с одинаковой скоростью. Очень распространенный вариант - умножать скорость на дельту (время между кадрами). Но это и вызывает статтеринг. Потому что ваше расстояние может получиться не ровно 1 пиксель, а 0.7, например.
И если вы каждый кадр будете прибавлять 0.7, то увидите следующую картину позиций спрайта:
0, 0.7, 1.4, 2.1, 2.8
при округлении в меньшую сторону получится:
0, 0, 1, 2, 2
видите? в позиции 0 спрайт отрисовывался 2 кадра, в позиции 1 - 1 кадр. потом в позиции 2 опять два кадра. эта неравномерность ведёт к неприятным для глаза рывкам, всё выглядит дёрганым. fixed timestep создан больше для внутренней логики и никак не уничтожает статтеринг.
Если у вас игра гладкая и в ней используется вся сетка пикселей (да еще и subpixel rendering), то эффект не так заметен. если же вы делаете pixel perfect игру с условным виртуальным разрешением 320x180, и отрисовываете персонажа чётко в больших виртуальных пикселях, то рывки будут очень заметны.
Ну так вот, как же я решил проблему. Очень просто - я намутил обычный пропуск кадров. Только его цель - не повышение производительности, а устранение статтеринга.
Тупо берем нашу базовую частоту за 30 гц. Теперь смотрим частоту обновления экрана игрока и находим ближайшее целое значение с нашим шагом. то есть нам доступны такие частоты как 30,60,90,120,150 и т.д.
Если угадали с частотой - замечательно, просто пропускаем столько обновлений логики, сколько шагов нам потребовалось. То есть на экране 30гц мы ничего не пропускаем. на экране 60гц мы пропускаем кадр через раз. на экране 90 гц мы пропускаем 2 кадра через раз.
если же у игрока монитор оказался 75 гц, что не делится ровно не тридцать, то просто берем ближайшее к нему значение. Это либо 60 либо 90. И всё, дальше по той же схеме. Таким образом состояние всегда будут обновляться равномерно. единственный минус - на мониторах вроде 75, 100, 144 гц, игра будет идти немного быстрее или немного медленнее. самый ужасный случай - 75 гц, так как он равномерно отдален от 60 и 90. значит игра будет идти на 25 процентов быстрее/медленнее. но по мере возрастания герцовки эта разница становится всё меньше. например при частоте 135гц у нас снова одинаково далеко и до 120 и до 150, но теперь разница в скорости будет уже не (75/60), а (150/135), что и без калькулятора понятно. Да, этот способ не подходит для мультиплеерных игр или игр, где важна одинаковая скорость. Спидраннеры могут выставлять нужный режим монитора при надобности. Но вот в играх, где скорость в 75% некритична, это топ способ уничтожить статтеринг. Игра выглядит идеально стабильной. Да, не 60 гц, но на тру пиксель арт стиле вы и так бы не сделали большую частоту. подумайте сами, если у вас сетка 320x180 и вы хотите выжать реальные 60 фпс, значит самое медленное движение в 1 пиксель за кадр даст вам прохождение пути в 60 пикселей за секунду. Это треть высоты. Довольно быстро. условные медленно падающие снежинки вы так не сделаете.
30 гц за базу я взял, потому что это хороший делитель для существующих 60, 90, 120 гц, и при этом не слишком маленький как 15 гц. Так что такие дела. Спустя 2 года борьбы с микростаттерингом, я наконец-то победил его. Ура!
В следующих постах расскажу вам про мой топ движок на C + SDL3. Я намутил форт подобную 16 битную виртуальную машину с банковой системой, синтез звука, сжатие lz4 для изображений и байткод версий скриптов. Короче будет весело
с аккумулятором тот же прикол получается и статтеринг не исчезает. смотри, представь, что ты задал движение 1 пиксель в секунду.
на 60гц у тебя будет капать в аккумулятор по 16,6666. это не 1.0 делить на целое число как например 0.5, 0.25, 0.125. А значит у тебя каждый каждый раз когда аккумулятор переполняется, будет оставаться остаток. И в какой то момент это выльется в то, что на 1 тик ты потратил 60 кадров, а на какой-то 59. И это тоже будет бить в глаза, когда персонаж двигается-двигается, а потом раз и заикнулся на мгновение.
это происходит при условии, что у тебя фиксированный ФПС
если игра разгоняет карту до 1200 ФПС, то погрешностью можно пренебречь
если ФПС фиксирован и известен, то ставь таймер на кол-во кадров, а не кол-во секунд
(но на самом деле при шаге в пиксель погрешность накопления при 60 кадрах и так незаметна глазу)
Не совсем понял про какой фиксированный фпс идет речь.
при вызове свапа буферов vsync ждёт сигнала от монитора. Возможно ты говоришь об отрисовке без vsync или о технологиях вроде freesync.
Без vsync будет тиринг и всё равно нужно как-то спать между кадрами, чтобы не греть процессор лишний раз. А спать ОС точно не умеют. у них гранулярность в 15 мс. То есть ты либо юзаешь vsync, либо спишь с возможностью упустить момент, либо спишь немного и остальное время доводишь через busy loop, который тоже такой себе способ.
Возможно зависит от человека, но мне редкое заикание при накоплении аккумулятора даже сильнее заметно, чем постоянное движение рывками. При постоянных рывках глаз хотя бы привыкает к этому как к стилю что ли (вроде постоянно дергающейся графики в baba is you). а в случае, когда у тебя игра идёт плавно, а потом раз в секунду условно заикается - очень заметно.
Смтрите, в зависимости от того, что у вас за игра - вы будете использовать самые разные технологии.
В статичных играх типа косынки можно рисовать прям в окно без всяких 3d api вроде opengl/vulkan тупо когда нужно
отобразить изменения на экране, в каких-то хитрых мегапроектах вам будет удобно юзать ECS.
где-то подойдёт сложное комплексное решение, где-то быстрое и самопальное.
Согласитесь, нафига юзать условный unreal engine для простенькой новелки.
Вот и с виртуальной машиной так же. ВМ даёт кучу плюсов, но самый топовый - это динамическая загрузка.
Представьте, что у вас огромная игра с кучей уровней.
Если вы скомпилировали игру и она у вас написана целиком на C, то при запуске бинарник помещается в память целиком.
На самом деле не совсем так, но суть в том, что вы не можете регулировать какой кусок кода сейчас вам нужен,
а какой - нет. Да, вы можете заморочиться с разделяемыми библиотеками, но это люто геморно. они платформозависимы,
требуют всякие хэдеры и прочее. С виртуальной машиной ты просто загружаешь скрипт в память и в любой момент выгружаешь.
Хочешь запустить мини игру в игре? без проблем - загрузил и играй. Выходишь из мини игры - выгрузил. Это супер топ фича.
Конкретно моя вм 16битная. Она использует всего 2 байта для хранения адреса скрипта. То есть каждый раз, когда ты
вызываешь функцию в скрипте или прыгаешь куда-то через if/else, тебе нужно всего 2 байта для указания абсолютно любой
позиции в коде. Ну и сами переменные тоже размером 16бит.
Кстати о переменных. зацените топ. У них нет типов. Все переменные - это тупо uint16. то есть значения от 0 до 65535.
это и обычное число, и индекс переменной, и адрес для прыжка. Ваще огнёрий. никаких кастов и усложнения вм.
А еще зацените топ тему. Вот вам пример моего скрипта:
ENEMY_AMOUNT 2
SPEED 5
speed_x
speed_y
arr enemies ENEMY_AMOUNT allot
: init
\ код для инициализации
;
: update
\ код обновления логики
;
и т.д
Прикол в том, что мне не нужны ключевые слова const или var (int);
это еще один большой плюс своей вм - ты можешь ее намутить ровно под свой проект, где не будет ничего лишнего.
У моего компилятора и вм есть договорённость:
Если у тебя в скрипте есть константы - они должны идти в первую очередь и писаться капсом.
Если у тебя в скрипте есть переменные - то они должны писаться маленькими буквами и идти после констант (если есть).
Если есть массивы, то после переменных
И только потом функции.
Компилятор зная эти правила легко понимает, что есть что. ему не нужно постоянно писать const const var var var
Смтрите, в зависимости от того, что у вас за игра - вы будете использовать самые разные технологии.
В статичных играх типа косынки можно рисовать прям в окно без всяких 3d api вроде opengl/vulkan тупо когда нужно
отобразить изменения на экране, в каких-то хитрых мегапроектах вам будет удобно юзать ECS.
где-то подойдёт сложное комплексное решение, где-то быстрое и самопальное.
Согласитесь, нафига юзать условный unreal engine для простенькой новелки.
Вот и с виртуальной машиной так же. ВМ даёт кучу плюсов, но самый топовый - это динамическая загрузка.
Представьте, что у вас огромная игра с кучей уровней.
Если вы скомпилировали игру и она у вас написана целиком на C, то при запуске бинарник помещается в память целиком.
На самом деле не совсем так, но суть в том, что вы не можете регулировать какой кусок кода сейчас вам нужен,
а какой - нет. Да, вы можете заморочиться с разделяемыми библиотеками, но это люто геморно. они платформозависимы,
требуют всякие хэдеры и прочее. С виртуальной машиной ты просто загружаешь скрипт в память и в любой момент выгружаешь.
Хочешь запустить мини игру в игре? без проблем - загрузил и играй. Выходишь из мини игры - выгрузил. Это супер топ фича.
Конкретно моя вм 16битная. Она использует всего 2 байта для хранения адреса скрипта. То есть каждый раз, когда ты
вызываешь функцию в скрипте или прыгаешь куда-то через if/else, тебе нужно всего 2 байта для указания абсолютно любой
позиции в коде. Ну и сами переменные тоже размером 16бит.
Кстати о переменных. зацените топ. У них нет типов. Все переменные - это тупо uint16. то есть значения от 0 до 65535.
это и обычное число, и индекс переменной, и адрес для прыжка. Ваще огнёрий. никаких кастов и усложнения вм.
А еще зацените топ тему. Вот вам пример моего скрипта:
ENEMY_AMOUNT 2
SPEED 5
speed_x
speed_y
arr enemies ENEMY_AMOUNT allot
: init
\ код для инициализации
;
: update
\ код обновления логики
;
и т.д
Прикол в том, что мне не нужны ключевые слова const или var (int);
это еще один большой плюс своей вм - ты можешь ее намутить ровно под свой проект, где не будет ничего лишнего.
У моего компилятора и вм есть договорённость:
Если у тебя в скрипте есть константы - они должны идти в первую очередь и писаться капсом.
Если у тебя в скрипте есть переменные - то они должны писаться маленькими буквами и идти после констант (если есть).
Если есть массивы, то после переменных
И только потом функции.
Компилятор зная эти правила легко понимает, что есть что. ему не нужно постоянно писать const const var var var
>персонаж движется 1 пиксель за кадр
Игровой мир нихуя не должен знать о пикселях, только о юнитах. Так же он нихуя не должен знать о кадрах и частоте монитора, только о частоте своих обновлений, которая фиксирована. Ты апдейтишь игровой мир, рендеришь фрейм в котором интерполируешь состояние мира, переводишь все юниты в пиксели на экране и все. В таком случае похуй, сколько фреймов между апдейтами(т.е похуй, какая частота монитора), они все будут отображать интерполированное состояние и передвигать персонажа плавно по экрану. А что за шизу ты пытаешься сделать я так и не понял до конца.
Так же я упомянул, что вм банковая. Она устроена так. Код с 0 по 32767 байт (примерно) - это код, в который загружается глобальный скрипт. В нём будут глобальные переменные, нужные между сценами и раличные утилиты.
А область с 32768 по 65535 доступна нам для загрузки нашего текущего скрипта. Таким образом часто повторяющуюся логику и данные необходимые между сценами мы храним в core.f, а данные самой сцены - в текущем скрипте. Текущий скрипт по договоренности знает в каких адресах какие функции и переменные лежат, так как это зашито на уровне компилятора. Поэтому мы тупо можем компилить скрипты вбайткод и готово. Никакой линковки в рантайме, никаких хранений символов для резолвинга. Это шикарно. Да, это не луа, где можно комбинировать кучу скриптов как блоки, но для моей игры это идеально подходит. Я хочу сделать максимально разнообразную игру, поэтому часто повторяющегося кода у меня будет немного. Например, вместо того, чтобы создавать мега объект player.f, с кучей состояний, я лучше просто для каждой сцены запилю свой код. Ценой некоторого дублирования, упрощается архитектура.
Вот вам наглядный пример:
Персонаж в доме. Рисуем его без верхней одежды. Ходит быстро.
Персонаж на улице в пургу. Рисуем его в куртке и идет в пургу он медленно. Персонаж в локации с озером умеет плавать и т.д. То есть если взять всю игру целиком, то может показаться, что персонаж - это лютый раздутый объект с кучей состояний, но на самом деле это просто конкретный скрипт сцены содержит необходимый код. Это позволяет мутить ваще любую дичь. Вот прям ваще. Хочешь в каком то уровне сделать не вид сбоку а вид сверху? Без проблем, хочешь где-то головоломку от первого лица с взаимодействием с рычагами? без проблем. И самое топовое, что весь этот код не хранится в памяти. вм тупо в любой момент времени жрет всего лишь 64кб оперативки на хранение кода и данных.
Так же я упомянул, что вм банковая. Она устроена так. Код с 0 по 32767 байт (примерно) - это код, в который загружается глобальный скрипт. В нём будут глобальные переменные, нужные между сценами и раличные утилиты.
А область с 32768 по 65535 доступна нам для загрузки нашего текущего скрипта. Таким образом часто повторяющуюся логику и данные необходимые между сценами мы храним в core.f, а данные самой сцены - в текущем скрипте. Текущий скрипт по договоренности знает в каких адресах какие функции и переменные лежат, так как это зашито на уровне компилятора. Поэтому мы тупо можем компилить скрипты вбайткод и готово. Никакой линковки в рантайме, никаких хранений символов для резолвинга. Это шикарно. Да, это не луа, где можно комбинировать кучу скриптов как блоки, но для моей игры это идеально подходит. Я хочу сделать максимально разнообразную игру, поэтому часто повторяющегося кода у меня будет немного. Например, вместо того, чтобы создавать мега объект player.f, с кучей состояний, я лучше просто для каждой сцены запилю свой код. Ценой некоторого дублирования, упрощается архитектура.
Вот вам наглядный пример:
Персонаж в доме. Рисуем его без верхней одежды. Ходит быстро.
Персонаж на улице в пургу. Рисуем его в куртке и идет в пургу он медленно. Персонаж в локации с озером умеет плавать и т.д. То есть если взять всю игру целиком, то может показаться, что персонаж - это лютый раздутый объект с кучей состояний, но на самом деле это просто конкретный скрипт сцены содержит необходимый код. Это позволяет мутить ваще любую дичь. Вот прям ваще. Хочешь в каком то уровне сделать не вид сбоку а вид сверху? Без проблем, хочешь где-то головоломку от первого лица с взаимодействием с рычагами? без проблем. И самое топовое, что весь этот код не хранится в памяти. вм тупо в любой момент времени жрет всего лишь 64кб оперативки на хранение кода и данных.
если у тебя включен vsync, то можешь мерять скорость спрайтов кол-вом кадров, как это сделано в старых дос играх, вот и всё, зачем тебе ещё какой-то источник синхронизации
а если его нет, то можно и процессор то тысячи кадров разогреть, ничего ему не будет
>у них гранулярность в 15 мс.
попробуй написать проигрыватель миди-файлов без статтера
заикание звука для человека заметней, чем заикание движения
Это всё понятно, но это вызывает статтеринг. Смотри. Вот у тебя есть виртуальная единица. Условный метр/клетка. Она в реале будет занимать сколько-то пикселей на экране, согласен? И суть в том, что если ты не будешь двигаться с 1/0.5/0.25/0.125 пикселей в секунду, то из-за накоплений у тебя будет статтеринг, понимаешь? Да, если при переводе твоих юнито тебе повезло что на определенном мониторе с определенным разрешением у тебя при каждом рендере объект проходит фиксированное количество реальных пикселей - то поздравляю, статтеринга не будет. но и ситуации такой не будет
>можешь мерять скорость спрайтов кол-вом кадров
И каковы твои дальнейщие действия? чтобы игра шла одинаково, тебе придется замедлять движение тем у кого герцовка выше, соглы? а замедлять ты обязан на такое значение, чтоб это было либо ровно пиксель, либо половина, либо четверть. То есть так замедлить, чтоб ты проходил реальный пиксель за одно и то же количество кадров каждый раз.
нет, игроку придётся переключиться на режим 60 гц если хочет помедленнее
т.к. мы по умолчанию делаем какой-то пиксельный кал для ностальгирующих 40-летних нердов, то он и так и так не окупится
>игроку придётся переключиться на режим 60 гц
Некоторые современные мониторы с повышенной герцовкой не поддерживают 60 гц. Точную модель я тебе не назову, но суть в том, что никто как бы не гарантирует, что монитор будет поддерживать 60гц
К тому же непонятно, что ты там собрался замерять, если у тебя стратегия "Всех принудительно под 60 гц". Просто делаешь игру под 60 гц и всё
Когда вм считывает байт скрипта, она смотрит на старший бит. если там 0 - значит это обычный опкод (OP_ADD, OP_DRAW и т.д) и она тупо его выполняет. Если же там 1 - это сигнал, что перед нами вызов функции. вм считывает добавочный байт и из полученных 15 бит получает адрес функции которую нужно выполнить. То есть для вызова функции нужно 2 байта а не 3. Но адресное пространство уменьшается до 32кб. Но так как на данный момент у меня сцены занимают максимум 2кб - то это ваще ни о чем ограничение. + эта оптимизация сама по себе уменьшает размер скрипта, как бы делая адресацию более компактной. ваще нойс
То есть если мы хотим к 100 прибавить -10, то мы прибавляем (-10+65536). таким образом результат переполнится до 90. Ну разве не топчик?
это неуловимый глазу статтеринг. курсор по экрану двигаешь, тебе статтерит? а он подвержен этой проблеме
его можно заметить только если в твоей игре а) низкий фпс, б) игра считается в низком разрешении и для рендера апскейлится. достаточно повергнуть низкий фпс и апскейл, и статтеринга не будет
курсор быстро проходит расстояние и всё. а в игре долго и плавно перемещается персонаж например, или фон движется.
так как это моя личная вм (да и потому что это форт), там можно в переменных юзать вопросительные знаки. Самое то для предикатов:
: near_exit? player_x @ 30300 < ;
: near_puff? player_x @ 31000 32000 >< ;
: near_gamepad? player_x @ 32000 32600 >< ;
: near_tv? player_x @ 33000 34500 >< ;
: near_console? player_x @ 35500 36500 >< ;
: near_speaker? player_x @ 37200 38200 >< ;
: near_vend? player_x @ 40500 41000 >< ;
: near_trash? player_x @ 41600 > ;
: has_gamepad? has_gamepad 1= ;
: player_state_normal? player_state @ PLAYER_STATE_NORMAL = ;
: player_state_license? player_state @ PLAYER_STATE_LICENSE = ;
: player_state_floppy? player_state @ PLAYER_STATE_FLOPPY = ;
: player_state_gamepad? player_state @ PLAYER_STATE_GAMEPAD = ;
: player_state_keymap? player_state @ PLAYER_STATE_KEYMAP = ;
: player_state_playing? player_state @ PLAYER_STATE_PLAYING = ;
: console_ready? console_ready 1= ;
: can_use_tv? near_tv? player_state_normal? and ;
: can_use_speaker? near_speaker? player_state_normal? and ;
: volume_not_max? volume @ 4 < ;
: volume_not_min? volume @ 0 > ;
: can_increase_volume? can_use_speaker? volume_not_max? and ;
: can_decrease_volume? can_use_speaker? volume_not_min? and ;
: can_check_trash? near_trash? player_state_normal? and ;
: can_use_trash? player_state_floppy? player_state_gamepad? or near_trash? and ;
так как это моя личная вм (да и потому что это форт), там можно в переменных юзать вопросительные знаки. Самое то для предикатов:
: near_exit? player_x @ 30300 < ;
: near_puff? player_x @ 31000 32000 >< ;
: near_gamepad? player_x @ 32000 32600 >< ;
: near_tv? player_x @ 33000 34500 >< ;
: near_console? player_x @ 35500 36500 >< ;
: near_speaker? player_x @ 37200 38200 >< ;
: near_vend? player_x @ 40500 41000 >< ;
: near_trash? player_x @ 41600 > ;
: has_gamepad? has_gamepad 1= ;
: player_state_normal? player_state @ PLAYER_STATE_NORMAL = ;
: player_state_license? player_state @ PLAYER_STATE_LICENSE = ;
: player_state_floppy? player_state @ PLAYER_STATE_FLOPPY = ;
: player_state_gamepad? player_state @ PLAYER_STATE_GAMEPAD = ;
: player_state_keymap? player_state @ PLAYER_STATE_KEYMAP = ;
: player_state_playing? player_state @ PLAYER_STATE_PLAYING = ;
: console_ready? console_ready 1= ;
: can_use_tv? near_tv? player_state_normal? and ;
: can_use_speaker? near_speaker? player_state_normal? and ;
: volume_not_max? volume @ 4 < ;
: volume_not_min? volume @ 0 > ;
: can_increase_volume? can_use_speaker? volume_not_max? and ;
: can_decrease_volume? can_use_speaker? volume_not_min? and ;
: can_check_trash? near_trash? player_state_normal? and ;
: can_use_trash? player_state_floppy? player_state_gamepad? or near_trash? and ;
баловство это сплошное
О да, братан! ваще улёт.
просто сравни
форт:
: sum_n_dif
2dup - -rot + . .
;
5 3 sum_n_dif
8 и 2
луа:
function sum_n_dif(a,b)
return a+b, a-b
end
sum(5,3)
и в исходниках форт занимает меньше места, а в байткоде в зависимости от реализации так ваще топ.
луа сохранит кучу дичи, а в моей вм этот скрипт займет
OP_PUSH8 5 OP_PUSH 3 ADDR
5 байт вместо сотни с чем-то у луа. Но я не хочу сказать, что луа плохая, просто она более мощная и универсальная, но мне то это нафиг не надо в игре.
Пивет C, не умеющему возвращать 2 значения.
Это кста тоже топ. Не нужно в вм и компиляторе ничего усложнять. Нужно вернуть несколько знаений - оставь их на стэке и всё. никаких механизмов возврата, стэкфреймов и прочего. AST? нафиг не надо. Токенайзер? тупо по пробелам разбил и всё. Форт это мегапростая штука
6 байт, не 5, точно
не через malloc, разумеется. тупо размер меняет текущей позиции и всё. malloc ваще кал. у меня в движке всё заранее просчитано, какой атлас, сколько чего. огнёрий
объём RAM не является проблемой для современных пека, особенно на таких малых масштабах
А это не только ram, но и место на диске.
Ты спросил про пользу. Это польза. Одно дело сделать костыль, от которого только вред. Здесь же только плюсы, минусов нет.
Если переменная не предполагает никаких изменений, то тогда делаешь ее константой. если предполагает - то чтоб не было багов из-за двух источников истины - выставляй её строго в определенной функции (init/reset) и т.д
вм в зависимости от опкода будет знать, сколько байт ей считывать. это тоже экономия на куче всяких маленьких чисел по 1 байту.
коньпеллятор находит адреса обязательных для скрипта слов:
: on_load ;
: on_keydown ;
: on_keyup ;
: on_update ;
: on_draw ;
и сохраняет их в конце скрипта.
вм при загрузке берет эти значения и запоминает. а дальше все просто - C цикл при нужных событиях тупо прыгает по этим адресам. В конце колбэка виртуальная машина останавливается и C часть дальше работает
Еще скажи что из одежды у тебя только трусы и густая борода, А еще ты босиком по снегу ходишь и пристаешь молодежи рассказывая как экононить память на переменных.
например, если мы сейчас биндим клавиши, то тогда мы вызываем on_keydown. или же если нажата клавиша, которая уже забинжена. В противном случае on_keydown просто не вызывается. То есть в скриптах не нужно постоянно отсеивать, нажата ли именно реально зареганная клавиша, или просто левая. вм не вызовет колбэк если это левая какая-то клавиша
Не, ну погоди. а что плохого то в экономии памяти? я согласен, что это не критично. но и поводов не экономить не вижу.
я просто чекаю, что клавиша 1 не равна клавише 2 в массиве забинженых клавиш. Прикол в том, что по умолчанию там хранятся ноли, а биндинг не позволяет забить одну и ту же клавишу на разные действия. поэтому если ты прошел биндинг, то в памяти эти значения будут разнцми. если нет - одинаковыми. Ну разве не топ тема?
: flag_mask 15 & 1 swap << ;
: flag_addr 4 >> flags + ;
: flag+
dup \ flag flag
flag_addr \ flag addr
swap \ addr flag
flag_mask \ addr mask
over @ \ addr mask addr_val
| \ addr mask|addr_val
swap \ mask|addr_val addr
! \ addr = addr_val|mask
;
: flag-
dup \ flag flag
flag_addr \ flag addr
swap \ addr flag
flag_mask ~ \ addr ~mask
over @ \ addr ~mask addr_val
& \ addr ~mask&addr_val
swap \ ~mask&addr_val addr
! \ addr = ~mask&addr_val
;
: flag1? dup flag_mask swap flag_addr @ & 0 = if 0 else 1 then ;
: flag0? flag1? 0 = if 1 else 0 then ;
: flag~
dup \ flag flag
flag_addr \ flag addr
swap \ addr flag
flag_mask \ addr mask
over @ \ addr mask addr_val
^ \ addr mask^addr_val
swap \ mask^addr_val addr
! \ addr = mask^addr_val
;
нафига тратить на бинарный флаг 16 бит, если можно тратить 1 бит, верно?
итого, что мы имеем. моя ram из 64кб делится на 32кб глобальной, и 32кб локальной для конкретной сцены. Но!
4096 переменных из глобальной памяти отданы под 65536 флагов.
таким образом у меня в игре доступно (32768 - 4096) глобальных переменных, 32768 локальных переменных и флаги я тоже условно делю на глобальные и локальные.
: flag_mask 15 & 1 swap << ;
: flag_addr 4 >> flags + ;
: flag+
dup \ flag flag
flag_addr \ flag addr
swap \ addr flag
flag_mask \ addr mask
over @ \ addr mask addr_val
| \ addr mask|addr_val
swap \ mask|addr_val addr
! \ addr = addr_val|mask
;
: flag-
dup \ flag flag
flag_addr \ flag addr
swap \ addr flag
flag_mask ~ \ addr ~mask
over @ \ addr ~mask addr_val
& \ addr ~mask&addr_val
swap \ ~mask&addr_val addr
! \ addr = ~mask&addr_val
;
: flag1? dup flag_mask swap flag_addr @ & 0 = if 0 else 1 then ;
: flag0? flag1? 0 = if 1 else 0 then ;
: flag~
dup \ flag flag
flag_addr \ flag addr
swap \ addr flag
flag_mask \ addr mask
over @ \ addr mask addr_val
^ \ addr mask^addr_val
swap \ mask^addr_val addr
! \ addr = mask^addr_val
;
нафига тратить на бинарный флаг 16 бит, если можно тратить 1 бит, верно?
итого, что мы имеем. моя ram из 64кб делится на 32кб глобальной, и 32кб локальной для конкретной сцены. Но!
4096 переменных из глобальной памяти отданы под 65536 флагов.
таким образом у меня в игре доступно (32768 - 4096) глобальных переменных, 32768 локальных переменных и флаги я тоже условно делю на глобальные и локальные.
но я не пишу его в скриптах под этим именем. компилятор превращает его в T_MENU
и это по сути просто индекс ассета от 0 до 65535
в дебаг версии у меня генерится массив путей для текстур, где textures[0] = "menu"
а в релиз версии будет юзаться массив смещений. Круто, да? мне не нужно будет ничего менять. просто в зависимости от того релизный у меня билд или нет, движок будет какой-нибудь M_PLAYER интерпретировать либо как "textures/" + textures[666] + ".bmp", либо как fseek(asset_offsets[666])
А это и сейчас круто. Никаких систем cmake и прочего. компиляция через tcc мгновенная. всё чётко работает без сюрпризов. в финальном билде никакого bloated кода нет.
SDL ваще шикарная штука. Решает ровно те задачи, которые платформозависимы, а то, что везде одинаково - мутишь сам. Ваще никак не лезет в твои дела.
Даже эти ужасные vulkan/metal абстрагирует. У тебя просто единый api для загрузки/отрисовки текстур.
Я хочу всё чётко сделать. план такой - добавлю сохранение кадра в bmp и открою игру в минимальном окне (320x180). похожу там, поделаю что-нибудь, а потом уже через какой-нить ffmpeg увеличю и соберу хоть в gif хоть в видос. а то если просто записывать - будет не то. не смогу всё величие передать.
так что ваще огонь будет
Только представьте - игра сразу доступна каждому, у кого есть комп и интернет. Никаких тебе проблем с RTL текстом, никаких китайских огромных атласов и гемора с отрисовкой их символов в низком разрешении. никакой арабской вязи, которая не приемлет monospace символы а требует единый красиво связный текст. Ну и конечно же нет проблем с тем, что переводчики не так передатут шутку или идиомы какие. Ну и в принципе, обычно переводят на самые популярные языки, а остальные в пролёте. здесь же все люди мира смогут играть. Шо ещё. Так как в игре не требуется мышка, то и на консоли можно портануть. Я уже реализовал некоторое количество загадок. Кароч в игре будет один связный мир, и куча загадок. но все они будут уникальны и не будут повторяться. Причем для базового прохождения будут не очень сложные, а опционально для 100 процентов придется попотеть.
Кста, настройки игры происходят в самой игре. Это и регулировка громкости, и выбор полный экран/окно. а использоваться будут лишь клавиши вверх вниз влево вправо и одна еще чтоб попадать в сцену menu (escape, например)
Не, это реально будет артефакт. я 4 года изучал нужные технологии, мутил прототипы, и вот, наконец начал мутить. Архитектурно всё готово, несколько сцен есть, идеи для загадок тоже. еще лет 6 и можно будет релизить
>если же вы делаете pixel perfect игру с условным виртуальным разрешением 320x180
>>58908
>открою игру в минимальном окне (320x180)
вот и причина статтеринга
Не, статтеринг это не про количество пройденных пикселей за кадр, а про равномерность.
там не совсем форт система. никакого словаря и шитого кода нет. просто скрипты компилятся в байткод, а потом вм его выполняет. поэтому я и написал, что вм форт подобная. от форта я взял синтаксис (@ ! . dup drop swap over), стэковый подход и обратную польскую нотацию.
а еще твой взгляд фиксируется на месте, куда ты хочешь попасть курсором. ты лишь переферийно палишь, когда пора стопорить курсор. а в игре ты наоборот следуешь глазами за движущимся персонажем. это два совершенно разных момента.
Никто не оценит. Вопрос не имеет смысла. Если решил быть как Пэй Мэй то сиди один на горе, а не демонстрируй свое "вяличие" перед мимокроками, а то плохо кончишь, как Пэй Мэй.
то есть всё самое худшее
ведь плюс форт-системы в том, что её можно запустить на картошке без ОС и в том, что пользователь может написать свой словарь, а не в обратной нотации и прямой манипуляции стэком
я вот сейчас проследил за своим виндовым курсором и заметил что он как-то дёргается. надо писать свою ОС.
ну, или можно юзать курсор по назначению - не следить за ним, а наводить в нужную точку.
>>58964
Наоборот. скрипты компактные, компилятор и вм просты как палка.
>>58963
Я просто максимально по простому рассказал шо такое игра. А то вдруг кому то слишком сложны все эти ваши тиринги, статтеринги, fixed timestepы и прочее. такие дела
плюс скорость исполнения лютая. вм либо функцию по индексу вызывает (опкод), либо прыгает в нужную позицию в коде (функция). никаких поисков по строкам нет. ни хэш таблиц не надо, ни malloc. всё чётко, быстро, просто, компактно. ух! УХ!
Для fixed timestep-ов и прочего необязательно говной обмазываться и писать свою ОС. Это базовые понятия и они применимы везде, а не только в шизе где экономят память на переменных.
>Для fixed timestep-ов и прочего необязательно говной обмазываться и писать свою ОС
Никто и не спорит. ОС уже написаны, курсоры уже статтерят. Если их использовать по назначению, то никаких проблем с ними нет.
Усё так
>шитого кода нет
>скрипты компилятся в байткод
>>58980
>компилятор и вм просты как палка
>>58982
>плюс скорость исполнения лютая
Шитый код быстрее и проще байткода.
Байткод - это вот такая портянка:
>switch (code) {
>case 0: a(); break;
>case 1: b(); break;
>case 2: c(); break;
>...
>case 255: z(); break;
>}
А шитый код - это pointer'ы функций.
Шитый код выполняется "как есть".
А четкая линия где проходит, не подскажешь? если сто мегабайт например всего сэкономил - это уже шиз или еще нет?
смотря сколько денег ты сэкономишь и какой у тебя бюджет
А вот и не портянка. никаких case. есть массив указателей на функции и ты просто их через индекс вызываешь.
>есть массив указателей на функции
Тогда это уже получается шитый код?
Байткод - это 1 байт на одну функцию.
Шитый код - это 4 (x32) или 8 (x64) байт.
шитый код в классической форт системе при вызове условного draw должен пройти по словарю и найти где такая запись есть. это дольше чем смещение + дереференс
>Тогда это уже получается шитый код?
Не. в форте все функции фигачатся в словарь.
а тут есть разделение - самая база (опкоды типа swap add over + высокоуровневые опкоды типа draw play_sfx blit) это реальные функции на C. Есть массив указателей на эти функции, и скрипты тупо юзают 1байтовый индекс для их вызова. если же мы говорим о пользовательских функциях, типа update_enemies, то это уже слово в скрипте. и вот оно вызывается используя 16бит (1 бит чтоб сообщить, что это вызов функции, а не базовый опкод), и 15 бит адрес. Кроссплатформенно и компактно.
Огнёрий одним словом
кароч, вижу, что слишком сложно для тебя. давай разжую всё.
Смотри - при вызове слова, определенного в скрипте, мы просто кладем в возвратный стэк значение и прыгаем по 15битному адресу. никаких массивов, дереференса и прочего. считал адрес - перешел. всё, изи.
но весь код невозможно написать на форте, ведь вм должна уметь выполнять базовые вещи, из которых ты уже и будешь формировать слова. И вот уже в этом случае мы пишем код на C. и для его вызова мутим массив функций. Теперь то дошло до тебя?
>должен пройти по словарю и найти
>>59044
>все функции фигачатся в словарь
Ты/вы путаешь/те интерпретацию с компиляцией.
Форт компилирует одну функцию сразу после ввода команды ";" и дальше вставляет указатель функции в процессе компиляции следующих функций. Именно поэтому Форт позволяет переопределять функции. Именно поэтому новая функция повлияет лишь на новейшие функции, записанные уже после того, как произошло переопределение функции.
Т.е. во время выполнения кода Форт не производит "поиска по словарю", он просто бегает по адресам, скомпилированным во время определения слов (т.е. функций). Поиск происходит только тогда, когда ты определяешь значения новых слов (функций).
Именно поэтому Форт быстрее всех скриптовых ЯП, поскольку он компилируется в шитый код сразу, а производительность шитого кода примерно равна машинному, потому что он выполняется самой этой машиной (процессором), а не каким-то там C/C++.
то есть ты предлагаешь в финальном билде тащить компилятор, а скрипты в исходном виде? оо да, вот это угар. смех да и только.
зато пользователь сможет переопределять слова, как жить то без этого
>выполняется самой этой машиной (процессором), а не каким-то там C/C++.
>Именно поэтому Форт быстрее всех скриптовых ЯП
Ого, ничего себе. Процессор быстрее чем вм. вот это да
dcpu-16 был регистровым вроде.
регистровая вм геморная во внутреннем устройстве. меня forth этим и зацепил.
есть просто стэк (2 стэка, включая возвратный) и ты туда просто кидаешь значения и работаешь с ними. главное соблюдать порядок, и ничего вручную указывать не придется. шикарно просто поток данных. форт напрямую готов к тому, чтоб на нем писать, не нужно поверх делать высокоуровневую обвязку как в луа, которая внутри с 5 версии тоже регистровая.
Кстати, можно вместо определения частоты монитора и frameskip просто юзать аккумулятор. Главное не вычитать из аккумулятора время, а просто обнулять его при переполнении.
Ставим значение не 33, а, например, 30 чтоб гарантировано переполнялось и всё.
Таким образом монитор с частотой 60гц будет прибавлять по 16-17мс, то есть гарантировано каждые 2 кадра будет происходить тик (32 > 30). и так для любой частоты. то есть нужно просто обнулять аккумулятор, чтоб никакие остатки не накапливались.
Проверка для большой частоты. Если монитор 1000гц, то он будет добавлять по 1 в аккумулятор. значит каждые 30 кадров будет происходить тик. Правда в таком случае нужно назвать аккумулятор как-то по другому. Хоть он и копит значение до 30, но остаток то не накапливается. Логичнее его просто счётчиком назвать и всё. Изи бризи таймер сквизи. Пользуйтесь, не благодарите.
условный 500 гц монитор будет сообщать то 1 то 2 мс (или ваще 0), и снова будет статтеринг из за разного времени между тиками. тогда уж надежнее не маленькие дельты замерять и копить, а просто проверять что get_ticks() - last_time >= 30.
а у тебя что все спрайты с одной скоростью всегда двигаются? в общем, ничего тупее я на этой неделе еще не слышал
неудивительно, что оп даже ни одной гифки не показал, все увидят как скачет его ламерский апскейл
>все спрайты с одной скоростью
Ох, ну давай я тебе по полочкам всё расставлю. Видимо это очень сложный момент для понимания.
Когда я говорю про равномерность, я говорю про то, что спрайт, какая бы у него скорость ни была, должен идти с этой скоростью равномерно. то есть если его скорость в пересчете на пиксели = 5 пикселей, то он каждый кадр должен ее проходить. Если он проходит то 5 пикселей, то 4, то опять 5, то 6, это будет дерганым движением. это и есть статтеринг. Дело не в том, что все спрайты имеют одинаковую скорость, или один спрайт не должен уметь разгоняться/замедляться. дело в том, что если у него в данный момент есть какая-то скорость, пусть и не линейная, он должен ее на уровне кадров поддерживать. Если он разоняется, то должно быть 5,6,7,8,9... или 5,5,5,5,6,6,6,7,7,8,9. но никак не 5,5,6,5,6,6,7,8,7,8,9. Понимаешь?
твоя система не поддерживает произвольные скорости. в 60 фпс скорость 80 п/с будет так: 1 1 2 1 1 2 1 1 2
ты ничего с этим не сделаешь, я всегда найду такую скорость которая полностью ломает твоё говно
то есть в твоей параше спрайт может двигаться либо со скоростями 15, 30, 60, 120 п/с
ну и выглядит это естественно еще хуже чем статтеринг, 0 плавности, какие-то инкрементальные ускорения решающие несущетсвующую проблему
естественно когда ты свою хуйню апскейлишь в 10 раз, то она у тебя статтерит прыгая то на 10 то на 0 пикселей за кадр, и выглядит это как говно, но нам пишешь про единичные пиксели, потому что уже понял что в апскейле вся проблема, но не хочешь это признавать
Разумеется это вранье. Статтеринг хуже. И какую такую ты там скорость собрался находить я не понял. Реверс инжиниринг сделаешь что ли?
>может двигаться либо со скоростями 15, 30, 60, 120 п/с
Ого, в пиксельарте дискретная сетка. Не может быть.
>мне не нужно
А ты и не можешь. Потому что в моей игре я пишу что и как будет двигаться. Твоё "а вот я могу там что-то поменять и всё сломается" это просто демагогия
пиксель-перфект это когда выглядит как позорное говнище где скорость меняется в 2 раза и скролл по 10 пикселей прыгает за раз
именно эти 2 слова лучше всего описывают то что ты считаешь пиксель-перфектом и твою потешную текстовую безгифочную победу над статтерингом
у адекватов на экране не 180 пикселей, а 1080, и всё равно всё пиксель-перфектно
Ты путаешь простой пиксель арт и pixel perfect. Понять разницу не сложно. Достаточно просто поизучать тему.
А статтеринг я победил, да. теперь движения без рывков. Которые, кстати, никуда не исчезают при не_pixel_perfect отрисовке.
удачи
Так результат тот же, не?
Кстати, tldr на оп-пост: замедляйте или ускоряйте игру соответственно частоте экрана игрока.
конечно же нет.
Аккумулятор, который не переполняется стабильно каждые n кадров, создаст статтеринг.
Единственный способ уничтожить статтеринг - либо принудительно переключать режим монитора, либо обновлять логику через n целых кадров). Тогда тебе не придется полагаться на неточное время, которое капает в аккумулятор.
да и если ты юзаешь интерполяцию - у тебя тоже будет статтеринг, потому что там при умножении на float у тебя нет контроля, сколько времени на каком пикселе у тебя пройдет.
Ты проходишь виртуальный юнит за секунду. Виртуальный юнит на определенном мониторе занял 7 пикселей. Частота 60гц.
Уже видно, что если ты растянешь 60 кадров на 7 пикселей, то каким то пикселям выпадет больше кадров, каким то меньше. привет, статтеринг
в позиции 0 спрайт отрисовывался 2 кадра, в позиции 1 - 1 кадр. потом в позиции 2 опять два кадра
>называли предотвращением субпиксельного движения
Ууу, как все запущено...
ты можешь языком трепать сколько угодно
выкладывай свою чудо-поделку без статтеринга, сами посмотрим своими глазами
>в позиции 0 спрайт отрисовывался 2 кадра, в позиции 1 - 1 кадр. потом в позиции 2 опять два кадра
Если у тебя было линейное движение - то это баг в интерполяции и ты обосрался, чини. Если интерполяция работает, как положено, то такой ситуации не возникает вообще.
Так это потому что ты долбоёб. Алгоритм Брезенхэма, подсчёт PPU и правильная интерполяция. Всё, нет проблемы.
Молодец, знаешь много умных слов. Но это не отменяет того, что 60 при делении на 7 не дает целого числа.
посмотри на ОП-пик, это наверное игра ОПа
у него пиксельное ретро с разрешением 160 на 120 и 8 цветами
юнитом там является пиксель, и применение алгоритма брезенхейма не нужно т.к. пиксели рисуются как есть
>>59687
это является проблемой только для безыгорников, которые дрочат форт вместо делания игр
>это является проблемой
Да, и называется это статтерингом. Только равномерное распределение кадров на пиксель решает эту проблему
нет, статтерингом называется другое, ты путаешь это с проблемой пиксель-перфектной сетки
лучше выложи скорее свой движок или игру на движке, демку какую-нибудь хотя бы
>что 60 при делении на 7 не дает целого числа
Потому заебись, что это никогда не требуется.
>>59692
>не нужно т.к. пиксели рисуются как есть
Он нужен хотя бы чтобы понять в каком именно пикселе рисовать сейчас, а в каком - в следующем кадре. С ним не будет такой хуйни, как долбоёб описывает, что у него движение рывками происходит. К тому же, даже если твоё пиксельное говно в разрешении 160х120 - ты никогда не будешь рендерить его в таком разрешении. И здесь опять опять нужен Брезенхэм.
>проблемой пиксель-перфектной сетки
неравномерное распределение кадров никак не зависит от того, какая у тебя сетка.
>это никогда не требуется
потребуется, конечно же.
частоты у мониторов разные. всегда можно подобрать частоту так, чтоб твой юнит проходил со статтерингом.
так в твоём случае проблемы статтеринга и нет. ты запутался
ставишь фиксированную частоту кадров и перемещаешь на фиксированное число пикселей
а статтеринг появляется в играх с высоким разрешением и трёхмерной графикой
вспомнил очень старую статью на эту тему
https://gafferongames.com/post/fix_your_timestep/
>ставишь фиксированную частоту кадров
ты про смену режима монитора?
если да, то я этот способ упоминал. у него свои минусы
ну а статья - это классический аккумулятор, в который и время между кадрами неточное капает (croteam на это жаловались), и интерполяция через умножение на альфу, которая снова приводит к неравномерному распределению. зато всё детерменировано, это да.
это не классический аккумулятор и он не пытается измерить время между кадрами
это решает проблему статтеринга но не решает проблему пиксельной сетки, потому что это разные проблемы. это ты смешал их в одну.
правда, при интерполяции появляется другая проблема - отображение отстаёт от симуляции на 1 кадр
>он не пытается измерить время между кадрами
double newTime = time();
double frameTime = newTime - currentTime;
if ( frameTime > 0.25 )
frameTime = 0.25;
currentTime = newTime;
accumulator += frameTime;
frametime как бы даже в названии
это для пополнения аккумулятора, а не для расчёта на сколько подвинуть симулируемый объект
нет, статтерингом называется другое, ты путаешь это с проблемой недетерменированной физики
статтеринг это и есть проблема недетерминированной физики/логики, когда твой апдейт игрового мира зависит от измерения частоты кадра, что нельзя сделать точно
а если он не зависит, то и статтеринга нет. проблема решена.
я не пытаюсь обесценить твой движок. лучше покажи его, чем вести пустые споры.
>статтеринг это и есть проблема недетерминированной физики/логики
ну разумеется это неправда.
у тебя игра может выглядеть идеально плавно, но при проигрывании replay у тебя условный снаряд в worms полетел по другой траектории.
и в том и в том примере всё выглядело идеально гладко.
ты путаешь. вот один и тот же результат из раза в раз - это детерменированность. а неравномерное распределение кадров на пиксель это статтеринг
статтеринг это "заикание", оно описывает такой эффект, когда твоя программа замеряет время кадра неправильно, т.к. точного механизма для этого не существует, и на основании этого двигает объекты
и да, это будет недетерминировано. в одном случае у тебя объект пролетит через стену, а в другом - оттолкнётся
если ты думаешь, что это из-за пикселей на экране, то у тебя и при проигрывании видео будет "статтеринг"
может тебе стоит купить новый компьютер? возможно, твой пека просто не может выдавать даже 30 кадров в секунду. тут никакие программные ухищрения не помогут
простой тебе пример.
у тебя двигается персонаж за 1 тик физики (60 гц, например) на 1 юнит.
ты для интер/экстраполяции умножаешь его cur_x на альфу, чтоб между тиками рисовать позиции. Но frametime всегда колеблется, и у тебя получается что и альфа сначала 0.16, потом 0.33, потом ваще 0.45. при умножении на нее у тебя каждый кадр будет разную финальную позицию выдавать.
но и это еще не все. ты можешь, как советовали crotek, выставить frametime в фиксированное значение, раз знаешь частоту, но даже если ты сделаешь так, включишь линейную фильтрацию (чтоб обьект плыл между пикселей, плавно меняя цвета пикселя от текущего к соседнему), то ты просто как бы увеличиваешь виртуальное разрешение. условный черный квадрат на белом фоне будет уметь перетекать очень мелкими шагами, а если контраст слабый, то таких шагов будет меньше.
>при проигрывании видео будет "статтеринг"
конечно, на всяких panning это всегда заметно, если на 60гц смотришь 24 кадра видео, или на каком нибудь 144гц смотришь 60 fps
>возможно, твой пека просто не может выдавать даже 30 кадров в секунду.
на джодоте - нет.
на топ движке sdl3 + форт - хоть тысячу. такие дела
от 25 на 50 практический такой же статерринг. тут низкий фреймрейт на sample-and-hold дисплее виноват.
Кста, насчет звукового сопровождения.
кароч мутите максимально разнообразный звук длиной секунд 20 примерно. Чтоб там и скрежет был, потом пение птиц, еще какую нибудь дичь. Жмёте чем нибудь, например тем же adpcm на 15 строк кода и храните в ассетах.
Затем методом гранулярного синтеза тупо берете и бегаете по этому мегасэмплу одновременно в нескольких местах с разными скоростями и громкостями. можно не мутить полноценный adsr.
я вот юзаю ar. то есть сколько звук нарастает до заданной громкости и сколько затухает.
Я на одном только пении птиц замедленном лютые эмбиент текстуры выдавал. ух, УХ!
Ну и звуки можно соответственно мутить, просто сократив атаку и спад. В итоге 20 секунд моно аудио с сэмплрейтом в 44100 в ima_adpcm будет весить если не ошибаюсь в районе 450кб. Можно ваще в качестве сэмпла заюзать пак с ассетами (текстуры + скрипты), но не уверен, что там можно будет много клевых звуков получить.
воть. ну и звуковые файлы это будет тупо формат с относительными значениями времени между нотами и их длительностями.
Пользуйтесь наздоровье, братишки
Ну и типа можно простенький hard knee limiter намутить. типа у меня в игре максимум может играть эмбиент окружение + 2 одновременных звука.
У них громкости будут 0.75.
значит даже если волны максимально неудачно совпадут, то пиковая громкость будет 0.75*3=2.25.
а нам нужно чтоб максимум был 1.0, а лучше чутка поменьше.
прижимать громкость будем значица начинать с 0.8.
множитель должен быть таким, чтоб 2.25 при умножении на него превращался в 1.0. если сэмплы тише 0.8 в сумме - то не трогаем, иначе берем 0.8 и прибавляем наше помноженное превышение. Я тестил кароч на трех одновременных звуках - ваще нойс, уж для эмбиента незаметно точно. Насчет резких ударных не скажу.
Не благодарите.
еще бы смешивания побольше. ощущение, будто максимум 2 прохода одновременных.
кста, чо только ща понял. у меня adpcm таблицы в глобальной области. но это имеет смысл, только если тебе нужно и кодирование и декодирование.
а так как в движке мне нужно только декодирование, то надо будет таблицы в функцию adpcm_decode прям запихать через static.
и назвать можно будет короче, и глобал лишний раз не засирать.
вот уж реально, когда рассказываешь резиновой уточке что то (или анончикам), то всякие топ озарения приходят. ваще огнёйс
держишь любую клавишу и фиолетовая сфера продолжает лететь (в фиксированном направлении). отпускаешь клавишу - фиолетовая сфера ждёт. тащемта нужно не столкнувшись с красными сферами как можно больше раз отрекошетить. слева "рекорды" разных персонажей мира
к логопеду запишись