因為之前接觸過手機營運類遊戲的開發跟維護,想分享一些心得。我自己工作算是 Unity 前端所以看得有限,希望如果有接觸過不同開發面向的朋友也能分享經驗。

製作

首先最直接的是該不該做「營運類遊戲」跟「手機遊戲」:

營運類遊戲

營運類遊戲目前非常競爭,大廠可以直接輸出全 3D 角色、一線日本聲優、大手繪師等等靠資本堆的護城河。要再投入戰局可能只能找藍一點的海攻,像是《新世界狂歡》跟《天下布魔》算是不錯的例子。不是說非做情色不可,只是還是要想清楚自己跟 Supercell、Cygames、米哈遊這類坐穩的大卡車們有多少距離,不會人家迴轉的時候被掃到,變成玩家在說玩這個不如玩某某的窘境。

最近《我滿懷青春的有病》有分享了開案時的想法與半年後的檢討,蠻值得看看的:

另外常見的問題是上市前沒有抓好製作新內容的速度。感覺看過太多案例了,上市前可以屯內容,但是屯的內容耗盡就是考驗內容製作工業化產出是否穩定的開始。遊戲上市後玩家人數留存就像是中了 DOT 會一直往下掉,需要新內容、新活動甚至是新機制來補血拉血量。出新機制又有可能出 Bug 要修,增加更新間的工作量。當內容青黃不接玩家開始流失時,看起來會真的很恐怖。營運類遊戲上市後就像一台飛機離地起飛,要一邊飛、一邊加油、一邊維修不能落地。所以開案前期就要弄清楚像是角色規格(立繪張數、角色要二頭身 Spline、全身 Spline、還是 3D 等)、場景規格、活動規格、劇本規格等等等,這些東西 Production 生產步調能不能滿足營運需求。這點可能是最容易被第一次嘗試的團隊忽略的大重點。

手機遊戲

手機做為軟體平台最大的特性是要把軟體留在上面比其他平台難得多很多,兩個平台平均每半年都會有新要求。沒有滿足要求會有相應的懲罰,輕則降觸及、不給 Feature,重則下架。之前在 TGDF 2019 演講時有特別提過這點,至今依然如此。所以遊戲在手機上要保持上架與觸及,會有不小的額外人力開銷,這在開案之前要考慮進去。也是為什麼現在比較難在手機上看到採用無廣告買斷的遊戲主要原因,可以參考 Simogo 寫的 The Year of The Devil

演講當年 2019 遇到 iPhone X 上市螢幕尺寸大改特別歡樂

因為現在大部份開發都是使用 Unity,要配合政策轉彎又會比寫 Native App 的難。有時候會牽動 Unity 要升版,有經驗的就會知道升級 Unity 有多可怕。

以前的山崩經驗。換個 Icon 結果頂到 SDK 不支援 iPhone X、要新 SDK 要升級 Xcode、要升 Xcode 要新 macOS、升級 macOS 的時候沒注意直接跳過 macOS 10.12 升到 10.13 硬碟格式被改成 APFS、當時用的 Unity 不支援 APFS 需要升級 Unity、最後升級 Unity 又把專案搞爛需要手動修。理論上還可以更慘,因為 Xcode 會有 macOS 需求macOS 又有硬體需求,如果當時運氣再好一點改個東西會滾雪球到要買整台新 Mac。

如果以前做單機、沒有接觸過手機遊戲環境的可能要參考一下資料。因為這些都是不小的開銷,要預留人力。

現在 Google 有提供一個各項政策期限的列表,可以在 修正期限將至的 Android 和 Google Play 事項Policy Deadlines 找到。至於 Android SDK API Level 規定可以在 符合 Google Play 的目標 API 級別規定 找到。Apple 可能只能訂閱 Developer News and UpdatesRSS 來被動接收通知。

旁觀企劃方面的想法

屬性相剋

屬性相剋感覺非常老套,水火相剋、劍職克槍職克弓職之類的。我有碰過設計上沒有屬性相剋的案子,覺得拿掉缺點比較明顯。最直接的就是雜魚、關卡屬性量產直接少了一個維度。再來也是策略上會失去一些可以做操作的深度。這些都會影響到上面提的內容量產速度。

體力

雖然市面上的遊戲體力幾乎都多到用不完,感覺留這個機制很雞肋。但是我也遇過拔掉體力的案子,最直接的影響當然是少了一個可以派發的獎勵。雖然平常可能用不完,但是可以在活動時加大消耗以凸顯營運派發體力的價值。跟技術比較相關的是我們無體力的案子遇到過有玩家狂刷關卡,但是無從判斷起到底是很執著在玩的人,還是自動腳本。有的人刷的量就是剛好微妙,在人力可及的範圍但是很多。Ban 會擔心誤殺最忠心的玩家,不 Ban 會擔心放過不合理遊玩的人。如果有體力的話至少能限制住自動腳本的活動上限而不用面對這種兩難。

代幣

由 IAP 購入的代幣要分有償跟贈送的無償算是基本,活動可能需要只有付費兌換的代幣才能參加的設計。但如果後端開發有餘力,會建議把有償的部分再分割成從哪個平台購入的。除了統計分析的需求外,如果有移植到日本的平台,有些平台會有「自己的平台買的代幣不能在其他平台登入時花」的要求。以前有營運中的遊戲進軍日本平台中這個要求,臨時要趕出分類系統。如果有做新案子會覺得來源平台區隔一開始就做進去會比較好。

