一、引言
當(dāng)你想做一個(gè)簡(jiǎn)單的手機(jī)游戲,比如 Flappy Bird、2048、貪吃蛇——你的第一反應(yīng)可能是打開(kāi) Unity 或者 Godot。但你有沒(méi)有想過(guò):對(duì)于一個(gè)只需要畫(huà)幾個(gè)矩形和圓的游戲,你真的需要一個(gè)完整的游戲引擎嗎?
引擎內(nèi)部數(shù)十萬(wàn)行的 C++ 代碼帶來(lái)的不只是便利,或許還有冗余。如果我們換一種思路:不用引擎,不依賴運(yùn)行時(shí),直接用一門現(xiàn)代語(yǔ)言編寫(xiě)游戲邏輯,編譯為原生機(jī)器碼,再搭配一個(gè)極簡(jiǎn)的圖形庫(kù),結(jié)果會(huì)怎樣?
這正是本文要探討的命題:使用 MoonBit(一門編譯到原生代碼的現(xiàn)代語(yǔ)言)和 Raylib(一個(gè)僅提供最基本圖形能力的 C 庫(kù)),從零構(gòu)建一個(gè)可以在 Android 手機(jī)上運(yùn)行的 Flappy Bird。
在這個(gè)過(guò)程中,你會(huì)看到:一個(gè)完整的移動(dòng)游戲,可以只有幾百行代碼、幾 MB 的 APK,以及零引擎依賴。
二、移動(dòng)游戲開(kāi)發(fā)的技術(shù)選型演進(jìn)
2015年前后,手機(jī)游戲開(kāi)發(fā)主要依賴三大技術(shù)路線:
引擎時(shí)代(Unity/Godot):提供一站式開(kāi)發(fā)環(huán)境,大幅降低門檻,但存在包體臃腫、底層黑箱、版本更新風(fēng)險(xiǎn)等問(wèn)題。Godot 雖開(kāi)源,思路仍類似。
跨平臺(tái)框架(React Native/Flutter):承諾一套代碼多端運(yùn)行,適合 UI 應(yīng)用。但在游戲中暴露出額外抽象層、GC 停頓、非為高頻渲染優(yōu)化等性能瓶頸。
原生 NDK(C/C++):性能最優(yōu),無(wú)中間開(kāi)銷,但開(kāi)發(fā)體驗(yàn)差,手動(dòng)內(nèi)存管理易引發(fā)段錯(cuò)誤等 bug,對(duì)業(yè)余項(xiàng)目成本過(guò)高。
有沒(méi)有一種方案,既能獲得原生性能,又能享受現(xiàn)代語(yǔ)言的開(kāi)發(fā)體驗(yàn)?
這正是 MoonBit 和 Raylib 的組合所提供的。
MoonBit 是一門為性能而設(shè)計(jì)的現(xiàn)代編程語(yǔ)言。它擁有強(qiáng)類型系統(tǒng)、模式匹配、類型推導(dǎo),編寫(xiě)體驗(yàn)接近 Rust 或 OCaml,但編譯目標(biāo)是 C——這意味著它可以直接對(duì)接 Android NDK 的工具鏈,最終生成和手寫(xiě) C 一樣高效的原生代碼。
Raylib 則是游戲圖形庫(kù)的極簡(jiǎn)主義代表。它不是引擎,不幫你管理場(chǎng)景,不提供編輯器——它只做四件事:開(kāi)窗口、畫(huà)圖形、讀輸入、放聲音。用戶面對(duì)的核心 API 集中在一個(gè)頭文件 raylib.h 中,沒(méi)有復(fù)雜的依賴關(guān)系,沒(méi)有狀態(tài)機(jī),沒(méi)有回調(diào)地獄。
把它們組合在一起,你得到的是:

