Feynman

ConjuGate

在學習遊戲裡做 Boss 戰:ConjuGate 的關卡設計

ConjuGate 是一個學日文動詞變化的遊戲。玩家一邊閃避敵人一邊回答文法題,答對加分,答錯扣血。這個循環玩 10 分鐘就膩了。每一關的體驗都一樣:敵人出現、答題、過關、下一關繼續。沒有高潮。 我加了 Boss 戰來解決這個問題。每關答對 8 題之後,畫面停止捲動,一隻史萊姆王從畫面頂端降下來。玩家要在 60 秒內答對 6 題把它打倒,答錯會被反擊扣血。打贏拿 500 分和 15 秒時間獎勵,打不贏 Boss 就飛走,遊戲結束。 Boss 戰的節奏設計 Boss 戰跟一般遊玩階段最大的差異是壓力來源不同。一般階段的壓力是空間性的:敵人從上面衝下來,你要一邊閃一邊答題。Boss 戰的壓力是時間性的:畫面不再捲動,你不用閃避,但你有 60
閱讀時間 6 分鐘
iOS 開發

從色塊到動畫:獨立遊戲的美術迭代踩坑記

ConjuGate 第一版的畫面:綠色方塊是玩家,紅色方塊是敵人,藍色方塊是無人機。全部都是 SKSpriteNode(color:size:) 生出來的純色矩形。 現在的畫面:史萊姆有 Run/Hurt/Death 三組動畫,玩家有四方向行走動畫,Boss 是一隻完整的 slime knight。從色塊到這裡,中間經過了大量的素材處理踩坑。這篇記錄我怎麼一步步把色塊換成動畫,以及過程中遇到的具體問題。 色塊階段:為什麼一開始不用美術 第一版用色塊有一個好處:我可以專注在 gameplay 上。移動手感對不對、碰撞判定準不準、出題節奏快不快,這些都跟美術無關。用色塊測試,改一行 code 就能跑,不用等素材。 色塊階段持續了大約整個大改有 20 多次 。在這段時間裡,我加了射擊系統、砲塔、10 關制、FSRS 出題排程。
閱讀時間 6 分鐘
ルート最適化アルゴリズム:巡回セールスマン問題から実戦の妥協へ
iOS 開發

ルート最適化アルゴリズム:巡回セールスマン問題から実戦の妥協へ

営業の友人からの一本の電話 外回りの営業をしている友人たちから、同じ悩みを何度も聞いた。毎日「訪問先の順番を決める」のに時間がかかりすぎる、と。 彼らの仕事の流れはこうだ。朝出発する前に、今日訪問する顧客をリストアップして、ルートを組み立てる。一部の顧客は到着時間を指定してくるので、逆算して出発時間を決める必要がある。途中で顧客がアポをキャンセルすれば、後の予定を全部組み直さなければならない。さらに厄介なのは、キャンセルした顧客が「やっぱり今日来てほしい」と電話してくるケースで、ルート全体を一から組み直すことになる。 以前は手帳やスマホのメモに予定を書いていたため、情報があちこちに散らばっていた。ある顧客の前回の訪問記録を探すのに、何ページもめくらなければならない。途中でスケジュールに空きができても、近くに寄れる顧客がいるかどうかもわからない。 こうした話を聞いて、「路順(RouteSync)」というアプリを作り始めた。目標はシンプルで、営業がスマホ上でその日の最短訪問ルートを組めること。そして顧客の時間指定や急なキャンセルにも対応できること。
閱讀時間 8 分鐘
E-Ink 設計系統:為什麼我的 App 故意不做動畫
iOS 開發

E-Ink 設計系統:為什麼我的 App 故意不做動畫

一個關於「拿掉」的設計決定 路順是一款外勤業務用的路線規劃 App。開發初期,我用了 SwiftUI 預設的樣式:圓角卡片、藍色按鈕、滑入滑出的轉場動畫。看起來跟市面上大多數 App 差不多,功能正常,但沒有任何辨識度。 我想讓路順看起來不一樣。但不是那種「配色大膽」或「加更多動效」的不一樣,而是反過來:把東西拿掉。 最後定下來的方向是電子紙(E-Ink)風格。灰階畫面、無圓角、無平滑動畫、點陣字體,整個 App 看起來像一台 Kindle 或早期的 Apple II 終端機。全畫面只有一個顏色:螢光綠,用在最重要的那顆按鈕上。 設計系統的四條規則 我在 EInkTheme.swift 的開頭寫了四條規則,之後所有的設計決定都從這四條出發:
閱讀時間 8 分鐘
路線優化演算法:從旅行推銷員問題到實戰妥協
iOS 開發

