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:

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:

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:

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:

If you want to jump by a certain amount you can use the addInterval(interval) method of timeManager, where interval can be one of:

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:


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:

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.