這不是說(shuō) MoonBit + Raylib 適合所有場(chǎng)景——如果你在做一款需要物理引擎、粒子系統(tǒng)、骨骼動(dòng)畫(huà)的大型游戲,Unity 仍然是更合理的選擇。但如果你的目標(biāo)是一款邏輯清晰的 2D 游戲,這套"極簡(jiǎn)主義"方案可能是最干凈的路徑。
讓我們來(lái)看看它是怎么工作的。
三、理解構(gòu)建鏈路:從源碼到 APK
在動(dòng)手寫(xiě)代碼之前,有一個(gè)問(wèn)題值得想清楚:你寫(xiě)的 MoonBit 代碼,是如何變成手機(jī)上可以運(yùn)行的 APK 的?
理解構(gòu)建鏈路不是為了背誦流程——而是為了在出問(wèn)題時(shí),知道該往哪里看。
1、構(gòu)建鏈路
整個(gè)過(guò)程可以用一條鏈來(lái)描述:

讓我們逐步拆解。
第一步:MoonBit → C。MoonBit 編譯器將你的 `.mbt` 源文件編譯為標(biāo)準(zhǔn) C 代碼。MoonBit 的強(qiáng)類型系統(tǒng)在編譯期就排除了大量常見(jiàn)錯(cuò)誤,生成的 C 代碼是高效的、確定性的。你可以把它理解為:MoonBit 幫你寫(xiě)了人類不太愿意手寫(xiě)的那種高質(zhì)量 C 代碼。
第二步:C → .so。Android NDK 中的交叉編譯器(通常是 clang)接手,將 C 代碼連同 Raylib 的源碼一起編譯為目標(biāo)架構(gòu)的共享庫(kù)(`.so` 文件)。這一步和你用 NDK 編譯任何 C/C++ 項(xiàng)目一樣。
第三步:打包成 APK。Gradle 構(gòu)建系統(tǒng)將 .so 文件打包進(jìn) APK。同時(shí),那個(gè)極輕量的 Kotlin 入口點(diǎn)(僅僅是加載庫(kù)和隱藏系統(tǒng) UI)會(huì)經(jīng)過(guò)標(biāo)準(zhǔn)的 Android 編譯流程:Kotlin 編譯器將其編譯為 JVM 字節(jié)碼,再由 D8 工具轉(zhuǎn)換為 Android 運(yùn)行時(shí)使用的 classes.dex。最終的 APK 結(jié)構(gòu)非常簡(jiǎn)單:
APK ├── classes.dex ← 極小的 Kotlin 膠水代碼 ├── lib/ │ ├── arm64-v8a/ │ │ └── libflappybird.so ← 你的游戲 + Raylib │ └── armeabi-v7a/ │ └── libflappybird.so └── AndroidManifest.xml
如果用一個(gè)類比:傳統(tǒng)引擎方案就像你寫(xiě)了一封信,然后把它裝進(jìn)一個(gè)帶有自動(dòng)翻譯器、排版引擎和朗讀功能的智能信封里寄出去。而 MoonBit + Raylib 的方案就像你直接把信折好,塞進(jìn)一個(gè)普通信封——信的內(nèi)容沒(méi)有變,但信封輕了十倍。
2、腳手架:一鍵搭建項(xiàng)目
理解了鏈路之后,實(shí)際操作反而很簡(jiǎn)單。MoonBit 生態(tài)提供了一個(gè)腳手架工具,可以一鍵生成上述所有構(gòu)建配置:
mooninstalltonyfettes/create-moonbit-raylib-android-app create-moonbit-raylib-android-app MyFlappyBird
生成的項(xiàng)目結(jié)構(gòu)看起來(lái)像這樣:
MyFlappyBird/ ├── gradlew # Gradle 構(gòu)建包裝器 ├── app/ │ ├── build.gradle.kts # Android 構(gòu)建配置 (NDK, ABI 目標(biāo)) │ ├── src/main/ │ │ ├── AndroidManifest.xml # 應(yīng)用清單 (NativeActivity) │ │ ├── java/.../MainActivity.kt# 輕量 Kotlin 入口點(diǎn) │ │ ├── moonbit/ # 你的游戲代碼存放處 │ │ │ ├── main.mbt # 游戲代碼 │ │ │ ├── moon.mod.json # MoonBit 模塊配置 │ │ │ └── moon.pkg # 包聲明 │ │ └── cpp/ │ │ └── CMakeLists.txt # 構(gòu)建管道膠水代碼 │ └── ... └── gradle/這里關(guān)鍵的只有一個(gè)目錄:app/src/main/moonbit/——你的所有游戲邏輯都寫(xiě)在這里。其余的 Gradle 配置、CMake 文件、Kotlin 入口點(diǎn),腳手架已經(jīng)幫你處理好了。 模塊配置(moon.mod.json)聲明了對(duì) Raylib 綁定的依賴:
{
"name":"username/myflappybird",
"version":"0.1.0",
"deps": {
"tonyfettes/raylib":"0.2.2"
},
"preferred-target":"native"
}
構(gòu)建和部署也是一行命令:
cdMyFlappyBird ./gradlew assembleDebug --no-daemon
第一次構(gòu)建需要幾分鐘(它會(huì)從源碼編譯 Raylib),之后的增量構(gòu)建會(huì)快得多。你也可以在 Android Studio 中打開(kāi)項(xiàng)目,點(diǎn)擊 Run 按鈕一鍵編譯部署。

