2016年8月9日 星期二

【Unity】轉珠系統製作Part.5 - 珠子移動和交換

Part.4    進度報告和告知

上次說到了珠子的消除、落下和生成,接下來就要進入珠子的移動與交換,以下是這次的目標。
使用滑鼠或手指按著珠子,接著拖曳珠子移動可以與其他位置的珠子交換,直到放開珠子或指定時間到時結束。

珠子交換本文中會使用collider,所以在珠子的預設物件上新增2D Collider。
這裡使用Circle Collider 2D,也就是2D的圓形碰撞器,後續會用到觸發函式,所以將Is Trigger打勾。
如果是盤面珠子數量固定的情況,可以直接將偏移量(Offset)和半徑(Radius)設定好數值,至於可調整盤面的情況,需要對Orb腳本做以下修改。
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.UI;
public class Orb : MonoBehaviour
{
    .
    .
    .
    public float width = 120;
    public float height = 120;
    void Start()
    {
        .
        .
        .
        GetComponent<CircleCollider2D>().offset = new Vector2(width / 2, height / 2);
        GetComponent<CircleCollider2D>().radius = width / 2 - 2;
    }
}
在Start中重新設定偏移量,由於珠子的支點在左下角,則偏移量寬高的1/2,半徑基本上為寬或高的1/2,因為想讓珠子之間留一點距離,減去一些固定數值。

接下來新增物件Finger和物件下元件。
新增Image物件Finger在Canvas之下,作為玩家滑鼠或手指的代理物件,物件下新增2D Collider、2D Rigidbody和腳本Finger。
Finger物件的RectTransform如下圖所示。
座標中心與盤面背景相同為左下角,位置與寬高會在Finger腳本中初始化。
以下是Finger腳本的內容,首先是需要的變數和初始化。
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
public class Finger : MonoBehaviour {
    public PuzzleSystem puzzleSystem;////(1)
    private RectTransform rect;////(2)
    public Vector2 initPos;////(3)
    private Image image;////(4)
    private Color display = new Color(1, 1, 1, 0.5f);////(5)
    private Color hide = new Color(1, 1, 1, 0);////(6)
    public Orb nowOrb;////(7)
    public bool moveState;////(8)
    void OnTriggerEnter2D(Collider2D other){}////(9)
    public void FingerInit()////(11)

    {
        rect.anchoredPosition = initPos;////(12)
        image.color = hide;////(13)
        nowOrb = null;////(14)
        moveState = false;////(15)
    }
    void Start() {
        rect = GetComponent<RectTransform>();
        rect.sizeDelta = new Vector2(puzzleSystem.BGRect.sizeDelta.x / puzzleSystem.columnCount,
                                    puzzleSystem.BGRect.sizeDelta.y / puzzleSystem.rowCount);////(10)
        image = GetComponent<Image>();
        FingerInit();
    }
}
(1)移動時需要盤面上珠子的資料,所以宣告puzzleSystem用來抓資料。
(2)Finger的RectTransform。
(3)Finger初始化的位置。
(4)Finger的Image。
(5)點擊珠子時Finger的透明度。
(6)放開珠子時Finger的透明度
(7)儲存被點擊珠子的資料。
(8)Finger是否在移動中。
(9)當Finger被觸發時做珠子交換,內容後續會提到。
(10)初始化Finger的寬高,與珠子大小相同。
(11)宣告FingerInit函式並在Start中呼叫,包含會重複初始化的內容。
(12)初始化Finger的位置。
(13)圖片預設為隱藏。
(14)點擊珠子預設為null。
(15)移動狀態為false。

