Getting Started with Your Game Using Flutter's Endless Runner Template
Motivation
If you want to create a flutter game other than an endless runner but want to utilize Flutter's endless runner template features like FlameGame
base template, collision detection, spawning, etc.; feel free to read on.
Environment
- IDE: Android Studio Hedgehog (2023.1.1)
- OS: MacOS 14.3.1 (Sonoma)
- Platform: Android
Preparation
- It's best to atleast get familiar with Flutter's Casual Game Toolkit.
- Download the template from GitHub: Flutter Games.
- Copy or move
~/games-main/templates/endless_runner/
outside the~/games-main/
folder. - Get familiar with the template by going through its readme file. It's also advisable to try building, running, and playing the game to make sure your setup is working.
flutter create
. For example, I only want to focus on android for now so I used this command:flutter create . --platforms android
Renaming Your Project
- I used Flutter's
rename
CLI tool to update my project's name and bundle ID but it doesn't seem to work fully. You can confirm this by checking, and if necessary updating your package names in:
MainActivity.java
AndroidManifest.xml
build.gradle
~/<project_name>/android/app/src/main/java/your/package/name/
More info here.
-
Optional: For clarity, you can also rename the file names and class names in
~/<project_name>/lib/flame_game/
. In my case, I added the prefixesendless_runner
andEndlessRunner
in each file name and class name respectively. -
Rebuild and run your project to confirm everything is working.
Creating Your Own Classes
First, create a new folder in ~/<project_name>/lib/
. For this example, let's name it my_flame_game
.
Create the following scripts inside my_flame_game
. The following code are the minimum of what I have to copy from the template in order to get me started.
my_world.dart
class MyWorld extends World with TapCallbacks, HasGameReference {
MyWorld({
required this.level,
required this.playerProgress,
Random? pRandom
}) : random = pRandom ?? Random();
final GameLevel level;
final PlayerProgress playerProgress;
final Random random;
}
my_game.dart
class MyGame extends FlameGame<MyWorld> with HasCollisionDetection {
MyGame({
required this.level,
required PlayerProgress playerProgress,
required this.audioController
}) : super (
world: MyWorld(level: level, playerProgress: playerProgress),
camera: CameraComponent.withFixedResolution(width: 1600, height: 720)
);
final GameLevel level;
final AudioController audioController;
}
my_game_screen.dart
class MyGameScreen extends StatelessWidget {
const MyGameScreen({required this.level, super.key});
final GameLevel level;
@override
Widget build(BuildContext context) {
final audioController = context.read<AudioController>();
return Scaffold(
body: GameWidget<MyGame>(
key: const Key('play session'),
game: MyGame(
level: level,
playerProgress: context.read<PlayerProgress>(),
audioController: audioController
)
),
);
}
}
With these classes, I can update router.dart
to open my game screen instead of the template one.
final router = GoRouter(
routes: [
GoRoute(
// ...
routes: [
GoRoute(
// ...
routes: [
GoRoute(
path: 'session/:level',
pageBuilder: (context, state) {
// ...
return buildPageTransition<void>(
// ...
// child: EndlessRunnerGameScreen(level: level),
child: MyGameScreen(level: level),
);
},
),
],
),
// ...
],
),
],
);
Now when you test your changes, a black screen should show up after selecting a level.
flame_game
folder as reference while working on my project. I just prefer to have everything as much as possible in one project to minimize switching between multiple windows.Optional Changes
The following changes are specific to my game but you might also find them useful:
Skipping Level Selection
After making these changes in router.dart
, pressing play should display your own game screen.
final router = GoRouter(
routes: [
GoRoute(
// ...
routes: [
GoRoute(
path: 'play',
pageBuilder: (context, state) => buildPageTransition<void>(
key: const ValueKey('play'),
// color: context.watch<Palette>().backgroundLevelSelection.color,
color: context.watch<Palette>().backgroundPlaySession.color,
// child: const LevelSelectionScreen(
// key: Key('level selection'),
// ),
child: MyGameScreen(level: gameLevels[0]),
),
// routes: [
// GoRoute(
// path: 'session/:level',
// pageBuilder: (context, state) {
// final levelNumber = int.parse(state.pathParameters['level']!);
// final level = gameLevels[levelNumber - 1];
// return buildPageTransition<void>(
// key: const ValueKey('level'),
// color: context.watch<Palette>().backgroundPlaySession.color,
// // child: EndlessRunnerGameScreen(level: level),
// child: MyGameScreen(level: level),
// );
// },
// ),
],
),
// ...
],
),
],
);
Switching to Portrait Orientation
In main.dart
:
void main() async {
// ...
// await Flame.device.setLandscape();
await Flame.device.setPortrait();
// ...
}
Then in my_game.dart
, swap the camera's resolution width and height.
// camera: CameraComponent.withFixedResolution(width: 720, height: 1600)
camera: CameraComponent.withFixedResolution(width: 1600, height: 720)
Next Steps
As said in Flutter's endless runner template,
Focus on making your core gameplay fun first.
Now we can proceed making our own core game by updating the three classes that we just created. We can start adding our own components, visual effects, and custom menus using the template as reference; just as what we did with the initial classes in this tutorial.
Happy coding!
Member discussion