46. Paths, Groups and Decisions¶
How things find their way, and how the computer decides what to do. The searches of this chapter work on any grid the game describes with one function, "may this tile be entered?"; the decisions are small tools that keep a character's modes in order, give the computer a picture of the battle, and let consequences spread in a fixed order. Choosing units with the mouse is here too, because it is the start of every order in a strategy game.
What may be entered: Passable¶
Package path · introduced in 25. Maze · source: Passable
Every search asks the game, not the map, whether a tile can be entered: (col, row) -> boolean. The same map gives different answers to different walkers. A ghost flying home may pass the door of its house, which is a wall to everyone else; a vehicle of Sands drives on sand and rock, the burrower only on sand; a tower that is only planned closes its tile for the question "would the way stay open?". So the searches never need to know what a tile is.
Which search?¶
| The question | Use | Cost |
|---|---|---|
| The way from everywhere to one place, in four directions: the ghosts' way home, the nearest safe tile | DistanceMap |
One breadth-first search for every tile at once |
| The cheapest way from here to there, with costs per tile: a computer player that avoids danger | AStar |
One search per question, looking first towards the goal |
| The way from everywhere to one place, in eight directions, for many units walking freely | FlowField |
One search for every tile; each unit only looks up an arrow |
| Many such goals, one per order | FieldCache |
A flow field per goal, kept until the map changes |
| The next road for a ware on a network of roads and flags | RoadNetwork.next |
One search from the goal over the flags, kept until the network changes |
| Where each unit of a group stops | Formation.slots and assign |
A small search around the target |
| May a unit drive straight from here to there? | Formation.clearLine |
One walk along the line |
The rule of thumb: when many things want to reach one place, search once from that place, outwards (DistanceMap, FlowField). When one thing wants to reach a place of its own, search from the thing towards the goal (AStar).
One goal, every tile: DistanceMap¶
Package path · introduced in 25. Maze · source: DistanceMap
What it is for. For every tile of a map, the number of steps to the nearest of one or more goals, found by breadth-first search: the goals have distance 0, their open neighbours 1, the neighbours of those 2, a wave that spreads through the corridors until it has filled them. downhill(col, row) is then the first step on a shortest way from anywhere. Search from where one stands instead, and the distances of the dots say which is the nearest of many things.
How it works. A queue in an int array, four directions in the order of Direction, steps over an edge of the map arriving on the far side as in TileMap.step. Of equally good steps downhill, the first direction wins, so the result never depends on chance. visited() is what the search cost. Maze sends its eaten ghosts home this way, and Blast's computer players find the nearest tile no flame will reach (chapter 26). The map need not be a level of tiles: Hover Cab's autopilot lays a grid of possible cab positions four units apart over a free-form scene, marks each open or not with a pixel mask, and flies down the distances to its pad (chapter 28).
public final class DistanceMap {
/** The distance of a tile no way leads to. */
public static final int UNREACHABLE = Integer.MAX_VALUE;
private final int width;
private final int height;
private final int[] distance;
private int visited;
private DistanceMap(int width, int height) {
this.width = width;
this.height = height;
this.distance = new int[width * height];
Arrays.fill(distance, UNREACHABLE);
}
/** The distances to the nearest of {@code goals} over the tiles {@code open} lets one enter. */
public static DistanceMap search(TileMap<?> map, Passable open, Cell... goals) {
DistanceMap result = new DistanceMap(map.width(), map.height());
int[] queue = new int[map.width() * map.height()];
int head = 0;
int tail = 0;
for (Cell goal : goals) {
int index = goal.row() * map.width() + goal.col();
if (result.distance[index] == UNREACHABLE) {
result.distance[index] = 0;
queue[tail++] = index;
}
}
while (head < tail) {
int index = queue[head++];
int col = index % map.width();
int row = index / map.width();
for (Direction d : Direction.values()) {
Cell next = map.step(col, row, d);
int n = next.row() * map.width() + next.col();
if (result.distance[n] != UNREACHABLE || !open.test(next.col(), next.row())) continue;
result.distance[n] = result.distance[index] + 1;
queue[tail++] = n;
}
}
result.visited = tail;
return result;
}
The cheapest way for one: AStar¶
Package path · introduced in 25. Maze · source: AStar
What it is for. The cheapest way from one tile to another when tiles cost differently to enter: a way past a ghost can cost more than a longer way round it. AStar.find(map, from, to, cost) returns a Path: the cells, the steps, the cost and how many tiles the search looked at; found() and first() for the step to take now.
How it works. Breadth-first search spreads evenly in all directions and counts steps. A* adds a cost per tile and looks first at the tiles that seem to lead to the goal: of all tiles at its border it takes the one with the smallest g + h, the cost so far plus a guess of the cost still to come. The guess is the number of steps without walls, across and along, the short way round over the edges. As long as every tile costs at least 1, the guess is never too high, and the way found is always one of the cheapest. A tile one cannot enter costs Double.POSITIVE_INFINITY.
public static Path find(TileMap<?> map, Cell from, Cell to, Cost cost) {
int width = map.width();
int size = width * map.height();
double[] g = new double[size];
Arrays.fill(g, Double.POSITIVE_INFINITY);
int[] cameFrom = new int[size];
Direction[] cameBy = new Direction[size];
boolean[] closed = new boolean[size];
// The border of the search, cheapest g + h first; of equal ones, the one nearer the goal.
PriorityQueue<Open> border = new PriorityQueue<>((a, b) -> a.f() != b.f()
? Double.compare(a.f(), b.f()) : Double.compare(a.h(), b.h()));
int start = from.row() * width + from.col();
int goal = to.row() * width + to.col();
g[start] = 0;
border.add(new Open(start, guess(map, from, to), guess(map, from, to)));
int visited = 0;
while (!border.isEmpty()) {
int index = border.poll().index();
if (closed[index]) continue; // an older, dearer entry of a tile seen again
closed[index] = true;
visited++;
if (index == goal) return path(map, cameFrom, cameBy, start, goal, g[goal], visited);
int col = index % width;
int row = index / width;
for (Direction d : Direction.values()) {
Cell next = map.step(col, row, d);
int n = next.row() * width + next.col();
if (closed[n]) continue;
double through = g[index] + cost.enter(next.col(), next.row());
if (through >= g[n]) continue; // no better than a way known already (or a wall)
g[n] = through;
cameFrom[n] = index;
cameBy[n] = d;
double h = guess(map, next, to);
border.add(new Open(n, through + h, h));
}
}
return new Path(List.of(), List.of(), Double.POSITIVE_INFINITY, visited);
}
/** The steps without walls, across and along, the short way round over the edges. Never too many. */
static double guess(TileMap<?> map, Cell a, Cell b) {
int dx = Math.abs(a.col() - b.col());
int dy = Math.abs(a.row() - b.row());
return Math.min(dx, map.width() - dx) + Math.min(dy, map.height() - dy);
}
The two searches work well together. The pilot of Maze, the computer player, plans again on every new tile: a DistanceMap from where it stands finds the nearest dot, and A* finds the way there, with costs that rise near the ghosts (chapter 25).
Many units, one goal: FlowField¶
Package path · introduced in 30. Towers · source: FlowField
What it is for. For every tile of a map, which way leads to the goal: a field of arrows that any number of units follow at once. A hundred orcs cost one search, not a hundred, and a new tower costs one new search, however many orcs must find a new way round it. The field also answers whether every tile still reaches the goal, which is how Towers refuses a tower that would close the way (chapter 30).
How it works. Units walk in eight directions. A step to a side costs 10, a diagonal one 14 (√2 × 10), so the distances are those of a real walk and not of a chess king. Because the steps differ, the search is Dijkstra's, with a queue sorted by distance, a binary heap of longs with the distance above and the tile below. A diagonal step is allowed only where both tiles beside it are open: no unit squeezes between two walls that touch at a corner. Unlike DistanceMap, a flow field has edges.
/**
* Dijkstra's search: the tile nearest to a goal that has not been settled yet is settled next, and its
* neighbours learn a shorter distance through it. Breadth-first search (chapter 25) is the same with
* equal steps; here diagonal steps cost more, so the queue must be sorted by distance.
*/
private void search(Passable open, Cell... goals) {
Heap queue = new Heap(width * height);
for (Cell goal : goals) {
if (!contains(goal.col(), goal.row())) throw new IllegalArgumentException(goal + " is outside the field");
int index = goal.row() * width + goal.col();
distance[index] = 0;
queue.push(0, index);
}
while (!queue.isEmpty()) {
long entry = queue.pop();
int index = (int) entry;
int here = (int) (entry >>> 32);
if (here > distance[index]) continue; // settled already by a shorter way
visited++;
int col = index % width;
int row = index / width;
for (int d = 0; d < 8; d++) {
int c = col + DX[d];
int r = row + DY[d];
if (!step(open, col, row, d)) continue;
int n = r * width + c;
int through = here + (d < 4 ? STRAIGHT : DIAGONAL);
if (through < distance[n]) {
distance[n] = through;
// The way from n leads back the way the search came: to the opposite neighbour.
next[n] = (byte) opposite(d);
queue.push(through, n);
}
}
}
}
/** Whether a unit may step from a tile to its neighbour {@code d}: open, and no corner cut. */
private boolean step(Passable open, int col, int row, int d) {
int c = col + DX[d];
int r = row + DY[d];
if (!contains(c, r) || !open.test(c, r)) return false;
if (d < 4) return true;
return open.test(c, row) && open.test(col, r);
}
Walking it. heading(x, y), positions in tiles (2.5 is the middle of column 2), is the direction, length 1, towards the middle of the next tile; the zero vector at the goal's middle or where no way leads. A unit that walks this way never touches a closed tile, and that holds for any point of the next tile, so a unit may aim a little to the side of the middle to walk beside the others instead of behind them (chapter 30).
| Method | What it does |
|---|---|
FlowField.toward(width, height, open, goals...) |
The field towards the nearest goal |
heading(x, y) |
Where a unit at x, y (in tiles) walks |
next(col, row) |
The next tile on a shortest way; null at a goal and where no way leads |
distance(col, row), reaches(col, row), isGoal(col, row) |
The cost, 10 per tile; whether a way leads to a goal |
visited() |
How many tiles the search settled |
Test: a wall in the way
@Test
void aNewWallCanCutTheWayAndTheFieldSaysSo() {
TileMap<Character> map = map("""
row ......
row XX.XXX
row ......
""");
Cell goal = new Cell(5, 2);
assertTrue(field(map, goal).reaches(0, 0));
map.set(2, 1, 'X'); // a tower in the only gap
FlowField cut = field(map, goal);
assertFalse(cut.reaches(0, 0));
assertEquals(FlowField.UNREACHABLE, cut.distance(0, 0));
assertEquals(Vector.ZERO, cut.heading(0.5, 0.5), "nowhere to go");
assertTrue(cut.reaches(0, 2));
}
Many fields: FieldCache¶
Package path · introduced in 31. Sands · source: FieldCache
In a strategy game every order has a goal of its own. toward(goal) gives the field towards a tile, from the cache or searched now: a group shares one field, two groups sent to the same place share it too, and the harvesters, all driving home to the same refinery, share theirs for the whole game. When a building goes up or comes down, invalidate() forgets every field, and each is searched again when it is next asked for. Only the 32 fields used last are kept, an access-ordered LinkedHashMap that drops its eldest entry, because a field of a map of 64 × 64 is 20 KB. searches() counts what the cache did not save: a test proves that six tanks with one order cost one search.
Roads between flags: RoadNetwork¶
Package road · introduced in 32. Colony · source: RoadNetwork
What it is for. Roads that the player lays from flag to flag, and the way of a ware over them. A road joins two flags and runs over the cells between; a new flag on a road makes two roads of it. A ware at a flag asks which road it takes next to reach the flag of the house it is meant for.
When to choose it. When things travel on a network the player builds, in hops between fixed points: the roads of a settlement, rails between stations, pipes between pumps. Things that walk freely over the map use the searches above.
How it works. The flags are the nodes of a graph and the roads its edges, each as long as its steps. The way to a goal is found by Dijkstra's search from the goal outwards, once, which gives every flag at once its distance and the road it takes: next is then a lookup. The searches are kept for the 64 goals used most recently and forgotten when a flag or road is added or removed. Ties go to the lower number, so the answer never depends on the order of a hash map, and a loaded game goes on exactly. Flags and roads may be added with a given number, and restore sets the counters, for loading.
/**
* The road a ware at flag {@code from} takes next on a shortest way to flag {@code to}; {@code null}
* when it is there already or no way leads there.
*/
public Road next(int from, int to) {
if (from == to) return null;
return search(to).next().get(from);
}
/** Steps on the shortest way from one flag to another; {@link Integer#MAX_VALUE} without a way. */
public int distance(int from, int to) {
if (!flags.containsKey(from) || !flags.containsKey(to)) return Integer.MAX_VALUE;
return search(to).distance().getOrDefault(from, Integer.MAX_VALUE);
}
public boolean connected(int from, int to) {
return distance(from, to) != Integer.MAX_VALUE;
}
/** Dijkstra's search from the goal outwards, kept until the network changes. */
private Search search(int goal) {
Search search = searches.get(goal);
if (search != null) return search;
Map<Integer, Integer> distance = new HashMap<>();
Map<Integer, Road> next = new HashMap<>();
// Ties go to the lower flag number, so the answer never depends on the order of a hash map.
PriorityQueue<int[]> open = new PriorityQueue<>((a, b) -> a[0] != b[0] ? Integer.compare(a[0], b[0])
: Integer.compare(a[1], b[1]));
if (flags.containsKey(goal)) {
distance.put(goal, 0);
open.add(new int[]{0, goal});
}
while (!open.isEmpty()) {
int[] head = open.poll();
int flag = head[1];
if (head[0] > distance.get(flag)) continue; // an older, longer entry of a flag settled since
for (Road road : touching.get(flag)) {
int other = road.other(flag);
int d = head[0] + road.length();
Integer known = distance.get(other);
if (known == null || d < known || d == known && road.id() < next.get(other).id()) {
distance.put(other, d);
next.put(other, road);
open.add(new int[]{d, other});
}
}
}
search = new Search(distance, next);
searches.put(goal, search);
searched++;
return search;
}
| Method | What it does |
|---|---|
addFlag(cell), removeFlag(flag) |
A new flag and its number; removes one with its roads |
connect(from, to, cells), remove(road) |
A road over the cells from one flag to another, checked; removes one |
split(cell) |
A flag in the middle of a road, two roads of one; the new flag |
next(from, to), distance(from, to), connected(from, to), way(from, to) |
The next road on the shortest way, its length, whether there is one, all of its roads |
flagAt(cell), roadAt(cell), cell(flag), road(id) |
What is where, for the mouse and the drawing |
flags(), roads(), roads(flag) |
Everything, and the roads at one flag |
version(), searches() |
Counts changes for the drawing; counts searches for tests |
nextFlag(), nextRoad(), restore(nextFlag, nextRoad) |
The counters, for a save file |
Groups on the move: Formation¶
Package path · introduced in 31. Sands · source: Formation
What it is for. Where a group of units stands when it arrives. Five tanks sent to one tile cannot all stand on it: each needs a slot of its own near the tile the player clicked, and each must take the slot that suits it, or they cross each other's way at the end and jostle.
How it works. In three static methods:
slots(target, count, width, height, open): the open tiles nearest the target, counted in steps from it by a breadth-first search in eight directions, so a slot never lies behind a wall. A closed target, such as a click on a rock, is replaced by the nearest open tile.assign(xs, ys, slots): which slot each unit takes. The closest pair of unit and slot first, then the closest of what is left. That is the greedy assignment, not the best one there is (the Hungarian method), but it is close for groups of a dozen, it is a few lines, and the paths it gives seldom cross.clearLine(x0, y0, x1, y1, width, height, open): whether a unit can drive straight from one point to another. It walks the line through the grid tile by tile, as a ray is traced through a grid, and refuses to cut a corner. A unit follows the group's flow field until it is near the goal and then drives straight to its own slot (chapter 31).
/**
* Which slot each unit takes: {@code result[unit]} is an index into {@code slots}, or -1 for a unit
* left without one when there are fewer slots than units. Positions are in tiles, 2.5 being the middle
* of column 2. The closest pair of unit and slot is taken first, then the closest of the rest.
*/
public static int[] assign(double[] xs, double[] ys, List<Cell> slots) {
int units = xs.length;
int[] slotOf = new int[units];
Arrays.fill(slotOf, -1);
int pairs = units * slots.size();
double[] distance = new double[pairs];
for (int u = 0; u < units; u++) {
for (int s = 0; s < slots.size(); s++) {
int pair = u * slots.size() + s;
distance[pair] = Math.hypot(slots.get(s).col() + 0.5 - xs[u], slots.get(s).row() + 0.5 - ys[u]);
}
}
// Sort the pairs by distance; ties keep the order of the units, then of the slots.
Integer[] sorted = new Integer[pairs];
for (int i = 0; i < pairs; i++) sorted[i] = i;
Arrays.sort(sorted, (a, b) -> {
int nearer = Double.compare(distance[a], distance[b]);
return nearer != 0 ? nearer : Integer.compare(a, b);
});
boolean[] taken = new boolean[slots.size()];
int left = Math.min(units, slots.size());
for (int i = 0; i < pairs && left > 0; i++) {
int u = sorted[i] / slots.size();
int s = sorted[i] % slots.size();
if (slotOf[u] >= 0 || taken[s]) continue;
slotOf[u] = s;
taken[s] = true;
left--;
}
return slotOf;
}
Slots keep a group apart at the end; on the way, the game pushes overlapping units apart itself, with a Grid.
Choosing units: Selection and BoxSelect¶
Package select · introduced in 31. Sands · source: Selection, BoxSelect
What it is for. The conventions of strategy games since the nineties. A click picks one thing, a drag opens a box and picks all in it, Shift adds to the choice (and a Shift-click on something already picked takes it out), Ctrl and a number keep the choice as a group that the number alone calls back. Selection keeps what is picked and the nine groups; BoxSelect tells a click from a box.
How it works. BoxSelect is arithmetic in screen or world coordinates: press, move and release with the mouse's position, in the update step. A hand that clicks moves the mouse a little, so the press only becomes a box after more than 4 units of movement (SLOP). release returns a sealed Result, a Click or a Box with sorted edges, so the game's switch must handle both, and Java checks that it does. While dragging(), the game draws the box.
public final class BoxSelect {
/** How far the mouse may move between press and release and still be a click, in logical units. */
public static final double SLOP = 4;
/** What a press and a release came to. */
public sealed interface Result {
}
/** The button went down and up on nearly the same spot. */
public record Click(double x, double y) implements Result {
}
/** A box dragged open, its edges sorted: left is less than right, top less than bottom. */
public record Box(double left, double top, double right, double bottom) implements Result {
public boolean contains(double x, double y) {
return x >= left && x <= right && y >= top && y <= bottom;
}
}
private boolean pressed;
private boolean dragging;
private double startX;
private double startY;
private double x;
private double y;
/** The left button went down here. */
public void press(double px, double py) {
pressed = true;
dragging = false;
startX = x = px;
startY = y = py;
}
/** The mouse is here now; once it has gone further than {@link #SLOP} from the press, it drags a box. */
public void move(double px, double py) {
if (!pressed || Double.isNaN(px)) return;
x = px;
y = py;
if (Math.abs(x - startX) > SLOP || Math.abs(y - startY) > SLOP) dragging = true;
}
/** The button came up here: a click or a box; {@code null} if it was never pressed. */
public Result release(double px, double py) {
if (!pressed) return null;
move(px, py);
pressed = false;
if (!dragging) return new Click(startX, startY);
dragging = false;
return box();
}
Selection keeps the picked things in a LinkedHashSet: none twice, in the order of picking, so the panel shows the first picked first. The game decides what may be picked: in Sands only the player's own vehicles, while a click on an enemy only shows it.
| Method | What it does |
|---|---|
pick(things, adding), click(thing, adding), clear() |
Picks; with adding, as with Shift |
store(number), recall(number), group(number) |
Groups 1 to 9 |
keepOnly(alive) |
Takes the destroyed out of the selection and every group: call it after every step |
all(), contains, size, isEmpty |
What is picked |
version() |
Changes with the selection: a panel redraws only then |
One state at a time: StateMachine¶
Package ai · introduced in 25. Maze · source: StateMachine
What it is for. Something that is always in exactly one of a few states, an enum, and changes between them only along ways allowed in advance: a ghost that waits, hunts, flees or flies home; a door; the phases of a round.
When to choose it. When the rules of a thing depend on its mode and the modes are few. The table of allowed changes is the point: the classic bug is a combination nobody meant, a ghost eaten while it already flies home, and with a table such a change is an IllegalStateException the moment it happens, naming both states, instead of a strange sight minutes later.
How it works. allow(from, to...) fills the table, to(state) changes and calls the onChange listener with the old and the new state (to start a sound, turn round, reset something), update(seconds) lets time pass and seconds() says how long the thing has been in its state. reset(state) goes back without asking the table, for a new level.
public final class StateMachine<S extends Enum<S>> {
private final Map<S, EnumSet<S>> allowed;
private S state;
private S previous;
private double seconds;
private BiConsumer<S, S> onChange = (from, to) -> { };
public StateMachine(S initial) {
this.allowed = new EnumMap<>(initial.getDeclaringClass());
this.state = initial;
this.previous = initial;
}
/** Allows the changes from {@code from} to each of {@code to}. */
@SafeVarargs
public final StateMachine<S> allow(S from, S... to) {
EnumSet<S> targets = allowed.computeIfAbsent(from, s -> EnumSet.noneOf(s.getDeclaringClass()));
for (S s : to) targets.add(s);
return this;
}
/** Called after each change with the old and the new state: to start a sound, turn round, reset a timer. */
public StateMachine<S> onChange(BiConsumer<S, S> listener) {
this.onChange = listener;
return this;
}
public boolean canChange(S to) {
EnumSet<S> targets = allowed.get(state);
return targets != null && targets.contains(to);
}
/** Changes to {@code to}, which must be allowed from the current state. */
public void to(S to) {
if (!canChange(to)) throw new IllegalStateException("not allowed: " + state + " → " + to);
previous = state;
state = to;
seconds = 0;
onChange.accept(previous, to);
}
/** Lets time pass in the current state. */
public void update(double seconds) {
this.seconds += seconds;
}
Strength on the map: InfluenceMap¶
Package ai · introduced in 31. Sands · source: InfluenceMap
What it is for. The computer opponent's picture of the battle. Every unit and building spreads its strength around it, full on its own tile and less further away, nothing beyond its reach; added up, the map shows where a side is strong and where it is weak, without looking at single units. With one per side, an opponent's questions become arithmetic: where is the enemy strongest (strongest())? Which of its buildings is defended worst (weakest(candidates))? Is this crystal field safe for a harvester (at(col, row) is 0)?
How it works. clear(), then add(col, row, strength, reach) for each thing: the strength falls in a straight line to nothing at reach tiles, as the crow flies. Sands builds the map anew each time the computer chooses whom to attack, with the damage per second of each weapon as its strength. What an influence map decides can be seen: draw it over the map, as Sands does with F3, and the computer's choice is no longer a mystery.
Events that cause events: EventQueue¶
Package event · introduced in 26. Blast · source: EventQueue
What it is for. Consequences that spread: a bomb explodes, its flames reach a second bomb, which explodes too, and its flames reach a player. Each event is handled by one method of the rules, and the handler may post further events; process(handler) handles them all, in the order they were posted, until none is left.
When to choose it. When one event can cause others in the same step, and the order matters. Games in which an event only reports what happened (a brick broke, play a sound) need no queue: the rules collect such events in a list for the user interface, as every game does from Paddle on.
How it works. The order is first in, first out, as in breadth-first search: everything the first event caused is handled before anything those events caused. A chain reaction runs in waves, the bombs hit by the first blast before those hit by theirs, and the order never depends on how deep a handler happens to call itself (chapter 26). Every handled event stays in happened() until clear(), so the user interface can show and play what happened in a step. A chain without end would hang the game; after limit events in one process, 10,000 by default, the queue gives up with an exception that names the last event.
public final class EventQueue<E> {
private final ArrayDeque<E> pending = new ArrayDeque<>();
private final List<E> happened = new ArrayList<>();
private final int limit;
/** @param limit most events one {@link #process} may handle */
public EventQueue(int limit) {
if (limit < 1) throw new IllegalArgumentException("limit must be positive: " + limit);
this.limit = limit;
}
public EventQueue() {
this(10_000);
}
/** Adds an event to the end of the queue; also from within a handler. */
public void post(E event) {
pending.addLast(event);
}
/** Hands every waiting event to {@code handler}, also those it posts, oldest first, until none is left. */
public void process(Consumer<? super E> handler) {
int handled = 0;
while (!pending.isEmpty()) {
E event = pending.removeFirst();
if (++handled > limit) {
pending.clear();
throw new IllegalStateException("more than " + limit + " events in one go, the last " + event);
}
happened.add(event);
handler.accept(event);
}
}