接下來是OnTriggerEnter2D函式的內容。
void OnTriggerEnter2D(Collider2D other)
{
    if (other.tag == "Orb")////(1)
    {
        if (nowOrb == null || nowOrb == other.GetComponent<Orb>())////(2)
        {
            other.GetComponent<Image>().color = hide;////(3)
            nowOrb = other.GetComponent<Orb>();////(4)
        }
        else
        {
            moveState = true;////(5)
            ////(6)
            Orb.OrbsType temp = other.GetComponent<Orb>().type;
            other.GetComponent<Orb>().type = puzzleSystem.orbs[nowOrb.number].type;
            puzzleSystem.orbs[nowOrb.number].type = temp;
            ////(7)
            other.GetComponent<Orb>().ChangeImage();
            other.GetComponent<Image>().color = hide;
            puzzleSystem.orbs[nowOrb.number].ChangeImage();
            puzzleSystem.orbs[nowOrb.number].GetComponent<Image>().color = Color.white;
            ////(8)
            nowOrb = other.GetComponent<Orb>();
        }
        ////(9)
        image.sprite = Resources.Load<Sprite>("Image/" + other.GetComponent<Orb>().type);
        image.color = display;
    }
}
(1)當觸發物件標籤為Orb,也就是珠子時才做移動和交換。
(2)當nowOrb為null或等於觸發物件,也就是點擊珠子但還沒開始移動交換珠子。
(3)隱藏點擊的珠子。
(4)將nowOrb設為觸發物件。
(5)當(2)不成立時,代表目前觸發到的珠子與nowOrb不同,也就是開始移動交換珠子,所以把moveState設為true
(6)做nowOrb與觸發珠子的屬性交換。
(7)呼叫珠子底下的函式ChangeImage換圖,並重新設定顯示顏色。
(8)將nowOrb設為觸發物件。
(9)將Finger的圖片設為目前珠子的屬性圖片,並修改顯示顏色。

到這裡我們做完了觸發到珠子物件時要做的交換,接下來要處理手指移動與移動狀態。
新增空物件PuzzleControl並新增腳本PuzzleControl,如果要顯示狀態和秒數文字也可以順便新增。

以下是PuzzleControl腳本內容。
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
public class PuzzleControl : MonoBehaviour {
    public enum PuzzleState { Standby, Start, Move, End };////(1)
    public PuzzleState state;////(2)
    public RectTransform BGRect;////(3)
    public RectTransform fingerRect;////(4)
    public Finger finger;////(5)
    public float puzzleTime = 4;////(6)
    public float timer = 0;////(7)
    public Text stateText;////(8)
    public Text timerText;////(9)

    Vector2 WorldToRect()////(10)
    {
        Vector2 screenPoint = RectTransformUtility.WorldToScreenPoint(Camera.main, Input.mousePosition);
        Vector2 Pos;
        RectTransformUtility.ScreenPointToLocalPointInRectangle(BGRect, screenPoint, Camera.main, out Pos);
        return Pos;
    }
    ////(11)
    void StandbyControl(){}
    void StartControl(){}
    void MoveControl(){}
    void EndControl(){}
    void Start ()
    {
        state = PuzzleState.Standby;////(12)
    }
    void Update()
    {
        ////(13)
        stateText.text = "狀態 " + state;
        timerText.text = "剩餘時間:" + (puzzleTime - timer) + "秒";
        ////(14)
        switch (state)
        {
            case PuzzleState.Standby:
                StandbyControl();
                break;
            case PuzzleState.Start:
                StartControl();
                break;
            case PuzzleState.Move:
                MoveControl();
                break;
            case PuzzleState.End:
                EndControl();
                break;
        }
    }
}
(1)列舉轉珠時的狀態,分別為待機(Standby)、開始點擊(Start)、移動中(Move)、轉珠結束(End)。
(2)儲存當前的轉珠狀態。
(3)背景物件的RectTransform,這裡用來做滑鼠點擊位置轉換到UI位置的基準。(上圖BGRect物件不小心拉錯,這裡要帶入的是背景圖物件Background。)
(4)Finger物件的RectTransform。
(5)Finger物件的腳本。
(6)轉珠時間。
(7)轉珠過程的計時器。
(8)顯示轉珠狀態的文字。
(9)顯示剩餘秒數的文字。
(10)將滑鼠點擊位置轉換成UI位置的函式,會回傳一個Vector2數值
(11)個狀態的控制函式,除了EndControl以外,其他會在下面提到。
(12)轉珠狀態初始化為待機。
(13)顯示文字。
(14)依state數值執行狀態函式。

接下來是個狀態的內容,這裡只提滑鼠點擊,觸控會暫時略過。
首先是StandbyControl。
void StandbyControl()
{
    if (Input.GetMouseButtonDown(0))////(1)
    {
        Vector2 fingerPos = WorldToRect();////(2)
        if (fingerPos.x < BGRect.sizeDelta.x && fingerPos.x > 0
            && fingerPos.y < BGRect.sizeDelta.y && fingerPos.y > 0)////(3)
            state = PuzzleState.Start;////(4)
    }
}
(1)當滑鼠左鍵按下時。
(2)儲存轉換的點擊位置。
(3)當點擊的範圍在下方轉珠的區塊時。
(4)把轉珠狀態切換成開始點擊(Start)。

