понедельник, 23 июня 2008 г.

Программируем музыку

Статей и уроков я никогда не писал и не претендую. Все что ниже — собранные мною грабли.

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

Поэтому, я решил сделать псевдо-генерацию саундтрека — взять несколько маленьких кусочков, и во время игры в реальном времени собирать из них мелодию. Просто и хорошо =)


Ножницы
Что ж, попробуем разделить песню на маленькие составляющие и управлять ими как захочется. Когда-то я накидал в FruityLoops простенький трек, выглядело это примерно так:


Белые прямоугольнички — это и есть звуки, паттерны, каждый длиной 8, 4, 2 и 1 условных квадратиков (бит). Сохраняем каждый паттерн как отдельный файлик — и трек разрезан, остаётся только склеить его обратно уже в игре.


На будущее: у меня паттерн длиной 8 получился равен 2,824 секундам (зависит от темпа!!) и был выбран как основной (самый большой).


Клей
Так. Получилось несколько наборов звуков: басы, пианинка и гитара. Разумеется, одновременно проигрывать, например, 2 гитары смысла нет — получится каша, поэтому рождается страшное понятие инструмента.

В моём случае, это просто массив из N целочисленных, где N — число различных паттернов для инструмента, а числа в массиве означают сколько ещё раз надо проигрывать соответствующий звук (наверное, ничего непонятно =)

Вот примерный кусочек кода для проигрывания одного инструмента:
procedure SoundUpdate(count : single);
begin
timer := timer + count;

// текущий паттерн в очередной раз закончился
if timer > patterns[current].length then
begin
Play(patterns[current]); // проигрываем ещё раз
patterns[current] := patterns[current] - 1; // и уменьшаем счетчик

// если счётчик достиг нуля, выбираем случайным образом новый
// текущий паттерн, и назначаем ему случайное число повторений
if patterns[current] <= 0 then
begin
current := random(high(patterns_beat) + 1);
patterns[current] := (1 + random(2));
end;

timer := 0;
end;
end;

Проблемы не-ООП
Это было очень легко. Но, неэкономно — уже для трёх инструментов получалось слишком много кода. Не страшно, конечно, но настоящие проблемы начались когда я попробовал добавить паттерн другой длины. Например, я хочу, чтобы перед тем как зазвучит гитара (длиной 8), проигрался паттерн с гитарным вступлением (длиной 4, ещё и с отступом на 4 у.е.)

Я попробовал завести несколько таймеров, чтобы отсчитывать нужные моменты для всех возможных длин (напомню: 8, 4, 2 и 1), но их получилось жутковатое количество — ведь паттерн длиной 4 может прозвучать как в первой, так и второй половине паттерна длины 8 (и неплохо бы это дело контролировать). Итого, получится аж 15 таймеров, управляемых "вручную" — неудобно. Значит, пишем класс!)


Класс
Это был самый сложный этап. Получилось так:
const
base = 2824; // "базовая" длина (в миллисекундах)

TSoundPatterns = class
patterns : array of integer; // счетчики воспроизведения
current : byte; // текущий паттерн
start : single; // отступ
timer : single; // таймер, ясное дело
played : boolean; // флаг "уже проигранности" паттерна
public
procedure Play; // момент воспроизведения
procedure Next; virtual; // выбор следующего паттерна
procedure Update(count : single); // обновление
end;


procedure TSoundPatterns.Play;
begin
// нулевой номер паттерна означает тишину (не воспроизводится)
if current <> 0 then
sound.Play(current); // псевдокод! просто отдаем звуковому движку
// команду проигрывать звук, соответствующий
// нашему инструменту и текущему паттерну

patterns[current] := patterns[current] - 1;
if patterns[current] <= 0 then
Next; // генерируем дальнейшую судьбу нашей мелодии
end;

procedure TSoundPatterns.Update(count : single);
begin
timer := timer + count;
if (timer > start) and (not played) then // время проиграть звук
begin
Play;
played := true;
end;

