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

pull/397/head
Jorge Coca 2 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: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';
/// {@template assets_manager_cubit}
/// Cubit responsable for pre loading any game assets
/// {@endtemplate}
class AssetsManagerCubit extends Cubit<AssetsManagerState> {
/// {@macro assets_manager_cubit}
AssetsManagerCubit(List<Future> loadables)
: super(
AssetsManagerState.initial(
loadables: loadables,
),
);
AssetsManagerCubit(this._game, this._player)
: super(const AssetsManagerState.initial());
final PinballGame _game;
final PinballPlayer _player;
/// Loads the assets
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 {
await loadable;
emit(state.copyWith(loaded: [...state.loaded, loadable]));
}).toList();
await Future.wait(all);
}
}

@ -11,9 +11,8 @@ class AssetsManagerState extends Equatable {
});
/// {@macro assets_manager_state}
const AssetsManagerState.initial({
required List<Future> loadables,
}) : this(loadables: loadables, loaded: const []);
const AssetsManagerState.initial()
: this(loadables: const [], loaded: const []);
/// List of futures to load
final List<Future> loadables;
@ -22,7 +21,11 @@ class AssetsManagerState extends Equatable {
final List<Future> loaded;
/// 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
/// updated

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

@ -1,3 +1,4 @@
import 'package:flame/extensions.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart' as components;
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.
extension PinballGameAssetsX on PinballGame {
/// Returns a list of assets to be loaded
List<Future> preLoadAssets() {
List<Future<Image>> preLoadAssets() {
const dashTheme = DashTheme();
const sparkyTheme = SparkyTheme();
const androidTheme = AndroidTheme();

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

@ -15,6 +15,8 @@ class $AssetsImagesGen {
$AssetsImagesComponentsGen get components =>
const $AssetsImagesComponentsGen();
$AssetsImagesLinkBoxGen get linkBox => const $AssetsImagesLinkBoxGen();
$AssetsImagesLoadingGameGen get loadingGame =>
const $AssetsImagesLoadingGameGen();
$AssetsImagesScoreGen get score => const $AssetsImagesScoreGen();
}
@ -62,6 +64,14 @@ class $AssetsImagesLinkBoxGen {
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 {
const $AssetsImagesScoreGen();

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

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

@ -35,6 +35,7 @@ void main() {
pinballPlayer: pinballPlayer,
),
);
await tester.pump(const Duration(milliseconds: 400));
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', () {
final future = Future<void>.value();
expect(
AssetsManagerState.initial(loadables: [future]),
AssetsManagerState.initial(),
equals(
AssetsManagerState(
loadables: [future],
loadables: const [],
loaded: const [],
),
),

@ -82,14 +82,26 @@ void main() {
);
});
testWidgets('renders PinballGameView', (tester) async {
await tester.pumpApp(
PinballGamePage(),
characterThemeCubit: characterThemeCubit,
gameBloc: gameBloc,
);
group('renders PinballGameView', () {
testWidgets('with debug mode turned on', (tester) async {
await tester.pumpApp(
PinballGamePage(),
characterThemeCubit: characterThemeCubit,
gameBloc: gameBloc,
);
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(
@ -106,9 +118,7 @@ void main() {
initialState: initialAssetsState,
);
await tester.pumpApp(
PinballGameView(
game: game,
),
PinballGameView(game),
assetsManagerCubit: assetsManagerCubit,
characterThemeCubit: characterThemeCubit,
);
@ -138,9 +148,7 @@ void main() {
);
await tester.pumpApp(
PinballGameView(
game: game,
),
PinballGameView(game),
assetsManagerCubit: assetsManagerCubit,
characterThemeCubit: characterThemeCubit,
gameBloc: gameBloc,
@ -151,61 +159,6 @@ void main() {
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', () {
@ -230,7 +183,7 @@ void main() {
testWidgets('renders game', (tester) async {
await tester.pumpApp(
PinballGameView(game: game),
PinballGameView(game),
gameBloc: gameBloc,
startGameBloc: startGameBloc,
);
@ -258,7 +211,7 @@ void main() {
);
await tester.pumpApp(
PinballGameView(game: game),
PinballGameView(game),
gameBloc: gameBloc,
startGameBloc: startGameBloc,
);
@ -276,7 +229,6 @@ void main() {
final gameState = GameState.initial().copyWith(
status: GameStatus.gameOver,
);
whenListen(
startGameBloc,
Stream.value(startGameState),
@ -287,17 +239,12 @@ void main() {
Stream.value(gameState),
initialState: gameState,
);
await tester.pumpApp(
PinballGameView(game: game),
Material(child: PinballGameView(game)),
gameBloc: gameBloc,
startGameBloc: startGameBloc,
);
expect(
find.byType(GameHud),
findsNothing,
);
expect(find.byType(GameHud), findsNothing);
});
testWidgets('keep focus on game when mouse hovers over it', (tester) async {
@ -307,7 +254,6 @@ void main() {
final gameState = GameState.initial().copyWith(
status: GameStatus.gameOver,
);
whenListen(
startGameBloc,
Stream.value(startGameState),
@ -319,28 +265,24 @@ void main() {
initialState: gameState,
);
await tester.pumpApp(
PinballGameView(game: game),
Material(child: PinballGameView(game)),
gameBloc: gameBloc,
startGameBloc: startGameBloc,
);
game.focusNode.unfocus();
await tester.pump();
expect(game.focusNode.hasFocus, isFalse);
final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer);
await gesture.moveTo((game.size / 2).toOffset());
await tester.pump();
expect(game.focusNode.hasFocus, isTrue);
});
testWidgets('mobile controls when the overlay is added', (tester) async {
await tester.pumpApp(
PinballGameView(game: game),
PinballGameView(game),
gameBloc: gameBloc,
startGameBloc: startGameBloc,
);
@ -357,23 +299,17 @@ void main() {
final gameState = GameState.initial().copyWith(
status: GameStatus.gameOver,
);
whenListen(
gameBloc,
Stream.value(gameState),
initialState: gameState,
);
await tester.pumpApp(
PinballGameView(game: game),
Material(child: PinballGameView(game)),
gameBloc: gameBloc,
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 {
@ -386,16 +322,13 @@ void main() {
initialState: gameState,
);
await tester.pumpApp(
PinballGameView(game: game),
Material(child: PinballGameView(game)),
gameBloc: gameBloc,
startGameBloc: startGameBloc,
);
await tester.tap(find.byType(IconButton));
await tester.pump();
expect(
find.byType(MoreInformationDialog),
findsOneWidget,
);
expect(find.byType(MoreInformationDialog), findsOneWidget);
});
});
});

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

Loading…
Cancel
Save