Убрать аллокации из игрового hot path

This commit is contained in:
2026-08-18 17:20:04 +03:00
parent de3b9485c1
commit 88a1ba9b34
3 changed files with 196 additions and 31 deletions
@@ -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<>();
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);
}
accessibilityStateCache.configureSignals(signalIds);
}
private boolean updateCriticalStatusCache() {
int activeSignals = 0;
for (LevelDefinition.Switch item : level.switches) signalIds.add(item.signalId);
for (String signalId : signalIds) if (signalActive(signalId)) activeSignals++;
if (level != null) {
for (String signalId : accessibilityStateCache.signalIds()) {
if (signalActive(signalId)) activeSignals++;
}
}
int openDoors = 0;
for (LevelDefinition.Door door : level.doors) if (doorOpen(door)) openDoors++;
return AccessibilityStatus.describe(strings, activeSignals, signalIds.size(), openDoors,
level.doors.size(), player.alive);
if (level != null) {
for (LevelDefinition.Door door : level.doors) if (doorOpen(door)) openDoors++;
}
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);