Skip to content

Tutorials · Navigation and AI · Chapter 9

Timers

By the end

  • A Timer on each guard, six seconds, one shot, that his script starts when he loses the thief and that sends him back to his round when it runs out
  • A Timer on the bell, four seconds, held until a guard chases her and held again when he stops
  • bell.rgs: the bell shows what is left of its countdown, and when it runs out it swings and calls every guard to where she was last seen
On this page

In chapter 8 a guard counted his own search down, one step at a time, in Patience. A Timer counts for him: a countdown on his node, in seconds, that his script starts, stops, and hears when it runs out. Each guard gets one, and his script hands it the search.

  1. Select guard_corridor. Click Add component and choose TimerComponent.
  2. Set Wait Time to 6, clicking its box once, typing and pressing Enter. Tick One Shot. Untick Autostart. Paused stays unticked.
  3. Select guard_hall and do the same: Wait Time 6, One Shot ticked, Autostart unticked.
Edit mode, looking through the game camera at the corridor’s west end: the corridor guard at his post, selected, the thief on the carpet east of him; the Inspector narrowed with the search box, timer, to his Timer: Wait Time 6.00 s, One Shot ticked, Autostart unticked, Paused unticked, Time Left 0.00 s, greyed
The corridor guard’s Timer. For this picture the thief was placed near him, so that looking through the game camera shows him.
Wait Time
The Timer’s Wait Time: how long its countdown lasts, in seconds. 6 is the search chapter 8 counted in steps, 360 of them. A new Timer’s is 1; at 0 it never fires.
One Shot
The Timer’s One Shot: ticked, it fires once at the end of its countdown and stops. Unticked, it starts the next countdown at once and fires every Wait Time, which suits a vent or a drip, not a search. A new Timer’s is unticked.
Autostart
The Timer’s Autostart: ticked, it starts counting on the node’s first step. A guard’s is unticked: his search starts when he loses her, and a countdown from the first step would fire six seconds into the game, for no search at all. A new Timer’s is ticked.
Paused
The Timer’s Paused: ticked, its countdown holds where it is, neither running down nor firing, until it is unticked. The guards leave it unticked; the bell, below, starts with it ticked.
Time Left
The Timer’s Time Left, read-only: the seconds left of its countdown, live in Play. 0 means stopped: not started yet, run out as a one shot, or stopped by a script.

His script hands it the search

  1. Click the Code tab, then guard.rgs in its Scripts list. Replace everything in it with the script below, and press Ctrl+S.
rgs guard.rgs
/// A guard's mind. He walks his round, corner to corner. A glimpse of the
/// thief, or a noise near enough by road, makes him suspicious: he creeps to
/// where he glimpsed her or heard it. Seen near enough to be sure, she is
/// chased at a run, and caught she is sent back to where she came in. Lost,
/// she is searched for about where he saw her last until his Timer runs out,
/// and then he goes back to his round. He tells the bell when he starts and
/// stops chasing, and when it rings he comes to search where she was last
/// seen.
enum Mode { Patrol, Suspicious, Chase, Search, Return }

