-
-
Notifications
You must be signed in to change notification settings - Fork 951
/
Copy pathcamera_component.dart
449 lines (416 loc) · 16.9 KB
/
camera_component.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame/src/camera/behaviors/bounded_position_behavior.dart';
import 'package:flame/src/camera/behaviors/follow_behavior.dart';
import 'package:flame/src/camera/behaviors/viewport_aware_bounds_behavior.dart';
import 'package:flame/src/camera/viewfinder.dart';
import 'package:flame/src/camera/viewport.dart';
import 'package:flame/src/camera/viewports/fixed_resolution_viewport.dart';
import 'package:flame/src/camera/viewports/max_viewport.dart';
import 'package:flame/src/effects/controllers/effect_controller.dart';
import 'package:flame/src/effects/move_by_effect.dart';
import 'package:flame/src/effects/move_effect.dart';
import 'package:flame/src/effects/move_to_effect.dart';
import 'package:flame/src/effects/provider_interfaces.dart';
import 'package:flame/src/experimental/geometry/shapes/circle.dart';
import 'package:flame/src/experimental/geometry/shapes/rectangle.dart';
import 'package:flame/src/experimental/geometry/shapes/rounded_rectangle.dart';
import 'package:flame/src/experimental/geometry/shapes/shape.dart';
import 'package:flame/src/game/flame_game.dart';
/// [CameraComponent] is a component through which a [World] is observed.
///
/// A camera consists of two parts: a [Viewport], and a [Viewfinder]. It also
/// references a [World] component, which is not mounted to the camera, but the
/// camera still knows about it. The world must be mounted somewhere else in
/// the game tree.
///
/// A camera is a regular component that can be placed anywhere in the game
/// tree. Most games will have at least one "main" camera for displaying the
/// main game world. However, additional cameras may also be used for some
/// special effects. These extra cameras may be placed either in parallel with
/// the main camera, or within the world. It is even possible to create a camera
/// that looks at itself. [FlameGame] has one [CameraComponent] added by default
/// which is called just [FlameGame.camera].
///
/// Since [CameraComponent] is a [Component], it is possible to attach other
/// components to it. In particular, adding components directly to the camera is
/// equivalent to adding them to the camera's parent. Components added to the
/// viewport will be affected by the viewport's position, but not by its clip
/// mask. Such components will be rendered on top of the viewport. Components
/// added to the viewfinder will be rendered as if they were part of the world.
/// That is, they will be affected both by the viewport and the viewfinder.
class CameraComponent extends Component {
CameraComponent({
this.world,
Viewport? viewport,
Viewfinder? viewfinder,
Component? backdrop,
List<Component>? hudComponents,
super.key,
}) : _viewport = (viewport ?? MaxViewport())..addAll(hudComponents ?? []),
_viewfinder = viewfinder ?? Viewfinder(),
_backdrop = backdrop ?? Component(),
// The priority is set to the max here to avoid some bugs for the users,
// if they for example would add any components that modify positions
// before the CameraComponent, since it then will render the positions
// of the last tick each tick.
super(priority: 0x7fffffff) {
addAll([_backdrop, _viewport, _viewfinder]);
}
/// Create a camera that shows a portion of the game world of fixed size
/// [width] x [height].
///
/// The viewport will be centered within the canvas, expanding as much as
/// possible on all sides while maintaining the [width]:[height] aspect ratio.
/// The zoom level will be set such in such a way that exactly [width] x
/// [height] pixels are visible within the viewport. The viewfinder will be
/// initially set up to show world coordinates (0, 0) at the center of the
/// viewport.
CameraComponent.withFixedResolution({
required double width,
required double height,
World? world,
Viewfinder? viewfinder,
Component? backdrop,
List<Component>? hudComponents,
ComponentKey? key,
}) : this(
world: world,
viewport: FixedResolutionViewport(resolution: Vector2(width, height)),
viewfinder: viewfinder ?? Viewfinder(),
backdrop: backdrop,
hudComponents: hudComponents,
key: key,
);
/// The [viewport] is the "window" through which the game world is observed.
///
/// Imagine that the world is covered with an infinite sheet of paper, but
/// there is a hole in it. That hole is the viewport: through that aperture
/// the world can be observed. The viewport's size is equal to or smaller
/// than the size of the game canvas. If it is smaller, then the viewport's
/// position specifies where exactly it is placed on the canvas.
Viewport get viewport => _viewport;
set viewport(Viewport newViewport) {
_viewport.removeFromParent();
_viewport = newViewport;
add(_viewport);
_viewfinder.updateTransform();
}
Viewport _viewport;
/// The [viewfinder] controls which part of the world is seen through the
/// viewport.
///
/// Thus, viewfinder's `position` is the world point which is seen at the
/// center of the viewport. In addition, viewfinder controls the zoom level
/// (i.e. how much of the world is seen through the viewport), and,
/// optionally, rotation.
Viewfinder get viewfinder => _viewfinder;
set viewfinder(Viewfinder newViewfinder) {
_viewfinder.removeFromParent();
_viewfinder = newViewfinder;
add(_viewfinder);
}
Viewfinder _viewfinder;
/// Special component that is designed to be the root of a game world.
///
/// Multiple cameras can observe the same [world] simultaneously, and the
/// world may itself contain cameras that look into other worlds, or even into
/// itself.
///
/// The [world] component is generally mounted externally to the camera, and
/// this variable is a mere reference to it.
World? world;
/// The [backdrop] component is rendered statically behind the world.
///
/// Here you can add things like the parallax component which should be static
/// when the camera moves around.
Component get backdrop => _backdrop;
Component _backdrop;
set backdrop(Component newBackdrop) {
_backdrop.removeFromParent();
add(newBackdrop);
_backdrop = newBackdrop;
}
/// The axis-aligned bounding rectangle of a [world] region which is currently
/// visible through the viewport.
///
/// This property can be useful in order to determine which components within
/// the game's world are currently visible to the player, and which aren't.
///
/// If the viewport is non-rectangular, or if the world's view is rotated,
/// then the [visibleWorldRect] will be larger than the actual viewing area.
/// Thus, this property is "conservative": everything outside of this rect
/// is definitely not visible, while the points inside may or may not be
/// visible.
///
/// This property is cached, and is recalculated whenever the camera moves
/// or the viewport is resized. At the same time, it may only be accessed
/// after the camera was fully mounted.
Rect get visibleWorldRect {
assert(
parent != null,
"This property can't be accessed before the camera is added to the game. "
'If you are using visibleWorldRect from another component (for example '
'the World), make sure that the CameraComponent is added before that '
'Component.',
);
return viewfinder.visibleWorldRect;
}
/// Renders the [world] as seen through this camera.
///
/// If the world is not mounted yet, only the viewport and viewfinder elements
/// will be rendered.
@override
void renderTree(Canvas canvas) {
canvas.save();
canvas.translate(
viewport.position.x - viewport.anchor.x * viewport.size.x,
viewport.position.y - viewport.anchor.y * viewport.size.y,
);
// Render the world through the viewport
if ((world?.isMounted ?? false) &&
currentCameras.length < maxCamerasDepth) {
canvas.save();
viewport.clip(canvas);
viewport.transformCanvas(canvas);
backdrop.renderTree(canvas);
canvas.save();
try {
currentCameras.add(this);
canvas.transform2D(viewfinder.transform);
world!.renderFromCamera(canvas);
// Render the viewfinder elements, which will be in front of the world,
// but with the same transforms applied to them.
viewfinder.renderTree(canvas);
} finally {
currentCameras.removeLast();
}
canvas.restore();
// Render the viewport elements, which will be in front of the world.
viewport.renderTree(canvas);
canvas.restore();
}
canvas.restore();
}
/// Converts from the global (canvas) coordinate space to
/// local (camera = viewport + viewfinder).
///
/// Opposite of [localToGlobal].
Vector2 globalToLocal(Vector2 point, {Vector2? output}) {
final viewportPosition = viewport.globalToLocal(point, output: output);
return viewfinder.globalToLocal(viewportPosition, output: output);
}
/// Converts from the local (camera = viewport + viewfinder) coordinate space
/// to global (canvas).
///
/// Opposite of [globalToLocal].
Vector2 localToGlobal(Vector2 position, {Vector2? output}) {
final viewfinderPosition =
viewfinder.localToGlobal(position, output: output);
return viewport.localToGlobal(viewfinderPosition, output: output);
}
@override
Iterable<Component> componentsAtLocation<T>(
T locationContext,
List<T>? nestedContexts,
T? Function(CoordinateTransform, T) transformContext,
bool Function(Component, T) checkContains,
) sync* {
final viewportPoint = transformContext(viewport, locationContext);
if (viewportPoint == null) {
return;
}
yield* viewport.componentsAtLocation(
viewportPoint,
nestedContexts,
transformContext,
checkContains,
);
if ((world?.isMounted ?? false) &&
currentCameras.length < maxCamerasDepth) {
if (checkContains(viewport, viewportPoint)) {
currentCameras.add(this);
final worldPoint = transformContext(viewfinder, viewportPoint);
if (worldPoint == null) {
return;
}
yield* viewfinder.componentsAtLocation(
worldPoint,
nestedContexts,
transformContext,
checkContains,
);
yield* world!.componentsAtLocation(
worldPoint,
nestedContexts,
transformContext,
checkContains,
);
currentCameras.removeLast();
}
}
}
/// A camera that currently performs rendering.
///
/// This variable is set to `this` when we begin rendering the world through
/// this particular camera, and reset back to `null` at the end. This variable
/// is not set when rendering components that are attached to the viewport.
static CameraComponent? get currentCamera {
return currentCameras.isEmpty ? null : currentCameras[0];
}
/// Stack of all current cameras in the render tree.
static final List<CameraComponent> currentCameras = [];
/// Maximum number of nested cameras that will be rendered.
///
/// This variable helps prevent infinite recursion when a camera is set to
/// look at the world that contains that camera.
static int maxCamerasDepth = 4;
/// Makes the [viewfinder] follow the given [target].
///
/// The [target] here can be any read-only [PositionProvider]. For example, a
/// [PositionComponent] is the most common choice of target. Alternatively,
/// you can use [PositionProviderImpl] to construct the target dynamically.
///
/// This method adds a [FollowBehavior] to the viewfinder. If there is another
/// [FollowBehavior] currently applied to the viewfinder, it will be removed
/// first.
///
/// Parameters [maxSpeed], [horizontalOnly] and [verticalOnly] have the same
/// meaning as in the [FollowBehavior.new] constructor.
///
/// If [snap] is true, then the viewfinder's starting position will be set to
/// the target's current location. If [snap] is false, then the viewfinder
/// will move from its current position to the target's position at the given
/// speed.
void follow(
ReadOnlyPositionProvider target, {
double maxSpeed = double.infinity,
bool horizontalOnly = false,
bool verticalOnly = false,
bool snap = false,
}) {
stop();
viewfinder.add(
FollowBehavior(
target: target,
owner: viewfinder,
maxSpeed: maxSpeed,
horizontalOnly: horizontalOnly,
verticalOnly: verticalOnly,
),
);
if (snap) {
viewfinder.position = target.position;
}
}
/// Removes all movement effects or behaviors from the viewfinder.
void stop() {
viewfinder.children.toList().forEach((child) {
if (child is FollowBehavior || child is MoveEffect) {
child.removeFromParent();
}
});
}
/// Moves the camera towards the specified world [point].
void moveTo(Vector2 point, {double speed = double.infinity}) {
stop();
viewfinder.add(
MoveToEffect(point, EffectController(speed: speed)),
);
}
/// Move the camera by the given [offset].
void moveBy(Vector2 offset, {double speed = double.infinity}) {
stop();
viewfinder.add(MoveByEffect(offset, EffectController(speed: speed)));
}
/// Sets or clears the world bounds for the camera's viewfinder.
///
/// The bound is a [Shape], given in the world coordinates. The viewfinder's
/// position will be restricted to always remain inside this region.
///
/// When [considerViewport] is true none of the viewport can go outside
/// of the bounds, when it is false only the viewfinder anchor is considered.
/// Note that this option only works with [Rectangle], [RoundedRectangle] and
/// [Circle] shapes.
void setBounds(Shape? bounds, {bool considerViewport = false}) {
final boundedBehavior = viewfinder.firstChild<BoundedPositionBehavior>();
final viewPortAwareBoundsBehavior =
viewfinder.firstChild<ViewportAwareBoundsBehavior>();
if (bounds == null) {
// When bounds is null, all bounds-related components need to be dropped.
boundedBehavior?.removeFromParent();
viewPortAwareBoundsBehavior?.removeFromParent();
return;
}
Future<void>? boundedBehaviorFuture;
if (boundedBehavior == null) {
final BoundedPositionBehavior ref;
viewfinder.add(
ref = BoundedPositionBehavior(
bounds: bounds,
priority: 1000,
),
);
boundedBehaviorFuture = ref.mounted;
} else {
boundedBehavior.bounds = bounds;
}
if (!considerViewport) {
// Edge case: remove pre-existing viewport aware components.
viewPortAwareBoundsBehavior?.removeFromParent();
return;
}
// Param `considerViewPort` was true and we have a bounds.
// Add a ViewportAwareBoundsBehavior component with
// our desired bounds shape or update the boundsShape if the
// component already exists.
if (viewPortAwareBoundsBehavior == null) {
switch (boundedBehaviorFuture) {
case null:
// This represents the case when BoundedPositionBehavior was mounted
// earlier in another cycle. This allows us to immediately add the
// ViewportAwareBoundsBehavior component which will subsequently adapt
// the camera to the virtual resolution this frame.
_addViewPortAwareBoundsBehavior(bounds);
case _:
// This represents the case when BoundedPositionBehavior was added
// in this exact cycle but did not mount into the tree.
// We must wait for that component to mount first in order for
// ViewportAwareBoundsBehavior to correctly affect the camera.
boundedBehaviorFuture
.whenComplete(() => _addViewPortAwareBoundsBehavior(bounds));
}
} else {
viewPortAwareBoundsBehavior.boundsShape = bounds;
}
}
void _addViewPortAwareBoundsBehavior(Shape bounds) {
viewfinder.add(
ViewportAwareBoundsBehavior(
boundsShape: bounds,
),
);
}
/// Returns true if this camera is able to see the [component].
/// Will always return false if
/// - [world] is null or
/// - [world] is not mounted or
/// - [component] is not mounted or
/// - [componentWorld] is non-null and does not match with [world]
///
/// If [componentWorld] is null, this method does not take into consideration
/// the world to which the given [component] belongs (if any). This means, in
/// such cases, any component overlapping the [visibleWorldRect] will be
/// reported as visible, even if it is not part of the [world] this camera is
/// currently looking at. This can be changed by passing the component's
/// world as [componentWorld].
bool canSee(PositionComponent component, {World? componentWorld}) {
if (!(world?.isMounted ?? false) ||
!component.isMounted ||
(componentWorld != null && componentWorld != world)) {
return false;
}
return visibleWorldRect.overlaps(component.toAbsoluteRect());
}
}