「有人在旁邊一起讀,你就比較坐得住」這件事,比任何一款專注 app 都老。

考前去圖書館佔位、跟同學約咖啡廳寫報告、甚至只是把書攤在客廳讓家人看得到——你做這些,理由其實一樣:你知道自己一個人關在房間,十分鐘就會摸到手機。旁邊有個人在認真,你會莫名其妙也跟著認真起來。心理學給這個現象取了個名字,叫 body doubling,但名字不重要,重要的是它真的有效,而且每個準備過考試的人都親身驗證過。

問題是,這幾年大家越來越多時間是一個人待在家裡的。圖書館遠、咖啡廳吵、同學各自在不同城市。於是有人想把「旁邊有人」這件事搬到線上——最直接的做法是開視訊,像 Focusmate 那樣,跟一個陌生人對著鏡頭各讀各的五十分鐘。能用,但總覺得哪裡怪:你得一直開著鏡頭,怕自己看起來在發呆,那份壓力跟「專注」其實是兩回事。

StudyClassRoom 是另一種切法。它沒有用視訊,而是用一個遊戲引擎。

把自習室做成一款 2D 遊戲

它是用 Godot 4.4 做的——對,就是那個做獨立遊戲的引擎。你跟朋友連進同一個 2D 虛擬教室,每個人在裡面有個角色,設定好自己要專注的時間(1 到 180 分鐘,Pomodoro 那一套),計時開始,大家就一起讀。讀完拿星星積分,上全域排行榜,平常還能加好友、傳私訊。

聽起來就是個普通的線上計時器套了張遊戲皮。真正有意思的設計只有一條規則:專注期間,你的角色一移動,就扣分。

第一次看到這條規則我愣了一下,因為它反過來了。一般的學習 app 都在想方設法「獎勵你坐好」——連續打卡送徽章、累積時數解鎖功能。StudyClassRoom 不來這套,它直接把「亂動」變成一件有代價的事。你想離開椅子滑個手機?可以,分數扣給你看,而且全教室的人都在排行榜上看得到你掉下去。

這就是它聰明的地方。它沒有試圖用意志力或正能量說服你坐好,它改了遊戲規則,讓「坐好」變成贏這場遊戲的唯一方法。你不是在對抗自己的分心,你是在玩一個「誰能不動最久」的比賽——而比賽,人是真的會認真玩的。

拆開來看,它其實是兩套系統黏在一起

如果只是想做個計時器,這專案不會有什麼好講的。值得看的是它底下的結構——它不是一個東西,是兩套連線方式各做各的事,再黏在一起。

多人遊戲的部分,走的是 ENet,也就是 Godot 內建的那套 P2P 連線。玩家在教室裡走動、位置同步,靠的是這條。為什麼用它?因為遊戲位置同步要的是「快」,慢個半秒角色就會瞬移、會抖,體驗就爛了,UDP 底的 ENet 剛好吃這種低延遲的場景。

但好友、聊天、排行榜這些,它就不走 ENet 了,改用一台 Go 寫的後端(Echo 框架 + GORM),標準的 RESTful API,私訊另外走 WebSocket。為什麼分兩套?因為這些資料要的不是快,是「不能掉」——你傳的訊息、累積的積分,必須可靠地存下來,這跟「角色走兩步」的需求是完全不同的東西。

一個動作要的是即時、可以容許偶爾掉幀;另一個要的是可靠、一筆都不能少。把這兩種需求硬塞進同一條連線,最後兩邊都做不好。分成兩套各司其職,看起來複雜,其實是想清楚之後最簡單的選擇。

這裡藏著一個一般教學文不太會講、但實作的人一定會踩到的細節:Go 後端跑了兩個 replica,前面用 Nginx 做負載均衡。問題來了——你傳私訊的時候,你連在 replica A,對方連在 replica B,A 收到的訊息怎麼讓 B 上的人收到?答案是 Redis 的 Pub/Sub:A 把訊息丟進 Redis 的頻道,B 訂閱著同一個頻道,於是訊息就過去了。沒有這層,多 replica 的聊天就是各說各話。

text
1
2
3
Godot Client ──HTTP/WS──► Nginx (LB) ──► Go API x2 ──► SQLite + Redis

└── ENet P2P (port 7000) + UDP Discovery (port 7001)

那些「文件不會寫、但你會半夜踩到」的坑

我沒有實際把這專案跑起來——以下是讀它的程式碼結構跟作者自己標的限制整理出來的,但有幾個點,凡是寫過類似東西的人看了都會點頭。

客戶端有個叫 HTTPRequest 防 GC 的處理,乍看莫名其妙:發一個 HTTP 請求而已,為什麼還要特地把它「留住」?因為 Godot 的 GDScript 會自動回收沒人引用的物件,你發出請求、還在等回應的時候,如果那個 HTTPRequest 節點剛好沒被任何變數抓著,它會被當成垃圾清掉——然後你的 callback 就永遠不會被呼叫,請求像是石沉大海。作者的解法是開一個 active_requests 陣列把它們暫時掛著,等回應回來再放掉。這種 bug 最折磨人,因為它不報錯,就只是「有時候沒反應」。

還有個更要命的,作者自己也老實標出來了:後端用 SQLite,但跑了兩個 replica。 SQLite 是單檔資料庫,本來就不是設計給多個 process 同時寫的,兩個 replica 同時想寫入,就會撞鎖。這在你自己一個人測的時候完全看不出問題,等到真有一群人同時上線、同時在更新積分,鎖競爭就會開始咬你。作者的建議很乾脆:要認真用,換 PostgreSQL。

其他還有一串「這是個人專案」的痕跡:WebSocket 端點沒做 JWT 驗證,任何人都能連進去;Nginx 只開了 HTTP 沒有 HTTPS;後端網址在好幾支 GDScript 裡硬編成 localhost,想換環境得一個一個改;docker-compose 裡的 JWT_SECRET 還是明文範例值。這些不是說它做得爛——一個拿來驗證想法的小專案本來就該這樣,先把核心玩法做出來,安全跟部署的事擺後面。重點是你心裡要清楚這條線在哪:它現在是個玩得起來的原型,不是可以直接開給外面用的服務。

真正值得帶走的,是那條規則的設計

技術細節有趣,但它們都只是手段。這專案最該偷學的,是它怎麼處理「人就是會分心」這個老問題。

大部分做專注工具的人,會往「怎麼幫使用者更有意志力」的方向想——更漂亮的計時器、更激勵的文案、更精美的成就徽章。StudyClassRoom 的作者選了另一條路:不去改人,去改規則。把分心從「一個你要靠意志力對抗的誘惑」,變成「一個會讓你輸掉比賽的動作」。人對抗誘惑很爛,但人玩遊戲想贏很強——它只是把問題搬到了人擅長的那一邊。

這招不只能用在自習。下次你想讓自己或團隊養成某個習慣,先別急著找更好的 app 或更狠的鬧鐘。先問一個問題:有沒有辦法改個規則,讓「做對的事」直接變成贏,讓「做錯的事」直接看得到代價?答得出來的那些習慣,往往不需要意志力就自己長出來了。


專案來源:StudyClassRoom(GitHub),技術棧 Godot 4.4 + Go (Echo) + SQLite + Redis + Docker。本文為閱讀其原始碼結構與作者標註之限制後的整理,未實際部署運行。