實戰(zhàn):從合成大西瓜到真機運行)
這兩年鴻蒙設備的保有量上來了身邊不少朋友問“能不能用Flutter跑鴻蒙”正好我用Flutter做了一個合成大西瓜游戲從環(huán)境搭建到上真機跑通一路踩了不少坑也積累了一些一手經(jīng)驗。這篇博文就圍繞“鴻蒙 Flutter 跨平臺開發(fā)”這條主線完整拆解一下合成大西瓜這個項目從零到一的實現(xiàn)過程包括架構設計、物理引擎選型、核心合成邏輯、鴻蒙端適配、HAP打包和真機調(diào)試這些環(huán)節(jié)希望能給準備在鴻蒙上做Flutter開發(fā)的同學一點參考。這不是一篇教程式的“Hello World”而是把整個項目拆開揉碎講清楚每一步為什么要這么選、這么寫以及哪些地方在鴻蒙上跟Android/iOS不一樣。內(nèi)容對剛接觸Flutter的初級開發(fā)者也很友好涉及物理引擎的部分我會用大白話解釋不會上來就是Box2D公式糊臉。如果你已經(jīng)在用Flutter做業(yè)務只是想知道鴻蒙適配怎么做可以直接跳到第2章和第5章。1. 項目背景與整體設計思路1.1 為什么是“鴻蒙 Flutter”這個組合合成大西瓜這種游戲單看玩法并不復雜掉落水果、相同水果碰撞合并、不斷往上疊加超過警戒線就結束。但它的核心體驗依賴兩件事物理碰撞的真實感和水果合并的即時反饋。如果我用鴻蒙原生去實現(xiàn)需要自己搞一套物理引擎或者對接第三方庫成本并不低如果我用Flutter則可以復用大量現(xiàn)成的跨平臺游戲開發(fā)方案。Flutter本身是一個UI框架嚴格意義上不是游戲引擎但它的渲染性能足夠應付2D休閑游戲。加上Flutter生態(tài)里已經(jīng)有Flame游戲引擎和Forge2D物理引擎合成大西瓜這類“水果掉落 碰撞合并”的場景在Flutter里實現(xiàn)比我預想中要順利。更關鍵的是Flutter官方和OpenHarmony社區(qū)這兩年一直在推進Flutter對鴻蒙的適配有一個相對成熟的鴻蒙分支可以直接用。這個組合的核心價值就是一套Dart代碼同時覆蓋Android、iOS、鴻蒙甚至后續(xù)可以擴展到桌面端對個人開發(fā)者和中小團隊來說研發(fā)成本確實能省下來。注意目前Flutter適配鴻蒙走的不是Flutter官方主干而是OpenHarmony社區(qū)的flutter_flutter分支。這個分支有對應的版本號比如3.7.12、3.7.24等不是直接下載flutter官網(wǎng)的穩(wěn)定版就能跑鴻蒙。1.2 合成大西瓜的核心玩法抽象在動手寫代碼之前我把游戲玩法抽象成了幾個狀態(tài)和行為水果類型不同等級對應不同水果從葡萄、橘子開始一直到大西瓜。掉落行為玩家選擇一個釋放點水果在重力作用下落到容器內(nèi)與已有水果發(fā)生碰撞。合并規(guī)則兩個同類水果碰撞后消除并生成更高一級的新水果新水果繼承碰撞位置和速度。結束判定容器內(nèi)已有水果堆積超過警戒線且玩家沒有空間繼續(xù)釋放新水果時游戲結束。抽象完以后整個項目的技術難點就清晰了物理世界怎么建、碰撞回調(diào)怎么處理、水果尺寸和物理半徑怎么匹配、合并后的新物體如何平滑插入場景。這些都是我在實現(xiàn)過程中反復調(diào)整的內(nèi)容后面會逐個展開。1.3 技術選型上的取舍在技術選型時我考慮過三條路Clutter鴻蒙原生、Unity導出鴻蒙包、Flutter Flame。Clutter原生開發(fā)的性能上限最高但開發(fā)周期長我要用不同平臺維護兩套代碼Unity做2D游戲很成熟但對于一個輕量休閑游戲來說工程重、包體大最后選了Flutter Flame理由有三點團隊已經(jīng)有Dart/Flutter經(jīng)驗上手成本低。Flame提供了游戲循環(huán)、組件管理、碰撞檢測等基礎設施不需要從零造輪子。鴻蒙特化社區(qū)維護了flutter_flutter分支已經(jīng)有真機運行的案例不是“畫餅”狀態(tài)。這里也提醒一下如果你對Flutter還不熟建議先把Flutter基礎跑通再來看這個項目。游戲開發(fā)和普通業(yè)務開發(fā)不一樣狀態(tài)管理、渲染樹、生命周期這些概念會以更復雜的方式交織在一起沒有基礎直接上會有點痛苦。2. 開發(fā)環(huán)境搭建與鴻蒙適配要點2.1 Flutter SDK 與鴻蒙側SDK的配置細節(jié)鴻蒙Flutter開發(fā)環(huán)境搭建可能是整個項目里最容易勸退新人的地方。首先你需要的不是Flutter官網(wǎng)的SDK而是OpenHarmony社區(qū)的flutter_flutter分支。我使用的是3.7.24版本這個版本在社區(qū)里經(jīng)過較多驗證配套的DevEco Studio版本也比較好確認。具體步驟如下克隆flutter_flutter倉庫git clone https://gitee.com/openharmony-sig/flutter_flutter.git。切換分支到flutter_3.7.24之類的release分支。把這個flutter命令加到PATH環(huán)境變量里注意要覆蓋掉之前安裝的Flutter官方SDK。安裝鴻蒙側的DevEco Studio建議5.0及以上版本并在DevEco里配置好鴻蒙SDK路徑。運行flutter doctor確認能看到HarmonyOS相關的檢查項。我用的是macOS環(huán)境Windows下流程類似但有些命令行工具路徑會有差異。環(huán)境變量配置好以后別忘了source一下或者重開終端否則容易遇到“flutter命令還是舊版本”的詭異問題。提示flutter_flutter分支更新速度比官方慢所以版本號不要追新。穩(wěn)定大于一切特別是做游戲這種對運行穩(wěn)定性要求高的項目。2.2 創(chuàng)建鴻蒙Flutter工程的正確姿勢環(huán)境配好以后創(chuàng)建工程不要直接用flutter create .因為默認模板不一定帶鴻蒙的工程結構。正確做法是先創(chuàng)建一個普通Flutter工程flutter create watermelon_game。然后在工程根目錄下手動添加鴻蒙的ohos目錄或者直接從社區(qū)模板里拷貝一份鴻蒙工程骨架。在pubspec.yaml里配置好依賴比如flame和forge2d。用DevEco Studio打開ohos目錄配置好簽名信息。先跑一個空工程做驗證確認鴻蒙真機上能出Flutter的默認計數(shù)頁面再開始寫游戲邏輯。這個“先跑通空工程”的步驟特別重要因為鴻蒙Flutter環(huán)境的問題往往不是代碼問題而是工程配置問題。如果空工程能跑后面出問題就可以聚焦在游戲邏輯上如果空工程都跑不起來先解決環(huán)境問題再繼續(xù)否則會浪費很多時間在錯誤方向上看日志。2.3 鴻蒙端運行Flutter的常見坑位我第一次往鴻蒙真機部署Flutter應用時遇到過幾個記憶深刻的問題熱重載失效鴻蒙端的熱重載支持比Android端弱有時候改了代碼點熱重載沒反應需要手動重新運行。這跟熱詞的“flutter熱重載后瀏覽器沒更新”是類似場景建議在鴻蒙上開發(fā)時把熱重載當作“輔助手段”不要依賴核心邏輯改動直接全量重啟。依賴下載失敗Flutter默認從Google的存儲下載依賴在國內(nèi)網(wǎng)絡環(huán)境下經(jīng)常失敗。解決辦法是在環(huán)境變量里配置鏡像源比如FLUTTER_STORAGE_BASE_URL指向國內(nèi)鏡像這個在鴻蒙場景下同樣適用。CMake報錯如果鴻蒙工程里涉及native插件CMake配置出問題會報一堆錯。這個不常遇到但如果你的Flutter插件里有native代碼就要提前確認鴻蒙側是否支持該插件的實現(xiàn)。還有一個細節(jié)鴻蒙上flutter showLicensePage之類的內(nèi)置頁面主題顏色可能跟Android/iOS不一樣這屬于平臺差異不影響游戲主流程但如果你做設置頁這類入口要注意適配。3. 游戲核心邏輯從物理世界到合成規(guī)則3.1 物理引擎選型Flame還是Forge2D合成大西瓜的物理碰撞是整個游戲的地基我直接用了Flame游戲引擎配合Forge2D物理引擎。Forge2D是Box2D的Dart移植版而Flame里的flame_forge2d包把兩者無縫集成在一起讓我能以組件的方式管理物理體和渲染體。為什么不用純Flame的自定義碰撞因為休閑游戲的物理模擬對實時性、穩(wěn)定性的要求很高自己寫碰撞檢測很容易出現(xiàn)“水果穿模”“彈跳異?!边@些問題。Forge2D是成熟的物理引擎重力、碰撞、摩擦、彈性系數(shù)都有成熟算法我只需要調(diào)參數(shù)就行。用生活類比來解釋Flame是游戲的“身體”負責游戲循環(huán)、動畫、音頻管理Forge2D是“物理規(guī)則”負責重力、碰撞、彈跳這些物理行為。兩者配合我只需要定義水果的物理屬性半徑、密度、摩擦系數(shù)、彈性剩下的交給引擎計算。3.2 碰撞檢測與合成判定水果合成判定是游戲的核心邏輯。我的實現(xiàn)思路是所有水果都是Forge2D里的BodyComponent在初始化時給每個水果綁定一個ContactCallback當兩個水果發(fā)生碰撞時引擎會回調(diào)beginContact方法。以下是關鍵代碼結構的簡化版本class Fruit extends BodyComponentFruit { final int level; // 水果等級 override Body createBody() { final shape CircleShape()..radius fruitRadius[level]!; final fixtureDef FixtureDef(shape) ..density 1.0 ..friction 0.5 ..restitution 0.2; // ... 創(chuàng)建body定義添加用戶數(shù)據(jù) } } class FruitCollision extends ContactCallbackFruit, Fruit { override void beginContact(Fruit a, Fruit b) { if (a.level b.level) { // 觸發(fā)合成移除a和b生成level1的水果 } } }這里有一個關鍵細節(jié)Fruit和Fruit的ContactCallback會攔截所有水果之間的碰撞因此我在回調(diào)里判斷a.level b.level完全相同才觸發(fā)合成。這個判斷必須放在beginContact里不能在endContact里做否則錯過碰撞瞬間的時機兩個水果會交錯而過。合成時還有一個“位置繼承”的細節(jié)新生成的高一級水果位置應該在兩個舊水果碰撞點的中間而不是固定出現(xiàn)在場景某處。我是這么處理的取a.body.position和b.body.position的平均值然后在該位置創(chuàng)建新水果。這樣視覺上更自然因為玩家看到的是“兩個葡萄碰在一起變成了一個橘子”而不是“橘子憑空從天上掉下來”。3.3 得分判定、Go判定與數(shù)據(jù)管理除了合成邏輯游戲還有得分和結束判定。得分相對簡單每次合成時根據(jù)水果等級加分等級越高加分越多。結束判定稍微復雜我設置了一個警戒線Y坐標當任何水果的body.position.y小于警戒線且持續(xù)一段時間后游戲結束。這里有一個容易踩的坑直接以水果坐標判斷結束會導致“剛碰到警戒線就結束”的誤判因為水果可能在彈跳過程中臨時碰到警戒線又彈回去。我加了一個緩沖機制只有水果在警戒線以上持續(xù)2秒以上或者有多個水果同時越過警戒線才觸發(fā)結束。數(shù)據(jù)管理方面我用了一個簡單的GameState類存儲當前分數(shù)、最高分、當前水果等級隊列用ChangeNotifier做狀態(tài)通知UI。這樣做的好處是游戲邏輯和界面解耦后續(xù)接排行榜或者分享功能時不需要重構核心邏輯。class GameState extends ChangeNotifier { int score 0; int highScore 0; bool isGameOver false; void addScore(int points) { score points; if (score highScore) { highScore score; } notifyListeners(); } }4. 視覺與交互讓游戲“有手感”4.1 水果尺寸、貼圖與縮放適配合成大西瓜里不同等級的水果尺寸差異很大。最小的葡萄和最大的西瓜半徑差距可能有六七倍。如果直接按像素尺寸硬編碼在不同屏幕寬度的鴻蒙設備上會出現(xiàn)比例失衡。我的做法是定義一套相對半徑表以游戲容器寬度為基準做等比縮放。貼圖資源用透明底PNG保證視覺大小和物理半徑盡量匹配。給不同等級的水果設置不同的縮放動畫合成時新水果會有一個“長大”的過渡效果增強視覺反饋。這里要注意一個細節(jié)物理半徑和貼圖半徑不完全是一回事。物理半徑?jīng)Q定了碰撞箱大小貼圖半徑?jīng)Q定了玩家看到的視覺大小。如果物理半徑比貼圖小很多玩家會看到“水果還沒碰到就合成了”如果物理半徑比貼圖大很多玩家會看到“水果重疊了好大一塊還沒觸發(fā)合成”。這兩者需要反復測試調(diào)整我在項目里把物理半徑設為貼圖視覺半徑的0.85倍左右碰撞手感比較自然。4.2 觸摸拖拽、瞄準線與拋落手感合成大西瓜的操作方式是“選擇釋放點水果落下”。我在實現(xiàn)的時候加了一條瞄準線提示玩家當前手機觸點對應的釋放位置。瞄準線的實現(xiàn)方式很簡單用一個PositionComponent畫一條虛線從屏幕頂部中央延伸到釋放點顏色半透明松手后消失。這里有個提升手感的小技巧水果釋放后在初始下落階段不要給它附加額外水平速度這樣玩家看到的路徑就是“直直落下”跟瞄準線一致。如果一開始就疊加隨機水平速度玩家會感覺“明明瞄準了落點卻有偏差”這是操作手感的大忌。還有一點釋放點在容器范圍內(nèi)才有效如果玩家在容器外松手要彈一個提示或者不響應。不然水果會掉落在物理世界邊界之外直接導致物理模擬異常。4.3 音效與動效在鴻蒙上的處理音效主要用flame_audio插件來播放支持常見的wav/ogg格式。合成、掉落、結束分別有不同的音效增加游戲反饋感。鴻蒙適配時需要注意flame_audio底層依賴的是Flutter的音頻播放能力鴻蒙分支對音頻的支持我認為已經(jīng)比較完善目前測試下來沒有遇到格式兼容問題但我建議用wav格式兼容性比mp3更好。動效方面合成時的“水果變大”動畫我用了一個簡單的TweenAnimationBuilder或者Flame里的ScaleEffect。用Flame的Effect系統(tǒng)更輕量可以直接在組件上掛縮放效果不用額外管理狀態(tài)。掉落時的“輕微陰影”我用了一個與水果形狀一致的半透明圓形組件跟隨水果位置移動成本低且效果不錯。5. 跨平臺打包與鴻蒙實機運行5.1 打包鴻蒙應用HAP的正確流程Flutter工程打包鴻蒙應用不是直接flutter build apk而是要生成鴻蒙側的HAP包。流程大致是在工程ohos目錄下用DevEco Studio打開工程。配置簽名在File Project Structure Signing Configs里勾選自動簽名登錄華為賬號自動生成簽名文件。配置module.json5設置應用包名、版本號、圖標等。用DevEco的Build菜單生成HAP包或者用命令行hvigorw assembleHap。這里有一個容易忽略的問題鴻蒙工程里的oh-package.json5需要顯式聲明對Flutter引擎的依賴如果沒有這個依賴聲明HAP包安裝到真機上會報“找不到flutter引擎”的錯誤。我在第一次打包時就在這個坑里爬了將近兩個小時。5.2 真機調(diào)試與性能參數(shù)真機調(diào)試時我用的方式是先用USB連接鴻蒙設備和電腦在DevEco里點擊運行把HAP裝到設備上。這樣能直接看Flutter的日志輸出方便定位問題。游戲性能方面合成大西瓜場景通常同時存在的物體數(shù)量大約是10到30個Forge2D物理模擬的壓力并不大。在鴻蒙真機上只要注意兩點基本能流暢運行避免頻繁創(chuàng)建和銷毀對象我用了對象池來管理水果組件同一等級的水果銷毀后不立即釋放而是復用給下次生成。這樣能明顯減少卡頓。限制物理迭代次數(shù)Forge2D支持設置velocityIterations和positionIterations數(shù)值越高越精確但越耗性能。我調(diào)成8和3實測在真機上表現(xiàn)良好肉眼看不出物理異常。5.3 常見問題速查表下面這個表格是開發(fā)過程中反復遇到的幾個典型問題我整理成速查表方便大家對照排查。問題現(xiàn)象可能原因解決方案鴻蒙真機裝不上HAP簽名未配置或包名沖突在DevEco里配置自動簽名檢查包名是否與已安裝應用沖突Flutter頁面白屏無報錯flutter引擎so未打包進HAP檢查oh-package.json5是否聲明Flutter依賴熱重載不生效鴻蒙分支對熱重載支持不完整手動重新運行不依賴熱重載水果穿透或合成失效物理半徑與貼圖半徑不匹配調(diào)整CircleShape.radius確保物理體覆蓋視覺體音頻播放無聲音音源格式不支持換成wav格式檢查資源路徑應用啟動慢首次加載引擎耗時較長盡量使用release包測試啟動速度debug包本身會更慢6. 鴻蒙Flutter開發(fā)的邊界與心得在鴻蒙上用Flutter做游戲聽起來是件有點“折騰”的事但實際體驗下來可行性和穩(wěn)定性比我想象中要好。這個合成大西瓜項目從開始搭建環(huán)境到最終跑通真機我大概用了兩個周末其中一半時間花在環(huán)境配置和排錯上真正寫游戲邏輯反而比較順利。我個人在實際操作中的體會是Flutter跨平臺開發(fā)的核心價值不在“某一天一套代碼跑所有平臺”而在于“業(yè)務邏輯和UI層的高度復用”。在鴻蒙生態(tài)還不算完全成熟的前期用Flutter把游戲邏輯先跑起來后續(xù)再做鴻蒙原生適配成本也可以控制得比較低。最后分享一個小技巧在做鴻蒙Flutter開發(fā)時多利用DevEco Studio自帶的日志過濾器只保留flutter和DartVM相關的tag日志噪音會小很多。開發(fā)節(jié)奏和心態(tài)也很重要——鴻蒙的Flutter支持還在快速迭代中保持跟社區(qū)更新跑一版就“鎖”一版依賴不要頻繁升級SDK版本。希望這個項目經(jīng)驗能幫到正在走同樣路線的同學也期待看到更多人把好玩的Flutter應用帶到鴻蒙設備上。