if timer >= base then // следущая интерация, все по новой
begin
timer := 0;
played := false;
end;
end;
Теперь, для каждого инструмента (класса-потомка) нужно переопределить процедуру Next — и у каждого будет своя судьба =)
procedure TSPBeat.Next;
begin
current := random(high(patterns)) + 1; // выбор номера следующего паттерна
patterns[current] := (1 + random(2)) * 2; // и назначение ему числа повторений
end;

procedure TSPGuitar.Next;
var
i : integer;
begin
i := current;
current := random(high(patterns)) + 1; // выбор номера следующего паттерна
patterns[current] := (2 + random(2)) * 2; // и назначение ему числа повторений

if (i = 0) and (current <> 0) then // если была тишина (нулевой паттерн), а
begin // дальше зазвучит гитара (ненулевой),
sp_guitar_start.current := 1; // то проигрываем вступление
sp_guitar_start.patterns[sp_guitar_start.current] := 1; // один раз
end;
end;
Довольно просто, но на написание (осознание) потратилось порядочно времени.


Повторения
Всё хорошо, но паттерн больше одного раза за промежуток времени base не произведётся. А хотелось — например мышеклик (длиной 1) должен звучать сразу после щелчка мыши, а не ждать секунду-две для синхронизации. Для этого немножко колдуем:

// вписать вместо флага played
repeat_next : integer; // счетчик повторений
repeat_step : integer; // шаг повторений

// вычисление шага, _repeats — необходимое число повторений
repeat_step := 8 + _start - _repeats + 1;

procedure TSoundPatterns.Update(count : single);
begin
timer := timer + count;
if timer > start + repeat_next / 8 * temp + 4 then
begin
Event;
repeat_next := repeat_next + repeat_step;
end;

if timer >= temp then
begin
timer := 0;
repeat_next := 0;
end;
end;
Теперь, для того чтобы воспроизвести звук один раз прямо сейчас (и с синхронизацией по музыке! =) достаточно задать _repeats = 8, посчитать repeats_step и написать процедуру из двух строчек. Вот такая она для мышеклика:
procedure Click;
begin
sp_click.current := 1;
sp_click.patterns[sp_click.current] := 1;
end;

Заключение
Во-первых, основное время (base) можно сделать своим для каждого звука (технически это просто), но дело в том, что обычно в треке паттерны пропорциональны друг другу, и с единственным base управляться немного проще.

Во-вторых, нужно не забыть "почистить" звуки, чтобы в конце не было щелчков, а зацикленность была плавной и незаметной, но тут уже дело слуха (кстати, тестировать музыку очень сложно).

На самом деле здесь всё очень просто, на осмысление и программирование ушло 2-3 часа (раза в полтора меньше чем на этот пост =), так что не бойтесь, результат того стоит:
— Твоя музыка?! о_О
— Генерируемая?!! О_О
Удачи!) Надеюсь, в целом понятно. Вопросы? ↓

воскресенье, 22 июня 2008 г.

07. Фруктовые паттерны

Бета-версия (1 Мб). Готова ровно половина квестов, и ни одной концовки. Зато. Что-то такое в ней есть. А вы как считаете??


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

Осталось сделать всякие шумы и будет вообще замечательно (грозы! дождя побольше! ветра мне!! =)

И ещё, я очень старался подобрать настроение. Получилось довольно веселое. Может это и к лучшему. Я так вижу (слышу) в конце концов! =)

пятница, 20 июня 2008 г.

06. Quite simple

Делать просто - сложно.

Анимацию своими силами сделать почти невозможно. Систему таймеров - долго. Движок сообщений - не хочется. Физику цепей - трудно. Логику связанных объектов - неприятно.

Поэтому всё летит в корзину, а я учусь додумываться до простых вещей. Получается так себе.



Бета на волоске.

среда, 18 июня 2008 г.

06. Монохромный хлорофилл

Я очень долго останавливал себя, не получилось. Решил всё-таки "не ссать" и сделать честную растительность из частичек. Нарисовать травяной эллипс совсем-совсем несложно:


Дальше - веселее. Геометрию я учил плохо и давно, и найти нормали я не смог, а то что нашел посчитал слишком муторным. Поэтому рассчет направления травинок оказался, мягко говоря, кривоватым:


