網易首頁 > 網易號 > 正文 申請入駐

用 200 行 MoonBit 構建原生移動端游戲

0
分享至


一、引言

當你想做一個簡單的手機游戲 , 比如 Flappy Bird、2048、貪吃蛇——你的第一反應可能是打開 Unity 或者 Godot。但你有沒有想過:對于一個只需要畫幾個矩形和圓的游戲,你真的需要一個完整的游戲引擎嗎?

引擎內部數十萬行的 C++ 代碼 帶來的不只是便利, 或許還有冗余 。如果我們換一種思路 : 不用引擎,不依賴運行時,直接用一門現代語言編寫游戲邏輯,編譯為原生機器碼,再搭配一個極簡的圖形庫 , 結果會怎樣?

這正是本文要探討的命題 : 使用 MoonBit (一門編譯到原生代碼的現代語言)和 Raylib (一個僅提供最基本圖形能力的 C 庫),從零構建一個可以在 Android 手機上運行的 Flappy Bird。

在這個過程中,你會看到:一個完整的移動游戲,可以只有幾百行代碼、幾 MB 的 APK,以及零引擎依賴。

Web 游戲試玩鏈接:

https://moonbit-community.github.io/tonyfettes-raylib-android-games

Android 游戲 APK 下載鏈接:

https://moonbit-community.github.io/tonyfettes-raylib-android-games/

二、移動游戲開發的技術選型 演進

2015年前后,手機游戲開發主要依賴三大技術路線:

  1. 引擎時代(Unity/Godot):提供一站式開發環境,大幅降低門檻,但存在包體臃腫、底層黑箱、版本更新風險等問題。Godot 雖開源,思路仍類似。

  2. 跨平臺框架(React Native/Flutter):承諾一套代碼多端運行,適合 UI 應用。但在游戲中暴露出額外抽象層、GC 停頓、非為高頻渲染優化等性能瓶頸。

  3. 原生 NDK(C/C++):性能最優,無中間開銷,但開發體驗差,手動內存管理易引發段錯誤等 bug,對業余項目成本過高。

有沒有一種方案,既能獲得原生性能,又能享受現代語言的開發體驗?

這正是 MoonBit 和 Raylib 的組合所提供的。

MoonBit 是一門為性能而設計的現代編程語言。它擁有強類型系統、模式匹配、類型推導,編寫體驗接近 Rust 或 OCaml,但編譯目標是 C——這意味著它可以直接對接 Android NDK 的工具鏈,最終生成和手寫 C 一樣高效的原生代碼。

Raylib 則是游戲圖形庫的極簡主義代表。它不是引擎,不幫你管理場景,不提供編輯器——它只做四件事: 開窗口、畫圖形、讀輸入、放聲音 。用戶面對的核心 API 集中在一個頭文件 raylib.h 中,沒有復雜的依賴關系,沒有狀態機,沒有回調地獄。

把它們組合在一起,你得到的是:


這不是說 MoonBit + Raylib 適合所有場景——如果你在做一款需要物理引擎、粒子系統、骨骼動畫的大型游戲,Unity 仍然是更合理的選擇。但如果你的目標是一款邏輯清晰的 2D 游戲,這套"極簡主義"方案可能是最干凈的路徑。

讓我們來看看它是怎么工作的。

三、理解構建鏈路:從源碼到 APK

在動手寫代碼之前,有一個問題值得想清楚: 你寫的 MoonBit 代碼,是如何變成手機上可以運行的 APK 的?

理解構建鏈路不是為了背誦流程——而是為了在出問題時,知道該往哪里看。

1、 構建鏈路

整個過程可以用一條鏈來描述:


讓我們逐步拆解。

第一步:MoonBit → C 。MoonBit 編譯器將你的 `.mbt` 源文件編譯為標準 C 代碼。MoonBit 的強類型系統在編譯期就排除了大量常見錯誤,生成的 C 代碼是高效的、確定性的。你可以把它理解為:MoonBit 幫你寫了人類不太愿意手寫的那種高質量 C 代碼。

