引言
本篇文章是本人使用ssRender引擎,配合使用Lua脚本语言,开发一个俄罗斯方块小游戏的案例。本篇文章将会详细介绍开发初期的构想、前端界面的布局、各个按钮的功能和部分俄罗斯方块功能的实现(包括界面渲染、方块初始化、左移、右移等功能)
一:界面准备
本篇幅主要介绍了通过ssRender引擎,制作一个简易的前端游戏界面
(一)资源导入
如图,导入背景方格资源,设置按钮,旋转、左移、右移、下落按钮的图片,按照如图布局
(二)方格设置
为了易于实现渲染函数的编写,且此游戏的方格场景有限,所以使用ssRender引擎对应方格数量设置多个Item节点(如图,该场景Size为20x10)。
按照如图所示的结构与命名规则,设置行列Item。设置Color为红色,visible显隐属性均为1。
如图,这是刚进入场景时的显示样式,在通过Lua脚本与ssRender封装的Lua接口对界面上的各个小方块进行显隐的控制,就能够实现俄罗斯方块的显示,移动,消除等效果!坐稳了,要发车了!
二:设计想法
本篇幅主要介绍:将Lua语言与游戏各个元素相对应的设计想法
(一)方块设计
如图所示,俄罗斯方块一共有七中形状,分别为I型、T型、L型、反L型、S型、Z型以及O型。其他的形状均是由这其七种类型旋转变化所得!
所以可以先设计这七种类型的方块,再使用适当的旋转方法,即可得到全部形状的俄罗斯方块!
又发现,I型的方块在横向摆放时占用横向四格、竖向摆放时占用竖向四格,所以我们只需一个4X4的二维数组(在Lua中称为二维表)即可将所有的方块摆放进去!
--I型方块
Block_I_1 = {
{0,1,0,0},
{0,1,0,0},
{0,1,0,0},
{0,1,0,0}
}
--T型方块
Block_T_1 = {
{0,0,0,0},
{0,0,0,0},
{1,1,1,0},
{0,1,0,0}
}
--L型方块
Block_L_1 = {
{0,0,0,0},
{0,1,0,0},
{0,1,0,0},
{0,1,1,0}
}
--反L型方块
Block_BL_1 = {
{0,0,0,0},
{0,1,0,0},
{0,1,0,0},
{1,1,0,0}
}
--S型方块
Block_S_1 = {
{0,0,0,0},
{0,0,0,0},
{0,1,1,0},
{1,1,0,0}
}
--Z型方块
Block_Z_1 = {
{0,0,0,0},
{0,0,0,0},
{1,1,0,0},
{0,1,1,0}
}
--O型方块
Block_O_1 = {
{0,0,0,0},
{0,0,0,0},
{0,1,1,0},
{0,1,1,0}
}
(二)方块旋转设计
方案1:找方块的中心点进行旋转
可以根据基本形状所对应的二维表,根据不同方块存放对应的中心点坐标值(注意,Lua与C++的起始位置不同,C++数组从0开始,Lua表从1开始);再通过中心点交换方块的二维表内的值实现方块旋转的效果。
虽然感觉很好,占用空间的情况也很优秀,但是在随机出现不同方格时,28种方格(7种方块x4种旋转角度)相同概率出现较难实现,并且在计算不同方块的旋转算法时为了避免出现BUG,要写不同的算法,情况很复杂。
所以,本案例使用的是简单粗暴的方案二
方案2:枚举全部的方块情况
同方块设计中七种不同的二维表,将不同旋转角度的方块全部使用二维表枚举出来!
此方案虽然在空间上要多申请21个4x4的二维表,但是不用再繁琐的设计旋转算法,简单而又粗暴!
Block_T_0 = {
{0,0,0,0},
{0,1,0,0},
{0,1,1,0},
{0,1,0,0}
}
Block_T_1 = {
{0,0,0,0},
{0,0,0,0},
{1,1,1,0},
{0,1,0,0}
}
Block_T_2 = {
{0,0,0,0},
{0,1,0,0},
{1,1,0,0},
{0,1,0,0}
}
Block_T_3 = {
{0,0,0,0},
{0,0,0,0},
{0,1,0,0},
{1,1,1,0}
}
以T型方块为例,大家可以试着实现其他方块的所有旋转情况。
注意:为了防止出现如下情况:旋转之后,间隔行数变大(如下方的I_1与I_2GiveUp所示)。这种“极限续命”的情况,我们统一把方块的最低点放置在4x4二维表的最后一行!(即将下面的I_2GiveUp统一改成I_2形式)
Block_I_1 = {
{0,1,0,0},
{0,1,0,0},
{0,1,0,0},
{0,1,0,0}
}
Block_I_2GiveUp = {
{0,0,0,0},
{1,1,1,1},
{0,0,0,0},
{0,0,0,0}
}
Block_I_2 = {
{0,0,0,0},
{0,0,0,0},
{0,0,0,0},
{1,1,1,1}
}
之后,把七种方块对应的不同旋转情况均放置到一个表中,方便我们通过随机数算法,平均概率的取到每一种方块;同时我们也要按照一定规律对方块表进行排列,以实现索引+1为顺时针旋转90度,索引-1为逆时针旋转90度的效果:
BlockList={ --方块表
[1] = {Block_I_0,Block_I_1,Block_I_2,Block_I_3},--3,2,1,0为逆时针;0123为顺时针
[2] = {Block_T_0,Block_T_1,Block_T_2,Block_T_3},
[3] = {Block_L_0,Block_L_1,Block_L_2,Block_L_3},
[4] = {Block_BL_0,Block_BL_1,Block_BL_2,Block_BL_3},
[5] = {Block_S_0,Block_S_1,Block_S_2,Block_S_3},
[6] = {Block_Z_0,Block_Z_1,Block_Z_2,Block_Z_3},
[7] = {Block_O_0,Block_O_1,Block_O_2,Block_O_3}
}
(三)场景刷新设计
如前端界面所显示,该场景Size为20行x10列,方块分为当前正在掉落的方块和落在下方物块的固定不动的方格。
如果当前方块每降落一行,就要把整个场景全部渲染一遍,就太粗暴、系统开销会上升很多!
所以产生了如下设计想法:
我们可以将固定不动的方块划分为一部分(称为bg部分),把当前正在掉落的方块划分为一部分(称为CurrentBlock部分),方块每次掉落的时候,只需要去渲染方格所在的4x4区域即可。
当方格不能在降落,即落到地面上之后(称为触底),再将方格添加到bg部分,执行一次渲染bg部分的操作,就能极大提高我们场景的刷新效率。bg初始化方法如下:
bg={}
function InitBG()
for i=1,20 do
bg[i] = {}
for j=1,10 do
bg[i][j] = 0
end
end
end
那么问题来了,我们如何确定当前方块(CurrentBlock)在整体bg上所在的位置呢?
只需要两个参数!那就是当前方块(CurrentBlock)所在的行(CurrentRow)与所在的列(CurrentColumn);同时为了方便记录方块形状、实现旋转效果,也要记录当前方块在方块表中的位置索引BlockListID(在方块表的位置,即行数),subID(形状表的位置,即方块表的列数),如下:
currentBlock = {
['blockListID'] = 1, --在方块表的位置 I T L BL S Z O
['subID'] = 1, --形状表的位置 旋转变化0 1 2 3
['curRow'] = 1, --block[1][1]处 在主背景的行数
['curColumn'] = 1, --block[1][1]处 在主背景的列数
['block'] = {} -- 方块现在具体的形状{{},{},{},{}}形式
}
需要注意方块所在Bg的行数与列数是:方块二维表[1, 1]所在的位置,即此方格所在整体BG上所在的位置。
这样,我们每秒钟调用一次方块下落函数+渲染当前方格函数;当方块触底之后,我们再调用当前方块插入Bg函数 + 渲染整体Bg函数,就能够轻松实现分组刷新的效果,显著提升效率!
三:功能实现
本篇幅将会详细介绍渲染功能,方块随机生成功能,方块左侧、右侧墙壁检测以及方块左移,右移功能的详细实现
(一)渲染功能实现
1.BG渲染
Lua代码如下:
--根据bg表 渲染 整体的BackGround
function RenderBG()
local setPropertyTable = {}
for i = 1, #bg do
for j = 1, #bg[1] do
setPropertyTable["Layer0//Item0//Row"..i.."//Item"..j.."//Visible"] = ""..bg[i][j]..""
end
end
ssr.setNodeProperty(setPropertyTable)
end
bg是行数20x列数10的二维表,#bg用于获取bg的行数,#bg[1]用于获取列数(即bg第一行的元素个数)。
还记得第一节中的规范的命名规则吗?就是为了在此处更方便的将前端节点与bg行列所对应;其中的ssr.setNodeProperty()方法则是ssRender引擎封装好的接口函数,根据表的索引值获取对应节点的visible属性,通过表的值来设置属性值!
其中..是Lua自带的字符串拼接的符号,由于ssr.setNodeProperty()方法所识别的表中参数需要是string字符串类型,所以我们要把bg对应的int型参数拼接为字符串(""..bg[i][j].."")才能正确渲染!
2.当前方块渲染
Lua代码如下:
function RenderCurrentBlock()
local setPropertyTable = {}
local block = currentBlock['block']
for i = 1, #block do
for j = 1, #block[1] do
local row = currentBlock['curRow'] + i - 1
local cloumn = currentBlock['curColumn'] + j -1
if block[i][j] == 0 and bg[row][cloumn] == 0 then
setPropertyTable["Layer0//Item0//Row"..row.."//Item"..cloumn.."//Visible"] = "0"
else
setPropertyTable["Layer0//Item0//Row"..row.."//Item"..cloumn.."//Visible"] = "1"
end
end
end
ssr.setNodeProperty(setPropertyTable)
end
已知currentBlock['block']存储的是当前方块的具体形状,是一个4x4的二维表,从上到下,从左到右的遍历当前方块的表,由于Lua的索引值是从1开始,所以currentBlock['curRow'] + i - 1就是正在遍历到的方格在整体bg所在的相对行数;currentBlock['curColumn'] + j -1就是正在遍历到的方格在整体bg所在的相对列数。
当相对位置处的bg方格为白色(不显示,即bg[row][cloumn] == 0),以及当前方块的当前方格也为白色(此处无方块,即block[i][j] == 0)才让此方格不显示,即visible = 0(都无方格则无显示);除此之外,无论是bg的对应方格处有方块,还是当前方块的当前方格有显示,均将visible 设置为1(任一有方格则显示)。
3.取消当前方块的渲染
Lua代码如下:
function CancelCurrentBlock()
local setPropertyTable = {}
local block = currentBlock['block']
for i = 1, #block do
for j = 1, #block[1] do
local row = currentBlock['curRow'] + i - 1
local cloumn = currentBlock['curColumn'] + j -1
if bg[row][cloumn] == 0 then
setPropertyTable["Layer0//Item0//Row"..row.."//Item"..cloumn.."//Visible"] = "0"
else
setPropertyTable["Layer0//Item0//Row"..row.."//Item"..cloumn.."//Visible"] = "1"
end
end
end
ssr.setNodeProperty(setPropertyTable)
end
此处逻辑与渲染当前方块的逻辑一致。
为什么要写一个当前方块取消渲染的函数呢?
因为,我们左移、右移、下落的时候,为了性能考虑,没有从新渲染整体的bg!所以,在不取消当前渲染的情况下,去渲染下一个状态的方块,就会有显示上的bug(比如是方块的残留)!
所以我们在调用左移、右移、下落函数之前,先取消当前方块的渲染,能否移动的条件以及移动前后的状态有移动函数控制(左移就是当前方块所在列数 - 1,右移 + 1,下落即当前行数+1),然后再从新渲染当前方块。在降低效率的同时,也能避免BUG的出现!
(二)方块随机生成功能实现
1.方块二维表的深拷贝
由于Lua对二维表之间的赋值操作,与C++对数组的赋值操作一致,是基于引用的(也就是地址拷贝而不是值拷贝)。这意味着当你将一个表赋值给另一个变量时,你实际上是在复制这个表的引用,而不是表的内容。当更改二者之一时,另一个表也会被更改!
为了防止对当前方块的方块表currentBlock['block']操作,而改变总方块表BlockList中的元素,我们就需要写一个值拷贝的克隆函数:
function Clone( src, dest )
for k,v in pairs(src) do
if type(v) == 'table' then
dest[k] = {}
Clone(v, dest[k])
else
dest[k] = v
end
end
end
如上,是表的值拷贝的标准递归函数,将src表的值赋给dest表。
2.方块随机生成
Lua代码如下:
function CreatNewBlock()
local current_time = os.time()
math.randomseed(current_time) -- 只设置一次种子,之后调用math.random()将生成不同的随机数
currentBlock['blockListID'] = math.random(1,8)
currentBlock['subID'] = math.random(1,5)
currentBlock['curRow'] = 1 --待更改
currentBlock['curColumn'] = 4
--currentBlock['block'] = BlockList[currentBlock['blockListID']][currentBlock['subID']]
Clone(BlockList[currentBlock['blockListID']][currentBlock['subID']], currentBlock['block'])
end
设置随机数种种子;
math.random(1, 8) -- 生成一个1到8之间的随机整数(包括1,不包括8),取得当前方块的形状;
math.random(1, 5) -- 生成一个1到5之间的随机整数(包括1,不包括5),取得当前方块的旋转形状;
Clone()方法,获取当前方块的具体形状。
方块生成功能测试(ssRender Edit工具端内):
1.首先先创建一个绿色的按钮
2.再新建一个Lua脚本,命名为creatBlock,双击打开
调用生成函数和渲染方块的函数
3.将绿色按钮的click事件触发调用creatBlock脚本
4.点击按钮,检查效果
效果如图所示,达到预期,我们继续往下来写左移与右移的方法!
(三)方块左移右移功能实现
1.左侧墙体检测
在方块向左移动时,要对方块左侧墙体进行预检测,假设移动完成后的方块,与现有的墙体以及固定的方格有无碰撞;有碰撞则返回true,无碰撞则返回false
function CheckLeftWall()
local block = currentBlock['block']
local row = currentBlock['curRow']
local column = currentBlock['curColumn']
--对应方块的正左侧有墙面 则返回true 从下到上,从左到右
for i = #block , 1 , -1 do
for j = 1, #block[1] do
if block[i][j] == 1 then
--所在列数 == 1的时候,碰到墙壁,不可左移,左侧墙为true
if column + j - 1 <= 1 then
return true
end
--行是当前行,列是前一列,
if bg[row + i-1][column +j -2] == 1 then
return true
end
end
end
end
return false
end
解析:由于俄罗斯方块从下方到上方逐渐堆积,现在也要实现向左侧移动,所以从下到上,从左到右依次遍历当前方格;如果小方格所在的列数<= 1,则说明已经靠近左侧墙壁,不能再向左移动,返回false;比较bg上对应方格的当前行,前一列有无物块阻拦,如果有则不能向左移动返回true;
2.向左移动
function LeftMove()
if CheckLeftWall() then
return
else
currentBlock['curColumn'] = currentBlock['curColumn'] - 1
end
end
首先,左侧墙壁预检测,如果有墙壁,函数执行结束;若没有墙壁,则将当前方块所在的列数减一;
右侧墙体检测与右移功能类比左移即可,代码部分很相似,只需注意二维表的遍历顺序改为从下到上,从右到左;以及最大列数为10即可!
左移功能测试(ssRender Edit工具端内):
1.创建leftMove的Lua脚本
2.双击,打开脚本,在脚本内调用上方函数
3.左移动按钮新增click事件调用leftMove脚本
4.点击按钮检查效果
效果如图所示,我们能够控制方块的左移与右移啦!
小结:
本篇文章是本人在使用ssRenderEdit工具中的Lua脚本相关内容来制作俄罗斯方块的创作记录,本篇主要实现了渲染功能、创建方块功能、左移右移功能!下一篇文章将会主要实现方块下落功能、方块旋转功能和满行消除功能的实现!若大家感兴趣,还请持续关注哦!