Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Callbacks in HudButtonComponent constructor and ViewportMargin mixin to avoid code duplication #1685

Merged
merged 7 commits into from
Jun 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/lib/stories/input/joystick_player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class JoystickPlayer extends SpriteComponent
Future<void> onLoad() async {
sprite = await gameRef.loadSprite('layers/player.png');
position = gameRef.size / 2;
add(RectangleHitbox()..debugMode = true);
add(RectangleHitbox());
}

@override
Expand Down
1 change: 1 addition & 0 deletions packages/flame/lib/components.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export 'src/components/fps_text_component.dart';
export 'src/components/input/joystick_component.dart';
export 'src/components/input/keyboard_listener_component.dart';
export 'src/components/isometric_tile_map_component.dart';
export 'src/components/mixins/component_viewport_margin.dart';
export 'src/components/mixins/draggable.dart';
export 'src/components/mixins/gesture_hitboxes.dart';
export 'src/components/mixins/has_game_ref.dart';
Expand Down
25 changes: 12 additions & 13 deletions packages/flame/lib/src/components/input/button_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,20 @@ class ButtonComponent extends PositionComponent with Tappable {
late final PositionComponent? buttonDown;

/// Callback for what should happen when the button is pressed.
/// If you want to interact with [onTapUp] or [onTapCancel] it is recommended
/// If you want to interact with [onTapCancel] it is recommended
/// to extend [ButtonComponent].
void Function()? onPressed;

/// Callback for what should happen when the button is released.
/// If you want to interact with [onTapCancel] it is recommended
/// to extend [ButtonComponent].
void Function()? onReleased;

ButtonComponent({
this.button,
this.buttonDown,
this.onPressed,
this.onReleased,
Vector2? position,
Vector2? size,
Vector2? scale,
Expand Down Expand Up @@ -54,12 +60,8 @@ class ButtonComponent extends PositionComponent with Tappable {
@override
@mustCallSuper
bool onTapDown(TapDownInfo info) {
if (buttonDown != null) {
if (button != null) {
remove(button!);
}
add(buttonDown!);
}
button?.removeFromParent();
buttonDown?.changeParent(this);
onPressed?.call();
return false;
}
Expand All @@ -68,18 +70,15 @@ class ButtonComponent extends PositionComponent with Tappable {
@mustCallSuper
bool onTapUp(TapUpInfo info) {
onTapCancel();
onReleased?.call();
return true;
}

@override
@mustCallSuper
bool onTapCancel() {
if (buttonDown != null) {
remove(buttonDown!);
if (button != null) {
add(button!);
}
}
buttonDown?.removeFromParent();
button?.changeParent(this);
return false;
}
}
71 changes: 12 additions & 59 deletions packages/flame/lib/src/components/input/hud_button_component.dart
Original file line number Diff line number Diff line change
@@ -1,33 +1,20 @@
import 'package:flame/components.dart';
import 'package:flame/input.dart';
import 'package:flutter/rendering.dart' show EdgeInsets;
import 'package:meta/meta.dart';

/// The [HudButtonComponent] bundles two [PositionComponent]s, one that shows
/// when the button is being pressed, and one that shows otherwise.
///
/// Note: You have to set the [button] in [onLoad] if you are not passing it in
/// through the constructor.
class HudButtonComponent extends HudMarginComponent with Tappable {
late final PositionComponent? button;
late final PositionComponent? buttonDown;

/// Callback for what should happen when the button is pressed.
/// If you want to directly interact with [onTapUp], [onTapDown] or
/// [onTapCancel] it is recommended to extend [HudButtonComponent].
void Function()? onPressed;

/// Callback for what should happen when the button is released.
/// If you want to directly interact with [onTapUp], [onTapDown] or
/// [onTapCancel] it is recommended to extend [HudButtonComponent].
void Function()? onReleased;

class HudButtonComponent extends ButtonComponent
with HasGameRef, ComponentViewportMargin {
HudButtonComponent({
this.button,
this.buttonDown,
PositionComponent? button,
PositionComponent? buttonDown,
EdgeInsets? margin,
this.onPressed,
this.onReleased,
Function()? onPressed,
Function()? onReleased,
Vector2? position,
Vector2? size,
Vector2? scale,
Expand All @@ -36,52 +23,18 @@ class HudButtonComponent extends HudMarginComponent with Tappable {
Iterable<Component>? children,
int? priority,
}) : super(
margin: margin,
button: button,
buttonDown: buttonDown,
position: position,
onPressed: onPressed,
onReleased: onReleased,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldnt this be part of a separate PR? For the sake of clear commits

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was the other way around really, I wanted the button components to be the same and to do that I introduced the mixin, I can change the title to reflect on that.

size: size ?? button?.size,
scale: scale,
angle: angle,
anchor: anchor,
children: children,
priority: priority,
);

@override
@mustCallSuper
void onMount() {
super.onMount();
assert(
button != null,
'The button has to either be passed in as an argument or set in onLoad',
);
final idleButton = button;
if (idleButton != null && !contains(idleButton)) {
add(idleButton);
}
}

@override
@mustCallSuper
bool onTapDown(TapDownInfo info) {
button?.removeFromParent();
buttonDown?.changeParent(this);
onPressed?.call();
return false;
}

@override
@mustCallSuper
bool onTapUp(TapUpInfo info) {
onTapCancel();
onReleased?.call();
return true;
}

@override
@mustCallSuper
bool onTapCancel() {
buttonDown?.removeFromParent();
button?.changeParent(this);
return false;
) {
this.margin = margin;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter/widgets.dart' show EdgeInsets;
import 'package:meta/meta.dart';

/// The [ComponentViewportMargin] positions itself by a margin to the edge of
/// the [Viewport] instead of by an absolute position on the screen or on the
/// game, so if the game is resized the component will move to keep its margin.
///
/// Note that the margin is calculated to the [Anchor], not to the edge of the
/// component.
///
/// If you set the position of the component instead of a margin when
/// initializing the component, the margin to the edge of the screen from that
/// position will be used.
mixin ComponentViewportMargin on PositionComponent, HasGameRef {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idk if hud button or button component have tests, but if they don't this would be a good opportunity to introduce a test suite that would end up testing both things.

@override
PositionType positionType = PositionType.viewport;

/// Instead of setting a position of the [PositionComponent] that uses
/// [ComponentViewportMargin] a margin from the edges of the viewport can be
/// used instead.
EdgeInsets? margin;

@override
@mustCallSuper
Future<void> onLoad() async {
super.onLoad();
// If margin is not null we will update the position `onGameResize` instead
if (margin == null) {
final screenSize = gameRef.size;
final topLeft = anchor.toOtherAnchorPosition(
position,
Anchor.topLeft,
scaledSize,
);
final bottomRight = screenSize -
anchor.toOtherAnchorPosition(
position,
Anchor.bottomRight,
scaledSize,
);
margin = EdgeInsets.fromLTRB(
topLeft.x,
topLeft.y,
bottomRight.x,
bottomRight.y,
);
} else {
size.addListener(_updateMargins);
}
_updateMargins();
}

@override
@mustCallSuper
void onGameResize(Vector2 gameSize) {
super.onGameResize(gameSize);
if (isMounted) {
_updateMargins();
}
}

void _updateMargins() {
final screenSize = positionType == PositionType.viewport
? gameRef.camera.viewport.effectiveSize
: gameRef.canvasSize;
final margin = this.margin!;
final x = margin.left != 0
? margin.left + scaledSize.x / 2
: screenSize.x - margin.right - scaledSize.x / 2;
final y = margin.top != 0
? margin.top + scaledSize.y / 2
: screenSize.y - margin.bottom - scaledSize.y / 2;
position.setValues(x, y);
position = Anchor.center.toOtherAnchorPosition(
position,
anchor,
scaledSize,
);
}
}
111 changes: 111 additions & 0 deletions packages/flame/test/components/button_component_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import 'package:flame/components.dart';
import 'package:flame/input.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';

import '../game/flame_game_test.dart';

void main() async {
group('ButtonComponent', () {
testWithGame<GameWithTappables>(
'correctly registers taps', GameWithTappables.new, (game) async {
var pressedTimes = 0;
var releasedTimes = 0;
final initialGameSize = Vector2.all(100);
final componentSize = Vector2.all(10);
final buttonPosition = Vector2.all(100);
late final ButtonComponent button;
game.onGameResize(initialGameSize);
await game.ensureAdd(
button = ButtonComponent(
button: RectangleComponent(size: componentSize),
onPressed: () => pressedTimes++,
onReleased: () => releasedTimes++,
position: buttonPosition,
size: componentSize,
),
);

expect(pressedTimes, 0);
expect(releasedTimes, 0);

game.onTapDown(1, createTapDownEvent(game));
expect(pressedTimes, 0);
expect(releasedTimes, 0);

game.onTapUp(
1,
createTapUpEvent(
game,
globalPosition: button.positionOfAnchor(Anchor.center).toOffset(),
),
);
expect(pressedTimes, 0);
expect(releasedTimes, 0);

game.onTapDown(
1,
createTapDownEvent(
game,
globalPosition: buttonPosition.toOffset(),
),
);
expect(pressedTimes, 1);
expect(releasedTimes, 0);

game.onTapUp(
1,
createTapUpEvent(
game,
globalPosition: buttonPosition.toOffset(),
),
);
expect(pressedTimes, 1);
expect(releasedTimes, 1);
});

testWithGame<GameWithTappables>(
'correctly registers taps onGameResize', GameWithTappables.new,
(game) async {
var pressedTimes = 0;
var releasedTimes = 0;
final initialGameSize = Vector2.all(100);
final componentSize = Vector2.all(10);
final buttonPosition = Vector2.all(100);
late final ButtonComponent button;
game.onGameResize(initialGameSize);
await game.ensureAdd(
button = ButtonComponent(
button: RectangleComponent(size: componentSize),
onPressed: () => pressedTimes++,
onReleased: () => releasedTimes++,
position: buttonPosition,
size: componentSize,
),
);
final previousPosition =
button.positionOfAnchor(Anchor.center).toOffset();
game.onGameResize(initialGameSize * 2);

game.onTapDown(
1,
createTapDownEvent(
game,
globalPosition: previousPosition,
),
);
expect(pressedTimes, 1);
expect(releasedTimes, 0);

game.onTapUp(
1,
createTapUpEvent(
game,
globalPosition: previousPosition,
),
);
expect(pressedTimes, 1);
expect(releasedTimes, 1);
});
});
}
Loading