第二步:C → .so 。Android NDK 中的交叉編譯器(通常是 clang)接手,將 C 代碼連同 Raylib 的源碼一起編譯為目標架構的共享庫(`.so` 文件)。這一步和你用 NDK 編譯任何 C/C++ 項目一樣。

第三步:打包成 APK 。Gradle 構建系統將 .so 文件打包進 APK。同時,那個極輕量的 Kotlin 入口點(僅僅是加載庫和隱藏系統 UI)會經過標準的 Android 編譯流程:Kotlin 編譯器將其編譯為 JVM 字節碼,再由 D8 工具轉換為 Android 運行時使用的 classes.dex 。最終的 APK 結構非常簡單:

APK
├── classes.dex ← 極小的 Kotlin 膠水代碼
├── lib/
│ ├── arm64-v8a/
│ │ └── libflappybird.so ← 你的游戲 + Raylib
│ └── armeabi-v7a/
│ └── libflappybird.so
└── AndroidManifest.xml

如果用一個類比:傳統引擎方案就像你寫了一封信,然后把它裝進一個帶有自動翻譯器、排版引擎和朗讀功能的智能信封里寄出去。而 MoonBit + Raylib 的方案就像你直接把信折好,塞進一個普通信封——信的內容沒有變,但信封輕了十倍。

2、 腳手架:一鍵搭建項目

理解了鏈路之后,實際操作反而很簡單。MoonBit 生態提供了一個腳手架工具,可以一鍵生成上述所有構建配置:

moon install tonyfettes/create-moonbit-raylib-android-app
create-moonbit-raylib-android-app MyFlappyBird

生成的項目結構看起來像這樣:

MyFlappyBird/
├── gradlew # Gradle 構建包裝器
├── app/
│ ├── build.gradle.kts # Android 構建配置 (NDK, ABI 目標)
│ ├── src/main/
│ │ ├── AndroidManifest.xml # 應用清單 (NativeActivity)
│ │ ├── java/.../MainActivity.kt # 輕量 Kotlin 入口點
│ │ ├── moonbit/ # 你的游戲代碼存放處
│ │ │ ├── main.mbt # 游戲代碼
│ │ │ ├── moon.mod.json # MoonBit 模塊配置
│ │ │ └── moon.pkg # 包聲明
│ │ └── cpp/
│ │ └── CMakeLists.txt # 構建管道膠水代碼
│ └── ...
└── gradle/

這里關鍵的只有一個目錄: app/src/main/moonbit/ ——你的所有游戲邏輯都寫在這里。其余的 Gradle 配置、CMake 文件、Kotlin 入口點,腳手架已經幫你處理好了。

模塊配置( moon.mod.json )聲明了對 Raylib 綁定的依賴:

{
"name": "username/myflappybird",
"version": "0.1.0",
"deps": {
"tonyfettes/raylib": "0.2.2"
},
"preferred-target": "native"
}

構建和部署也是一行命令:

cd MyFlappyBird
./gradlew assembleDebug --no-daemon

第一次構建需要幾分鐘(它會從源碼編譯 Raylib),之后的增量構建會快得多。你也可以在 Android Studio 中打開項目,點擊 Run 按鈕一鍵編譯部署。


在運行時,輕量的 MainActivity 加載 .so 庫,NDK 膠水代碼啟動原生端,Raylib 初始化 OpenGL ES 上下文,然后調用 main() ——也就是你用 MoonBit 寫的那個 fn main 。

基礎設施講完了。現在讓我們進入真正有趣的部分:游戲邏輯。

四、構建 Flappy Bird

1、游戲循環

從《超級馬里奧》到《原神》,所有實時游戲在最底層都共享同一個結構——初始化(Init)、循環執行更新(Update)與繪制(Draw)、最后清理(Cleanup):


