1. 程式人生 > >Unity xLua學習之開發消消樂小遊戲

Unity xLua學習之開發消消樂小遊戲

一、前言

這兩天用xLua開發了一個消消樂小遊戲,在此記錄並總結一下開發過程中遇到的問題和體會。

二、效果圖

這裡寫圖片描述

三、實現功能

  • 消除同行或同列存在連續三個及以上相同的方塊
  • 射線檢測玩家點選到了哪個方塊
  • 玩家交換方塊後,如果存在1中的條件,則進行消除,否則返回原始位置

四、實現過程

1、匯入xlua外掛
2、新建C#指令碼GlopsByLua,用於載入Lua指令碼Main.lua.txt(參考Xlua的Example中的LuaBehaviour.cs)

public class GlopsByLua : MonoBehaviour
{

    public static
GlopsByLua Instance { get; private set; } private Action luaStart; private Action luaUpdate; private Action luaOnDestroy; public LuaEnv luaEnv; private LuaTable scriptEnv; private void Awake() { Instance = this; luaEnv = new LuaEnv(); scriptEnv = luaEnv.NewTable(); LuaTable meta = luaEnv.NewTable(); meta.Set("__index"
, luaEnv.Global); scriptEnv.SetMetaTable(meta); meta.Dispose(); scriptEnv.Set("self", this); luaEnv.DoString("require('Main')", "GlopsByLua", scriptEnv); Action luaAwake = scriptEnv.Get<Action>("awake"); scriptEnv.Get("start", out luaStart); scriptEnv.Get("update"
, out luaUpdate); scriptEnv.Get("ondestroy", out luaOnDestroy); if (luaAwake != null) luaAwake(); } private void Start() { if (luaStart != null) luaStart(); } private void Update() { if (luaUpdate != null) luaUpdate(); if(luaEnv != null) luaEnv.Tick(); } private void OnDestory() { if (luaOnDestroy != null) luaOnDestroy(); scriptEnv.Dispose(); luaEnv.Dispose(); } }

3、Main.lua.txt再載入 消消樂的主邏輯程式碼 Example.lua.txt

require('Example')

function start()
    print("Main start")
    Example.Start() 
end 


function update()
    --print("Main update")
    Example.Update()
end


function ondestory()
    print("Main ondestory")
end

4、Example.lua.txt檔案中編寫消消樂的邏輯程式碼

Example = {}

RowCount = 9;
--判定時:移動一格的時間
MoveSinglePositionTime = 0.1;
--交換時,移動的時間
SwitchTime = 0.5;
--棋子的預製體
mCubePrefabs ={};
--全部的棋子
mCubes={}
--陣列的索引表示列數, list表示
mMarkChessList = {}
--交換時第一次點選
mGoFirstClickChess = nil;
--交換時的第二次點選
mGoSecondClickChess = nil;
--現在是否有標記過的棋子
mIsMarkedChess = false;
--背景
mGoBackground = nil;
--開始移動時間
mStartMoveTime = 0;
--待移動的被標記的棋子
mMoveMarkChesses = {};
--待移動的非標記的棋子
mMoveNoMarkChesses = {};
--待移動的兩個交換的棋子
mMoveSwitchChesses = {};

function Example:Start()
    mGoBackground = CS.UnityEngine.GameObject.Find("Plane")
    --載入棋子預製體
    Example:LoadResource()
    --建立棋子
    Example:CreateChesses()

end

function Example:LoadResource()
    for index = 1,8 do
        --獲取全部的預製體
        mCubePrefabs[index] = CS.UnityEngine.Resources.Load("cube"..index)
    end
end

function Example:CreateChesses()
    for i = 0, 8 do
        for j = 0, 8 do
            --獲取隨機數,由於會獲取到浮點型的,所以利用上取整轉化為整型
            local range =math.ceil(CS.UnityEngine.Random.Range(0, 8));
            --獲取cube的預製體
            local cubePrefabs = mCubePrefabs[range];
            --例項化預製體
            local cubeItem = CS.UnityEngine.GameObject.Instantiate(cubePrefabs)
            --設定位置
            cubeItem.transform.position = CS.UnityEngine.Vector3(i, j, 0);
            --設定名字
            cubeItem.name = range;
            --設定父物體
            cubeItem.transform.parent = mGoBackground.transform;
            --建立二維陣列
            if(mCubes[i] == nil) then
                mCubes[i] = {}
            end
            --將棋子放二維陣列中,集中管理
            mCubes[i][j] = cubeItem;
        end
    end
