Текстуры острова (2048x2048) и воды (1024x1024) генерируются как функция от высоты, шума (от координат) и затенения (от норамли). Затенение чисто ламбертовское (ambient + diffuse), самозатенения нет. Солнце достаточно высоко, так что это, вроде, не сильно заметно.
Чтобы текстура острова не выглядела такой размытой, поверх неё накладывается бесшовная повторяющаяся детализированная шумовая текстура (256x256), без оглядки на высоту.
Шрифт - нарисованный ещё в 10-м, что-ли, классе матричный 6x8 шрифт, его точки используются как заряды для потенциального поля. Потенциал задаёт как прозрачность, так и высоту, которая используется для затенения букв (без этого они на мой взгляд, смотрелись хуже).
В шрифте 96 знаков (32..127), в изначальной версии их больше (256, кодовая страница 866).
Есть табличка, где для каждой буквы указана на глаз подобранная ширина (шрифт не моноширинный; хотя изначально таковым был).
Шрифт всё-равно так себе, а на больших размерах смотрится и ещё более невнятно.
Этим во многом и обусловлен размер надписей в игре, которые многим, видимо, пришлись не по вкусу.
Мешей (кроме острова и вспомогательных (квады для UI и т. д.)) в игре ровно 2: башня и обелиск. Оба они не отличаются замысловатостью.
Остальные объекты в игре рисуются билбордами.
Прозрачка сортируется (по расстоянию до центра треугольника) и рисуется за 1 вызов (так же рисуется UI; для 2D и 3D сортировка чуть разная).
Атлас спрайтов:
+ Показать
Вопреки расширению изображения - это png.
Манера рисовать деревья 2-мя скрещенными билбордами меня классически не радует. Поэтому у меня билбордов 3 (трёхгранной призмой).
Почти все эффекты реализуются одним и тем же спрайтом (resources::SprNames::spell), представляющим собой круг с анимированным шумом Перлина:
+ указатель позиции
+ сияние вокруг башни, и анимация Ascension (сильно вытянутый спрайт)
+ свет над обелисками и кораблями
+ создание/уничтожение здания/существа
+ частицы маны над деревьями
+ молния
Корабль собирается из нескольких повторно использованных спрайтов.
Звук:
Для звука используется самописный движок поверх waveOut (с DirectSound разбираться не стал).
Движок архитектурно сильно вдохновлён OpenAL, но заметно топорнее.
Есть 3D позиционирование, но только громкостью, но не задержкой.
Музыка. Я пробовал найти композитора, но безуспешно. Возможно, композиторы тему конкурса не читают, а стучать людям в личку мне не хватило наглости. Ну и можно было, наверно, создать тему в разделе "Собираю команду", но тоже не стал.
Хотелось чего-то очень фонового для внутриигровой музыки и чуть более бодрого, с темами исследования, загадочности, магии - для меню.
Ориентиром для меня был OST к Эадору за авторством Кот (скорее оригинальный, чем "Возвращение в Эадор").
Намечалась перспектива делать то же, что и в Gates of Canta Eneccainen - перерыть несколько мегабайт MIDI-шек классической музыки (сама музыка - public domain за давностью лет, MIDI-шки, вроде, тоже свободны для использования). Это заняло тогда довольно много времени, и конечный результат меня не очень радовал (музыка, на мой вкус, лишь весьма условно подходит к Gates of Canta Eneccainen).
И тут мне повезло. Вспомнилось, что в связи с Эадором часто натыкался на ключевое слово "celtic". Запрос в google "celtic midi" сразу же выдал на удивление много пригодных к употреблению мелодий. Причём опять же - классические ирландские, например, мотивы, в public domain.
Выбрал, почти сразу, King of the Fairies с http://sacred-texts.com/neu/celt/music/index.htm .
Генерация музыки взята из синтезатора; предпросчитанная таблица (одна на инструмент), поверх неё ADSR.
Источник - MIDI, заэмбеженый в код, с препроцессингом, чтобы лучше сжимался. Препроцессинг не особо навороченный (~ дельта компрессия), ну так и MIDI-шка маленькая (9 КБ).
Исходные инструменты я заменил, чтобы результат нормально звучал в моём синтезаторе:
Clavinet -> Orchestral Harp
Pan Flute -> Ocarina
Cello -> Violin
Музыка формально стерео, но оба канала идентичны, хотя настоящее стерео, наверное, пошло бы ей на пользу.
Звук океана - розовый шум, с осциллирующей (по-разному для левого и правого каналов) амплитудой.
Остальные звуки - незатейливые и, на мой вкус, довольно невнятные, как не странно, народу вроде понравились.
Кстати, чем?
Игровая механика:
Игровой мир обновляется 50 раз в секунду (фиксированный шаг времени), отрисовка идёт с той скоростью, с которой получится.
Абстрактный игровой мир (структура World) самодостаточен, и понятия не имеет, что его кто-то рисует. Функция обновления мира принимает ввод от игроков (человека и ИИ, могла бы и сеть, если бы была сетевая игра) и делает один шаг игровой логики, в качестве побочного эффекта производя события (events), для вещей, которые плохо ложатся на концепцию "чистая функция от состояния игрового мира" - звуков и эффектов заклинаний и т. п..
Рендер принимает текущее состояние игрового мира и накопившиеся события.
Ввод от игроков задаётся структурой:
struct Input
{
si32 cast;
si32 learn;
vec<2,float> pos;
};
-1 означает, что на данном шаге ничего не скастовано/выучено. pos указывает цель каста.
Следствие: за один шаг обновления можно скастовать не более одного заклинания.
Функция высоты довольно затратная (10 вызовов одной только stb_perlin_noise3), поэтому в начале игры заполняется сетка 513x513, и дальше её билинейная интерполяция используется в качестве аппроксимации функции высоты.
Кстати, для представления о масштабе:
+ Остров имеет размер 50x50 км
+ Высота башен 700 м
+ Высота дерева 250 м
+ Минимальная высота камеры - 2.5 км
+ Размах крыльев дракона 125 м
Координаты точки под указателем мыши определяются raymarching'ом на CPU.
Движок теоретически держит 8k зданий 16k существ. При полной застройке острова количество зданий приближается к этому пределу, но количество существ, типично, намного ниже, и скорость работы при 16k существ не тестировалась (карта, вплотную застроенная жучарнями, по идее, может упереться в этот предел).
Для ускорения поиска соседей введена сетка 32x32 (совпадение, почти ровно квадратная миля), в которую помещены индексы зданий и существ, находящихся в соответствующих клетках (крайние клетки неограниченно простираются вдаль). Ничего более умного (quad-tree и т. д.) для ускорения этих запросов не предпринималось.
Обновления зданий дешёвые (раз в столько-то тактов добавить маны; раз в столько-то тактов сгенерировать жука; обелиск вообще стоит столбом и не думает).
Обновления существ дороже.
Каждый шаг существо пытается идти в выбранном направлении. Если высота позволяет (для каждого существа есть допустимые высоты; проверка производит 1 вызов аппроксимированной функции высоты) - ход происходит, иначе существо остаётся на месте.
Раз в некоторое количество ходов (64..256 в зависимости от типа существа) - выбирается новое направление: либо случайное, либо на ближайшее вражеское здание/существо.
Раз в несколько ходов (32, кроме Walker'ов, которые не атакуют) существо проверяет наличие цели в радиусе атаки и, при наличии таковой,- атакует.
С точки зрения ввода игрок и ИИ в одинаковых условиях - передают функции обновления одну и ту же структурку Input. Но сама функция обновления различает их в том, что выдаёт ИИ больше или меньше маны в зависимости от Fairness (другие различия, вроде бы, отсутствуют).
Секретную функцию подсчёта маны я тогда ещё не вывел, и значения подбирал явно.
GUI существует весьма условным образом: есть код игровой логики, который обрабатывает нажатия мыши, есть код рендера, который рисует менюшки. Друг о друге они не знают, структур, описывающих элементы GUI нет. Координаты захардкожены, в 2-х местах.
Искусственный интеллект.
AI туп, как дерево. Вся логика его поведения занимает чуть более страницы кода.
Для изучения используется таблица "настало время учить спелл XYZ", без оглядки на развитие, ману, противников (но с оглядкой на Fairness).
Если учить нечего, то AI пытается скастовать спелл в случайную точку (несколько проб, если точка не в зоне влияния). Если он предсказывает, что каст удастся (удалось ли на самом деле не смотрит; но предсказание вроде очень хорошее), то рандомом выбирает следующее заклинание (вероятности определяются весами, хранящимися в табличке).
Балансировалось тупо: запускалось 5 AI на скорости x10 (в ходе чего выяснилось, что да, движок такую скорость держит) и подгонялись коэффициенты так, чтобы он не загонял себя в откровенно дурацкие состояния.