路線優化演算法:從旅行推銷員問題到實戰妥協

業務朋友的一通電話 幾個跑業務的朋友跟我提過同一個問題:每天花太多時間在「安排拜訪順序」這件事上。 他們的工作模式是這樣的:早上出門前,把今天要拜訪的客戶列出來,然後想辦法排出一條路線。有些客戶會指定抵達時間,業務得倒推出發時間,確保準時到達。如果中途有客戶臨時取消,後面的行程就要重新安排。更麻煩的是,有時取消的客戶又突然打電話來說「今天還是要拜訪」,整條路線又得打掉重排。 過去他們把行程記在筆記本或手機備忘錄裡,資料散落在不同地方。要找某個客戶上次拜訪的紀錄,得翻好幾頁。行程中途出現空檔,也不知道附近有沒有其他客戶可以順路拜訪。 聽了這些之後,我開始做「路順」這個 App。目標很簡單:讓業務在手機上直接排出當天最省時的拜訪路線,而且能處理客戶指定時間、臨時取消這些狀況。 教科書上的旅行推銷員問題 排路線這件事,在演算法課本裡有個經典的名字:旅行推銷員問題(Traveling Salesman Problem,TSP)。給你一堆地點,找出拜訪所有地點後總路程最短的路線。 TSP 是 NP-hard 問題,意思是當地點數量增加時,計算
閱讀時間 10 分鐘
iOS 開發

遊戲音訊的爆音除錯:四輪修復的踩坑記

Crystal Hangul 的試玩回饋裡,「爆音」這個詞出現了三次。第一次是進戰鬥的時候,第二次是殺第一隻敵人的時候,第三次是戰鬥結束跳結算畫面的時候。三個爆音的原因各不相同,我修了四輪才全部解決。 第一輪:進戰鬥爆音 玩家從選關畫面進入戰鬥,畫面切換的瞬間聽到一聲短促的 click。每次進關都會發生。 原因是 AVAudioSession 和 AVAudioEngine 的第一次啟動。iOS 的音訊硬體在 session 被啟用的那一刻會做一次配置,這個配置過程會產生一個聽得到的 click 聲。原本的 code 在進入戰鬥時才啟動音訊引擎,所以每次進關都是一次「第一次啟動」。 我把啟動時機移到 App 啟動畫面的背後。App 一開的時候就呼叫 prewarm(),把 session 啟用、引擎跑起來、所有音效 buffer 全部預載。之後進戰鬥、回選單、再進戰鬥,音訊引擎全程在跑,
閱讀時間 6 分鐘
iOS 開發

在地化雙語支援:讓遊戲同時服務中文和英文玩家

Crystal Hangul 是一個韓文學習遊戲。遊戲裡有三種語言同時在場:韓文(學習目標)、中文(母語解說)、英文(給非中文母語的玩家)。韓文永遠原文呈現,中文和英文之間可以在標題頁切換。 聽起來很單純,做起來踩了不少坑。 三種語言,兩種角色 韓文在這個遊戲裡的角色和中英文不同。韓文是學習內容,玩家要看、要拼、要聽 TTS 發音。韓文字塊上面的文字、句子的韓文原文,不管選什麼語系都不會變。 中文和英文是「解說語言」。UI 上的按鈕文字(「開始」/「Start」)、文法解說(「은/는 主題助詞」/「은/는 topic particle」)、句子翻譯、字塊的逐字釋義,這些跟著玩家選的語系走。 這個區分很重要。如果我不小心把韓文也加進在地化系統,玩家切到英文的時候韓文字塊會變成英文翻譯,遊戲就失去了學韓文的意義。 Loc.t:最簡單的雙語切換
閱讀時間 6 分鐘
五聲音階組句:用音樂理論設計遊戲音效
iOS 開發

五聲音階組句:用音樂理論設計遊戲音效

Crystal Hangul 的字塊點擊音效不是從素材包裡挑的。每點一個字塊,你聽到的音高由它在句子裡的位置決定。第一個字塊是 Do,第二個是 Re,第三個是 Mi,依此類推。拼完整句之後,會落一個 Do-Mi-Sol 和弦收尾。 拼句的過程聽起來像在彈一段上行旋律。拼得越順,旋律越流暢。答錯的時候聽到一個低沉的 deny 音效,旋律斷掉。combo 連續答對的時候,背景音樂會加入新的樂器層。整個聽覺體驗和遊戲表現綁在一起。 為什麼用五聲音階 五聲音階(pentatonic scale)只有五個音:宮、商、角、徵、羽,對應西方的 Do、Re、Mi、Sol、La。它的特色是沒有半音,任何兩個音放在一起都不會產生不和諧的感覺。 這個特性很適合遊戲音效。玩家點字塊的順序是固定的(句子的正確語序)
閱讀時間 6 分鐘
iOS 開發

