refactor: improve performance of loading screen and load I/O Pinball asset (#394)

pull/397/head
Jorge Coca 3 years ago committed by GitHub
parent 2284f57ac2
commit 28a3c15fe2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

@ -1,27 +1,39 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_audio/pinball_audio.dart';
part 'assets_manager_state.dart'; part 'assets_manager_state.dart';
/// {@template assets_manager_cubit}
/// Cubit responsable for pre loading any game assets
/// {@endtemplate}
class AssetsManagerCubit extends Cubit<AssetsManagerState> { class AssetsManagerCubit extends Cubit<AssetsManagerState> {
/// {@macro assets_manager_cubit} AssetsManagerCubit(this._game, this._player)
AssetsManagerCubit(List<Future> loadables) : super(const AssetsManagerState.initial());
: super(
AssetsManagerState.initial( final PinballGame _game;
loadables: loadables, final PinballPlayer _player;
),
);
/// Loads the assets
Future<void> load() async { Future<void> load() async {
/// Assigning loadables is a very expensive operation. With this purposeful
/// delay here, which is a bit random in duration but enough to let the UI
/// do its job without adding too much delay for the user, we are letting
/// the UI paint first, and then we start loading the assets.
await Future<void>.delayed(const Duration(milliseconds: 300));
emit(
state.copyWith(
loadables: [
_game.preFetchLeaderboard(),
..._game.preLoadAssets(),
..._player.load(),
...BonusAnimation.loadAssets(),
...SelectedCharacter.loadAssets(),
],
),
);
final all = state.loadables.map((loadable) async { final all = state.loadables.map((loadable) async {
await loadable; await loadable;
emit(state.copyWith(loaded: [...state.loaded, loadable])); emit(state.copyWith(loaded: [...state.loaded, loadable]));
}).toList(); }).toList();
await Future.wait(all); await Future.wait(all);
} }
} }

@ -11,9 +11,8 @@ class AssetsManagerState extends Equatable {
}); });
/// {@macro assets_manager_state} /// {@macro assets_manager_state}
const AssetsManagerState.initial({ const AssetsManagerState.initial()
required List<Future> loadables, : this(loadables: const [], loaded: const []);
}) : this(loadables: loadables, loaded: const []);
/// List of futures to load /// List of futures to load
final List<Future> loadables; final List<Future> loadables;
@ -22,7 +21,11 @@ class AssetsManagerState extends Equatable {
final List<Future> loaded; final List<Future> loaded;
/// Returns a value between 0 and 1 to indicate the loading progress /// Returns a value between 0 and 1 to indicate the loading progress
double get progress => loaded.length / loadables.length; double get progress =>
loadables.isEmpty ? 0 : loaded.length / loadables.length;
/// Only returns false if all the assets have been loaded
bool get isLoading => progress != 1;
/// Returns a copy of this instance with the given parameters /// Returns a copy of this instance with the given parameters
/// updated /// updated

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/assets_manager/assets_manager.dart'; import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/gen/gen.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
@ -20,10 +21,9 @@ class AssetsLoadingPage extends StatelessWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Padding(
l10n.ioPinball, padding: const EdgeInsets.symmetric(horizontal: 20),
style: headline1!.copyWith(fontSize: 80), child: Assets.images.loadingGame.ioPinball.image(),
textAlign: TextAlign.center,
), ),
const SizedBox(height: 40), const SizedBox(height: 40),
AnimatedEllipsisText( AnimatedEllipsisText(

@ -1,3 +1,4 @@
import 'package:flame/extensions.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart' as components; import 'package:pinball_components/pinball_components.dart' as components;
import 'package:pinball_theme/pinball_theme.dart' hide Assets; import 'package:pinball_theme/pinball_theme.dart' hide Assets;
@ -5,7 +6,7 @@ import 'package:pinball_theme/pinball_theme.dart' hide Assets;
/// Add methods to help loading and caching game assets. /// Add methods to help loading and caching game assets.
extension PinballGameAssetsX on PinballGame { extension PinballGameAssetsX on PinballGame {
/// Returns a list of assets to be loaded /// Returns a list of assets to be loaded
List<Future> preLoadAssets() { List<Future<Image>> preLoadAssets() {
const dashTheme = DashTheme(); const dashTheme = DashTheme();
const sparkyTheme = SparkyTheme(); const sparkyTheme = SparkyTheme();
const androidTheme = AndroidTheme(); const androidTheme = AndroidTheme();

@ -21,12 +21,6 @@ class PinballGamePage extends StatelessWidget {
final bool isDebugMode; final bool isDebugMode;
static Route route({bool isDebugMode = kDebugMode}) {
return MaterialPageRoute<void>(
builder: (_) => PinballGamePage(isDebugMode: isDebugMode),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final characterThemeBloc = context.read<CharacterThemeCubit>(); final characterThemeBloc = context.read<CharacterThemeCubit>();
@ -48,53 +42,39 @@ class PinballGamePage extends StatelessWidget {
l10n: context.l10n, l10n: context.l10n,
gameBloc: gameBloc, gameBloc: gameBloc,
); );
return Container(
final loadables = [ decoration: const CrtBackground(),
game.preFetchLeaderboard(), child: Scaffold(
...game.preLoadAssets(), backgroundColor: PinballColors.transparent,
...player.load(), body: BlocProvider(
...BonusAnimation.loadAssets(), create: (_) => AssetsManagerCubit(game, player)..load(),
...SelectedCharacter.loadAssets(), child: PinballGameView(game),
]; ),
),
return BlocProvider(
create: (_) => AssetsManagerCubit(loadables)..load(),
child: PinballGameView(game: game),
); );
} }
} }
class PinballGameView extends StatelessWidget { class PinballGameView extends StatelessWidget {
const PinballGameView({ const PinballGameView(this.game, {Key? key}) : super(key: key);
Key? key,
required this.game,
}) : super(key: key);
final PinballGame game; final PinballGame game;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isLoading = context.select( return BlocBuilder<AssetsManagerCubit, AssetsManagerState>(
(AssetsManagerCubit bloc) => bloc.state.progress != 1, builder: (context, state) {
); return state.isLoading
return Container(
decoration: const CrtBackground(),
child: Scaffold(
backgroundColor: PinballColors.transparent,
body: isLoading
? const AssetsLoadingPage() ? const AssetsLoadingPage()
: PinballGameLoadedView(game: game), : PinballGameLoadedView(game);
), },
); );
} }
} }
@visibleForTesting @visibleForTesting
class PinballGameLoadedView extends StatelessWidget { class PinballGameLoadedView extends StatelessWidget {
const PinballGameLoadedView({ const PinballGameLoadedView(this.game, {Key? key}) : super(key: key);
Key? key,
required this.game,
}) : super(key: key);
final PinballGame game; final PinballGame game;

@ -15,6 +15,8 @@ class $AssetsImagesGen {
$AssetsImagesComponentsGen get components => $AssetsImagesComponentsGen get components =>
const $AssetsImagesComponentsGen(); const $AssetsImagesComponentsGen();
$AssetsImagesLinkBoxGen get linkBox => const $AssetsImagesLinkBoxGen(); $AssetsImagesLinkBoxGen get linkBox => const $AssetsImagesLinkBoxGen();
$AssetsImagesLoadingGameGen get loadingGame =>
const $AssetsImagesLoadingGameGen();
$AssetsImagesScoreGen get score => const $AssetsImagesScoreGen(); $AssetsImagesScoreGen get score => const $AssetsImagesScoreGen();
} }
@ -62,6 +64,14 @@ class $AssetsImagesLinkBoxGen {
const AssetGenImage('assets/images/link_box/info_icon.png'); const AssetGenImage('assets/images/link_box/info_icon.png');
} }
class $AssetsImagesLoadingGameGen {
const $AssetsImagesLoadingGameGen();
/// File path: assets/images/loading_game/io_pinball.png
AssetGenImage get ioPinball =>
const AssetGenImage('assets/images/loading_game/io_pinball.png');
}
class $AssetsImagesScoreGen { class $AssetsImagesScoreGen {
const $AssetsImagesScoreGen(); const $AssetsImagesScoreGen();

@ -148,10 +148,6 @@
"@loading": { "@loading": {
"description": "Text shown to indicate loading times" "description": "Text shown to indicate loading times"
}, },
"ioPinball": "I/O Pinball",
"@ioPinball": {
"description": "I/O Pinball - Name of the game"
},
"enter": "Enter", "enter": "Enter",
"@enter": { "@enter": {
"description": "Text shown on the mobile controls enter button" "description": "Text shown on the mobile controls enter button"

@ -62,6 +62,7 @@ flutter:
- assets/images/bonus_animation/ - assets/images/bonus_animation/
- assets/images/score/ - assets/images/score/
- assets/images/link_box/ - assets/images/link_box/
- assets/images/loading_game/
flutter_gen: flutter_gen:
line_length: 80 line_length: 80

@ -35,6 +35,7 @@ void main() {
pinballPlayer: pinballPlayer, pinballPlayer: pinballPlayer,
), ),
); );
await tester.pump(const Duration(milliseconds: 400));
expect(find.byType(PinballGamePage), findsOneWidget); expect(find.byType(PinballGamePage), findsOneWidget);
}); });
}); });

