mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-02 03:15:43 +08:00
respectCamera and respectViewport proposal: coordinate system enum (#1167)
* Coordinate system * Fixess * . * . * Fix tests * Rename CoordinateSystem to PositioningType * Update docs for PositioningType * Use switch instead of if-else * PositioningType -> PositionType * Don't do unnecessary transforms * Added tests for PositionType.widget * Update doc/components.md Co-authored-by: Luan Nico <luanpotter27@gmail.com> * Added PositionType tests Co-authored-by: Lukas Klingsbo <me@lukas.fyi> Co-authored-by: Lukas Klingsbo <lukas.klingsbo@gmail.com>
This commit is contained in:
@ -114,6 +114,26 @@ void update(double dt) {
|
||||
}
|
||||
```
|
||||
|
||||
### Positioning types
|
||||
If you want to create a HUD (Head-up display) or another component that isn't positioned in relation
|
||||
to the game coordinates, you can change the `PositionType` of the component.
|
||||
The default `PositionType` is `positionType = PositionType.game` and that can be changed to
|
||||
either `PositionType.viewport` or `PositionType.widget` depending on how you want to position
|
||||
the component.
|
||||
|
||||
- `PositionType.game` (Default) - Respects camera and viewport.
|
||||
- `PositionType.viewport` - Respects viewport only (ignores camera).
|
||||
- `PositionType.widget` - Position in relation to the coordinate system of the Flutter game
|
||||
widget (i.e. the raw canvas).
|
||||
|
||||
Most of your components will probably be positioned according to `PositionType.game`, since you
|
||||
want them to respect the `Camera` and the `Viewport`. But quite often you want for example buttons
|
||||
and text to always show on the screen, no matter if you move the camera, then you want to use
|
||||
`PositionType.viewport`. In some rare cases you want to use `PositionType.widget` to position
|
||||
your widgets, when you don't want the component to respect the camera nor the viewport; this could
|
||||
for example be for controls or joysticks that would be unergonomic to use if they had to stay within
|
||||
the viewport.
|
||||
|
||||
## PositionComponent
|
||||
|
||||
This class represent a positioned object on the screen, being a floating rectangle or a rotating
|
||||
|
||||
@ -163,11 +163,11 @@ class JoystickAdvancedExample extends FlameGame
|
||||
speedText = TextComponent(
|
||||
text: 'Speed: 0',
|
||||
textRenderer: _regular,
|
||||
)..respectCamera = false;
|
||||
)..positionType = PositionType.viewport;
|
||||
directionText = TextComponent(
|
||||
text: 'Direction: idle',
|
||||
textRenderer: _regular,
|
||||
)..respectCamera = false;
|
||||
)..positionType = PositionType.viewport;
|
||||
|
||||
final speedWithMargin = HudMarginComponent(
|
||||
margin: const EdgeInsets.only(
|
||||
|
||||
@ -9,10 +9,10 @@
|
||||
- Introduce `updateTree` to follow the `renderTree` convention
|
||||
- Fix `Parallax.load` with different loading times
|
||||
- Fix render order of components and add tests
|
||||
- `isHud` renamed to `respectCamera`
|
||||
- Fix `HitboxCircle` when component is flipped
|
||||
- `MoveAlongPathEffect` can now be absolute, and can auto-orient the object along the path
|
||||
- `ScaleEffect.by` now applies multiplicatively instead of additively
|
||||
- `isHud` replaced with `PositionType`
|
||||
- Remove web fallback for `drawAtlas` in SpriteBatch, but added flag `useAtlas` to activate it
|
||||
|
||||
## [1.0.0-releasecandidate.18]
|
||||
|
||||
@ -16,6 +16,7 @@ export 'src/components/nine_tile_box_component.dart';
|
||||
export 'src/components/parallax_component.dart';
|
||||
export 'src/components/particle_component.dart';
|
||||
export 'src/components/position_component.dart';
|
||||
export 'src/components/positioning_type.dart';
|
||||
export 'src/components/shape_component.dart';
|
||||
export 'src/components/sprite_animation_component.dart';
|
||||
export 'src/components/sprite_animation_group_component.dart';
|
||||
|
||||
@ -9,6 +9,7 @@ import '../../input.dart';
|
||||
import '../extensions/vector2.dart';
|
||||
import '../game/mixins/loadable.dart';
|
||||
import 'cache/value_cache.dart';
|
||||
import 'positioning_type.dart';
|
||||
|
||||
/// This represents a Component for your game.
|
||||
///
|
||||
@ -18,15 +19,12 @@ import 'cache/value_cache.dart';
|
||||
/// called automatically once the component is added to the component tree in
|
||||
/// your game (with `game.add`).
|
||||
class Component with Loadable {
|
||||
/// Whether this component should respect the camera or not.
|
||||
///
|
||||
/// Components that have this property set to false will ignore the
|
||||
/// `FlameGame.camera` when rendered (so their position coordinates are
|
||||
/// considered relative only to the viewport instead).
|
||||
/// What coordinate system this component should respect (i.e. should it
|
||||
/// observe camera, viewport, or use the raw canvas).
|
||||
///
|
||||
/// Do note that this currently only works if the component is added directly
|
||||
/// to the root `FlameGame`.
|
||||
bool respectCamera = true;
|
||||
PositionType positionType = PositionType.game;
|
||||
|
||||
/// Whether this component has been prepared and is ready to be added to the
|
||||
/// game loop.
|
||||
@ -157,9 +155,14 @@ class Component with Loadable {
|
||||
|
||||
@protected
|
||||
Vector2 eventPosition(PositionInfo info) {
|
||||
return respectCamera
|
||||
? info.eventPosition.game
|
||||
: info.eventPosition.viewportOnly;
|
||||
switch (positionType) {
|
||||
case PositionType.game:
|
||||
return info.eventPosition.game;
|
||||
case PositionType.viewport:
|
||||
return info.eventPosition.viewport;
|
||||
case PositionType.widget:
|
||||
return info.eventPosition.widget;
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the component from its parent in the next tick.
|
||||
|
||||
@ -4,6 +4,7 @@ import 'package:meta/meta.dart';
|
||||
import '../../../components.dart';
|
||||
import '../../../extensions.dart';
|
||||
import '../../../game.dart';
|
||||
import '../positioning_type.dart';
|
||||
|
||||
/// The [HudMarginComponent] positions itself by a margin to the edge of the
|
||||
/// screen instead of by an absolute position on the screen or on the game, so
|
||||
@ -18,7 +19,7 @@ import '../../../game.dart';
|
||||
class HudMarginComponent<T extends FlameGame> extends PositionComponent
|
||||
with HasGameRef<T> {
|
||||
@override
|
||||
bool respectCamera = false;
|
||||
PositionType positionType = PositionType.viewport;
|
||||
|
||||
/// Instead of setting a position of the [HudMarginComponent] a margin
|
||||
/// from the edges of the viewport can be used instead.
|
||||
|
||||
15
packages/flame/lib/src/components/positioning_type.dart
Normal file
15
packages/flame/lib/src/components/positioning_type.dart
Normal file
@ -0,0 +1,15 @@
|
||||
/// Used to define which coordinate system a given top-level component respects.
|
||||
///
|
||||
/// Normally components live in the "game" coordinate system, which just means
|
||||
/// they respect both the camera and viewport.
|
||||
enum PositionType {
|
||||
/// Default type. Respects camera and viewport (applied on top of widget).
|
||||
game,
|
||||
|
||||
/// Respects viewport only (ignores camera) (applied on top of widget).
|
||||
viewport,
|
||||
|
||||
/// Position in relation to the coordinate system of the Flutter game widget
|
||||
/// (i.e. the raw canvas).
|
||||
widget,
|
||||
}
|
||||
@ -38,8 +38,8 @@ import '../projector.dart';
|
||||
///
|
||||
/// Note: in the context of the FlameGame, the camera effectively translates
|
||||
/// the position where components are rendered with relation to the Viewport.
|
||||
/// Components marked as `respectCamera = false` are always rendered in screen
|
||||
/// coordinates, bypassing the camera altogether.
|
||||
/// Components marked as `positionType = PositionType.viewport;` are
|
||||
/// always rendered in screen coordinates, bypassing the camera altogether.
|
||||
class Camera extends Projector {
|
||||
Camera() : _viewport = DefaultViewport() {
|
||||
_combinedProjector = Projector.compose([this, _viewport]);
|
||||
|
||||
@ -22,24 +22,32 @@ class CameraWrapper {
|
||||
}
|
||||
|
||||
void render(Canvas canvas) {
|
||||
camera.viewport.render(canvas, (_canvas) {
|
||||
var hasCamera = false; // so we don't apply unnecessary transformations
|
||||
PositionType? _previousType;
|
||||
canvas.save();
|
||||
world.forEach((component) {
|
||||
if (component.respectCamera && !hasCamera) {
|
||||
final sameType = component.positionType == _previousType;
|
||||
if (!sameType) {
|
||||
if (_previousType != null && _previousType != PositionType.widget) {
|
||||
canvas.restore();
|
||||
canvas.save();
|
||||
}
|
||||
switch (component.positionType) {
|
||||
case PositionType.game:
|
||||
camera.viewport.apply(canvas);
|
||||
camera.apply(canvas);
|
||||
hasCamera = true;
|
||||
} else if (!component.respectCamera && hasCamera) {
|
||||
canvas.restore();
|
||||
hasCamera = false;
|
||||
break;
|
||||
case PositionType.viewport:
|
||||
camera.viewport.apply(canvas);
|
||||
break;
|
||||
case PositionType.widget:
|
||||
}
|
||||
}
|
||||
canvas.save();
|
||||
component.renderTree(canvas);
|
||||
canvas.restore();
|
||||
_previousType = component.positionType;
|
||||
});
|
||||
if (hasCamera) {
|
||||
|
||||
if (_previousType != PositionType.widget) {
|
||||
canvas.restore();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,9 +52,18 @@ abstract class Viewport extends Projector {
|
||||
/// size changes.
|
||||
void resize(Vector2 newCanvasSize);
|
||||
|
||||
/// Applies to the Canvas all necessary transformations to apply this
|
||||
/// viewport.
|
||||
void apply(Canvas c);
|
||||
|
||||
/// This transforms the canvas so that the coordinate system is viewport-
|
||||
/// -aware. All your rendering logic should be put inside the lambda.
|
||||
void render(Canvas c, void Function(Canvas c) renderGame);
|
||||
void render(Canvas c, void Function(Canvas) renderGame) {
|
||||
c.save();
|
||||
apply(c);
|
||||
renderGame(c);
|
||||
c.restore();
|
||||
}
|
||||
|
||||
/// This returns the effective size, after viewport transformation.
|
||||
/// This is not the game widget size but for all intents and purposes,
|
||||
@ -75,9 +84,7 @@ abstract class Viewport extends Projector {
|
||||
/// This basically no-ops the viewport.
|
||||
class DefaultViewport extends Viewport {
|
||||
@override
|
||||
void render(Canvas c, void Function(Canvas c) renderGame) {
|
||||
renderGame(c);
|
||||
}
|
||||
void apply(Canvas c) {}
|
||||
|
||||
@override
|
||||
void resize(Vector2 newCanvasSize) {
|
||||
@ -122,7 +129,7 @@ class DefaultViewport extends Viewport {
|
||||
/// transformation whatsoever, and if the a device with a different ratio is
|
||||
/// used it will try to adapt the best as possible.
|
||||
class FixedResolutionViewport extends Viewport {
|
||||
/// By default, the viewport will clip anything rendered outside.
|
||||
/// By default, this viewport will clip anything rendered outside.
|
||||
/// Use this variable to control that behaviour.
|
||||
bool noClip;
|
||||
|
||||
@ -171,14 +178,11 @@ class FixedResolutionViewport extends Viewport {
|
||||
}
|
||||
|
||||
@override
|
||||
void render(Canvas c, void Function(Canvas) renderGame) {
|
||||
c.save();
|
||||
void apply(Canvas c) {
|
||||
if (!noClip) {
|
||||
c.clipRect(_clipRect);
|
||||
}
|
||||
c.transform(_transform.storage);
|
||||
renderGame(c);
|
||||
c.restore();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -163,7 +163,7 @@ mixin Game on Loadable {
|
||||
Projector projector = IdentityProjector();
|
||||
|
||||
/// This is the projector used by components that don't respect the camera
|
||||
/// (`respectCamera = false`).
|
||||
/// (`positionType = PositionType.viewport;`).
|
||||
/// This can be overridden on your [Game] implementation.
|
||||
Projector viewportProjector = IdentityProjector();
|
||||
|
||||
|
||||
@ -3,11 +3,17 @@ import 'package:flutter/gestures.dart';
|
||||
import '../../extensions.dart';
|
||||
import '../game/mixins/game.dart';
|
||||
|
||||
/// [EventPosition] converts position based events to three different coordinate systems (global, local and game).
|
||||
/// [EventPosition] converts position based events to three different coordinate
|
||||
/// systems (global, local and game).
|
||||
///
|
||||
/// global: coordinate system relative to the entire app; same as `globalPosition` in Flutter
|
||||
/// widget: coordinate system relative to the GameWidget widget; same as `localPosition` in Flutter
|
||||
/// game: same as `widget` but also applies any transformations from the camera or viewport to the coordinate system
|
||||
/// global: coordinate system relative to the entire app; same as
|
||||
/// `globalPosition` in Flutter.
|
||||
/// widget: coordinate system relative to the GameWidget widget; same as
|
||||
/// `localPosition` in Flutter.
|
||||
/// viewport: same as `widget` but also applies any transformations from the
|
||||
/// viewport to the coordinate system.
|
||||
/// game: same as `widget` but also applies any transformations from the camera
|
||||
/// and viewport to the coordinate system.
|
||||
class EventPosition {
|
||||
final Game _game;
|
||||
final Offset _globalPosition;
|
||||
@ -18,20 +24,24 @@ class EventPosition {
|
||||
/// Coordinates of the event relative to the game widget position/size
|
||||
late final Vector2 widget = _game.convertGlobalToLocalCoordinate(global);
|
||||
|
||||
/// Coordinates of the event relative to the game position/size but applying only viewport transformations (not camera).
|
||||
late final Vector2 viewportOnly =
|
||||
_game.viewportProjector.unprojectVector(widget);
|
||||
/// Coordinates of the event relative to the game position/size but applying
|
||||
/// only viewport transformations (not camera).
|
||||
late final Vector2 viewport = _game.viewportProjector.unprojectVector(widget);
|
||||
|
||||
/// Coordinates of the event relative to the game position/size and transformations
|
||||
/// Coordinates of the event relative to the game position/size and
|
||||
/// transformations
|
||||
late final Vector2 game = _game.projector.unprojectVector(widget);
|
||||
|
||||
EventPosition(this._game, this._globalPosition);
|
||||
}
|
||||
|
||||
/// [EventDelta] converts deltas based events to two different values (game and global).
|
||||
/// [EventDelta] converts deltas based events to two different values
|
||||
/// (game and global).
|
||||
///
|
||||
/// [global]: this is the raw value received by the event without any scale applied to it; this is always the same as local because Flutter doesn't apply any scaling.
|
||||
/// [game]: the scalled value applied all the game transformations.
|
||||
/// [global]: this is the raw value received by the event without any scale
|
||||
/// applied to it; this is always the same as local because Flutter doesn't
|
||||
/// apply any scaling.
|
||||
/// [game]: the scaled value with all the game transformations applied.
|
||||
class EventDelta {
|
||||
final Game _game;
|
||||
final Offset _delta;
|
||||
@ -39,9 +49,9 @@ class EventDelta {
|
||||
/// Raw value relative to the game transformations
|
||||
late final Vector2 global = _delta.toVector2();
|
||||
|
||||
/// Scaled value relative to the game viewport only transformations (not camera).
|
||||
late final Vector2 viewportOnly =
|
||||
_game.viewportProjector.unscaleVector(global);
|
||||
/// Scaled value relative to the game viewport only transformations (not
|
||||
/// camera).
|
||||
late final Vector2 viewport = _game.viewportProjector.unscaleVector(global);
|
||||
|
||||
/// Scaled value relative to the game transformations
|
||||
late final Vector2 game = _game.projector.unscaleVector(global);
|
||||
|
||||
@ -23,6 +23,47 @@ class _TestComponent extends PositionComponent {
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('widget', () {
|
||||
flameGame.test(
|
||||
'viewport does not affect component with PositionType.widget',
|
||||
(game) async {
|
||||
game.camera.viewport = FixedResolutionViewport(Vector2.all(50));
|
||||
game.onGameResize(Vector2.all(200.0));
|
||||
await game.ensureAdd(
|
||||
_TestComponent(Vector2.zero())..positionType = PositionType.widget,
|
||||
);
|
||||
|
||||
final canvas = MockCanvas();
|
||||
game.render(canvas);
|
||||
expect(
|
||||
canvas,
|
||||
MockCanvas()
|
||||
..translate(0, 0) // transform in PositionComponent.renderTree
|
||||
..drawRect(const Rect.fromLTWH(0, 0, 1, 1)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
flameGame.test(
|
||||
'camera does not affect component with PositionType.widget',
|
||||
(game) async {
|
||||
await game.ensureAdd(
|
||||
_TestComponent(Vector2.zero())..positionType = PositionType.widget,
|
||||
);
|
||||
game.camera.snapTo(Vector2(100, 100));
|
||||
|
||||
final canvas = MockCanvas();
|
||||
game.render(canvas);
|
||||
expect(
|
||||
canvas,
|
||||
MockCanvas()
|
||||
..translate(0, 0) // transform in PositionComponent.renderTree
|
||||
..drawRect(const Rect.fromLTWH(0, 0, 1, 1)),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('viewport', () {
|
||||
flameGame.test('default viewport does not change size', (game) {
|
||||
game.onGameResize(Vector2(100.0, 200.0));
|
||||
|
||||
@ -11,9 +11,9 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
class _MyComponent extends Component {
|
||||
@override
|
||||
bool respectCamera;
|
||||
PositionType positionType;
|
||||
|
||||
_MyComponent(int priority, {this.respectCamera = true})
|
||||
_MyComponent(int priority, {this.positionType = PositionType.game})
|
||||
: super(priority: priority);
|
||||
|
||||
@override
|
||||
@ -27,7 +27,7 @@ class _MyComponent extends Component {
|
||||
void main() {
|
||||
group('components are rendered according to their priorities', () {
|
||||
flameGame.test(
|
||||
'only camera components',
|
||||
'PositionType.game',
|
||||
(game) async {
|
||||
await game.ensureAddAll([
|
||||
_MyComponent(4),
|
||||
@ -52,12 +52,12 @@ void main() {
|
||||
);
|
||||
|
||||
flameGame.test(
|
||||
'only HUD components',
|
||||
'PositionType.viewport',
|
||||
(game) async {
|
||||
await game.ensureAddAll([
|
||||
_MyComponent(4, respectCamera: false),
|
||||
_MyComponent(1, respectCamera: false),
|
||||
_MyComponent(2, respectCamera: false),
|
||||
_MyComponent(4, positionType: PositionType.viewport),
|
||||
_MyComponent(1, positionType: PositionType.viewport),
|
||||
_MyComponent(2, positionType: PositionType.viewport),
|
||||
]);
|
||||
final canvas = MockCanvas();
|
||||
game.camera.snapTo(Vector2(12.0, 18.0));
|
||||
@ -73,16 +73,41 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
flameGame.test(
|
||||
'PositionType.widget',
|
||||
(game) async {
|
||||
await game.ensureAddAll([
|
||||
_MyComponent(5, positionType: PositionType.widget),
|
||||
_MyComponent(1, positionType: PositionType.widget),
|
||||
_MyComponent(2, positionType: PositionType.widget),
|
||||
]);
|
||||
|
||||
final canvas = MockCanvas();
|
||||
game.camera.snapTo(Vector2(12.0, 18.0));
|
||||
game.render(canvas);
|
||||
|
||||
expect(
|
||||
canvas,
|
||||
MockCanvas()
|
||||
..drawRect(const Rect.fromLTWH(1, 1, 1, 1))
|
||||
..drawRect(const Rect.fromLTWH(2, 2, 1, 1))
|
||||
..drawRect(const Rect.fromLTWH(5, 5, 1, 1)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
flameGame.test(
|
||||
'mixed',
|
||||
(game) async {
|
||||
await game.ensureAddAll([
|
||||
_MyComponent(4),
|
||||
_MyComponent(1),
|
||||
_MyComponent(2, respectCamera: false),
|
||||
_MyComponent(5, respectCamera: false),
|
||||
_MyComponent(3, respectCamera: false),
|
||||
_MyComponent(7, positionType: PositionType.viewport),
|
||||
_MyComponent(5, positionType: PositionType.viewport),
|
||||
_MyComponent(3, positionType: PositionType.viewport),
|
||||
_MyComponent(0),
|
||||
_MyComponent(6, positionType: PositionType.widget),
|
||||
_MyComponent(2, positionType: PositionType.widget),
|
||||
]);
|
||||
|
||||
final canvas = MockCanvas();
|
||||
@ -101,7 +126,9 @@ void main() {
|
||||
..translate(-12.0, -18.0)
|
||||
..drawRect(const Rect.fromLTWH(4, 4, 1, 1))
|
||||
..translate(0.0, 0.0)
|
||||
..drawRect(const Rect.fromLTWH(5, 5, 1, 1)),
|
||||
..drawRect(const Rect.fromLTWH(5, 5, 1, 1))
|
||||
..drawRect(const Rect.fromLTWH(6, 6, 1, 1))
|
||||
..drawRect(const Rect.fromLTWH(7, 7, 1, 1)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user