這就是 游戲循環(Game Loop) 。它揭示了實時游戲的本質: 游戲不是一系列事件的響應,而是一幀又一幀的持續模擬 。和 Web 應用的事件驅動模型不同,游戲代碼每秒執行 60 次,無論用戶是否操作——用戶輸入不是觸發器,而是被每一幀"采樣"的信號。

一個良好的游戲架構應該將 狀態更新(update) 和 畫面繪制(draw) 嚴格分離:update 只修改數據,draw 只讀取數據,不存在交叉副作用。讓我們用這個原則來構建 Flappy Bird。

2、定義游戲世界

首先,用結構體描述游戲中的所有對象:

///|
privstructBird {
muty : Float
mutvelocity : Float
}

///|
privstructPipe {
mutx : Float
mutgap_y : Float
mutscored : Bool
}

///|
privstructGame {
sw : Float
sh : Float
bird_x : Float
bird : Bird
bird_radius : Float
gravity : Float
jump_force : Float
pipes : Array[Pipe]
pipe_width : Float
gap_size : Float
pipe_speed : Float
pipe_spacing : Float
mut score : Int
mut game_over : Bool
}

Bird 只保存 每幀變化 的值(位置和速度),而 不變的屬性 (水平位置、半徑)由 Game 持有—— 可變狀態越少,bug 越少 。每個 Pipe 記錄水平位置 x 、空隙中心點 gap_y (開口的垂直中點)和計分標志 `scored`。

另一個值得關注的細節:所有大小都從屏幕尺寸( sw 、 sh )派生——鳥的半徑是 sh / 25.0 ,重力加速度是 sh * 1.5 。這意味著游戲在任何分辨率的設備上都能保持相同的視覺比例和手感,不需要額外的適配邏輯。

3、游戲邏輯

update 函數處理所有狀態變化——物理模擬、水管移動、碰撞檢測和計分:

///|
fn update(game : Game, dt : Float) -> Unit {
if game.game_over {
if@raylib.is_gesture_detected(@raylib.GestureTap) {
reset(game)
}
return
}

if@raylib.is_gesture_detected(@raylib.GestureTap) {
game.bird.velocity = game.jump_force
}

game.bird.velocity += game.gravity * dt
game.bird.y += game.bird.velocity * dt

// 限制在屏幕邊緣內
if game.bird.y < game.bird_radius {
game.bird.y = game.bird_radius
game.bird.velocity = 0.0
}
if game.bird.y > game.sh - game.bird_radius {
game.bird.y = game.sh - game.bird_radius
game.bird.velocity = 0.0
}

for pipe in game.pipes {
pipe.x -= game.pipe_speed * dt
// 水管滾出左邊緣后回收到右側
if pipe.x < -game.pipe_width {
pipe.x += Float::from_int(game.pipes.length()) * game.pipe_spacing
pipe.gap_y = random_gap_y(game)
pipe.scored = false
}

// AABB 碰撞檢測
if game.bird_x + game.bird_radius > pipe.x &&
game.bird_x - game.bird_radius < pipe.x + game.pipe_width {
if game.bird.y - game.bird_radius < pipe.gap_y - game.gap_size / 2.0 ||
game.bird.y + game.bird_radius > pipe.gap_y + game.gap_size / 2.0 {
game.game_over = true
}
}

// 飛過水管時計分
if not(pipe.scored) && pipe.x + game.pipe_width < game.bird_x {
game.score += 1
pipe.scored = true
}
}
}