Bot

如果是製作有對戰要素的遊戲,除了 Match Making,Bot 是必要的功能。一個是確保新手上手的過程平順,一個是填充缺乏玩家的分數區間。如果開案時就決定要有對戰要素,Bot 的開發就必須要納入時程。

申請 Domain Name

在開始宣傳前要確定預計使用的 Domain Name 是否已掌握,包含可能像是說怕玩家拼錯的近似 Domain Name 或是其他國家 TLD 的網址。Domain Name 蟑螂非常猖獗,開始宣傳後才發現沒有掌握很可能會要付一大筆贖金。

技術細節

Unity 版本

上市前後 Unity 版本策略-應該是開案時越新越好,如果開發中能更新也儘量更新。上市後則盡量少更新,除非遇到 Unity LTS 支援接近結束。因為 Unity LTS 支援只有兩年較其他軟體短(像 Debian 跟 Ubuntu LTS 都是至少五年),上市前會希望能在越新的版本通過測試越好,以便上市後受到越長的 LTS 保障。上市後當然求營運穩定能不動 Unity 就不動,但是接近 LTS 尾聲可能還是要找情況許可的機會往前跳兩年的 LTS。有些針對前面所說的手機 OS 要求的 Unity 引擎改動會不 Backport 給過期的 LTS 版本,使用過期 LTS 版本有臨時被手機 OS 逼迫突然要升級 Unity 的風險。

手機市場調查

市佔率

上面說的角色、場景量產規格有時也會跟想支援的手機規格最低到多低有關,要支援到多差多舊的手機需要資料參考。提醒如果是剛開案,下面找到的資料可能還要再經過預測外推。像是預期開案後要製作兩年,就要想想兩年後上市的時間點大概的分佈會是如何。

Android

iOS

抬高支援底線可以少處理一些舊版本的問題,尤其是 貼圖格式 會造成很大的影響。(貼圖格式支援跟硬體較相關、OS 比較次要)如果能統一使用 ASTC 或是 ETC2,避開超難處理的 ETC1(沒有 Alpha Channel)跟 PVRTCiPhone 6 以前 iOS 裝置 預設,條件限制多壓縮品質又差)會節省很多功,但就是會少一點可觸及的市佔。(也是有可能前面開案的規格就高到舊機器不可能跑,那就直接 ASTC 即可)

手機規格

手機規格通常都能在 GSMArena 找到,比較特例的是三星的手機常常會有同一台手機不同 SoC 的情況,會有相同機型其中一些使用三星 Exynos 情形,如果遇到問題回報在三星手機上查找時要特別留意。

iOS

Apple 裝置除了大家常見的名稱外還有 Model Number 跟 Model Identifier 之類的識別,如果遇到錯誤回報是報這類資料,可以用 EveryMac 反查。

iOS 裝置單一 App 可用記憶體是沒有明確界線,在記憶體使用過多時試會先給 警告,如果在警告後沒有改善 App 會被強制關閉(使用者看到的行為會像是閃退,有接著手機看 Console 通常會看到 Jetsammed 字樣)。警告範圍跟實際殺掉 App 的點跟機器還有機器上的 OS 有關,相當複雜,幸好有人製作測試程式並記錄了測試結果在 這裡,是 iOS 開發上相當重要的參考資料。

關於 Unity 的記憶體區別可以參考:

關於 iOS 裝置的螢幕大小,pt 與 px 的量可以在 Apple 的 Human Interface Guidelines / Layout 找到。可以做為美術準備圖素或是 UI 時解析度的參考。也有一些 其他網站 保留了比較舊的機器的資料。對於 Point (pt)、Pixel (px)、Points per inch (PPI) 的差異、還有 Scale Factor 說明可以參考:

最後是 iOS 裝置上的功能清單,像是 CPU 種類、有沒有支援 Metal 之類,這個叫 Device Capability,可以在 官方總表 查找。

自動化建置

要持續更新,遊戲建置的自動化是不可或缺的。如果團隊沒有 自動化建置 Unity 專案 的經驗,個人會覺得真的不太適合走營運類的作品。如果有自動化建置經驗只是對手機環境不太熟,對手機建置最重要的莫過於用 fastlane 填補 Unity 建置後如何把 Xcode 專案再建置成 IPA 跟與 Google、Apple 後台互動的需求。Builder Commandline 跟 Unity 互動、另外還有 Patch Unity 產生的 Xcode 專案的需求,這方面有許多現成的工具,前公司也有 Open Source 相關的 Mimiron LitePatchwerk Lite

Google 與 Apple 的額外要求

除了審查的必要需求外,有時候 Google 跟 Apple 會給出額外的要求來交換商店 Feature 機會。雖然商店 Feature 已經不像以前那麼有力,但是如果有想要爭取的,在這裡列一下看過的要求做為參考:

Android

加入 Google Play Games 第三方登入

