Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
1a93b7d4a2
|
|||
|
7cb3f25c74
|
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:wolf_3d_dart/src/engine/save/game_session_snapshot.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ class Player {
|
|||||||
|
|
||||||
// Classic face animation (UpdateFace/FACETICS random glance frames)
|
// Classic face animation (UpdateFace/FACETICS random glance frames)
|
||||||
math.Random _faceRng = math.Random(0);
|
math.Random _faceRng = math.Random(0);
|
||||||
|
int _faceSeed = 0;
|
||||||
int _faceFrame = 0;
|
int _faceFrame = 0;
|
||||||
double _faceCountTics = 0.0;
|
double _faceCountTics = 0.0;
|
||||||
int _nextFaceChangeThreshold = 0;
|
int _nextFaceChangeThreshold = 0;
|
||||||
@@ -81,6 +83,7 @@ class Player {
|
|||||||
int get hudFaceFrame => _faceFrame;
|
int get hudFaceFrame => _faceFrame;
|
||||||
|
|
||||||
void setHudFaceAnimationSeed(int seed) {
|
void setHudFaceAnimationSeed(int seed) {
|
||||||
|
_faceSeed = seed;
|
||||||
_faceRng = math.Random(seed);
|
_faceRng = math.Random(seed);
|
||||||
_faceFrame = 0;
|
_faceFrame = 0;
|
||||||
_faceCountTics = 0.0;
|
_faceCountTics = 0.0;
|
||||||
@@ -93,6 +96,99 @@ class Player {
|
|||||||
_godModeFaceEnabled = enabled;
|
_godModeFaceEnabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PlayerSaveState toSaveState() {
|
||||||
|
final Map<WeaponType, WeaponSaveState> weaponStates =
|
||||||
|
<WeaponType, WeaponSaveState>{};
|
||||||
|
for (final MapEntry<WeaponType, Weapon?> entry in weapons.entries) {
|
||||||
|
final Weapon? weapon = entry.value;
|
||||||
|
if (weapon == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
weaponStates[entry.key] = weapon.toSaveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
return PlayerSaveState(
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
angle: angle,
|
||||||
|
health: health,
|
||||||
|
ammo: ammo,
|
||||||
|
score: score,
|
||||||
|
lives: lives,
|
||||||
|
damageFlash: damageFlash,
|
||||||
|
bonusFlash: bonusFlash,
|
||||||
|
chaingunPickupFaceMsRemaining: _chaingunPickupFaceMsRemaining,
|
||||||
|
mutantDeathFaceActive: _mutantDeathFaceActive,
|
||||||
|
godModeFaceEnabled: _godModeFaceEnabled,
|
||||||
|
faceSeed: _faceSeed,
|
||||||
|
faceFrame: _faceFrame,
|
||||||
|
faceCountTics: _faceCountTics,
|
||||||
|
nextFaceChangeThreshold: _nextFaceChangeThreshold,
|
||||||
|
hasGoldKey: hasGoldKey,
|
||||||
|
hasSilverKey: hasSilverKey,
|
||||||
|
hasMachineGun: hasMachineGun,
|
||||||
|
hasChainGun: hasChainGun,
|
||||||
|
currentWeaponType: currentWeapon.type,
|
||||||
|
weaponStates: weaponStates,
|
||||||
|
switchStateIndex: switchState.index,
|
||||||
|
pendingWeaponType: pendingWeaponType,
|
||||||
|
weaponAnimOffset: weaponAnimOffset,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void restoreFromSaveState(PlayerSaveState saveState) {
|
||||||
|
x = saveState.x;
|
||||||
|
y = saveState.y;
|
||||||
|
angle = saveState.angle;
|
||||||
|
health = saveState.health;
|
||||||
|
ammo = saveState.ammo;
|
||||||
|
score = saveState.score;
|
||||||
|
lives = saveState.lives;
|
||||||
|
damageFlash = saveState.damageFlash;
|
||||||
|
bonusFlash = saveState.bonusFlash;
|
||||||
|
_chaingunPickupFaceMsRemaining = saveState.chaingunPickupFaceMsRemaining;
|
||||||
|
_mutantDeathFaceActive = saveState.mutantDeathFaceActive;
|
||||||
|
_godModeFaceEnabled = saveState.godModeFaceEnabled;
|
||||||
|
_faceSeed = saveState.faceSeed;
|
||||||
|
_faceRng = math.Random(_faceSeed);
|
||||||
|
_faceFrame = saveState.faceFrame;
|
||||||
|
_faceCountTics = saveState.faceCountTics;
|
||||||
|
_nextFaceChangeThreshold = saveState.nextFaceChangeThreshold;
|
||||||
|
hasGoldKey = saveState.hasGoldKey;
|
||||||
|
hasSilverKey = saveState.hasSilverKey;
|
||||||
|
hasMachineGun = saveState.hasMachineGun;
|
||||||
|
hasChainGun = saveState.hasChainGun;
|
||||||
|
switchState = WeaponSwitchState.values[saveState.switchStateIndex];
|
||||||
|
pendingWeaponType = saveState.pendingWeaponType;
|
||||||
|
weaponAnimOffset = saveState.weaponAnimOffset;
|
||||||
|
|
||||||
|
weapons.updateAll((_, _) => null);
|
||||||
|
for (final MapEntry<WeaponType, WeaponSaveState> entry
|
||||||
|
in saveState.weaponStates.entries) {
|
||||||
|
final weapon = _createWeapon(entry.key);
|
||||||
|
weapon.restoreFromSaveState(entry.value);
|
||||||
|
weapons[entry.key] = weapon;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentWeapon =
|
||||||
|
weapons[saveState.currentWeaponType] ??
|
||||||
|
_createWeapon(saveState.currentWeaponType);
|
||||||
|
final WeaponSaveState? currentWeaponState =
|
||||||
|
saveState.weaponStates[saveState.currentWeaponType];
|
||||||
|
if (currentWeaponState != null) {
|
||||||
|
currentWeapon.restoreFromSaveState(currentWeaponState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Weapon _createWeapon(WeaponType weaponType) {
|
||||||
|
return switch (weaponType) {
|
||||||
|
WeaponType.knife => Knife(),
|
||||||
|
WeaponType.pistol => Pistol(),
|
||||||
|
WeaponType.machineGun => MachineGun(),
|
||||||
|
WeaponType.chainGun => ChainGun(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// --- General Update ---
|
// --- General Update ---
|
||||||
|
|
||||||
void tick(Duration elapsed) {
|
void tick(Duration elapsed) {
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:wolf_3d_dart/src/entities/entities/door.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/entities/entities/weapon/weapon.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/entities/entity.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
|
class WeaponSaveState {
|
||||||
|
const WeaponSaveState({
|
||||||
|
required this.type,
|
||||||
|
required this.state,
|
||||||
|
required this.frameIndex,
|
||||||
|
required this.lastFrameTime,
|
||||||
|
required this.triggerReleased,
|
||||||
|
});
|
||||||
|
|
||||||
|
final WeaponType type;
|
||||||
|
final WeaponState state;
|
||||||
|
final int frameIndex;
|
||||||
|
final int lastFrameTime;
|
||||||
|
final bool triggerReleased;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlayerSaveState {
|
||||||
|
const PlayerSaveState({
|
||||||
|
required this.x,
|
||||||
|
required this.y,
|
||||||
|
required this.angle,
|
||||||
|
required this.health,
|
||||||
|
required this.ammo,
|
||||||
|
required this.score,
|
||||||
|
required this.lives,
|
||||||
|
required this.damageFlash,
|
||||||
|
required this.bonusFlash,
|
||||||
|
required this.chaingunPickupFaceMsRemaining,
|
||||||
|
required this.mutantDeathFaceActive,
|
||||||
|
required this.godModeFaceEnabled,
|
||||||
|
required this.faceSeed,
|
||||||
|
required this.faceFrame,
|
||||||
|
required this.faceCountTics,
|
||||||
|
required this.nextFaceChangeThreshold,
|
||||||
|
required this.hasGoldKey,
|
||||||
|
required this.hasSilverKey,
|
||||||
|
required this.hasMachineGun,
|
||||||
|
required this.hasChainGun,
|
||||||
|
required this.currentWeaponType,
|
||||||
|
required this.weaponStates,
|
||||||
|
required this.switchStateIndex,
|
||||||
|
required this.pendingWeaponType,
|
||||||
|
required this.weaponAnimOffset,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double x;
|
||||||
|
final double y;
|
||||||
|
final double angle;
|
||||||
|
final int health;
|
||||||
|
final int ammo;
|
||||||
|
final int score;
|
||||||
|
final int lives;
|
||||||
|
final double damageFlash;
|
||||||
|
final double bonusFlash;
|
||||||
|
final int chaingunPickupFaceMsRemaining;
|
||||||
|
final bool mutantDeathFaceActive;
|
||||||
|
final bool godModeFaceEnabled;
|
||||||
|
final int faceSeed;
|
||||||
|
final int faceFrame;
|
||||||
|
final double faceCountTics;
|
||||||
|
final int nextFaceChangeThreshold;
|
||||||
|
final bool hasGoldKey;
|
||||||
|
final bool hasSilverKey;
|
||||||
|
final bool hasMachineGun;
|
||||||
|
final bool hasChainGun;
|
||||||
|
final WeaponType currentWeaponType;
|
||||||
|
final Map<WeaponType, WeaponSaveState> weaponStates;
|
||||||
|
final int switchStateIndex;
|
||||||
|
final WeaponType? pendingWeaponType;
|
||||||
|
final double weaponAnimOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
class EntitySaveState {
|
||||||
|
const EntitySaveState({
|
||||||
|
required this.kind,
|
||||||
|
required this.x,
|
||||||
|
required this.y,
|
||||||
|
required this.spriteIndex,
|
||||||
|
required this.angle,
|
||||||
|
required this.state,
|
||||||
|
required this.mapId,
|
||||||
|
required this.lastActionTime,
|
||||||
|
this.extraData = const <String, Object?>{},
|
||||||
|
});
|
||||||
|
|
||||||
|
final String kind;
|
||||||
|
final double x;
|
||||||
|
final double y;
|
||||||
|
final int spriteIndex;
|
||||||
|
final double angle;
|
||||||
|
final EntityState state;
|
||||||
|
final int mapId;
|
||||||
|
final int lastActionTime;
|
||||||
|
final Map<String, Object?> extraData;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DoorSaveState {
|
||||||
|
const DoorSaveState({
|
||||||
|
required this.x,
|
||||||
|
required this.y,
|
||||||
|
required this.mapId,
|
||||||
|
required this.state,
|
||||||
|
required this.offset,
|
||||||
|
required this.openTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int x;
|
||||||
|
final int y;
|
||||||
|
final int mapId;
|
||||||
|
final DoorState state;
|
||||||
|
final double offset;
|
||||||
|
final int openTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PushwallSaveState {
|
||||||
|
const PushwallSaveState({
|
||||||
|
required this.x,
|
||||||
|
required this.y,
|
||||||
|
required this.mapId,
|
||||||
|
required this.dirX,
|
||||||
|
required this.dirY,
|
||||||
|
required this.offset,
|
||||||
|
required this.tilesMoved,
|
||||||
|
required this.isActive,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int x;
|
||||||
|
final int y;
|
||||||
|
final int mapId;
|
||||||
|
final int dirX;
|
||||||
|
final int dirY;
|
||||||
|
final double offset;
|
||||||
|
final int tilesMoved;
|
||||||
|
final bool isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
class GameSessionSnapshot {
|
||||||
|
const GameSessionSnapshot({
|
||||||
|
required this.currentGameIndex,
|
||||||
|
required this.currentEpisodeIndex,
|
||||||
|
required this.currentLevelIndex,
|
||||||
|
required this.returnLevelIndex,
|
||||||
|
required this.difficulty,
|
||||||
|
required this.timeAliveMs,
|
||||||
|
required this.lastAcousticAlertTime,
|
||||||
|
required this.isMapOverlayVisible,
|
||||||
|
required this.isMenuOverlayVisible,
|
||||||
|
required this.player,
|
||||||
|
required this.currentLevel,
|
||||||
|
required this.areaGrid,
|
||||||
|
required this.areasByPlayer,
|
||||||
|
required this.entities,
|
||||||
|
required this.doors,
|
||||||
|
required this.pushwalls,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int currentGameIndex;
|
||||||
|
final int currentEpisodeIndex;
|
||||||
|
final int currentLevelIndex;
|
||||||
|
final int? returnLevelIndex;
|
||||||
|
final Difficulty difficulty;
|
||||||
|
final int timeAliveMs;
|
||||||
|
final int lastAcousticAlertTime;
|
||||||
|
final bool isMapOverlayVisible;
|
||||||
|
final bool isMenuOverlayVisible;
|
||||||
|
final PlayerSaveState player;
|
||||||
|
final List<List<int>> currentLevel;
|
||||||
|
final List<List<int>> areaGrid;
|
||||||
|
final List<bool> areasByPlayer;
|
||||||
|
final List<EntitySaveState> entities;
|
||||||
|
final List<DoorSaveState> doors;
|
||||||
|
final List<PushwallSaveState> pushwalls;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'dart:developer';
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
|
import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/engine/save/game_session_snapshot.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
||||||
@@ -70,6 +71,12 @@ class WolfEngine {
|
|||||||
List<WolfensteinData> get availableGames =>
|
List<WolfensteinData> get availableGames =>
|
||||||
List.unmodifiable(_availableGames);
|
List.unmodifiable(_availableGames);
|
||||||
|
|
||||||
|
int get currentGameIndex => _currentGameIndex;
|
||||||
|
|
||||||
|
int get currentEpisodeIndex => _currentEpisodeIndex;
|
||||||
|
|
||||||
|
int get currentLevelIndex => _currentLevelIndex;
|
||||||
|
|
||||||
int _currentGameIndex = 0;
|
int _currentGameIndex = 0;
|
||||||
|
|
||||||
/// The currently active game data set.
|
/// The currently active game data set.
|
||||||
@@ -233,6 +240,125 @@ class WolfEngine {
|
|||||||
/// Whether the current gameplay session can be resumed from the main menu.
|
/// Whether the current gameplay session can be resumed from the main menu.
|
||||||
bool get canResumeGame => _hasActiveSession;
|
bool get canResumeGame => _hasActiveSession;
|
||||||
|
|
||||||
|
GameSessionSnapshot captureSaveState() {
|
||||||
|
if (!_hasActiveSession || difficulty == null) {
|
||||||
|
throw StateError('Cannot capture save state without an active session.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return GameSessionSnapshot(
|
||||||
|
currentGameIndex: _currentGameIndex,
|
||||||
|
currentEpisodeIndex: _currentEpisodeIndex,
|
||||||
|
currentLevelIndex: _currentLevelIndex,
|
||||||
|
returnLevelIndex: _returnLevelIndex,
|
||||||
|
difficulty: difficulty!,
|
||||||
|
timeAliveMs: _timeAliveMs,
|
||||||
|
lastAcousticAlertTime: _lastAcousticAlertTime,
|
||||||
|
isMapOverlayVisible: isMapOverlayVisible,
|
||||||
|
isMenuOverlayVisible: _isMenuOverlayVisible,
|
||||||
|
player: player.toSaveState(),
|
||||||
|
currentLevel: _cloneGrid(currentLevel),
|
||||||
|
areaGrid: _cloneGrid(_areaGrid),
|
||||||
|
areasByPlayer: List<bool>.from(_areasByPlayer),
|
||||||
|
entities: entities.map(_captureEntityState).toList(growable: false),
|
||||||
|
doors: doorManager.doors.values
|
||||||
|
.map(
|
||||||
|
(door) => DoorSaveState(
|
||||||
|
x: door.x,
|
||||||
|
y: door.y,
|
||||||
|
mapId: door.mapId,
|
||||||
|
state: door.state,
|
||||||
|
offset: door.offset,
|
||||||
|
openTime: door.openTime,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(growable: false),
|
||||||
|
pushwalls: pushwallManager.pushwalls.values
|
||||||
|
.map(
|
||||||
|
(pushwall) => PushwallSaveState(
|
||||||
|
x: pushwall.x,
|
||||||
|
y: pushwall.y,
|
||||||
|
mapId: pushwall.mapId,
|
||||||
|
dirX: pushwall.dirX,
|
||||||
|
dirY: pushwall.dirY,
|
||||||
|
offset: pushwall.offset,
|
||||||
|
tilesMoved: pushwall.tilesMoved,
|
||||||
|
isActive: identical(pushwallManager.activePushwall, pushwall),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(growable: false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void restoreSaveState(GameSessionSnapshot snapshot) {
|
||||||
|
if (snapshot.currentGameIndex < 0 ||
|
||||||
|
snapshot.currentGameIndex >= _availableGames.length) {
|
||||||
|
throw RangeError(
|
||||||
|
'Snapshot game index ${snapshot.currentGameIndex} is out of range.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentGameIndex = snapshot.currentGameIndex;
|
||||||
|
audio.activeGame = data;
|
||||||
|
onGameSelected?.call(data);
|
||||||
|
|
||||||
|
_currentEpisodeIndex = snapshot.currentEpisodeIndex;
|
||||||
|
_currentLevelIndex = snapshot.currentLevelIndex;
|
||||||
|
_returnLevelIndex = snapshot.returnLevelIndex;
|
||||||
|
difficulty = snapshot.difficulty;
|
||||||
|
_timeAliveMs = snapshot.timeAliveMs;
|
||||||
|
_lastAcousticAlertTime = snapshot.lastAcousticAlertTime;
|
||||||
|
isMapOverlayVisible = snapshot.isMapOverlayVisible;
|
||||||
|
_isMenuOverlayVisible = snapshot.isMenuOverlayVisible;
|
||||||
|
_hasActiveSession = true;
|
||||||
|
|
||||||
|
_loadLevel(preservePlayerState: false);
|
||||||
|
|
||||||
|
currentLevel = _cloneGrid(snapshot.currentLevel);
|
||||||
|
_areaGrid = _cloneGrid(snapshot.areaGrid);
|
||||||
|
_areaCount = snapshot.areasByPlayer.length;
|
||||||
|
_areasByPlayer = List<bool>.from(snapshot.areasByPlayer);
|
||||||
|
_lastPatrolTileByEnemy.clear();
|
||||||
|
|
||||||
|
player.restoreFromSaveState(snapshot.player);
|
||||||
|
|
||||||
|
doorManager.doors.clear();
|
||||||
|
for (final doorState in snapshot.doors) {
|
||||||
|
final door = Door(x: doorState.x, y: doorState.y, mapId: doorState.mapId)
|
||||||
|
..state = doorState.state
|
||||||
|
..offset = doorState.offset
|
||||||
|
..openTime = doorState.openTime;
|
||||||
|
doorManager.doors[((door.y & 0xFFFF) << 16) | (door.x & 0xFFFF)] = door;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushwallManager.pushwalls.clear();
|
||||||
|
pushwallManager.activePushwall = null;
|
||||||
|
for (final pushwallState in snapshot.pushwalls) {
|
||||||
|
final pushwall =
|
||||||
|
Pushwall(
|
||||||
|
pushwallState.x,
|
||||||
|
pushwallState.y,
|
||||||
|
pushwallState.mapId,
|
||||||
|
)
|
||||||
|
..dirX = pushwallState.dirX
|
||||||
|
..dirY = pushwallState.dirY
|
||||||
|
..offset = pushwallState.offset
|
||||||
|
..tilesMoved = pushwallState.tilesMoved;
|
||||||
|
pushwallManager.pushwalls['${pushwall.x},${pushwall.y}'] = pushwall;
|
||||||
|
if (pushwallState.isActive) {
|
||||||
|
pushwallManager.activePushwall = pushwall;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entities = snapshot.entities
|
||||||
|
.map(_restoreEntityState)
|
||||||
|
.whereType<Entity>()
|
||||||
|
.toList(growable: true);
|
||||||
|
|
||||||
|
if (_isMenuOverlayVisible) {
|
||||||
|
menuManager.showMainMenu(hasResumableGame: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Replaces the shared framebuffer when dimensions change.
|
/// Replaces the shared framebuffer when dimensions change.
|
||||||
void setFrameBuffer(int width, int height) {
|
void setFrameBuffer(int width, int height) {
|
||||||
if (width <= 0 || height <= 0) {
|
if (width <= 0 || height <= 0) {
|
||||||
@@ -1332,6 +1458,79 @@ class WolfEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static SpriteMap _cloneGrid(SpriteMap grid) {
|
||||||
|
return List<List<int>>.generate(
|
||||||
|
grid.length,
|
||||||
|
(int y) => List<int>.from(grid[y]),
|
||||||
|
growable: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EntitySaveState _captureEntityState(Entity entity) {
|
||||||
|
if (entity is Enemy) {
|
||||||
|
return entity.toSaveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, Object?> extraData = <String, Object?>{};
|
||||||
|
if (entity is AmmoCollectible) {
|
||||||
|
extraData['ammoAmount'] = entity.ammoAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EntitySaveState(
|
||||||
|
kind: entity.runtimeType.toString(),
|
||||||
|
x: entity.x,
|
||||||
|
y: entity.y,
|
||||||
|
spriteIndex: entity.spriteIndex,
|
||||||
|
angle: entity.angle,
|
||||||
|
state: entity.state,
|
||||||
|
mapId: entity.mapId,
|
||||||
|
lastActionTime: entity.lastActionTime,
|
||||||
|
extraData: extraData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity? _restoreEntityState(EntitySaveState entityState) {
|
||||||
|
final Entity? entity = switch (entityState.kind) {
|
||||||
|
'SmallAmmoCollectible' => SmallAmmoCollectible(
|
||||||
|
x: entityState.x,
|
||||||
|
y: entityState.y,
|
||||||
|
),
|
||||||
|
'AmmoCollectible' => AmmoCollectible(
|
||||||
|
x: entityState.x,
|
||||||
|
y: entityState.y,
|
||||||
|
ammoAmount: (entityState.extraData['ammoAmount'] as num?)?.toInt() ?? 8,
|
||||||
|
),
|
||||||
|
_ => EntityRegistry.spawn(
|
||||||
|
entityState.mapId,
|
||||||
|
entityState.x,
|
||||||
|
entityState.y,
|
||||||
|
difficulty!,
|
||||||
|
data.sprites.length,
|
||||||
|
isSharewareMode: data.version == GameVersion.shareware,
|
||||||
|
registry: data.registry,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (entity == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity is Enemy) {
|
||||||
|
entity.restoreFromSaveState(entityState);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
entity
|
||||||
|
..x = entityState.x
|
||||||
|
..y = entityState.y
|
||||||
|
..spriteIndex = entityState.spriteIndex
|
||||||
|
..angle = entityState.angle
|
||||||
|
..state = entityState.state
|
||||||
|
..mapId = entityState.mapId
|
||||||
|
..lastActionTime = entityState.lastActionTime;
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns true if a tile is empty or contains a door that is sufficiently open.
|
/// Returns true if a tile is empty or contains a door that is sufficiently open.
|
||||||
bool isWalkable(int x, int y) {
|
bool isWalkable(int x, int y) {
|
||||||
// 1. Boundary Guard: Prevent range errors by checking if coordinates are on the map
|
// 1. Boundary Guard: Prevent range errors by checking if coordinates are on the map
|
||||||
|
|||||||
@@ -34,6 +34,25 @@ class Dog extends Enemy {
|
|||||||
health = type.hitPointsFor(difficulty);
|
health = type.hitPointsFor(difficulty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, Object?> exportExtraSaveState() {
|
||||||
|
return <String, Object?>{
|
||||||
|
'dodgeAngleOffset': _dodgeAngleOffset,
|
||||||
|
'dodgeTicTimer': _dodgeTicTimer,
|
||||||
|
'stuckFrames': _stuckFrames,
|
||||||
|
'wasMoving': _wasMoving,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void importExtraSaveState(Map<String, Object?> saveState) {
|
||||||
|
_dodgeAngleOffset =
|
||||||
|
(saveState['dodgeAngleOffset'] as num?)?.toDouble() ?? 0.0;
|
||||||
|
_dodgeTicTimer = (saveState['dodgeTicTimer'] as num?)?.toInt() ?? 0;
|
||||||
|
_stuckFrames = (saveState['stuckFrames'] as num?)?.toInt() ?? 0;
|
||||||
|
_wasMoving = saveState['wasMoving'] as bool? ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
EnemyAnimation animationForState(EntityState state) {
|
EnemyAnimation animationForState(EntityState state) {
|
||||||
return switch (state) {
|
return switch (state) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:wolf_3d_dart/src/engine/save/game_session_snapshot.dart';
|
||||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/dog.dart';
|
import 'package:wolf_3d_dart/src/entities/entities/enemies/dog.dart';
|
||||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart';
|
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart';
|
||||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/guard.dart';
|
import 'package:wolf_3d_dart/src/entities/entities/enemies/guard.dart';
|
||||||
@@ -146,6 +147,78 @@ abstract class Enemy extends Entity {
|
|||||||
int _patrolDirX = 0;
|
int _patrolDirX = 0;
|
||||||
int _patrolDirY = 0;
|
int _patrolDirY = 0;
|
||||||
|
|
||||||
|
EntitySaveState toSaveState() {
|
||||||
|
return EntitySaveState(
|
||||||
|
kind: runtimeType.toString(),
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
spriteIndex: spriteIndex,
|
||||||
|
angle: angle,
|
||||||
|
state: state,
|
||||||
|
mapId: mapId,
|
||||||
|
lastActionTime: lastActionTime,
|
||||||
|
extraData: <String, Object?>{
|
||||||
|
'ticCount': _ticCount,
|
||||||
|
'ticAccumulator': _ticAccumulator,
|
||||||
|
'currentFrame': currentFrame,
|
||||||
|
'health': health,
|
||||||
|
'damage': damage,
|
||||||
|
'isDying': isDying,
|
||||||
|
'hasDroppedItem': hasDroppedItem,
|
||||||
|
'hasPlayedDeathSound': hasPlayedDeathSound,
|
||||||
|
'isAlerted': isAlerted,
|
||||||
|
'reactionTimeMs': reactionTimeMs,
|
||||||
|
'patrolTargetTileX': _patrolTargetTile?.x,
|
||||||
|
'patrolTargetTileY': _patrolTargetTile?.y,
|
||||||
|
'patrolDirX': _patrolDirX,
|
||||||
|
'patrolDirY': _patrolDirY,
|
||||||
|
...exportExtraSaveState(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void restoreFromSaveState(EntitySaveState saveState) {
|
||||||
|
x = saveState.x;
|
||||||
|
y = saveState.y;
|
||||||
|
spriteIndex = saveState.spriteIndex;
|
||||||
|
angle = saveState.angle;
|
||||||
|
state = saveState.state;
|
||||||
|
mapId = saveState.mapId;
|
||||||
|
lastActionTime = saveState.lastActionTime;
|
||||||
|
|
||||||
|
_ticCount = (saveState.extraData['ticCount'] as num?)?.toInt() ?? 0;
|
||||||
|
_ticAccumulator =
|
||||||
|
(saveState.extraData['ticAccumulator'] as num?)?.toDouble() ?? 0.0;
|
||||||
|
currentFrame = (saveState.extraData['currentFrame'] as num?)?.toInt() ?? 0;
|
||||||
|
health = (saveState.extraData['health'] as num?)?.toInt() ?? health;
|
||||||
|
damage = (saveState.extraData['damage'] as num?)?.toInt() ?? damage;
|
||||||
|
isDying = saveState.extraData['isDying'] as bool? ?? false;
|
||||||
|
hasDroppedItem = saveState.extraData['hasDroppedItem'] as bool? ?? false;
|
||||||
|
hasPlayedDeathSound =
|
||||||
|
saveState.extraData['hasPlayedDeathSound'] as bool? ?? false;
|
||||||
|
isAlerted = saveState.extraData['isAlerted'] as bool? ?? false;
|
||||||
|
reactionTimeMs =
|
||||||
|
(saveState.extraData['reactionTimeMs'] as num?)?.toInt() ?? 0;
|
||||||
|
|
||||||
|
final int? patrolTargetTileX =
|
||||||
|
(saveState.extraData['patrolTargetTileX'] as num?)?.toInt();
|
||||||
|
final int? patrolTargetTileY =
|
||||||
|
(saveState.extraData['patrolTargetTileY'] as num?)?.toInt();
|
||||||
|
if (patrolTargetTileX != null && patrolTargetTileY != null) {
|
||||||
|
_patrolTargetTile = (x: patrolTargetTileX, y: patrolTargetTileY);
|
||||||
|
} else {
|
||||||
|
_patrolTargetTile = null;
|
||||||
|
}
|
||||||
|
_patrolDirX = (saveState.extraData['patrolDirX'] as num?)?.toInt() ?? 0;
|
||||||
|
_patrolDirY = (saveState.extraData['patrolDirY'] as num?)?.toInt() ?? 0;
|
||||||
|
|
||||||
|
importExtraSaveState(saveState.extraData);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object?> exportExtraSaveState() => const <String, Object?>{};
|
||||||
|
|
||||||
|
void importExtraSaveState(Map<String, Object?> saveState) {}
|
||||||
|
|
||||||
/// Processes elapsed time and returns true if the enemy's animation frame has completed.
|
/// Processes elapsed time and returns true if the enemy's animation frame has completed.
|
||||||
///
|
///
|
||||||
/// Movement is applied continuously during the frame, but state changes (like deciding
|
/// Movement is applied continuously during the frame, but state changes (like deciding
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:wolf_3d_dart/src/engine/save/game_session_snapshot.dart';
|
||||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart';
|
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart';
|
||||||
import 'package:wolf_3d_dart/src/entities/entity.dart';
|
import 'package:wolf_3d_dart/src/entities/entity.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
@@ -54,6 +55,23 @@ abstract class Weapon {
|
|||||||
_triggerReleased = true;
|
_triggerReleased = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WeaponSaveState toSaveState() {
|
||||||
|
return WeaponSaveState(
|
||||||
|
type: type,
|
||||||
|
state: state,
|
||||||
|
frameIndex: frameIndex,
|
||||||
|
lastFrameTime: lastFrameTime,
|
||||||
|
triggerReleased: _triggerReleased,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void restoreFromSaveState(WeaponSaveState saveState) {
|
||||||
|
state = saveState.state;
|
||||||
|
frameIndex = saveState.frameIndex;
|
||||||
|
lastFrameTime = saveState.lastFrameTime;
|
||||||
|
_triggerReleased = saveState.triggerReleased;
|
||||||
|
}
|
||||||
|
|
||||||
bool fire(int currentTime, {required int currentAmmo}) {
|
bool fire(int currentTime, {required int currentAmmo}) {
|
||||||
if (state == WeaponState.idle && currentAmmo > 0) {
|
if (state == WeaponState.idle && currentAmmo > 0) {
|
||||||
if (!isAutomatic && !_triggerReleased) {
|
if (!isAutomatic && !_triggerReleased) {
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ export 'src/engine/player/player.dart';
|
|||||||
export 'src/engine/player_locomotion_constants.dart';
|
export 'src/engine/player_locomotion_constants.dart';
|
||||||
export 'src/engine/rendering/renderer_settings.dart';
|
export 'src/engine/rendering/renderer_settings.dart';
|
||||||
export 'src/engine/rendering/renderer_settings_persistence.dart';
|
export 'src/engine/rendering/renderer_settings_persistence.dart';
|
||||||
|
export 'src/engine/save/game_session_snapshot.dart';
|
||||||
export 'src/engine/wolf_3d_engine_base.dart';
|
export 'src/engine/wolf_3d_engine_base.dart';
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_input.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test(
|
||||||
|
'captureSaveState and restoreSaveState round-trip live session state',
|
||||||
|
() {
|
||||||
|
final engine = _buildEngine();
|
||||||
|
engine.init();
|
||||||
|
|
||||||
|
engine.player
|
||||||
|
..health = 47
|
||||||
|
..ammo = 33
|
||||||
|
..score = 1200
|
||||||
|
..lives = 5
|
||||||
|
..x = 10.5
|
||||||
|
..y = 11.5
|
||||||
|
..angle = 1.25
|
||||||
|
..hasGoldKey = true
|
||||||
|
..hasMachineGun = true
|
||||||
|
..weapons[WeaponType.machineGun] = MachineGun()
|
||||||
|
..currentWeapon = MachineGun();
|
||||||
|
|
||||||
|
engine.currentLevel[8][8] = 0;
|
||||||
|
engine.currentLevel[5][6] = 98;
|
||||||
|
|
||||||
|
final door = engine.doorManager.doors.values.first
|
||||||
|
..state = DoorState.opening
|
||||||
|
..offset = 0.42
|
||||||
|
..openTime = 1337;
|
||||||
|
|
||||||
|
final pushwall = engine.pushwallManager.pushwalls.values.first
|
||||||
|
..dirX = 1
|
||||||
|
..dirY = 0
|
||||||
|
..offset = 0.5
|
||||||
|
..tilesMoved = 1;
|
||||||
|
engine.pushwallManager.activePushwall = pushwall;
|
||||||
|
|
||||||
|
final guard =
|
||||||
|
EntityRegistry.spawn(
|
||||||
|
MapObject.guardStart,
|
||||||
|
12.5,
|
||||||
|
13.5,
|
||||||
|
Difficulty.medium,
|
||||||
|
engine.data.sprites.length,
|
||||||
|
registry: engine.data.registry,
|
||||||
|
)!
|
||||||
|
as Guard
|
||||||
|
..health = 17
|
||||||
|
..isAlerted = true
|
||||||
|
..state = EntityState.attacking
|
||||||
|
..currentFrame = 2
|
||||||
|
..lastActionTime = 222;
|
||||||
|
|
||||||
|
final droppedAmmo = SmallAmmoCollectible(x: 7.5, y: 9.5)
|
||||||
|
..spriteIndex = 999 % engine.data.sprites.length;
|
||||||
|
|
||||||
|
engine.entities = <Entity>[guard, droppedAmmo];
|
||||||
|
|
||||||
|
final snapshot = engine.captureSaveState();
|
||||||
|
|
||||||
|
engine.player
|
||||||
|
..health = 1
|
||||||
|
..ammo = 0
|
||||||
|
..score = 0
|
||||||
|
..x = 2.5
|
||||||
|
..y = 2.5
|
||||||
|
..hasGoldKey = false;
|
||||||
|
engine.currentLevel[8][8] = 55;
|
||||||
|
door
|
||||||
|
..state = DoorState.closed
|
||||||
|
..offset = 0.0
|
||||||
|
..openTime = 0;
|
||||||
|
engine.pushwallManager.activePushwall = null;
|
||||||
|
engine.entities = <Entity>[];
|
||||||
|
|
||||||
|
engine.restoreSaveState(snapshot);
|
||||||
|
|
||||||
|
expect(engine.currentGameIndex, 0);
|
||||||
|
expect(engine.currentEpisodeIndex, 0);
|
||||||
|
expect(engine.currentLevelIndex, 0);
|
||||||
|
|
||||||
|
expect(engine.player.health, 47);
|
||||||
|
expect(engine.player.ammo, 33);
|
||||||
|
expect(engine.player.score, 1200);
|
||||||
|
expect(engine.player.lives, 5);
|
||||||
|
expect(engine.player.x, closeTo(10.5, 0.001));
|
||||||
|
expect(engine.player.y, closeTo(11.5, 0.001));
|
||||||
|
expect(engine.player.angle, closeTo(1.25, 0.001));
|
||||||
|
expect(engine.player.hasGoldKey, isTrue);
|
||||||
|
expect(engine.player.hasMachineGun, isTrue);
|
||||||
|
expect(engine.player.currentWeapon.type, WeaponType.machineGun);
|
||||||
|
|
||||||
|
expect(engine.currentLevel[8][8], 0);
|
||||||
|
expect(engine.currentLevel[5][6], 98);
|
||||||
|
|
||||||
|
final restoredDoor = engine.doorManager.doors.values.first;
|
||||||
|
expect(restoredDoor.state, DoorState.opening);
|
||||||
|
expect(restoredDoor.offset, closeTo(0.42, 0.001));
|
||||||
|
expect(restoredDoor.openTime, 1337);
|
||||||
|
|
||||||
|
expect(engine.pushwallManager.activePushwall, isNotNull);
|
||||||
|
expect(
|
||||||
|
engine.pushwallManager.activePushwall!.offset,
|
||||||
|
closeTo(0.5, 0.001),
|
||||||
|
);
|
||||||
|
expect(engine.pushwallManager.activePushwall!.tilesMoved, 1);
|
||||||
|
expect(engine.pushwallManager.activePushwall!.dirX, 1);
|
||||||
|
|
||||||
|
expect(engine.entities, hasLength(2));
|
||||||
|
expect(engine.entities.first, isA<Guard>());
|
||||||
|
final restoredGuard = engine.entities.first as Guard;
|
||||||
|
expect(restoredGuard.health, 17);
|
||||||
|
expect(restoredGuard.isAlerted, isTrue);
|
||||||
|
expect(restoredGuard.state, EntityState.attacking);
|
||||||
|
expect(restoredGuard.currentFrame, 2);
|
||||||
|
expect(engine.entities.last, isA<SmallAmmoCollectible>());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TestInput extends Wolf3dInput {
|
||||||
|
@override
|
||||||
|
void update() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SilentAudio implements EngineAudio {
|
||||||
|
@override
|
||||||
|
WolfensteinData? activeGame;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> debugSoundTest() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> init() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playLevelMusic(Music music) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playMenuMusic() {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playSoundEffect(SoundEffect effect) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playSoundEffectId(int sfxId) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void stopMusic() {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> stopAllAudio() async {}
|
||||||
|
}
|
||||||
|
|
||||||
|
WolfEngine _buildEngine() {
|
||||||
|
final wallGrid = _buildGrid();
|
||||||
|
final objectGrid = _buildGrid();
|
||||||
|
_fillBoundaries(wallGrid, 2);
|
||||||
|
|
||||||
|
objectGrid[2][2] = MapObject.playerEast;
|
||||||
|
objectGrid[4][4] = MapObject.pushwallTrigger;
|
||||||
|
wallGrid[2][3] = 90;
|
||||||
|
wallGrid[4][4] = 5;
|
||||||
|
|
||||||
|
return WolfEngine(
|
||||||
|
data: WolfensteinData(
|
||||||
|
version: GameVersion.retail,
|
||||||
|
dataVersion: DataVersion.unknown,
|
||||||
|
registry: RetailAssetRegistry(),
|
||||||
|
walls: [
|
||||||
|
_solidSprite(1),
|
||||||
|
_solidSprite(1),
|
||||||
|
_solidSprite(2),
|
||||||
|
_solidSprite(2),
|
||||||
|
],
|
||||||
|
sprites: List.generate(436, (_) => _solidSprite(255)),
|
||||||
|
sounds: List.generate(200, (_) => PcmSound(Uint8List(1))),
|
||||||
|
adLibSounds: const [],
|
||||||
|
music: const [],
|
||||||
|
vgaImages: const [],
|
||||||
|
episodes: [
|
||||||
|
Episode(
|
||||||
|
name: 'Episode 1',
|
||||||
|
levels: [
|
||||||
|
WolfLevel(
|
||||||
|
name: 'Level 1',
|
||||||
|
wallGrid: wallGrid,
|
||||||
|
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
||||||
|
objectGrid: objectGrid,
|
||||||
|
music: Music.level01,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
difficulty: Difficulty.medium,
|
||||||
|
startingEpisode: 0,
|
||||||
|
frameBuffer: FrameBuffer(64, 64),
|
||||||
|
input: _TestInput(),
|
||||||
|
onGameWon: () {},
|
||||||
|
engineAudio: _SilentAudio(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SpriteMap _buildGrid() => List.generate(64, (_) => List.filled(64, 0));
|
||||||
|
|
||||||
|
void _fillBoundaries(SpriteMap grid, int wallId) {
|
||||||
|
for (int i = 0; i < 64; i++) {
|
||||||
|
grid[0][i] = wallId;
|
||||||
|
grid[63][i] = wallId;
|
||||||
|
grid[i][0] = wallId;
|
||||||
|
grid[i][63] = wallId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Sprite _solidSprite(int paletteIndex) {
|
||||||
|
return Sprite(Uint8List.fromList(<int>[paletteIndex]));
|
||||||
|
}
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
/// Shared Flutter renderer shell for driving the Wolf3D engine from a widget tree.
|
/// Shared Flutter renderer shell for driving the Wolf3D engine from a widget tree.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
|
||||||
|
typedef PresentFrameAction = void Function(VoidCallback onComplete);
|
||||||
|
|
||||||
/// Base widget for renderers that present frames from a [WolfEngine].
|
/// Base widget for renderers that present frames from a [WolfEngine].
|
||||||
abstract class BaseWolfRenderer extends StatefulWidget {
|
abstract class BaseWolfRenderer extends StatefulWidget {
|
||||||
/// Engine instance that owns world state and the shared framebuffer.
|
/// Engine instance that owns world state and the shared framebuffer.
|
||||||
@@ -32,6 +37,17 @@ abstract class BaseWolfRendererState<T extends BaseWolfRenderer>
|
|||||||
final FocusNode focusNode = FocusNode();
|
final FocusNode focusNode = FocusNode();
|
||||||
|
|
||||||
Duration _lastTick = Duration.zero;
|
Duration _lastTick = Duration.zero;
|
||||||
|
bool _isPresenting = false;
|
||||||
|
PresentFrameAction? _queuedPresentAction;
|
||||||
|
|
||||||
|
int _tickWindowCount = 0;
|
||||||
|
int _presentWindowCount = 0;
|
||||||
|
int _coalescedPresentWindowCount = 0;
|
||||||
|
int _tickWindowMicrosTotal = 0;
|
||||||
|
int _renderWindowMicrosTotal = 0;
|
||||||
|
int _presentWindowMicrosTotal = 0;
|
||||||
|
|
||||||
|
static const int _perfLogWindowFrames = 180;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -48,12 +64,18 @@ abstract class BaseWolfRendererState<T extends BaseWolfRenderer>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Duration delta = elapsed - _lastTick;
|
final Duration delta = elapsed - _lastTick;
|
||||||
_lastTick = elapsed;
|
_lastTick = elapsed;
|
||||||
|
|
||||||
|
final Stopwatch tickStopwatch = Stopwatch()..start();
|
||||||
widget.engine.tick(delta);
|
widget.engine.tick(delta);
|
||||||
|
tickStopwatch.stop();
|
||||||
|
_tickWindowMicrosTotal += tickStopwatch.elapsedMicroseconds;
|
||||||
|
_tickWindowCount++;
|
||||||
|
|
||||||
performRender();
|
performRender();
|
||||||
|
|
||||||
|
_maybeLogPerfWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -66,6 +88,80 @@ abstract class BaseWolfRendererState<T extends BaseWolfRenderer>
|
|||||||
/// Renders the latest engine state into the concrete renderer's output type.
|
/// Renders the latest engine state into the concrete renderer's output type.
|
||||||
void performRender();
|
void performRender();
|
||||||
|
|
||||||
|
/// Schedules presentation work while coalescing to the latest requested frame.
|
||||||
|
@protected
|
||||||
|
void scheduleLatestPresent(PresentFrameAction action) {
|
||||||
|
if (_isPresenting) {
|
||||||
|
_queuedPresentAction = action;
|
||||||
|
_coalescedPresentWindowCount++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_runPresentAction(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Records CPU-side rendering stage duration in microseconds.
|
||||||
|
@protected
|
||||||
|
void recordRenderStageMicros(int microseconds) {
|
||||||
|
_renderWindowMicrosTotal += microseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _runPresentAction(PresentFrameAction action) {
|
||||||
|
_isPresenting = true;
|
||||||
|
final Stopwatch presentStopwatch = Stopwatch()..start();
|
||||||
|
bool isCompleted = false;
|
||||||
|
|
||||||
|
void onComplete() {
|
||||||
|
if (isCompleted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isCompleted = true;
|
||||||
|
presentStopwatch.stop();
|
||||||
|
_presentWindowMicrosTotal += presentStopwatch.elapsedMicroseconds;
|
||||||
|
_presentWindowCount++;
|
||||||
|
_isPresenting = false;
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final PresentFrameAction? nextAction = _queuedPresentAction;
|
||||||
|
_queuedPresentAction = null;
|
||||||
|
if (nextAction != null) {
|
||||||
|
scheduleMicrotask(() => _runPresentAction(nextAction));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
action(onComplete);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _maybeLogPerfWindow() {
|
||||||
|
if (!kDebugMode || _tickWindowCount < _perfLogWindowFrames) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final double avgTickMs = _tickWindowMicrosTotal / _tickWindowCount / 1000.0;
|
||||||
|
final double avgRenderMs =
|
||||||
|
_renderWindowMicrosTotal / _tickWindowCount / 1000.0;
|
||||||
|
final double avgPresentMs = _presentWindowCount > 0
|
||||||
|
? _presentWindowMicrosTotal / _presentWindowCount / 1000.0
|
||||||
|
: 0.0;
|
||||||
|
debugPrint(
|
||||||
|
'Renderer perf window ($_tickWindowCount frames): '
|
||||||
|
'avg tick ${avgTickMs.toStringAsFixed(2)}ms, '
|
||||||
|
'avg render ${avgRenderMs.toStringAsFixed(2)}ms, '
|
||||||
|
'avg present ${avgPresentMs.toStringAsFixed(2)}ms, '
|
||||||
|
'coalesced $_coalescedPresentWindowCount',
|
||||||
|
);
|
||||||
|
|
||||||
|
_tickWindowCount = 0;
|
||||||
|
_presentWindowCount = 0;
|
||||||
|
_coalescedPresentWindowCount = 0;
|
||||||
|
_tickWindowMicrosTotal = 0;
|
||||||
|
_renderWindowMicrosTotal = 0;
|
||||||
|
_presentWindowMicrosTotal = 0;
|
||||||
|
}
|
||||||
|
|
||||||
/// Builds the visible viewport widget for the latest rendered frame.
|
/// Builds the visible viewport widget for the latest rendered frame.
|
||||||
Widget buildViewport(BuildContext context);
|
Widget buildViewport(BuildContext context);
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ class _WolfFlutterRendererState
|
|||||||
final SoftwareRenderer _renderer = SoftwareRenderer();
|
final SoftwareRenderer _renderer = SoftwareRenderer();
|
||||||
|
|
||||||
ui.Image? _renderedFrame;
|
ui.Image? _renderedFrame;
|
||||||
bool _isRendering = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -45,31 +44,41 @@ class _WolfFlutterRendererState
|
|||||||
@override
|
@override
|
||||||
Color get scaffoldColor => Colors.black;
|
Color get scaffoldColor => Colors.black;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_renderedFrame?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void performRender() {
|
void performRender() {
|
||||||
if (_isRendering) return;
|
scheduleLatestPresent((onComplete) {
|
||||||
_isRendering = true;
|
final FrameBuffer frameBuffer = widget.engine.frameBuffer;
|
||||||
|
final Stopwatch renderStopwatch = Stopwatch()..start();
|
||||||
|
_renderer.render(widget.engine);
|
||||||
|
renderStopwatch.stop();
|
||||||
|
recordRenderStageMicros(renderStopwatch.elapsedMicroseconds);
|
||||||
|
|
||||||
final FrameBuffer frameBuffer = widget.engine.frameBuffer;
|
// Convert the engine-owned framebuffer into a GPU-friendly ui.Image on
|
||||||
_renderer.render(widget.engine);
|
// the Flutter side while preserving nearest-neighbor pixel fidelity.
|
||||||
|
ui.decodeImageFromPixels(
|
||||||
// Convert the engine-owned framebuffer into a GPU-friendly ui.Image on
|
frameBuffer.pixels.buffer.asUint8List(),
|
||||||
// the Flutter side while preserving nearest-neighbor pixel fidelity.
|
frameBuffer.width,
|
||||||
ui.decodeImageFromPixels(
|
frameBuffer.height,
|
||||||
frameBuffer.pixels.buffer.asUint8List(),
|
ui.PixelFormat.rgba8888,
|
||||||
frameBuffer.width,
|
(ui.Image image) {
|
||||||
frameBuffer.height,
|
if (mounted) {
|
||||||
ui.PixelFormat.rgba8888,
|
setState(() {
|
||||||
(ui.Image image) {
|
_renderedFrame?.dispose();
|
||||||
if (mounted) {
|
_renderedFrame = image;
|
||||||
setState(() {
|
});
|
||||||
_renderedFrame?.dispose();
|
} else {
|
||||||
_renderedFrame = image;
|
image.dispose();
|
||||||
});
|
}
|
||||||
}
|
onComplete();
|
||||||
_isRendering = false;
|
},
|
||||||
},
|
);
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ class _WolfGlslRendererState extends BaseWolfRendererState<WolfGlslRenderer> {
|
|||||||
ui.Image? _renderedFrame;
|
ui.Image? _renderedFrame;
|
||||||
ui.FragmentProgram? _shaderProgram;
|
ui.FragmentProgram? _shaderProgram;
|
||||||
ui.FragmentShader? _shader;
|
ui.FragmentShader? _shader;
|
||||||
bool _isRendering = false;
|
|
||||||
bool _isShaderUnavailable = false;
|
bool _isShaderUnavailable = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -67,31 +66,31 @@ class _WolfGlslRendererState extends BaseWolfRendererState<WolfGlslRenderer> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void performRender() {
|
void performRender() {
|
||||||
if (_isRendering) {
|
scheduleLatestPresent((onComplete) {
|
||||||
return;
|
final FrameBuffer frameBuffer = widget.engine.frameBuffer;
|
||||||
}
|
final Stopwatch renderStopwatch = Stopwatch()..start();
|
||||||
_isRendering = true;
|
_renderer.render(widget.engine);
|
||||||
|
renderStopwatch.stop();
|
||||||
|
recordRenderStageMicros(renderStopwatch.elapsedMicroseconds);
|
||||||
|
|
||||||
final FrameBuffer frameBuffer = widget.engine.frameBuffer;
|
ui.decodeImageFromPixels(
|
||||||
_renderer.render(widget.engine);
|
frameBuffer.pixels.buffer.asUint8List(),
|
||||||
|
frameBuffer.width,
|
||||||
ui.decodeImageFromPixels(
|
frameBuffer.height,
|
||||||
frameBuffer.pixels.buffer.asUint8List(),
|
ui.PixelFormat.rgba8888,
|
||||||
frameBuffer.width,
|
(ui.Image image) {
|
||||||
frameBuffer.height,
|
if (mounted) {
|
||||||
ui.PixelFormat.rgba8888,
|
setState(() {
|
||||||
(ui.Image image) {
|
_renderedFrame?.dispose();
|
||||||
if (mounted) {
|
_renderedFrame = image;
|
||||||
setState(() {
|
});
|
||||||
_renderedFrame?.dispose();
|
} else {
|
||||||
_renderedFrame = image;
|
image.dispose();
|
||||||
});
|
}
|
||||||
} else {
|
onComplete();
|
||||||
image.dispose();
|
},
|
||||||
}
|
);
|
||||||
_isRendering = false;
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
Reference in New Issue
Block a user