遊戲開發日誌 #03


開發進度還算理想,基本都困在自己最弱、資歷最淺的3D模型上。。。

程序上由於對 C# 的特性和 Unity 的環境雙方的了解還不足夠,花了不時間去研究和嘗試。。。

為了對自己的幫助更深,所以也開始解答一些問題或提供解決方案之類的討論,獲益不少。

這篇除了分享一下開發的進度,也分享一下這幾天一個比較有趣的討論。



<現時的進度>




游戲中非常重要的要素 - 互動性

一般聽到互動性,就會想成是玩家和遊戲間的互動,因為現時大多數都是 FPS/TPS 類的遊戲,但實在上互動性並不只是玩家和遊戲,還包括角色和遊戲世界間的互動。

玩家和遊戲間的互動非常重要,雖然對於內容的豐富程度並沒有什麼影響力,但玩家停留在一個遊戲的時間,直接跟這個互動程度拉上關係,所以不能忽視。
這次加入了最基本的一個效果 - 手機震動;除了把絆倒的動作優化外,同時加入了手機震動效果,在跑過那紅色絆腳東西時會微震一下,在跌倒時撞向地面的一刻也會有震動。

加強「角色存在於遊戲世界中」這個表現,就是角色和遊戲世界間互動的工作。要製造這個表現,必須要增加一些角色與遊戲世界間的互動要素。例如:
  • 角色對路人甲乙丙都沒反應,但接近路人丁時就會往他看過去,那這個路人丁不是任務關連角色,就一定是重要角色吧!
  • 角色走過一列書架,卻只會看著那一列書,那裡的書一定有重要情報!
  • 角色在路上慢慢走到一些機關前時會震驚一下,但跑的話就不能發現機關。
這些角色和遊戲世界的互動,大大增加玩家要處理的資訊,會有一個「內容很豐富」的錯覺。

這次我先加入了「看東西」這個功能。由於3D角色並沒有使用 Humanoid,所以不能使用簡單的 Animator.SetLookAtPosition,必須自己去設計一個配合自己系統的東西出來。
花了兩三天時間開發和調整,就得出影片中的效果了。



<Random 隨機數>

最近在導師的私人群組中,有一個比較有趣的討論,關於隨機數。

在電腦編程上使用的 random 其實是「偽隨機性」的,簡單說一句就是可追溯的。隨了可以追溯隨機數的源頭、準則外,也會出現可預測的循環結果。

關於那個有趣的討論,在這裡簡單說一下:
  • 問題:從 1 至 100 抽一個隨機數,但不包括 15 和 45。
以公平機率隨機抽樣,而要除去其中幾個數的話,有四個方法:

如抽樣和除去範圍不變的話,最好就是 (1)
如抽樣和除去範圍不定,那使用 (2) 或 (3) 或 (4)
  1. 從 array 中 random。
  2. 重複抽樣直至不是除去範圍。
  3. 抽樣時先減少除去範圍的數量,再看結果來加上數值(if => 15 +1, if =>45 +2 )。
  4. 同 (3),若抽中除去範圍,則放到最後(if =15 99, if=45 100)。
但 (2) 的方法其實並不公平,因為抽樣範圍並不正確,而且會出現重複抽樣的機會。只抽一次而抽樣數量不變的話,就可以保住抽樣的公平性,又不會耗費太多程序去工作。

有網友也提出了數學上的機會率問題,因為只要重抽次數是無限次,那機會率其實不會變,也是 1/98,即使第一次抽時是 1/100。

可惜,Solution 解決方案並不是算式,而是 case by case,按情況來決定。

數學上的結果一樣並不等於公平,全視乎情況,例如敵人生成,給予不同的行為等等,這些機率差距會很大影響,因為比率並不是百分之一,而是只有幾分一。機率一但大的話,抽不中的機率自然提高,即使重抽在數學上可以得到同一個答案,但真的應該在程序上選擇這種不確定而低效率的方法嗎?答案是按情況來決定。

重抽的做法,就像是把 1/5 = 0.25 合理化一樣的東西,就是那個多出來的 0.05 要如何處理的問題。雖然重抽也可以,但是否所有情況都應該用重抽?絕不,但不使用重抽的話,除了效能更佳外,還可以應付所有必要的 randow with exception 的情況。

或者寫一個 redraw 太簡單,才比較多人使用吧,但 (3) 和 (4) 也不見得是困難的東西,而且絕對優於 redraw。

(3)
private int RandomInt(int start, int end, int[] exclude) {
   int rEnd = (end + 1 - exclude.Length);
   int x = Random.Range(start, rEnd);
   foreach (int n in exclude)  {
      if (x >= n)
         x++;
   }
   return x;
}