component Guard {
    /// His round's first corner.
    a: Vec2,
    /// Its second.
    b: Vec2,
    /// Its third; a round of two corners never comes here.
    c: Vec2,
    /// Its fourth.
    d: Vec2,
    /// How many corners his round has: 2 or 4.
    corners: i32 = 4,
    /// How far by road a noise can be and still be heard.
    #[suffix("u")]
    hearing: f32 = 320.0,
    /// How far he sees.
    #[suffix("u")]
    sight: f32 = 112.0,
    /// How far to either side of straight ahead he sees, in radians.
    cone: f32 = 0.6,
    /// The collision layers that hide what is behind them.
    walls: i32 = 1,
    /// How near he must see her to be sure it is her.
    #[suffix("u")]
    sure: f32 = 64.0,
    /// How fast he walks his round.
    walk: f32 = 40.0,
    /// How fast he creeps when suspicious.
    creep: f32 = 24.0,
    /// How fast he runs after her.
    run: f32 = 72.0,
    /// What is on his mind.
    #[readonly]
    mode: Mode = .Patrol,
    /// Whether the thief is in his cone, as this step began.
    #[readonly]
    sees: bool = false,
    /// Which way he faces: the way he last walked.
    #[hidden]
    facing: Vec2,
    /// Where he stood the step before.
    #[hidden]
    was: Vec2,
    /// Where he saw her last.
    #[hidden]
    last_seen: Vec2,
    /// The corner he is walking to, from 0.
    #[hidden]
    next: i32 = 0,
    /// Whether to set off for that corner this step.
    #[hidden]
    to_round: bool = true,

    fn on_create() {
        was = position();
        facing = vec2(0.0, 1.0);
    }

    fn loop() {
        let moved = position() - was;
        if moved.length() > 0.1 {
            facing = moved;
        }
        was = position();
        let thief = nearest("thief");
        let at = position_of(thief);
        let toward = at - position();
        // Returning, he has given up; he looks again on his round.
        sees = mode != .Return
            && thief != node(0)
            && toward.length() < sight
            && angle_between(facing, toward) < cone
            && can_see(at, walls);
        if sees {
            last_seen = at;
        }

        if mode == .Patrol || mode == .Return {
            if sees {
                mode = .Suspicious;
                set_number("NavAgentComponent", "speed", creep);
                walk_to(at);
            }
        } else if mode == .Suspicious {
            if sees && toward.length() < sure {
                mode = .Chase;
                set_number("NavAgentComponent", "speed", run);
                emit_to("bell", "spotted");
            }
        } else if mode == .Chase {
            if !sees {
                mode = .Search;
                start_timer();
                emit_to("bell", "lost");
                walk_to(last_seen);
            } else if toward.length() < 10.0 {
                emit_to("thief", "caught");
                emit_to("bell", "lost");
                mode = .Return;
                to_round = true;
            } else {
                keep_point("seen", at);
                walk_to(at);
            }
        } else if mode == .Search {
            if sees {
                stop_timer();
                mode = .Chase;
                set_number("NavAgentComponent", "speed", run);
                emit_to("bell", "spotted");
            }
        }

        if to_round {
            to_round = false;
            set_number("NavAgentComponent", "speed", walk);
            if next == 0 {
                walk_to(a);
            } else if next == 1 {
                walk_to(b);
            } else if next == 2 {
                walk_to(c);
            } else {
                walk_to(d);
            }
        }
        set_param_flag("alert", mode == .Chase || mode == .Search);
    }

    fn on_signal() {
        if heard("noise") && (mode == .Patrol || mode == .Return) {
            let noise = kept_point("noise", position());
            let way = road_distance(noise);
            if way >= 0.0 && way < hearing {
                mode = .Suspicious;
                set_number("NavAgentComponent", "speed", creep);
                walk_to(noise);
            }
        }
        if heard("alarm") && (mode == .Patrol || mode == .Return) {
            mode = .Search;
            last_seen = kept_point("seen", position());
            start_timer();
            set_number("NavAgentComponent", "speed", run);
            walk_to(last_seen);
        }
        if heard("nav:ended") {
            if mode == .Patrol || mode == .Return {
                if arrived() {
                    next += 1;
                    if next >= corners {
                        next = 0;
                    }
                }
                mode = .Patrol;
                to_round = true;
            } else if mode == .Suspicious {
                mode = .Return;
                to_round = true;
            } else if mode == .Search {
                let far = 40.0 * sqrt(random(0.0, 1.0));
                let turn = random(0.0, tau());
                let spot = last_seen + vec2(far * turn.cos(), far * turn.sin());
                if can_reach(spot) {
                    walk_to(spot);
                } else {
                    walk_to(last_seen);
                }
            }
        }
        // An arrival on the step the search runs out is the search's, so it is heard first.
        if heard("timer:fired") && mode == .Search {
            mode = .Return;
            to_round = true;
        }
    }
}

The script no longer has Patience, or the hidden count it kept, and each guard’s Guard still shows Patience: the editor keeps a field a script stops declaring, as chapter 8 said. Clear it the same way.

  1. Select guard_corridor, remove his Guard with the bin icon at the right of its header, click Add component and choose Guard. Set A to 40, 232, B to 600, 232 and Corners to 2, as in chapter 8.
  2. Select guard_hall and do the same, with A 56, 72, B 264, 72, C 264, 168 and D 56, 168. Corners stays 4.
  3. Press Ctrl+S.

Four parts changed. A chase that loses her starts his Timer, and tells the bell, below:

rgs part of guard.rgs
        } else if mode == .Chase {
            if !sees {
                mode = .Search;
                start_timer();
                emit_to("bell", "lost");
                walk_to(last_seen);
            } else if toward.length() < 10.0 {
                emit_to("thief", "caught");
                emit_to("bell", "lost");
                mode = .Return;
                to_round = true;
            } else {
                keep_point("seen", at);
                walk_to(at);
            }

Seen again while he searches, the Timer is stopped, and the chase starts again:

rgs part of guard.rgs
        } else if mode == .Search {
            if sees {
                stop_timer();
                mode = .Chase;
                set_number("NavAgentComponent", "speed", run);
                emit_to("bell", "spotted");
            }
        }

And when the Timer runs out, he returns. This is the last thing his on_signal asks:

rgs part of guard.rgs
        // An arrival on the step the search runs out is the search's, so it is heard first.
        if heard("timer:fired") && mode == .Search {
            mode = .Return;
            to_round = true;
        }
start_timer
Starts this node’s Timer on a fresh countdown of its Wait Time: six seconds from the step he loses her. Started again while it runs, it starts from six again.
stop_timer
Stops this node’s Timer and throws away what was left, as the step ends: seen again, his search is over, and a later one starts from six.
timer:fired
The signal a node’s Timer sends to the script on that node when its countdown runs out, heard once for each firing. Only his own Timer’s: the bell’s firing is the bell’s.

The alarm

