Убрать аллокации из игрового 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.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A dependency-free 2D puzzle-platformer renderer and deterministic physics loop.
|
* 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, Boolean> toggleSignals = new HashMap<>();
|
||||||
private final Map<String, Float> timedSignals = new HashMap<>();
|
private final Map<String, Float> timedSignals = new HashMap<>();
|
||||||
private final Map<String, Boolean> previousSwitchContact = new HashMap<>();
|
private final Map<String, Boolean> previousSwitchContact = new HashMap<>();
|
||||||
|
private final AccessibilityStateCache accessibilityStateCache =
|
||||||
|
new AccessibilityStateCache();
|
||||||
|
|
||||||
private Screen screen = Screen.LEVELS;
|
private Screen screen = Screen.LEVELS;
|
||||||
private LevelDefinition level;
|
private LevelDefinition level;
|
||||||
@@ -199,7 +199,16 @@ public final class GameView extends View implements Runnable {
|
|||||||
private GameLanguage language;
|
private GameLanguage language;
|
||||||
private GameStrings strings;
|
private GameStrings strings;
|
||||||
private AccessibilitySettings accessibilitySettings;
|
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) {
|
public GameView(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
@@ -289,6 +298,8 @@ public final class GameView extends View implements Runnable {
|
|||||||
timedSignals.clear();
|
timedSignals.clear();
|
||||||
previousSwitchContact.clear();
|
previousSwitchContact.clear();
|
||||||
completionResult = null;
|
completionResult = null;
|
||||||
|
configureAccessibilitySignals();
|
||||||
|
invalidateHudTextCache();
|
||||||
for (LevelDefinition.Platform item : level.platforms) platformStates.add(new PlatformState(item));
|
for (LevelDefinition.Platform item : level.platforms) platformStates.add(new PlatformState(item));
|
||||||
spawnPlayer();
|
spawnPlayer();
|
||||||
lastFrameNanos = System.nanoTime();
|
lastFrameNanos = System.nanoTime();
|
||||||
@@ -978,20 +989,19 @@ public final class GameView extends View implements Runnable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void drawHud(Canvas canvas) {
|
private void drawHud(Canvas canvas) {
|
||||||
|
ensureHudTextCache();
|
||||||
paint.setColor(Color.argb(178, 15, 20, 34));
|
paint.setColor(Color.argb(178, 15, 20, 34));
|
||||||
canvas.drawRoundRect(new RectF(28, 26, 940, 138), 24, 24, paint);
|
canvas.drawRoundRect(new RectF(28, 26, 940, 138), 24, 24, paint);
|
||||||
canvas.save();
|
canvas.save();
|
||||||
canvas.clipRect(40, 30, 925, 135);
|
canvas.clipRect(40, 30, 925, 135);
|
||||||
drawText(canvas, String.format(Locale.US, "%02d %s", level.number,
|
drawText(canvas, cachedLevelHeader, 56, 72, 34, Color.rgb(248, 220, 151),
|
||||||
strings.levelTitle(level.number)), 56, 72, 34,
|
false);
|
||||||
Color.rgb(248, 220, 151), false);
|
drawText(canvas, cachedLevelHint, 56, 116, cachedLevelHintSize,
|
||||||
drawFittedText(canvas, strings.levelHint(level.number), 56, 116, 25, 16,
|
Color.rgb(231, 231, 235), false);
|
||||||
850, Color.rgb(231, 231, 235), false);
|
|
||||||
canvas.restore();
|
canvas.restore();
|
||||||
canvas.save();
|
canvas.save();
|
||||||
canvas.clipRect(990, 28, 1590, 130);
|
canvas.clipRect(990, 28, 1590, 130);
|
||||||
drawText(canvas, strings.hudStats(deaths, (long) (levelElapsed * 1000)),
|
drawText(canvas, hudStatsText(), 1010, 82, 31, Color.WHITE, false);
|
||||||
1010, 82, 31, Color.WHITE, false);
|
|
||||||
canvas.restore();
|
canvas.restore();
|
||||||
paint.setColor(accessibilitySettings.highContrastEnabled()
|
paint.setColor(accessibilitySettings.highContrastEnabled()
|
||||||
? Color.BLACK : Color.argb(178, 15, 20, 34));
|
? 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;
|
language = updated == null ? GameLanguage.EN : updated;
|
||||||
strings = GameStrings.forLanguage(language);
|
strings = GameStrings.forLanguage(language);
|
||||||
languageStore.save(language);
|
languageStore.save(language);
|
||||||
|
invalidateHudTextCache();
|
||||||
refreshAccessibilityDescription(false);
|
refreshAccessibilityDescription(false);
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
@@ -1824,20 +1835,76 @@ public final class GameView extends View implements Runnable {
|
|||||||
private void updateAccessibilitySettings(AccessibilitySettings updated) {
|
private void updateAccessibilitySettings(AccessibilitySettings updated) {
|
||||||
accessibilitySettings = updated;
|
accessibilitySettings = updated;
|
||||||
accessibilitySettingsStore.save(updated);
|
accessibilitySettingsStore.save(updated);
|
||||||
|
invalidateHudTextCache();
|
||||||
refreshAccessibilityDescription(false);
|
refreshAccessibilityDescription(false);
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String criticalStatusText() {
|
private void invalidateHudTextCache() {
|
||||||
if (level == null) return AccessibilityStatus.describe(strings, 0, 0, 0, 0, true);
|
cachedHudLevelNumber = -1;
|
||||||
Set<String> signalIds = new HashSet<>();
|
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);
|
||||||
|
}
|
||||||
|
accessibilityStateCache.configureSignals(signalIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean updateCriticalStatusCache() {
|
||||||
int activeSignals = 0;
|
int activeSignals = 0;
|
||||||
for (LevelDefinition.Switch item : level.switches) signalIds.add(item.signalId);
|
if (level != null) {
|
||||||
for (String signalId : signalIds) if (signalActive(signalId)) activeSignals++;
|
for (String signalId : accessibilityStateCache.signalIds()) {
|
||||||
|
if (signalActive(signalId)) activeSignals++;
|
||||||
|
}
|
||||||
|
}
|
||||||
int openDoors = 0;
|
int openDoors = 0;
|
||||||
for (LevelDefinition.Door door : level.doors) if (doorOpen(door)) openDoors++;
|
if (level != null) {
|
||||||
return AccessibilityStatus.describe(strings, activeSignals, signalIds.size(), openDoors,
|
for (LevelDefinition.Door door : level.doors) if (doorOpen(door)) 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() {
|
private String currentAccessibilityDescription() {
|
||||||
@@ -1859,23 +1926,16 @@ public final class GameView extends View implements Runnable {
|
|||||||
userPaused);
|
userPaused);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String accessibilityStateKey() {
|
|
||||||
return screen + "|" + userPaused + "|" + deaths + "|" + criticalStatusText()
|
|
||||||
+ "|" + controlSettings.controlSizeLabel()
|
|
||||||
+ "|" + controlSettings.controlOpacityLabel()
|
|
||||||
+ "|" + controlSettings.hapticsEnabled()
|
|
||||||
+ "|" + accessibilitySettings.textSizeLabel()
|
|
||||||
+ "|" + accessibilitySettings.highContrastEnabled()
|
|
||||||
+ "|" + language;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void refreshAccessibilityDescriptionIfStateChanged() {
|
private void refreshAccessibilityDescriptionIfStateChanged() {
|
||||||
String stateKey = accessibilityStateKey();
|
if (updateCriticalStatusCache()) applyAccessibilityDescription(false);
|
||||||
if (!stateKey.equals(lastAccessibilityStateKey)) refreshAccessibilityDescription(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshAccessibilityDescription(boolean announce) {
|
private void refreshAccessibilityDescription(boolean announce) {
|
||||||
lastAccessibilityStateKey = accessibilityStateKey();
|
updateCriticalStatusCache();
|
||||||
|
applyAccessibilityDescription(announce);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyAccessibilityDescription(boolean announce) {
|
||||||
setContentDescription(currentAccessibilityDescription());
|
setContentDescription(currentAccessibilityDescription());
|
||||||
if (announce && isAttachedToWindow()) {
|
if (announce && isAttachedToWindow()) {
|
||||||
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
|
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