這段代碼有幾個值得注意的設計決策:

  • 幀率無關性 :所有涉及"隨時間變化"的量都乘以 `dt`(自上一幀經過的秒數)。`game.bird.velocity += game.gravity * dt` 意味著"每秒增加 `gravity` 這么多速度"——無論設備是 60fps 還是 30fps,物理效果一致。

  • 對象回收 :整個游戲只有 4 個水管對象。當一根水管滾出左邊緣,直接把 `x` 坐標加上偏移量"傳送"到最右邊,重新隨機空隙位置。不需要對象池框架——一個 `if` 和一次坐標重置就夠了。

  • AABB 碰撞檢測 :將圓形小鳥近似為外接矩形,檢測它與水管矩形是否重疊——先查水平方向重疊,再查小鳥是否在空隙之外。不是像素級精確,但對休閑游戲完全足夠。

  • 游戲結束檢查 :`update` 頂部的 `game_over` 檢查攔截一切后續邏輯,讓游戲"凍結"在撞擊瞬間,只允許點擊重啟。

draw 函數只負責將當前狀態繪制到屏幕:

///|
fn draw(game : Game) -> Unit {
@raylib.begin_drawing()
@raylib.clear_background(@raylib.skyblue)
for pipe in game.pipes {
let px = pipe.x.to_int()
let pw = game.pipe_width.to_int()
let gap_top = (pipe.gap_y - game.gap_size / 2.0).to_int()
let gap_bottom = (pipe.gap_y + game.gap_size / 2.0).to_int()
@raylib.draw_rectangle(px, 0, pw, gap_top, @raylib.darkgreen)
@raylib.draw_rectangle(
px,
gap_bottom,
pw,
@raylib.get_screen_height() - gap_bottom,
@raylib.darkgreen,
)
}
@raylib.draw_circle_v(
@raylib.Vector2::new(game.bird_x, game.bird.y),
game.bird_radius,
@raylib.yellow,
)
@raylib.end_drawing()
}

先畫水管再畫小鳥,確保小鳥總在最上層。所有繪圖調用必須在 begin_drawing() 和 end_drawing() 之間。

輔助函數和初始化邏輯:

///|
fn random_gap_y(game : Game) -> Float {
Float::from_int(
@raylib.get_random_value(
(game.gap_size / 2.0 + 50.0).to_int(),
(game.sh - game.gap_size / 2.0 - 50.0).to_int(),
),
)
}


///|
fn reset(game : Game) -> Unit {
game.bird.y = game.sh / 2.0
game.bird.velocity = 0.0
game.score = 0
game.game_over = false
for i in 0..
game.pipes[i].x = game.sw + Float::from_int(i) * game.pipe_spacing
game.pipes[i].gap_y = random_gap_y(game)
game.pipes[i].scored = false
}
}

最后, main 將一切連接起來:

///|
fn main {
@raylib.init_window(0, 0, "Flappy Bird")
@raylib.set_target_fps(60)
@raylib.set_exit_key(0)
letsw = Float::from_int(@raylib.get_screen_width())
letsh = Float::from_int(@raylib.get_screen_height())

let game : Game = {
sw,
sh,
bird_x: sw * 0.2,
bird: { y: 0.0, velocity: 0.0 },
bird_radius:sh / 25.0,
gravity: sh * 1.5,
jump_force: sh * -0.65,
pipes: Array::make(4, fn() { { x: 0.0, gap_y: 0.0, scored: false } }),
pipe_width: sw / 8.0,
gap_size: sh / 4.0,
pipe_speed: sw * 0.4,
pipe_spacing:sw / 2.5,
score: 0,
game_over: false,
}
reset(game)

while not(@raylib.window_should_close()) {
let dt = @raylib.get_frame_time()
update(game, dt)
draw(game)
}
@raylib.close_window()
}

init_window(0, 0, ...) 表示使用屏幕全尺寸——在 Android 上就是全屏。游戲循環本身只有三行:獲取 dt、更新、繪制。 is_gesture_detected(GestureTap) 同時響應觸屏和鼠標點擊,可以在桌面開發測試后無縫部署到手機。

構建并部署:

cd MyFlappyBird
./gradlew assembleDebug --no-daemon
adb install -r app/build/outputs/apk/debug/app-debug.apk


重力、水管、碰撞檢測、計分、游戲結束和重啟——全在一個文件、約 200 行代碼里。沒有引擎,沒有運行時,沒有框架。

