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: Add lookAt method for PositionComponent #1891

Merged
merged 33 commits into from
Sep 18, 2022
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
75c324e
Add lookAt method
ufrshubham Sep 6, 2022
a4287c9
Add test for lookAt
ufrshubham Sep 6, 2022
d269b52
Add angularOffset
ufrshubham Sep 6, 2022
39e3f83
Replace angularOffset with nativeAngle
ufrshubham Sep 10, 2022
fdc1985
Merge branch 'main' into feat/lookAt
ufrshubham Sep 10, 2022
609f239
Add lookAt tests with nativeAngle
ufrshubham Sep 10, 2022
3d05af8
Merge branch 'main' into feat/lookAt
ufrshubham Sep 10, 2022
8ba397c
Resolve merge conflicts
ufrshubham Sep 10, 2022
d2d3048
Add nativeAngle as optional parameter
ufrshubham Sep 10, 2022
f48b3a3
Remove _nativeDirection
ufrshubham Sep 10, 2022
7f65989
Add angleTo
ufrshubham Sep 11, 2022
02eb465
Add corner case for lookAt
ufrshubham Sep 11, 2022
e1ad4ea
Revert "Add nativeAngle as optional parameter"
ufrshubham Sep 11, 2022
d8c500e
Add nativeAngle as input for sprite components
ufrshubham Sep 11, 2022
e26083e
Merge branch 'main' into feat/lookAt
ufrshubham Sep 11, 2022
edc4948
Consider absoluteAngle in angleTo calculations
ufrshubham Sep 11, 2022
ab218d8
Add lookAt test for nested components
ufrshubham Sep 11, 2022
08957d8
Update comments with csys for target
ufrshubham Sep 12, 2022
c9d7e2f
Auto-format components.md
ufrshubham Sep 13, 2022
01a2173
Add nativeAngle docs
ufrshubham Sep 13, 2022
b5e3509
Add lookAt and angleTo examples
ufrshubham Sep 13, 2022
2870f4d
Fixed codelink
ufrshubham Sep 13, 2022
4df5648
Merge branch 'main' into feat/lookAt
ufrshubham Sep 13, 2022
ec53148
Revert auto-format changes
ufrshubham Sep 13, 2022
f213ec3
Fix grammar in example description
ufrshubham Sep 13, 2022
7d75fdd
Rename comp to component
ufrshubham Sep 13, 2022
48bbed3
Return delta angle from angleTo
ufrshubham Sep 13, 2022
91ff297
Update expected values for angleTo tests
ufrshubham Sep 13, 2022
499eef0
Use RotateEffect.by instead of RotateEffect.to
ufrshubham Sep 13, 2022
09fdc45
Merge branch 'main' into feat/lookAt
ufrshubham Sep 13, 2022
1863a15
Merge branch 'main' into feat/lookAt
ufrshubham Sep 17, 2022
2a9de0c
Merge branch 'main' into feat/lookAt
ufrshubham Sep 18, 2022
51266f6
Merge branch 'main' into feat/lookAt
ufrshubham Sep 18, 2022
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
16 changes: 16 additions & 0 deletions doc/flame/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,22 @@ The `angle` is the rotation angle around the anchor, represented as a double in
relative to the parent's angle.


### Native Angle
ufrshubham marked this conversation as resolved.
Show resolved Hide resolved