如果要實作這項,實作完成後記得對一下這項 Google Play 遊戲服務品質檢查清單。如果使用者拒絕了登入 Google Play Games,那下次啟動應要求是不應該再幫使用者自動跑登入流程。

符合 Material Design

Google 會要求即使是用 Unity 兜出 UI 的遊戲也要符合 Material Design 設計語言,實際上看到的要求好像只有 按鈕要有「按下」與「放開」兩段獨立動畫,按下去馬上伸縮一下的一段動畫不算。如果想要滿足這項要求的話,按鈕的 Prefab 要盡早準備好相應的 Animator,等到按鈕都鋪完才要加會有點痛苦。

UI 支援 Back Button

遊戲內的 UI 也要對 Android 系統上的返回鍵有反應。

不能在遊戲啟動時就要求權限

Android 6.0 以前與過往 Unity 預設行為是在啟動時要求所有執行階段權限,Android 6.0 後 Guideline 變成執行階段權限要在使用前才徵求使用者同意。AndroidManifest.xml 權限宣告還是一樣要有,只是真的要拿到權限,執行中要呼叫 Android.Permission API。呼叫 Android.Permission 之前要顯示為何需要這項權限的說明,然後請忽略 Guideline 對 shouldShowRequestPermissionRationale() 的敘述。審查實際經驗是不管 shouldShowRequestPermissionRationale() 回傳是什麼,都要顯示說明。

iOS

實作 Sign-in with Apple

有任何第三方登入就要實作 Sign-in with Apple,不過如果想要 Feature 就算只有自己的帳號系統,編輯審查也還是會希望看到 Sign-in with Apple。關於 Sign-in with Apple,下面的第三方登入段落有更多的描述。

遊戲啟動不能馬上要求登入資訊

要讓玩家看到一些畫面,到非登入不可時才要求登入資訊。連帶是如果你的遊戲有需要呼叫 IAP Restore 也不能在遊戲開始時直接呼叫 Restore,因為 Restore 可能會彈出 Apple 帳號登入視窗。正確的作法是要準備一個 Restore 按鈕,玩家按下去時才 Restore IAP。

要求使用者評分需要使用系統 UI

iOS 上現在要使用者評分 App 要用系統給的 SKStoreReviewController,現在的版本 Unity 有包好 Device.RequestStoreReview。但這個 UI 行為有些乖僻,條列說明如下:

  • Development 會 一直 彈出
  • Testflight 不會 彈出(參考 requestReview Note 說明)
  • 上線後使用者
    • 365 天內最多只會看到三次 UI
    • 因為呼叫了不一定會有反應,不應該綁在按鈕上,而是開發者自己看適合的時機呼叫
    • 沒有 API 查詢是否已經不會彈出

這個 API 設計還蠻微妙的,建議詳讀 Ratings, reviews, and responses 理解一下 Apple 的想法。還有 Stack Overflow 的 討論

注意時間複雜度

有學過演算法應該會熟悉 Big O Notation。平常會覺得演算法理論跟遊戲製作有點遠,但是我看過、自己也被 Dawson’s Law 咬過:

有的時候高時間複雜度的 Code 會混進遊戲裡,在遊戲剛上市時因為 Input 數量小發覺不出來,營運久了突然某個時間點效能開始大幅下降。我自己的經驗是在 Unity JSON 方案還不多時使用了 MiniJSON.cs,不太確定這個 Parser 的準確時間複雜度,但就自己的經驗應該是超過 O(N),所以只是線性地加資料在某個時間點突然讀取變超久。同樣的情形、也是 JSON Parser 也發生在 GTA 5 Online 啟動上。(他們的問題會這麼久才發現一部分是因為 GTA 5 程式有混淆,外部的人要有解讀的知識才能分析)

關於這個問題好像現下除了不要用少人用的程式庫,跟 Code Review 緊一點之外沒有特別的想法,也許有時間能故意餵大測資看看。提出來希望大家如果未來有遇到營運到一半突然效能大幅下降,能想到可能是很久以前課本上的 Big O 又回來了。

資料盡量不要使用文字格式儲存

JSON 使用、閱讀、編輯都方便,但是在手機上要 Parse 純文字 JSON 時間跟 GC 的消耗都不小。另外 Parse 純文字成別的資料型別在多國語系下有許多坑,會在後面多國語言詳述。如果團隊沒時間鑽研建議使用 Json.NET 處理 JSON,在開發編輯時使用 JSON,Production 時改用 Json.NET 的 BSON 格式在小幅度修改 Serialization API 的前提下節約一些時間與 GC。如果真的有餘裕應該研究看看 MessagePackprotobuf 之類的格式。

避免使用 WWW 或是 UnityWebRequest 跟 Server 溝通

現在用 WWWUnityWebRequest 基本上是同一件事。WWW decompile 出來也只是:

1
2
3
4
5
public WWW(string url)
{
    _uwr = UnityWebRequest.Get(url);
    _uwr.SendWebRequest();
}

UnityWebRequest 有很多奇怪的行為像是:Post JSON String 會被自動 Escape需要手動 Escape 回來)或是 Authentication Header 消失。面對這些清況建議是跟 Server 溝通直接改用 Best HTTP/2,如果有問提可以到 Best HTTP/2 作者的 Discord 發問,作者回覆蠻快的,確認有問題時也會願意提供 Hotfix 版本。UnityWebRequest 只用在從 CDN 下載 Binary 還有開 Android 的 StreamingAssets 就好。

