Flame:02、画布中创建背景图
农场种菜类型的游戏背景图尺寸是1800*1624需要新建2个文件分别控制画布的背景图和相机视角相机视角缩放0.5倍用于看到更多内容默认背景图定位到正中间可以米字形拖拽game_background.dart 背景图importpackage:flame/cache.dart;importpackage:flame/components.dart;/// 游戏背景图组件。////// 只负责一件事把 assets/images/bg.png 放进游戏世界。classGameBackgroundextendsSpriteComponent{GameBackground({requiredImagesimages,}):_imagesimages,super(anchor:Anchor.topLeft,position:Vector2.zero(),size:worldSize,priority:-100,);/// Flame 默认图片目录是 assets/images/。////// 所以这里写 bg.png实际加载路径就是 assets/images/bg.png。staticconstStringimagePathbg.png;/// 默认相机缩放。////// 背景保持 1800 x 1624 的设计稿坐标相机用 0.5 倍视角观察/// 这样一屏可以看到更多地图内容后续摆放碰撞区和角色也仍然按设计稿坐标写。staticconstdouble defaultCameraZoom0.5;/// 背景图在游戏世界中的尺寸。////// 这里直接使用原设计稿尺寸不再把图片尺寸 /2。staticVector2getworldSizeVector2(1800,1624);finalImages_images;overrideFuturevoidonLoad()async{awaitsuper.onLoad();finalbackgroundImageawait_images.load(imagePath);spriteSprite(backgroundImage);}}game_camera_controller.dartimportpackage:flame/components.dart;/// 游戏相机控制器。////// 只负责一件事根据拖动手势移动相机并限制视角不滑出游戏世界边界。classGameCameraController{GameCameraController({requiredVector2worldSize,this.preferredZoom1,}):worldSizeworldSize.clone();/// 游戏世界尺寸这里对应背景图的设计稿尺寸。finalVector2worldSize;/// 期望相机缩放。////// 小于 1 表示看得更远、同屏展示更多地图内容如果屏幕比例导致会露出地图外区域/// 实际 zoom 会被提高到刚好铺满屏幕。finaldouble preferredZoom;Vector2_viewportSizeVector2.zero();/// 初始化相机的基本设置。////// Viewfinder 的 position 表示“当前视角左上角看到的世界坐标”这样更接近地图拖动的直觉。voidconfigureCamera(CameraComponentcamera,Vector2viewportSize){camera.viewfinder.anchorAnchor.topLeft;updateViewportSize(camera,viewportSize);camera.viewfinder.position_calculateCenteredPosition(viewportSize:_viewportSize,worldSize:worldSize,zoom:camera.viewfinder.zoom,);}/// 屏幕尺寸变化时重新限制相机位置。////// 例如横竖屏切换后原来的相机位置可能会露出背景外区域需要重新 clamp。voidupdateViewportSize(CameraComponentcamera,Vector2viewportSize){_viewportSizeviewportSize.clone();finalnextZoomcalculateCameraZoom(preferredZoom:preferredZoom,viewportSize:_viewportSize,worldSize:worldSize,);camera.viewfinder.zoomnextZoom;camera.viewfinder.positionclampCameraPosition(requestedPosition:camera.viewfinder.position,viewportSize:_viewportSize,worldSize:worldSize,zoom:nextZoom,);}/// 根据拖动距离移动相机。////// 手指往左拖时视角应该往右看所以相机位置要减去 dragDelta。/// DragUpdateEvent 同时保留 x/y 两个方向的位移因此斜向拖动会自然形成“米字形”视角移动。voidmoveByDragDelta(CameraComponentcamera,Vector2dragDelta){camera.viewfinder.positioncalculatePositionAfterDrag(currentPosition:camera.viewfinder.position,dragDelta:dragDelta,viewportSize:_viewportSize,worldSize:worldSize,zoom:camera.viewfinder.zoom,);}/// 根据拖动距离计算下一帧相机位置。staticVector2calculatePositionAfterDrag({requiredVector2currentPosition,requiredVector2dragDelta,requiredVector2viewportSize,requiredVector2worldSize,double zoom1,}){finalsafeZoomzoom0?1.0:zoom;finalrequestedPositioncurrentPosition-dragDelta/safeZoom;returnclampCameraPosition(requestedPosition:requestedPosition,viewportSize:viewportSize,worldSize:worldSize,zoom:safeZoom,);}/// 计算默认居中展示时的相机位置。////// 因为 viewfinder 使用 topLeft 作为锚点所以居中不是把 position 放到世界中心/// 而是把“可视区域左上角”放到居中后的起点。staticVector2_calculateCenteredPosition({requiredVector2viewportSize,requiredVector2worldSize,required double zoom,}){finalsafeZoomzoom0?1.0:zoom;finalvisibleSizeviewportSize/safeZoom;finalrequestedPositionVector2((worldSize.x-visibleSize.x)/2,(worldSize.y-visibleSize.y)/2,);returnclampCameraPosition(requestedPosition:requestedPosition,viewportSize:viewportSize,worldSize:worldSize,zoom:safeZoom,);}/// 计算相机最小铺满缩放。////// 当相机 zoom 过小时可视区域会超过背景边界这个值用于保证背景刚好能铺满屏幕。staticdoublecalculateCoverZoom({requiredVector2viewportSize,requiredVector2worldSize,}){if(viewportSize.x0||viewportSize.y0||worldSize.x0||worldSize.y0){return1.0;}finalwidthZoomviewportSize.x/worldSize.x;finalheightZoomviewportSize.y/worldSize.y;finalcoverZoomwidthZoomheightZoom?widthZoom:heightZoom;returncoverZoom;}/// 计算实际相机缩放。////// preferredZoom 决定“想看多远”coverZoom 决定“不能露出地图外区域”最终取更大的值。staticdoublecalculateCameraZoom({required double preferredZoom,requiredVector2viewportSize,requiredVector2worldSize,}){finalsafePreferredZoompreferredZoom0?1.0:preferredZoom;finalcoverZoomcalculateCoverZoom(viewportSize:viewportSize,worldSize:worldSize,);returnsafePreferredZoomcoverZoom?safePreferredZoom:coverZoom;}/// 把相机位置限制在游戏世界内部。////// 因为 viewfinder.anchor 是 topLeft所以 position 可以理解成当前屏幕左上角的世界坐标。staticVector2clampCameraPosition({requiredVector2requestedPosition,requiredVector2viewportSize,requiredVector2worldSize,double zoom1,}){finalsafeZoomzoom0?1.0:zoom;finalvisibleSizeviewportSize/safeZoom;finalmaxX(worldSize.x-visibleSize.x).clamp(0,worldSize.x);finalmaxY(worldSize.y-visibleSize.y).clamp(0,worldSize.y);returnVector2(requestedPosition.x.clamp(0,maxX).toDouble(),requestedPosition.y.clamp(0,maxY).toDouble(),);}}最后记得在GameHomeCanvasGame中添加游戏相机控制器和背景图importpackage:flame/events.dart;importpackage:flame/game.dart;importpackage:flutter/material.dart;importcomponents/game_background.dart;importcomponents/game_camera_controller.dart;/// 游戏首页的最基础 Flame 画布。////// 当前只负责提供一个可渲染的游戏世界后续玩家、地图、碰撞等组件都从这里添加。classGameHomeCanvasGameextendsFlameGamewithDragCallbacks{finalGameCameraControllergameCameraControllerGameCameraController(worldSize:GameBackground.worldSize,preferredZoom:GameBackground.defaultCameraZoom,);overrideColorbackgroundColor()constColor(0xFF101828);overrideFuturevoidonLoad()async{awaitsuper.onLoad();// 相机控制器gameCameraController.configureCamera(camera,size);// 背景要先添加后续玩家、敌人、按钮等组件再添加时会自然盖在背景上方。awaitworld.add(GameBackground(images:images));}overridevoidonGameResize(Vector2size){super.onGameResize(size);gameCameraController.updateViewportSize(camera,size);}overridevoidonDragUpdate(DragUpdateEventevent){super.onDragUpdate(event);gameCameraController.moveByDragDelta(camera,event.canvasDelta);}}代码理解GameBackground它可以理解成“地图组件”。 imagePath 表示 Flame 要加载哪张图。这里写 bg.png实际对应 assets/images/bg.png。 worldSize 表示这张背景图在游戏世界里的尺寸。现在是 Vector2(1800,1624)也就是按设计稿原尺寸来摆放。后面你放角色、建筑、碰撞区域都可以按这个坐标系理解。 defaultCameraZoom 默认相机缩放。现在是0.5意思是相机看得更远一屏能看到更多地图。 onLoad()组件加载时执行。这里做的事很简单加载 bg.png然后把它变成 Flame 能渲染的 Sprite。 GameCameraController 它可以理解成“摄像机控制器”不管背景图怎么画只管镜头位置、缩放、拖动边界。 worldSize 地图总尺寸也就是1800x1624。相机需要知道地图多大才能避免拖出边界。 preferredZoom 你希望相机默认缩放多少。0.5 表示缩小视角、看更多内容。 configureCamera()初始化相机。现在做三件事 把相机锚点设成左上角。 根据屏幕尺寸计算实际 zoom。 把初始视角定位到地图中心。 updateViewportSize()屏幕尺寸变化时重新计算相机比如横竖屏切换、首次布局。它会重新算 zoom并确保相机没有跑出地图外。 moveByDragDelta()拖动时调用。它根据手指滑动距离移动相机。因为 dragDelta 同时有 x/y所以天然支持上下左右和斜向拖动。 calculateCameraZoom()计算最终相机缩放。逻辑是 最终 zoommax(你想要的 zoom, 不露边所需的最小 zoom)比如你想用0.5但某个长屏手机如果0.5会露出地图外背景那它会自动调大一点。 calculateCoverZoom()计算“刚好不露出地图外区域”的最小 zoom。 _calculateCenteredPosition()计算初始居中位置。因为相机锚点是左上角所以不是把相机 position 设成地图中心而是算 相机左上角(地图尺寸 - 当前可视尺寸)/2clampCameraPosition()限制相机不要拖出地图边界。比如你使劲往左上拖最小只能到(0,0)往右下拖也只能到地图右下边界。一句话总结GameBackground 决定“地图有多大、用哪张图”GameCameraController 决定“玩家现在从哪个位置、用多远的镜头看地图”。