Log of Alprog
1.19K subscribers
98 photos
91 links
加入频道
Про бег и уравнения
#код
По моим наблюдениям практически единственное, что у людей остаётся в памяти из школьной программы по математике — это как находить дискриминант квадратного уравнения. Забавно, но при разработке игр это как раз надо крайне редко. Я вот, например, без гугла и не скажу, где там минус и из кого. А вот тригонометрию всякую помню наизусть, потому что нужно каждый день. Но иногда всё-таки дискриминант тоже нужен. Сейчас приведу пример. Собственно, весь дальнейший пост будет просто иллюстрацией типичного случая, когда нужно немножко математики (серьёзно, ничего другого не будет).

Вот потребовалось мне сделать, чтобы человек на бегу останавливался синхронно с анимацией. У нас в аниматоре есть параметр скорости движения. Один метр в секунду — лёгкий шаг, два быстрый, три и более — бег. И так настроено, что ноги двигаются аккурат с этой скоростью. Резко этот параметр дёргать нельзя (будет скачок), надо плавненько. Но и когда моделька на месте стоит, долго крутить ползунки тоже нельзя — будет ногами в воздухе болтать. В идеале надо чтобы всегда скорость движения персонажа совпадала с анимацией. То есть для этого надо начать тормозить чуть заранее фактической остановки.

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

Вот и получается, что в каждый момент времени у нас есть текущая скорость, расстояние до ближайшей поворотной точки и скорость, которая должна быть на финише. Если персонаж разгоняется до максимальной скорости, скажем, за один метр пути, а тормозит за два, и при этом у нас 100 метров до финиша, то вопросов не возникает. Но как быть, если расстояние до следующей маршрутной точки всего метр? Сколько времени мы можем позволить себе разгоняться на этом пятачке, прежде, чем начать тормозить, чтобы остановиться точно в конце? А может у нас уже ненулевая скорость и вообще нет времени сопли жевать и пора экстренно тормозить? Может быть даже резче обычного (если дверь закрылась прямо перед носом). Ну и, конечно же, надо, чтобы всё эта байда от FPS никак не зависела.

Как быть в этой ситуации? Известно как: составлять систему уравнений и решать, как в школе. Сперва я, правда, вычитаю дистанцию, которую персонажу в любом случае надо пройти, чтобы компенсировать разницу между начальной и конечной скоростями. Так что в начале и в конце манёвра мы имеем одинаковую известную скорость v1. А в конце ускорения (и перед началом торможения) неизвестную скорость v2. Ещё нам известна общая длина пути s, а также a1 и a2, то есть ускорения разгона и торможения соответственно, но неизвестны их длительности t1 и t2.

Записываем это в виде формул равноускоренного движения и понеслась. Я имею привычку набрасывать решения в paint.net, так что весь процесс вы можете наблюдать в шапке поста. В конце остаётся посчитать коэффициенты и найти корни квадратного уравнения.

Вот примерно так у нас теперь персонажи ходят. Всё, правда, несколько сложнее, потому что надо ещё погемороиться с поворотами, но это уже другая история.

Обсудить
Две истории моей юности про вирусы
#код
Чатик ожил и пристыдил меня за молчание на канале. Что ж, давайте я вам расскажу две истории про вирусы. В первой я буду выступать злостным создателем вредоносного ПО, а в другой, напротив, окажусь доблестным антихакером (блин, количество баек, которые я могу рассказывать в барчиках стремительно сокращается из-за бложека).

Когда мне было лет 14, я уже два года писал примитивные игры на бейсике, делал карты для Half-Life (иногда даже на заказ), и тому подобные вещи. Словом, файлы моего производства распространялись среди знакомых довольно широко и запускались несмотря на угрозы антивируса без подозрений. А ещё мы тогда играли в StarCraft через HyperTerminal (некоторые подписчики, наверняка, таких слов даже не слышали, но это была такая штука, позволяющая соединяться в локальную сеть через телефонные модемы). Само собой, я тоже захотел сделать сетевую игру. После успешного релиза и плейтеста моих крестиков-ноликов на WinSockets, у меня появился мой первый (и последний) коварный план по написанию вирусов. Идея была поистине хитроумной для моих лет. Я собирался написать клиент шахмат, который бы также позволял лазить по файловой системе другого игрока, пока ты якобы размышляешь над ходами. Му-ха-ха! Оценили масштаб вероломства?