防作弊

遊戲上線後肯定會有人想要編輯或重送封包來作弊(或是亂送資料到 Endpoint 看能不能混水摸魚),比較消極的做法可能只能手寫一些規則剔除明顯玩遊戲不可能出現的情境,或是提供玩家檢舉的機制。最治本的方法當然是 Server 端有相同的遊戲邏輯做輸入驗算,但是 Client 與 Server 通常不同語言,要用不同語言實作相同邏輯然後保持每個版本都同步是一件很困難的事。目前大多能做的可能就只是請資料分析的人撈太扯的資料,加入嫌疑人名單或是 Ban 帳號,如果是對戰類可以考慮做成嫌疑人只能 Match Making 到其他嫌疑人。

比較天馬行空的做法有像 Cygame 的公主自走棋使用 CySharp 的 MagicOnion Library。概念大致上是 Client 與 Server 都用 C# 寫放在同一個 Project 下,平常開發時 Client/Server 是 Function call,部屬後由 Library 引導至 RPC。這樣就能實現 Client 與 Server 共用同一套遊戲邏輯,也就能簡單地做玩家行為的驗算。不過一方面是 C# 作為 Server 的效能有疑慮,另一方面是去年 MagicOnion 底層用的 gRPC.Core 進入 維護模式 且提早移除 Unity support(最後一個有 Unity 的 Daily Build 停在 2022-04-19)。自己試過單純導入 gRPC.Core,在 Android 跟 iOS 上有蠻多未預期行為,不知道 Cygames 是怎麼處理到能上架的。現在 gRPC 官方開發主力在 gRPC for .NET,Unity support 還是 Open Issue 但是卡在 Grpc.Net.Client 要 HTTP/2 而 Unity 本身沒有支援。有些人嘗試使用 Best HTTP/2 串接(grpc-dotnet via HTTP2 for Unity)不過我實驗就沒有到這麼遠了。

參考資料:

防拆包

未上市的資料被有心玩家拆出來公布會對營運規劃造成不小困擾。AssetBundle 格式有很多工具可以解讀:AssetStudioUtinyRipper 等等。雖然有一些基於 AssetBundle.LoadFromStream 加密 AssetBundle 的 方案,但是無法防範玩家從遊戲本體裡找出加密鑰匙。(除非你想繼續加碼使用 App 加固服務之類的,但是加固也不是堅不可摧)

比較治本就是公開的資料不要包含還不想給玩家看的資料。我們做過的作法是改寫 AssetStudio 成一個 Commandline 工具,在每次自動打包 AssetBundle 後自動拆自己然後 git commit push 到一個 Git Repository。再讓一位企劃大略看過這次拆包結果跟上次拆包結果的 Diff 來抓是否有洩漏。

無用/首抽帳號處理

