Lua实现Unity堆叠消除小游戏摘要

Lua实现Unity堆叠消除小游戏,游戏功能每次随机生成四个不同形状的方块,将方块拖入10*10的格子棋盘,如果放入的棋盘格都有空格则可以放入方块,当放满一行或者一列时,消除满行满列的放入格。

- 创建拼图地图格数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
---拼图地图格数据
---@class ROPuzzleMapTileData
local m = {
---@type number 位置标记
index = 0,
---@type number 所在行
x = 0,
---@type number 所在列
y = 0,
---@type number 地图格上放置的方块ID 为0 表示空格
blockId = 0,
}

function m.New(index,x,y,id)
local o = Clone(m)
o:Init(index,x,y,id)
return o
end

function m:Init(index,x,y,id)
self.index = index
self.x = x
self.y = y
self.blockId = id
end

return m

- 创建拼图游戏数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
---@class ROPuzzleGameData
local m = {
---@type CfgRestaurantOperationPuzzleControlData
cfgPuzzleGameControlData = nil,
---@type number 当前积分
score = 0,
---@type ROPuzzleMapTileData[]
tileMaps = {},
---@type number 最大行数
maxLine = 10,
---@type number 最大列数
maxColumn = 10,
---@type number 单次生成的方块数量(上限为4)
randomBlock = {},
---@type boolean 游戏是否已经开始(点了开始按钮视为游戏开始)
isGameStart = false,
}

local ROPuzzleMapTileData = require("IQIGame.Module.CommonActivity.RestaurantOperation.ROPuzzleGame.ROPuzzleMapTileData")

function m.New(cfgData)
local o = Clone(m)
o:Init(cfgData)
return o
end

function m:Init(cfgData)
self.score = 0
self.cfgPuzzleGameControlData = cfgData
self.tileMaps = self:CreateTiles()
self.isGameStart = true
end

---开始新游戏,创建拼图地图
function m:CreateTiles()
local tab = {}
local index = 0
for line = 1, self.maxLine do
for column = 1, self.maxColumn do
index = index + 1
local tileData = ROPuzzleMapTileData.New(index,line,column,0)
tab[index] = tileData
end
end
return tab
end

---随机生成四个方块
function m:RefreshRandomBlock()
self.randomBlock = {}
local blockGroup = table.clone(self.cfgPuzzleGameControlData.BlockGroup)
---洗牌操作
for i = #blockGroup, 2, -1 do
local random_index = math.random(i)
blockGroup[i], blockGroup[random_index] = blockGroup[random_index],blockGroup[i]
end
---根据配置数据取方块
for i = 1, self.cfgPuzzleGameControlData.BlockNum do
local id = blockGroup[i]
table.insert(self.randomBlock,id)
end
end

---消除满行满列,并返回消除的行列
---@return number, number[],number[] 消除的总行数,同时消除的行和列
function m:Eliminate()
local lines = {}
local columns = {}
---行检测
for i = 1, self.maxLine do
local isFull = true
for j = 1, self.maxColumn do
local pos = (i-1) * self.maxLine + j
if self.tileMaps[pos].blockId == 0 then
isFull = false
break
end
end
if isFull then
table.insert(lines,i)
end
end
---列检测
for j = 1, self.maxColumn do
local isFull = true
for i = 1, self.maxLine do
local pos = (i-1) * self.maxLine + j
if self.tileMaps[pos].blockId == 0 then
isFull = false
break
end
end
if isFull then
table.insert(columns,j)
end
end
local totalNum = #lines + #columns
return totalNum,lines,columns
end

---增加积分
---@param score number
function m:AddScore(score)
self.score = self.score + score
end

---检测方块还有没有空地能放下
---@param blockCell PuzzleGameBlockCell
---@return boolean
function m:CheckBlockCellCanPutDown(blockCell)
local posTab = blockCell.blockData
for i, tileData in pairs(self.tileMaps) do
---找到了一个空格子
local canPutDown = false
if tileData.blockId == 0 then
---所有检测点都满足返回true
for j, pos in pairs(posTab) do

end
end
end
return false
end

return m

通过调用RefreshRandomBlock()方法,每次随机生成不重复的四个方块

- 创建地图格控件

根据ROPuzzleMapTileData数据生成地图格PuzzleGameMapTileCell。地图格包含背景,拖动经过效果,放入拖动方块三个节点,根据不同状态显示不同节点表现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
---@class PuzzleGameMapTileCell
local m = {
View = nil,
---@type ROPuzzleMapTileData
tileData = nil,
---@type UnityEngine.Collider2D
collider2D = nil
}

function m.New(view)
local obj = Clone(m)
obj:Init(view)
return obj
end

function m:Init(view)
self.View = view
LuaCodeInterface.BindOutlet(self.View, self)

