Отслеживание времени

Материал из Клуб любителей рогаликов
Перейти к: навигация, поиск

Данная статья была написана для RLNews Darren'а Hebden'а

Введение

При создании roguelike-игры, вам нередко преподносится вызывающая задача разработки интересного мира, в котором происходит игра. И, естественно, большинство существ в большинстве миров, даже если бы они были бессмертными, должны контролировать ход времени для личных, правительственных (налоги) и религиозных (праздники) целей. Эта статья имеет дело с вопросами, обычно возникающими при добавлении в вашу игру временного измерения.

Несомненно, что вы не ограничены в разработке вашего времени необходимостью соответствовать традиционному значению с секундами, минутами и часами. Если бы не простота дискуссии, мы допустим, что вы действуете в рамках, которые состоят из общеизвестных единиц времени - секунды, которые составляют минуты, минуты, которые составляют часы, часы, которые составляют дни и дни, которые составляют годы. Мы также допустим, что будут в силе типичные земные стандарты, с исключением, что год всегда состоит из 365 дней (а не из 365.24624, как в действительности).

После определения как структурированы ваши единицы времени, важно решить, что вы будете использовать в игре для их представления. Самый простой метод - присвоить разные переменные каждой различной единице; signed char (байт) для секунд, минут и часов и unsigned int для дней. Это простое решение имеет, тем не менее, существенный недостаток: вы тратите место для хранения. Это, конечно, не большая потеря, но помогает забыть о вопросах памяти и развивает привычки плохого программирования.

Следовательно, более эффективным путём решения нашей проблемы будет введение *скалярного времени*. Скалярное время является равномерной величиной; оно представляет из себя сумму базовых единиц времени - в нашем случае секунд - которые прошли со специфического момента времени. Эта точка может быть чисто произвольной, но должна быть выбрана раньше начала ваших приключении так, чтобы по возможности избежать 'отрицательного времени'. Тип переменной для скалярного времени должен быть 32-битным int (хорошая идея сделать её signed int, чтобы избежать проблем с отрицательным временем). Удивительно, но 32 бита достаточно, чтобы покрыть период, превышающий 68 земных лет, вероятно более, чем даже самая длинная жизнь типичного приключенца в опасных для странствий подземельях. Если вы всё ещё думаете что этого не достаточно, вы можете сделать переменную для доступного периода unsigned и double. Вы можете даже увеличить длину переменной до 64 бит, и это покроет (подавитесь) около 292 триллионов лет. Теперь, ЭТОГО должно быть достаточно, даже если бы ваша игра начиналась с Большого Взрыва...

Основное Хронометрирование

Мы начнём программную часть с определения нескольких базовых количеств:

#define MINUTE 60
#define HOUR (60 * MINUTE)
#define DAY (24 * HOUR)
#define YEAR (365 * DAY)

Это будет полезно при добавлении или вычитании больших промежутков времени к/из наших скалярных часов.

Затем, мы должны написать функцию, которая сделает для нас грязную работу по вычислению фактического представления времени. Программисты называют это 'разбором' скалярной величины. Предположив, что единицы времени всегда равной длины, мы сможем решить эту задачу очень легко, не прибегая к арифметике с плавающей точкой.

Вот функция, которую я использовал в GSNband (немного изменённая)

int break_scalar_time (int what)
{
switch (what)
{
case SEC:
    return (sc_time % MINUTE);
case MINUTE:
    return ((sc_time / MINUTE) % 60);
case HOUR:
    return ((sc_time / HOUR) % 24);
case DAY:
    return ((sc_time / DAY) % 365);
case YEAR:
    return (sc_time / YEAR);
default:
return (0);
}
}   

