Table of Contents |
Extensions >
Objtime
Objective Time
Overview
“Objective Time” module. This implements a form of in-game time-keeping that advances the clock by so much per turn. By default each turn is reckoned to take one minute, but this can be changed in as fine-grained a manner as game authors wish, for example to make some actions take longer than others.
New Classes, Objects and Properties
In addition to a number of properties intended purely for internal use, this extension defines the following new classes, functions, objects and properties for use by game authors:
- Classes: TimeFuse, SenseTimeFuse.
- Objects: timeManager.
- Properties/methods on timeManager:
currentTime
,formatDate(fmt)
,setTime(\[args\])
,addInterval(interval)
. - Properties/methods added to Action:
advanceOnFailure
,timeTaken
,implicitTimeTaken
. - Properties added to GameMainDef:
gameStartTime
. - Properties/methods added to TravelConnector:
traversalTime
,traversalTimeFrom(origin)
. - Functions:
addTime(secs)
,takeTime(secs)
.
Usage
Include the objtime.t file after the library files but before your game source files.
When defining your gameMain object, you now need to define its
gameStartTime property to set the date and time of day at which your
game notionally starts. This should be set to a list of numbers in the
format \[year, month, day, hour, minute, second,
millisecond\]
. Trailing zero elements may be omitted. For example
setting gameStartTime
to
\[2014, 7, 23, 15, 30\]
would setting the
starting time of your game clock to 3.30pm on 23rd July 2014.
Alternatively the gameStartTime
may be
specified as a Date object (for details of which consult the TADS 3
System Manual), for example:
gameMain: GameMainDef
initialPlayerChar = me
gameStartTime = static new Date(2014, 6, 22, 15, 30, 0, 0)
;
This is all you actually need to do to use the extension, but it won’t be very useful if it’s simply updating the time each turn unless you do something with the information. We shall now go on to discuss the things you can do with it.
Displaying the Time
One of the most basic and obvious things you can do with the time is to
display it somewhere. To do this you can use the formatDate(fmt)
method of the dateManager
to return a string
containing the date, time, or date and time in a suitable format defined
by the fmt parameter. For example, to display the date and time at the
right-hand end of the status line you could do this:
modify statusLine
showStatusRight()
{
"<<timeManager.formatDate('%c')>>";
}
;
For a full list of possible format strings, consult the chapter on the Date intrinsic class in the TADS 3 System Manual. A few of the more convenient ones are listed below:
%r
the full 12-hour clock time, e.g. ‘3:30:00 PM’%R
the 24-hour clock time with minutes, e.g. ‘15:30’%T
the 24-hour clock time with seconds, e.g. ‘15:30:00’%X
the preferred time representation according to the locale settings; e.g. ‘15:30:00’%c
the preferred date and time stamp for the locale, e.g.’Sun Jun 22 15:30:00 2014’%D
the short date (in American format), e.g. to ‘06/22/14’%d/%m/%y
the short date (in European format), e.g. ‘22/06/14’%d-%b-%y
the short date in dd-mmm-yy format, e,g, ‘22-Jun-14’ (probably the best format to use to avoid USA/Europe ambiguity)%Y
the four-digit year number, e.g. 2014; use in place of%y
in any of the above when a four-digit year is required.%I:%M %P
the 12-hour clock time with minutes, e.g. ‘03:30 PM’%a
the abbreviated weekday name (‘Mon’)%A
the full weekday name (‘Monday’)
Note that these can be combined as needed, for example
timeManager.formatDate('%A, %d-%b-%y %I:%M
%P')
might return the string ‘Sunday, 22-Jun-14 03:30 PM’. The
formatDate()
method may, of course, be used
wherever you like in your game code, not just in the status line.
Scheduling Events — TimeFuse and SenseTimeFuse
We can use the standard Fuse class to
make something happen so many turns in the future. But what if we want
something to occur not after so many turns, but after a certain amount
of time (or indeed, at a certain time)? Since the objtime extension
allows us to keep track of time, we may sometimes find this a more
useful way to specify when something should happen than in terms of so
many turns. To that end the objtime extension defines a TimeFuse
class that works just like a Fuse
, but is
specified in terms of time rather than turns.
To create set up a TimeFuse, just create a new TimeFuse object thus:
new TimeFuse(obj, prop, timespec
)
This defines a TimeFuse that will call
obj.(prop)
, i.e. the prop method of obj, after
an timespec of timespec turns or at the time specified by timespec.
How timespec is interpreted depends on how it is specified. It can be
any of the following:
- A list in the format
\[years, months, days, hours, minutes, seconds\]
(trailing elements can be omitted if they are zero). The TimeFuse will then execute after the timespec specified by the list. - An integer, in which case the TimeFuse will execute after that number of minutes.
- A BigNumber (which can be specified by simply including a decimal
point), in which case the TimeFuse will execute after than number of
hours. For example, an
timespec
of 1.25 will cause the TimeFuse to execute 1 hour and 15 minutes after the TimeFuse is created. Note than antimespec
of 1.0 thus means one hour an whiletimespec
of 1 means one minute. - A Date (i.e. an object of the Date class), in which case the TimeFuse will execute at the Date specified.
- A single-quoted string, in which case the TimeFuse will execute at the time specified by the string. This can be either in the format ‘hh:mm’ (e.g. ‘15:34’) or ‘hh:mm:ss’ (e.g. ‘15:34:30’) to specify a time in the current day, or the format ‘yyyy:mm:dd hh:mm:ss’ (e.g. ‘2014:06:22 15:34:00’, meaning 15:34 on 22nd June 2014), or in any of the other formats accepted by the parseDate method of the Date class (for which see the SystemManual); in practice the examples just given here are probably the easiest to work with and should cover anything you’re likely to need.
Note, therefore, that to specify how far into the future (the amount of time after which) a TimeFuse should execute, the timespec parameter needs to be a number or a list of numbers, while to specify the date/time at which a TimeFuse should execute, the timespec parameter needs to be a Date object or a string.
The SenseTimeFuse works just like a SenseFuse, except that the timespec parameter works in the same way as for a TimeFuse, i.e.:
new SenseTimeFuse(obj, prop, timespec[, senseProp, senseObj]
)
Where senseProp must be one of &canSee, &canHear or &canSmell (or conceivably &canReach) and senseObj is the object that must be sensed appropriately. Note that you can just supply the senseProp parameter if you want to test whether obj can be heard or smelled, but that if you supply the senseObj parameter you have to supply the senseProp parameter as well.
Finally, note that since Fuses (including TimeFuses and SenseTimeFuses) are only checked for execution once per turn, a turn may conceivably carry the game time beyond the time at which a TimeFuse (or SenseTimeFuse) was set to execute. If that happens, it will still be executed at the end of the turn. In other words, towards the end of turn cycle, every TimeFuse (and SenseTimeFuse) that has either reached or passed the time at which it is due to execute will be executed.
Varying the Rate at which Time Passes
Used straight out of the box, the objtime extension simply advances the game time by one minute every turn. This may not be want you want, in which case there are several ways you can customize it to your requirements.
Failed Actions
Probably the simplest of these is to prevent failed actions (e.g. those that are stopped at the verify stage) from taking any time at all, since in this case nothing has actually been accomplished and the player has simply been informed why the proposed action can’t be carried out. To do this we simply need to set the Action property advanceOnFailure to nil, e.g.:
modify Action
advanceOnFailure = nil
;
Action Times
The next simplest adjustment is to change the time taken by various actions, by overriding their timeTaken property (which is specified in seconds). You could do this globally for all actions, if you wished; for example to make every action in the game take 20 seconds:
modify Action
timeTaken = 20
;
Or you may want different actions to take different amounts of time. For example, to make examining something take 15 seconds and taking something take 30 seconds you could do this:
modify Examine
timeTaken = 15
;
modify Take
timeTaken = 30
;
And so on for as many action times as you care to customize.
Adjusting Time Taken on Particular Turns
For even finer-grained control you can tweak the time taken for
particular actions with particular objects under particular
circumstances by using the addTime(secs) and takeTime(secs)
functions. The addTime(secs) function adds secs seconds to the time
taken for the turn. It can also reduce the time taken for the turn if
secs is negative, but it can never reduce it below zero. You can use
the addTime(secs)
function as many times as
you like in the course of a turn, and its effect will be cumulative.
The takeTime(secs)
function, on the other
hand, replaces
whatever the time taken for
turn would have been with secs. If
takeTime()
is used more than once in a turn,
it will be the latest one that takes effect, and if
takeTime()
is used on a turn, both the
timeTaken
property for the action and any
calls to addTime()
on the same turn will
simply be ignored.
As an alternative to calling addTime(n)
and
takeTime(n)
from within an embedded expression
you can use \<\<add n secs\>\>
and
\<\<take n secs\>\>
respectively, together
with some obvious variants.
In particular all these do the same thing:
"<<takeTime(n)>>";
"<<take n seconds>>";
"<<take n secs>>";
"<<take n second>>";
"<<take n sec>>";
And all these do the same thing as one another:
"<<addTime(n)>>";
"<<add n seconds>>";
"<<add n secs>>";
"<<add n second>>";
"<<add n sec>>";
So, for example, if unlocking a door involves shifting a particularly rusty bolt you could write:
+ oldDoor: Door 'old oak door'
...
lockability = lockableWithoutKey
isLocked = true
dobjFor(Unlock)
{
inherited();
"The bolt is covered with rust and feels incredibly stiff, but after
wrestling with it a while you finally manage to unlock the door.
<<take 150 seconds>> ";
}
Implicit Action Times
Another facet of time-keeping you may want to control is whether implicit actions should count towards the time taken for the turn in which they occur. By default they don’t since the default behaviour is simply to take one minute per turn, but you may think it perverse that the commands TAKE BALL, PUT BALL IN BOX should take two minutes in total if both are given explicitly, but only one minute in total if TAKE BOX is performed as implicit action in response to PUT BALL IN BOX. To change this behaviour you can override the implicitTimeTaken property of an Action to be the number of seconds that Action should take when performed implicitly. To have every Action take the same time when performed implicitly as it does when carried out in response to an explicit command you can simply write:
modify Action
implicitTimeTaken = timeTaken
;
Alternatively, you can override the
implicitTimeTaken
property to any number of
seconds you please, either globally or on individual actions.
Travel Times
As things stand, every travel action will take one minute, whether it’s going from the kitchen to the hall or crossing a large field to enter a wood on the far side. Moreover, if fastGoTo is enabled (either globally or in a particular region) then a GO TO command could whip the player character across the field, through the wood, up a path and over a bridge all in one go, and the turn would still only take a minute.
If you want to change this, the best way is probably to turn off the time taken by travel actions and instead make traversing TravelConnectors take time instead, which you can do by setting their traversalTime property. As a start you might do something like this:
modify TravelAction
timeTaken = 0
;
modify TravelConnector
traversalTime = 120
;
This assumes that you reckon that in your game world, travel from one
place to the next should typically take two minutes. If your map was on
a smaller scale, you’d obviously choose a smaller number, perhaps only
30 seconds. Of course, it may be that not all distances on your game map
are equal. You can deal with this by assigning different
traversalTime
values to different
TravelConnectors. Note that unless we’ve explicitly changed them in our
own code, we don’t need to override the
timeTaken
properties of the GoTo and Continue
actions (as we otherwise might), since by default these take their
values from TravelAction.timeTaken
.
That’s all very well up to a point, but what if many (if not most) of
your connections are direct from one room to another, without any other
TravelConnector object intervening? Since Room inherits from
TravelConnector, you can give a Room a
traversalTime
, but then that will be the time
taken to enter the room from any other room (unless the travel is
carried out via some intermediate TravelConnector such as a Door,
Stairway or Passage, in which case that intermediary TravelConnector’s
traversalTime
will apply instead). You
probably won’t want to create a lot of extra TravelConnector objects
just to vary the TravelTime, however, so fortunately there’s another way
to handle this situation. Instead of overriding the room’s
traversalTime
property, you can override its
traversalTimeFrom(origin) method (where origin is the other room
the traveler is coming from) to return different traversal times
depending on the origin. For example, suppose you think it should take
30 seconds to reach the meadow from the road, a minute to reach the
meadow from the river bank, and two minutes to reach it from the wood,
you could define the meadow’s
traversalTimeFrom()
method accordingly:
meadow: Room 'Meadow'
"The road lies just to the south..."
south = road
north = wood
east = riverbank
traversalTimeFrom(origin)
{
switch(origin)
{
case road:
return 30;
case riverbank:
return 60;
case wood:
return 120;
default:
return traversalTime;
}
}
;
The default in the switch statement may not be strictly necessary, but
it may be as well to put in for safety; if you were later to add another
connection to the meadow and forget to change the
traversalTimeFrom()
method, it would at least
then handle it reasonably gracefully. Perhaps a more serious problem is
that this method of specifying travel times between different rooms
could quickly become quite tedious and long-winded if there were a lot
to specify. One way to alleviate this would be to use some kind of data
structure, perhaps a LookupTable, to store the different traversal times
between pairs of rooms and then define the traversalTimeFrom() method to
make use of it, e.g.:
modify Room
traversalTab = static [
[road, meadow] -> 30,
[riverbank, meadow] -> 60,
[wood, meadow] -> 120,
...
]
traversalTimeFrom(origin)
{
local tim = traversalTab[[origin, self]];
return tim == nil ? traversalTime : tim;
}
;
One alternative would be to use a list defined on each room of the travel times from other rooms, something like:
modify Room
timeFrom = []
traversalTimeFrom(origin)
{
local idx = timeFrom.indexOf(origin);
return idx == nil ? traversalTime : timeFrom[idx + 1];
}
;
meadow: Room 'Meadow'
"The road lies just to the south..."
south = road
north = wood
east = riverbank
timeFrom = [road, 30, riverbank, 60, wood, 120]
;
This may well be easier to work with, since the relevant data is kept
with the room it relates to. Either way you would, of course, only have
to specify the exceptions, since for any route not otherwise specified
the room’s traversalTime
would be used.
Time Leaps
The final kind of time adjustment you may want to deal with is leaps in
time. These may occur in your game if it contains a flashback, for
example, or if you cut from one scene to another that takes place hours
or even days later. In principle this is simple enough to do: you just
call the setTime() method of the
timeManager
object, or else update
timeManager.currentTime
directly. If you use
the setTime()
method the arguments to pass to
it are the same as those you’d pass to the Date constructor (for which
see the TADS 3 System Manual). The two simplest formats to use are
probably:
timeManager.setTime(year, month, day, hour, minutes, seconds, ms)
: possibly the best bet if you want to change both the date and the time; year is the year number (e.g., 2012), month is the month (1 to 12), day is the day of the month (1 to 31), hour is the clock hour (0 for midnight to 23 for 11 PM), minutes is the number of minutes past the hour (0 to 59), seconds is the number of seconds past the minute (0 to 59), and ms is the number of milliseconds past the second (0 to 999).timeManager.setTime('hour:minutes:seconds')
ortimeManager.setTime('hour:minutes')
, where hour, minutes and seconds have the same meaning as before. This is probably the best option to use if you simply want to jump to a different time on the same day. (Note that if you were using the Date constructor directly you would need to supply a couple of extra arguments to make sure the day did stay the same, but thesetTime()
method takes care of this for you.)
If you want to jump by a certain amount you can use the addInterval(interval) method of timeManager, where interval can be one of:
- A list of numbers in the form
\[years, months, days, hours, minutes, seconds\]
; trailing zero elements may be omitted. - A single integer, in which case this will be taken as the number of minutes to advance the time.
- A single BigNumber (a number containing a decimal point), in which case this will be taken as the number of hours to advance the time.
Thus calling timeManager.addInterval(15)
and
timeManager.addInterval(0.25)
would both
advance the time by a quarter of an hour. Note that these numbers can be
negative as well as positive, so that, for example,
timeManager.addInterval(-24.0)
would send the
time a day into the past. A further subtlety to note is that unless you
do something to prevent it, calling
timeManager.addInterval()
won’t prevent the
normal advance of time that takes place in any case on each turn. So,
for example, if each turn would normally take one minute and you call
timeManager.addInterval(1)
you’ll find that
the game time has advanced by two minutes at the end of the turn, one
minute from addInterval()
and the other from
the normal turn handling.
Final Considerations
Is it worth it?
The Objective Time extension is capable of providing you with extensive control over the passage of time in your game, but to do so in any great detail will probably involve you in a large amount of work. Do you really need it? After all, most Interactive Fiction gets by perfectly well without giving players any explicit notification of the passage of time, and if all you need is a few vague indications of time, to mark the occurrence of major plot events, say, you’re probably better off using the Subjective Time extension (subtime), which will cause you a lot less work. So why might you need to use objtime at all?
The kind of circumstance in which objtime might be useful are when:
- You want to display the game time in the status line (subtime is quite unsuitable for this).
- Your game contains a number of puzzles for which timing is crucial and needs to be measured with a plausible degree of accuracy.
- For dramatic reasons you want to create a sense of urgency by making the player aware of the passage of time.
Objective Time and Subjective Time
The objective time module is used, as we have seen, to keep track of time based on the player’s actions, either at a constant rate per turn, or at a number of seconds per turn determined by the game author as described above. The subjective time module, on the other hand can be used to assign times to particular events in the game (e.g. once the player enters the throne room, it’s three o’clock in the afternoon; when he first meets the princess it’s half past four) and then attempts to extrapolate between them if the player occasionally consults the time (by looking at a clock in the hallway, for example). The subjective time module also ensures that the time of the next event is not reached prematurely. If the player character leaves the throne room and wanders around for a while, the time will never reach half past four until he meets the princess; if necessary the subjective time module will slow down the passage of time to ensure that four thirty is not reached prematurely (for a fuller explanation, see the explanation of the Rationale of the Subjective Time module). This should work reasonably well so long as the player doesn’t try to check the time too often, since it relies on the player not being fully aware of how much time ought to have passed.
Most games should not include both the subtime and objtime modules, since they use different (and basically incompatible) methods of reckoning the time. It is, however, possible to include both modules in the sense that your game will still compile, and it will even attempt to synchronize the two incompatible clocks, so far as that can be done. This will produce some odd results, however, such as the time occasionally (or even quite frequently) jumping backwards from what the objective time module thinks it should be, and there can seldom be many occasions when it’s actually a good idea to include both modules. The only good reason for doing do is if your game wants to use one method for reckoning time in one segment of the game, and the other method in another.
If both modules are present, the Subjective Time module attempts to synchronize the two versions of time in the following ways:
- When a ClockEvent occurs, the Subjective Time module sets the Objective Time module’s time to the same time.
- When the Subjective Time module commits to a time between two ClockEvents, the the Subjective Time module sets the Objective Time module’s time to the same time.
- When the Subjective Time module needs to calculate a time between two ClockEvents (since it’s being asked to check the time), it will use the Objective Time’s reckoning of time if and only if that would result in an earlier time than it would otherwise have used (since one of the main functions of the Subjective Time module is to prevent the time of the next ClockEvent being reached too quickly).
- Once the last ClockEvent has been passed, the Subjective Time module simply takes its time from the Objective Time module (rather than its own reckoning of so many minutes per hundred turns).
Note, however, that these attempts to synchronize two basically incompatible methods of reckoning time can only be partially successful at best, and may well produce odd results. In particular, it would be a bad idea to display the objective time anywhere (at least until the last ClockEvent had been past), since it is likely to jump backwards and forwards in strange ways as it is synchronized with the subjective time.
In sum, then, don’t include both the Objective Time module and the Subjective Time module in the same game unless you have a very good reason for doing so and you’re quite sure you know what you’re doing.
This covers most of what you need to know to use this extension. For additional information see the source code and comments in the objtime.t file.
Adv3Lite Manual
Table of Contents |
Extensions >
Objtime