diff --git a/doc/README.md b/doc/README.md index cae361a4ff2..5584d86589d 100644 --- a/doc/README.md +++ b/doc/README.md @@ -44,10 +44,129 @@ There is a set of tutorials that you can follow to get started in the Simple examples for all features can be found in the [examples folder](https://github.com/flame-engine/flame/tree/main/examples). -You can also check out the [awesome flame -repository](https://github.com/flame-engine/awesome-flame#user-content-articles--tutorials), -it contains quite a lot of good tutorials and articles written by the community -to get you started with Flame. +To run Flame you need use the `GameWidget`, which is just another widget that can live anywhere in +your widget tree. You can use it as the root widget of your app, or as a child of another widget. + +Here is a simple example of how to use the `GameWidget`: + +```dart +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; + +void main() { + runApp( + GameWidget( + game: FlameGame(), + ), + ); +} +``` + +In Flame we provide a concept called the Flame Component System (FCS), which is a way to organize +your game objects in a way that makes it easy to manage them. You can read more about it in the +[Components](flame/components.md) section. + +When you want to start a new game you either have to extend the `FlameGame` class or the `World` +class. The `FlameGame` is the root of your game and is responsible for managing the game loop and +the components. The `World` class is a component that can be used to create a world in your game. + +So to create a simple game you can do something like this: + +```dart +import 'package:flame/game.dart'; +import 'package:flame/components.dart'; +import 'package:flutter/widgets.dart'; + +void main() { + runApp( + GameWidget( + game: FlameGame(world: MyWorld()), + ), + ); +} + +class MyWorld extends World { + @override + Future onLoad() async { + add(Player(position: Vector2(0, 0))); + } +} +``` + +As you can see, we have created a `MyWorld` class that extends the `World` class. We have overridden +the `onLoad` method to add a `Player` component (which doesn't exist yet) to the world. In the +`FlameGame` class we by default have a `camera` that is watching the world, and by default it is +looking at the (0, 0) position of the world in the center of the screen, to learn more about the +camera and the world you can read the [Camera Component](flame/camera.md) section. + +The `Player` component can be whatever type of component that you want, to get started we recommend +to use the `SpriteComponent` class, which is a component that can render a sprite (image) on the +screen. + +For example something like this: + +```dart +import 'package:flame/components.dart'; +import 'package:flame/geometry.dart'; +import 'package:flame/extensions.dart'; + +class Player extends SpriteComponent { + Player({super.position}) : + super(size: Vector2.all(200), anchor: Anchor.center); + + @override + Future onLoad() async { + sprite = await Sprite.load('player.png'); + } +} +``` + +In this example, we have created a `Player` class that extends the `SpriteComponent` class. We have +overridden the `onLoad` method to set the sprite of the component to a sprite that we load from an +image file called `player.png`. The image has to be in the `assets/images` directory in your project +(see the [Assets Directory Structure](flame/structure.md)) and you have to add it to the +[assets section](https://docs.flutter.dev/ui/assets/assets-and-images) of your `pubspec.yaml` file. +In this class we also set the size of the component to 200x200 and the [anchor](flame/components.md#anchor) +to the center of the component by sending them to the `super` constructor. We also let the user of +the `Player` class set the position of the component when creating it +(`Player(position: Vector2(0, 0))`). + +To handle input on a component you can add any of our [input mixins](flame/inputs/inputs.md) to the +component. For example, if you want to handle tap input you can add the `TapCallbacks` mixin to the +player component, and receive tap events within the bounds of the player component. Or if you want +to handle tap input on the whole world you can add the `TapCallbacks` mixin to the extended `World` +class. + +The following example handles taps on the player component, and when the player component is +tapped the size of the player will increase by 50 pixels in both width and height. + +```dart +import 'package:flame/components.dart'; +import 'package:flame/geometry.dart'; +import 'package:flame/extensions.dart'; + +class Player extends SpriteComponent with TapCallbacks { + Player({super.position}) : + super(size: Vector2.all(200), anchor: Anchor.center); + + @override + Future onLoad() async { + sprite = await Sprite.load('player.png'); + } + + @override + void onTapUp(TapUpInfo info) { + size += Vector2.all(50); + } +} +``` + +This is just a simple example of how to get started with Flame, there are many more features that you +can use (and probably need) to create your game, but this should give you a good starting point. + +You can also check out the [awesome flame repository](https://github.com/flame-engine/awesome-flame#user-content-articles--tutorials), +it contains quite a lot of good tutorials and articles written by the community to get you started +with Flame. ## Outside of the scope of the engine diff --git a/doc/flame/structure.md b/doc/flame/structure.md index ca38dd3aabe..9e610f99666 100644 --- a/doc/flame/structure.md +++ b/doc/flame/structure.md @@ -1,4 +1,4 @@ -# Assets File Structure +# Assets Directory Structure Flame has a proposed structure for your project that includes the standard Flutter `assets` directory in addition to some children: `audio`, `images` and `tiles`. diff --git a/packages/flame_3d/example/lib/keyboard_controlled_camera.dart b/packages/flame_3d/example/lib/keyboard_controlled_camera.dart deleted file mode 100644 index 936e8685631..00000000000 --- a/packages/flame_3d/example/lib/keyboard_controlled_camera.dart +++ /dev/null @@ -1,196 +0,0 @@ -import 'package:flame/components.dart' show KeyboardHandler; -import 'package:flame_3d/camera.dart'; -import 'package:flame_3d/game.dart'; -import 'package:flutter/gestures.dart' show kMiddleMouseButton; -import 'package:flutter/services.dart' - show KeyEvent, KeyRepeatEvent, LogicalKeyboardKey, PointerEvent; - -class KeyboardControlledCamera extends CameraComponent3D with KeyboardHandler { - KeyboardControlledCamera({ - super.world, - super.viewport, - super.viewfinder, - super.backdrop, - super.hudComponents, - }) : super( - projection: CameraProjection.perspective, - mode: CameraMode.firstPerson, - position: Vector3(0, 2, 4), - target: Vector3(0, 2, 0), - up: Vector3(0, 1, 0), - fovY: 60, - ); - - final double moveSpeed = 0.9; - final double rotationSpeed = 0.3; - final double panSpeed = 2; - final double orbitalSpeed = 0.5; - - Set _keysDown = {}; - PointerEvent? pointerEvent; - double scrollMove = 0; - - final Matrix4 _orbitalMatrix = Matrix4.identity(); - - @override - bool onKeyEvent(KeyEvent event, Set keysPressed) { - _keysDown = keysPressed; - - // Switch camera mode - if (isKeyDown(Key.digit1)) { - mode = CameraMode.free; - up = Vector3(0, 1, 0); // Reset roll - } else if (isKeyDown(Key.digit2)) { - mode = CameraMode.firstPerson; - up = Vector3(0, 1, 0); // Reset roll - } else if (isKeyDown(Key.digit3)) { - mode = CameraMode.thirdPerson; - up = Vector3(0, 1, 0); // Reset roll - } else if (isKeyDown(Key.digit4)) { - mode = CameraMode.orbital; - up = Vector3(0, 1, 0); // Reset roll - } - - if (isKeyDown(Key.keyP) && event is! KeyRepeatEvent) { - if (projection == CameraProjection.perspective) { - // Create an isometric view. - mode = CameraMode.thirdPerson; - projection = CameraProjection.orthographic; - - position = Vector3(0, 2, -100); - target = Vector3(0, 2, 0); - up = Vector3(0, 1, 0); - fovY = 20; - - yaw(-135 * degrees2Radians, rotateAroundTarget: true); - pitch(-45 * degrees2Radians, lockView: true, rotateAroundTarget: true); - } else if (projection == CameraProjection.orthographic) { - // Reset to default view. - mode = CameraMode.thirdPerson; - projection = CameraProjection.perspective; - - position = Vector3(0, 2, 10); - target = Vector3(0, 2, 0); - up = Vector3(0, 1, 0); - fovY = 60; - } - } - - return false; - } - - @override - void update(double dt) { - final moveInWorldPlane = switch (mode) { - CameraMode.firstPerson || CameraMode.thirdPerson => true, - _ => false, - }; - final rotateAroundTarget = switch (mode) { - CameraMode.thirdPerson || CameraMode.orbital => true, - _ => false, - }; - final lockView = switch (mode) { - CameraMode.free || CameraMode.firstPerson || CameraMode.orbital => true, - _ => false, - }; - - if (mode == CameraMode.orbital) { - final rotation = _orbitalMatrix - ..setIdentity() - ..rotate(up, orbitalSpeed * dt); - final view = rotation.transform3(position - target); - position = target + view; - } else { - // Camera rotation - if (isKeyDown(Key.arrowDown)) { - pitch( - -rotationSpeed * dt, - lockView: lockView, - rotateAroundTarget: rotateAroundTarget, - ); - } else if (isKeyDown(Key.arrowUp)) { - pitch( - rotationSpeed * dt, - lockView: lockView, - rotateAroundTarget: rotateAroundTarget, - ); - } - if (isKeyDown(Key.arrowRight)) { - yaw(-rotationSpeed * dt, rotateAroundTarget: rotateAroundTarget); - } else if (isKeyDown(Key.arrowLeft)) { - yaw(rotationSpeed * dt, rotateAroundTarget: rotateAroundTarget); - } - if (isKeyDown(Key.keyQ)) { - roll(-rotationSpeed * dt); - } else if (isKeyDown(Key.keyE)) { - roll(rotationSpeed * dt); - } - - // Camera movement, if mode is free and mouse button is down we pan the - // camera. - if (pointerEvent != null) { - if (mode == CameraMode.free && - pointerEvent?.buttons == kMiddleMouseButton) { - final mouseDelta = pointerEvent!.delta; - if (mouseDelta.dx > 0) { - moveRight(panSpeed * dt, moveInWorldPlane: moveInWorldPlane); - } else if (mouseDelta.dx < 0) { - moveRight(-panSpeed * dt, moveInWorldPlane: moveInWorldPlane); - } - if (mouseDelta.dy > 0) { - moveUp(-panSpeed * dt); - } else if (mouseDelta.dy < 0) { - moveUp(panSpeed * dt); - } - } else { - const mouseMoveSensitivity = 0.003; - yaw( - (pointerEvent?.delta.dx ?? 0) * mouseMoveSensitivity, - rotateAroundTarget: rotateAroundTarget, - ); - pitch( - (pointerEvent?.delta.dy ?? 0) * mouseMoveSensitivity, - lockView: lockView, - rotateAroundTarget: rotateAroundTarget, - ); - } - pointerEvent = null; - } - - // Keyboard movement - if (isKeyDown(Key.keyW)) { - moveForward(moveSpeed * dt); - } else if (isKeyDown(Key.keyS)) { - moveForward(-moveSpeed * dt); - } - if (isKeyDown(Key.keyA)) { - moveRight(-moveSpeed * dt); - } else if (isKeyDown(Key.keyD)) { - moveRight(moveSpeed * dt); - } - - if (mode == CameraMode.free) { - if (isKeyDown(Key.space)) { - moveUp(moveSpeed * dt); - } else if (isKeyDown(Key.controlLeft)) { - moveUp(-moveSpeed * dt); - } - } - } - - // if (mode == CameraMode.thirdPerson || - // mode == CameraMode.orbital || - // mode == CameraMode.free) { - // moveToTarget(-scrollMove); - // if (isKeyDown(Key.numpadSubtract)) { - // moveToTarget(2 * dt); - // } else if (isKeyDown(Key.numpadAdd)) { - // moveToTarget(-2 * dt); - // } - // } - } - - bool isKeyDown(Key key) => _keysDown.contains(key); -} - -typedef Key = LogicalKeyboardKey; diff --git a/packages/flame_3d/example/lib/main.dart b/packages/flame_3d/example/lib/main.dart index d58aea29c58..18c0d8dcec5 100644 --- a/packages/flame_3d/example/lib/main.dart +++ b/packages/flame_3d/example/lib/main.dart @@ -2,37 +2,25 @@ import 'dart:async'; import 'dart:math'; import 'package:example/crate.dart'; -import 'package:example/keyboard_controlled_camera.dart'; import 'package:example/player_box.dart'; import 'package:example/rotating_light.dart'; -import 'package:example/simple_hud.dart'; +import 'package:example/touch_controlled_camera.dart'; import 'package:flame/events.dart'; -import 'package:flame/extensions.dart' as v64 show Vector2; -import 'package:flame/game.dart' show FlameGame, GameWidget; +import 'package:flame/game.dart' show GameWidget; import 'package:flame_3d/camera.dart'; import 'package:flame_3d/components.dart'; import 'package:flame_3d/game.dart'; import 'package:flame_3d/resources.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart' show runApp, Color, Colors, Listener; +import 'package:flutter/material.dart' show runApp, Color, Colors; -class ExampleGame3D extends FlameGame - with HasKeyboardHandlerComponents { +class ExampleGame3D extends FlameGame3D + with DragCallbacks, ScrollDetector { ExampleGame3D() : super( world: World3D(clearColor: const Color(0xFFFFFFFF)), - camera: KeyboardControlledCamera( - viewport: FixedResolutionViewport( - resolution: v64.Vector2(800, 600), - ), - hudComponents: [SimpleHud()], - ), + camera: TouchControlledCamera(), ); - @override - KeyboardControlledCamera get camera => - super.camera as KeyboardControlledCamera; - @override FutureOr onLoad() async { world.addAll([ @@ -146,30 +134,38 @@ class ExampleGame3D extends FlameGame ); } } + + @override + void onScroll(PointerScrollInfo info) { + const scrollSensitivity = 0.01; + final delta = info.scrollDelta.global.y.clamp(-10, 10) * scrollSensitivity; + + camera.distance += delta; + } + + @override + void onDragUpdate(DragUpdateEvent event) { + camera.delta.setValues(event.deviceDelta.x, event.deviceDelta.y); + super.onDragUpdate(event); + } + + @override + void onDragEnd(DragEndEvent event) { + camera.delta.setZero(); + super.onDragEnd(event); + } + + @override + void onDragCancel(DragCancelEvent event) { + camera.delta.setZero(); + super.onDragCancel(event); + } } void main() { final example = ExampleGame3D(); - runApp( - Listener( - onPointerMove: (event) { - if (!event.down) { - return; - } - example.camera.pointerEvent = event; - }, - onPointerSignal: (event) { - if (event is! PointerScrollEvent || !event.down) { - return; - } - example.camera.scrollMove = event.delta.dy / 3000; - }, - onPointerUp: (event) => example.camera.pointerEvent = null, - onPointerCancel: (event) => example.camera.pointerEvent = null, - child: GameWidget(game: example), - ), - ); + runApp(GameWidget(game: example)); } extension on Random { diff --git a/packages/flame_3d/example/lib/player_box.dart b/packages/flame_3d/example/lib/player_box.dart index 39bc0c34e70..e15444b965b 100644 --- a/packages/flame_3d/example/lib/player_box.dart +++ b/packages/flame_3d/example/lib/player_box.dart @@ -2,7 +2,6 @@ import 'dart:ui'; import 'package:example/main.dart'; import 'package:flame/components.dart' show HasGameReference; -import 'package:flame_3d/camera.dart'; import 'package:flame_3d/components.dart'; import 'package:flame_3d/game.dart'; import 'package:flame_3d/resources.dart'; @@ -20,10 +19,6 @@ class PlayerBox extends MeshComponent with HasGameReference { @override void renderTree(Canvas canvas) { - // Only show the box if we are in third person mode. - if (game.camera.mode == CameraMode.thirdPerson) { - position.setFrom(game.camera.target); - super.renderTree(canvas); - } + game.camera.target = position + Vector3(0, 2, 0); } } diff --git a/packages/flame_3d/example/lib/simple_hud.dart b/packages/flame_3d/example/lib/simple_hud.dart index 757a70a34a1..3cc3d69c9a2 100644 --- a/packages/flame_3d/example/lib/simple_hud.dart +++ b/packages/flame_3d/example/lib/simple_hud.dart @@ -61,7 +61,6 @@ Camera controls: canvas, ''' FPS: $fps -Mode: ${game.camera.mode.name} Projection: ${game.camera.projection.name} Culled: ${game.world.culled} diff --git a/packages/flame_3d/example/lib/touch_controlled_camera.dart b/packages/flame_3d/example/lib/touch_controlled_camera.dart new file mode 100644 index 00000000000..eb9e76e18ee --- /dev/null +++ b/packages/flame_3d/example/lib/touch_controlled_camera.dart @@ -0,0 +1,27 @@ +import 'package:example/simple_hud.dart'; +import 'package:flame/extensions.dart' as v64 show Vector2; +import 'package:flame_3d/camera.dart'; +import 'package:flame_3d/core.dart'; + +class TouchControlledCamera extends ThirdPersonCamera { + TouchControlledCamera() + : super( + following: Vector3(0, 2, 0), + followDamping: 1, + position: Vector3(0, 2, 4), + projection: CameraProjection.perspective, + distance: 3, + viewport: FixedResolutionViewport( + resolution: v64.Vector2(800, 600), + ), + hudComponents: [SimpleHud()], + ); + + Vector2 delta = Vector2.zero(); + + @override + void update(double dt) { + super.update(dt); + rotate(delta.x * dt, delta.y * dt); + } +} diff --git a/packages/flame_3d/lib/camera.dart b/packages/flame_3d/lib/camera.dart index cf4e46ba3ba..78d020f6928 100644 --- a/packages/flame_3d/lib/camera.dart +++ b/packages/flame_3d/lib/camera.dart @@ -3,4 +3,6 @@ export 'package:flame/camera.dart'; export 'package:vector_math/vector_math.dart' show Frustum; export 'src/camera/camera_component_3d.dart'; +export 'src/camera/first_person_camera.dart'; +export 'src/camera/third_person_camera.dart'; export 'src/camera/world_3d.dart'; diff --git a/packages/flame_3d/lib/src/camera/camera_component_3d.dart b/packages/flame_3d/lib/src/camera/camera_component_3d.dart index 0d849f66501..1f96ac8cf55 100644 --- a/packages/flame_3d/lib/src/camera/camera_component_3d.dart +++ b/packages/flame_3d/lib/src/camera/camera_component_3d.dart @@ -3,8 +3,6 @@ import 'package:flame_3d/game.dart'; enum CameraProjection { perspective, orthographic } -enum CameraMode { custom, free, orbital, firstPerson, thirdPerson } - /// {@template camera_component_3d} /// [CameraComponent3D] is a component through which a [World3D] is observed. /// {@endtemplate} @@ -13,16 +11,17 @@ class CameraComponent3D extends CameraComponent { CameraComponent3D({ this.fovY = 60, Vector3? position, + Quaternion? rotation, Vector3? target, Vector3? up, this.projection = CameraProjection.perspective, - this.mode = CameraMode.free, World3D? super.world, super.viewport, super.viewfinder, super.backdrop, super.hudComponents, }) : position = position?.clone() ?? Vector3.zero(), + rotation = rotation ?? Quaternion.identity(), target = target?.clone() ?? Vector3(0, 0, -1), _up = up?.clone() ?? Vector3(0, 1, 0); @@ -56,152 +55,61 @@ class CameraComponent3D extends CameraComponent { set up(Vector3 up) => _up.setFrom(up); final Vector3 _up; + /// The rotation of the camera. + Quaternion rotation; + /// The current camera projection. CameraProjection projection; - /// The current camera mode. - CameraMode mode; - /// The view matrix of the camera, this is without any projection applied on /// it. Matrix4 get viewMatrix => _viewMatrix..setAsViewMatrix(position, target, up); final Matrix4 _viewMatrix = Matrix4.zero(); /// The projection matrix of the camera. - Matrix4 get projectionMatrix { - final aspectRatio = viewport.virtualSize.x / viewport.virtualSize.y; - return switch (projection) { - CameraProjection.perspective => _projectionMatrix - ..setAsPerspective(fovY, aspectRatio, distanceNear, distanceFar), - CameraProjection.orthographic => _projectionMatrix - ..setAsOrthographic(fovY, aspectRatio, distanceNear, distanceFar) - }; - } - + Matrix4 get projectionMatrix => switch (projection) { + CameraProjection.perspective => _projectionMatrix + ..setAsPerspective( + fovY, + viewport.virtualSize.x / viewport.virtualSize.y, + distanceNear, + distanceFar, + ), + CameraProjection.orthographic => _projectionMatrix + ..setAsOrthographic( + fovY, + viewport.virtualSize.x / viewport.virtualSize.y, + distanceNear, + distanceFar, + ) + }; final Matrix4 _projectionMatrix = Matrix4.zero(); + /// The view projection matrix used for rendering. Matrix4 get viewProjectionMatrix => _viewProjectionMatrix ..setFrom(projectionMatrix) ..multiply(viewMatrix); final Matrix4 _viewProjectionMatrix = Matrix4.zero(); - final Frustum _frustum = Frustum(); - + /// The frustum of the [viewProjectionMatrix]. Frustum get frustum => _frustum..setFromMatrix(viewProjectionMatrix); + final Frustum _frustum = Frustum(); - void moveForward(double distance, {bool moveInWorldPlane = false}) { - final forward = this.forward..scale(distance); - - if (moveInWorldPlane) { - forward.y = 0; - forward.normalize(); - } - - position.add(forward); - target.add(forward); - } - - void moveUp(double distance) { - final up = this.up..scale(distance); - position.add(up); - target.add(up); - } - - void moveRight(double distance, {bool moveInWorldPlane = false}) { - final right = this.right..scale(distance); - - if (moveInWorldPlane) { - right.y = 0; - right.normalize(); - } - - position.add(right); - target.add(right); - } - - void moveToTarget(double delta) { - var distance = position.distanceTo(target); - distance += delta; - - if (distance <= 0) { - distance = 0.001; - } - - final forward = this.forward; - position.setValues( - target.x + (forward.x * -distance), - target.y + (forward.y * -distance), - target.z + (forward.z * -distance), - ); - } - - void yaw(double angle, {bool rotateAroundTarget = false}) { - final targetPosition = (target - position)..applyAxisAngle(up, angle); - - if (rotateAroundTarget) { - position.setValues( - target.x - targetPosition.x, - target.y - targetPosition.y, - target.z - targetPosition.z, - ); - } else { - target.setValues( - position.x + targetPosition.x, - position.y + targetPosition.y, - position.z + targetPosition.z, - ); - } - } - - void pitch( - double angle, { - bool lockView = false, - bool rotateAroundTarget = false, - bool rotateUp = false, - }) { - var localAngle = angle; - final up = this.up; - final targetPosition = target - position; - - if (lockView) { - final maxAngleUp = up.angleTo(targetPosition); - if (localAngle > maxAngleUp) { - localAngle = maxAngleUp; - } - - var maxAngleDown = (-up).angleTo(targetPosition); - maxAngleDown *= -1.0; - - if (localAngle < maxAngleDown) { - localAngle = maxAngleDown; - } - } - - final right = this.right; - targetPosition.applyAxisAngle(right, localAngle); - - if (rotateAroundTarget) { - position.setValues( - target.x - targetPosition.x, - target.y - targetPosition.y, - target.z - targetPosition.z, - ); - } else { - target.setValues( - position.x + targetPosition.x, - position.y + targetPosition.y, - position.z + targetPosition.z, - ); - } - - if (rotateUp) { - _up.applyAxisAngle(right, angle); - } + /// Rotates the camera's yaw and pitch. + /// + /// Both [yawDelta] and [pitchDelta] are in radians. + void rotate(double yawDelta, double pitchDelta) { + // Create quaternions for both yaw and pitch rotations. + final yawRotation = Quaternion.axisAngle(Vector3(0, 1, 0), yawDelta); + final pitchRotation = Quaternion.axisAngle(Vector3(1, 0, 0), pitchDelta); + + // Multiply the yaw with the current and pitch rotation to get the new + // camera rotation. + rotation = (yawRotation * rotation * pitchRotation)..normalize(); } - void roll(double angle) { - _up.applyAxisAngle(forward, angle); - } + /// Resets the camera's rotation to its default state, making it look forward. + void resetRotation() => rotation = Quaternion.identity(); static CameraComponent3D? get currentCamera => CameraComponent.currentCamera as CameraComponent3D?; diff --git a/packages/flame_3d/lib/src/camera/first_person_camera.dart b/packages/flame_3d/lib/src/camera/first_person_camera.dart new file mode 100644 index 00000000000..34aa06552aa --- /dev/null +++ b/packages/flame_3d/lib/src/camera/first_person_camera.dart @@ -0,0 +1,37 @@ +import 'package:flame_3d/camera.dart'; +import 'package:flame_3d/core.dart'; +import 'package:meta/meta.dart'; + +class FirstPersonCamera extends CameraComponent3D { + FirstPersonCamera({ + required this.following, + super.fovY, + super.position, + super.rotation, + super.up, + super.projection, + super.world, + super.viewport, + super.viewfinder, + super.backdrop, + super.hudComponents, + }); + + /// The point the camera should follow. + Vector3 following; + + @override + @mustCallSuper + void update(double dt) { + // Always set the camera's position to the following point. + position.setFrom(following); + + // Compute the desired target to look at. + target.setFrom(position + _getForwardDirection()); + } + + Vector3 _getForwardDirection() { + final forward = Vector3(0, 0, -1)..applyQuaternion(rotation); + return forward.normalized(); + } +} diff --git a/packages/flame_3d/lib/src/camera/third_person_camera.dart b/packages/flame_3d/lib/src/camera/third_person_camera.dart new file mode 100644 index 00000000000..493c1e50310 --- /dev/null +++ b/packages/flame_3d/lib/src/camera/third_person_camera.dart @@ -0,0 +1,52 @@ +import 'package:flame_3d/camera.dart'; +import 'package:flame_3d/core.dart'; +import 'package:meta/meta.dart'; + +class ThirdPersonCamera extends CameraComponent3D { + ThirdPersonCamera({ + required this.following, + double distance = 5.0, + this.followDamping = 1.0, + super.fovY, + super.position, + super.rotation, + super.up, + super.projection, + super.world, + super.viewport, + super.viewfinder, + super.backdrop, + super.hudComponents, + }) : _distance = distance; + + /// The point the camera should follow. + Vector3 following; + + /// The distance the camera should maintain from the `following` point. + double get distance => _distance; + set distance(double value) => _distance = value.clamp(0.1, double.infinity); + double _distance; + + /// Damping factor for smoothing out rotation and position changes. + /// + /// If the value is `1`, no damping is applied. + double followDamping; + + @override + @mustCallSuper + void update(double dt) { + // Compute the desired position based on the rotation and distance. + final desiredPosition = following + _getRotatedOffset(); + + // Smoothly interpolate the camera's position toward the desired position. + position = position + (desiredPosition - position) * (followDamping * dt); + + // Always look at the following point. + target.setFrom(following); + } + + Vector3 _getRotatedOffset() { + final forward = Vector3(0, 0, -1)..applyQuaternion(rotation); + return forward.normalized() * -distance; + } +} diff --git a/packages/flame_3d/lib/src/game/flame_game_3d.dart b/packages/flame_3d/lib/src/game/flame_game_3d.dart index ec3fa7d8ec9..1ec37a35ecf 100644 --- a/packages/flame_3d/lib/src/game/flame_game_3d.dart +++ b/packages/flame_3d/lib/src/game/flame_game_3d.dart @@ -3,16 +3,17 @@ import 'dart:ui'; import 'package:flame/game.dart'; import 'package:flame_3d/camera.dart'; -class FlameGame3D extends FlameGame { +class FlameGame3D + extends FlameGame { FlameGame3D({ super.children, W? world, - CameraComponent3D? camera, + C? camera, }) : super( world: world ?? World3D(clearColor: const Color(0xFFFFFFFF)) as W, - camera: camera ?? CameraComponent3D(), + camera: camera ?? CameraComponent3D() as C, ); @override - CameraComponent3D get camera => super.camera as CameraComponent3D; + C get camera => super.camera as C; } diff --git a/packages/flame_3d/lib/src/resources/texture/color_texture.dart b/packages/flame_3d/lib/src/resources/texture/color_texture.dart index eff49668560..4b4d161de4c 100644 --- a/packages/flame_3d/lib/src/resources/texture/color_texture.dart +++ b/packages/flame_3d/lib/src/resources/texture/color_texture.dart @@ -14,10 +14,10 @@ class ColorTexture extends Texture { List.filled( width * height, // Convert to a 32 bit value representing this color. - (color.a ~/ 255) << 24 | - (color.r ~/ 255) << 16 | - (color.g ~/ 255) << 8 | - (color.b ~/ 255), + (color.a * 255).round() << 24 | + (color.r * 255).round() << 16 | + (color.g * 255).round() << 8 | + (color.b * 255).round(), ), ).buffer.asByteData(), width: width,