Flutter 勇闯2D像素游戏之路(一):一个 Hero 的诞生
Flutter 勇闯2D像素游戏之路(二):绘制加载游戏地图
Flutter 勇闯2D像素游戏之路(三):人物与地图元素的交互
前言
在上篇文章中,我们初识了2D 地图编辑器 Tiled,完成了从地图绘制、资源加载到基础墙体碰撞的搭建,为游戏世界奠定了最基本的空间规则。
但仅有能走、能挡的地图,还远不足以构成一个真正有生命力的游戏场景。
真正让地图活起来的,是人物与地图元素之间的交互。
在游戏中,地图并不仅仅是静态的背景,它往往承载着多种功能性的元素,例如:
- 可被触发的宝箱、机关
- 控制通行逻辑的钥匙与门
- 带来风险与挑战的陷阱、伤害型障碍物
这些元素都需要与人物产生行为上的联动:
接触、判断、反馈、结果,共同构成完整的交互闭环。
因此,本篇文章将着重于构建人物与地图元素的交互,逐步介绍、实现几类常见的地图交互对象,构建一个更加丰富、可扩展的地图交互体系。
前情提示
1. 地图
在进行本章的内容实践之前,请先将之前地图中门、宝箱、钥匙、地刺元素先行清除。
本章将把他们从单纯装饰件,进化到真正的可交互件。
接下来所需要用到的门、宝箱、钥匙、地刺等图片素材,已在github仓库中添加。
2. 墙体
首先,先感谢兄弟的反馈 😊
接下来我们就用Object Layer和Tile Layer实现上一期的墙体碰撞区,看看区别。
Object Layer
Tile Layer
从图中可以看到:
- Tile Layer的碰撞区完美和地图中wall图层匹配
- Object Layer的碰撞区完美和Collisions对象层匹配
在本文素材的前提之下:
- 如果按Tile Layer,那么碰撞区范围就偏大了,需要你重新绘制墙体,但是你会发现素材中墙体与下面的地板是在一块的,不能分离,这就不太完美。
- 而Object Layer可以让我们在不破坏改变墙体的前提下,更精细调整墙体的碰撞区,让它更合理。
因此,各有利弊,看大家的取舍。
本文中水面的碰撞区就是Tile Layer方法,大家可以看一看。
MyHero
一. 本章目标
二. 人物碰撞矩形优化
1. 优化前
// hero_component.dart@overrideFuture<void>onLoad()async{...size=Vector2(100,100);position=Vector2(1000,1000);_hitbox=RectangleHitbox();add(_hitbox);}默认情况下,人物的碰撞区域RectangleHitbox会继承父组件的 size,也就是说碰撞矩形就是100×100,所以碰撞框很大,通常会比人物实际可视部分大。
这样就导致上图中问题,在一个狭窄的走道内,由于你的超大碰撞区域,而被卡住。
2. 优化后
// hero_component.dart@overrideFuture<void>onLoad()async{...size=Vector2(100,100);position=Vector2(1000,1000);_hitbox=RectangleHitbox.relative(Vector2(0.5,0.2),// 碰撞区域占组件宽高比例parentSize:size,position:Vector2(size.x*0.25,size.y*0.7),// 碰撞区域的偏移);add(_hitbox);}优化后,我们使用RectangleHitbox.relative手动缩小碰撞矩形,并调整偏移,使碰撞区域只覆盖角色脚底,即可获取正常的游戏体验:
- 碰撞矩形与角色可视部分更贴合,避免卡墙。
- 在狭窄走道中角色可以顺畅移动。
- 保留足够的碰撞检测区域,保证游戏逻辑和物理交互仍然有效。
✨总结:
通过简单缩小并下移碰撞矩形,实现了视觉与碰撞逻辑的分离,就可以极大提升可控性和玩家体验。
三. 统一碰撞管理
1. 必要性
在游戏开发中,墙体、门、障碍区域等碰撞对象往往分布在地图的不同层级或组件中。
这类对象的共同特点是:不一定需要渲染,但必须参与碰撞判断。
如果每个组件各自实现碰撞逻辑,容易出现以下问题:
- 逻辑分散:碰撞判断分布在多个组件中,角色移动与阻挡逻辑难以统一管理。
- 重复实现:不同碰撞对象往往需要相同的尺寸、位置和碰撞检测代码,容易产生冗余。
- 扩展不便:新增一种可碰撞对象时,需要重复编写或修改现有碰撞相关逻辑。
因此,有必要建立一个统一的碰撞管理,将可参与碰撞的能力集中管理。
2. 实现
(1)新建碰撞基类
抽象出一个BlockerComponent作为统一的碰撞基类,后续所有需要参与碰撞的组件直接继承该类即可,避免在各个组件中重复编写碰撞逻辑。
// blocker_component.dartabstractclassBlockerComponentextendsSpriteComponentwithCollisionCallbacks{latefinalRectangleHitbox hitbox;BlockerComponent({super.position,requiredsuper.size,bool addHitbox=true,}){if(addHitbox){hitbox=RectangleHitbox();add(hitbox);}}@overrideFuture<void>onLoad()async{// 创建 1×1 的透明 sprite,占位以满足 SpriteComponent 要求finalrecorder=PictureRecorder();finalcanvas=Canvas(recorder);finalpaint=Paint()..color=constColor(0x00000000);canvas.drawRect(constRect.fromLTWH(0,0,1,1),paint);finalpicture=recorder.endRecording();finalimage=awaitpicture.toImage(1,1);sprite=Sprite(image);}boolcollidesWith(Rect heroRect){returnhitbox.toAbsoluteRect().overlaps(heroRect);}}作为可碰撞组件的基础类
- 继承
SpriteComponent,统一管理位置与尺寸 - 混入
CollisionCallbacks,接入 Flame 碰撞体系 - 使用
RectangleHitbox作为实际的碰撞区域
- 继承
解决 SpriteComponent 必须绑定 sprite 的限制
- 在
onLoad中创建一个1×1的透明 sprite - 组件本身不可见,但在引擎层面是合法的渲染组件
- 在
提供手动碰撞检测能力
- 通过
collidesWith(Rect heroRect)判断是否发生重叠 - 适用于角色移动过程中的阻挡判断与位移修正
- 通过
(2)修改代码
首先,我们就可以将原来的wall_component.dart继承了BlockerComponent。
// wall_component.dartclassWallComponentextendsBlockerComponent{WallComponent({Vector2?position,required Vector2 size}):super(position:position,size:size);}其次,在my_game.dart中,我们将原来只用于收集墙体的列表修改:
// my_game.dartfinalList<WallComponent>walls--->finalList<BlockerComponent>blockers// 统一添加管理blockers.add();最后,在hero_component.dart中修改:
- 碰撞性能优化集合
finalSet<BlockerComponent>_nearbyBlockers={};late RectangleHitbox _hitbox;- 碰撞检测方法
bool_wouldCollideWithBlockers(){finalheroRect=_hitbox.toAbsoluteRect();// 优先使用游戏维护的 blockers 集合for(finalblockeringame.blockers){if(blocker.collidesWith(heroRect))returntrue;}...// 兼容附近 blockersfor(finalnearbyin_nearbyBlockers){if(nearby.collidesWith(heroRect))returntrue;}returnfalse;}- 碰撞回调
@overridevoidonCollisionStart(Set<Vector2>intersectionPoints,PositionComponent other,){super.onCollisionStart(intersectionPoints,other);if(otherisBlockerComponent){_nearbyBlockers.add(other);}}@overridevoidonCollisionEnd(PositionComponent other){super.onCollisionEnd(other);if(otherisBlockerComponent){_nearbyBlockers.remove(other);}}四. Tile Layer 构建水面碰撞区
构建水面碰撞区,简单来说就是遍历Tiled地图中的water图层,将所有非空瓦片转换为对应的WaterComponent,并按瓦片位置和大小添加到游戏世界中,用于表示水面。
方式一:逐瓦片创建组件(仅展示)
finalwaterLayer=tiled.tileMap.getLayer<TileLayer>('water');if(waterLayer!=null&&waterLayer.data!=null){finalwidth=waterLayer.width;finalheight=waterLayer.height;finaldata=waterLayer.data!;for(vary=0;y<height;y++){for(varx=0;x<width;x++){finalindex=y*width+x;finalgid=data[index];if(gid!=0){finalwater=WaterComponent(position:Vector2(x*tileSize*mapScale,y*tileSize*mapScale,),size:Vector2(tileSize*mapScale,tileSize*mapScale,),);// 添加至统一碰撞列表blockers.add(water);awaitworld.add(water);}}}}这里将地图中每一个8x8的水瓦片,都单独变成一个WaterComponent对象。
可以看见地图中水面数量巨大,产生了大量组件,极大降低渲染和碰撞效率。
方式二:批量合并创建组件(推荐)
// my_game.dartfinalwaterLayer=tiled.tileMap.getLayer<TileLayer>('water');if(waterLayer!=null&&waterLayer.data!=null){awaitaddMergedTileLayerV2(tileData:waterLayer.data!,width:waterLayer.width,height:waterLayer.height,tileSize:tileSize,scale:mapScale,createComponent:(position,size)async{finalwater=WaterComponent(position:position,size:size);// 添加至统一碰撞列表blockers.add(water);returnwater;},parent:world,);}这种方式通过addMergedTileLayerV2将相邻的水瓦片批量合并为较少的组件:
- 大幅减少
Component数量,提高性能 - 保持碰撞逻辑和渲染一致
- 可以统一加入
blockers列表,方便英雄碰撞预测
可以看见地图中,数量巨大的组件,经过
addMergedTileLayerV2方法批量合并后,仅为3个了。
批量合并方法
// common.dart/// 扫描 TileLayer 数据,将连续非零瓦片合并成矩形组件(水平+垂直合并)/// [tileData] - TileLayer.data,按行展开/// [width] - TileLayer 宽度(列数)/// [height] - TileLayer 高度(行数)/// [tileSize] - 每个瓦片大小/// [scale] - 地图缩放/// [createComponent] - 创建每个碰撞块的方法/// [parent] - 父组件,用于添加生成的组件Future<void>addMergedTileLayerV2({required List<int>tileData,required int width,required int height,required double tileSize,double scale=1.0,required Future<PositionComponent>Function(Vector2 position,Vector2 size)createComponent,required Component parent,})async{// Step1: 先按行生成水平合并块List<List<int>>rects=[];// 每行的开始列和宽度List<List<int>>horizontalRects=List.generate(height,(_)=>[]);for(vary=0;y<height;y++){int startX=-1;for(varx=0;x<=width;x++){finalisFilled=x<width&&tileData[y*width+x]!=0;if(isFilled&&startX==-1){startX=x;}if((!isFilled||x==width)&&startX!=-1){horizontalRects[y].addAll([startX,x-startX]);startX=-1;}}}// Step2: 垂直合并相同列和宽度的块List<List<int>>processed=List.generate(height,(_)=>List.filled(width,0));for(vary=0;y<height;y++){for(vari=0;i<horizontalRects[y].length;i+=2){finalxStart=horizontalRects[y][i];finalw=horizontalRects[y][i+1];if(processed[y][xStart]==1)continue;// 尝试向下合并int rectHeight=1;for(varyy=y+1;yy<height;yy++){bool canMerge=false;for(varj=0;j<horizontalRects[yy].length;j+=2){if(horizontalRects[yy][j]==xStart&&horizontalRects[yy][j+1]==w){canMerge=true;break;}}if(canMerge){rectHeight++;for(varcol=xStart;col<xStart+w;col++){processed[yy][col]=1;}}else{break;}}// 创建组件finalposition=Vector2(xStart*tileSize*scale,y*tileSize*scale);finalsize=Vector2(w*tileSize*scale,rectHeight*tileSize*scale);finalblock=awaitcreateComponent(position,size);awaitparent.add(block);for(varcol=xStart;col<xStart+w;col++){processed[y][col]=1;}}}}
addMergedTileLayerV2用于扫描 TileLayer 的瓦片数据,将相邻的非空瓦片先进行水平方向合并,再在此基础上进行垂直方向合并,最终把多个连续瓦片合并为更大的矩形组件。
五. 宝箱 - 无验证交互式解锁
1. 简述
宝箱是一种无需条件验证的可交互地图对象,通过角色接触即可触发交互。
继承关系:无需继承碰撞基类
功能:提供可拾取奖励
交互逻辑:
- 玩家靠近宝箱即可触发打开动作
- 打开后玩家可选择是否拾取奖励
2. Tiled 前置工作
- 新建
Object Layer图层,命名为treasure。 - 点击使用上方矩形框选工具。
- 在地图中所需位置,正确绘制宝箱大小的矩形。
- 添加属性:
- type:trasure
- status:表示宝箱状态,一共有三种:closed(关闭)、full(打开未拿取)、enmpty(打开已拿取)
3. 构建宝箱类
// treasure_component.dartclassTreasureComponentextendsSpriteComponentwithHasGameReference<MyGame>,CollisionCallbacks{String status;late RectangleHitbox _hitbox;TreasureComponent({requiredthis.status,required Vector2 position,required Vector2 size,}):super(position:position,size:size);@overrideFuture<void>onLoad()async{awaitsuper.onLoad();sprite=awaitSprite.load(status=='closed'?'closed_treasure.png':status=='full'?'full_treasure.png':'empty_treasure.png',);_hitbox=RectangleHitbox();add(_hitbox);}@overridevoidonCollisionStart(Set<Vector2>intersectionPoints,PositionComponent other,){super.onCollisionStart(intersectionPoints,other);if(otherisHeroComponent){attemptOpen(other);}}Future<void>_open()async{if(status=='closed'){status='full';sprite=awaitSprite.load('full_treasure.png');}}Future<void>_collect(HeroComponent hero)async{if(status=='full'){status='empty';sprite=awaitSprite.load('empty_treasure.png');UiNotify.showToast(game,'获得宝物');}}voidattemptOpen(HeroComponent hero){if(status=='closed'){_open();}elseif(status=='full'){finalexists=game.camera.viewport.children.query<DialogComponent>().isNotEmpty;if(!exists){finaldialog=DialogComponent.confirm(message:'是否拿取宝物',onConfirm:()=>_collect(hero),onCancel:(){},);game.camera.viewport.add(dialog);}}}}TreasureComponent作为宝箱实体类,负责显示与交互的逻辑:
继承
SpriteComponent+ 碰撞回调- 具备位置、尺寸和碰撞检测能力
状态管理
status = closed:关闭状态,触碰后自动打开status = full:已打开状态,弹出对话框确认是否拾取status = empty:已拿取状态,仅显示空宝箱
视图更新
- 根据宝箱状态动态加载不同贴图
交互逻辑
- 人物触碰到宝箱区域后,根据当前状态,决定下一步行为
4. 加载宝箱
// my_game.dartfinaltreasureLayer=tiled.tileMap.getLayer<ObjectGroup>('treasure');if(treasureLayer!=null){for(finalobjintreasureLayer.objects){if(obj.properties['type']?.value=='treasure'){finalstatus=obj.properties['status']!.valueasString;finalx=mapScale*obj.x;finaly=mapScale*obj.y;finalw=mapScale*obj.width;finalh=mapScale*obj.height;finaltreasureComponent=TreasureComponent(status:status,position:Vector2(x,y),size:Vector2(w,h),);treasureComponent.debugMode=true;awaitworld.add(treasureComponent);}}}六. 钥匙与门 - 验证交互式解锁
1. 简述
钥匙与门是一组具备条件验证的交互对象,通过拾取钥匙 → 解锁对应门实现地图通行控制。
继承关系:门继承碰撞基类,当门未打开时具备阻挡能力,与墙体或水面碰撞逻辑一致
钥匙:可拾取对象,角色接触后获得对应
keyId门:阻挡地图通行,需角色持有匹配钥匙才能打开
交互逻辑:
- 角色与门交互时,判断是否持有匹配钥匙
- 条件满足时门打开,否则保持阻挡状态
2. Tiled 前置工作
- 新建两个
Object Layer图层,分别命名为key、door。 - 点击使用上方矩形框选工具。
- 在地图中所需位置,正确绘制钥匙和门大小的矩形。
- 添加属性:
钥匙:- type:key
- keyId:对应指定的钥匙与门
门:- type:door
- keyId:对应指定的钥匙与门
- status:表示门状态,一共有两种:closed(关闭)、open(打开)
3. 构建钥匙类
// key_component.dartclassKeyComponentextendsSpriteComponentwithHasGameReference<MyGame>,CollisionCallbacks{finalString keyId;KeyComponent({requiredthis.keyId,required Vector2 position,required Vector2 size,}):super(position:position,size:size);@overrideFuture<void>onLoad()async{awaitsuper.onLoad();sprite=awaitSprite.load('key.png');// 直接加载 assets/key.pngadd(RectangleHitbox());}@overridevoidonCollisionStart(Set<Vector2>intersectionPoints,PositionComponent other){super.onCollisionStart(intersectionPoints,other);if(otherisHeroComponent){other.addKey(keyId);removeFromParent();// 拾取后移除当前 tile}}}KeyComponent作为可拾取的钥匙实体,用于角色解锁对应门:
继承
SpriteComponent+ 碰撞回调- 具备位置、尺寸和碰撞检测能力
钥匙标识
keyId- 用于与门的
keyId对应,实现解锁验证
- 用于与门的
碰撞拾取逻辑
- 当角色碰到钥匙:
将keyId存入角色钥匙集合
从地图中移除钥匙实体,避免重复拾取
- 当角色碰到钥匙:
4. 构建门类
// door_component.dartclassDoorComponentextendsBlockerComponentwithHasGameReference<MyGame>{finalString keyId;bool isOpen;DoorComponent({requiredthis.keyId,requiredthis.isOpen,super.position,requiredsuper.size,}):super(addHitbox:!isOpen);@overrideFuture<void>onLoad()async{awaitsuper.onLoad();sprite=awaitSprite.load(isOpen?'open_door.png':'closed_door.png');}void_unlock()async{if(!isOpen){isOpen=true;sprite=awaitSprite.load('open_door.png');hitbox.removeFromParent();}}voidattemptOpen(HeroComponent hero){if(!isOpen&&hero.hasKey(keyId)){unlock();}elseif(!isOpen){UiNotify.showToast(game,'需要钥匙 $keyId 才能打开');}}}DoorComponent作为门的实体类,主要职责是阻挡角色通行并支持钥匙验证解锁:
继承
BlockerComponent- 默认有碰撞体阻挡角色
- 可根据
isOpen状态决定是否添加碰撞体
状态管理
isOpen = false:门关闭,阻挡角色isOpen = true:门打开,移除碰撞体,允许角色通过
钥匙验证逻辑
attemptOpen(hero):当角色接触门时触发- 如果角色持有匹配
keyId,调用_unlock()打开门 - 否则弹出提示,告知需要钥匙
- 如果角色持有匹配
视图更新
- 根据门状态动态加载不同贴图(
open_door.png/closed_door.png) - 打开时同步移除碰撞体,保证角色能通过
- 根据门状态动态加载不同贴图(
5. 加载钥匙与门
// my_game.dart// ---- 处理 Key Layer 中的钥匙 ----finalkeyLayer=tiled.tileMap.getLayer<ObjectGroup>('key');if(keyLayer!=null){for(finalobjinkeyLayer.objects){if(obj.properties['type']?.value=='key'){finalkeyId=obj.properties['keyId']!.valueasString;finalx=mapScale*obj.x;finaly=mapScale*obj.y;finalw=mapScale*obj.width;finalh=mapScale*obj.height;finalkeyComponent=KeyComponent(keyId:keyId,// Tiled 对象坐标为左上或底对齐;这里使用底对齐放置更符合 tile 外观position:Vector2(x,y),size:Vector2(w,h),);keyComponent.debugMode=true;awaitworld.add(keyComponent);}}}// ---- 处理 Door Layer 中的门 ----finaldoorLayer=tiled.tileMap.getLayer<ObjectGroup>('door');if(doorLayer!=null){for(finalobjindoorLayer.objects){if(obj.properties['type']?.value=='door'){finalkeyId=obj.properties['keyId']!.valueasString;finalx=mapScale*obj.x;finaly=mapScale*obj.y;finalw=mapScale*obj.width;finalh=mapScale*obj.height;finaldoorComponent=DoorComponent(keyId:keyId,isOpen:obj.properties['status']?.value=='open'?true:false,// Tiled 对象坐标为左上或底对齐;这里使用底对齐放置更符合 tile 外观position:Vector2(x,y),size:Vector2(w,h),);doorComponent.debugMode=true;awaitworld.add(doorComponent);}}}6. 完善逻辑
在HeroComponent中,为角色添加钥匙管理与门验证功能:
(1). 钥匙管理
finalSet<String>keys={};- 存储角色已拾取的钥匙
keyId集合
voidaddKey(String keyId){keys.add(keyId);UiNotify.showToast(game,'获得钥匙: $keyId');}- 角色拾取钥匙时调用
- 弹出提示通知玩家获得钥匙
boolhasKey(String keyId)=>keys.contains(keyId);- 判断角色是否持有指定钥匙
- 用于门解锁验证
(2). 门交互逻辑
for(finaldooringame.world.children.query<DoorComponent>()){if(!door.isOpen&&door.collidesWith(heroRect)){door.attemptOpen(this);if(!door.isOpen)returntrue;}}在角色移动碰撞检测_wouldCollideWithBlockers()中:
- 遍历所有门组件
- 检测角色是否与关闭的门发生碰撞
- 调用
door.attemptOpen(this)尝试解锁 - 如果门仍未打开,则返回阻挡信息,阻止角色移动
七. 地刺
1. 简述
地刺是一种周期性造成伤害的地图障碍,通过状态切换形成完整的伤害与死亡判定逻辑。
继承关系:无需继承碰撞基类
功能:对接触的角色造成伤害
状态:突出与收起两种状态,周期性切换
交互逻辑:
- 角色接触突出状态的地刺会受到伤害
- 触发死亡判定后可触发游戏重启或复活逻辑
- 周期性切换状态增强地图挑战性
2. Tiled 前置工作
- 新建
Object Layer图层,命名为thorn。 - 点击使用上方矩形框选工具。
- 在地图中所需位置,正确绘制地刺大小的矩形。
- 添加属性:
- type:thorn
- status:表示地刺状态,一共有两种:on(突出)、off(收入)
3. 构建地刺类
// thorn_component.dartclassThornComponentextendsSpriteComponentwithHasGameReference<MyGame>,CollisionCallbacks{String status;late RectangleHitbox _hitbox;double _elapsed=0;double period=2.0;bool hurted=false;ThornComponent({super.position,requiredsuper.size,requiredthis.status});@overrideFuture<void>onLoad()async{awaitsuper.onLoad();sprite=awaitSprite.load(status=='on'?'thorn_on.png':'thorn_off.png',);_hitbox=RectangleHitbox();add(_hitbox);}@overridevoidonCollisionStart(Set<Vector2>intersectionPoints,PositionComponent other,){super.onCollisionStart(intersectionPoints,other);if(otherisHeroComponent){attemptDamage(other);}}voidattemptDamage(HeroComponent hero){if(status=='on'&&!hurted){hero.loseHp(1);hurted=true;}}void_toggle()async{if(status=='on'){status='off';}else{status='on';hurted=false;}sprite=awaitSprite.load(status=='on'?'thorn_on.png':'thorn_off.png',);}@overridevoidupdate(double dt){super.update(dt);_elapsed+=dt;if(_elapsed>=period){_elapsed=0;_toggle();}}}ThornComponent负责地刺的显示、周期切换和伤害逻辑:
继承
SpriteComponent+ 碰撞回调- 具备位置、尺寸和碰撞检测能力
状态管理
on:突出,触碰造成伤害off:收起,不造成伤害
伤害逻辑
attemptDamage(hero):保证每周期只造成一次伤害
周期切换
_toggle()在固定period时间间隔内切换状态,并更新贴图
4. 加载地刺
// my_game.dart// ---- 处理 thorn 中的荆棘 ----finalthornLayer=tiled.tileMap.getLayer<ObjectGroup>('thorn');if(thornLayer!=null){for(finalobjinthornLayer.objects){if(obj.properties['type']?.value=='thorn'){finalstatus=obj.properties['status']!.valueasString;finalx=mapScale*obj.x;finaly=mapScale*obj.y;finalw=mapScale*obj.width;finalh=mapScale*obj.height;finalthornComponent=ThornComponent(status:status,position:Vector2(x,y),size:Vector2(w,h),);thornComponent.debugMode=true;awaitworld.add(thornComponent);}}}5. 完善逻辑
在HeroComponent中, 实现了角色生命值管理 + 死亡判定的功能:
增加
hp属性,记录角色生命值// 生命值int hp=5;loseHp(amount):被地刺等障碍伤害时调用,扣除生命值并显示提示voidloseHp(int amount){hp=hp-amount;if(hp<=0){UiNotify.showToast(game,'死亡');}else{UiNotify.showToast(game,'HP -$amount 剩余 $hp');}}当
hp <= 0:触发gameOver()- 播放死亡动画
- 2 秒后显示重新开始弹窗
Future<void>gameOver()async{if(_isGameOver)return;_isGameOver=true;// 播放死亡动画if(animations.containsKey(HeroState.dead)){_setState(HeroState.dead);}// 等待 2 秒,让死亡动画播放awaitFuture.delayed(constDuration(seconds:2));// 显示重新开始弹窗finalexists=game.camera.viewport.children.query<RestartOverlay>().isNotEmpty;if(!exists){game.camera.viewport.add(RestartOverlay());}}
八. 总结与展望
总结
本章主要介绍了Flutter&Flame开发2D像素游戏关于人物与地图元素交互的基础实践。
通过上述步骤,我们完成了宝箱、钥匙、门 和 地刺这几类常见元素的交互 。
截至目前为止,游戏主要包括了以下内容:
- 角色与动画:使用精灵图 (
SpriteSheet) 创建角色,支持 idle/run 等动画状态切换。 - 玩家交互:通过摇杆控制角色移动,并根据方向翻转动画。
- 地图加载:通过
Tiled绘制并在Flame中加载的2d像素地图。 - 地图交互:通过组件化模式,新建了多个可供交互的组件如(门、钥匙、宝箱、地刺),为游戏增加了互动性。
- 统一碰撞区检测:将角色与需要产生碰撞的物体统一管理,并实现碰撞时的平滑侧移。
展望
思考 🤔 一个有趣的游戏机制ing …
完成攻击与技能系统,包括动画切换、攻击范围和远程弹道。
实现怪物生成、自动攻击与玩家碰撞逻辑。
支持局域网多玩家联机功能。
🚪 github 源码
💻 个人门户网站
之前尝试的Demo预览