Skip to content

Commit f03cdf3

Browse files
committed
Cache hard FoW, soft FoW and clear area, along with visible area
This avoids the need to `.intersect()` the visible area with the cleared area on every frame, which is relatively expensive both in terms of speed and memory allocations. This is used in the `FogRenderer` to great effect, and to lesser effect - due to the subsequent transformation - in `ZoneRenderer#showBlockedMoves()`. Also add clarifying doc comment to ZoneView#getExposedArea() that it is only to be used when the Zone has fog enabled.
1 parent 6842e26 commit f03cdf3

5 files changed

Lines changed: 160 additions & 103 deletions

File tree

src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java

Lines changed: 146 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.stream.Collectors;
2626
import java.util.stream.Stream;
2727
import javax.annotation.Nonnull;
28+
import net.rptools.lib.CodeTimer;
2829
import net.rptools.maptool.client.AppUtil;
2930
import net.rptools.maptool.client.MapTool;
3031
import net.rptools.maptool.client.ui.zone.Illumination.LumensLevel;
@@ -139,11 +140,57 @@ private void addLightSourceToken(Token token, Set<Player.Role> roles) {
139140
*/
140141
private final Map<PlayerView, Illumination> illuminationsPerView = new HashMap<>();
141142

142-
/** Map the PlayerView to its exposed area. */
143-
private final Map<PlayerView, Area> exposedAreaMap = new HashMap<>();
143+
/**
144+
* Holds the visibility information for a {@link PlayerView}.
145+
*
146+
* <p>The {@link #visibleArea()} and {@link #exposedArea()} are the fundamental areas that
147+
* describe the view's visibility and fog of war. The {@link #clearArea()} and {@link
148+
* #softFogArea()} are derived from these based on the zone's configuration.
149+
*
150+
* <p>When {@link Zone#hasFog()} returns {@code false}, the {@link #exposedArea()} is meaningless.
151+
* In this case, the area will be empty and should not be used. Because {@link #clearArea()} and
152+
* {@link #softFogArea()} derive from {@link #exposedArea()}, these are also meaningless and will
153+
* be set to the empty area.
154+
*
155+
* <p>Because of the way Fog of War has to be rendered, {@link #softFogArea()} may not be very
156+
* intuitive. Keep this rendering logic in mind when thinking about these areas:
157+
*
158+
* <ol>
159+
* <li>Start by assuming everything is covered in hard FoW.
160+
* <li>Use {@link #softFogArea()} to carve out part of the hard FoW.
161+
* <li>Use {@link #clearArea()} to carve out part of the soft FoW and hard Fow.
162+
* </ol>
163+
*
164+
* @param visibleArea The combined visible area of all tokens in the view.
165+
* @param exposedArea The combined exposed area of all tokens in the view.
166+
* @param softFogArea The area that is clear of hard FoW. Typically, the same as {@link
167+
* #exposedArea()}, unless vision is off in which case it is empty.
168+
* @param clearArea The area that is clear of soft FoW and hard Fow. Typically, the intersection
169+
* of {@link #visibleArea()} and {@link #exposedArea()}, unless vision if off in which case it
170+
* is just {@link #exposedArea()}.
171+
*/
172+
public record Visibility(
173+
@Nonnull Area visibleArea,
174+
@Nonnull Area exposedArea,
175+
@Nonnull Area softFogArea,
176+
@Nonnull Area clearArea) {
177+
178+
/**
179+
* Creates a visibility record for when the zone does not have fog enabled.
180+
*
181+
* <p>The {@link #exposedArea()}, {@link #softFogArea()}, and {@link #clearArea()} will all be
182+
* set to empty areas.
183+
*
184+
* @param visibleArea The visible area for the view.
185+
* @return A visibility record that only contains the visible area.
186+
*/
187+
public static @Nonnull Visibility withoutFog(@Nonnull Area visibleArea) {
188+
return new Visibility(visibleArea, new Area(), new Area(), new Area());
189+
}
190+
}
144191

145-
/** Map the PlayerView to its visible area. */
146-
private final Map<PlayerView, Area> visibleAreaMap = new HashMap<>();
192+
/** Map the PlayerView to its visible area and exposed area. */
193+
private final Map<PlayerView, Visibility> visibilityMap = new HashMap<>();
147194

148195
// endregion
149196

@@ -169,41 +216,97 @@ public ZoneView(Zone zone) {
169216
new MapToolEventBus().getMainEventBus().register(this);
170217
}
171218

172-
public Area getExposedArea(PlayerView view) {
173-
Area exposed = exposedAreaMap.get(view);
174-
175-
if (exposed == null) {
176-
boolean combinedView =
177-
!isUsingVision()
178-
|| MapTool.isPersonalServer()
179-
|| !MapTool.getServerPolicy().isUseIndividualFOW()
180-
|| view.isGMView();
181-
182-
if (view.isUsingTokenView() || combinedView) {
183-
exposed = zone.getExposedArea(view);
184-
} else {
185-
// Not a token-specific view, but we are using Individual FoW. So we build up all the owned
186-
// tokens' exposed areas to build the soft FoW. Note that not all owned tokens may still
187-
// have sight (so weren't included in the PlayerView), but could still have previously
188-
// exposed areas.
189-
exposed = new Area();
190-
for (Token tok : zone.getTokensForLayers(Zone.Layer::supportsVision)) {
191-
if (!AppUtil.playerOwns(tok)) {
192-
continue;
193-
}
194-
ExposedAreaMetaData meta = zone.getExposedAreaMetaData(tok.getExposedAreaGUID());
195-
Area exposedArea = meta.getExposedAreaHistory();
196-
exposed.add(new Area(exposedArea));
219+
private @Nonnull Area calculateExposedArea(PlayerView view) {
220+
boolean combinedView =
221+
!isUsingVision()
222+
|| MapTool.isPersonalServer()
223+
|| !MapTool.getServerPolicy().isUseIndividualFOW()
224+
|| view.isGMView();
225+
226+
@Nonnull Area exposed;
227+
if (view.isUsingTokenView() || combinedView) {
228+
exposed = zone.getExposedArea(view);
229+
} else {
230+
// Not a token-specific view, but we are using Individual FoW. So we build up all the owned
231+
// tokens' exposed areas to build the soft FoW. Note that not all owned tokens may still
232+
// have sight (so weren't included in the PlayerView), but could still have previously
233+
// exposed areas.
234+
exposed = new Area();
235+
for (Token tok : zone.getTokensForLayers(Zone.Layer::supportsVision)) {
236+
if (!AppUtil.playerOwns(tok)) {
237+
continue;
197238
}
239+
ExposedAreaMetaData meta = zone.getExposedAreaMetaData(tok.getExposedAreaGUID());
240+
Area exposedArea = meta.getExposedAreaHistory();
241+
exposed.add(new Area(exposedArea));
198242
}
199-
200-
exposedAreaMap.put(view, exposed);
201243
}
202244
return exposed;
203245
}
204246

247+
private @Nonnull Area calculateVisibleArea(PlayerView view) {
248+
final var visibleArea = new Area();
249+
getTokensForView(view).map(token -> this.getVisibleArea(token, view)).forEach(visibleArea::add);
250+
return visibleArea;
251+
}
252+
253+
public @Nonnull Visibility getVisibility(PlayerView view) {
254+
return visibilityMap.computeIfAbsent(
255+
view,
256+
view2 -> {
257+
var timer = CodeTimer.get();
258+
var tokenCount = view.isUsingTokenView() ? view.getTokens().size() : 0;
259+
260+
timer.start("ZoneView.getVisibility(%d tokens)-getVisibleArea", tokenCount);
261+
var visibleArea = calculateVisibleArea(view2);
262+
timer.stop("ZoneView.getVisibility(%d tokens)-getVisibleArea", tokenCount);
263+
264+
if (!zone.hasFog()) {
265+
// Exposed and clear areas are meaningless when not using fog of war.
266+
return Visibility.withoutFog(visibleArea);
267+
}
268+
269+
timer.start("ZoneView.getVisibility(%d tokens)-getExposedArea", tokenCount);
270+
var exposedArea = calculateExposedArea(view2);
271+
timer.stop("ZoneView.getVisibility(%d tokens)-getExposedArea", tokenCount);
272+
273+
/*
274+
* Hard FOW is cleared by exposed areas. The exposed area itself has two regions: the
275+
* visible area (rendered clear) and the soft FOW area (rendered translucent). But if
276+
* vision is off, treat the entire exposed area as clear with no soft FOW.
277+
*/
278+
279+
timer.start("ZoneView.getVisibility(%d tokens)-getClearArea", tokenCount);
280+
Area softFogArea;
281+
Area clearArea;
282+
if (isUsingVision()) {
283+
softFogArea = exposedArea;
284+
clearArea = new Area(visibleArea);
285+
clearArea.intersect(softFogArea);
286+
} else {
287+
softFogArea = new Area();
288+
clearArea = exposedArea;
289+
}
290+
timer.stop("ZoneView.getVisibility(%d tokens)-getClearArea", tokenCount);
291+
292+
return new Visibility(visibleArea, exposedArea, softFogArea, clearArea);
293+
});
294+
}
295+
296+
/**
297+
* Gets the exposed area (area without hard FoW0 for the given view.
298+
*
299+
* <p>The results of this method are only meaningful if the zone has fog enabled.
300+
*
301+
* @param view The player view
302+
* @return The exposed area for {@code view}.
303+
*/
304+
public @Nonnull Area getExposedArea(PlayerView view) {
305+
return getVisibility(view).exposedArea();
306+
}
307+
205308
/**
206-
* Calculate the visible area of the view, cache it in visibleAreaMap, and return it
309+
* Gets the visible area of the view, cache it in visibleAreaMap, and return it
207310
*
208311
* <p>The visible area is calculated for each token in the view. The token's visible area is its
209312
* vision obstructed by topology and restricted to the illuminated portions of the map.
@@ -212,15 +315,7 @@ public Area getExposedArea(PlayerView view) {
212315
* @return the visible area
213316
*/
214317
public @Nonnull Area getVisibleArea(PlayerView view) {
215-
return visibleAreaMap.computeIfAbsent(
216-
view,
217-
view2 -> {
218-
final var visibleArea = new Area();
219-
getTokensForView(view2)
220-
.map(token -> this.getVisibleArea(token, view2))
221-
.forEach(visibleArea::add);
222-
return visibleArea;
223-
});
318+
return getVisibility(view).visibleArea();
224319
}
225320

226321
/**
@@ -722,8 +817,8 @@ public Collection<DrawableLight> getDrawableLights(PlayerView view) {
722817
}
723818

724819
/**
725-
* Clear the vision caches (@link #tokenVisionCachePerView}, {@link #visibleAreaMap}), fog cache
726-
* ({@link #exposedAreaMap}), and illumination caches ({@link #illuminationModels}.
820+
* Clear the vision caches (@link #tokenVisionCachePerView}, {@link #visibilityMap}), and
821+
* illumination caches ({@link #illuminationModels}.
727822
*
728823
* <p>Needs to be called whenever topology changes, fog is edited, or map vision settings are
729824
* changed. These are all external factors that directly affect vision and illumination. In the
@@ -739,14 +834,13 @@ public void flush() {
739834

740835
tokenVisionCachePerView.clear();
741836
illuminationsPerView.clear();
742-
exposedAreaMap.clear();
743-
visibleAreaMap.clear();
837+
flushFog();
744838

745839
flushLights();
746840
}
747841

748842
public void flushFog() {
749-
exposedAreaMap.clear();
843+
visibilityMap.clear();
750844
}
751845

752846
private void flushLights() {
@@ -756,8 +850,8 @@ private void flushLights() {
756850

757851
/**
758852
* Flush the ZoneView cache of the token. Remove token from {@link #tokenVisionCachePerView}, and
759-
* {@link #illuminationModels}. Can clear {@link #tokenVisionCachePerView}, {@link
760-
* #visibleAreaMap}, and {@link #exposedAreaMap} depending on the token.
853+
* {@link #illuminationModels}. Can clear {@link #tokenVisionCachePerView}, and {@link
854+
* #visibilityMap} depending on the token.
761855
*
762856
* @param token the token to flush.
763857
*/
@@ -776,14 +870,12 @@ public void flush(Token token) {
776870
contributedPersonalLightsByToken.remove(token.getId());
777871
tokenVisionCachePerView.clear();
778872
illuminationsPerView.clear();
779-
exposedAreaMap.clear();
780-
visibleAreaMap.clear();
873+
flushFog();
781874
drawableLights.clear();
782875
} else if (token.getHasSight()) {
783876
contributedPersonalLightsByToken.remove(token.getId());
784877
illuminationsPerView.clear();
785-
exposedAreaMap.clear();
786-
visibleAreaMap.clear();
878+
flushFog();
787879
drawableLights.clear();
788880
}
789881

@@ -884,16 +976,15 @@ private void onTokensChanged(TokensChanged event) {
884976

885977
/**
886978
* Update {@link #lightSourceMap} with the light sources of the tokens, and clear {@link
887-
* #visibleAreaMap} and {@link #exposedAreaMap} if one of the tokens has sight.
979+
* #visibilityMap} if one of the tokens has sight.
888980
*
889981
* @param tokens the list of tokens
890982
*/
891983
private void processTokenAddChangeEvent(List<Token> tokens) {
892984
updateLightSourcesFromTokens(tokens);
893985

894986
if (tokens.stream().anyMatch(Token::getHasSight)) {
895-
exposedAreaMap.clear();
896-
visibleAreaMap.clear();
987+
flushFog();
897988
}
898989

899990
if (tokens.stream().anyMatch(Token::hasAnyMaskTopology)) {

src/main/java/net/rptools/maptool/client/ui/zone/gdx/GdxRenderer.java

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -855,24 +855,9 @@ private void renderFog(PlayerView view) {
855855

856856
var zoneView = zoneCache.getZoneView();
857857

858-
timer.start("renderFog-visibleArea");
859-
Area visibleArea = zoneView.getVisibleArea(view);
860-
timer.stop("renderFog-visibleArea");
861-
862-
timer.start("renderFog-combined(%d)", view.isUsingTokenView() ? view.getTokens().size() : 0);
863-
Area exposedArea = zoneView.getExposedArea(view);
864-
timer.stop("renderFog-combined(%d)", view.isUsingTokenView() ? view.getTokens().size() : 0);
865-
866-
Area softFogArea;
867-
Area clearArea;
868-
if (zoneView.isUsingVision()) {
869-
softFogArea = exposedArea;
870-
clearArea = new Area(visibleArea);
871-
clearArea.intersect(softFogArea);
872-
} else {
873-
softFogArea = new Area();
874-
clearArea = exposedArea;
875-
}
858+
var visibility = zoneView.getVisibility(view);
859+
Area softFogArea = visibility.softFogArea();
860+
Area clearArea = visibility.clearArea();
876861

877862
timer.start("renderFog");
878863
ScreenUtils.clear(Color.CLEAR);

src/main/java/net/rptools/maptool/client/ui/zone/renderer/FogRenderer.java

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -52,33 +52,17 @@ public void render(Graphics2D g, PlayerView view) {
5252
}
5353

5454
private void renderWorld(Graphics2D worldG, PlayerView view) {
55-
var timer = CodeTimer.get();
56-
57-
/* The tricky thing in this method is that the areas we have (exposed, visible) are the areas
55+
/*
56+
* The tricky thing in this method is that the areas we have (exposed, visible) are the areas
5857
* where we should _not_ render. So we have to do clipped fills and clears instead of directly
59-
* rendering the areas. */
60-
timer.start("renderFog-getVisibleArea");
61-
Area visibleArea = zoneView.getVisibleArea(view);
62-
timer.stop("renderFog-getVisibleArea");
58+
* rendering the areas.
59+
*/
6360

64-
var tokenCount = view.isUsingTokenView() ? view.getTokens().size() : 0;
65-
timer.start("renderFog-getExposedArea(%d)", tokenCount);
66-
Area exposedArea = zoneView.getExposedArea(view);
67-
timer.stop("renderFog-getExposedArea(%d)", tokenCount);
61+
var timer = CodeTimer.get();
6862

69-
// Hard FOW is cleared by exposed areas. The exposed area itself has two regions: the visible
70-
// area (rendered clear) and the soft FOW area (rendered translucent). But if vision is off,
71-
// treat the entire exposed area as visible.
72-
Area softFogArea;
73-
Area clearArea;
74-
if (zoneView.isUsingVision()) {
75-
softFogArea = exposedArea;
76-
clearArea = new Area(visibleArea);
77-
clearArea.intersect(softFogArea);
78-
} else {
79-
softFogArea = new Area();
80-
clearArea = exposedArea;
81-
}
63+
var visibility = zoneView.getVisibility(view);
64+
Area softFogArea = visibility.softFogArea();
65+
Area clearArea = visibility.clearArea();
8266

8367
var originalClip = worldG.getClip();
8468

src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,11 +1181,8 @@ protected void showBlockedMoves(Graphics2D g, PlayerView view, Set<SelectionSet>
11811181
// Regardless of vision settings, no need to render beyond the fog.
11821182
Area clearArea = null;
11831183
if (!view.isGMView()) {
1184-
if (zone.hasFog() && zoneView.isUsingVision()) {
1185-
clearArea = new Area(zoneView.getExposedArea(view));
1186-
clearArea.intersect(zoneView.getVisibleArea(view));
1187-
} else if (zone.hasFog()) {
1188-
clearArea = zoneView.getExposedArea(view);
1184+
if (zone.hasFog()) {
1185+
clearArea = zoneView.getVisibility(view).clearArea();
11891186
} else if (zoneView.isUsingVision()) {
11901187
clearArea = zoneView.getVisibleArea(view);
11911188
}

src/main/java/net/rptools/maptool/model/Zone.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1316,7 +1316,7 @@ public long getCreationTime() {
13161316
* @param view holds whether or not tokens are selected
13171317
* @return the exposed area
13181318
*/
1319-
public Area getExposedArea(PlayerView view) {
1319+
public @Nonnull Area getExposedArea(PlayerView view) {
13201320
Area combined = new Area(exposedArea);
13211321

13221322
// Don't need to worry about StrictTokenOwnership since the PlayerView only contains tokens we

0 commit comments

Comments
 (0)