The bell hangs in the great hall. From this chapter it counts too: four seconds, but only while a guard is chasing the thief. A guard who starts a chase tells it spotted, and one who stops tells it lost. When its four seconds of chase have run out, it swings, and calls every guard. A guard on his round comes running to search where she was last seen.

That is the last two changes to his script. In a chase he keeps where she is, under the name seen, on every step; and he hears the bell’s alarm:

rgs part of guard.rgs
        if heard("alarm") && (mode == .Patrol || mode == .Return) {
            mode = .Search;
            last_seen = kept_point("seen", position());
            start_timer();
            set_number("NavAgentComponent", "speed", run);
            walk_to(last_seen);
        }

Only a guard on his round or on his way back answers: one who is already chasing or searching has her in hand. He searches at a run, from the point the chaser kept with keep_point, for his own Timer’s six seconds.

  1. Select bell. Under Groups, type bell into Add group and press Enter: it is the group the guards’ emit_to("bell", …) reaches.
  2. Click Add component and choose TimerComponent. Set Wait Time to 4. Tick One Shot, and tick Paused. Autostart stays ticked, as it comes.
  3. In Assets, press Create, choose New script, type bell, and press Enter. In the Code tab, replace everything in bell.rgs with the script below, and press Ctrl+S.
  4. Select bell, click Add component and choose Bell.
  5. Press Ctrl+S.
rgs bell.rgs
/// The alarm bell. Its Timer starts held, and counts down only while a
/// guard is chasing the thief; when it runs out the bell rings, and every
/// guard comes to search where she was last seen.
component Bell {
    /// Seconds left before it rings, as this step began.
    #[readonly]
    left: f32 = 0.0,

    fn loop() {
        left = time_left();
    }

    fn on_signal() {
        if heard("spotted") {
            set_flag("TimerComponent", "paused", false);
        }
        if heard("lost") {
            set_flag("TimerComponent", "paused", true);
        }
        if heard("timer:fired") {
            pulse_turn(0.3, 0.15, .InOutSine);
            emit_to("guards", "alarm");
        }
    }
}
Autostart and Paused, together
The bell’s Timer starts on its first step, and starts held: its countdown is running, at four seconds, and does not move until something unticks Paused. Its script does, on spotted, and ticks it again on lost, so it counts only the seconds of chase, all of them added up, however many chases it takes.
set_flag
Writes true or false onto another component on this node: set_flag("TimerComponent", "paused", false) unticks the Timer’s Paused, from the next step on.
time_left
The seconds left on this node’s Timer, as the step began; 0 when it is stopped. The bell writes it into Left on every step, so you can read the countdown in the Inspector, where the Timer’s Time Left shows it too.
Left
The bell’s script’s one field, read-only: what time_left said this step.
pulse_turn
Turns this node by an angle, in radians, and back, for ever, taking the seconds given each way, eased by the curve: 0.3 of a radian, about 17 degrees, each way in 0.15 s, .InOutSine. The bell turns about its top, where its bracket holds it, so it swings. It swings from the moment it rings to the end of the game.
spotted, lost and alarm
The three signals between the guards and the bell. A guard sends spotted to the group bell as a chase starts, and lost as it stops, whether she is lost or caught. The bell sends alarm to the group guards when it rings. Each is heard on the step after it is sent.

The bell, in Play

  1. Press F5, walk the thief up into the hall under the bell, and select bell. In the Inspector, type timer into the search box and press Enter: Paused is ticked and Time Left holds at 4.00 s.
  2. Type bell into the search box instead and press Enter, so its Left shows, and walk her towards the hall guard on his round. When he chases her, Left falls; when he catches her or loses her, it holds where it got to.
  3. Press F5 again to stop.
Play, looking through the game camera at the great hall: the bell on the north wall, selected, the thief standing below it, the cat at the hearth, the two tables with their discs, and a lime line along the hall’s top to the hall guard’s next corner; the Inspector narrowed with the search box, timer, to the bell’s Timer: Wait Time 4.00 s, One Shot, Autostart and Paused ticked, Time Left 4.00 s
After 60 steps, 1 s, of Play: the bell’s Timer running and held, four seconds left. For this picture the thief was placed in the hall below the bell from the start.
Play, looking through the game camera at the great hall: the hall guard with a red mark over his head, a lime line from him down to the thief, east of the tables, the cat at the hearth and the bell, selected, on the north wall; the Inspector narrowed with the search box, bell, to the bell’s script: Left 3.7833
After 210 steps, 3.5 s, of Play: the hall guard chases her, and the bell counts: 3.7833 seconds left. For this picture the thief was placed in the hall near his round from the start.

What the chapter set

NodeComponentFieldValue
bellnodeGroupsbell
TimerWait Time4
One Shotticked
Pausedticked
Belladded
guard_hallTimerWait Time6
One Shotticked
Autostartunticked
Guardremoved and added again, its corners typed again
guard_corridorTimerWait Time6
One Shotticked
Autostartunticked
Guardremoved and added again, its corners typed again

Every other field is as it comes: the bell’s Autostart ticked, the guards’ Paused unticked. The corners are chapter 8’s.

Check it works