От злодеяния меня тогда остановила только внутренняя тяга к свету и уважение личных границ… Сейчас, глядя на код тех крестиков-ноликов, можно подумать, что меня также остановила техническая неспособность написать что-то столь сложное, как шахматы. Но пару лет спустя, уже в университетские годы, когда я написал сетевые «точки» (родственник игры «го»), я всё-таки встроил в клиента возможность лазить по чужой файловой системе. Встроил, сыграл с жертвой, но не воспользовался. Потому что я кайфанул от самого факта «взлома», а шпионить за людьми всё же нехорошо.

Первая история была для разминки, но вторая будет более захватывающей. Родители никогда не воспринимали всерьёз моё увлечение разработкой игр, поскольку не особенно отличали это занятие от собственно игр, долго отговаривали поступать на программиста, но в какой-то момент всё же смирились и даже в середине первого курса подарили мне личный ноутбук. Даже у старшего брата не было отдельного компа, а у меня был. И я с гордостью таскался с этой почти 5-килограммовой махиной в универ. Опять же, некоторые подписчики могут офигеть, но тогда это был нормальный вес для ноутбука и в них, представьте себе, пихали даже CD-ROM.

Так вот. Родители на тот момент хотя и расщедрились, но всё же не особо видели во мне программиста. Всё изменилось, когда однажды родители подхватили вирус. Стандартное по тем временам дерьмо про то, что комп заблокирован, и вы должны куда-то там перевести деньги. Курсор не реагирует, диспетчер задач не вызывается, безопасный режим не помогает. Переустанавливать винду не хочется.

В определённый момент я запихиваю в привод диск какой-то игрушки и слышу, что играет музыка автоплея, хотя самого окна на экране нет. В это мгновение я понимаю, как вирус работает. Я тогда как раз читал книгу форума VBStreets — сборник различных нетривиальных ухищрений с Visual Basic. Среди всего прочего там был пример по созданию дополнительных рабочих столов в Windows. Если кто не знал, такая возможность программно была встроена даже в XP, просто не была протянута в интерфейс. И вот то, как вёл себя вирус, было чертовски похоже на запуск примеров кода по рабочим столам (если тебя переключили на другой стол, ты никак не можешь повлиять на другие).

Что же делаю я? Я сообщаю родителям, что могу написать антивирус, сажусь за ноутбук, пишу за час-другой программу, которая создаёт новый десктоп и открывает там окно-переключалку между столами; создаю файл autoplay и записываю его на болванку. Затем вставляю свежезапечённый диск в заражённую машину и — о чудо — на экране появляется окошко с кнопкой, возвращающей управление компьютером. После чего уже руками нахожу и убиваю остатки вируса. Стоит ли говорить, как после этого выросло уважение ко мне со стороны родителей?

Обсудить
Переобувания Блаба
#код
Что-то нашло настроение поразмышлять о том, как часто я менял мнение на программерском поприще. Такое на самом деле случается нечасто, потому что мы все живём в пузырях из собственного опыта. Первый инструмент или подход, который хорошо себя зарекомендовал, формирует в нас эффект утёнка. Затем мы этим самым утёнком какое-то время восторженно забиваем все гвозди в округе. И вот спустя десятки птичьих, натянутых на разнообразные модели Земли, когда по-хорошему стоило бы задуматься об этичности происходящего, у нас уже достаточно травматичного опыта, чтобы спутать стокгольмский синдром с любовью.

Когда твоя позиция сформулирована и озвучена коллегам возле кулера, когда по теме написаны посты и твиты, бывает уже очень трудно выйти на следующий виток спирали понимания и начать декларировать противоположное своим же вчерашним взглядам. Самая главная моя ошибка прошлого — это конечно то, как упорно я в юности не признавал, что Visual Basic плохой язык (если вам резануло ухо слово «плохой», то переформулирую: неподходящий для моих задач). Осознание этого было длительным и болезненным. Я топал ножкой и капризничал, лишь бы не учить С++. Но сегодня я фанат крестов. Я добрался сюда через пару витков .Net, но что-то мне подсказывает, что на этом моя личная спираль обрывается. Конечно, сейчас я привязан к С++ во многом стокгольмским синдромом, но как мне кажется, у меня есть и рациональные аргументы, почему оглядываясь назад через 10 лет я не сочту это ошибкой.

