This article was originally written for Darren Hebden's RLNews
When creating a roguelike, more often than not you are presented with
the challenging task of devising an interesting world in which the
game takes place. And, naturally, most beings in most worlds, even if
they are immortal, need to keep the flow of time for personal,
governmental (taxes) and religious (feasts) purposes. This article
deals with issues commonly encountered upon adding a time dimension to
your game.
Most assuredly, you are not bound to design your time according to the
traditional sense, with seconds, minutes and hours. But for the
simplicity of the discussion, we will assume that you operate in a
framework that consists of time units that everyone knows - seconds
that make minutes, minutes that make hours, hours that make days and
days that make years. We'll also assume that typical Earth standards
are in force, with the exception that a year is always 365 days
(not 365.24624 as in reality).
After determining how are your time units structured, it is important
to decide what within the game will you use to represent it. The
simplest approach is to assign a different variable to each of the
various units; a signed char (byte) to seconds, minutes and hours, and
an unsigned int to days. This simple solution has, however, a
significant drawback: you waste storage space. It's certainly not a
big waste, but helps to forget about memory issues and develop bad
programming habits.
Therefore, a more efficient way to approach our problem will be the
introduction of *scalar time*. Scalar time is an uniform figure; it
represents the amount of basic time units - in our case seconds - that
have elapsed from a specific point in time. This point can be purely
arbitrary, but should be antecedent to the start of your adventure, so
as to avoid 'negative time', if possible. The variable type for scalar
time should be a 32-bit int (it is a good idea to make it a signed
int, to avoid problems with negative time). 32 bits are, surprisingly
enough, sufficient to cover a period exceeding 68 Earth years,
probably more than the longest lifetime of a typical adventurer in
danger-ridden dungeons. If you still think it's not enough, you can
make the variable unsigned and double the available period. You can
even increase the variable length to 64 bits, and it will cover (gasp)
some 292 trillion years. Now THAT should be enough even if your game
starts at Big Bang...
Basic Timekeeping
We'll start the coding part by defining some basic quantities:
#define MINUTE 60
#define HOUR (60 * MINUTE)
#define DAY (24 * HOUR)
#define YEAR (365 * DAY)
These will be helpful in adding or substracting larger amounts of time
to/from our scalar clock.
Next, we must write a function that will do for us the dirty work of
calculating actual time representation. Programmers call that
'breaking down' of the scalar value. Assuming the time units are
always of equal length, we can achieve the task very easily without
resorting to floating point arithmetics.
Here's a function I used in GSNband (slightly changed)
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 is my 32-bit time variable; SEC, MINUTE etc. are #defined modes
the function is called with; they may be any value you want).
In short, to calculate the remainder corresponding to any unit, we first
divide the scalar time by the amount of base units correspoding to that
unit (note that in the first case sc_time is equivalent to sc_time / 1),
and then take the modulus of it and the amount of units it takes to fill
the next higher unit. The last case does not include the modulus, since
there is no higher unit that a year, and we immediately know how many
years have passed on our clock.
This is a rather unoptimised function that will not work well in a
compiling environement that generates lots of extra code for each
function call. It that case, it is better to rewrite the function header
using pointers, like this:
int break_scalar_time (int *sec, int *min, int *hour, int *day, int *year)
And then call it after having initialized the variables:
int sec, min, hour, day, year;
break_scalar_time (&sec, &min, &hour, &day, &year);
Yet another solution would be a structure to keep the information
returned, or even better an union (since all the fields are of equal
length).
Months and Weeks
The next thing you want is a calendar. Unless, of course, the inhabitants
of your world customarily refer to the likes of 'day 164 of the year'.
For a calendar, you'll need months and perhaps weeks (if each months
contains an equal number of weeks).
If you wanted to emulate the Gregorian calendar (without a leap day),
your best bet would be to make two arrays like this:
char *month_names[12] =
{
"January", "February" ...
}
int month_table[12] = /* No. of first day of each month in a year (0-364) */
{
0, 31, 59 ...
};
(we're counting from 0 to be in common with the values break_scalar_time()
returns)
And then we can calculate the month and day of month like this (with the result
in 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;
/* Take account of annoying English */
p = "th";
j = day % 10;
if ((day / 10) == 1) /* nothing */;
else if (j == 1) p = "st";
else if (j == 2) p = "nd";
else if (j == 3) p = "rd";
(void)sprintf (out_str, "%d%s day of %s", day, p, month_names[i]);
}
}
What To Do With Time?
After we've written our timekeeping routines, we must consider how
much time will elapse while the player is doing something, IOW, how
fast will the game time progress. If you game employs large wilderness
areas, you will probably want to hasten time flow while the player is
there. OTOH, in a dungeon settings time should pass more slowly.
First, define the area of the basic grid of your dungeon, if you
haven't already. For example, in Angband this is 10 feet (somewhat
over three metres). How fast can you expect the player to cover a
distance of ten feet, moving at a somewhat cautious pace? What about
combat - how long does a blow take? (I based time elapsed between blows
on dexterity and weapon weight.) Do monsters act in a separate time
piece or does time not move while they have a turn? How fast do you use
magical items? Change levels? Eat something? (think about weight/volume)
Etc. etc. Providing for all actions the player may undertake is vital
if you want the time flow to correspond, at least vaguely, to what
you've been doing.
Some other ideas employing the concept of (recurring) time:
- Shops that are open 7/11 or on certain weekdays
- NPCs that can be had only at specific hours (otherwise they sleep,
work, wander away etc.)
- Quests given may be restricted to a deadline
- Artifacts that activate a set number of times per day (or, if that's
too sparse, per hour). Same goes for recharging time.
- Your character may have to spend a few hours, or even days, to learn
new spells at a guild.
- Article prices fluctuate over time (probably in a random way, unless
you have an economy model handy. :)
- Delayed magical effects, like a retarded fireball or a time
bomb/grenade (the concept of delayed function calls probably merits
its own article)
Not to mention time travel...
Any questions, corrections or notes? Mail naskrent@artemida.amu.edu.pl
|