讓玩家刷首抽有增加下載量帳面數據的行銷好處,但也同時會產生許多無用帳號。當營運到帳號量接近一個 DB Instance 的上限時會對後端很麻煩,可能要計劃是照定型化契約刪除無動作帳號。但又怕有玩家想回鍋撲空,或是像暴雪公告會依法刪除卻炎上。(雖然行政院 網路連線遊戲服務定型化契約應記載及不得記載事項 十八項:企業經營者得與消費者約定,若消費者逾___期間(不得少於一年)未登入使用本遊戲服務,企業經營者得定相當期限(不得少於十五日)通知消費者登入,如消費者屆期仍未登入使用,則企業經營者得終止本契約。,暴雪只是照抄但是因為逆風太久還是 燒起來

可能要有一套判斷帳號價值的方法,像是有無課金過之類的避開刪除到有可能長時間後復活的帳號。(Google 想要 刪除兩年未使用帳號,後來可能是被抗議到受不了改成 有上傳過 YouTube 影片不刪)我之前有提過一個想法是把無用帳號搬去冷儲存,如果有玩家真的想回鍋無法登入找客服,再從冷儲存還原。不過這個只是想法沒有實際跑過就是。

第三方登入

為了方便玩家,通常除了自己的帳號密碼系統會接入第三方登入,要確定遊戲帳號本身對登入方式是一對多,玩家有介面可以登記多種登入方式登入同一個遊戲帳號。要不然遇到像最近的 Twitter 不穩定與收費改動時無法引導去使用別的登入方式會很痛苦。

Twitter

在日本相當流行的第三方登入,但是在收購後發生數次 API 不穩定跟 API 收費的不確定性,相當讓人困擾。(現在第三方登入應該是不需要收費)另一點是 Twitter 手機 SDK TwitterKit 很早以前就 Archive 不更新了,現在在 iOS 上會因為沒從 UIWebView 升級到 WKWebView 被蘋果審查擋。要實作要自己用 WebView (UniWebView 之類的 Plugin)跑 網頁的 OAuth 流程,有點麻煩。

另外要注意一間公司只能有 一個 Twitter App,不能一個遊戲開一個。會被 Twitter 要求合併。

Facebook

Facebook 有提供 Unity SDK 算是好接很多,難的是在 Facebook App 申請審查來回很久,審查成功前只有測試使用者可以進行登入測試,而且每個遊戲要申請一次。如果想撈使用者的 Facebook 好友又要再過一次 user_friends 權限的審查。還有是 Facebook Graph API 有定期的 Expiration Date,在過期之前需要升級相應的 Unity SDK,Unity SDK 跟 Graph API 的對應可以在 Unity SDK 的 Changelog 找到。

另外 Facebook 跟 Twitter 相反 不能不用 SDK 登入,自己接 OAuth 被 Facebook 發現會被退。

Google

Android 用的 Google Play Games 跟 iOS 用的 Google Sign-in 是不同的 SDK,但是經實驗發現兩者拿到的使用者 ID Token 是相同的。如果要使用 Firebase 同時雙平台支援 Google 登入設定流程有點複雜,要安裝兩個 Unity Plugin 操作三個 Google 後台:

  • Firebase Console 開 Firebase 專案
  • 在 Firebase 增加一個 Web App
  • 等待對應的 Google Cloud Platform 專案生成
  • Google Play Console / Play Games Services / Setup and management / Configuration 下
    選擇 Yes, my game already uses Google APIs 然後選取剛剛產生的 Google Cloud Platform 專案
    這樣會創造 Play Games Services 並跟 Firebase 對應到的同一 Google Cloud Platform 專案連結
  • 最後在 Google Cloud Platform Console 選擇生成的 Google Cloud Platform 專案,進入 APIs & Serives / Credentials
  • 應該會看到 OAuth 2.0 Client IDs 有 Web client (auto created by Google Service)
  • 先完成 OAuth Consent Screen,選擇 External 後填入開發者資料,這裡的輸入會出現在玩家授權給你時的畫面,要清楚填寫玩家看得懂的字樣
  • 回到 Firebase 創造 Android App 與 iOS App,Android App 的 SHA-1 Fingerprint 要跟用來 Sign APK 或是 ABB 的 Keystore Fingerprint 一樣,否則會登入失敗(可以登錄一個以上的 Android App 分隔 Production Keystore 跟 QA 用 Keystore)
  • 在 Google Cloud Platform Console 看到有 (auto created by Google Service) 的 Web, Android, iOS client 就可以繼續照說明開始安裝設定 Unity SDK 了。注意雖然開了 Android 跟 iOS 的 OAuth Client ID,但是設定 Google Play Games 跟 Google Sign-in 時要求填入的都是 Web OAuth Client ID,且 Android 跟 iOS 的 OAuth Client ID 不能刪除

Google Play Games 可以不需要使用者輸入直接跑登入流程,算是對營運上相當好的特性,因為任何額外需要互動的步驟都可能造成玩家流失,可惜只有 Android 能用。Android 登入 Google Play Games 流程的示範可以參考 Supercell 的產品,像是皇室戰爭第一次開啟是直接進入教學同時執行背景的 Google Play Games 登入,如果能從 Google Play Games 撈到玩家資料即中斷教學跑登入流程,整套將玩家輸入壓到最低,非常漂亮。

Sign-in with Apple

Apple 的規定是如果有其他第三方登入就要實做 Sign-in with Apple(自己實做的 E-mail 登入不算第三方),關於審查細節已經在之前的 文章 寫過了,這邊就不再重複。

先前是用 lupidan/apple-signin-unity 實作的,現在 Unity 也有自己的 Unity Authentication 實作。要注意如果實作完每次登入都彈出 Sign-in with Apple 登入視窗,那實作是錯的,雖然審查通常不會被退。正確的流程是要先檢查登入狀態,如果是 Authorized 要走 Quick Login 避免再次彈出 Sign-in with Apple 登入視窗。

最佳化

遊戲最佳化是個大哉問,可能超過這篇文章該有的篇幅。之前有寫過一些關於最佳化的文章,現在算是過時了就不在這裡連結。覺得如果公司能接受,建議訂閱引入 UWA 的服務。他們提供持續性的效能測試與報告,對不管是否要持續營運的遊戲都很有幫助,理想上自動建置要加入一個有 UWA SDK 的分支定期與 UWA 檢討效能。

另外 UWA 上也有很多文章知乎)、課程 跟自己的 問答區,算是中文方面非官方最大的 Unity 資源。

錯誤回報

目前遠端追蹤玩家遊戲發生錯誤的方法還是 Unity 自己的 Cloud Diagnostics 最好,因為是 C# Call Stack。不管是不是營運類遊戲都推薦設定起來。Firebase Crashlytics 雖然號稱能支援 Unity,但是 Callstack 好像是用某種方法逆推回來的,參考價值有限。至於 Native 的 Crash Reporting 像是 Android vitalsApp Store Crash Reports 用途就很少了,因為沒有 Unity Source Code 難以判讀,判讀出來也只能回報不能修改。

Android Native Crash 有進階的 Symbolicate 技巧,可以恢復出 Native Call Stack 的函式名稱。不過一樣主要是用在向 Unity 回報用居多。

IAP

Unity IAP vs Ultimate Mobile Pro