@ -1,35 +0,0 @@
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/assets_manager/assets_manager.dart';
void main() {
group('AssetsManagerCubit', () {
final completer1 = Completer<void>();
final completer2 = Completer<void>();
final future1 = completer1.future;
final future2 = completer2.future;
blocTest<AssetsManagerCubit, AssetsManagerState>(
'emits the loaded on the order that they load',
build: () => AssetsManagerCubit([future1, future2]),
act: (cubit) {
cubit.load();
completer2.complete();
completer1.complete();
},
expect: () => [
AssetsManagerState(
loadables: [future1, future2],
loaded: [future2],
),
AssetsManagerState(
loadables: [future1, future2],
loaded: [future2, future1],
),
],
);
});
}

@ -13,12 +13,11 @@ void main() {
}); });
test('has the correct initial state', () { test('has the correct initial state', () {
final future = Future<void>.value();
expect( expect(
AssetsManagerState.initial(loadables: [future]), AssetsManagerState.initial(),
equals( equals(
AssetsManagerState( AssetsManagerState(
loadables: [future], loadables: const [],
loaded: const [], loaded: const [],
), ),
), ),

@ -82,7 +82,8 @@ void main() {
); );
}); });
testWidgets('renders PinballGameView', (tester) async { group('renders PinballGameView', () {
testWidgets('with debug mode turned on', (tester) async {
await tester.pumpApp( await tester.pumpApp(
PinballGamePage(), PinballGamePage(),
characterThemeCubit: characterThemeCubit, characterThemeCubit: characterThemeCubit,
@ -92,6 +93,17 @@ void main() {
expect(find.byType(PinballGameView), findsOneWidget); expect(find.byType(PinballGameView), findsOneWidget);
}); });
testWidgets('with debug mode turned off', (tester) async {
await tester.pumpApp(
PinballGamePage(isDebugMode: false),
characterThemeCubit: characterThemeCubit,
gameBloc: gameBloc,
);
expect(find.byType(PinballGameView), findsOneWidget);
});
});
testWidgets( testWidgets(
'renders the loading indicator while the assets load', 'renders the loading indicator while the assets load',
(tester) async { (tester) async {
@ -106,9 +118,7 @@ void main() {
initialState: initialAssetsState, initialState: initialAssetsState,
); );
await tester.pumpApp( await tester.pumpApp(
PinballGameView( PinballGameView(game),
game: game,
),
assetsManagerCubit: assetsManagerCubit, assetsManagerCubit: assetsManagerCubit,
characterThemeCubit: characterThemeCubit, characterThemeCubit: characterThemeCubit,
); );
@ -138,9 +148,7 @@ void main() {
); );
await tester.pumpApp( await tester.pumpApp(
PinballGameView( PinballGameView(game),
game: game,
),
assetsManagerCubit: assetsManagerCubit, assetsManagerCubit: assetsManagerCubit,
characterThemeCubit: characterThemeCubit, characterThemeCubit: characterThemeCubit,
gameBloc: gameBloc, gameBloc: gameBloc,
@ -151,61 +159,6 @@ void main() {
expect(find.byType(PinballGameLoadedView), findsOneWidget); expect(find.byType(PinballGameLoadedView), findsOneWidget);
}); });
group('route', () {
Future<void> pumpRoute({
required WidgetTester tester,
required bool isDebugMode,
}) async {
await tester.pumpApp(
Scaffold(
body: Builder(
builder: (context) {
return ElevatedButton(
onPressed: () {
Navigator.of(context).push<void>(
PinballGamePage.route(
isDebugMode: isDebugMode,
),
);
},
child: const Text('Tap me'),
);
},
),
),
characterThemeCubit: characterThemeCubit,
gameBloc: gameBloc,
);
await tester.tap(find.text('Tap me'));
// We can't use pumpAndSettle here because the page renders a Flame game
// which is an infinity animation, so it will timeout
await tester.pump(); // Runs the button action
await tester.pump(); // Runs the navigation
}
testWidgets('route creates the correct non debug game', (tester) async {
await pumpRoute(tester: tester, isDebugMode: false);
expect(
find.byWidgetPredicate(
(w) => w is PinballGameView && w.game is! DebugPinballGame,
),
findsOneWidget,
);
});
testWidgets('route creates the correct debug game', (tester) async {
await pumpRoute(tester: tester, isDebugMode: true);
expect(
find.byWidgetPredicate(
(w) => w is PinballGameView && w.game is DebugPinballGame,
),
findsOneWidget,
);
});
});
}); });
group('PinballGameView', () { group('PinballGameView', () {
@ -230,7 +183,7 @@ void main() {
testWidgets('renders game', (tester) async { testWidgets('renders game', (tester) async {
await tester.pumpApp( await tester.pumpApp(
PinballGameView(game: game), PinballGameView(game),
gameBloc: gameBloc, gameBloc: gameBloc,
startGameBloc: startGameBloc, startGameBloc: startGameBloc,
); );
@ -258,7 +211,7 @@ void main() {
); );
await tester.pumpApp( await tester.pumpApp(
PinballGameView(game: game), PinballGameView(game),
gameBloc: gameBloc, gameBloc: gameBloc,
startGameBloc: startGameBloc, startGameBloc: startGameBloc,
); );
@ -276,7 +229,6 @@ void main() {
final gameState = GameState.initial().copyWith( final gameState = GameState.initial().copyWith(
status: GameStatus.gameOver, status: GameStatus.gameOver,
); );
whenListen( whenListen(
startGameBloc, startGameBloc,
Stream.value(startGameState), Stream.value(startGameState),
@ -287,17 +239,12 @@ void main() {
Stream.value(gameState), Stream.value(gameState),
initialState: gameState, initialState: gameState,
); );
await tester.pumpApp( await tester.pumpApp(
PinballGameView(game: game), Material(child: PinballGameView(game)),
gameBloc: gameBloc, gameBloc: gameBloc,
startGameBloc: startGameBloc, startGameBloc: startGameBloc,
); );
expect(find.byType(GameHud), findsNothing);
expect(
find.byType(GameHud),
findsNothing,
);
}); });
testWidgets('keep focus on game when mouse hovers over it', (tester) async { testWidgets('keep focus on game when mouse hovers over it', (tester) async {
@ -307,7 +254,6 @@ void main() {
final gameState = GameState.initial().copyWith( final gameState = GameState.initial().copyWith(
status: GameStatus.gameOver, status: GameStatus.gameOver,
); );
whenListen( whenListen(
startGameBloc, startGameBloc,
Stream.value(startGameState), Stream.value(startGameState),
@ -319,28 +265,24 @@ void main() {
initialState: gameState, initialState: gameState,
); );
await tester.pumpApp( await tester.pumpApp(
PinballGameView(game: game), Material(child: PinballGameView(game)),
gameBloc: gameBloc, gameBloc: gameBloc,
startGameBloc: startGameBloc, startGameBloc: startGameBloc,
); );
game.focusNode.unfocus(); game.focusNode.unfocus();
await tester.pump(); await tester.pump();
expect(game.focusNode.hasFocus, isFalse); expect(game.focusNode.hasFocus, isFalse);
final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: Offset.zero); await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await gesture.moveTo((game.size / 2).toOffset()); await gesture.moveTo((game.size / 2).toOffset());
await tester.pump(); await tester.pump();
expect(game.focusNode.hasFocus, isTrue); expect(game.focusNode.hasFocus, isTrue);
}); });
testWidgets('mobile controls when the overlay is added', (tester) async { testWidgets('mobile controls when the overlay is added', (tester) async {
await tester.pumpApp( await tester.pumpApp(
PinballGameView(game: game), PinballGameView(game),
gameBloc: gameBloc, gameBloc: gameBloc,
startGameBloc: startGameBloc, startGameBloc: startGameBloc,
); );
@ -357,23 +299,17 @@ void main() {
final gameState = GameState.initial().copyWith( final gameState = GameState.initial().copyWith(
status: GameStatus.gameOver, status: GameStatus.gameOver,
); );
whenListen( whenListen(
gameBloc, gameBloc,
Stream.value(gameState), Stream.value(gameState),
initialState: gameState, initialState: gameState,
); );
await tester.pumpApp( await tester.pumpApp(
PinballGameView(game: game), Material(child: PinballGameView(game)),
gameBloc: gameBloc, gameBloc: gameBloc,
startGameBloc: startGameBloc, startGameBloc: startGameBloc,
); );
expect(find.image(Assets.images.linkBox.infoIcon), findsOneWidget);
expect(
find.image(Assets.images.linkBox.infoIcon),
findsOneWidget,
);
}); });
testWidgets('opens MoreInformationDialog when tapped', (tester) async { testWidgets('opens MoreInformationDialog when tapped', (tester) async {
@ -386,16 +322,13 @@ void main() {
initialState: gameState, initialState: gameState,
); );
await tester.pumpApp( await tester.pumpApp(
PinballGameView(game: game), Material(child: PinballGameView(game)),
gameBloc: gameBloc, gameBloc: gameBloc,
startGameBloc: startGameBloc, startGameBloc: startGameBloc,
); );
await tester.tap(find.byType(IconButton)); await tester.tap(find.byType(IconButton));
await tester.pump(); await tester.pump();
expect( expect(find.byType(MoreInformationDialog), findsOneWidget);
find.byType(MoreInformationDialog),
findsOneWidget,
);
}); });
}); });
}); });

@ -76,6 +76,10 @@
application. For more information, see: application. For more information, see:
https://developers.google.com/web/fundamentals/primers/service-workers --> https://developers.google.com/web/fundamentals/primers/service-workers -->
<script> <script>
fetch("assets/assets/images/loading_game/io_pinball.png").catch(function (e) {
console.warn(e);
});
var serviceWorkerVersion = null; var serviceWorkerVersion = null;
var scriptLoaded = false; var scriptLoaded = false;
function loadMainDartJs() { function loadMainDartJs() {

Loading…
Cancel
Save