Убрать аллокации из игрового hot path
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/** Allocation-free critical-state comparison after a level has been configured. */
|
||||
final class AccessibilityStateCache {
|
||||
private List<String> signalIds = Collections.emptyList();
|
||||
private GameLanguage language;
|
||||
private int activeSignals = Integer.MIN_VALUE;
|
||||
private int openDoors = Integer.MIN_VALUE;
|
||||
private int totalDoors = Integer.MIN_VALUE;
|
||||
private boolean playerAlive;
|
||||
private String statusText;
|
||||
|
||||
void configureSignals(List<String> sourceIds) {
|
||||
ArrayList<String> unique = new ArrayList<>();
|
||||
for (String signalId : sourceIds) {
|
||||
if (!unique.contains(signalId)) unique.add(signalId);
|
||||
}
|
||||
signalIds = Collections.unmodifiableList(unique);
|
||||
language = null;
|
||||
activeSignals = Integer.MIN_VALUE;
|
||||
openDoors = Integer.MIN_VALUE;
|
||||
totalDoors = Integer.MIN_VALUE;
|
||||
statusText = null;
|
||||
}
|
||||
|
||||
List<String> signalIds() {
|
||||
return signalIds;
|
||||
}
|
||||
|
||||
boolean update(GameStrings strings, int updatedActiveSignals, int updatedOpenDoors,
|
||||
int updatedTotalDoors, boolean updatedPlayerAlive) {
|
||||
GameLanguage updatedLanguage = strings.language();
|
||||
if (statusText != null
|
||||
&& language == updatedLanguage
|
||||
&& activeSignals == updatedActiveSignals
|
||||
&& openDoors == updatedOpenDoors
|
||||
&& totalDoors == updatedTotalDoors
|
||||
&& playerAlive == updatedPlayerAlive) {
|
||||
return false;
|
||||
}
|
||||
language = updatedLanguage;
|
||||
activeSignals = updatedActiveSignals;
|
||||
openDoors = updatedOpenDoors;
|
||||
totalDoors = updatedTotalDoors;
|
||||
playerAlive = updatedPlayerAlive;
|
||||
statusText = AccessibilityStatus.describe(strings, activeSignals, signalIds.size(),
|
||||
openDoors, totalDoors, playerAlive);
|
||||
return true;
|
||||
}
|
||||
|
||||
String statusText() {
|
||||
return statusText;
|
||||
}
|
||||
}
|
||||
@@ -18,12 +18,10 @@ import android.view.accessibility.AccessibilityEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A dependency-free 2D puzzle-platformer renderer and deterministic physics loop.
|
||||
@@ -175,6 +173,8 @@ public final class GameView extends View implements Runnable {
|
||||
private final Map<String, Boolean> toggleSignals = new HashMap<>();
|
||||
private final Map<String, Float> timedSignals = new HashMap<>();
|
||||
private final Map<String, Boolean> previousSwitchContact = new HashMap<>();
|
||||
private final AccessibilityStateCache accessibilityStateCache =
|
||||
new AccessibilityStateCache();
|
||||
|
||||
private Screen screen = Screen.LEVELS;
|
||||
private LevelDefinition level;
|
||||
@@ -199,7 +199,16 @@ public final class GameView extends View implements Runnable {
|
||||
private GameLanguage language;
|
||||
private GameStrings strings;
|
||||
private AccessibilitySettings accessibilitySettings;
|
||||
private String lastAccessibilityStateKey = "";
|
||||
private String cachedLevelHeader = "";
|
||||
private String cachedLevelHint = "";
|
||||
private float cachedLevelHintSize = 25f;
|
||||
private int cachedHudLevelNumber = -1;
|
||||
private GameLanguage cachedHudLanguage;
|
||||
private float cachedHudTextScale = Float.NaN;
|
||||
private String cachedHudStats = "";
|
||||
private long cachedHudStatsSecond = Long.MIN_VALUE;
|
||||
private int cachedHudStatsDeaths = Integer.MIN_VALUE;
|
||||
private GameLanguage cachedHudStatsLanguage;
|
||||
|
||||
public GameView(Context context) {
|
||||
super(context);
|
||||
@@ -289,6 +298,8 @@ public final class GameView extends View implements Runnable {
|
||||
timedSignals.clear();
|
||||
previousSwitchContact.clear();
|
||||
completionResult = null;
|
||||
configureAccessibilitySignals();
|
||||
invalidateHudTextCache();
|
||||
for (LevelDefinition.Platform item : level.platforms) platformStates.add(new PlatformState(item));
|
||||
spawnPlayer();
|
||||
lastFrameNanos = System.nanoTime();
|
||||
@@ -978,20 +989,19 @@ public final class GameView extends View implements Runnable {
|
||||
}
|
||||
|
||||
private void drawHud(Canvas canvas) {
|
||||
ensureHudTextCache();
|
||||
paint.setColor(Color.argb(178, 15, 20, 34));
|
||||
canvas.drawRoundRect(new RectF(28, 26, 940, 138), 24, 24, paint);
|
||||
canvas.save();
|
||||
canvas.clipRect(40, 30, 925, 135);
|
||||
drawText(canvas, String.format(Locale.US, "%02d %s", level.number,
|
||||
strings.levelTitle(level.number)), 56, 72, 34,
|
||||
Color.rgb(248, 220, 151), false);
|
||||
drawFittedText(canvas, strings.levelHint(level.number), 56, 116, 25, 16,
|
||||
850, Color.rgb(231, 231, 235), false);
|
||||
drawText(canvas, cachedLevelHeader, 56, 72, 34, Color.rgb(248, 220, 151),
|
||||
false);
|
||||
drawText(canvas, cachedLevelHint, 56, 116, cachedLevelHintSize,
|
||||
Color.rgb(231, 231, 235), false);
|
||||
canvas.restore();
|
||||
canvas.save();
|
||||
canvas.clipRect(990, 28, 1590, 130);
|
||||
drawText(canvas, strings.hudStats(deaths, (long) (levelElapsed * 1000)),
|
||||
1010, 82, 31, Color.WHITE, false);
|
||||
drawText(canvas, hudStatsText(), 1010, 82, 31, Color.WHITE, false);
|
||||
canvas.restore();
|
||||
paint.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? Color.BLACK : Color.argb(178, 15, 20, 34));
|
||||
@@ -1743,6 +1753,7 @@ public final class GameView extends View implements Runnable {
|
||||
language = updated == null ? GameLanguage.EN : updated;
|
||||
strings = GameStrings.forLanguage(language);
|
||||
languageStore.save(language);
|
||||
invalidateHudTextCache();
|
||||
refreshAccessibilityDescription(false);
|
||||
invalidate();
|
||||
}
|
||||
@@ -1824,20 +1835,76 @@ public final class GameView extends View implements Runnable {
|
||||
private void updateAccessibilitySettings(AccessibilitySettings updated) {
|
||||
accessibilitySettings = updated;
|
||||
accessibilitySettingsStore.save(updated);
|
||||
invalidateHudTextCache();
|
||||
refreshAccessibilityDescription(false);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
private String criticalStatusText() {
|
||||
if (level == null) return AccessibilityStatus.describe(strings, 0, 0, 0, 0, true);
|
||||
Set<String> signalIds = new HashSet<>();
|
||||
int activeSignals = 0;
|
||||
private void invalidateHudTextCache() {
|
||||
cachedHudLevelNumber = -1;
|
||||
cachedHudLanguage = null;
|
||||
cachedHudTextScale = Float.NaN;
|
||||
cachedHudStatsSecond = Long.MIN_VALUE;
|
||||
cachedHudStatsDeaths = Integer.MIN_VALUE;
|
||||
cachedHudStatsLanguage = null;
|
||||
}
|
||||
|
||||
private void ensureHudTextCache() {
|
||||
if (level == null) return;
|
||||
float textScale = accessibilitySettings.textScale();
|
||||
if (cachedHudLevelNumber == level.number
|
||||
&& cachedHudLanguage == language
|
||||
&& Float.compare(cachedHudTextScale, textScale) == 0) {
|
||||
return;
|
||||
}
|
||||
cachedHudLevelNumber = level.number;
|
||||
cachedHudLanguage = language;
|
||||
cachedHudTextScale = textScale;
|
||||
cachedLevelHeader = String.format(Locale.US, "%02d %s", level.number,
|
||||
strings.levelTitle(level.number));
|
||||
cachedLevelHint = strings.levelHint(level.number);
|
||||
cachedLevelHintSize = fittedTextSize(cachedLevelHint, 25f, 16f, 850f, false);
|
||||
}
|
||||
|
||||
private String hudStatsText() {
|
||||
long displayedSecond = (long) (levelElapsed * 1000f) / 1000L;
|
||||
if (cachedHudStatsSecond != displayedSecond
|
||||
|| cachedHudStatsDeaths != deaths
|
||||
|| cachedHudStatsLanguage != language) {
|
||||
cachedHudStatsSecond = displayedSecond;
|
||||
cachedHudStatsDeaths = deaths;
|
||||
cachedHudStatsLanguage = language;
|
||||
cachedHudStats = strings.hudStats(deaths, displayedSecond * 1000L);
|
||||
}
|
||||
return cachedHudStats;
|
||||
}
|
||||
|
||||
private void configureAccessibilitySignals() {
|
||||
ArrayList<String> signalIds = new ArrayList<>();
|
||||
if (level != null) {
|
||||
for (LevelDefinition.Switch item : level.switches) signalIds.add(item.signalId);
|
||||
for (String signalId : signalIds) if (signalActive(signalId)) activeSignals++;
|
||||
}
|
||||
accessibilityStateCache.configureSignals(signalIds);
|
||||
}
|
||||
|
||||
private boolean updateCriticalStatusCache() {
|
||||
int activeSignals = 0;
|
||||
if (level != null) {
|
||||
for (String signalId : accessibilityStateCache.signalIds()) {
|
||||
if (signalActive(signalId)) activeSignals++;
|
||||
}
|
||||
}
|
||||
int openDoors = 0;
|
||||
if (level != null) {
|
||||
for (LevelDefinition.Door door : level.doors) if (doorOpen(door)) openDoors++;
|
||||
return AccessibilityStatus.describe(strings, activeSignals, signalIds.size(), openDoors,
|
||||
level.doors.size(), player.alive);
|
||||
}
|
||||
return accessibilityStateCache.update(strings, activeSignals, openDoors,
|
||||
level == null ? 0 : level.doors.size(), level == null || player.alive);
|
||||
}
|
||||
|
||||
private String criticalStatusText() {
|
||||
if (accessibilityStateCache.statusText() == null) updateCriticalStatusCache();
|
||||
return accessibilityStateCache.statusText();
|
||||
}
|
||||
|
||||
private String currentAccessibilityDescription() {
|
||||
@@ -1859,23 +1926,16 @@ public final class GameView extends View implements Runnable {
|
||||
userPaused);
|
||||
}
|
||||
|
||||
private String accessibilityStateKey() {
|
||||
return screen + "|" + userPaused + "|" + deaths + "|" + criticalStatusText()
|
||||
+ "|" + controlSettings.controlSizeLabel()
|
||||
+ "|" + controlSettings.controlOpacityLabel()
|
||||
+ "|" + controlSettings.hapticsEnabled()
|
||||
+ "|" + accessibilitySettings.textSizeLabel()
|
||||
+ "|" + accessibilitySettings.highContrastEnabled()
|
||||
+ "|" + language;
|
||||
}
|
||||
|
||||
private void refreshAccessibilityDescriptionIfStateChanged() {
|
||||
String stateKey = accessibilityStateKey();
|
||||
if (!stateKey.equals(lastAccessibilityStateKey)) refreshAccessibilityDescription(false);
|
||||
if (updateCriticalStatusCache()) applyAccessibilityDescription(false);
|
||||
}
|
||||
|
||||
private void refreshAccessibilityDescription(boolean announce) {
|
||||
lastAccessibilityStateKey = accessibilityStateKey();
|
||||
updateCriticalStatusCache();
|
||||
applyAccessibilityDescription(announce);
|
||||
}
|
||||
|
||||
private void applyAccessibilityDescription(boolean announce) {
|
||||
setContentDescription(currentAccessibilityDescription());
|
||||
if (announce && isAttachedToWindow()) {
|
||||
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public final class AccessibilityStateCacheTest {
|
||||
@Test
|
||||
public void unchangedFramesReuseOneStatusAndOneDeduplicatedSignalSet() {
|
||||
AccessibilityStateCache cache = new AccessibilityStateCache();
|
||||
cache.configureSignals(Arrays.asList("lift", "lift", "door"));
|
||||
|
||||
assertEquals(Arrays.asList("lift", "door"), cache.signalIds());
|
||||
assertTrue(cache.update(GameStrings.forLanguage(GameLanguage.EN), 1, 0, 1, true));
|
||||
String first = cache.statusText();
|
||||
|
||||
for (int frame = 0; frame < 600; frame++) {
|
||||
assertFalse(cache.update(GameStrings.forLanguage(GameLanguage.EN), 1, 0, 1, true));
|
||||
assertSame(first, cache.statusText());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void criticalStateAndLanguageChangesInvalidateExactlyOnce() {
|
||||
AccessibilityStateCache cache = new AccessibilityStateCache();
|
||||
cache.configureSignals(Arrays.asList("plate"));
|
||||
cache.update(GameStrings.forLanguage(GameLanguage.EN), 0, 0, 1, true);
|
||||
String initial = cache.statusText();
|
||||
|
||||
assertTrue(cache.update(GameStrings.forLanguage(GameLanguage.EN), 1, 1, 1, false));
|
||||
String changed = cache.statusText();
|
||||
assertNotSame(initial, changed);
|
||||
assertTrue(changed.contains("Signals 1/1"));
|
||||
assertTrue(changed.contains("Volunteer respawning"));
|
||||
assertFalse(cache.update(GameStrings.forLanguage(GameLanguage.EN), 1, 1, 1, false));
|
||||
|
||||
assertTrue(cache.update(GameStrings.forLanguage(GameLanguage.RU), 1, 1, 1, false));
|
||||
assertTrue(cache.statusText().contains("Сигналы 1/1"));
|
||||
assertFalse(cache.update(GameStrings.forLanguage(GameLanguage.RU), 1, 1, 1, false));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user