接著是StartControl。
void StartControl()
{
    if (Input.GetMouseButton(0))////(1)
    {
        Vector2 fingerPos = WorldToRect();////(2)
        fingerRect.anchoredPosition = new Vector2(Mathf.Clamp(fingerPos.x, 0, BGRect.sizeDelta.x),
                      Mathf.Clamp(fingerPos.y, 0, BGRect.sizeDelta.y));////(3)
        if (finger.moveState)////(4)
            state = PuzzleState.Move;
    }
    else if (Input.GetMouseButtonUp(0))////(5)
    {
        GameObject.Find("PuzzleSystem").GetComponent<PuzzleSystem>().orbs[finger.nowOrb.number].GetComponent<Image>().color = Color.white;////(6)
        finger.FingerInit();////(7)
        state = PuzzleState.Standby;////(8)
    }
}
(1)當點著滑鼠左鍵時改變finger物件位置。
(2)儲存轉換的點擊位置。
(3)將移動範圍限制在下方的轉珠區塊並帶入finger物件位置。
(4)當finger開始移動時,把轉珠狀態切換成移動。
(5)當放開滑鼠左鍵時。
(6)將當前點擊的珠子顯示顏色修改回來。
(7)初始化finger物件數值。
(8)將轉珠狀態設回待機。

最後是MoveControl。
void MoveControl()
{
    ////(1)
    if (Input.GetMouseButton(0))
    {
        Vector2 fingerPos = WorldToRect();
        fingerRect.anchoredPosition = new Vector2(Mathf.Clamp(fingerPos.x, 0, BGRect.sizeDelta.x),
                      Mathf.Clamp(fingerPos.y, 0, BGRect.sizeDelta.y));
    }
    ////(2)
    else if (Input.GetMouseButtonUp(0))
    {
        GameObject.Find("PuzzleSystem").GetComponent<PuzzleSystem>().orbs[finger.nowOrb.number].GetComponent<Image>().color = Color.white;
        finger.FingerInit();
        state = PuzzleState.End;
    }
    ////(3)
    timer += Time.deltaTime;
    if (timer >= puzzleTime)
    {
        GameObject.Find("PuzzleSystem").GetComponent<PuzzleSystem>().orbs[finger.nowOrb.number].GetComponent<Image>().color = Color.white;
        finger.FingerInit();
        type = PuzzleState.End;
        timer = 0;
    }
}
(1)與StartControl相同,點擊時改變finger物件位置。
(2)滑鼠放開時做finger初始化等等動作,轉珠狀態切換為結束(End)
(3)除了滑鼠方開外,還要做計時並判斷是否超時的動作,超時同樣要切換成結束狀態。

沒意外的話執行後可以看到以下結果。
以上珠子移動與交換的說明,製作過程中遇到一些難點,基本上都是有關判斷的問題,使用UI物件或許不算是好方式,但硬著頭皮還是做出來了。
MoveControl那段寫得太累贅,滑鼠放開和超時的內容可以在合併。
另外StandbyControl存在一個不小的bug,會產生沒點到珠子但轉珠狀態卻換成開始的問題,物理上的解決方式可以調整Collider,或是追加判斷的變數。
最後在EndControl中做Part.2到Part.4的內容,轉珠系統的本體就完成的差不多了,最後就是追加上害計算和動畫。
這次就到這裡。

3 則留言:

  1. 請問一下 裡面提到的nowOrb.number 是在Orb的裡面的哪一個參數??

    回覆刪除
    回覆
    1. number是int變數,前面文章好像漏掉。
      在生成珠子的時候依序給編號就可以了。

      刪除
  2. 請問最後說的在「EndControl中做Part.2到Part.4的內容」是甚麼意思不太了解
    還有在Finger物件上做了2D Rigidbody後,雖然一開始執行遊戲時不會掉落
    可是移動珠子過後卻會往下掉落不會消失
    非常感謝教學分享!!!

    回覆刪除

【自製小遊戲】水平思考猜謎(海龜湯)

遊戲連結 海龜湯的玩法是由出題者提出一個難以理解的事件,參與猜題者可以提出任何問題以試圖縮小範圍並找出事件背後真正的原因。但出題者僅能以「是」、「不是」或「沒有關係」來回答問題。 本遊戲蒐集各種論壇、平台的42個題目,提供給想玩海龜湯卻愁找不到題目的你們。 ...