The `nativeAngle` is an angle in radians, measured clockwise, representing the default orientation of the component. It can be used to define the direction in which the component is facing when [angle](#angle) is zero.

It is specially helpful when making a sprite based component look at a specific target. If the original image of the sprite is not facing in the up/north direction, the calculated angle to make the component look at the target will need some offset to make it look correct. For such cases, `nativeAngle` can be used to let the component know what direction the original image is faces.

An example could be a bullet image pointing in east direction. In this case `nativeAngle` can be set to pi/2 radians. Following are some common directions and their correspondin native angle values.

Direction | Native Angle | In degrees
----------|--------------|-------------
Up/North | 0 | 0
Down/South| pi or -pi | 180 or -180
Left/West | -pi/2 | -90
Right/East| pi/2 | 90


### Anchor

The `anchor` is where on the component that the position and rotation should be defined from (the
Expand Down
14 changes: 14 additions & 0 deletions examples/lib/stories/components/components.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import 'package:examples/commons/commons.dart';
import 'package:examples/stories/components/composability_example.dart';
import 'package:examples/stories/components/debug_example.dart';
import 'package:examples/stories/components/game_in_game_example.dart';
import 'package:examples/stories/components/look_at_example.dart';
import 'package:examples/stories/components/look_at_smooth_example.dart';
import 'package:examples/stories/components/priority_example.dart';
import 'package:flame/game.dart';

Expand Down Expand Up @@ -31,5 +33,17 @@ void addComponentsStories(Dashbook dashbook) {
(_) => GameWidget(game: GameInGameExample()),
codeLink: baseLink('components/game_in_game_example.dart'),
info: GameInGameExample.description,
)
..add(
'Look At',
(_) => GameWidget(game: LookAtExample()),
codeLink: baseLink('components/look_at_example.dart'),
info: LookAtExample.description,
)
..add(
'Look At Smooth',
(_) => GameWidget(game: LookAtSmoothExample()),
codeLink: baseLink('components/look_at_smooth_example.dart'),
info: LookAtExample.description,
);
}
114 changes: 114 additions & 0 deletions examples/lib/stories/components/look_at_example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import 'dart:math';

import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flame/palette.dart';
import 'package:flame/sprite.dart';
import 'package:flutter/material.dart';

class LookAtExample extends FlameGame with TapDetector {
static const description = 'This example demonstrates how a component can be '
'made to look at a specific target using the lookAt method. Tap anywhere '
'to change the target point for both the choppers. '
'It also shows how nativeAngle can be used to make the component '
'oriented in the desired direction if the image is not facing the '
'correct direction.';

late SpriteAnimationComponent _chopper1;
late SpriteAnimationComponent _chopper2;

final CircleComponent _targetComponent = CircleComponent(
radius: 5,
anchor: Anchor.center,
paint: BasicPalette.black.paint(),
);

@override
Color backgroundColor() => const Color.fromARGB(255, 96, 145, 112);

@override
Future<void>? onLoad() async {
camera.viewport = FixedResolutionViewport(Vector2(640, 360));
final spriteSheet = SpriteSheet(
image: await images.load('animations/chopper.png'),
srcSize: Vector2.all(48),
);

_spawnChoppers(spriteSheet);
_spawnInfoText();

return super.onLoad();
}

@override
void onTapDown(TapDownInfo info) {
if (!_targetComponent.isMounted) {
add(_targetComponent);
}
_targetComponent.position = info.eventPosition.game;

_chopper1.lookAt(_targetComponent.absolutePosition);
_chopper2.lookAt(_targetComponent.absolutePosition);

super.onTapDown(info);
}

void _spawnChoppers(SpriteSheet spriteSheet) {
// Notice now the nativeAngle is set to pi because the chopper
// is facing in down/south direction in the original image.
add(
_chopper1 = SpriteAnimationComponent(
nativeAngle: pi,
size: Vector2.all(64),
anchor: Anchor.center,
animation: spriteSheet.createAnimation(row: 0, stepTime: 0.05),
position: Vector2(size.x * 0.3, size.y * 0.5),
),
);

// This chopper does not use correct nativeAngle, hence using
// lookAt on it results in the sprite pointing in incorrect
// direction visually.
add(
_chopper2 = SpriteAnimationComponent(
size: Vector2.all(64),
anchor: Anchor.center,
animation: spriteSheet.createAnimation(row: 0, stepTime: 0.05),
position: Vector2(size.x * 0.6, size.y * 0.5),
),
);
}

// Just displays some information. No functional contribution to the example.
void _spawnInfoText() {
final _shaded = TextPaint(
style: TextStyle(
color: BasicPalette.white.color,
fontSize: 20.0,
shadows: const [
Shadow(offset: Offset(1, 1), blurRadius: 1),
],
),
);

add(
TextComponent(
text: 'nativeAngle = pi',
textRenderer: _shaded,
anchor: Anchor.center,
position: _chopper1.absolutePosition + Vector2(0, -50),
),
);

add(
TextComponent(
text: 'nativeAngle = 0',
textRenderer: _shaded,
anchor: Anchor.center,
position: _chopper2.absolutePosition + Vector2(0, -50),
),
);
}
}
134 changes: 134 additions & 0 deletions examples/lib/stories/components/look_at_smooth_example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import 'dart:math';

import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/extensions.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flame/palette.dart';
import 'package:flame/sprite.dart';
import 'package:flutter/material.dart';

class LookAtSmoothExample extends FlameGame with TapDetector {
static const description = 'This example demonstrates how a component can be '
'made to smoothly rotate towards a target using the angleTo method. '
'Tap anywhere to change the target point for both the choppers. '
'It also shows how nativeAngle can be used to make the component '
'oriented in the desired direction if the image is not facing the '
'correct direction.';

bool _isRotating = false;
late SpriteAnimationComponent _chopper1;
late SpriteAnimationComponent _chopper2;

final CircleComponent _targetComponent = CircleComponent(
radius: 5,
anchor: Anchor.center,
paint: BasicPalette.black.paint(),
);

@override
Color backgroundColor() => const Color.fromARGB(255, 96, 145, 112);

@override
Future<void>? onLoad() async {
camera.viewport = FixedResolutionViewport(Vector2(640, 360));
final spriteSheet = SpriteSheet(
image: await images.load('animations/chopper.png'),
srcSize: Vector2.all(48),
);

_spawnChoppers(spriteSheet);
_spawnInfoText();

return super.onLoad();
}

@override
void onTapDown(TapDownInfo info) {
if (!_targetComponent.isMounted) {
add(_targetComponent);
}

// Ignore if choppers are already rotating.
if (!_isRotating) {
_isRotating = true;
_targetComponent.position = info.eventPosition.game;

_chopper1.add(
RotateEffect.by(
_chopper1.angleTo(_targetComponent.absolutePosition),
LinearEffectController(1),
onComplete: () => _isRotating = false,
),
);

_chopper2.add(
RotateEffect.by(
_chopper2.angleTo(_targetComponent.absolutePosition),
LinearEffectController(1),
onComplete: () => _isRotating = false,
),
);
}

super.onTapDown(info);
}

void _spawnChoppers(SpriteSheet spriteSheet) {
// Notice now the nativeAngle is set to pi because the chopper
// is facing in down/south direction in the original image.
add(
_chopper1 = SpriteAnimationComponent(
nativeAngle: pi,
size: Vector2.all(64),
anchor: Anchor.center,
animation: spriteSheet.createAnimation(row: 0, stepTime: 0.05),
position: Vector2(size.x * 0.3, size.y * 0.5),
),
);

// This chopper does not use correct nativeAngle, hence using
// lookAt on it results in the sprite pointing in incorrect
// direction visually.
add(
_chopper2 = SpriteAnimationComponent(
size: Vector2.all(64),
anchor: Anchor.center,
animation: spriteSheet.createAnimation(row: 0, stepTime: 0.05),
position: Vector2(size.x * 0.6, size.y * 0.5),
),
);
}

// Just displays some information. No functional contribution to the example.
void _spawnInfoText() {
final _shaded = TextPaint(
style: TextStyle(
color: BasicPalette.white.color,
fontSize: 20.0,
shadows: const [
Shadow(offset: Offset(1, 1), blurRadius: 1),
],
),
);

add(
TextComponent(
text: 'nativeAngle = pi',
textRenderer: _shaded,
anchor: Anchor.center,
position: _chopper1.absolutePosition + Vector2(0, -50),
),
);

add(
TextComponent(
text: 'nativeAngle = 0',
textRenderer: _shaded,
anchor: Anchor.center,
position: _chopper2.absolutePosition + Vector2(0, -50),
),
);
}
}
37 changes: 37 additions & 0 deletions packages/flame/lib/src/components/position_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class PositionComponent extends Component
Vector2? size,
Vector2? scale,
double? angle,
this.nativeAngle = 0,
Anchor? anchor,
super.children,
super.priority,
Expand All @@ -98,6 +99,15 @@ class PositionComponent extends Component
final NotifyingVector2 _size;
Anchor _anchor;

/// The angle where this component is looking at when it is in
/// the default state, i.e. when [angle] is equal to zero.
/// For example, a nativeAngle of
/// 0 implies up/north direction
/// pi/2 implies right/east direction
/// pi implies down/south direction
/// -pi/2 implies left/west direction
double nativeAngle;
st-pasha marked this conversation as resolved.
Show resolved Hide resolved

/// The decorator is used to apply visual effects to a component.
///
/// By default, the [PositionComponent] is equipped with a
Expand Down Expand Up @@ -332,6 +342,33 @@ class PositionComponent extends Component
/// The absolute center of the component.
Vector2 get absoluteCenter => absolutePositionOfAnchor(Anchor.center);

/// Returns the angle formed by component's orientation vector and a vector
/// starting at component's absolute position and ending at [target]. This
/// angle is measured in clockwise direction. [target] should be in absolute/world
/// coordinate system.
///
/// Uses [nativeAngle] to decide the orientation direction of the component.
/// See [lookAt] to make the component instantly rotate towards target.
///
/// Note: If target coincides with the current component, then it is treated
/// as being north.
double angleTo(Vector2 target) {
return math.atan2(
ufrshubham marked this conversation as resolved.
Show resolved Hide resolved
target.x - absolutePosition.x,
absolutePosition.y - target.y,
) -
spydon marked this conversation as resolved.
Show resolved Hide resolved
(nativeAngle + absoluteAngle);
}

/// Rotates/snaps the component to look at the [target].
///
/// This method sets the [angle] so that the component's orientation
/// vector (as determined by the [nativeAngle]) is pointing at the target.
/// [target] should to be in absolute/world coordinate system.
///
/// See also: [angleTo]
void lookAt(Vector2 target) => angle += angleTo(target);

//#endregion

//#region Mutators
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class SpriteAnimationComponent extends PositionComponent
super.size,
super.scale,
super.angle,
super.nativeAngle,
super.anchor,
super.children,
super.priority,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class SpriteAnimationGroupComponent<T> extends PositionComponent
super.size,
super.scale,
super.angle,
super.nativeAngle,
super.anchor,
super.children,
super.priority,
Expand Down
1 change: 1 addition & 0 deletions packages/flame/lib/src/components/sprite_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class SpriteComponent extends PositionComponent
Vector2? size,
super.scale,
super.angle,
super.nativeAngle,
super.anchor,
super.children,
super.priority,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class SpriteGroupComponent<T> extends PositionComponent
super.size,
super.scale,
super.angle,
super.nativeAngle,
super.anchor,
super.children,
super.priority,
Expand Down
Loading