Unity - PuzzleGame 2 (環形旋轉拼圖)

  旋轉拼圖,其實也就是個多層轉盤而已,主要目的在調整每一層環形圖片的角度,使得每一層環的圖片可以接在一起,形成一張完整沒有折斷的圖。

  遊戲的機制也很簡單,就讓使用者調整每個環旋轉的角度,然後比對每個環跟前後兩個環的角度,如果差異在某個誤差值以內就視為擺放正確,而如果每個環都擺放正確就遊戲結束。


  製作使用的Unity版本為5.2.2f1。



  實作測試 (WebGL build,載入似乎會花一點時間),滑鼠拖曳右方小物件放到中心圓圈,接著開始調整環的角度,當角度都正確就遊戲結束。




1、環形物件

  製作這個旋轉拼圖這次分為三個簡單的部分,第一個部分是環的部分,負責滑鼠拖曳旋轉;第二個部分是假的Inventory欄位,負責顯示迷你物件、滑鼠點擊拖曳;第三個部分是版面,負責接收迷你物件、檢查每個環的角度。

  先從環的部分開始,這邊偷懶就直接先把圖片裁切好一環一環的圖,先製作一個簡單的Script,同樣這個部分滑鼠的點擊判斷也直接使用Unity提供的方法,這個Component之後要放在環形圖片上。

public class Ring : MonoBehaviour
{
    //設定或取得物件的角度
    public float Angle { get { return this.transform.eulerAngles.z; } set { this.transform.eulerAngles = new Vector3(0, 0, value); } }

    private bool isDrag; //是否拖曳中
    private float preAngle; //開始拖曳前的角度
    private Vector3 mousePos; //開始拖曳前的滑鼠位置

    void Update()
    {
        if (isDrag)
        {
            //基本三角點
            Vector2 ringWorldPos = new Vector2(this.transform.position.x, this.transform.position.y);
            Vector2 startWorldPos = Camera.main.ScreenToWorldPoint(mousePos);
            Vector2 mouseWorldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);

            //計算夾角
            float angle = Vector2.Angle(startWorldPos - ringWorldPos, mouseWorldPos - ringWorldPos);
            Vector3 cross = Vector3.Cross(startWorldPos - ringWorldPos, mouseWorldPos - ringWorldPos);
            if (cross.z > 0) angle = 360 - angle;

            //調整圖片角度
            this.transform.eulerAngles = new Vector3(0, 0, preAngle - angle);
        }
    }

    void OnMouseDown()
    {
        if (!this.gameObject.activeSelf) return; //如果物件沒有顯示就不動作

        isDrag = true;
        preAngle = this.transform.eulerAngles.z; //紀錄開始拖曳前的角度
        mousePos = Input.mousePosition; //紀錄開始拖曳前的滑鼠位置
    }

    void OnMouseUp()
    {
        if (!this.gameObject.activeSelf) return; //如果物件沒有顯示就不動作
        isDrag = false;
    }

    //環形圖片物件的顯示/關閉
    public void ActiveRing(bool active)
    {
        this.gameObject.SetActive(active);
        if (active) this.transform.eulerAngles = new Vector3(0, 0, Random.Range(0, 360)); //亂數調整角度
    }
}

  接著把每張環形圖片放到場景中,每一個環形圖片的物件都加上Circle Collider 2D及上方自己做的Component,接著稍稍調整每個環形圖片的Z軸座標,如圖片形成一個塔型,這邊假設原始完整的圖角度的Z都是0度。

  為何如此做的原因,因為使用OnMouseDown()、OnMouseUp()去判斷,而如果Collider重疊在同一個位置上,判斷上會出問題。形成塔型,小的在前面,大的在後面,這樣點擊小的環就不會觸動大的環。





2、欄位

  雖然說是製作欄位,但是這邊其實只是個擺放小圖的位置,直接把(9宮格總和15)的物件拿來修改一點就可以直接使用了。

public class Unit : MonoBehaviour
{
    public int index; //這個物件代表的環的位置, 由外到內 0~n
    private Vector3 startPosition; //起始位置
    private bool isDrag; //是否拖曳中

    void Start ()
    {
        startPosition = this.transform.position;
    }

    void Update ()
    {
        if (isDrag)
        {
            Vector2 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            this.transform.position = mousePos;
        }
    }

