
农场种菜类型的游戏,背景图尺寸是1800*1624
需要新建2个文件,分别控制画布的背景图和相机视角
相机视角缩放0.5倍,用于看到更多内容,
默认背景图定位到正中间,可以米字形拖拽
game_background.dart 背景图
dart
import 'package:flame/cache.dart';
import 'package:flame/components.dart';
/// 游戏背景图组件。
///
/// 只负责一件事:把 assets/images/bg.png 放进游戏世界。
class GameBackground extends SpriteComponent {
GameBackground({
required Images images,
}) : _images = images,
super(
anchor: Anchor.topLeft,
position: Vector2.zero(),
size: worldSize,
priority: -100,
);
/// Flame 默认图片目录是 assets/images/。
///
/// 所以这里写 bg.png,实际加载路径就是 assets/images/bg.png。
static const String imagePath = 'bg.png';
/// 默认相机缩放。
///
/// 背景保持 1800 x 1624 的设计稿坐标,相机用 0.5 倍视角观察,
/// 这样一屏可以看到更多地图内容,后续摆放碰撞区和角色也仍然按设计稿坐标写。
static const double defaultCameraZoom = 0.5;
/// 背景图在游戏世界中的尺寸。
///
/// 这里直接使用原设计稿尺寸,不再把图片尺寸 /2。
static Vector2 get worldSize => Vector2(1800, 1624);
final Images _images;
@override
Future<void> onLoad() async {
await super.onLoad();
final backgroundImage = await _images.load(imagePath);
sprite = Sprite(backgroundImage);
}
}
game_camera_controller.dart
dart
import 'package:flame/components.dart';
/// 游戏相机控制器。
///
/// 只负责一件事:根据拖动手势移动相机,并限制视角不滑出游戏世界边界。
class GameCameraController {
GameCameraController({
required Vector2 worldSize,
this.preferredZoom = 1,
}) : worldSize = worldSize.clone();
/// 游戏世界尺寸,这里对应背景图的设计稿尺寸。
final Vector2 worldSize;
/// 期望相机缩放。
///
/// 小于 1 表示看得更远、同屏展示更多地图内容;如果屏幕比例导致会露出地图外区域,
/// 实际 zoom 会被提高到刚好铺满屏幕。
final double preferredZoom;
Vector2 _viewportSize = Vector2.zero();
/// 初始化相机的基本设置。
///
/// Viewfinder 的 position 表示"当前视角左上角看到的世界坐标",这样更接近地图拖动的直觉。
void configureCamera(CameraComponent camera, Vector2 viewportSize) {
camera.viewfinder.anchor = Anchor.topLeft;
updateViewportSize(camera, viewportSize);
camera.viewfinder.position = _calculateCenteredPosition(
viewportSize: _viewportSize,
worldSize: worldSize,
zoom: camera.viewfinder.zoom,
);
}
/// 屏幕尺寸变化时重新限制相机位置。
///
/// 例如横竖屏切换后,原来的相机位置可能会露出背景外区域,需要重新 clamp。
void updateViewportSize(CameraComponent camera, Vector2 viewportSize) {
_viewportSize = viewportSize.clone();
final nextZoom = calculateCameraZoom(
preferredZoom: preferredZoom,
viewportSize: _viewportSize,
worldSize: worldSize,
);
camera.viewfinder.zoom = nextZoom;
camera.viewfinder.position = clampCameraPosition(
requestedPosition: camera.viewfinder.position,
viewportSize: _viewportSize,
worldSize: worldSize,
zoom: nextZoom,
);
}
/// 根据拖动距离移动相机。
///
/// 手指往左拖时,视角应该往右看,所以相机位置要减去 dragDelta。
/// DragUpdateEvent 同时保留 x/y 两个方向的位移,因此斜向拖动会自然形成"米字形"视角移动。
void moveByDragDelta(CameraComponent camera, Vector2 dragDelta) {
camera.viewfinder.position = calculatePositionAfterDrag(
currentPosition: camera.viewfinder.position,
dragDelta: dragDelta,
viewportSize: _viewportSize,
worldSize: worldSize,
zoom: camera.viewfinder.zoom,
);
}
/// 根据拖动距离计算下一帧相机位置。
static Vector2 calculatePositionAfterDrag({
required Vector2 currentPosition,
required Vector2 dragDelta,
required Vector2 viewportSize,
required Vector2 worldSize,
double zoom = 1,
}) {
final safeZoom = zoom <= 0 ? 1.0 : zoom;
final requestedPosition = currentPosition - dragDelta / safeZoom;
return clampCameraPosition(
requestedPosition: requestedPosition,
viewportSize: viewportSize,
worldSize: worldSize,
zoom: safeZoom,
);
}
/// 计算默认居中展示时的相机位置。
///
/// 因为 viewfinder 使用 topLeft 作为锚点,所以居中不是把 position 放到世界中心,
/// 而是把"可视区域左上角"放到居中后的起点。
static Vector2 _calculateCenteredPosition({
required Vector2 viewportSize,
required Vector2 worldSize,
required double zoom,
}) {
final safeZoom = zoom <= 0 ? 1.0 : zoom;
final visibleSize = viewportSize / safeZoom;
final requestedPosition = Vector2(
(worldSize.x - visibleSize.x) / 2,
(worldSize.y - visibleSize.y) / 2,
);
return clampCameraPosition(
requestedPosition: requestedPosition,
viewportSize: viewportSize,
worldSize: worldSize,
zoom: safeZoom,
);
}
/// 计算相机最小铺满缩放。
///
/// 当相机 zoom 过小时,可视区域会超过背景边界;这个值用于保证背景刚好能铺满屏幕。
static double calculateCoverZoom({
required Vector2 viewportSize,
required Vector2 worldSize,
}) {
if (viewportSize.x <= 0 ||
viewportSize.y <= 0 ||
worldSize.x <= 0 ||
worldSize.y <= 0) {
return 1.0;
}
final widthZoom = viewportSize.x / worldSize.x;
final heightZoom = viewportSize.y / worldSize.y;
final coverZoom = widthZoom > heightZoom ? widthZoom : heightZoom;
return coverZoom;
}
/// 计算实际相机缩放。
///
/// preferredZoom 决定"想看多远",coverZoom 决定"不能露出地图外区域",最终取更大的值。
static double calculateCameraZoom({
required double preferredZoom,
required Vector2 viewportSize,
required Vector2 worldSize,
}) {
final safePreferredZoom = preferredZoom <= 0 ? 1.0 : preferredZoom;
final coverZoom = calculateCoverZoom(
viewportSize: viewportSize,
worldSize: worldSize,
);
return safePreferredZoom > coverZoom ? safePreferredZoom : coverZoom;
}
/// 把相机位置限制在游戏世界内部。
///
/// 因为 viewfinder.anchor 是 topLeft,所以 position 可以理解成当前屏幕左上角的世界坐标。
static Vector2 clampCameraPosition({
required Vector2 requestedPosition,
required Vector2 viewportSize,
required Vector2 worldSize,
double zoom = 1,
}) {
final safeZoom = zoom <= 0 ? 1.0 : zoom;
final visibleSize = viewportSize / safeZoom;
final maxX = (worldSize.x - visibleSize.x).clamp(0, worldSize.x);
final maxY = (worldSize.y - visibleSize.y).clamp(0, worldSize.y);
return Vector2(
requestedPosition.x.clamp(0, maxX).toDouble(),
requestedPosition.y.clamp(0, maxY).toDouble(),
);
}
}
最后记得在GameHomeCanvasGame中添加游戏相机控制器和背景图
dart
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'components/game_background.dart';
import 'components/game_camera_controller.dart';
/// 游戏首页的最基础 Flame 画布。
///
/// 当前只负责提供一个可渲染的游戏世界,后续玩家、地图、碰撞等组件都从这里添加。
class GameHomeCanvasGame extends FlameGame with DragCallbacks {
final GameCameraController gameCameraController = GameCameraController(
worldSize: GameBackground.worldSize,
preferredZoom: GameBackground.defaultCameraZoom,
);
@override
Color backgroundColor() => const Color(0xFF101828);
@override
Future<void> onLoad() async {
await super.onLoad();
// 相机控制器
gameCameraController.configureCamera(camera, size);
// 背景要先添加,后续玩家、敌人、按钮等组件再添加时会自然盖在背景上方。
await world.add(GameBackground(images: images));
}
@override
void onGameResize(Vector2 size) {
super.onGameResize(size);
gameCameraController.updateViewportSize(camera, size);
}
@override
void onDragUpdate(DragUpdateEvent event) {
super.onDragUpdate(event);
gameCameraController.moveByDragDelta(camera, event.canvasDelta);
}
}
代码理解:
bash
GameBackground它可以理解成"地图组件"。
imagePath
表示 Flame 要加载哪张图。这里写 bg.png,实际对应 assets/images/bg.png。
worldSize
表示这张背景图在游戏世界里的尺寸。现在是:
Vector2(1800, 1624)
也就是按设计稿原尺寸来摆放。后面你放角色、建筑、碰撞区域,都可以按这个坐标系理解。
defaultCameraZoom
默认相机缩放。现在是 0.5,意思是相机看得更远,一屏能看到更多地图。
onLoad()
组件加载时执行。这里做的事很简单:加载 bg.png,然后把它变成 Flame 能渲染的 Sprite。
GameCameraController
它可以理解成"摄像机控制器",不管背景图怎么画,只管镜头位置、缩放、拖动边界。
worldSize
地图总尺寸,也就是 1800 x 1624。相机需要知道地图多大,才能避免拖出边界。
preferredZoom
你希望相机默认缩放多少。0.5 表示缩小视角、看更多内容。
configureCamera()
初始化相机。现在做三件事:
把相机锚点设成左上角。
根据屏幕尺寸计算实际 zoom。
把初始视角定位到地图中心。
updateViewportSize()
屏幕尺寸变化时重新计算相机,比如横竖屏切换、首次布局。它会重新算 zoom,并确保相机没有跑出地图外。
moveByDragDelta()
拖动时调用。它根据手指滑动距离移动相机。因为 dragDelta 同时有 x/y,所以天然支持上下左右和斜向拖动。
calculateCameraZoom()
计算最终相机缩放。逻辑是:
最终 zoom = max(你想要的 zoom, 不露边所需的最小 zoom)
比如你想用 0.5,但某个长屏手机如果 0.5 会露出地图外背景,那它会自动调大一点。
calculateCoverZoom()
计算"刚好不露出地图外区域"的最小 zoom。
_calculateCenteredPosition()
计算初始居中位置。因为相机锚点是左上角,所以不是把相机 position 设成地图中心,而是算:
相机左上角 = (地图尺寸 - 当前可视尺寸) / 2
clampCameraPosition()
限制相机不要拖出地图边界。比如你使劲往左上拖,最小只能到 (0, 0);往右下拖,也只能到地图右下边界。
一句话总结:GameBackground 决定"地图有多大、用哪张图";GameCameraController 决定"玩家现在从哪个位置、用多远的镜头看地图"。