1
0
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:
Filip Hracek
2022-05-10 15:08:43 +02:00
committed by GitHub
parent 5143bcf302
commit daa024a829
208 changed files with 8993 additions and 0 deletions

View 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);
}

View 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;
}
}
}

View 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);
}

View 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,
),
),
),
)
],
),
),
],
);
}
},
);
}
}

View 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');