做了才知道不要的東西:兩次 Revert 教我的事

ConjuGate 的 git log 裡面有兩個 revert commit。一個把無人機的水晶動畫砍回藍色方塊,一個把石磚道路砍回純色背景。兩個功能加起來花了五個 commit,最後全部退回原點。 為什麼 revert 不是浪費,以及怎麼 revert 才不會痛。 Case 1:水晶無人機 ConjuGate 裡有無人機繞著玩家轉圈射擊。一開始是 16×16 的藍色方塊,能用但醜。我找了一組水晶的 sprite sheet,重構了兩次才做好,而且code調很久: * 第一次重構:用水晶 sprite 替換藍色方塊,載入四個方向的動畫 atlas(上下左右各 6 幀) * 第二次重構:根據無人機繞圈的位置自動切換面向方向 加完之後跑起來看,水晶 sprite 的風格跟遊戲裡的史萊姆敵人完全不搭。史萊姆是圓潤的 pixel
閱讀時間 4 分鐘
試玩回饋驅動開發:十幾條真實反應怎麼改變一款遊戲
Crystal Hangul

試玩回饋驅動開發:十幾條真實反應怎麼改變一款遊戲

Crystal Hangul 上架之前經過了兩輪試玩。我把未完成的版本丟給朋友玩,坐在旁邊看他們操作,記錄他們卡住的地方和說出來的話。兩輪下來收到了十幾條回饋,每一條都對應了一個具體的改動。 試玩回饋改變了遊戲的面貌。有些問題是我自己測一百次也不會發現的,因為我太熟悉自己的遊戲了。 「太快了,來不及想」 這是第一輪試玩最普遍的回饋。試玩者看到韓文字塊,還在辨認是哪個字,敵人就走了一大段。他們不是看不懂韓文,是需要幾秒鐘把字塊和意思配對起來。 我把第一章的敵人速度壓低了將近一半,逐波微升。第一波還加了順序數字提示,讓新玩家先學會「按順序點字塊」這個操作,不用同時處理語言辨認和遊戲規則。 第三章的回饋更具體:「3-2 開始太快了,來不及看字跟讀」。第三章的句子從 3 塊升到 4 塊,每句需要更多時間辨認。我把整個第三章的敵人速度和血量都調低,讓壓力來自語言本身而不是手速。 「出怪點被動態島擋住了」 這個問題我在模擬器上不會發現,因為模擬器的動態島跟實機大小不同。試玩者拿 iPhone 15 Pro 玩,敵人從畫面頂端生成的時候被動態島完
閱讀時間 7 分鐘
SpriteKit 效能調校:三個讓遊戲不再掉幀的改動
iOS 開發

SpriteKit 效能調校:三個讓遊戲不再掉幀的改動

ConjuGate 在只有玩家和敵人的時候跑得很順。加了砲塔、無人機、散射之後,畫面開始掉幀,手機開始發燙。 我在三個地方找到了瓶頸,修完之後掉幀消失,耗電量也降了下來。這篇把三個問題和對應的解法寫下來。 問題一:每秒 68 次 alloc ConjuGate 的武器系統有普通子彈、散射、雷射、無人機、砲塔。砲塔加無人機全開的時候,一秒大約發射 68 顆子彈。每顆子彈都是 BulletNode(style:) 新建一個 SKSpriteNode instance。 一秒 68 次 alloc + init,Swift 的 ARC 要追蹤 68 個新物件的 reference count,SpriteKit 要把 68 個新 node
閱讀時間 6 分鐘
拼句子建塔:把語言學習塞進塔防機制
Crystal Hangul

拼句子建塔:把語言學習塞進塔防機制

