Unity - PuzzleGame 3 (倒水問題)

  一個經典的倒水問題,桌上只有一個5公升跟一個3公升的水瓶,假設有一個無窮的水源,要你使用這兩個瓶子來量取某一個數量的水,可以是1公升也可以是4公升,諸如此類。

  這個題目也不局限於5公升跟3公升的水瓶,不過這邊就用最簡單的這兩個單位來製作,基本原理是一樣的,差別在於達到需求水量所需的步驟數不同。


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


  實作測試 (WebGL build,似乎會花點時間載入),滑鼠左鍵點中間兩個燒杯可以拿取,滑鼠右鍵會放下燒杯,拿取燒杯的情況下點右邊的水可以裝滿,點左下角的水槽可以倒掉,點另一個燒杯可以倒過去,左上角的是目標水量燒杯,只有在燒杯水量符合才能倒進去






1、物件Tags

  因為這次依靠物件的Tag來作為判斷,所以需要增加幾個Tag來使用。


  這邊Infinity用在無窮水源的物件上,Target用在目標水盆,Sink是倒掉水的物件,Beaker是燒杯物件。





2、燒杯

  接著從燒杯的部分開始,先做一個燒杯的Component來使用,這個Component紀錄燒杯當前的水量,以及這個燒杯可以裝的最大水量,如此之外就是增減水量的方法、調整水圖片的方法。

public class Beaker : MonoBehaviour
{
    public int maxWater; //燒杯最大水位
    public int currentWater; //目前水位
    private Vector3 startPosition; //燒杯起始位置

    void Start()
    {
        //開始時紀錄起始位置同時重設水量圖片
        ResetWaterImage();
        startPosition = this.transform.position;
    }

    //填滿或清空水量
    public void AddWater(bool isFill)
    {
        currentWater = (isFill) ? maxWater : 0;
        ResetWaterImage();
    }

    //倒入另一個燒杯水量
    public void AddWater(Beaker other)
    {
        int waterSpace = maxWater - currentWater; //還可以容納的水量
        if (waterSpace == 0) return; //已經滿了

        //可以容納的水量大於等於加入的水
        if (waterSpace >= other.currentWater)
        {
            AddWater(other.currentWater);
            other.AddWater(false);
        }
        else
        {
            AddWater(waterSpace);
            other.AddWater(-waterSpace);
        }
    }

    //倒入水量
    public void AddWater(int value)
    {
        currentWater = Mathf.Clamp(currentWater + value, 0, maxWater);
        ResetWaterImage();
    }

    //回到初始位置
    public void ReturnStartPosition()
    {
        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;
        }
    }

    //只在水量有變動的時候呼叫,調整水量圖,可以加入自己的介面或物件等公式
    public void ResetWaterImage()
    {
        Debug.Log("Reset Image");
    }
}

  接著開始在場景中擺放幾個Sprite物件,這邊我總共放個5個Sprite物件,每個都加上一個Box Collider 2D,接著把每個物件的Tag都設定好。


  把每個Sprite物件的Tag都設定完畢後,在中間兩個燒瓶物件加上Beaker這個Component,然後設定好最大水量的數值。


  最後就是WaterTarget這個物件也加上Beaker這一個Component,單純只是廢物利用而已,使用Beaker的MaxWater來做為目標水量的判斷,所以這邊設定一個想到達成的水量,不過這邊因為我在判斷上是只有完全符合水量才能倒進去,無法一次倒一點累積,所以目標水量要避免超過場上最大的燒杯。






3、板子

  最後做一個板子物件,這邊偷懶直接讓所有的動作都在這Component裡面做判斷,這個只要放在場上隨意一個物件就可以。

public class Board : MonoBehaviour
{
    private Beaker dragBeaker;

    void Update()
    {
        //如果有拾取燒杯,就讓燒杯跟著滑鼠
        if (dragBeaker != null)
        {
            Vector2 pos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            dragBeaker.transform.position = pos;
        }

        //滑鼠點左鍵,檢查看是要拿取燒杯或是做動作
        if (Input.GetKeyDown(KeyCode.Mouse0))
        {
            RaycastHit2D[] hits = Physics2D.RaycastAll(Camera.main.ScreenToWorldPoint(Input.mousePosition), Vector2.zero);
            foreach (RaycastHit2D hit in hits)
            {
                if (dragBeaker == null) //沒有拿取燒杯的時候
                {
                    //只檢查是否點到燒杯
                    if (hit.transform.tag == "Beaker") dragBeaker = hit.transform.GetComponent<Beaker>();
                }
                else //有拿取燒杯的時候
                {
                    if (hit.transform == dragBeaker.transform) continue; //跳過拿取中的燒杯

                    //簡單做個判斷看點到什麼要做什麼
                    if (hit.transform.tag == "WaterInfinity")
                        dragBeaker.AddWater(true); //點到無窮水物件,就把燒杯水填滿
                    else if (hit.transform.tag == "WaterSink")
                        dragBeaker.AddWater(false); //點到水槽就把燒杯水倒掉
                    else if (hit.transform.tag == "Beaker")
                        hit.transform.GetComponent<Beaker>().AddWater(dragBeaker); //點到燒杯就把水倒過去
                    else if (hit.transform.tag == "WaterTarget")
                    {
                        Beaker b = hit.transform.GetComponent<Beaker>();
                        if (b.maxWater == dragBeaker.currentWater) //使用WaterTarget物件上Beaker的maxWater來做判斷
                        {
                            //達到目標,遊戲結束
                            Debug.Log("GameOver");
                        }
                    }
                }
            }
        }

        //點滑鼠右鍵就讓燒杯回原位
        if (Input.GetKeyDown(KeyCode.Mouse1))
        {
            dragBeaker.ReturnStartPosition();
            dragBeaker = null;
        }
    }
}




  大致上就這樣,雖然有些是寫死的,不過有達到目標即可,可以再修改許多地方讓整個機制更彈性,像是亂數決定中間兩個燒杯的大小、目標水量之類的,可以讓整個稍微有點變化。



No comments:

Post a Comment