mirror of
https://github.com/flutter/samples.git
synced 2026-04-03 18:22:45 +00:00
Add game_template (#1180)
Adds a template / sample for games built in Flutter, with all the bells and whistles, like ads, in-app purchases, audio, main menu, settings, and so on. Co-authored-by: Parker Lougheed Co-authored-by: Shams Zakhour
This commit is contained in:
234
game_template/lib/src/style/confetti.dart
Normal file
234
game_template/lib/src/style/confetti.dart
Normal file
@@ -0,0 +1,234 @@
|
||||
// Copyright 2022, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:collection';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Shows a confetti (celebratory) animation: paper snippings falling down.
|
||||
///
|
||||
/// The widget fills the available space (like [SizedBox.expand] would).
|
||||
///
|
||||
/// When [isStopped] is `true`, the animation will not run. This is useful
|
||||
/// when the widget is not visible yet, for example. Provide [colors]
|
||||
/// to make the animation look good in context.
|
||||
///
|
||||
/// This is a partial port of this CodePen by Hemn Chawroka:
|
||||
/// https://codepen.io/iprodev/pen/azpWBr
|
||||
class Confetti extends StatefulWidget {
|
||||
static const _defaultColors = [
|
||||
Color(0xffd10841),
|
||||
Color(0xff1d75fb),
|
||||
Color(0xff0050bc),
|
||||
Color(0xffa2dcc7),
|
||||
];
|
||||
|
||||
final bool isStopped;
|
||||
|
||||
final List<Color> colors;
|
||||
|
||||
const Confetti({
|
||||
this.colors = _defaultColors,
|
||||
this.isStopped = false,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<Confetti> createState() => _ConfettiState();
|
||||
}
|
||||
|
||||
class ConfettiPainter extends CustomPainter {
|
||||
final defaultPaint = Paint();
|
||||
|
||||
final int snippingsCount = 200;
|
||||
|
||||
late final List<_PaperSnipping> _snippings;
|
||||
|
||||
Size? _size;
|
||||
|
||||
DateTime _lastTime = DateTime.now();
|
||||
|
||||
final UnmodifiableListView<Color> colors;
|
||||
|
||||
ConfettiPainter(
|
||||
{required Listenable animation, required Iterable<Color> colors})
|
||||
: colors = UnmodifiableListView(colors),
|
||||
super(repaint: animation);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
if (_size == null) {
|
||||
// First time we have a size.
|
||||
_snippings = List.generate(
|
||||
snippingsCount,
|
||||
(i) => _PaperSnipping(
|
||||
frontColor: colors[i % colors.length],
|
||||
bounds: size,
|
||||
));
|
||||
}
|
||||
|
||||
final didResize = _size != null && _size != size;
|
||||
final now = DateTime.now();
|
||||
final dt = now.difference(_lastTime);
|
||||
for (final snipping in _snippings) {
|
||||
if (didResize) {
|
||||
snipping.updateBounds(size);
|
||||
}
|
||||
snipping.update(dt.inMilliseconds / 1000);
|
||||
snipping.draw(canvas);
|
||||
}
|
||||
|
||||
_size = size;
|
||||
_lastTime = now;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class _ConfettiState extends State<Confetti>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
painter: ConfettiPainter(
|
||||
colors: widget.colors,
|
||||
animation: _controller,
|
||||
),
|
||||
willChange: true,
|
||||
child: const SizedBox.expand(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant Confetti oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.isStopped && !widget.isStopped) {
|
||||
_controller.repeat();
|
||||
} else if (!oldWidget.isStopped && widget.isStopped) {
|
||||
_controller.stop(canceled: false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
// We don't really care about the duration, since we're going to
|
||||
// use the controller on loop anyway.
|
||||
duration: const Duration(seconds: 1),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
if (!widget.isStopped) {
|
||||
_controller.repeat();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _PaperSnipping {
|
||||
static final Random _random = Random();
|
||||
|
||||
static const degToRad = pi / 180;
|
||||
|
||||
static const backSideBlend = Color(0x70EEEEEE);
|
||||
|
||||
Size _bounds;
|
||||
|
||||
late final _Vector position = _Vector(
|
||||
_random.nextDouble() * _bounds.width,
|
||||
_random.nextDouble() * _bounds.height,
|
||||
);
|
||||
|
||||
final double rotationSpeed = 800 + _random.nextDouble() * 600;
|
||||
|
||||
final double angle = _random.nextDouble() * 360 * degToRad;
|
||||
|
||||
double rotation = _random.nextDouble() * 360 * degToRad;
|
||||
|
||||
double cosA = 1.0;
|
||||
|
||||
final double size = 7.0;
|
||||
|
||||
final double oscillationSpeed = 0.5 + _random.nextDouble() * 1.5;
|
||||
|
||||
final double xSpeed = 40;
|
||||
|
||||
final double ySpeed = 50 + _random.nextDouble() * 60;
|
||||
|
||||
late List<_Vector> corners = List.generate(4, (i) {
|
||||
final angle = this.angle + degToRad * (45 + i * 90);
|
||||
return _Vector(cos(angle), sin(angle));
|
||||
});
|
||||
|
||||
double time = _random.nextDouble();
|
||||
|
||||
final Color frontColor;
|
||||
|
||||
late final Color backColor = Color.alphaBlend(backSideBlend, frontColor);
|
||||
|
||||
final paint = Paint()..style = PaintingStyle.fill;
|
||||
|
||||
_PaperSnipping({
|
||||
required this.frontColor,
|
||||
required Size bounds,
|
||||
}) : _bounds = bounds;
|
||||
|
||||
void draw(Canvas canvas) {
|
||||
if (cosA > 0) {
|
||||
paint.color = frontColor;
|
||||
} else {
|
||||
paint.color = backColor;
|
||||
}
|
||||
|
||||
final path = Path()
|
||||
..addPolygon(
|
||||
List.generate(
|
||||
4,
|
||||
(index) => Offset(
|
||||
position.x + corners[index].x * size,
|
||||
position.y + corners[index].y * size * cosA,
|
||||
)),
|
||||
true,
|
||||
);
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
void update(double dt) {
|
||||
time += dt;
|
||||
rotation += rotationSpeed * dt;
|
||||
cosA = cos(degToRad * rotation);
|
||||
position.x += cos(time * oscillationSpeed) * xSpeed * dt;
|
||||
position.y += ySpeed * dt;
|
||||
if (position.y > _bounds.height) {
|
||||
// Move the snipping back to the top.
|
||||
position.x = _random.nextDouble() * _bounds.width;
|
||||
position.y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void updateBounds(Size newBounds) {
|
||||
if (!newBounds.contains(Offset(position.x, position.y))) {
|
||||
position.x = _random.nextDouble() * newBounds.width;
|
||||
position.y = _random.nextDouble() * newBounds.height;
|
||||
}
|
||||
_bounds = newBounds;
|
||||
}
|
||||
}
|
||||
|
||||
class _Vector {
|
||||
double x, y;
|
||||
_Vector(this.x, this.y);
|
||||
}
|
||||
124
game_template/lib/src/style/my_transition.dart
Normal file
124
game_template/lib/src/style/my_transition.dart
Normal file
@@ -0,0 +1,124 @@
|
||||
// Copyright 2022, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
CustomTransitionPage<T> buildMyTransition<T>({
|
||||
required Widget child,
|
||||
required Color color,
|
||||
String? name,
|
||||
Object? arguments,
|
||||
String? restorationId,
|
||||
LocalKey? key,
|
||||
}) {
|
||||
return CustomTransitionPage<T>(
|
||||
child: child,
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
return _MyReveal(
|
||||
animation: animation,
|
||||
color: color,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
key: key,
|
||||
name: name,
|
||||
arguments: arguments,
|
||||
restorationId: restorationId,
|
||||
transitionDuration: const Duration(milliseconds: 700),
|
||||
);
|
||||
}
|
||||
|
||||
class _MyReveal extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
final Animation<double> animation;
|
||||
|
||||
final Color color;
|
||||
|
||||
const _MyReveal({
|
||||
required this.child,
|
||||
required this.animation,
|
||||
required this.color,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_MyReveal> createState() => _MyRevealState();
|
||||
}
|
||||
|
||||
class _MyRevealState extends State<_MyReveal> {
|
||||
static final _log = Logger('_InkRevealState');
|
||||
|
||||
bool _finished = false;
|
||||
|
||||
final _tween = Tween(begin: const Offset(0, -1), end: Offset.zero);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
widget.animation.addStatusListener(_statusListener);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _MyReveal oldWidget) {
|
||||
if (oldWidget.animation != widget.animation) {
|
||||
oldWidget.animation.removeStatusListener(_statusListener);
|
||||
widget.animation.addStatusListener(_statusListener);
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.animation.removeStatusListener(_statusListener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
SlideTransition(
|
||||
position: _tween.animate(
|
||||
CurvedAnimation(
|
||||
parent: widget.animation,
|
||||
curve: Curves.easeOutCubic,
|
||||
reverseCurve: Curves.easeOutCubic,
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
color: widget.color,
|
||||
),
|
||||
),
|
||||
AnimatedOpacity(
|
||||
opacity: _finished ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: widget.child,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _statusListener(AnimationStatus status) {
|
||||
_log.fine(() => 'status: $status');
|
||||
switch (status) {
|
||||
case AnimationStatus.completed:
|
||||
setState(() {
|
||||
_finished = true;
|
||||
});
|
||||
break;
|
||||
case AnimationStatus.forward:
|
||||
case AnimationStatus.dismissed:
|
||||
case AnimationStatus.reverse:
|
||||
setState(() {
|
||||
_finished = false;
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
37
game_template/lib/src/style/palette.dart
Normal file
37
game_template/lib/src/style/palette.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright 2022, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A palette of colors to be used in the game.
|
||||
///
|
||||
/// The reason we're not going with something like Material Design's
|
||||
/// `Theme` is simply that this is simpler to work with and yet gives
|
||||
/// us everything we need for a game.
|
||||
///
|
||||
/// Games generally have more radical color palettes than apps. For example,
|
||||
/// every level of a game can have radically different colors.
|
||||
/// At the same time, games rarely support dark mode.
|
||||
///
|
||||
/// Colors taken from this fun palette:
|
||||
/// https://lospec.com/palette-list/crayola84
|
||||
///
|
||||
/// Colors here are implemented as getters so that hot reloading works.
|
||||
/// In practice, we could just as easily implement the colors
|
||||
/// as `static const`. But this way the palette is more malleable:
|
||||
/// we could allow players to customize colors, for example,
|
||||
/// or even get the colors from the network.
|
||||
class Palette {
|
||||
Color get pen => const Color(0xff1d75fb);
|
||||
Color get darkPen => const Color(0xFF0050bc);
|
||||
Color get redPen => const Color(0xFFd10841);
|
||||
Color get inkFullOpacity => const Color(0xff352b42);
|
||||
Color get ink => const Color(0xee352b42);
|
||||
Color get backgroundMain => const Color(0xffffffd1);
|
||||
Color get backgroundLevelSelection => const Color(0xffa2dcc7);
|
||||
Color get backgroundPlaySession => const Color(0xffffebb5);
|
||||
Color get background4 => const Color(0xffffd7ff);
|
||||
Color get backgroundSettings => const Color(0xffbfc8e3);
|
||||
Color get trueWhite => const Color(0xffffffff);
|
||||
}
|
||||
125
game_template/lib/src/style/responsive_screen.dart
Normal file
125
game_template/lib/src/style/responsive_screen.dart
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright 2022, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A widget that makes it easy to create a screen with a square-ish
|
||||
/// main area, a smaller menu area, and a small area for a message on top.
|
||||
/// It works in both orientations on mobile- and tablet-sized screens.
|
||||
class ResponsiveScreen extends StatelessWidget {
|
||||
/// This is the "hero" of the screen. It's more or less square, and will
|
||||
/// be placed in the visual "center" of the screen.
|
||||
final Widget squarishMainArea;
|
||||
|
||||
/// The second-largest area after [squarishMainArea]. It can be narrow
|
||||
/// or wide.
|
||||
final Widget rectangularMenuArea;
|
||||
|
||||
/// An area reserved for some static text close to the top of the screen.
|
||||
final Widget topMessageArea;
|
||||
|
||||
/// How much bigger should the [squarishMainArea] be compared to the other
|
||||
/// elements.
|
||||
final double mainAreaProminence;
|
||||
|
||||
const ResponsiveScreen({
|
||||
required this.squarishMainArea,
|
||||
required this.rectangularMenuArea,
|
||||
this.topMessageArea = const SizedBox.shrink(),
|
||||
this.mainAreaProminence = 0.8,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// This widget wants to fill the whole screen.
|
||||
final size = constraints.biggest;
|
||||
final padding = EdgeInsets.all(size.shortestSide / 30);
|
||||
|
||||
if (size.height >= size.width) {
|
||||
// "Portrait" / "mobile" mode.
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SafeArea(
|
||||
bottom: false,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: topMessageArea,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: (mainAreaProminence * 100).round(),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
minimum: padding,
|
||||
child: squarishMainArea,
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
top: false,
|
||||
maintainBottomViewPadding: true,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: rectangularMenuArea,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
// "Landscape" / "tablet" mode.
|
||||
final isLarge = size.width > 900;
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: isLarge ? 7 : 5,
|
||||
child: SafeArea(
|
||||
right: false,
|
||||
maintainBottomViewPadding: true,
|
||||
minimum: padding,
|
||||
child: squarishMainArea,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Column(
|
||||
children: [
|
||||
SafeArea(
|
||||
bottom: false,
|
||||
left: false,
|
||||
maintainBottomViewPadding: true,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: topMessageArea,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
left: false,
|
||||
maintainBottomViewPadding: true,
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: rectangularMenuArea,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
18
game_template/lib/src/style/snack_bar.dart
Normal file
18
game_template/lib/src/style/snack_bar.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright 2022, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shows [message] in a snack bar as long as a [ScaffoldMessengerState]
|
||||
/// with global key [scaffoldMessengerKey] is anywhere in the widget tree.
|
||||
void showSnackBar(String message) {
|
||||
final messenger = scaffoldMessengerKey.currentState;
|
||||
messenger?.showSnackBar(
|
||||
SnackBar(content: Text(message)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Use this when creating [MaterialApp] if you want [showSnackBar] to work.
|
||||
final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
|
||||
GlobalKey(debugLabel: 'scaffoldMessengerKey');
|
||||
Reference in New Issue
Block a user