Осталось немножко - развернуть травинки как надо, расставить их в нужном порядке, нарисовать текстуры и добиться красоты:



Получился откровенный шлак. Мусор. Я потратил часа два на рисование травинок (вариантов десять), на подбор их размеров и углов, а получилась невнятная каша. Откат.

воскресенье, 15 июня 2008 г.

06. Трансплантация

Придумал и реализовал два с половиной квеста. Стало гораздо веселее, игра подошла к своей самой важной черте - она готова ожить, готова стать Игрой. Это очень хрупкое место, постараюсь не прозевать.

суббота, 14 июня 2008 г.

06. Read Me!

Как обратить внимание игрока на подписи к объектам? Правильно - рисовать эти подписи там, куда смотрит игрок. Другими словами, сделать всеми любимые "облачка". Пробуем:


Жестоко!) Даже если учесть, что на экране будут одна-две подсказки, оставлять такое нельзя - текст будет невозможно прочитать (особенно мой корявый почерк =). Значит, рисуем подложки:


Так, лучше. Остается только растащить подсказки, нарисовать подложку и скрыть лишнее. Просто, свежо и симпатично:


Первая видимая проблема - расположение подсказок на другом языке. Решение - тот же самый ini-файл, дописывать координаты прямо рядом с текстом.

Вторая проблема - что делать с длинными и многострочными подсказками? Скролл или многоэтажный текст - плохо. Не делать длинных подсказок? Думаю, да.

Первый хороший бонус - автоматически решен вопрос вывода "событийных" сообщений. Которых, может, и не будет, но всё же.

И еще тележка приятностей: не надо косить глаза влево-вверх, игроку понятнее какой именно предмет выбран, на экране стало меньше резких движений и мусора.

05. От фонаря

Задача: сделать красивый прожектор малыми силами.

Нарисовать фонарик легко, остаётся только заставить его светить. Сначала простенький контур для наглядности:


Дальше - сам луч. Рисовать его текстурой не хочется - угол "на лету" не поменяешь, большую текстуру таскать придется да и просто возня лишняя. Спасение - честная модель луча (суть обычный треугольник =)


Ну и вишенка: немножко пошуметь, выставить смешивание на умножение и подогнать углы и прозрачность.

Красота:

Результат очень радует. Потрачено где-то полчаса, а плюшка та-а-акая вкусная =)

пятница, 13 июня 2008 г.

05. Поражение интеллекта

Самый неожиданный подводный камень: думать сложно (sic!!). С грехом-пополам расписал первый квест. Один квест!! Самый простой. А до беты неделя, кошмар!.. (дальше, якобы, паника)

Но есть и радость: отыскался со-автор и переводчик на великобританский - Самурай Джек. Поразительная штука, это ОУЕО, со столькими людьми знакомлюсь, офигеть.

p.s. Адекватность и рационализм ломаются в крошки случайно и незаметно. Одна система частиц способна подарить часы медитации.

пятница, 6 июня 2008 г.

04. Рукописный кислород

Якобы альфа (300 кб). И только один готовый полуквест (про цветы). Надо думать, думать, думать, думать...

04. Охота за пикселями

Размеры текстур, повторюсь, равны степеням двойки. Объекты в такое вписываются с трудом и скрипом:


Дерево - вообще клинический случай, занимает пол неба. Управление было жутко неудобным, курсор попадал не туда, куда хотел игрок, даже при малом количестве объектов пиксель-хантинг доканывал, а без подсветки объектов игра вообще становилась машиной случайностей (проблема до конца не решена). По шестерёнке кликнуть было вообще нереально - ее перекрывало основание острова.

Я очень долго ленился и боялся делать баундинг-боксы - мол, громоздко, долго, много и неудобно. Заняло это минуты три. Ещё две минуты - раскидать сами боксы по объектам. Стало действительно удобно. Красота:


Бесплатный бонус - можно делать области попадания больше объектов и "фиктивные" области. В идеале, конечно, надо делать многоугольниками. Несложно, но, опять таки, лениво.

Сегодня будет "альфа". Понятия не имею что делать с квестами - их нет.

четверг, 5 июня 2008 г.