Другие примеры изменения моего мнения не столь драматичны, но тоже случались. Например, я помню, как 10 лет назад вкручивал локализацию в игру для Wii. И я тогда на полном серьёзе втирал, что для геймдева строки лучше хранить в 8-битных кодировках с переключением кодовых страниц (Windows-1251, 1252 — вот это вот всё). Мне тогда представлялось важным иметь доступ к нужному символу по смещению в строке, а необходимость помнить кодировку совсем не смущала и не казалась геморройной. Я проникся силой юникода уже буквально через год, но стыдно до сих пор.

А вот если говорить о вечном, то помнится в известном холиваре порядка байт я раньше был на стороне тупоконечников. Превалировало во мне что-то такое дикое, что-то из мира человеческого письма. А теперь я как будто больше проникся машинной красотой остроконечности. Следуя этой же логике я, по идее, должен был стать и фанатом column-мажоров из OpenGL, ведь это так математично. Но здесь я почему-то пропутешествовал на один виток дальше и всё-таки остановился на том, что они (вместе со всей математикой) наркоманы и курильщики, а нормальные люди матрицы перемножают слева направо.

Это далеко не самый интересный для чтения мой пост получился: всем, в общем-то, плевать, какие конкретно метаморфозы проходило чужое мнение. Но как мне кажется, это довольно занятная тема, чтобы порассуждать и проанализировать свои взгляды. А вы помните, как кардинально меняли своё мнение по холиварным темам?
Логарифм и резинки
#код
Как-то в одном из чатов обронили фразу: «а когда вам последний раз был нужен логарифм?». Забавно, но мне он потребовался буквально на следующий день. Это ещё один маленький пост о том, какого рода математика нужна геймплейному программисту в повседневной жизни.

Часто нам нужно сглаживать какие-то процессы, у которых нет чёткого конца или он меняется во времени. Ну, например, у нас один объект — пусть это будет ручной дракончик — движется на воображаемой резинке за другим объектом, который тоже постоянно в движении: скажем, это будет персонаж игрока. Если игрок оказался далеко от дракончика (например, телепортировался), то дракончик сперва летит к нему очень быстро, но при приближении постепенно замедляется, чтобы это смотрелось хорошо.

Новички обычно просто домножают скорость дракончика на расстояние до игрока. Это простое решение, но ужасно плохое, поскольку время, за которое дракончик долетит до игрока, будет напрямую зависеть от FPS. При низком фпс он будет добираться до цели быстро, а при большом топтаться на месте неожиданно долго. А если FPS скачет, то и вовсе придётся лицезреть нечто рывкообразное.

Юнитологи классом повыше обычно используют небезызвестную функцию SmoothDamp. Внутри там скрывается мудрённое решение из книги Game Programming Gems 4. Вот только нам приходится где-то хранить текущую скорость для каждого процесса сглаживания, да и в целом довольно страшно выглядит. Нельзя ли как-то попроще сделать и без лишних переменных в местах вызова?

На самом деле если мы задумаемся, как будет выглядеть FPS-независимый способ приближения со сглаживанием, то быстро поймём, что нам надо проходить одинаковую долю расстояния за одинаковое время. Например, за первую секунду проходим половину пути, за вторую секунду половину от половины, то есть остаётся четверть, затем 1/8, 1/16 и так далее. И никогда мы по настоящему не достигаем цели, но нам это и не надо. При таком движении неважно в какой точке этого процесса мы оказались (на первой секунде, второй и т.п.), мы всегда знаем, как рассчитать движение дальше. От пути всегда остаётся лишь

1 / 2^t

А значит пройденное расстояние от времени вычисляется по формуле:

1 - 1 / 2^t

Двойка здесь всего лишь указатель на то, что в качестве одинаковых промежутков мы выбрали половину расстояния. Мы можем подставить туда 3, чтобы получить треть, или любое другое число больше 1. Можно думать об этом числе, как о степени агрессивности нашего In в нашем FPS-независимом сглаживании (аналог InCubic, InQuad и т.д.). Формула продолжит работать.