(4)
private int RandomInt_r(int start, int end, int[] exclude) {
   int rEnd = (end + 1 - exclude.Length);
   int x = Random.Range(start, rEnd);
   foreach (int n in exclude) {
      if (x == n)
         x = rEnd + System.Array.IndexOf(exclude, n);
   }
   return x;
}

另外,編程上的 random,其實並非由指定的樣本中抽樣的。random 是從 0 至 1 抽一個浮點數作為比例來得出答案。

以賭場的輪盤作例子,38 個空格,球子掉落其中一個,就是結果。如果 17 沒有了,但球子又掉落去 17 的話,其實 17 這個空格應該是 16/18 的,甚至每一個空格的一小部份都應該變成旁邊的才是。
看似是 1/38 的機會率,其實如果沒有間隔出 38 個空格的話,球子掉落的位置就已經多了數千數萬個(0 至 1 抽一個浮點數),只是輪盤把這數千數萬個結果像切蛋糕一樣平分成 38 份,也把球子會彈或移到其他位置的可能抹掉。
一開始就知道只有 37 份,卻因為沒有把 17 抽走,才會出現本來不是這個結果的結果,迫使要重新掉一次球。

好像很複雜,但希望可以了解是什麼一回事。只抽樣一次是公平抽樣的準則之一,所以收集遊戲永不公開抽樣法則。


還有一種做法,抽樣次數多於一次,但卻保留公平性,就是分層抽樣法

由 0 至 100,除去 50 的話,其實是分了 0-49 和 51-100 兩組各 50 個樣本的組別,可以先隨機選一組,再在那一組抽一個樣本出來就可以,這樣是公平和簡單的方法,但有一個條件,就是各組的樣本數量必須一樣。

如把 1 至 100 分三組,1-20, 21-50, 51-100 的話,但又要使用這方法的話,就要在第一輪抽樣時按每組樣本數比例來做隨機。各組的樣本數分別是 20, 30, 50,比例是2:3:5,只要先從 1 至 10 中抽一個數,1-2 為第一組、3-5為第二組、6-10為第三組,這樣就可以保留公平性。

這種方法其實就是收集遊戲的抽獎機率方法,因為這方法可以有效分組和增加層級,而且可以簡單控制每層級中每一組的機率。不過,實際上還有其他的的控制項,以及情況不同都會把機率改變,跟遊戲公司說明的有出入;最好的例子就是限定數量,這是平衡市場的一種手段,例如有新 SS 咭推出,超犯規數值,如大量玩家擁有必會失衡,所以會在推出期間平均抽中率,如推出一星期,當時市場只可容納二十一個玩家,那每天只可有三次抽中的機會。

這就是為何新咭或限量咭要分開一個抽獎池的主因,因為這部份沒有公平的方法。

除了上面的問題,還有一個非常重要但也算是同類的因素,在不同系統不同處理器中, 隨機數的結果會有偏向性。

先看看以下影片:

這是去年(2017)的功課,因為搬家的關係,很多東西沒有做好。

在進入關卡後,控制玩家向下走,邊吃水晶邊避開火怪,一個很簡單的無限生成遊戲。

在樓層生成時,會同時生成火怪,火怪有四種,開始的 100 米會有紅藍兩種,以後每 100 米追加一種亞種,最多四種同時生成。怪是有生成機率的,這是避免太多連續生成或不生成,保持遊戲的難度,透過每 100 米增加生成機率來提高難度。

以上是最初的設定,在 Editor 上測試過機率的可行性,雖然有時在開始時就連續 5 層有怪生成,或在 200 米後超過 10 層都沒有怪,但情況真的很少很少,20 次也沒有 1 次,所以就放到手機上作測試,結果居然相反!

這時才想到不同的系統或處理器的 random seed 可能不同,所以做了一個很簡單的測試:

print((Random.value > 0.90f));

這是在 0 至 1 中隨機抽一個浮點數,如大於 0.9 則 true,反之 false。

在這個測試中發現,獲得 true 的機率在手機上大增,但又有很少的機率會相反,這是測試了大半天,超過1000次的 true or false 測試,以及上面遊戲的超過 100 次測試結果。

雖然結果沒有絕對性,但偏向是肯定的,所以我在火怪生成抽樣的結果上,加入了每 100 米為刻度的重複生成次數限制以及不生成次數限制,來把難易度平衡過來。


只要是「偽隨機性」的抽樣,其實就難以做到公平性,只有盡力去保持平衡。

數學是沉悶的,有趣的地方永遠在於解決問題,因為解決方案不能只靠數理來作準。

留言

此網誌的熱門文章

[教學]一起來開發遊戲吧 - Unity C# 基礎

QUMARION

[教學]一起來開發遊戲吧(二) - Character Controller, Pool System