目前最大宗使用的可能還是 Unity IAP,但是使用上要小心測試。有遇過幾次嚴重 Bug 導致 大量營收損失。如果用 Unity IAP 建議是除非有 Google 的 Billing Library 升級需求 否則不要動它。之前有替代方案 Ultimate Mobile Pro,用起來不錯又有附 Android 與 iOS Native 實作的原始碼。但是作者是基輔人,在俄烏戰爭開始後就停止更新了,目前 Plugin 已經 下架。先前是參考它的 Code 自己實作了一次,要自己維護 Billing 開銷不小,但是 Unity IAP 的穩定度跟更新速度堪憂是件很為難的事情。

Android

如果在測試 Android IAP 時無法刷過,可以參考這個 Stack Overflow 問題

Acknowledge

在 Unity IAP IStoreListener.ProcessPurchase 回傳PurchaseProcessingResult.Complete Unity 稱之為 Finish Transaction,底層 Google API 是發動叫做 Acknowledge 的行為,等於是跟 Google 說我跟玩家銀貨兩訖了。所以應該在發出虛擬商品後馬上呼叫,如果沒有的話依照 Google 的規則是三天後會無條件退款。上面提的 Unity IAP 大 Bug 其中之一就是 2.2.0 ~ 2.2.6 間有約五成的機率不會呼叫 acknowledgePurchase,當時的更新就損失了差不多五成的營收。自己在實作的時候要特別注意 Acknowledge,另外測試帳號沒有呼叫 Acknowledge 自動退款的時間是五分鐘。QA 時不用傻傻地等上三天。

Pending 與超商付款

Google Play Billing 2.0 加入了超商附款選項,連帶加入了 Pending 交易 的概念(Unity IAP 是在 2.2.0AIDL 換成 Billing v3 時引入了 Pending 交易)。直接地說是看到收據不代表玩家有付款,有可能交易還處在 Pending 狀態。這時候還不能給虛擬商品,要等玩家到超商付款完才能給獎。連帶的代表說交易完成需要 Finish / Acknowledge 的時間點可能在遊戲不在交易流程甚至遊戲沒有開啟的狀態下,所以要多設檢查點檢查有沒有付好錢的交易進來。這算是比較大的改動,如果沒有做 Pending 檢查會被下單但是沒有去超商付款的玩家吃霸王餐。這項功能沒有全球開放且 無法關閉If this method is not called, BillingClient instance creation fails.),測試上會比較麻煩,可以參考 Android 文件在測試環境 蓄意製造 Pending 狀態

建議團隊要找人熟讀 Google Billing Library 文件,了解 Acknowledge、Pending 之類的概念以備不時之需。

Roadmap

目前 Google 宣告 Billing Library 一年一更新兩年後淘汰,現在的 4.0 加入了 Multi-quantity purchases,因為不強制啟用。除了升級 AAR Library 之外就沒有做特別處理了。

在 Billing 3.0 後一年會出一次新版,每年 八月跟十一月 以前還是要持續了解 Google Play Billing Library 的新特性,跟 Unity IAP 更新進度與有沒有災情。

Apple

Apple 比較沒有像 Google 這麼多花招,除了訂閱好像沒有特別複雜的部份。不過訂閱我沒有處理過所以雙平台都沒有辦法給出任何建議。只是記得 iOS 的交易也需要 Finish 從 SKPaymentQueue 移除。目前看過對 iOS IAP 流程講解最好的是 WWDC 演講,但是 Apple 很莫名地把舊演講藏起來,需要用些 方法 挖出來。有幾場很舊的演講對釐清觀念很有幫助,有時間可以看一下:

熱更新

AssetBundle

Addressable

目前 Addressable 是 Unity 推薦的 AssetBundle 管理方案,但是有些特性不太適合用在營運類遊戲上。包含沒有辦法一鍵切換 Asset 版本都要靠手動上傳(遇到更新有問題需要 Rollback 會很痛苦),還有結構上有 Catelog.json 列舉了所有 AssetBundle 的網址,其實是對拆包玩家來說蠻方便的索引。

自己實做一個需要的功能

在 Addressable 出現之前有一些下載 AssetBundle 的工具存在,之前也替前公司維護過一個,如果要實作有些要注意的點:

  • 要下載到 Application.temporaryCachePath 不是 Application.persistentDataPath,在 iOS 上 Application.persistentDataPath 會被備份進 iCloud 占用玩家的 iCloud 空間,發生這種情況會被退審。但是下載到 Application.temporaryCachePath 有可能被清除,所以每次啟動都要檢查 AssetBundle 還在不在。
  • AssetBundle Build 是 Non-deterministic 的,意思是相同的 Commit 位置在不同的 Build Machine 上可能會 Build 出相容但是 Binary 不同的 AssetBundle。雖然不影響功能,但是會徒增下載容量,而下載量越高玩家越容易在需要更新時流失。解決方向有複製 Library 資料夾到所有機器上與計算 Source Hash。之前是做 Source Hash 計算,但是看起來國外比較多是跨機器同步 Library 資料夾(參考 File System Tricks for Safe Incremental BuildsThe Legends of Runeterra CI/CD Pipeline)。
    Source Hash 即計算 AssetBundle Input 檔案的總和 Hash,Input 可能包含:
    • Asset
    • .meta
    • AssetBundle Name (AssetBundle Name 改變就無法跨 AssetBundle derefrerence)
    • Scripts (Scripts 裡面可能有 AssetImporter 會改變 Import 結果)
    • Sprite Atlas 設定
      比對時先比對 Source Hash 再比對 Binary Hash,兩者皆不同時才替換 AssetBundle,以壓低下載量。
  • 提供進退版的後台方便營運處理事故,或是對內方便 QA。

