2026/2/6 9:48:32
网站建设
项目流程
门户网站开发哪家好,厦门公司注册网址,做网站这么便宜可以吗,阿里邮箱Flutter 勇闯2D像素游戏之路#xff08;一#xff09;#xff1a;一个 Hero 的诞生 Flutter 勇闯2D像素游戏之路#xff08;二#xff09;#xff1a;绘制加载游戏地图 Flutter 勇闯2D像素游戏之路#xff08;三#xff09;#xff1a;人物与地图元素的交互
前言
在…Flutter 勇闯2D像素游戏之路一一个 Hero 的诞生Flutter 勇闯2D像素游戏之路二绘制加载游戏地图Flutter 勇闯2D像素游戏之路三人物与地图元素的交互前言在上篇文章中我们初识了2D 地图编辑器 Tiled完成了从地图绘制、资源加载到基础墙体碰撞的搭建为游戏世界奠定了最基本的空间规则。但仅有能走、能挡的地图还远不足以构成一个真正有生命力的游戏场景。真正让地图活起来的是人物与地图元素之间的交互。在游戏中地图并不仅仅是静态的背景它往往承载着多种功能性的元素例如可被触发的宝箱、机关控制通行逻辑的钥匙与门带来风险与挑战的陷阱、伤害型障碍物这些元素都需要与人物产生行为上的联动接触、判断、反馈、结果共同构成完整的交互闭环。因此本篇文章将着重于构建人物与地图元素的交互逐步介绍、实现几类常见的地图交互对象构建一个更加丰富、可扩展的地图交互体系。前情提示1. 地图在进行本章的内容实践之前请先将之前地图中门、宝箱、钥匙、地刺元素先行清除。本章将把他们从单纯装饰件进化到真正的可交互件。接下来所需要用到的门、宝箱、钥匙、地刺等图片素材已在github仓库中添加。2. 墙体首先先感谢兄弟的反馈 接下来我们就用Object Layer和Tile Layer实现上一期的墙体碰撞区看看区别。Object LayerTile Layer从图中可以看到Tile Layer的碰撞区完美和地图中wall图层匹配Object Layer的碰撞区完美和Collisions对象层匹配在本文素材的前提之下如果按Tile Layer那么碰撞区范围就偏大了需要你重新绘制墙体但是你会发现素材中墙体与下面的地板是在一块的不能分离这就不太完美。而Object Layer可以让我们在不破坏改变墙体的前提下更精细调整墙体的碰撞区让它更合理。因此各有利弊看大家的取舍。本文中水面的碰撞区就是Tile Layer方法大家可以看一看。MyHero一. 本章目标二. 人物碰撞矩形优化1. 优化前// hero_component.dartoverrideFuturevoidonLoad()async{...sizeVector2(100,100);positionVector2(1000,1000);_hitboxRectangleHitbox();add(_hitbox);}默认情况下人物的碰撞区域RectangleHitbox会继承父组件的 size也就是说碰撞矩形就是100×100所以碰撞框很大通常会比人物实际可视部分大。这样就导致上图中问题在一个狭窄的走道内由于你的超大碰撞区域而被卡住。2. 优化后// hero_component.dartoverrideFuturevoidonLoad()async{...sizeVector2(100,100);positionVector2(1000,1000);_hitboxRectangleHitbox.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 addHitboxtrue,}){if(addHitbox){hitboxRectangleHitbox();add(hitbox);}}overrideFuturevoidonLoad()async{// 创建 1×1 的透明 sprite占位以满足 SpriteComponent 要求finalrecorderPictureRecorder();finalcanvasCanvas(recorder);finalpaintPaint()..colorconstColor(0x00000000);canvas.drawRect(constRect.fromLTWH(0,0,1,1),paint);finalpicturerecorder.endRecording();finalimageawaitpicture.toImage(1,1);spriteSprite(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.dartfinalListWallComponentwalls---finalListBlockerComponentblockers// 统一添加管理blockers.add();最后在hero_component.dart中修改碰撞性能优化集合finalSetBlockerComponent_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(SetVector2intersectionPoints,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并按瓦片位置和大小添加到游戏世界中用于表示水面。方式一逐瓦片创建组件仅展示finalwaterLayertiled.tileMap.getLayerTileLayer(water);if(waterLayer!nullwaterLayer.data!null){finalwidthwaterLayer.width;finalheightwaterLayer.height;finaldatawaterLayer.data!;for(vary0;yheight;y){for(varx0;xwidth;x){finalindexy*widthx;finalgiddata[index];if(gid!0){finalwaterWaterComponent(position:Vector2(x*tileSize*mapScale,y*tileSize*mapScale,),size:Vector2(tileSize*mapScale,tileSize*mapScale,),);// 添加至统一碰撞列表blockers.add(water);awaitworld.add(water);}}}}这里将地图中每一个8x8的水瓦片都单独变成一个WaterComponent对象。可以看见地图中水面数量巨大产生了大量组件极大降低渲染和碰撞效率。方式二批量合并创建组件推荐// my_game.dartfinalwaterLayertiled.tileMap.getLayerTileLayer(water);if(waterLayer!nullwaterLayer.data!null){awaitaddMergedTileLayerV2(tileData:waterLayer.data!,width:waterLayer.width,height:waterLayer.height,tileSize:tileSize,scale:mapScale,createComponent:(position,size)async{finalwaterWaterComponent(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] - 父组件用于添加生成的组件FuturevoidaddMergedTileLayerV2({required ListinttileData,required int width,required int height,required double tileSize,double scale1.0,required FuturePositionComponentFunction(Vector2 position,Vector2 size)createComponent,required Component parent,})async{// Step1: 先按行生成水平合并块ListListintrects[];// 每行的开始列和宽度ListListinthorizontalRectsList.generate(height,(_)[]);for(vary0;yheight;y){int startX-1;for(varx0;xwidth;x){finalisFilledxwidthtileData[y*widthx]!0;if(isFilledstartX-1){startXx;}if((!isFilled||xwidth)startX!-1){horizontalRects[y].addAll([startX,x-startX]);startX-1;}}}// Step2: 垂直合并相同列和宽度的块ListListintprocessedList.generate(height,(_)List.filled(width,0));for(vary0;yheight;y){for(vari0;ihorizontalRects[y].length;i2){finalxStarthorizontalRects[y][i];finalwhorizontalRects[y][i1];if(processed[y][xStart]1)continue;// 尝试向下合并int rectHeight1;for(varyyy1;yyheight;yy){bool canMergefalse;for(varj0;jhorizontalRects[yy].length;j2){if(horizontalRects[yy][j]xStarthorizontalRects[yy][j1]w){canMergetrue;break;}}if(canMerge){rectHeight;for(varcolxStart;colxStartw;col){processed[yy][col]1;}}else{break;}}// 创建组件finalpositionVector2(xStart*tileSize*scale,y*tileSize*scale);finalsizeVector2(w*tileSize*scale,rectHeight*tileSize*scale);finalblockawaitcreateComponent(position,size);awaitparent.add(block);for(varcolxStart;colxStartw;col){processed[y][col]1;}}}}addMergedTileLayerV2用于扫描 TileLayer 的瓦片数据将相邻的非空瓦片先进行水平方向合并再在此基础上进行垂直方向合并最终把多个连续瓦片合并为更大的矩形组件。五. 宝箱 - 无验证交互式解锁1. 简述宝箱是一种无需条件验证的可交互地图对象通过角色接触即可触发交互。继承关系无需继承碰撞基类功能提供可拾取奖励交互逻辑玩家靠近宝箱即可触发打开动作打开后玩家可选择是否拾取奖励2. Tiled 前置工作新建Object Layer图层命名为treasure。点击使用上方矩形框选工具。在地图中所需位置正确绘制宝箱大小的矩形。添加属性typetrasurestatus表示宝箱状态一共有三种closed关闭、full打开未拿取、enmpty打开已拿取3. 构建宝箱类// treasure_component.dartclassTreasureComponentextendsSpriteComponentwithHasGameReferenceMyGame,CollisionCallbacks{String status;late RectangleHitbox _hitbox;TreasureComponent({requiredthis.status,required Vector2 position,required Vector2 size,}):super(position:position,size:size);overrideFuturevoidonLoad()async{awaitsuper.onLoad();spriteawaitSprite.load(statusclosed?closed_treasure.png:statusfull?full_treasure.png:empty_treasure.png,);_hitboxRectangleHitbox();add(_hitbox);}overridevoidonCollisionStart(SetVector2intersectionPoints,PositionComponent other,){super.onCollisionStart(intersectionPoints,other);if(otherisHeroComponent){attemptOpen(other);}}Futurevoid_open()async{if(statusclosed){statusfull;spriteawaitSprite.load(full_treasure.png);}}Futurevoid_collect(HeroComponent hero)async{if(statusfull){statusempty;spriteawaitSprite.load(empty_treasure.png);UiNotify.showToast(game,获得宝物);}}voidattemptOpen(HeroComponent hero){if(statusclosed){_open();}elseif(statusfull){finalexistsgame.camera.viewport.children.queryDialogComponent().isNotEmpty;if(!exists){finaldialogDialogComponent.confirm(message:是否拿取宝物,onConfirm:()_collect(hero),onCancel:(){},);game.camera.viewport.add(dialog);}}}}TreasureComponent作为宝箱实体类,负责显示与交互的逻辑继承SpriteComponent 碰撞回调具备位置、尺寸和碰撞检测能力状态管理status closed关闭状态触碰后自动打开status full已打开状态弹出对话框确认是否拾取status empty已拿取状态仅显示空宝箱视图更新根据宝箱状态动态加载不同贴图交互逻辑人物触碰到宝箱区域后根据当前状态决定下一步行为4. 加载宝箱// my_game.dartfinaltreasureLayertiled.tileMap.getLayerObjectGroup(treasure);if(treasureLayer!null){for(finalobjintreasureLayer.objects){if(obj.properties[type]?.valuetreasure){finalstatusobj.properties[status]!.valueasString;finalxmapScale*obj.x;finalymapScale*obj.y;finalwmapScale*obj.width;finalhmapScale*obj.height;finaltreasureComponentTreasureComponent(status:status,position:Vector2(x,y),size:Vector2(w,h),);treasureComponent.debugModetrue;awaitworld.add(treasureComponent);}}}六. 钥匙与门 - 验证交互式解锁1. 简述钥匙与门是一组具备条件验证的交互对象通过拾取钥匙 → 解锁对应门实现地图通行控制。继承关系门继承碰撞基类当门未打开时具备阻挡能力与墙体或水面碰撞逻辑一致钥匙可拾取对象角色接触后获得对应keyId门阻挡地图通行需角色持有匹配钥匙才能打开交互逻辑角色与门交互时判断是否持有匹配钥匙条件满足时门打开否则保持阻挡状态2. Tiled 前置工作新建两个Object Layer图层分别命名为key、door。点击使用上方矩形框选工具。在地图中所需位置正确绘制钥匙和门大小的矩形。添加属性钥匙typekeykeyId对应指定的钥匙与门门typedoorkeyId对应指定的钥匙与门status表示门状态一共有两种closed关闭、open打开3. 构建钥匙类// key_component.dartclassKeyComponentextendsSpriteComponentwithHasGameReferenceMyGame,CollisionCallbacks{finalString keyId;KeyComponent({requiredthis.keyId,required Vector2 position,required Vector2 size,}):super(position:position,size:size);overrideFuturevoidonLoad()async{awaitsuper.onLoad();spriteawaitSprite.load(key.png);// 直接加载 assets/key.pngadd(RectangleHitbox());}overridevoidonCollisionStart(SetVector2intersectionPoints,PositionComponent other){super.onCollisionStart(intersectionPoints,other);if(otherisHeroComponent){other.addKey(keyId);removeFromParent();// 拾取后移除当前 tile}}}KeyComponent作为可拾取的钥匙实体用于角色解锁对应门继承SpriteComponent 碰撞回调具备位置、尺寸和碰撞检测能力钥匙标识keyId用于与门的keyId对应实现解锁验证碰撞拾取逻辑当角色碰到钥匙将keyId存入角色钥匙集合从地图中移除钥匙实体避免重复拾取4. 构建门类// door_component.dartclassDoorComponentextendsBlockerComponentwithHasGameReferenceMyGame{finalString keyId;bool isOpen;DoorComponent({requiredthis.keyId,requiredthis.isOpen,super.position,requiredsuper.size,}):super(addHitbox:!isOpen);overrideFuturevoidonLoad()async{awaitsuper.onLoad();spriteawaitSprite.load(isOpen?open_door.png:closed_door.png);}void_unlock()async{if(!isOpen){isOpentrue;spriteawaitSprite.load(open_door.png);hitbox.removeFromParent();}}voidattemptOpen(HeroComponent hero){if(!isOpenhero.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 中的钥匙 ----finalkeyLayertiled.tileMap.getLayerObjectGroup(key);if(keyLayer!null){for(finalobjinkeyLayer.objects){if(obj.properties[type]?.valuekey){finalkeyIdobj.properties[keyId]!.valueasString;finalxmapScale*obj.x;finalymapScale*obj.y;finalwmapScale*obj.width;finalhmapScale*obj.height;finalkeyComponentKeyComponent(keyId:keyId,// Tiled 对象坐标为左上或底对齐这里使用底对齐放置更符合 tile 外观position:Vector2(x,y),size:Vector2(w,h),);keyComponent.debugModetrue;awaitworld.add(keyComponent);}}}// ---- 处理 Door Layer 中的门 ----finaldoorLayertiled.tileMap.getLayerObjectGroup(door);if(doorLayer!null){for(finalobjindoorLayer.objects){if(obj.properties[type]?.valuedoor){finalkeyIdobj.properties[keyId]!.valueasString;finalxmapScale*obj.x;finalymapScale*obj.y;finalwmapScale*obj.width;finalhmapScale*obj.height;finaldoorComponentDoorComponent(keyId:keyId,isOpen:obj.properties[status]?.valueopen?true:false,// Tiled 对象坐标为左上或底对齐这里使用底对齐放置更符合 tile 外观position:Vector2(x,y),size:Vector2(w,h),);doorComponent.debugModetrue;awaitworld.add(doorComponent);}}}6. 完善逻辑在HeroComponent中为角色添加钥匙管理与门验证功能(1). 钥匙管理finalSetStringkeys{};存储角色已拾取的钥匙keyId集合voidaddKey(String keyId){keys.add(keyId);UiNotify.showToast(game,获得钥匙: $keyId);}角色拾取钥匙时调用弹出提示通知玩家获得钥匙boolhasKey(String keyId)keys.contains(keyId);判断角色是否持有指定钥匙用于门解锁验证(2). 门交互逻辑for(finaldooringame.world.children.queryDoorComponent()){if(!door.isOpendoor.collidesWith(heroRect)){door.attemptOpen(this);if(!door.isOpen)returntrue;}}在角色移动碰撞检测_wouldCollideWithBlockers()中遍历所有门组件检测角色是否与关闭的门发生碰撞调用door.attemptOpen(this)尝试解锁如果门仍未打开则返回阻挡信息阻止角色移动七. 地刺1. 简述地刺是一种周期性造成伤害的地图障碍通过状态切换形成完整的伤害与死亡判定逻辑。继承关系无需继承碰撞基类功能对接触的角色造成伤害状态突出与收起两种状态周期性切换交互逻辑角色接触突出状态的地刺会受到伤害触发死亡判定后可触发游戏重启或复活逻辑周期性切换状态增强地图挑战性2. Tiled 前置工作新建Object Layer图层命名为thorn。点击使用上方矩形框选工具。在地图中所需位置正确绘制地刺大小的矩形。添加属性typethornstatus表示地刺状态一共有两种on突出、off收入3. 构建地刺类// thorn_component.dartclassThornComponentextendsSpriteComponentwithHasGameReferenceMyGame,CollisionCallbacks{String status;late RectangleHitbox _hitbox;double _elapsed0;double period2.0;bool hurtedfalse;ThornComponent({super.position,requiredsuper.size,requiredthis.status});overrideFuturevoidonLoad()async{awaitsuper.onLoad();spriteawaitSprite.load(statuson?thorn_on.png:thorn_off.png,);_hitboxRectangleHitbox();add(_hitbox);}overridevoidonCollisionStart(SetVector2intersectionPoints,PositionComponent other,){super.onCollisionStart(intersectionPoints,other);if(otherisHeroComponent){attemptDamage(other);}}voidattemptDamage(HeroComponent hero){if(statuson!hurted){hero.loseHp(1);hurtedtrue;}}void_toggle()async{if(statuson){statusoff;}else{statuson;hurtedfalse;}spriteawaitSprite.load(statuson?thorn_on.png:thorn_off.png,);}overridevoidupdate(double dt){super.update(dt);_elapseddt;if(_elapsedperiod){_elapsed0;_toggle();}}}ThornComponent负责地刺的显示、周期切换和伤害逻辑继承SpriteComponent 碰撞回调具备位置、尺寸和碰撞检测能力状态管理on突出触碰造成伤害off收起不造成伤害伤害逻辑attemptDamage(hero)保证每周期只造成一次伤害周期切换_toggle()在固定period时间间隔内切换状态并更新贴图4. 加载地刺// my_game.dart// ---- 处理 thorn 中的荆棘 ----finalthornLayertiled.tileMap.getLayerObjectGroup(thorn);if(thornLayer!null){for(finalobjinthornLayer.objects){if(obj.properties[type]?.valuethorn){finalstatusobj.properties[status]!.valueasString;finalxmapScale*obj.x;finalymapScale*obj.y;finalwmapScale*obj.width;finalhmapScale*obj.height;finalthornComponentThornComponent(status:status,position:Vector2(x,y),size:Vector2(w,h),);thornComponent.debugModetrue;awaitworld.add(thornComponent);}}}5. 完善逻辑在HeroComponent中, 实现了角色生命值管理 死亡判定的功能增加hp属性记录角色生命值// 生命值int hp5;loseHp(amount)被地刺等障碍伤害时调用扣除生命值并显示提示voidloseHp(int amount){hphp-amount;if(hp0){UiNotify.showToast(game,死亡);}else{UiNotify.showToast(game,HP -$amount 剩余 $hp);}}当hp 0触发gameOver()播放死亡动画2 秒后显示重新开始弹窗FuturevoidgameOver()async{if(_isGameOver)return;_isGameOvertrue;// 播放死亡动画if(animations.containsKey(HeroState.dead)){_setState(HeroState.dead);}// 等待 2 秒让死亡动画播放awaitFuture.delayed(constDuration(seconds:2));// 显示重新开始弹窗finalexistsgame.camera.viewport.children.queryRestartOverlay().isNotEmpty;if(!exists){game.camera.viewport.add(RestartOverlay());}}八. 总结与展望总结本章主要介绍了FlutterFlame开发2D像素游戏关于人物与地图元素交互的基础实践。通过上述步骤我们完成了宝箱、钥匙、门 和 地刺这几类常见元素的交互 。截至目前为止游戏主要包括了以下内容角色与动画使用精灵图 (SpriteSheet) 创建角色支持 idle/run 等动画状态切换。玩家交互通过摇杆控制角色移动并根据方向翻转动画。地图加载通过Tiled绘制并在Flame中加载的2d像素地图。地图交互通过组件化模式新建了多个可供交互的组件如门、钥匙、宝箱、地刺为游戏增加了互动性。统一碰撞区检测将角色与需要产生碰撞的物体统一管理并实现碰撞时的平滑侧移。展望思考 一个有趣的游戏机制ing …完成攻击与技能系统包括动画切换、攻击范围和远程弹道。实现怪物生成、自动攻击与玩家碰撞逻辑。支持局域网多玩家联机功能。 github 源码 个人门户网站之前尝试的Demo预览