end

--檢測是否同行或同列有連續三個及以上的相同棋子
function Example:CheckAndRefresh()
    Example:CheckAll()
    Example:Refresh()
end

function Example:InitMarkChesses()
    mIsMarkedChess = false;
    for i = 0, 8 do
        mMarkChessList[i]={}
    end
     mMoveMarkChesses = {};
     mMoveNoMarkChesses = {};
end

function Example:CheckAll()
    --清除之前標記的棋子
    Example:InitMarkChesses()
    --橫向掃描
    for i = 0, 6 do
        for j = 0, 8 do
            if ((mCubes[i][j].name == mCubes[i + 1][j].name) and (mCubes[i][j].name == mCubes[i + 2][j].name))
            then
                --橫著相同,標記這三個
                Example:MarkChess(i, j);
                Example:MarkChess(i + 1, j);
                Example:MarkChess(i + 2, j);
            end
        end
    end
    --縱向掃描
    for i = 0, 8 do
        for j = 0, 6 do
            if ((mCubes[i][j].name == mCubes[i][j+ 1].name) and (mCubes[i][j].name == mCubes[i][j+ 2].name))
            then
                --豎著相同,標記這三個
                Example:MarkChess(i, j);
                Example:MarkChess(i, j + 1);
                Example:MarkChess(i, j + 2);
            end
        end
    end
end

function Example:MarkChess(x, y) 
    local markChessCount = #mMarkChessList[x] + 1;
    for index = 1, markChessCount do
        if(mMarkChessList[x][index] == mCubes[x][y]) then
            --該棋子已被標記,直接退出
            return;
        end
    end

    if(mMarkChessList[x][markChessCount] == nil) then
        mMarkChessList[x][markChessCount] = mCubes[x][y]
        mIsMarkedChess = true;
    end
end

