承接上篇: 上次我們深入了解了GameController這個遊戲大腦,今天要來探討兩個讓遊戲變得生動有趣的關鍵系統:FishMaker(魚群生成器)和GunFollow(砲台跟隨系統)。這兩個看似簡單的功能,背後其實藏著不少有趣的技術細節!
🐟 FishMaker:打造永不枯竭的海洋世界
想像一下,如果捕魚機遊戲裡的魚只有固定幾條,玩家很快就會覺得無聊。FishMaker.cs 就像是海洋的造物主,負責源源不絕地創造出各種魚類,讓遊戲世界永遠充滿生機。
魚群生成的基本邏輯:隨機中的規律
FishMaker的工作原理其實很簡單:每隔一段時間就決定要在哪裡生成什麼魚,生成多少條,然後讓它們以什麼方式游泳。但這個「決定」的過程包含了許多巧思。
void Start()
{
// 每隔固定時間生成魚群
InvokeRepeating("MakeFishes", 0, waveGenWaitTime);
}
void MakeFishes()
{
int genPosIndex = Random.Range(0, genPositions.Length); // 隨機選位置
int fishPreIndex = Random.Range(0, fishPrefabs.Length); // 隨機選魚種
int maxNum = fishPrefabs[fishPreIndex].GetComponent<FishAttr>().maxNum;
int num = Random.Range((maxNum / 2) + 1, maxNum); // 決定數量
}
這裡用了 InvokeRepeating 來定時呼叫生成函式,就像是設定了一個永不停歇的鬧鐘。每次鬧鐘響起,系統就會隨機選擇一個生成點和一種魚類,然後決定這次要生成多少條魚。
魚群數量的智慧計算
你有沒有想過,為什麼有時候會看到一大群小魚,有時候只有幾條大魚?這不是巧合,而是經過精心設計的。
int maxNum = fishPrefabs[fishPreIndex].GetComponent<FishAttr>().maxNum;
int num = Random.Range((maxNum / 2) + 1, maxNum);
這個算法確保每次生成的魚群數量都在合理範圍內。假設某種魚的最大生成數量是10條,那麼實際生成數量會在6到10條之間隨機選擇。這樣既有變化性,又不會太極端——不會只生成1條魚讓畫面太空曠,也不會總是滿屏都是魚讓玩家眼花撩亂。
兩種游泳模式:直線與轉彎的藝術
真正讓魚群看起來生動的關鍵在於它們的游泳方式。FishMaker設計了兩種基本的游泳模式:直線游泳和轉彎游泳。
int moveType = Random.Range(0, 2); // 0:直走;1:轉彎
if (moveType == 0) // 直線游泳
{
angOffset = Random.Range(-22, 22); // 微調角度,讓直線不會太死板
StartCoroutine(GenStraightFish(genPosIndex, fishPreIndex, num, speed, angOffset));
}
else // 轉彎游泳
{
// 決定順時針還是逆時針轉彎
if (Random.Range(0, 2) == 0)
angSpeed = Random.Range(-15, -9); // 逆時針
else
angSpeed = Random.Range(9, 15); // 順時針
StartCoroutine(GenTrunFish(genPosIndex, fishPreIndex, num, speed, angSpeed));
}
直線游泳看似簡單,但加了 -22 到 22 度的隨機角度偏移,讓魚群不會像機器人一樣呆板地水平移動。轉彎游泳則會隨機決定轉彎的方向和速度,創造出更自然的游泳軌跡。
從上面的動畫可以看到,魚群會以不同的方式在螢幕中游動:有些魚採用直線游泳(可能帶有輕微的角度偏移),有些魚則會在游泳過程中轉彎,形成弧形的移動軌跡。這種多樣化的移動模式讓遊戲畫面更加生動有趣。
協程的妙用:錯開生成時間避免重疊
如果所有魚都同時出現在同一個位置,畫面會變得很混亂。FishMaker用協程來解決這個問題:
IEnumerator GenStraightFish(int genPosIndex, int fishPreIndex, int num, int speed, int angOffset)
{
for (int i = 0; i < num; i++)
{
GameObject fish = Instantiate(fishPrefabs[fishPreIndex]);
// 設定魚的位置、旋轉、屬性...
yield return new WaitForSeconds(fishGenWaitTime); // 關鍵:等待一下再生成下一條
}
}
這個 yield return new WaitForSeconds(fishGenWaitTime) 讓每條魚的生成都有個小間隔,通常是0.5秒。這樣魚群會一條接一條地出現,形成整齊的隊伍,而不是亂糟糟的一團。
魚群個性化:讓每條魚都獨一無二
即使是同一批生成的魚,FishMaker也會為它們設定不同的屬性:
fish.GetComponent<SpriteRenderer>().sortingOrder += i; // 避免重疊閃爍
fish.AddComponent<Ef_AutoMove>().speed = speed; // 動態添加移動腳本
sortingOrder += i 這行很重要,它讓每條魚的渲染順序都不同,避免了多條魚重疊時出現閃爍的問題。動態添加移動腳本則讓每條魚能夠自主移動,不需要FishMaker持續控制。
🎯 GunFollow:砲台的精準跟隨術
如果說FishMaker是遊戲的內容創造者,那GunFollow就是玩家與遊戲互動的橋樑。它讓砲台能夠精準地跟隨滑鼠移動,給玩家直接操控遊戲的感覺。
如上圖所示,當玩家移動滑鼠時,砲台會即時地轉向滑鼠位置,這種即時回饋讓玩家感覺自己真的在操控一門砲台。這個看似簡單的功能,背後其實涉及了座標轉換和角度計算等技術細節。
滑鼠位置轉換:從螢幕到遊戲世界
砲台跟隨的第一個挑戰是座標轉換。滑鼠位置是螢幕座標,但砲台是在遊戲世界中,這兩個座標系統是不同的。
void Update()
{
Vector3 mousePos;
// 將螢幕中的滑鼠座標轉為世界座標
RectTransformUtility.ScreenPointToWorldPointInRectangle(
UGUICanvas,
new Vector2(Input.mousePosition.x, Input.mousePosition.y),
mainCamera,
out mousePos
);
}
這個轉換函式考慮了UGUI的縮放問題,確保在不同解析度和UI縮放下都能正確計算滑鼠位置。這是很多初學者容易忽略的細節。
角度計算的數學魔法
砲台要面向滑鼠位置,就需要計算角度。這裡用到了向量角度計算:
float z; // 砲台需要旋轉的角度
if (mousePos.x > transform.position.x) // 滑鼠在砲台右邊
{
z = -Vector3.Angle(Vector3.up, mousePos - transform.position);
}
else // 滑鼠在砲台左邊
{
z = Vector3.Angle(Vector3.up, mousePos - transform.position);
}
transform.localRotation = Quaternion.Euler(0, 0, z);
這個計算分成左右兩種情況處理。Vector3.Angle 計算兩個向量之間的夾角,這裡是計算「向上方向」和「砲台指向滑鼠方向」之間的角度。左邊用正角度,右邊用負角度,這樣砲台就能正確地指向滑鼠位置。
為什麼要分左右處理?
你可能會好奇,為什麼要分左右兩種情況?這是因為 Vector3.Angle 只會回傳 0 到 180 度的值,它不知道角度的方向(順時針還是逆時針)。透過判斷滑鼠在砲台的左邊還是右邊,我們可以決定角度的正負號,讓砲台能夠 360 度旋轉。
🔄 系統整合:三個腦袋一個身體
現在我們看看這三個系統如何協同工作。GameController負責統籌,FishMaker負責提供目標,GunFollow負責瞄準,它們之間的配合就像是一支訓練有素的樂隊。
時機控制:誰在什麼時候做什麼
遊戲開始時:
GameController → 初始化所有系統
FishMaker → 開始定時生成魚群
GunFollow → 開始監聽滑鼠移動
遊戲進行中:
每0.3秒:FishMaker生成新的魚群
每一幀:GunFollow更新砲台角度
每一幀:GameController檢查射擊輸入
這個時間安排很重要。FishMaker不需要每一幀都運作,因為生成魚群是比較耗資源的操作。但GunFollow必須每一幀都更新,否則砲台跟隨會顯得卡頓。
資料流向:資訊如何在系統間傳遞
FishMaker生成的魚會自動獲得移動能力,不需要回報給GameController。但當玩家射擊時,GameController會檢查砲台的角度(由GunFollow控制),然後決定子彈的發射方向。
// 在GameController的射擊系統中
bullet.transform.rotation = gunGos[costIndex / 4].transform.Find("FirePos").transform.rotation;
這行程式碼讓子彈的方向跟隨砲台的角度,而砲台的角度又是由GunFollow根據滑鼠位置計算的。這樣整個操作鏈就連接起來了:滑鼠移動 → 砲台轉向 → 子彈發射方向。
🎮 性能考量:讓遊戲跑得更順暢
雖然這些系統看起來簡單,但在性能優化上還是有一些值得注意的地方。
FishMaker的性能陷阱
頻繁的 Instantiate 操作是FishMaker最大的性能負擔。每次創建新物件都會觸發記憶體分配,大量的魚群生成可能會導致遊戲卡頓。
// 目前的做法:每次都創建新物件
GameObject fish = Instantiate(fishPrefabs[fishPreIndex]);
這就是為什麼物件池技術在捕魚機遊戲中這麼重要。如果你想深入了解如何實作魚群的物件池系統,可以參考我們的進階教學:Unity進階物件池教學:打造高效能捕魚機魚群生成系統,那裡有完整的實作步驟和優化技巧。
GunFollow的優化空間
GunFollow目前每一幀都在做座標轉換和角度計算,雖然這些計算不算複雜,但還是可以優化:
void Update()
{
// 可以考慮:只有滑鼠位置改變時才重新計算
Vector3 mousePos;
RectTransformUtility.ScreenPointToWorldPointInRectangle(/*...*/);
// 計算角度...
}
不過在大多數情況下,這個性能影響微乎其微,除非你的遊戲有很多砲台同時運作。
🎯 設計思維:從功能到體驗
這兩個系統展現了遊戲設計的一個重要原則:技術服務於體驗。FishMaker的隨機演算法不是為了展示技術複雜性,而是為了創造變化豐富的遊戲內容。GunFollow的數學計算不是為了炫耀數學技巧,而是為了提供直觀的操作感受。
FishMaker的設計智慧
- 適度隨機:不是完全隨機,而是在合理範圍內變化
- 視覺考量:通過錯開生成時間避免視覺混亂
- 多樣性:兩種游泳模式增加視覺豐富度
GunFollow的設計巧思
- 即時回饋:每一幀都更新,讓操作感受即時
- 數學精準:準確的角度計算讓瞄準變得可靠
- 適配性:考慮了不同解析度和UI縮放的相容性
🚀 總結:小系統,大學問
FishMaker和GunFollow雖然功能相對單純,但它們展現了遊戲開發中「小系統,大學問」的特色。每個看似簡單的功能背後,都需要考慮使用者體驗、性能優化、系統整合等多個面向。
FishMaker教我們如何用程式邏輯創造生動的遊戲內容,如何在規律中加入變化,在隨機中保持合理。GunFollow則展示了如何將玩家的物理操作轉換成遊戲中的精準控制,如何處理不同座標系統間的轉換。
這兩個系統與GameController配合,共同構成了一個完整的遊戲體驗:玩家移動滑鼠,砲台跟隨轉向,瞄準游來游去的魚群,點擊發射子彈。這個看似簡單的循環,背後是三個系統精密協作的結果。
🎣 下期預告:深入物件屬性系統
下一篇文章我們將探討 FishAttr.cs、BulletAttr.cs 和 WebAttr.cs 這三個屬性腳本。它們定義了遊戲中各種物件的行為和特性,包括魚類的血量系統、子彈的碰撞檢測、漁網的傷害計算等。我們會深入了解Unity的碰撞系統、事件通信機制,以及如何設計靈活的遊戲物件屬性。敬請期待!