(sc_time - моя 32-bit переменная времени; SEC, MINUTE и др. - #defined способы вызова функции; они могут иметь любое значение, которое вы пожелаете).

Короче говоря, чтобы вычислить остаток, соответствующий любой единице, мы сначала разделим скалярное время на количество базовых единиц, соответствующее данной единице (заметьте, что в первом случае sc_time - эквивалент sc_time / 1), затем возьмём его модуль и количество единиц, необходимое чтобы заполнить следующую более крупную единицу. В последнем случае модуль не включается, поскольку нет единицы больше года, и мы немедленно узнаем, сколько лет прошло по нашим часам.

Это довольно неоптимальная функция, которая сгенерирует массу дополнительного кода для каждого вызова функции. В этом случае лучше перезаписать заголовок функции с использованием указателей, как здесь:

int break_scalar_time (int *sec, int *min, int *hour, int *day, int *year)

А затем вызываем его после того как инициализировали переменные:

int sec, min, hour, day, year;
break_scalar_time (&sec, &min, &hour, &day, &year);

Ещё одним решением может быть структура для хранения возвращаемой информации, или лучше даже объединение (union) (поскольку все области равной длины).

Месяцы и Недели

Следующая вещь, которая вам нужна - календарь. Если, конечно, у жителей вашего мира не принято ссылаться, скажем, на '164 день года'. Для календаря, вам нужны месяцы и возможно недели (если каждый месяц содержит равное количество недель).

Если вы хотите эмулировать Григорианский календарь (без дня перехода), наилучшим ходом с вашей стороны будет сделать два массива, к примеру как здесь:

char *month_names[12] =
{
   "January", "February" ... 
}

/* Номер первого дня для каждого месяца в году (0-364) */
int month_table[12] = 
{  
   0, 31, 59 ...
};

(мы считаем от 0 до конца, возвращая значение break_scalar_time())

А потом мы вычисляем месяц и день месяца как показано здесь (с результатом в out_str):

int day, month, i, j;
int year_day = break_scalar_time(DAY);
char *p;
char *out_str;

for (i=0; i <= 12; i++)
{
if (year_day >= month_table[i])
   {
      month = i;
      day = year_day - month_table[i] + 1;

      /* Учтем специфику English */
      p = "th";
      j = day % 10;
      if ((day / 10) == 1) /* ничего */;
      else if (j == 1) p = "st";
      else if (j == 2) p = "nd";
      else if (j == 3) p = "rd";

      (void)sprintf (out_str, "%d%s день от %s", day, p, month_names[i]);
    }
}

Что делать со временем?

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

Сначала, определите область основной сетки вашего подземелья, если у вас её ещё нет. Например, в Angband это 10 футов (немного более трёх метров). Как быстро, по вашим ожиданиям, игрок покроет расстояние десять футов, перемещаясь в отчасти осторожном темпе? Как насчёт боя - сколько времени занимает удар? (Я основываю время между ударами в зависимости от ловкости и веса оружия.) Действуют ли монстры в продолжение определённого промежутка времени или время на протяжении их хода не движется? Как быстро вы используете волшебные предметы? Меняете уровни? Едите что-нибудь? (подумайте о весе/объеме) И т.п. и т.п. Предусмотреть все действия, которые может предпринять игрок, жизненно важно если вы хотите, чтобы течение времени, хотя бы смутно, соответствовало тому, что вы делаете.

Некоторые другие идеи, применяющие понятие времени (возвращаясь):

  • Магазины, которые - открытые 7/11 (ежедневно, 11 часов в день) или в определенные дни недели.
  • NPC-персонажи которых можно встретить только в специфичное время (в противном случае они спят, работают, где-нибудь бродят и т.п.).
  • Полученный квест может быть ограничен по сроку выполнения.
  • Артефакты, которые активируются несколько раз в день (или, если это слишком редко, за час). Тоже самое с временем перезаряжания.
  • Ваш персонаж должно затратить несколько часов, или даже дней, чтобы выучить новые заклинания в гильдии.
  • Расценки со временем меняются (вероятно, случайным образом если у вас нет под рукой экономической модели. :)
  • Задержка магических эффектов, подобно запаздывающей шаровой молнии или бомбе/гранате замедленного действия (понятие функции задержки с перечислением его возможных достоинств приводится в отдельной статье).

Не говоря уже о путешествиях во времени...

Есть вопросы, исправления или замечания? Пишите naskrent@artemida.amu.edu.pl



Автор: Gwidon S. Naskrent.
Источник: Time Tracking.
Перевел: Дмитрий О. Бужинский [Bu], 24.05.2005.