目錄數組 |
1 保存隨機性微信 1.1 記錄隨機狀態app 1.2 讀取隨機狀態dom 1.3 JSON序列化編輯器 1.4 解耦關卡測試 1.5 兩種方式都支持flex 2 持久化關卡數據url 2.2 存儲遊戲關卡spa 2.3 加載關卡數據.net 2.4 緩衝數據 3 關卡狀態 3.1 序列化符合生成區 3.2 記住下一個索引 3.3 追蹤持久對象 3.4 爲新遊戲從新加載 3.5 旋轉對象 4 建立和釋放 4.1 保存和加載 4.2 確切時間 4.3 速度設置 4.4 更新文本標籤 |
本文重點:
一、追蹤隨機性
二、保存關卡數據
三、在生成區作循環
四、建立旋轉的關卡對象
這是關於對象管理的系列教程中的第六篇。除了生成形狀和關卡索引以外,它還包括保存更多遊戲狀態。
本教程使用Unity 2017.4.4f1編寫。
(可重複生成的隨機形狀)
1 保存隨機性
當生成形狀時使用隨機性的重點是會獲得不可預知的結果,但這不必定是咱們想要的。假設你先保存了遊戲,又再生成了一些形狀。而後,再次加載遊戲並從新生成剛纔同樣多的形狀。那麼你會獲得徹底相同的形狀呢,仍是不一樣的呢?就目前而言,你會獲得不一樣的。但若是想讓兩次生成的形狀徹底一致,咱們也是能夠支持的。
由Unity的隨機方法生成的數字並非真正隨機的,是僞隨機。它是由數學公式生成的一串數字。在遊戲開始時,這個序列會根據當前時間用一個任意的種子值初始化。若是你使用相同的種子開始一個新的序列,你將獲得徹底相同的數字。
1.1 記錄隨機狀態
只存儲初始種子值是不夠的,由於這將把咱們帶回到序列的開始,而不是遊戲被保存時序列中的點。可是Random必須跟蹤它在序列中的位置。若是咱們能到達這個狀態,那麼咱們能夠稍後恢復它,以繼續舊的序列。
隨機狀態定義爲一個狀態結構,嵌套在隨機類中。因此咱們能夠聲明Random.State這種類型的字段或參數。爲了保存它,咱們必須向GameDataWriter添加一個能夠寫入這樣一個值的方法。如今添加這個方法,但將它的實現留到以後。
經過這種方法,咱們能夠保存遊戲的隨機狀態。讓咱們在Game.Save一開始的時就作這個操做。而後,在寫下形狀計數後當即保存。一樣,增長保存版本號以表示新的格式。
1.2 讀取隨機狀態
若要讀取隨機狀態,請向GameDataReader添加ReadRandomState方法。因爲咱們還沒有編寫任何內容,所以暫時不閱讀任何內容。取而代之,咱們返回當前的隨機狀態,所以,實際上沒有任何變化。當前狀態能夠經過靜態Random.state屬性找到。
隨機狀態的設置是經過相同的屬性完成的,咱們會在Game.Load中作,但僅用於保存文件版本爲3或更高的時候。
1.3 JSON序列化
Random.State包含四個浮點數。可是,它們不能公開訪問,所以咱們不可能簡單地寫入它們。必須使用一些間接方法。
幸運的是,Random.State是可序列化的類型,所以可使用Unity的JsonUtility類的ToJson方法將其轉換爲相同數據的字符串表示形式。咱們會獲得一個JSON字符串。要查看它的內容的話,請將其記錄到控制檯。
Json是什麼意思?
正確的拼寫是JSON,全部字母均爲大寫。它表明JavaScript對象表示法。它定義了一種簡單的人類可讀數據格式。
保存遊戲後,控制檯如今將在大括號之間記錄一個字符串,該字符串包含四個從s0到s3的數字。相似於{「 s0」:-1409360059,「 s1」:1814992068,「 s2」:-772955632,「 s3」:1503742856}。
咱們將此字符串寫入文件。若是使用文本編輯器打開保存的文件的話,則能夠在文件開頭附近看到此字符串。
一樣,在ReadRandomState中,經過調用ReadString讀取此字符串,而後使用JsonUtility.FromJson將其轉換回適當的隨機狀態。
除了數據以外,FromJson還須要知道應該從JSON數據建立的何種類型。咱們可使用該方法的通用版本,指定應建立一個Random.State值。
1.4 解耦關卡
咱們的遊戲如今有保存和恢復隨機狀態的能力了。你能夠經過開始一個遊戲,保存,以後再建立一些形狀,而後加載,它再次建立徹底相同的形狀。但你能夠更進一步。甚至能夠在加載後開始一個新遊戲,而且在那以後仍然建立相同的形狀。因此咱們是能夠經過在一個新遊戲開始以前,先加載一個狀態來影響它的隨機性,但這是不太好的實現方式。理想狀況下,不一樣遊戲的隨機性應該是獨立的,就好像咱們從新啓動了整個遊戲同樣。但咱們能夠經過每次開始一個新遊戲時指定一個新的隨機種子來實現這一點。
要選擇一個新的種子值,咱們必須使用隨機性。能夠用Random.value,但必須確保這些值來自它們本身的隨機序列。爲此,在遊戲中添加一個主隨機狀態字段。在遊戲開始時,將其設置爲由Unity初始化的隨機狀態。
當玩家開始一個新遊戲時,第一步就是恢復主隨機狀態。而後獲取一個隨機值並使用它做爲種子,在InitState方法裏,經過random初始化一個新的僞隨機序列。
爲了使種子更加不可預測,咱們將它們與當前播放時間混合在一塊兒,能夠經過Time.unscaledTime訪問。按位異或運算符^會是很好的方式。
異或的做用是什麼?
對於每一個位,若是兩個輸入1個是1,1個是0的話,則結果爲1,不一樣則結果爲0。換句話說,就是看輸入是否不一樣。由於是位操做,結果在數學上並不明顯,就像加法同樣,只是不帶進位。
爲了跟蹤主要隨機序列的進展,請在獲取下一個值後存儲狀態,而後再爲新遊戲初始化狀態。
如今正在加載遊戲,而且你在每一個遊戲中所作的事情再也不影響同一會話中其餘遊戲的隨機性。可是要確保此方法正確運行,咱們還必須爲每一個會話的第一個遊戲調用BeginNewGame。
1.5 兩種方式都支持
固然,你也有可能不但願使用可重現的隨機性,而是但願在加載後得到新結果。所以,經過向Game添加一個reseedOnLoad切換開關,來支持這兩種方法。
(控制是否須要從新生成種子)
咱們須要更改的只是加載遊戲時是否須要從新設置隨機狀態。因此能夠繼續保存和加載它,也所以保存文件能夠始終支持這兩個選項。
2 持久化關卡數據
咱們能夠保存遊戲中產生的形狀,能夠保存正在玩的關卡,還能夠保存隨機狀態。固然咱們也可使用相同的方法來保存可比較的數據,例如產生和破壞了多少個形狀,或者在播放時能夠建立的其餘東西。可是,若是咱們想保存關卡中某些內容的狀態怎麼辦?假如在關卡場景中放了些物體,可是在遊玩的過程當中它們會發生變化嗎?爲了支持這一點,咱們也必須保存關卡的狀態。
2.1 使用當前關卡取代Game單例
爲了保存關卡,遊戲必須在保存時包含它。這意味着它必須以某種方式得到對當前關卡的引用。咱們能夠在Game中添加一個屬性,併爲已加載的關卡分配本身的屬性,可是接下來,咱們將有關關卡的兩個相關聯的事物直接放在Game內部:關卡自己及其生成區域。這多是一種有效的方法,但讓咱們轉換一下。沒必要依賴Game單例,而是能夠全局訪問當前關卡。
將靜態Current屬性添加到GameLevel。每一個人均可以獲取當前關卡,可是隻有關卡自己才能夠設置它,在OnEnabled裏執行此操做。
如今,無需設置遊戲的生成點,關卡就能夠公開其生成點供遊戲使用。實際上,咱們能夠更進一步,讓GameLevel直接提供SpawnPoint屬性,將請求轉發到其生成區域。所以,該關卡充當其生成區域的門面。
這意味着遊戲再也不須要了解生成區域。它只是須要當前關卡。
(Game只知道當前的關卡)
此時,GameLevel再也不須要引用Game。因爲靜態實例未在其餘任何地方使用,所以將其刪除。
不使用Game.Instance了,咱們不能保留它嗎?
能夠,可是在項目中留下被稱爲死代碼的未使用代碼會使維護更加困難。如今是比較簡單的代碼,若是咱們在未來須要它,咱們只需再次添加它便可。
2.2 存儲遊戲關卡
爲了能夠保存關卡,請將其設置爲PersistableObject。關卡對象自己的transform數據沒有用,所以請覆蓋Save和Load方法,以使它們暫時不執行任何操做。
在Game.Save中,有意義的是在玩遊戲時建立的全部內容以前寫入關卡數據。讓咱們將其放在關卡構建索引以後。
2.3 加載關卡數據
加載時,咱們如今必須在讀取關卡構建索引以後讀取關卡數據。可是,只有在加載了關卡場景以後才能這樣作,不然咱們會將其應用於將要卸載的關卡場景。所以,須要推遲讀取其他的保存文件,直到LoadLevel協程完成爲止。爲了實現這一點,讓咱們將整個加載過程變成協程。
確認支持保存版本後,啓動新的LoadGame協程,而後結束Game.Load。在此以後使用的代碼將成爲新的LoadGame協程方法,該方法須要 reader 做爲參數。
在LoadGame中,在LoadLevel上產生收益,而不是調用StartCoroutine。以後咱們能夠調用gamelev.current。加載,固然,是須要咱們在版本3或更高的文件的狀況下。
幸的是,咱們在嘗試加載遊戲時會出現錯誤。
2.4 緩衝數據
咱們獲得的錯誤告訴咱們咱們正在嘗試從一個封閉的BinaryReader實例中讀取。因爲PersistentStorage.Load中的using塊而被關閉。它保證了該方法調用完成後,咱們對文件的保留將被釋放。咱們如今試圖稍後經過協程讀取關卡數據,所以它失敗了。
有兩種方法能夠解決此問題。首先是取消using塊,稍後經過顯式關閉閱讀器來手動釋放對保存文件的保留。這要求咱們更當心追蹤是否要持有Reader並確保將其關閉,即便咱們在途中遇到錯誤也是如此。第二種方法是一次性讀取整個文件,對其進行緩衝,而後再從緩衝區中讀取。這意味着咱們沒必要擔憂釋放文件,而只須要將其所有內容存儲在內存中一段時間??。因爲咱們的保存文件很小,所以咱們將使用緩衝區的方法。
能夠經過調用file來讀取整個文件。ReadAllBytes,它給咱們一個字節數組。這將是咱們在PersistentStorage.Load中的新方法。
咱們仍然必須使用BinaryReader,它須要一個流,而不是一個數組。咱們能夠建立一個包裝數組的MemoryStream實例,並將其提供給讀取器。而後,像之前同樣加載GameDataReader。
3 關卡狀態
咱們已經能夠保存關卡數據,可是目前咱們尚未什麼可存儲的。所以,先搞一些要保存的東西。
3.1 序列化符合生成區
到目前爲止,咱們擁有的最複雜的關卡結構是複合生成區域。它具備一組生成區域,每次須要新的生成點時都會使用一個元素。在實際操做中,你沒法預測下一個使用的區域。形狀的放置也是任意的,不須要統一,但從長遠來看,它將平均分佈在全部區域中。
(隨機生成區)
咱們能夠經過依次遍歷生成區域來更改此設置。兩種方法都是可行的,所以咱們將同時支持這兩種方法。向CompositeSpawnZone添加一個切換選項。
(順序複合生成區)
順序生成須要咱們跟蹤下一步必須使用哪一個區域索引。所以,若是咱們處於順序模式,則添加一個nextSequentialIndex字段並將其用於SpawnPoint中的索引。以後增長字段。
爲了使其循環,當咱們通過數組的末尾時,跳回到第一個索引。
順序生成區的行爲與隨機生成區明顯不一樣。儘管它們在每一個區域中的位置仍然是隨機的,但其生成模式清晰,形狀在區域之間均勻分佈。
(順序生成)
3.2 記住下一個索引
保存遊戲時,如今必須保存順序複合生成區域的狀態,不然序列將在加載後重置。所以,它必須成爲可持久的對象。它已經繼承了SpawnZone,所以咱們必須使SpawnZone繼承自PersistableObject。這使得全部生成區域類型均可以保留其狀態。
只需編寫和讀取nextSequentialIndex,便可覆蓋CompositeSpawnZone中的Save和Load方法。不管區域是連續的仍是無序的,咱們都會這樣作。咱們還能夠調用基本方法,以保存區域的transform數據,但如今咱們僅關注序列。該區域不會自行移動。
3.3 追蹤持久對象
生成區域如今能夠持久保存,但還沒有保存。GameLevel必須調用其Save和Load方法。咱們能夠簡單地使用spawnZone字段,可是隻容許保存一個生成區域。若是咱們想將多個順序的生成區域放置在一個關卡(複合區域層次結構的全部部分)中,該怎麼辦?
咱們可使複合區域負責保存和加載它包含的全部區域,可是若是咱們在應該保存的關卡上添加其餘內容,該怎麼辦?爲了使其儘量靈活,讓咱們添加一種方法來配置保存關卡時應該保留的對象。最簡單的方法是向GameLevel添加一系列的持久對象,咱們能夠在設計關卡場景時進行填充。
如今GameLevel能夠保存不少這樣的物體,而後保存每一個物體,就像Game爲它的形狀列表所作的那樣。
加載過程也是如此,可是因爲關卡對象是場景的一部分,所以無需實例化任何內容。
請注意,從如今開始,你必須確保放入該數組的內容保持在同一索引下,不然將破壞與較早保存文件的向後兼容性。可是,你未來能夠添加更多內容。加載舊文件時,這些新對象將被跳過,保留它們在場景中的保存方式。
另外一個重要的點是,咱們全部場景中的GameLevel實例都沒有自動得到新的數組。你必須打開並保存全部關卡場景,不然在加載關卡時可能會出現空引用異常。另外,咱們能夠檢查在播放中啓用關卡對象時是否存在數組。若是沒有,請建立一個。若是有多個關卡,這是一種更方便的方法,若是有第三方爲你的遊戲建立了你也但願支持的關卡,則這是惟一的選擇。
如今,咱們能夠經過將順序組合生成區域顯式添加到關卡的持久對象中來最終保存它。
(Level3)
3.4 爲新遊戲從新加載
如今,在加載關卡時,序列索引會恢復,可是當玩家在同一關卡中開始新遊戲時,它目前不會重置。解決方案是在這種狀況下也加載關卡,從而重置整個關卡狀態。
3.5 旋轉對象
讓咱們添加另外一種也必須存儲狀態的關卡對象。一個簡單的旋轉對象。這是一個具備可配置角速度的持久對象。使用3D向量,所以速度能夠沿任何方向。要使其旋轉,請給它提供一個Update方法,該方法調用其轉換的Rotate方法,並使用由時間增量縮放的速度做爲參數。
爲了演示旋轉的對象,我建立了第四個場景。在其中,有一個根對象繞Y軸以90的速度旋轉。它的惟一子對象是另外一個繞X軸以15的速度旋轉的對象。更深一層的位置是一個順序複合生成區域,其中有兩個球形生成區域子級。兩個球體的半徑均爲1,而且在沿Z軸的兩個方向上距原點十個單位。
(旋轉生成區的層級)
要持久化關卡狀態,必須將旋轉對象和複合生成區域都放入持久對象數組中。它們的順序可有可無,但之後不該更改。
(關卡4的持久化對象)
這種配置會在較大球體的相對兩側建立兩個小生成區,圍繞它們旋轉並上下移動。
(圍繞生成區旋轉)
經過自動建立速度而不是手動生成形狀,很容易看到它的實際效果。而後,你還能夠測試保存和加載,以驗證關卡狀態確實存在並已還原。可是,有時咱們會獲得不一樣的生成結果。咱們將在下一部分中處理。
4 建立和釋放
自動建立和銷燬過程也是遊戲狀態的一部分。咱們目前還沒有保存,所以建立和銷燬進度不受保存和加載的影響。這意味着當建立速度大於零時,加載遊戲後,你可能不會得到徹底相同的形狀放置。形狀破壞的時間也同樣。咱們應該確保時間安排徹底相同。
4.1 保存和加載
保存進度僅需在Game.Save中寫入兩個值便可。在寫入隨機狀態以後進行。
加載時,請在適當的時候讀回它們。
4.2 確切時間
咱們仍然沒有徹底相同的時機。那是由於咱們遊戲的幀頻不是很穩定。每一個幀的時間增量是可變的。若是幀花費的時間比之前更長,那麼足以早於上一次生成一個形狀就足夠了。不然可能會在之後顯示一幀。結合基於相同時間增量的移動生成區,形狀可能會終止於其餘位置。
經過使用一個固定的時間增量來更新創造和釋放的進程,從而使時間精確。這是經過將相關代碼從Update方法移動到新的FixedUpdate方法來實現的。
如今,形狀的自動建立和銷燬再也不受可變幀速率的影響。可是旋轉器仍然是。爲了使其完美,咱們也應該對RotatingObject中的旋轉使用FixedUpdate。
FixedUpdate何時調用?
在Update以後,它會在每一個幀中被調用。調用多少次取決於幀時間和固定時間步長,你能夠經過「Edit/ Project Settings / Time」進行配置。
默認的固定時間步長爲0.02,即每秒50次。所以,若是你的遊戲以每秒剛好10幀的速度運行,則FixedUpdate將每幀調用五次。並且,若是你的遊戲每秒運行50幀以上,則有時在一幀內根本不會調用FixedUpdate。若是須要更多或更少的時間粒度,則可使用不一樣的時間步長。
在使用物理引擎或須要可靠的可重複計時時,可使用FixedUpdate,在本教程中就是這種狀況。
4.3 速度設置
除了進度外,咱們還能夠考慮遊戲狀態中的速度設置部分。咱們要作的就是在保存時也寫入速度屬性。
並在加載時讀取它們。
在開始新遊戲時重置速度也頗有必要。
4.4 更新文本標籤
如今,速度設置已保存,並在咱們加載遊戲時恢復。可是UI並無意識到這一點,所以若是咱們碰巧加載了不一樣的速度的時候,則不會變化。加載後,咱們必須手動刷新滑塊。爲了使之成爲可能,遊戲須要引用滑塊,所以爲它們添加兩個配置字段。
(對滑動條的引用)
不能把UI綁定到屬性上嗎?
目前沒有內置的方法能夠作到這一點。咱們能夠提出一個自定義解決方案,但這超出了本教程的範圍。對於咱們的簡單狀況,滑塊引用就足夠了。
重置速度時,咱們如今能夠經過分配滑塊的value屬性來更新它們。
經過語法糖賦值,可使此代碼更加簡潔。
在Load方法中執行相同的操做。
如今,在加載或開始新遊戲後,UI也會更新了。
下一個教程是 可配置形狀。
本文翻譯自 Jasper Flick的系列教程
原文地址:
https://catlikecoding.com/unity/tutorials
![](http://static.javashuo.com/static/loading.gif)
本文分享自微信公衆號 - 壹種念頭(OneDay1Idea)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。