一、前言
这两天用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