After Width: | Height: | Size: 6.3 KiB |
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 5.9 KiB |
@ -0,0 +1,56 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/effects.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// {@template camera_zoom}
|
||||
/// Applies zoom to the camera of the game where this is added to
|
||||
/// {@endtemplate}
|
||||
class CameraZoom extends Effect with HasGameRef {
|
||||
/// {@macro camera_zoom}
|
||||
CameraZoom({
|
||||
required this.value,
|
||||
}) : super(
|
||||
EffectController(
|
||||
duration: 0.4,
|
||||
curve: Curves.easeOut,
|
||||
),
|
||||
);
|
||||
|
||||
/// The total zoom value to be applied to the camera
|
||||
final double value;
|
||||
|
||||
late final Tween<double> _tween;
|
||||
|
||||
final Completer<void> _completer = Completer();
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
_tween = Tween(
|
||||
begin: gameRef.camera.zoom,
|
||||
end: value,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void apply(double progress) {
|
||||
gameRef.camera.zoom = _tween.transform(progress);
|
||||
}
|
||||
|
||||
/// Returns a [Future] that completes once the zoom is finished
|
||||
Future<void> get completed {
|
||||
if (controller.completed) {
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
return _completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
void onRemove() {
|
||||
_completer.complete();
|
||||
|
||||
super.onRemove();
|
||||
}
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
// ignore_for_file: avoid_renaming_method_parameters
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
/// {@template slingshots}
|
||||
/// A [Blueprint] which creates the left and right pairs of [Slingshot]s.
|
||||
/// {@endtemplate}
|
||||
class Slingshots extends Forge2DBlueprint {
|
||||
@override
|
||||
void build(_) {
|
||||
// TODO(allisonryan0002): use radians values instead of converting degrees.
|
||||
final leftUpperSlingshot = Slingshot(
|
||||
length: 5.66,
|
||||
angle: -1.5 * (math.pi / 180),
|
||||
spritePath: Assets.images.slingshot.leftUpper.keyName,
|
||||
)..initialPosition = Vector2(-29, 1.5);
|
||||
|
||||
final leftLowerSlingshot = Slingshot(
|
||||
length: 3.54,
|
||||
angle: -29.1 * (math.pi / 180),
|
||||
spritePath: Assets.images.slingshot.leftLower.keyName,
|
||||
)..initialPosition = Vector2(-31, -6.2);
|
||||
|
||||
final rightUpperSlingshot = Slingshot(
|
||||
length: 5.64,
|
||||
angle: 1 * (math.pi / 180),
|
||||
spritePath: Assets.images.slingshot.rightUpper.keyName,
|
||||
)..initialPosition = Vector2(22.3, 1.58);
|
||||
|
||||
final rightLowerSlingshot = Slingshot(
|
||||
length: 3.46,
|
||||
angle: 26.8 * (math.pi / 180),
|
||||
spritePath: Assets.images.slingshot.rightLower.keyName,
|
||||
)..initialPosition = Vector2(24.7, -6.2);
|
||||
|
||||
addAll([
|
||||
leftUpperSlingshot,
|
||||
leftLowerSlingshot,
|
||||
rightUpperSlingshot,
|
||||
rightLowerSlingshot,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/// {@template slingshot}
|
||||
/// Elastic bumper that bounces the [Ball] off of its straight sides.
|
||||
/// {@endtemplate}
|
||||
class Slingshot extends BodyComponent with InitialPosition {
|
||||
/// {@macro slingshot}
|
||||
Slingshot({
|
||||
required double length,
|
||||
required double angle,
|
||||
required String spritePath,
|
||||
}) : _length = length,
|
||||
_angle = angle,
|
||||
_spritePath = spritePath,
|
||||
super(priority: 1);
|
||||
|
||||
final double _length;
|
||||
|
||||
final double _angle;
|
||||
|
||||
final String _spritePath;
|
||||
|
||||
List<FixtureDef> _createFixtureDefs() {
|
||||
final fixturesDef = <FixtureDef>[];
|
||||
const circleRadius = 1.55;
|
||||
|
||||
final topCircleShape = CircleShape()..radius = circleRadius;
|
||||
topCircleShape.position.setValues(0, _length / 2);
|
||||
final topCircleFixtureDef = FixtureDef(topCircleShape)..friction = 0;
|
||||
fixturesDef.add(topCircleFixtureDef);
|
||||
|
||||
final bottomCircleShape = CircleShape()..radius = circleRadius;
|
||||
bottomCircleShape.position.setValues(0, -_length / 2);
|
||||
final bottomCircleFixtureDef = FixtureDef(bottomCircleShape)..friction = 0;
|
||||
fixturesDef.add(bottomCircleFixtureDef);
|
||||
|
||||
final leftEdgeShape = EdgeShape()
|
||||
..set(
|
||||
Vector2(circleRadius, _length / 2),
|
||||
Vector2(circleRadius, -_length / 2),
|
||||
);
|
||||
final leftEdgeShapeFixtureDef = FixtureDef(leftEdgeShape)
|
||||
..friction = 0
|
||||
..restitution = 5;
|
||||
fixturesDef.add(leftEdgeShapeFixtureDef);
|
||||
|
||||
final rightEdgeShape = EdgeShape()
|
||||
..set(
|
||||
Vector2(-circleRadius, _length / 2),
|
||||
Vector2(-circleRadius, -_length / 2),
|
||||
);
|
||||
final rightEdgeShapeFixtureDef = FixtureDef(rightEdgeShape)
|
||||
..friction = 0
|
||||
..restitution = 5;
|
||||
fixturesDef.add(rightEdgeShapeFixtureDef);
|
||||
|
||||
return fixturesDef;
|
||||
}
|
||||
|
||||
@override
|
||||
Body createBody() {
|
||||
final bodyDef = BodyDef()
|
||||
..userData = this
|
||||
..position = initialPosition
|
||||
..angle = _angle;
|
||||
|
||||
final body = world.createBody(bodyDef);
|
||||
_createFixtureDefs().forEach(body.createFixture);
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
await _loadSprite();
|
||||
renderBody = false;
|
||||
}
|
||||
|
||||
Future<void> _loadSprite() async {
|
||||
final sprite = await gameRef.loadSprite(_spritePath);
|
||||
|
||||
await add(
|
||||
SpriteComponent(
|
||||
sprite: sprite,
|
||||
size: sprite.originalSize / 10,
|
||||
anchor: Anchor.center,
|
||||
angle: _angle,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:sandbox/common/common.dart';
|
||||
import 'package:sandbox/stories/ball/basic_ball_game.dart';
|
||||
|
||||
class SlingshotGame extends BasicBallGame {
|
||||
SlingshotGame({
|
||||
required this.trace,
|
||||
}) : super(color: const Color(0xFFFF0000));
|
||||
|
||||
static const info = '''
|
||||
Shows how Slingshots are rendered.
|
||||
|
||||
- Activate the "trace" parameter to overlay the body.
|
||||
- Tap anywhere on the screen to spawn a ball into the game.
|
||||
''';
|
||||
|
||||
final bool trace;
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
|
||||
final center = screenToWorld(camera.viewport.canvasSize! / 2);
|
||||
|
||||
final leftUpperSlingshot = Slingshot(
|
||||
length: 5.66,
|
||||
angle: -1.5 * (math.pi / 180),
|
||||
spritePath: Assets.images.slingshot.leftUpper.keyName,
|
||||
)..initialPosition = center + Vector2(-29, 1.5);
|
||||
|
||||
final leftLowerSlingshot = Slingshot(
|
||||
length: 3.54,
|
||||
angle: -29.1 * (math.pi / 180),
|
||||
spritePath: Assets.images.slingshot.leftLower.keyName,
|
||||
)..initialPosition = center + Vector2(-31, -6.2);
|
||||
|
||||
final rightUpperSlingshot = Slingshot(
|
||||
length: 5.64,
|
||||
angle: 1 * (math.pi / 180),
|
||||
spritePath: Assets.images.slingshot.rightUpper.keyName,
|
||||
)..initialPosition = center + Vector2(22.3, 1.58);
|
||||
|
||||
final rightLowerSlingshot = Slingshot(
|
||||
length: 3.46,
|
||||
angle: 26.8 * (math.pi / 180),
|
||||
spritePath: Assets.images.slingshot.rightLower.keyName,
|
||||
)..initialPosition = center + Vector2(24.7, -6.2);
|
||||
|
||||
await addAll([
|
||||
leftUpperSlingshot,
|
||||
leftLowerSlingshot,
|
||||
rightUpperSlingshot,
|
||||
rightLowerSlingshot,
|
||||
]);
|
||||
|
||||
if (trace) {
|
||||
leftUpperSlingshot.trace();
|
||||
leftLowerSlingshot.trace();
|
||||
rightUpperSlingshot.trace();
|
||||
rightLowerSlingshot.trace();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import 'package:dashbook/dashbook.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:sandbox/common/common.dart';
|
||||
import 'package:sandbox/stories/slingshot/slingshot_game.dart';
|
||||
|
||||
void addSlingshotStories(Dashbook dashbook) {
|
||||
dashbook.storiesOf('Slingshots').add(
|
||||
'Basic',
|
||||
(context) => GameWidget(
|
||||
game: SlingshotGame(
|
||||
trace: context.boolProperty('Trace', true),
|
||||
),
|
||||
),
|
||||
codeLink: buildSourceLink('slingshot_game/basic.dart'),
|
||||
info: SlingshotGame.info,
|
||||
);
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/input.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:sandbox/common/common.dart';
|
||||
|
||||
class BasicCameraZoomGame extends BasicGame with TapDetector {
|
||||
static const info = '''
|
||||
Simple game to demonstrate how the CameraZoom can be used.
|
||||
Tap to zoom in/out
|
||||
''';
|
||||
|
||||
bool zoomApplied = false;
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
final sprite = await loadSprite(Assets.images.flutterSignPost.keyName);
|
||||
|
||||
await add(
|
||||
SpriteComponent(
|
||||
sprite: sprite,
|
||||
size: Vector2(4, 8),
|
||||
anchor: Anchor.center,
|
||||
),
|
||||
);
|
||||
|
||||
camera.followVector2(Vector2.zero());
|
||||
}
|
||||
|
||||
@override
|
||||
void onTap() {
|
||||
if (firstChild<CameraZoom>() == null) {
|
||||
final zoom = CameraZoom(value: zoomApplied ? 30 : 10);
|
||||
add(zoom);
|
||||
zoomApplied = !zoomApplied;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import 'package:dashbook/dashbook.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:sandbox/common/common.dart';
|
||||
import 'package:sandbox/stories/zoom/basic_zoom_game.dart';
|
||||
|
||||
void addZoomStories(Dashbook dashbook) {
|
||||
dashbook.storiesOf('CameraZoom').add(
|
||||
'Basic',
|
||||
(context) => GameWidget(
|
||||
game: BasicCameraZoomGame(),
|
||||
),
|
||||
codeLink: buildSourceLink('zoom/basic_zoom_game.dart'),
|
||||
info: BasicCameraZoomGame.info,
|
||||
);
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
// ignore_for_file: cascade_invocations
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
import '../../helpers/helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('CameraZoom', () {
|
||||
final tester = FlameTester(TestGame.new);
|
||||
|
||||
tester.testGameWidget(
|
||||
'renders correctly',
|
||||
setUp: (game, tester) async {
|
||||
game.camera.followVector2(Vector2.zero());
|
||||
game.camera.zoom = 10;
|
||||
final sprite = await game.loadSprite(
|
||||
Assets.images.flutterSignPost.keyName,
|
||||
);
|
||||
|
||||
await game.add(
|
||||
SpriteComponent(
|
||||
sprite: sprite,
|
||||
size: Vector2(4, 8),
|
||||
anchor: Anchor.center,
|
||||
),
|
||||
);
|
||||
|
||||
await game.add(CameraZoom(value: 40));
|
||||
},
|
||||
verify: (game, tester) async {
|
||||
await expectLater(
|
||||
find.byGame<TestGame>(),
|
||||
matchesGoldenFile('golden/camera_zoom/no_zoom.png'),
|
||||
);
|
||||
|
||||
game.update(0.2);
|
||||
await tester.pump();
|
||||
await expectLater(
|
||||
find.byGame<TestGame>(),
|
||||
matchesGoldenFile('golden/camera_zoom/in_between.png'),
|
||||
);
|
||||
|
||||
game.update(0.4);
|
||||
await tester.pump();
|
||||
await expectLater(
|
||||
find.byGame<TestGame>(),
|
||||
matchesGoldenFile('golden/camera_zoom/finished.png'),
|
||||
);
|
||||
game.update(0.1);
|
||||
await tester.pump();
|
||||
|
||||
expect(game.firstChild<CameraZoom>(), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
tester.test(
|
||||
'completes when checked after it is finished',
|
||||
(game) async {
|
||||
await game.add(CameraZoom(value: 40));
|
||||
game.update(10);
|
||||
final cameraZoom = game.firstChild<CameraZoom>();
|
||||
final future = cameraZoom!.completed;
|
||||
|
||||
expect(future, completes);
|
||||
},
|
||||
);
|
||||
|
||||
tester.test(
|
||||
'completes when checked before it is finished',
|
||||
(game) async {
|
||||
final zoom = CameraZoom(value: 40);
|
||||
final future = zoom.completed;
|
||||
|
||||
await game.add(zoom);
|
||||
game.update(10);
|
||||
game.update(0);
|
||||
|
||||
expect(future, completes);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 49 KiB |
@ -0,0 +1,97 @@
|
||||
// ignore_for_file: cascade_invocations
|
||||
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
|
||||
import '../../helpers/helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('Slingshot', () {
|
||||
final flameTester = FlameTester(TestGame.new);
|
||||
const length = 2.0;
|
||||
const angle = 0.0;
|
||||
final spritePath = Assets.images.slingshot.leftUpper.keyName;
|
||||
|
||||
flameTester.testGameWidget(
|
||||
'renders correctly',
|
||||
setUp: (game, tester) async {
|
||||
await game.addFromBlueprint(Slingshots());
|
||||
await game.ready();
|
||||
game.camera.followVector2(Vector2.zero());
|
||||
},
|
||||
// TODO(allisonryan0002): enable test when workflows are fixed.
|
||||
// verify: (game, tester) async {
|
||||
// await expectLater(
|
||||
// find.byGame<Forge2DGame>(),
|
||||
// matchesGoldenFile('golden/slingshots.png'),
|
||||
// );
|
||||
// },
|
||||
);
|
||||
|
||||
flameTester.test(
|
||||
'loads correctly',
|
||||
(game) async {
|
||||
final slingshot = Slingshot(
|
||||
length: length,
|
||||
angle: angle,
|
||||
spritePath: spritePath,
|
||||
);
|
||||
await game.ensureAdd(slingshot);
|
||||
|
||||
expect(game.contains(slingshot), isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
flameTester.test(
|
||||
'body is static',
|
||||
(game) async {
|
||||
final slingshot = Slingshot(
|
||||
length: length,
|
||||
angle: angle,
|
||||
spritePath: spritePath,
|
||||
);
|
||||
await game.ensureAdd(slingshot);
|
||||
|
||||
expect(slingshot.body.bodyType, equals(BodyType.static));
|
||||
},
|
||||
);
|
||||
|
||||
flameTester.test(
|
||||
'has restitution',
|
||||
(game) async {
|
||||
final slingshot = Slingshot(
|
||||
length: length,
|
||||
angle: angle,
|
||||
spritePath: spritePath,
|
||||
);
|
||||
await game.ensureAdd(slingshot);
|
||||
|
||||
final totalRestitution = slingshot.body.fixtures.fold<double>(
|
||||
0,
|
||||
(total, fixture) => total + fixture.restitution,
|
||||
);
|
||||
expect(totalRestitution, greaterThan(0));
|
||||
},
|
||||
);
|
||||
|
||||
flameTester.test(
|
||||
'has no friction',
|
||||
(game) async {
|
||||
final slingshot = Slingshot(
|
||||
length: length,
|
||||
angle: angle,
|
||||
spritePath: spritePath,
|
||||
);
|
||||
await game.ensureAdd(slingshot);
|
||||
|
||||
final totalFriction = slingshot.body.fixtures.fold<double>(
|
||||
0,
|
||||
(total, fixture) => total + fixture.friction,
|
||||
);
|
||||
expect(totalFriction, equals(0));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|