哈囉,大家好。
試著以我理解的方式來介紹一下,
Unity 內建提供,好用的存檔機制 PlayerPrefs。
相信很多已經不是初學者的人,應該都已經不陌生了,
居然連這樣的文章也想要騙一篇創作之類的XDDDD
請壓抑著想吐槽的心情看下去吧:)
我在學習一個新事物的時候,會先試著了解它,認識它,
這麼一來就不會因為太多未知,而感到恐懼。
那麼我們先來好好認識一下這個機制。
PlayerPrefs 這個 Class ,提供了一些靜態的方法,所以我們可以在任何的地方使用它。
乍看之下,它提供了我們三種型態的資料存取,整數、浮點數跟字串。
存取的原理其實是簡單的 dictionary 字典運用,使用一個 key 去查找對應的 value。
實際上它可以做一個簡單的應用,達到更多方便的效果。
舉例來說,我們可以寫一個 class,叫做 MyPlayerPrefs,然後再新增以下幾個靜態方法。
public static bool CheckValue(string key)
{
// 需要先定義好,1 為 true,0 為 false;相反也可以,看個人習慣。
return PlayerPrefs.GetInt(key, 0) == 1;
}
public static Point GetStartPoint()
{
// GetInt("Key", DefaultValue), 另外兩個 Get 也可以帶預設值,如果查不到會回傳預設。
return new Point( PlayerPrefs.GetInt("PointX", 0), PlayerPrefs.GetInt("PointY", 0));
}
public static Vector3 GetStartPosition()
{
return new Vector3( PlayerPrefs.GetInt("PosX", 0), PlayerPrefs.GetInt("PosY", 0),
PlayerPrefs.GetInt("PosZ", 0));
}
剛剛說的乍看之下,就是這個意思。
其實我們是能夠擴充 bool, point, vector3......等等,你想得到的資料型態的。
(分別使用 MyPlayerPrefs.CheckValue(), MyPlayerPrefs.GetStartPoint(), MyPlayerPrefs.GetStartPosition())
既然有 Get,Set 就自然而然可以用同樣方式撰寫。
再進一步說,其實有字串資料型態,基本上就可以實現各式各樣的變化了。
像是我把 "Ch-3-2-1" 存進名為 "CurrentLevel" 的 key 裡面。
取出來時做些處理,利用字串分割 Split
string[] array = PlayerPrefs.GetString("CurrentLevel").Split('-');
//array[0] = "CH";
//array[1] = "3";
//array[2] = "2";
//array[3] = "1";
這樣我們就能得知,玩家當前關卡進度在第 3 章第 2 關,然後難度是 1 (簡單)。
而剛剛展示的 bool 的延伸,把它變成 2 進制的資料來使用,
就可以實現資料庫那些超方便的紀錄方法,例如"000"~"111" 8 種變化。
000 - 玩家當前無狀態
001 - 玩家中毒了
010 - 玩家麻痺了
100 - 玩家被魅惑了
011 - 玩家中毒又麻痺了
101 - 玩家中毒又被魅惑了
110 - 玩家麻痺又被魅惑了(魅你麻痺!)
111 - 玩家全部負面狀態都中了
相信看了這些衍伸應用,你應該也能找到適合自己的擴充。
----------------------------------------------------------------------------------------------------
接下來,我們來看些比較內層的東西。
開頭說的,這是一個簡單的存檔機制,那麼檔案是存在哪裡?
我先說,Unity 是一個跨平台的開發引擎,所以每個平台對應的存檔位置是不一樣的。
在 Windows 作業系統中,它其實是存在登錄檔裡面的。
( 之前若有碰過要處理安裝程式 / 流程這塊的朋友,相信應該對這不陌生。 )
你可以透過 Cmd (命令提示字元),然後打上 regedit 後找到它(登錄編輯程式)。
存在兩個位置,一個是
電腦\HKEY_CURRENT_USER\Software\Unity\UnityEditor\DefaultCompany\ProjectName
=> 這個是你平常用 Editor 上測試時,會寫入的地方。
另一個是 電腦\HKEY_CURRENT_USER\Software\DefaultCompany\ProjectName
=> 這個是你玩這個遊戲,或別人玩你的遊戲,會存在他電腦的地方。
( 要修改就要改這個才有用,喂!別亂教。 )
我這邊依照整數、浮點數與字串,個別存入一些值做測試。
字串被記錄下來的樣子與位置。
整數被記錄下來的樣子跟位置。
浮點數被記錄下來的樣子與位置。......嗯?
其實只是因為登錄檔的資料類型的顯示沒有正確對應到而已,是顯示問題。
( 應該使用 64-bit 的 QWORD,就會看到正確的顯示。 )
不要擔心,你存的數值有幾乎正確的被記錄下來的。
這個幾乎正確,才是浮點數這邊要提的重點。
浮點數是個有誤差值的資料型態,使用上需要特別注意。
相信有不少朋友應該都有踩過浮點數的坑,
當你今天真的有需求需要紀錄某個 % 數,某個需要用到小數點的資料時,
可以考慮用別的方式替代處理。
public static float GetCurrentHpRatio()
{
// 取當前血量 % 數時,改用另外兩個值去紀錄
return PlayerPrefs.GetInt("CurrentHp", 0) / PlayerPrefs.GetInt("CurrentMaxHp", 1);
}
=> 這邊注意我的除數,我的預設是填 1,因為除以 0 會有致命錯誤,
當然也要特別小心在存這個值的時後,別把 0 寫進去了!
public static float GetSSRchance()
{
// 機率 0.001 可以記錄成1; 0.123 可以記錄成 123,取值時再去除以百或千就好。
int value = int.Parse(PlayerPrefs.GetString("SSR_change", 0));
return (float)value / 1000;
}
有很多方式可以替代,如果你真的還是想要用,那就拿去用也不是什麼大問題。
基本上你的值都會被正確紀錄到的,存取也都會是正常的,
但就是某些場合要特別小心就是了。
----------------------------------------------------------------------------------------------------
那麼我們也知道了它們實際上存在哪裡了,還有什麼需要注意的嗎?
這樣一個登錄檔,作為 Unity 在這個應用程式的暫存,是有個限制的。
它的大小最多只能存到 1 M。
初始什麼都沒有,只記錄一些必要的紀錄檔的情況下,是不到 1000 位元組( 連 1 KB都不到 )。
但每紀錄 100 個字元多 0.5 KB,每多一個索引紀錄多 0.1 KB。
也就是說都以字串來記錄使用的話,可以記下約 20 萬個字元。
以索引來使用的話,可以使用 100 萬個紀錄空值的索引。
在這個區間內要怎麼去調整使用,就看每個人的使用方式了。
這樣的紀錄量夠不夠使用,基本上是,見仁見智的。
應用上來看,以一款完整的遊戲來說,存檔機制是不可或缺的。
但以小品遊戲、game jam 來說,不是必要,但絕對錦上添花。
說故事時間:
我舉 kuso game jam 一個有趣的作品為例,叫做「是男人就撐 60 秒」!
這個遊戲很單純,它會用很多逗趣的圖片,引人入勝的劇情,劇情的斷點讓你非常想往下看。
但是每個操作都必須等待 60 秒,你才能跟遊戲中按鈕互動,
不然就 game over,想重新遊戲亦同,也要等那個 60 秒。
撇開它就是主打想惡整玩家來說,其實它的圖片跟故事真的頗有意思的XD
而且是真的能破關喔,有結局喔!
但我想應該很少人會去看到最後。
這個時候!這個時候!
如果我們使用 PlayerPrefs 加入了對每一段劇情的達成與否做個紀錄,
如果這段玩家已經耐著性子看過了,那就無視那 60 秒等待,可以點到下一個。
如此一來,這個遊戲的細節會讓人非常玩味,也會讓撐到最後的玩家獲得成就感。
( 雖然依舊會氣得牙癢癢的。 )
例子太多,其實舉是舉不完,小型成就的應用也可以。
打一隻波莉,kill_count++; PlayerPrefs.SetInt( "KillPoly", kill_count );
因為這個值只要不清除,它是會一直紀錄住的!
在玩家玩了好幾次之後,一但殺超過 100 隻波莉,波莉王就會出現這樣的小彩蛋。
解謎遊戲,玩家繞圈或者一直摸尚未觸發的機關,做個紀錄。
第一層機關們都檢查著這個數值,一但玩家已經陷入迷路狀態時,
亂點亂敲,數值就 +1,當這個值超過 10 次( 玩家崩潰邊緣 ),機關發光表示提示。
......等等等,這些不加依舊不影響遊戲本身好玩,但加了肯定是會讓玩家會心一笑的細節。
說在最後:
1. 存檔
PlayerPrefs 在使用上你可能會疏忽的部分。
當你只是單純在使用 Get 系列, Set 系列時,其實它在你遊戲跑的那個生命週期,
是正常紀錄並執行的。
不過一旦你關掉遊戲,離開遊戲模式回到編輯模式,值是不會有變化的。
要記得 PlayerPrefs.Save() 去做儲存才有用。
基本上可以每寫一次就存一次,但這樣其實有點小吃效能。( 因為 IO 開銷 )
假設是設定類型的,可以在開關設定介面時,做存檔。
找一個存檔時機給它。
另外也有一個應用是寫在 destroy 的時後,幫你 save。
看最適合你的情況使用,記得存檔就是了。
2. 安全性
上面其實已經小小提到一下,基本上任何的紀錄方式,都是危險的。
透過網路把資料存在遊戲主機的資料庫裡,可能是相對安全一點。
但以單機遊戲來說,任何存檔的格式被破解什麼,只是難易度的差別而已。
當然也是有一些手段,混淆,加密,壓縮後加密再簽章......等,
我自己是覺得道高一尺,魔高一丈啦ˊˋ
實在是防不勝防,都可以額外開另一篇文來講這個議題了。
主要是先說明,這個儲存的方式,要是紀錄什麼重要的資訊,
必須要有這是會被輕易修改的心理準備,
當然,也會有其它更方便較安全的存檔方法,我們留在下一篇討論。
3. 存檔的需求確立
遊戲是否用到了大量的數值,例如 50 個屬性與能力,
這樣的記錄方式就挺辛苦的,不必要勉強使用 50 幾個 SetInt 跟 GetInt 去維護。
換一個存讀檔的方式會更好。
資料是否有區分,帳號型資料,跟設定資料。
不用想成連網的遊戲,舉例來說,一個遊戲提供 3 個紀錄格。
那就表示那是 Save1、Save2 跟 Save3,三個帳號;
背景音樂、音效開關、鏡頭遠近,這些算是設定資料。
設定資料使用 PlayerPrefs 紀錄是沒什麼問題的,
帳號資料當然有字串去分隔不是不能處理,只是不方便到令人痛苦......
先了解需求,再去使用;就像開頭說的,我們先認識、學習這個東西後,
我們就比較清楚再那些場合可以使用了。
以上,謝謝收看。
開個玩笑,
最近公會的收錄區出現了小異狀,
可能是我最近都在畫畫,感覺不務正業,
我已經從程式被除名了,嚇得我趕快發一篇程式相關技術文壓壓驚XDDDD
放著放著就轉職,感覺真有趣?!
看到很多認識的人突然轉職,真是令人莞爾。XDDDDDD