在運(yùn)行時(shí),輕量的 MainActivity 加載 .so 庫(kù),NDK 膠水代碼啟動(dòng)原生端,Raylib 初始化 OpenGL ES 上下文,然后調(diào)用 main()——也就是你用 MoonBit 寫(xiě)的那個(gè) fn main。
基礎(chǔ)設(shè)施講完了。現(xiàn)在讓我們進(jìn)入真正有趣的部分:游戲邏輯。
四、構(gòu)建 Flappy Bird
1、游戲循環(huán)
從《超級(jí)馬里奧》到《原神》,所有實(shí)時(shí)游戲在最底層都共享同一個(gè)結(jié)構(gòu)——初始化(Init)、循環(huán)執(zhí)行更新(Update)與繪制(Draw)、最后清理(Cleanup):

這就是游戲循環(huán)(Game Loop)。它揭示了實(shí)時(shí)游戲的本質(zhì):游戲不是一系列事件的響應(yīng),而是一幀又一幀的持續(xù)模擬。和 Web 應(yīng)用的事件驅(qū)動(dòng)模型不同,游戲代碼每秒執(zhí)行 60 次,無(wú)論用戶是否操作——用戶輸入不是觸發(fā)器,而是被每一幀"采樣"的信號(hào)。
一個(gè)良好的游戲架構(gòu)應(yīng)該將狀態(tài)更新(update)和畫(huà)面繪制(draw)嚴(yán)格分離:update 只修改數(shù)據(jù),draw 只讀取數(shù)據(jù),不存在交叉副作用。讓我們用這個(gè)原則來(lái)構(gòu)建 Flappy Bird。
2、定義游戲世界
首先,用結(jié)構(gòu)體描述游戲中的所有對(duì)象:
///|
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
mutscore: Int
mutgame_over: Bool
}
Bird 只保存每幀變化的值(位置和速度),而不變的屬性(水平位置、半徑)由 Game 持有——可變狀態(tài)越少,bug 越少。每個(gè) Pipe 記錄水平位置 x、空隙中心點(diǎn) gap_y(開(kāi)口的垂直中點(diǎn))和計(jì)分標(biāo)志 `scored`。 另一個(gè)值得關(guān)注的細(xì)節(jié):所有大小都從屏幕尺寸(sw、sh)派生——鳥(niǎo)的半徑是 sh / 25.0,重力加速度是 sh * 1.5。這意味著游戲在任何分辨率的設(shè)備上都能保持相同的視覺(jué)比例和手感,不需要額外的適配邏輯。
3、游戲邏輯
update 函數(shù)處理所有狀態(tài)變化——物理模擬、水管移動(dòng)、碰撞檢測(cè)和計(jì)分:
///|
fnupdate(game : Game, dt : Float)-> Unit{
ifgame.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
// 限制在屏幕邊緣內(nèi)
ifgame.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
}
forpipe in game.pipes {
pipe.x -= game.pipe_speed * dt
// 水管滾出左邊緣后回收到右側(cè)
ifpipe.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 碰撞檢測(cè)
? ??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
}
}
// 飛過(guò)水管時(shí)計(jì)分
ifnot(pipe.scored)&& pipe.x + game.pipe_width < game.bird_x?{
? ? ? game.score +=?1
? ? ? pipe.scored =?true
? ? }
? }
}
這段代碼有幾個(gè)值得注意的設(shè)計(jì)決策:
幀率無(wú)關(guān)性:所有涉及"隨時(shí)間變化"的量都乘以 `dt`(自上一幀經(jīng)過(guò)的秒數(shù))。`game.bird.velocity += game.gravity * dt` 意味著"每秒增加 `gravity` 這么多速度"——無(wú)論設(shè)備是 60fps 還是 30fps,物理效果一致。
對(duì)象回收:整個(gè)游戲只有 4 個(gè)水管對(duì)象。當(dāng)一根水管滾出左邊緣,直接把 `x` 坐標(biāo)加上偏移量"傳送"到最右邊,重新隨機(jī)空隙位置。不需要對(duì)象池框架——一個(gè) `if` 和一次坐標(biāo)重置就夠了。
AABB 碰撞檢測(cè):將圓形小鳥(niǎo)近似為外接矩形,檢測(cè)它與水管矩形是否重疊——先查水平方向重疊,再查小鳥(niǎo)是否在空隙之外。不是像素級(jí)精確,但對(duì)休閑游戲完全足夠。
游戲結(jié)束檢查:`update` 頂部的 `game_over` 檢查攔截一切后續(xù)邏輯,讓游戲"凍結(jié)"在撞擊瞬間,只允許點(diǎn)擊重啟。
draw 函數(shù)只負(fù)責(zé)將當(dāng)前狀態(tài)繪制到屏幕:
///|
fn draw(game : Game) -> Unit {
@raylib.begin_drawing()
@raylib.clear_background(@raylib.skyblue)
forpipeingame.pipes {
letpx = pipe.x.to_int()
letpw = game.pipe_width.to_int()
letgap_top = (pipe.gap_y - game.gap_size /2.0).to_int()
letgap_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()
}
先畫(huà)水管再畫(huà)小鳥(niǎo),確保小鳥(niǎo)總在最上層。所有繪圖調(diào)用必須在begin_drawing()和end_drawing()之間。輔助函數(shù)和初始化邏輯:
///|
fnrandom_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(),
),
)
}
///|
fnreset(game : Game)-> Unit{
game.bird.y = game.sh /2.0
game.bird.velocity =0.0
game.score =0
game.game_over =false
fori in0..
最后,main將一切連接起來(lái):
///|
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())
letgame : 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)
whilenot(@raylib.window_should_close()) {
letdt = @raylib.get_frame_time()
update(game, dt)
draw(game)
}
@raylib.close_window()
}
init_window(0, 0, ...)表示使用屏幕全尺寸——在 Android 上就是全屏。游戲循環(huán)本身只有三行:獲取 dt、更新、繪制。is_gesture_detected(GestureTap)同時(shí)響應(yīng)觸屏和鼠標(biāo)點(diǎn)擊,可以在桌面開(kāi)發(fā)測(cè)試后無(wú)縫部署到手機(jī)。
構(gòu)建并部署:
cdMyFlappyBird
./gradlew assembleDebug --no-daemon
adb install -r app/build/outputs/apk/debug/app-debug.apk
重力、水管、碰撞檢測(cè)、計(jì)分、游戲結(jié)束和重啟——全在一個(gè)文件、約 200 行代碼里。沒(méi)有引擎,沒(méi)有運(yùn)行時(shí),沒(méi)有框架。
五、總結(jié)與展望
讓我們回顧一下整個(gè)技術(shù)脈絡(luò)。
我們從一個(gè)簡(jiǎn)單的問(wèn)題出發(fā):一個(gè)休閑小游戲,真的需要一個(gè)完整的游戲引擎嗎? 然后沿著"從重到輕"的路徑,審視了移動(dòng)游戲開(kāi)發(fā)的幾種技術(shù)選型——從引擎(Unity/Godot)到跨平臺(tái)框架,再到原生 NDK,最終到達(dá) MoonBit + Raylib 這個(gè)極簡(jiǎn)組合。
在構(gòu)建鏈路層面,我們看到 MoonBit 編譯到 C、C 通過(guò) NDK 編譯到 .so、.so 打包進(jìn) APK 的清晰路徑——每一步都是確定性的,沒(méi)有黑箱。
在游戲架構(gòu)層面,我們理解了游戲循環(huán)這個(gè)"所有游戲的共同骨架",以及為什么 update/draw 分離、幀率無(wú)關(guān)的物理模擬是重要的設(shè)計(jì)原則。
在具體實(shí)現(xiàn)層面,我們用約 200 行代碼構(gòu)建了一個(gè)完整的 Flappy Bird,其中涉及了對(duì)象回收(窮人版對(duì)象池)和 AABB 碰撞檢測(cè)等實(shí)用技巧。
這套方案不是萬(wàn)能的。如果你需要 3D 渲染管線、物理引擎、骨骼動(dòng)畫(huà)、熱更新——使用 Unity 或 Godot 仍然是更務(wù)實(shí)的選擇。但如果你的目標(biāo)是一款輕量的 2D 游戲,追求的是小包體、高性能、完全可控的代碼,那么"做減法"的思路值得一試。
這里有一些可以繼續(xù)探索的方向:
tonyfettes/raylib —— MoonBit 的 Raylib 綁定庫(kù),涵蓋圖形、紋理、音頻、3D 模型、著色器等完整功能
selene —— 一個(gè)用 MoonBit 編寫(xiě)的實(shí)驗(yàn)性游戲引擎,支持 WebGPU 和 Raylib 后端,專為網(wǎng)頁(yè)和原生游戲設(shè)計(jì)
MoonBit 文檔 —— 語(yǔ)言詳細(xì)文檔
另外值得注意一點(diǎn)的是,本文中所有示例代碼均由 AI 生成,甚至包括 Raylib 綁定庫(kù)本身。我們利用 AI Agent 的 Subagent 并行化地在桌面、Web 和 Android 平臺(tái)上產(chǎn)出了超過(guò) 150 款游戲。更多詳情可參見(jiàn) tonyfettes/raylib 下的 examples/ 目錄。
從 Unity 的"給你一切"到 MoonBit + Raylib 的"只給你需要的",這不僅是技術(shù)選型的變化,更是一種開(kāi)發(fā)哲學(xué)的轉(zhuǎn)變——最好的代碼不是寫(xiě)出來(lái)的,而是不需要寫(xiě)的。
-
Android
+關(guān)注
關(guān)注
12文章
4031瀏覽量
134128 -
游戲
+關(guān)注
關(guān)注
2文章
791瀏覽量
27499 -
移動(dòng)端
+關(guān)注
關(guān)注
0文章
44瀏覽量
4754
原文標(biāo)題:用 200 行 MoonBit 構(gòu)建原生移動(dòng)端游戲
文章出處:【微信號(hào):OSC開(kāi)源社區(qū),微信公眾號(hào):OSC開(kāi)源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
原生js實(shí)現(xiàn)移動(dòng)端Touch輪播圖的方法步驟是什么
如何使用PVRTexTool提升移動(dòng)端圖形的效果
HarmonyOS/OpenHarmony原生應(yīng)用開(kāi)發(fā)-華為Serverless云端服務(wù)支持說(shuō)明(一)
5G云游生態(tài)計(jì)劃-云原生游戲開(kāi)發(fā)生態(tài)構(gòu)建大會(huì)順利舉行
如何使用PVRTexTool提升移動(dòng)端圖形效果
MediaTek天璣9000支持移動(dòng)端游戲超分技術(shù)
移動(dòng)端游戲體驗(yàn)的發(fā)展趨勢(shì)
Mavenir致力于利用云原生軟件構(gòu)建面向未來(lái)的網(wǎng)絡(luò)
AICAN 服務(wù)器平臺(tái)大幅加快云端移動(dòng)游戲的傳輸速度
Cocos 攜手 Google 打造Web端游戲開(kāi)發(fā)新體驗(yàn)
Katalon:移動(dòng)端測(cè)試
MediaTek與Unity中國(guó)攜手合作,打造次世代移動(dòng)游戲體驗(yàn)新標(biāo)桿
MediaTek 與 Unity 中國(guó)攜手合作,打造次世代移動(dòng)游戲體驗(yàn)新標(biāo)桿
開(kāi)源編程語(yǔ)言MoonBit 2024年度技術(shù)盤(pán)點(diǎn)
使用MoonBit和Raylib構(gòu)建原生移動(dòng)端游戲
評(píng)論