塔防遊戲的核心循環是:敵人來了,蓋塔擋住。Crystal Hangul 把「蓋塔」這個動作換成「拼韓文句子」。畫面下方有一排打散的韓文字塊,玩家按正確順序點選,拼完一句話就蓋一座塔。敵人在上面走,塔在下面長,你拼得越快塔打得越痛。 把語言學習塞進塔防機制,最難的部分是讓兩者的節奏對上。拼句子需要思考時間,塔防需要時間壓力。思考時間太長敵人就走進核心了,時間壓力太大玩家就沒辦法好好讀句子。這篇記錄我怎麼處理這個矛盾。 塔 = 你的記憶 Crystal Hangul 有三種塔:名詞塔、動詞塔、助詞塔。每種塔對應一種文法。開局的時候場上沒有任何塔,是空地。 玩家第一次拼對某個文法的句子時,對應的塔才會從地底長出來。名詞句拼對了,名詞塔出現。動詞句拼對了,動詞塔出現。塔出現的瞬間會產生一圈脈衝波,對附近的敵人造成傷害。 之後每拼對一句同文法的句子,那座塔就會立即發射一道強光束打向最前面的敵人。拼得越快,光束越強。連續答對不出錯的話,combo 倍率會一路往上加。 這個設計讓「學習」
閱讀時間 7 分鐘
讓遊戲記住你哪裡不熟:FSRS 間隔重複的簡單實作
iOS 開發

讓遊戲記住你哪裡不熟:FSRS 間隔重複的簡單實作

你有 80 個日文單字要學,每個單字有 5 種變化形。隨機出題的話,你已經會的「食べます」和你老是搞混的「食べさせられる」出現的機率一樣。你的時間有一半浪費在已經會的東西上。 FSRS(Free Spaced Repetition Scheduler)解決的就是這個問題:讓你不熟的東西多出現,熟的東西少出現。我在 ConjuGate 裡面實作了一個簡化版的 FSRS,這篇用最簡單的方式解釋它怎麼運作。 先從一張卡片開始想 假設你正在學「飲む」的 ます形。你第一次看到它,答對了。下一次什麼時候該再出現? 如果太快再出,浪費時間。如果太晚再出,你已經忘了。FSRS 的核心就是找到這個「快要忘記但還沒忘」的甜蜜點。 做法很直覺:每張卡片有一個 interval(間隔),代表「隔多久再出一次」。答對了,interval
閱讀時間 5 分鐘
用 RealityKit 做 2.5D 塔防:為什麼不用 SpriteKit
iOS 開發

用 RealityKit 做 2.5D 塔防:為什麼不用 SpriteKit

Crystal Hangul(水晶韓語塔防)的玩法是 2D 的:敵人沿著固定路線走,玩家在路邊蓋塔。但整個場景是用 RealityKit 做的 3D 渲染:低多邊形的水晶漂浮在夜空中,塔是旋轉的幾何體,敵人死的時候碎成晶片四散。 我最初考慮過 SpriteKit,最後選了 RealityKit。以下是原因和過程中遇到的挑戰。 SpriteKit 做不到的東西 Crystal Hangul 的美術方向是「低多邊形水晶」。塔是正四面體、正八面體、正十二面體,面與面之間有銳利的折角,在光線下產生不同的明暗。敵人是隨機擾動的碎晶,每隻長得不一樣但風格一致。場景裡有漂浮島、水晶尖刺、發光的裂縫門。 這些東西在 SpriteKit 裡要用預先畫好的 2D 圖片模擬。水晶的光暈可以用半透明的 sprite 疊出來,但旋轉的時候會露餡,因為 sprite 是平的。光影變化要手動畫多幀或用
閱讀時間 6 分鐘
遊戲寫到一半才懂的事 — 什麼時候該拆 Class?
iOS 開發

遊戲寫到一半才懂的事 — 什麼時候該拆 Class?

如果你問我「寫遊戲的時候,什麼時候該把程式碼拆開?」我的答案是:等你痛了再拆。 這不是在開玩笑。我在做 ConjuGate(一個用 SpriteKit 寫的日文動詞變化學習遊戲)的過程中,真的是先把所有東西塞進一個 GameScene.swift,等到痛到不行了才開始拆。回頭看,這反而是對的。 一開始:所有東西都在 GameScene 裡 第一版的 ConjuGate,整個遊戲邏輯就是一個 1,132 行的 GameScene.swift。玩家移動、敵人生成、子彈發射、HUD 更新、碰撞判定——全部在同一個檔案裡。 你可能會說:「這不是很糟嗎?」 其實不會。在早期階段,你還在探索遊戲到底長什麼樣子。今天加了一個機制,明天可能就砍掉。如果一開始就花時間設計完美的架構,這些架構很可能跟著被砍掉的功能一起進垃圾桶。 想像你在畫草稿——你不會先把鉛筆線條上色。先畫出形狀,確定構圖對了,
閱讀時間 5 分鐘