五 、總結與展望

讓我們回顧一下整個技術脈絡。

我們從一個簡單的問題出發: 一個休閑小游戲,真的需要一個完整的游戲引擎嗎? 然后沿著"從重到輕"的路徑,審視了移動游戲開發的幾種技術選型——從引擎(Unity/Godot)到跨平臺框架,再到原生 NDK,最終到達 MoonBit + Raylib 這個極簡組合。

在構建鏈路層面,我們看到 MoonBit 編譯到 C、C 通過 NDK 編譯到 .so 、 .so 打包進 APK 的清晰路徑——每一步都是確定性的,沒有黑箱。

在游戲架構層面,我們理解了游戲循環這個"所有游戲的共同骨架",以及為什么 update/draw 分離、幀率無關的物理模擬是重要的設計原則。

在具體實現層面,我們用約 200 行代碼構建了一個完整的 Flappy Bird,其中涉及了對象回收(窮人版對象池)和 AABB 碰撞檢測等實用技巧。

這套方案不是萬能的。如果你需要 3D 渲染管線、物理引擎、骨骼動畫、熱更新——使用 Unity 或 Godot 仍然是更務實的選擇。但如果你的目標是一款輕量的 2D 游戲,追求的是 小包體、高性能、完全可控的代碼 ,那么"做減法"的思路值得一試。

這里有一些可以繼續探索的方向:

  • tonyfettes/raylib —— MoonBit 的 Raylib 綁定庫,涵蓋圖形、紋理、音頻、3D 模型、著色器等完整功能

  • selene —— 一個用 MoonBit 編寫的實驗性游戲引擎,支持 WebGPU 和 Raylib 后端,專為網頁和原生游戲設計

  • MoonBit 文檔 —— 語言詳細文檔

另外值得注意一點的是,本文中所有示例代碼均由 AI 生成,甚至包括 Raylib 綁定庫本身。我們利用 AI Agent 的 Subagent 并行化地在桌面、Web 和 Android 平臺上產出了超過 150 款游戲。更多詳情可參見 tonyfettes/raylib 下的 examples/ 目錄。

從 Unity 的"給你一切"到 MoonBit + Raylib 的"只給你需要的",這不僅是技術選型的變化,更是一種開發哲學的轉變—— 最好的代碼不是寫出來的,而是不需要寫的 。

特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相關推薦
熱點推薦
社保基金一季度重倉股揭秘:新進26股 增持34股

社保基金一季度重倉股揭秘:新進26股 增持34股

證券時報
2026-04-23 09:36:11
勞務派遣在央國企殺瘋了!

勞務派遣在央國企殺瘋了!

燈錦年
2026-04-21 17:56:52
7輪0球0助攻 國足希望之星狀態斷崖式下滑 恐遭申花國足雙線棄用

7輪0球0助攻 國足希望之星狀態斷崖式下滑 恐遭申花國足雙線棄用

零度眼看球
2026-04-23 06:48:45
男神真的老了,頭發白了,胡子也白了。大家看出來他是誰了嗎?

男神真的老了,頭發白了,胡子也白了。大家看出來他是誰了嗎?

東方不敗然多多
2026-04-23 10:50:45
林良鋒:暫時奪回榜首,但曼城別高興得太早

林良鋒:暫時奪回榜首,但曼城別高興得太早

體壇周報
2026-04-23 18:10:17
因為GPT-image-2,整個互聯網都變成了巨大的黑暗森林。

因為GPT-image-2,整個互聯網都變成了巨大的黑暗森林。

數字生命卡茲克
2026-04-23 10:13:15
知乎高贊帖!為什么女兒反應這么強烈?

知乎高贊帖!為什么女兒反應這么強烈?

丫頭舫
2026-04-23 10:17:08
李奇微晚年曾說過,戰斗力一流的國家只有三個,其他的都不值一提