Но для полного счастья нам не хватает настройки времени, за которое дракончик будет визуально догонять персонажа из любой точки. Конечно, полностью он догнать не может, но нам хватит преодоления, скажем, 98% пути:

1 - 1 / base^t = 0,98
base^t = 1 / (1 - 0,98)
t = log(1 / (1 - 0,98), base)

Ну вот и всё. Теперь мы можем инициализировать этими параметрами нашу бесконечную резинку-пружинку, после чего ей можно будет скармливать deltaTime, а в ответ получать LerpK. Таким образом получилось простое FPS-независимое сглаживание для всего, что можно лерпать. Финальный класс можно видеть на скриншоте.

По-моему, симпатично получилось. А вы что думаете?
Поправочка!
#код
Ох, что-то я написал этот пост и тут же понял, что лишнего наворотил. Движение, при котором мы за равные промежутки проходим половину, затем половину от остатка и так далее — это фактически тоже самое движение, когда мы за равные промежутки проходим треть, затем треть от остатка и так далее. Это просто свойство перевёрнутой экспоненты самой по себе. И неважно какое основание. А чтобы управлять агрессивностью In-прыжка, достаточно слегка менять threshold-порог. Чем выше выставить процент порога, тем агрессивнее будет прыжок в начале.

Правду говорят, что если хочешь в чём-то разобраться сам, то расскажи это другому. Вот и здесь так получилось.

public struct EndlessSpring
{
private float TimeScale;

// time is amount of time that needeed to pass
// threshold-share of the distance (never pass 100%)
public EndlessSpring(float time, float threashold = 0.98f)
{
var expectedValue = 1 / (1 - threashold);
this.TimeScale = Mathf.Log(expectedValue, 2) / time;
}

public float GetLerpK(float deltaTime)
{
return 1 - 1 / Mathf.Pow(2, deltaTime * TimeScale);
}
}
Вы точно понимаете корутины?
#код
Много-много лет назад, когда я первый раз пришёл на настоящую работу программировать за настоящие деньги, меня научили корутинам. Ну, не то, чтобы прям научили. Просто там был проект с корутинами, и мне волей-неволей пришлось вникнуть, что это за yield такой непонятный. Новый мир, зазиявший передо мной, в корне перевернул моё тогдашнее представление о том, как можно писать геймплейный код. Я и до сих пор это воспринимаю, как одну из важнейших ментальных ступенек для программиста.

Речь сейчас идёт о тех диких временах, когда воевали с луками и стрелами против боевых машин самописными движками на плюсах. Я тогда успел немного походить из конторы в контору, но везде обнаруживал несчастных грустных людей, не вкусивших плодов корутин. Многие и слов таких не знали. И я нёс знания в простой народ. И жизнь людская преображалась на этих проектах.

Но эти времена давно прошли. Теперь каждый знает, что такое StartCoroutine() в Unity. Все к ним привыкли, и никого ими не удивишь; но я заметил, что самое крутое назначение корутин все ещё ускользает от многих в наше время.

Новички считают, что это просто удобный способ делать задержки. Каждый второй, делающий тестовое про самолётик в DarkCrystalGames, делал таймаут выстрелам через запуск корутины и WaitForSeconds(). Мой внутренний эстет при чтении такого кода всегда бьёт себя по лицу, но это вкусовщина. А проблема в том, что эти люди, как правило, только таким использованием и ограничиваются. Ничего более интересного, чем задержки, на корутинах и не пишут. Какое неуважение.

Но этого поста не было бы, если бы проблемы с пониманием возникали только у новичков. Опытные сеньористые ветераны порой думают о корутинах, как о неком легковесном способе организации мультипоточки.

Нет, нет и ещё раз нет! Мультипоточка тут вообще ни при чём!

Недопонимание происходит из-за того, что к одной и той же вещи можно прийти с двух сторон. Можно с одной стороны постепенно облегчать мультизадачность и повышать безопасность и прийти от потоков к так называемым Green Threads. А можно вообще идти с другой стороны, пытаясь в однопоточном приложении улучшить читабельность колбеков и стейт-машин, и прийти внезапно к тому же самому. В этом случае результат скорее назовут корутинами. Разница между получившимися вещами с точки зрения функционала будет весьма условна, но отличается мотивация и назначение. Если вы познакомились с корутинами не с той стороны, то эта статья для вас.