程式

程式熱更新可惜我沒有實作過,常見的有 Lua。另外有一派 ILRuntime 的可惜我都止於閱讀過資料的階段。想推薦在中國 UWA 上的討論跟測試:

多國語言

除非非常確定只做一種語言(不太可能),否則多國語言的結構都要在開始就做進遊戲裡,像是從程式碼裡輸出字串給螢幕顯示這種事情就不能做。另外語言資料需要熱更新,所以也不建議使用 I2 Localization。尤其不要想去用 I2 的自動讀取 Google Sheet 功能 實現多國語言熱更新,一般正常的流量就會頂到 Google Sheet 的 Rate Limit 啟動不了。建議可以自己實作一個從網路上更新多國語言 Dictionary 的 Localization Library,如果真的不想自己來想要用 I2 Localization 的 Component,也是可以修改 I2 的程式將自己下載多國語言字串塞入 I2 Localization 的資料結構。

字體

最保險的還是 Noto Sans CJK 系列,因為無法預期玩家會輸入什麼。但是要注意 CJK Glyph 不同的問題(GSUB),用 TextMesh Pro 可能需要下載 Noto San CJK 的 .ttf (OTC) 檔案,然後創造不同的 Font Family 的 TextMesh Pro Asset 來應對,至於舊的 uGUI Text 則是 沒有應對方式,只能重複包字體。

日文使用者會對字體誤用特別敏感,這是 Google IO 2023 介紹 Bard 時發生的誤用

`NotoSansCJKjp` 預設的「語」 Glyph 注意上面言部是 橫 的

`NotoSansCJKtc` 預設的「語」 Glyph 注意上面言部是 豎 的

`NotoSansTC` 言部是豎的,但不包含橫的 Glyph

可以看到 NotoSansTC 不包含其他 CJK 的 Glyph,NotoSansCJKjpNotoSansCJKtc Glyph 量相同但是預設不同。

使用同一個 .ttf (OTC) 創造兩個 Font Face 不同的 TMP_Font Asset

TextMesh Pro 3.2.0-pre.4 之後對 .ttf (OTC) TMP_Font Asset 可以選擇 Font Face,用一套字體創造多個 Font Asset 應對不同語系該有的顯示。

對這個議題有興趣研究,關鍵字是 中日韓統一表意文字Han unification

使用其他的字體要看字體支援的廣度,還有版權,Noto 好在版權開放,其他字體使用時授權如果不小心可能會吃官司。

另外有個問題是 iOS 13 之後 Unity 走系統字可能無法正確顯示泰文,Issue 與解釋在 這裡。這也只能靠使用 Noto 替代系統字來解決。

語系問題

先前有提到資料數值盡量不要用文字儲存再 Parse,如果真的非做不可在多國語言下會有坑要注意。

浮點數的點不一定是 .

有些語言像是法文、德文、印尼文,浮點數的點是 , 不是 .,如果資料儲存成像是 "3.14" 在預設為這類語系下會 Parse 失敗,可以做以下實驗:

1
2
NumberFormatInfo format = new CultureInfo( "fr-FR", false ).NumberFormat;
float.Parse("3.14", format);

結果會是

1
2
3
[System.FormatException: Input string was not in a correct format.]
   at System.Number.ParseSingle(String value, NumberStyles options, NumberFormatInfo numfmt)
   at System.Single.Parse(String s, IFormatProvider provider)

或是可以看神魔之塔的 案例

可以設定 AppDomain 或是 Thread 的 Culture 或是 NumberDecimalSeparator 來繞過:

1
2
3
4
5
6
7
8
// AppDomain
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture;

// Thread
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
// 設定小數點
Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator = ".";

不過這些改動範圍都蠻廣的,或是浮點數轉換都要記得加 System.Globalization.CultureInfo.InvariantCulture 參數:

1
2
float.Parse("3.14", System.Globalization.CultureInfo.InvariantCulture)
System.Convert.ToSingle(3.14, System.Globalization.CultureInfo.InvariantCulture);

建議還是治本不要從字串 Parse 出浮點數的設定。

參考資料:

“I”.ToLower() 不一定是 “i”

土耳其文有兩個小寫 i,當在做 ToLower() 時會是大家比較不熟悉、上面沒有點的 ı (U+0131)

1
2
3
4
Thread.CurrentThread.CurrentCulture = new CultureInfo("tr-TR");

Console.WriteLine("I".ToLower());
Console.WriteLine("I".ToLowerInvariant());

結果

1
2
ı
i

這問題可能會在拿 enum ToString().ToLower() 之後跟字串做比對踩中。

1
2
3
4
5
6
7
enum UIType{
    CONFIG,
    ...
}

Thread.CurrentThread.CurrentCulture = new CultureInfo("tr-TR");
Console.WriteLine(UIType.CONFIG.ToString().ToLower() == "config");