李奇微晚年曾說過,戰斗力一流的國家只有三個,其他的都不值一提

老范談史
2026-04-23 17:35:54
利物浦2500萬歐撿漏:國米自己把門開了

利物浦2500萬歐撿漏:國米自己把門開了

綠茵狂熱者
2026-04-23 16:02:04
14歲已是人間尤物,被富豪1億娶回家,54歲風韻猶存

14歲已是人間尤物,被富豪1億娶回家,54歲風韻猶存

眼底星碎
2026-04-23 18:35:04
連斬20多名將領!美防長血洗部隊,陸軍部長掀桌,美國軍政大分裂

連斬20多名將領!美防長血洗部隊,陸軍部長掀桌,美國軍政大分裂

云舟史策
2026-04-23 07:17:39
官宣已明示:下一艘航母就是核動力,舷號19!

官宣已明示:下一艘航母就是核動力,舷號19!

52赫茲實驗室
2026-04-23 15:25:39
重大調整!上海學區有大變化....

重大調整!上海學區有大變化....

新浪財經
2026-04-23 16:12:48
女子踹保安后被扇耳光后續:保安被開除,雙方責任待認定

女子踹保安后被扇耳光后續:保安被開除,雙方責任待認定

凡知
2026-04-23 10:16:47
千億市值的大瓜:當“金絲雀”遇上“正宮團”!

千億市值的大瓜:當“金絲雀”遇上“正宮團”!

挖掘機007
2026-04-21 11:31:00
終于不用再當冤大頭!特斯拉FSD有望第三季度在中國獲批

終于不用再當冤大頭!特斯拉FSD有望第三季度在中國獲批

快科技
2026-04-23 18:56:55
申花-10分開局并列扣分最多,如今7場不敗位列被扣分球隊之首

申花-10分開局并列扣分最多,如今7場不敗位列被扣分球隊之首

懂球帝
2026-04-22 22:21:01
梅西也八卦?埃雷拉:內馬爾派對上,我們想下樓看熱鬧被老婆攔下

梅西也八卦?埃雷拉:內馬爾派對上,我們想下樓看熱鬧被老婆攔下

星耀國際足壇
2026-04-23 14:38:09
英國議會通過法案,2008年后出生者終身不得購煙

英國議會通過法案,2008年后出生者終身不得購煙

澎湃新聞
2026-04-22 14:53:04
消息稱阿里巴巴等巨頭洽談投資DeepSeek 估值超過200億美元

消息稱阿里巴巴等巨頭洽談投資DeepSeek 估值超過200億美元

財聯社
2026-04-22 19:24:36
2026-04-23 19:35:00
開源中國 incentive-icons
開源中國
每天為開發者推送最新技術資訊
7705文章數 34536關注度
往期回顧 全部

游戲要聞

姜文代言猛攻節?《三角洲行動》要的就是不被定義!

頭條要聞

五角大樓"斬"海軍部長 知情人士:他沒認清誰是老大

頭條要聞

五角大樓"斬"海軍部長 知情人士:他沒認清誰是老大

體育要聞

萊斯特城降入英甲,一場虧麻了的豪賭

娛樂要聞

王大陸因涉黑討債被判 女友也一同獲刑

財經要聞

關于AI算力鏈"瓶頸" 這是高盛的最新看法

科技要聞

馬斯克喊出"史上最大產品",但量產難預測

汽車要聞

令人驚艷的奇瑞車 風云A9可不只是樣子貨

態度原創

本地
旅游
游戲
教育
手機

本地新聞

SAGA GIRLS 2026女團選秀

旅游要聞

迪士尼小鎮十歲啦,一站式體驗再升級

R星還是卡普空?十年磨一劍還是年年有得玩?

教育要聞

現在的家長真的不關注成績了,期中試卷只有少部分家長簽字了!

手機要聞

三星研發新型顯示系統,可實現手機平板2D/3D畫面自由切換

無障礙瀏覽 進入關懷版