self.Effect:SetActive(false)

self.collider2D = self.View:GetComponent(typeof(UnityEngine.Collider2D))

self:AddListener()
end

function m:AddListener()

end

function m:RemoveListener()

end

---@param data ROPuzzleMapTileData
function m:SetData(data)
self.tileData = data
self.View.name = self.tileData.index
self:UpdateView()
end

function m:ShowPassNode(top)
self.PassNode:SetActive(top)
end

function m:UpdateView()
self:ShowPassNode(false)
self.ImgBlock:SetActive(self.tileData.blockId > 0)
if self.tileData.blockId > 0 then
---@type CfgRestaurantOperationBlockListData
local cfgData = CfgRestaurantOperationBlockListTable[self.tileData.blockId]
local path = UIGlobalApi.GetImagePath(cfgData.ImageRes)
AssetUtil.LoadImage(self,path,self.ImgBlock:GetComponent("Image"))
end
end

---播放成功消除特效
function m:PlayEffect()
if self.timer then
self.timer:Stop()
end
self.Effect:SetActive(true)
self.timer = Timer.New(function ()
self.Effect:SetActive(false)
end,0.5)
self.timer:Start()

end

function m:Dispose()
AssetUtil.UnloadAsset(self)
self:RemoveListener()
if self.timer then
self.timer:Stop()
end
self.timer = nil

LuaCodeInterface.ClearOutlet(self.View, self)
UnityEngine.Object.Destroy(self.View)
self.collider2D = nil
self.View = nil
end

return m

- 创建随机生成的方块控件

通过prefab实例化不同的方块控件,每个方块包含多个小方格,每个小方格通过name属性标记位置,布置成各种形状。小方格挂Collider2D组件,用来检测拖动过程中和地图格进行碰撞检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
---拼图游戏随机生的方块格
---@class PuzzleGameBlockCell
local m = {
View = nil,
---@type number
cid = nil,
---@type function
onDrag = nil,
---@type function
endDrag = nil,
---@type UnityEngine.Collider2D
collider2DList = {},
---@type number 方块的组成格数量
blockNum = 0,
---@type boolean 是否被成功放入棋盘
isPutDown = false,
---@type table<number,number> []方块模型的占位信息
blockData = {},
}

function m.New(view)
local obj = Clone(m)
obj:Init(view)
return obj
end

function m:Init(view)
self.View = view
LuaCodeInterface.BindOutlet(self.View, self)

self.blockData = {}
self.blockNum = self.View.transform.childCount
for i = 0, self.View.transform.childCount - 1 do
local childObj = self.View.transform:GetChild(i).gameObject
local collider = childObj:GetComponent(typeof(UnityEngine.Collider2D))
self.collider2DList[i] = collider

local nameStr = childObj.name
local posTab = string.split(nameStr,"_")
table.insert(self.blockData,{tonumber(posTab[1]),tonumber(posTab[2])})
end

self.View:GetComponent("UIDrag").onBeginDrag = function(eventData) self:OnBeginDrag(eventData) end
self.View:GetComponent("UIDrag").onDrag = function(pointerEventData) self:OnDrag(pointerEventData) end
self.View:GetComponent("UIDrag").onReachTargetSuccess = function(eventData) self:OnDragSuccess(eventData) end
self.View:GetComponent("UIDrag").onReachTargetFailure = function(eventData) self:OnFailure(eventData) end

self:AddListener()
end

function m:AddListener()

end

function m:RemoveListener()

end

function m:OnBeginDrag()

end

function m:OnDrag(eventData)
if self.onDrag then
self.onDrag(self)
end
end

function m:OnDragSuccess(eventData)
if self.endDrag then
self.endDrag(self)
end
end

function m:OnFailure(eventData)
self.View.transform.position = self.startPos
end

---成功放入棋盘准备回收
function m:PutDownMap()
self.isPutDown = true
self.View:SetActive(false)
self.View.transform.position = self.startPos
end

---@param id number
function m:SetData(id)
self.isPutDown = false
self.cid = id
self.startPos = self.View.transform.position
end

function m:Dispose()
self.collider2DList = {}
AssetUtil.UnloadAsset(self)
self:RemoveListener()

LuaCodeInterface.ClearOutlet(self.View, self)
UnityEngine.Object.Destroy(self.View)
self.View = nil
end

return m

- 游戏流程实现

点击开始游戏,根据游戏数据创建地图,生成随机方块,拖动方块放入地图,检测方块能不能正确放入地图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
---拖动方块
---@param cell PuzzleGameBlockCell
function RestaurantGameBlocksUI:OnDrag(cell)
local tab = self:GetBlockOccupiedAreas(cell)
for i, v in pairs(self.mapTileCells) do
local isShow = table.indexOf(tab,v.tileData.index) ~= -1
v:ShowPassNode(isShow)
end
end