function Example:Refresh()
    for col = 0, 8 do
        --獲取當前列的 被標記的棋子的數量
        curColMarkChessesCount = #mMarkChessList[col]
        if(curColMarkChessesCount > 0) then
            --標記的棋子被清除後,需要將被標記棋子的上方的非標記棋子移動到下方,進行填充
            for row = 0, 8 do
                --非標記的棋子要下降的高度
                local notMarkDescend = 0
                for k = 1, curColMarkChessesCount do
                    --說明當前棋子是 已被標記的棋子, 我們目前是調整非標記棋子的位置,所以直接退出當前迴圈
                    if(mCubes[col][row] == mMarkChessList[col][k]) then
                        notMarkDescend = 0;
                        break;
                    end
                    if(mCubes[col][row].transform.position.y > mMarkChessList[col][k].transform.position.y) then
                        notMarkDescend = notMarkDescend + 1;
                    end
                end
                if(notMarkDescend > 0) then
                    mCubes[col][row - notMarkDescend] = mCubes[col][row];
                    --移動棋子
                    local startPos = mCubes[col][row - notMarkDescend].transform.position;
                    local endPos =  CS.UnityEngine.Vector3(col, row-notMarkDescend, 0);
                    local time = notMarkDescend * MoveSinglePositionTime + 0.3
                    mMoveNoMarkChesses[#mMoveNoMarkChesses + 1] = {mCubes[col][row - notMarkDescend], startPos, endPos, time}
                end
            end
            --將被標記的棋子,全部移至頂部
            for i = 1, curColMarkChessesCount do
                markIndex = 0;
                for j = 1, curColMarkChessesCount do
                    --按照移動前的高低放置
                    if(mMarkChessList[col][i].transform.position.y < mMarkChessList[col][j].transform.position.y) then
                        markIndex = markIndex + 1;
                    end
                end
                mCubes[col][8 - markIndex] = mMarkChessList[col][i];
            end
            --curColMarkChessesCount大於0,當前位於頂部的棋子,都是被標記的棋子了
            for row = (9 - curColMarkChessesCount), 8 do

                local moveDistance = row + RowCount;
                if( row + moveDistance > 12) then
                    moveDistance = 12 - row;
                end
                --重置棋子,更換棋子游戲物體名稱、位置、材質
                mCubes[col][row].transform.position = CS.UnityEngine.Vector3(col, row + moveDistance, 0);
                local random = math.ceil(CS.UnityEngine.Random.Range(0, 8));
                mCubes[col][row]:GetComponent(typeof(CS.UnityEngine.MeshRenderer)).material = mCubePrefabs[random]:GetComponent(typeof(CS.UnityEngine.MeshRenderer)).sharedMaterial;
                mCubes[col][row].name = ""..random;
                local startPos = mCubes[col][row].transform.position;
                local endPos = CS.UnityEngine.Vector3(col, row, 0);
                mMoveMarkChesses[#mMoveMarkChesses + 1] ={mCubes[col][row], startPos, endPos, SwitchTime};
            end
        end
    end
    if ((#mMoveMarkChesses > 0) or (#mMoveNoMarkChesses > 0)) then
        mStartMoveTime = CS.UnityEngine.Time.time;
    end
end

--[[
    傳入引數:一個待移動的二維陣列,陣列單項引數如下
    index 1 : gameObject
    index 2 : 開始移動的位置
    index 3 : 目標位置
    index 4 : 所費時間
]]--
function Example:MoveTo(chesses)
    local chessCount = #chesses;
    if(chessCount <= 0)then
        return false;
    end

    local finishCount = 0;
    for index = 1, chessCount do
        local goTarget = chesses[index][1];
        local startPos = chesses[index][2];
        local endPos = chesses[index][3];
        local time = chesses[index][4];

        local position = CS.UnityEngine.Vector3.Lerp(startPos, endPos, (CS.UnityEngine.Time.time - mStartMoveTime) / time);

        goTarget.transform.position = position;

        if(CS.UnityEngine.Vector3.Distance(endPos, position) == 0) then
            finishCount = finishCount + 1;
        end
    end
    return finishCount == chessCount;
end 

function Example:Update()
    if(CS.UnityEngine.Time.frameCount == 60)  then
        Example:CheckAndRefresh()
    end
    --Move 先讓非標記棋子移動, 再讓標記棋子移動,最後如果有棋子交換,則讓棋子交換
    if(#mMoveNoMarkChesses > 0) then
        if(Example:MoveTo(mMoveNoMarkChesses)) then
            mMoveNoMarkChesses = {};
            mStartMoveTime = CS.UnityEngine.Time.time;
        end
    elseif(#mMoveMarkChesses > 0) then
        if(Example:MoveTo(mMoveMarkChesses)) then
            mMoveMarkChesses = {};
            --被銷燬的棋子,下降完畢後再重新整理一次
            Example:CheckAndRefresh();
            mStartMoveTime = CS.UnityEngine.Time.time;
        end
    elseif(#mMoveSwitchChesses > 0) then --棋子交換
        if(Example:MoveTo(mMoveSwitchChesses)) then
            mMoveSwitchChesses = {};
            --交換過後,檢查是否有棋子被標記
            Example.CheckAll();
            if(mIsMarkedChess) then
                --交換後,有棋子被標記
                Example.Refresh();
            else
                --如果沒有標記棋子,那麼就讓棋子返回交換前的位置
                Example:SwtichChess();
            end
            Example:CancelSeleted();
        end
    else --沒有棋子移動,判斷是否有使用者輸入
        if(CS.UnityEngine.Input.GetMouseButtonDown(0)) then
            Example:RaycastChess();
        end
    end
end

function Example:RaycastChess()
    local ray = CS.UnityEngine.Camera.main:ScreenPointToRay(CS.UnityEngine.Input.mousePosition)
    if(CS.UnityEngine.Physics.Raycast(ray) == false) then return; end
    local hit = CS.UnityEngine.Physics.RaycastAll(ray)[0];
    if(hit.transform.parent == mGoBackground.transform) then
        --獲取到了被點選的棋子了
        if(Example:IsSwitchable(hit.transform.gameObject)) then
            Example:SwtichChess()
        end
    else
        --取消選中
        Example:CancelSeleted();
    end
end

--取消交換
function Example:CancelSeleted()
    if(mGoFirstClickChess == nil) then
        return;
    end
    mGoFirstClickChess:GetComponent(typeof(CS.UnityEngine.MeshRenderer)).material.color = CS.UnityEngine.Color.white;
    mGoFirstClickChess = nil;
    mGoSecondClickChess = nil;
end

--檢測能否交換(基本檢查:連續兩次點選是否為同種型別、距離不為1)
function Example:IsSwitchable(goHit)
    if(mGoFirstClickChess == nil) then
        --第一次點選,選中棋子
        mGoFirstClickChess = goHit;
        mGoFirstClickChess:GetComponent(typeof(CS.UnityEngine.MeshRenderer)).material.color = CS.UnityEngine.Color.red;
        --直接退出,等待第二次點選
        return false;
    end
    --點選同種型別的棋子
    if(mGoFirstClickChess.name == goHit.name) then
        Example:CancelSeleted();
        return false;
    end
    --超過距離範圍,  取1.1和0.9 是防止Unity在移動時的bug(可能會具體座標不是整數)
    local distance = CS.UnityEngine.Vector3.Distance(goHit.transform.position, mGoFirstClickChess.transform.position);
    if((distance > 1.1) or (distance < 0.9)) then
        Example:CancelSeleted();
        return false
    end
    mGoSecondClickChess = goHit;
    return true;
end

function Example:SwtichChess()
    --這裡需要判空,因為在退回交換前的位置時,退回完畢後,還會呼叫此方法,此時,mGoFirstClickChess與mGoSecondClickChess已經被取消選中而置空了
    if ((mGoFirstClickChess == nil) and (mGoSecondClickChess == nil)) then
        return
    end
    local posFirst = mGoFirstClickChess.transform.position;
    local posSecond = mGoSecondClickChess.transform.position;
    mCubes[Round(posFirst.x)][Round(posFirst.y)] = mGoSecondClickChess;
    mCubes[Round(posSecond.x)][Round(posSecond.y)] = mGoFirstClickChess;

    mStartMoveTime = CS.UnityEngine.Time.time;
    mMoveSwitchChesses[1] = {mGoFirstClickChess, posFirst, posSecond, SwitchTime}
    mMoveSwitchChesses[2] = {mGoSecondClickChess, posSecond, posFirst, SwitchTime}
end

--四捨五入
function Round(value)
    if(math.ceil(value) <= value + 0.5) then
        return math.ceil(value)
    else
        return math.floor(value)
    end
end

五、遇到的問題及解決辦法

問題1:射線檢測問題
在C#中 直接用個RaycastHit接收檢測到的物體,但是在lua中沒法用out關鍵字,因為xlua中對於out的解決辦法是通過作為第二返回值(如果函式原本就有返回值的話)的方式,而Physics.Raycast(ray)恰好又是另一個過載函式,所以 Physics.Raycast(ray, out hit)就沒法用了。
解決辦法:通過Physics.RaycastAll(ray)獲取到所有被檢測的物體

六、小技巧(可能也不是)

在使用xlua開發這個消消樂遊戲之後,知道了下面幾個小技巧

  • 通過#table + 1模擬一個List的add方法
if( table[#table + 1] == nil ) then
    table[#table + 1] = value
end
  • 由於xlua只能支援lua本身的所有型別的過載,而C#中的int和float,對lua而言都是Number型別
    比如獲取UnityEngine的隨機數
range = math.ceil(CS.UnityEngine.Random.Range(0, 8));
  • lua中的math不支援四捨五入,自己寫了個這樣的方法
--四捨五入
function Round(value)
    if(math.ceil(value) <= value + 0.5) then
        return math.ceil(value)
    else
        return math.floor(value)
    end
end

七、原始碼下載