結果

1
False

也是不建議做類似操作,或者也只能記得使用 ToLowerInvariant()

參考資料:

DateTime.ToString() Crash

在預設 Unity 環境下 DateTime.ToString() 有可能會 Crash,因為 DateTime.ToString() 需要當下語系的日曆類別,但是通常沒有明確使用的日曆類別會被建置過程 Strip 掉。這問題容易在泰文區遇到,因為泰文預設日曆是佛曆 System.Globalization.ThaiBuddhistCalendar。解決方法是參考 Xamarin 的 討論,在不會被呼叫到的地方加入:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
if (Environment.CurrentDirectory == "_never_POSSIBLE_")
{
    new System.Globalization.ChineseLunisolarCalendar();
    new System.Globalization.HebrewCalendar();
    new System.Globalization.HijriCalendar();
    new System.Globalization.JapaneseCalendar();
    new System.Globalization.JapaneseLunisolarCalendar();
    new System.Globalization.KoreanCalendar();
    new System.Globalization.KoreanLunisolarCalendar();
    new System.Globalization.PersianCalendar();
    new System.Globalization.TaiwanCalendar();
    new System.Globalization.TaiwanLunisolarCalendar();
    new System.Globalization.ThaiBuddhistCalendar();
    new System.Globalization.UmAlQuraCalendar();
}

文化忌諱

CJK Glyph 顯示

上面已經提過了日文使用者對於誤用字體會很反感,同時他們也對斷字、換行之類的要求偏高。如果有日文 Localization 的需求,建議還是找專業的人士校對。

另外常見被抱怨的是日文與簡體中文的標點符號,也是 CJK 的問題。全形逗號 , U+3001 Ideographic Comma 跟全形句號 。 U+3002 Ideographic Full Stop 一樣有上面的 GSUB 問題要小心處理。

全形句號位置的差別

旭日旗

普通像是納粹符號之類的大家都知道,會去避諱。但是不時會看到有人會無意畫出類似 旭日旗 的圖像,引發韓國人與中國人的反感。韓國人反應尤其敏感,要特別小心。像是 Deemo 曲圖曾經 踩過,後來要作出修正跟道歉。或是像鬼滅之刃的 例子,Street Fighter II 重製的修改:

Analytics

資料分析對遊戲營運也是相當重要的一環,不過我做前端熟悉的只有幫忙埋點而已。埋點以後需要做測試,因為大部分的 Analytics Library 都會 Cache 事件,讓事件累積後再發出節約資源。所以要熟悉各個 Analytics Library 在測試環境強制 Flush 的方法,像是 Unity Analytics 的 Analytics.FlushEvents、Firebase 的 Debug Mode、Adjust 的 Sandbox Mode

資料管理

資料管理算是對營運非常重要的議題,放在這麼後面談單純是因為我沒有什麼好的解法,現有的方案包含常見的 Google Sheet 跟 Excel 都能在製作單機遊戲時發揮很好的功效。但是營運遊戲,資料開始有分「版本」後,就找不太到大家都滿意的方案了。可能線上有線上的數值資料、臨時 Hotfix 時會有 Hotfix 的修改項目,要怎麼合併回開發中的分支才不會下次又漏了 Hotfix 的內容。如果上線資料跟開發資料放在一起,那要怎樣保證開發中的資料或是測試資料不會流到 Production。有相當多的問題,也有相當多的解法,像是在資料上標註日期,輸出時依照日期過濾。但都不理想。很核心的兩點:

  • 類似 Excel 的公式系統
  • Diff 與合併兩份資料的能力

還沒有看到兩者都具備的方案,有為了 Diff 與合併研究到 Jetbrains MPS,但是要在裡面客製化出類似 Sheet 的介面與公式系統功太大了,最後只能放棄。

自己為了尋找好的解決方法找了許多資料:

但到現在還是沒有甚麼頭緒,歡迎大家如果有想法一起分享。

未竟之事

雖然洋洋灑灑寫了一堆,但還是有些想提的因為自己對後端的知識不足無法給出具體建議,簡單條列像是:

  • 後端後台
    • 測試用,要怎麼方便 QA 解鎖資源、跳關,還有 偏移 Server 時間 實測未來期間限定的活動。
    • 客服用,方便客服服務玩家、綁定或尋回帳號也可能要 Ban 玩家。
    • 資料分析後台,Analytics 收集回來的資料要怎麼呈現、呈現給誰看、誰有權限看、是否即時呈現。
  • 如何進入停機維修的流程,還在玩的玩家要如何處理,維修中要如何阻止玩家嘗試登入。
  • 廣告 Attribution,資料分析要如何跟廣告投放結合,算出各個管道廣告的效益。
  • CDN,如何佈署讓各個區域的流量費用都盡量低,尤其要注意如果不小心跨過中國境內境外費用會很高。
  • 發生營運事故的 SOP,輪值的後端要如何分配,要如何指定現場指揮官,事後要如何檢討。

希望有對營運、後端熟的朋友也能一起分享,讓大家開發都更加順利。

鳴謝

感謝 于修Johnson Lin低分少年、Topy 頭皮、Julia、Kannushi Link、DK Liao 與 Kiloyd 的校閱。