---成功将方块拖入目标区域
---@param cell PuzzleGameBlockCell
function RestaurantGameBlocksUI:OnEndDrag(cell)
for i, mapTile in pairs(self.mapTileCells) do
mapTile:ShowPassNode(false)
end
---拖放结束后,在棋盘区域所占据的地图格
local tab = self:GetBlockOccupiedAreas(cell)
---判断能否成功放入,方块内所有格子都能放入棋盘空格中
local emptyTileNum = 0
for i, v in ipairs(tab) do
if self.mapTileCells[v].tileData.blockId == 0 then
emptyTileNum = emptyTileNum + 1
end
end
---能成功放下
if emptyTileNum == cell.blockNum then
for i, v in ipairs(tab) do
---将方块所携带的格子数据赋值地图格
self.mapTileCells[v].tileData.blockId = cell.cid
---更新地图显示
self.mapTileCells[v]:UpdateView()
---将拖放的格子隐藏等待回收
cell:PutDownMap()
end
---检测要不要生成新的方块
self:CheckBlocksNum()
---检测能否消除满行满列的格子
self:CheckEliminate()
else
cell:OnFailure() ---不能成功放入,方块回位
end
---检测游戏结束
self:CheckEndGame()
end

---检测能否消除满行满列的格子
function RestaurantGameBlocksUI:CheckEliminate()
local totalNum,lines,columns = ROPuzzleGameModule.roPuzzleGameData:Eliminate()
---消除棋盘上对应的格子显示
---消除行
for i, v in ipairs(lines) do
for col = 1, ROPuzzleGameModule.roPuzzleGameData.maxColumn do
local pos = (v-1)*ROPuzzleGameModule.roPuzzleGameData.maxColumn + col
self.mapTileCells[pos]:PlayEffect()
self.mapTileCells[pos].tileData.blockId = 0
self.mapTileCells[pos]:UpdateView()
end
end
---消除列
for i, col in ipairs(columns) do
for line = 1, ROPuzzleGameModule.roPuzzleGameData.maxLine do
local pos = (line - 1)*ROPuzzleGameModule.roPuzzleGameData.maxLine + col
self.mapTileCells[pos]:PlayEffect()
self.mapTileCells[pos].tileData.blockId = 0
self.mapTileCells[pos]:UpdateView()
end
end
local isPlaySound = totalNum > 0
if isPlaySound then
GameEntry.Sound:PlaySound(10802, Constant.SoundGroup.ENVIRONMENT)
end
---加积分
local addScore = ROPuzzleGameModule.GetAddScore(totalNum)
ROPuzzleGameModule.roPuzzleGameData:AddScore(addScore)
ROPuzzleGameModule.Puzzle()
self:RefreshScore()
end

---获取剩下的方块
---@return PuzzleGameBlockCell[]
function RestaurantGameBlocksUI:GetRemainingBlocks()
local blockTab = {}
for i, blocks in pairs(self.blockCells) do
for j, blockCell in pairs(blocks) do
if not blockCell.isPutDown then
table.insert(blockTab,blockCell)
end
end
end
return blockTab
end

---检测方块数量 如果没有方块创建新的方块
function RestaurantGameBlocksUI:CheckBlocksNum()
local blocks = self:GetRemainingBlocks()
if #blocks == 0 then
ROPuzzleGameModule.roPuzzleGameData:RefreshRandomBlock()
self:CreateBlocks()
end
end


---检测格子能否成功放入棋盘
---@param cell PuzzleGameBlockCell
function RestaurantGameBlocksUI:CheckPutDownBlock(cell)
local tab = {}
for i, collider in pairs(cell.collider2DList) do
for j, mapTile in pairs(self.mapTileCells) do
local res = self:CheckBoundsIntersects(collider,mapTile.collider2D)
if res then
table.insert(tab,mapTile.tileData.index)
end
mapTile:ShowPassNode(false)
end
end
for i, v in pairs(tab) do
self.mapTileCells[v]:ShowPassNode(true)
end
end

---返回方块格所占的棋盘区域
---@param cell PuzzleGameBlockCell
---@return number[] 对应的棋盘区域格
function RestaurantGameBlocksUI:GetBlockOccupiedAreas(cell)
local tab = {}
for i, collider in pairs(cell.collider2DList) do
for j, mapTile in pairs(self.mapTileCells) do
local res = self:CheckBoundsIntersects(collider,mapTile.collider2D)
if res then
table.insert(tab,mapTile.tileData.index)
end
end
end
return tab
end

---检测两个格子是否有交集
---@return boolean
function RestaurantGameBlocksUI:CheckBoundsIntersects(collider2D1,collider2D2)
return collider2D1.bounds:Intersects(collider2D2.bounds)
end