    void OnMouseDown()
    {
        StopAllCoroutines(); //停止移動的Coroutine
        isDrag = true;
    }

    void OnMouseUp()
    {
        isDrag = false;

        //檢查是否放在板子上
        bool isOnBoard = false;
        RaycastHit2D[] hits = Physics2D.RaycastAll(Camera.main.ScreenToWorldPoint(Input.mousePosition), Vector2.zero);
        foreach (RaycastHit2D hit in hits)
        {
            //檢查所有的Hit,只取Tag是Board的物件
            if (hit.transform.tag == "Board")
            {
                //放到板子上,用SendMessage呼叫啟動第幾個環
                hit.transform.SendMessage("ActiveRing", index);
                this.gameObject.SetActive(false); //關閉這個小物件顯示
                isOnBoard = true;
            }
        }

        //如果滑鼠座標沒有接觸到板子,就送回起始位置
        if(!isOnBoard) StartCoroutine("ReturnPosition", startPosition);
    }

    //隨意做的回原位方法
    IEnumerator ReturnPosition(Vector3 pos)
    {
        Vector3 target = new Vector3(pos.x, pos.y, this.transform.position.z);
        while (true)
        {
            this.transform.position = Vector3.MoveTowards(this.transform.position, target, 10f * Time.deltaTime);
            if (this.transform.position == target) break;
            yield return null;
        }
    }
}


  接著在場景中隨意放四個空白方塊當作欄位,接著把小圖放進每個位置中,我這邊是直接用原本環的圖片縮小Scale來用,每個小圖加上Box Collider 2D以及自己做的Component即可,這邊放上Unit後要記得設定每個物件代表的Index數值。

  這邊就不用管Z軸位置了,因為到時候放在板子上是用RayCast去抓所有的,不過這邊要注意圖片的SortingOrder,稍微調整前後避免跟底圖、欄位方塊圖同個SortingOrder造成顯示問題。





3、板子

  最後就是板子了,這邊就簡單地接收是否顯示某個環,然後用Update()來一直檢查每個環的角度是否都正確。

public class Board : MonoBehaviour
{
    public Ring[] ringList; //由外到內
    
    void Update()
    {
        CheckRingsAngle();
    }

    public void ActiveRing(int index)
    {
        if (index < 0 || index >= ringList.Length)
            return;

        ringList[index].ActiveRing(true);
    }

    private void CheckRingsAngle()
    {
        //假設每個環正確角度都是0度,比對跟正確角度的差異,在誤差範圍內就視為正確
        bool isGameOver = true;
        for (int i = 0; i < ringList.Length; ++i)
        {
            if (ringList[i].gameObject.activeSelf)
            {
                float ringAngle = ringList[i].Angle;
                float offset = Mathf.Abs(0 - ringAngle);
                if (offset > 180) offset = 360 - offset;
                if (offset > 5) isGameOver = false; //超過+-5度就沒有擺放正確
            }
            else
            {
                isGameOver = false;
            }
        }
        
        if (isGameOver)
        {
            Debug.Log("GameOver");
        }
    }
}


  接著在背景圖這邊,製作一個空的物件並加上Circle Collider 2D以及Board這兩個Component,把這個Collider大小調整到跟背景圖空缺的大小差不多大小,Z軸位置不用調整,記得這個Collider物件的Tag設定為Board好讓Unit去判斷。

  最放把RingList放上去,由最外環到最中心的順序,放完後記得把每個環顯示都關閉。


  這樣整體就算完成了,再調整小細節或是製作介面就差不多了。




  這個版本不用在乎效能,所以我每個環都是獨立一張圖片,這個其實會造成一些問題,像是如果環的數量越多圖片的數量也就越多這是一點,另外一點就是環是中空的,就算把這些圖集合成一張Atlas,中間透明部分就會占用空間。

  如果有任何問題歡迎提出。

4 comments:

Anonymous said...

我想請問一下,這可以在unity3d文件裡做出來嗎?還是一定要在2D文件裡?

VervProject said...

2D的只要轉Z軸就可以了,3D當然也可以,多轉兩個軸而已,看畫面要怎樣呈現而已,可以做成像星球儀一樣,每一個環都可以自由旋轉

Altari Chau said...
This comment has been removed by the author.
Altari Chau said...

請問一下可以分享大大的專案嗎?
有些INSPECTOR的設定不太清楚

Post a Comment