啊,我真的懒得写了。明明应该好好学 Python
的,怎么学起 Lua
了……
其实,我是看到别人用 Lua
和 LÖVE
框架做了俄罗斯方块,自己也想做一个,才研究了一下 Lua
(明明 Python
也可以做啊)。
Lua
在官网上下载即可,然后配置环境变量,将包含 Lua
的文件夹路径加入环境变量中。之后就可以在命令行执行:
shelllua54
显示相关信息,则说明 Lua
配置成功。要注意的是,下载的版本不同,执行的命令可能也不同,我的版本是 lua-5.4.2
。这一问题在后面配置 VSCode 时还会遇到。
LÖVE
在这里下载,配置环境变量。将包含 LÖVE
的文件夹路径加入环境变量中。之后在命令行执行:
shelllove
会启动默认的 LÖVE
程序。
没错,我还是想用 VSCode ,所以就需要配置一下了。
VSCode 需要使用相应的拓展。我使用的是 Code Runner
和 Love2D Support
,在配置拓展时,需要对 Code Runner
的默认配置进行修改,并设置 love.exe
的路径(我也不知道为什么还要再设一次路径,明明有环境变量了)。
json"code-runner.executorMap": {
"lua": "lua54",
},
"pixelbyte.love2d.path": "xxx\\love-11.4-win64\\love.exe",
Code Runner
是用来跑 Lua
的, Love2D Support
是跑 LÖVE
的。在 Love2D Support
拓展的设置中,可以设置开启 LÖVE
的控制台,这样就能看见 print()
的输出了({% del 草 %})。
测试 Lua
的情况。
luaprint("hello world!")
print("你好,世界!")
如果中文输出出现乱码,很有可能因为使用的终端是 cmd
,有两种方法解决该问题。第一种方法是将使用的终端改为 powershell
;第二种方法则是将 cmd
的编码改为 UTF-8
。
json"terminal.integrated.defaultProfile.windows": "PowerShell",
"terminal.integrated.shellArgs.windows": ["/K chcp 65001 >nul"],
测试 LÖVE
的情况。
lua-- 初始化矩形的一些默认值
function love.load()
x, y, w, h = 20, 20, 60, 20
end
-- 每一帧增加矩形的尺寸
function love.update(dt)
w = w + 1
h = h + 1
end
-- 绘制有颜色的矩形
function love.draw()
love.graphics.setColor(0, 100, 100)
love.graphics.rectangle("fill", x, y, w, h)
end
按住 Alt
+ L
,应当会有 LÖVE
程序启动,绘制一个不断变大的青色矩形。当然,拓展的这个快捷键可以在设置中进行修改。
在游戏目录中的 conf.lua
文件夹,会在 LÖVE
模块加载前运行,可以使用该文件重写 love.conf
函数。
love.conf
函数有一个参数,该参数是一个包含所有默认值的 table
类型参数。通过 love.conf
函数,可以修改默认值,关闭不需要的模块:
luafunction love.conf(t)
-- 修改默认值
t.window.width = 1024
t.window.height = 768
-- 关闭不需要的模块
t.modules.joystick = false
t.modules.physics = false
end
以下是 11.3
和 11.4
的全部默认值:
luafunction love.conf(t)
t.identity = nil -- The name of the save directory (string)
t.appendidentity = false -- Search files in source directory before save directory (boolean)
t.version = "11.3" -- The LÖVE version this game was made for (string)
t.console = false -- Attach a console (boolean, Windows only)
t.accelerometerjoystick = true -- Enable the accelerometer on iOS and Android by exposing it as a Joystick (boolean)
t.externalstorage = false -- True to save files (and read from the save directory) in external storage on Android (boolean)
t.gammacorrect = false -- Enable gamma-correct rendering, when supported by the system (boolean)
t.audio.mic = false -- Request and use microphone capabilities in Android (boolean)
t.audio.mixwithsystem = true -- Keep background music playing when opening LOVE (boolean, iOS and Android only)
t.window.title = "Untitled" -- The window title (string)
t.window.icon = nil -- Filepath to an image to use as the window's icon (string)
t.window.width = 800 -- The window width (number)
t.window.height = 600 -- The window height (number)
t.window.borderless = false -- Remove all border visuals from the window (boolean)
t.window.resizable = false -- Let the window be user-resizable (boolean)
t.window.minwidth = 1 -- Minimum window width if the window is resizable (number)
t.window.minheight = 1 -- Minimum window height if the window is resizable (number)
t.window.fullscreen = false -- Enable fullscreen (boolean)
t.window.fullscreentype = "desktop" -- Choose between "desktop" fullscreen or "exclusive" fullscreen mode (string)
t.window.vsync = 1 -- Vertical sync mode (number)
t.window.msaa = 0 -- The number of samples to use with multi-sampled antialiasing (number)
t.window.depth = nil -- The number of bits per sample in the depth buffer
t.window.stencil = nil -- The number of bits per sample in the stencil buffer
t.window.display = 1 -- Index of the monitor to show the window in (number)
t.window.highdpi = false -- Enable high-dpi mode for the window on a Retina display (boolean)
t.window.usedpiscale = true -- Enable automatic DPI scaling when highdpi is set to true as well (boolean)
t.window.x = nil -- The x-coordinate of the window's position in the specified display (number)
t.window.y = nil -- The y-coordinate of the window's position in the specified display (number)
t.modules.audio = true -- Enable the audio module (boolean)
t.modules.data = true -- Enable the data module (boolean)
t.modules.event = true -- Enable the event module (boolean)
t.modules.font = true -- Enable the font module (boolean)
t.modules.graphics = true -- Enable the graphics module (boolean)
t.modules.image = true -- Enable the image module (boolean)
t.modules.joystick = true -- Enable the joystick module (boolean)
t.modules.keyboard = true -- Enable the keyboard module (boolean)
t.modules.math = true -- Enable the math module (boolean)
t.modules.mouse = true -- Enable the mouse module (boolean)
t.modules.physics = true -- Enable the physics module (boolean)
t.modules.sound = true -- Enable the sound module (boolean)
t.modules.system = true -- Enable the system module (boolean)
t.modules.thread = true -- Enable the thread module (boolean)
t.modules.timer = true -- Enable the timer module (boolean), Disabling it will result 0 delta time in love.update
t.modules.touch = true -- Enable the touch module (boolean)
t.modules.video = true -- Enable the video module (boolean)
t.modules.window = true -- Enable the window module (boolean)
end
各配置详细的作用请参考:Config Files - LOVE (love2d.org)
love.run()
是运行的主函数,它包含了运行时的主循环,不同版本的默认值不同。
lua-- The default function for 11.0, used if you don't supply your own.
function love.run()
if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
-- We don't want the first frame's dt to include time taken by love.load.
if love.timer then love.timer.step() end
local dt = 0
-- Main loop time.
return function()
-- Process events.
if love.event then
love.event.pump()
for name, a,b,c,d,e,f in love.event.poll() do
if name == "quit" then
if not love.quit or not love.quit() then
return a or 0
end
end
love.handlers[name](a,b,c,d,e,f)
end
end
-- Update dt, as we'll be passing it to update
if love.timer then dt = love.timer.step() end
-- Call update and draw
if love.update then love.update(dt) end -- will pass 0 if love.timer is disabled
if love.graphics and love.graphics.isActive() then
love.graphics.origin()
love.graphics.clear(love.graphics.getBackgroundColor())
if love.draw then love.draw() end
love.graphics.present()
end
if love.timer then love.timer.sleep(0.001) end
end
end
可以参见 love.run - LOVE (love2d.org)
做一款现代俄罗斯方块,具有以下特点:
7-Bag
而不是纯随机的出块方式在写代码时,学习参考了 MrZ 大佬的代码 tetris-in-100-lines
以下是我的代码,还没写完(小声):Tetris
场地的话,计划使用 10×40 的场地,其中正常游戏只使用 10×20,剩下 10×20 用于存储顶出显示场地的方块。通过建立一个以场地为参考的坐标描述,我们可以描述方块的位置信息,并将游戏逻辑与绘图的逻辑分开。
实现起来,主要是通过一个二维数组记录 10*40 场地的情况。其中,每格不同的数值代表不同的方块。目前,0
代表无方块,1~7
代表七种方块。这样,以后既可以拓展格子的类型,又可以绘制彩色的场地。
从数据结构的角度,先消除在线性表靠后数据,再消除在线性表靠前的数据,操作要更方便一些。因此,消行的顺序就很关键。
方块,以及方块与其他要素的交互是最麻烦的一部分。
方块表达时,采用以下的方式。
方块的边界框是包含四个方向形状的外接矩形。除了 I
和 O
的边界框是 4×4 和 2×2 以外,其他方块的边界框是 3×3 ,若要确定每个 mino
的位置,需要知道此刻边界框的位置和方块的朝向。在我们这次的编写中,我们通过边界框左上角的场地坐标,确定方块的位置。此外,在方块新生成的时候, I
和 O
是居中的,而其他方块是偏左的,它们的边界框左上角坐标也不相同。这些生成时的初始坐标,记录在一个表中。
lua-- 方块活动框左上角坐标 (x,y)
local initPos={
{4,22}, -- I
{4,22}, -- J
{4,22}, -- L
{5,22}, -- O
{4,22}, -- S
{4,22}, -- Z
{4,22} -- T
}
为了表示各个方向下的方块形状,也采用了表结构存储,在已知方块类型和方块朝向后,就能立刻获取其形状。
lua-- 记录的各方向的方块形状(从下往上):1-原位(0) 2-顺时针位(R) 3-180度位(2) 4-逆时针位(L)
local blocks={
-- I
{
{{0,0,0,0},{0,0,0,0},{1,1,1,1},{0,0,0,0}}, --I1
{{0,0,1,0},{0,0,1,0},{0,0,1,0},{0,0,1,0}}, --I2
{{0,0,0,0},{1,1,1,1},{0,0,0,0},{0,0,0,0}}, --I3
{{0,1,0,0},{0,1,0,0},{0,1,0,0},{0,1,0,0}} --I4
},
-- J
{
{{0,0,0},{1,1,1},{1,0,0}}, --J1
{{0,1,0},{0,1,0},{0,1,1}}, --J2
{{0,0,1},{1,1,1},{0,0,0}}, --J3
{{1,1,0},{0,1,0},{0,1,0}} --J4
},
-- L
{
{{0,0,0},{1,1,1},{0,0,1}}, --L1
{{0,1,1},{0,1,0},{0,1,0}}, --L2
{{1,0,0},{1,1,1},{0,0,0}}, --L3
{{0,1,0},{0,1,0},{1,1,0}} --L4
},
-- O
{
{{1,1},{1,1}}, --O1
{{1,1},{1,1}}, --O2
{{1,1},{1,1}}, --O3
{{1,1},{1,1}} --O4
},
-- S
{
{{0,0,0},{1,1,0},{0,1,1}}, --S1
{{0,0,1},{0,1,1},{0,1,0}}, --S2
{{1,1,0},{0,1,1},{0,0,0}}, --S3
{{0,1,0},{1,1,0},{1,0,0}} --S4
},
-- Z
{
{{0,0,0},{0,1,1},{1,1,0}}, --Z1
{{0,1,0},{0,1,1},{0,0,1}}, --Z2
{{0,1,1},{1,1,0},{0,0,0}}, --Z3
{{1,0,0},{1,1,0},{0,1,0}} --Z4
},
-- T
{
{{0,0,0},{1,1,1},{0,1,0}}, --T1
{{0,1,0},{0,1,1},{0,1,0}}, --T2
{{0,1,0},{1,1,1},{0,0,0}}, --T3
{{0,1,0},{1,1,0},{0,1,0}} --T4
}
}
在后面的实现中,还使用了不少的表,比如行状态表、列状态表、颜色表、踢墙表等等。
这点是最复杂的地方,而且牵扯到踢墙的概念。
由于每个方块的边界框{% emp 并不与某一方向时方块的最小外接矩形重合 %},方块与场地边界的碰撞检测就稍显麻烦。不对边界框中完全没有 mino
的行或列进行特殊处理的话,就可能造成(场地下边界或场地左右边界)数组越界或是方块悬空的状况。因此,根据方块的形状表,我计算了对应的行状态与列状态表(遇事不决就打表)。在此基础上,可以实现一个判断方块是否超出场地,与场地内方块重叠的函数。通过该函数,我们可以做到判断方块是否落地(落地了就要计时,准备延迟锁定了)。通过该函数,我们可以判断踢墙的结果是否可行。
踢墙的概念这里不再多说。有踢墙表之后,只需要按顺序进行平移的尝试,成功则采用,不成功则方块不能旋转。
目前我只做了延迟锁定,其表现为:
O
的旋转虽然看起来没有变化,但它是确确实实成功旋转了的,所以会刷新锁定延迟。同时,需要注意,为了避免无限刷新锁定延迟的情况,应该增加一个操作次数限制:
O
的旋转虽然看起来没有变化,但它是确确实实成功旋转了的,所以会计一次操作次数。今天(2022.8.23)我实现操作次数的时候,发现还有一个问题:
如果操作次数用尽后,踢墙产生的偏移不能向上。
这个规定的意义在于,避免玩家通过踢墙将方块“抬高”,然后方块下落,刷新延迟锁定,导致无限操作而不锁定的问题。
实际上就是一个队列,当队列短于设定的数量时。按选定的随机生成器,生成一段新序列加到队尾。
如果是 bag
出块,则保证要添加的新序列中每种方块各出现一次
暂存的实现思路很简单,就是记录下暂存的方块 id 和暂存的操作次数限制而已。但是在这个过程中,我意识到关于暂存计数、锁延计数的重置时机问题。
根据游戏逻辑,在暂存之后,更换的方块会重新从场地顶部开始下落,并且方块的锁延计数会重置。暂存计数就是为了避免玩家通过无限的暂存,重置锁延计数的情况。所以暂存之后,锁延计数会重置,暂存计数 -1
。而暂存计数重置的时机,应当是一个方块成功锁定后,这是毫无疑问的。
可是,锁延计数是什么时候重置呢?在没有暂存功能的情况下,锁延计数在方块成功锁定后,或者新生成方块时重置都是没有问题的。但是引入了暂存功能后,暂存方块会导致方块的锁延计数重置。因此,如果将锁延计数在方块成功锁定后重置,会导致暂存后新方块的锁延计数未重置。所以锁延计数应当在生成方块时更新。
换个角度想想,暂存后的方块,本质就是新生成了一个方块,这和方块锁定后新生成方块的原理应当一致。而锁延计数会在方块锁定后和暂存后重置,因此在生成方块时重置锁延计数能很好地解决这两种情况。
此外,在暂存中无方块和暂存中有方块时,对序列的处理有一定的区别。前者是将当前块暂存,从序列中新取一块作为当前块;后者是将当前块与暂存块交换。
将游戏文件变为 .exe
文件的步骤比较简单:
.zip
压缩文件中.love
此文件就可以通过以下方式运行:
shelllove xxx.love
love.exe
与 .zip
文件合并为一个 .exe
文件.dll
文件、许可 license.txt
和 合并得到的 .exe
文件放在同一个目录下,即可运行游戏推荐使用 Davidobot/love.js,该作者目前还保持着更新,适用于 11.4
版本
通过以下方式安装:
shellnpm i love.js
或
shellnpm -g i love.js
然后构建兼容版本:
shelllove.js game.love game -c
如果命令无法正常执行(比如说,打开了 love.js
文件),则可以使用以下命令:
shelllove.js.cmd game.love game -c
即可完成构建。该命令会在当前目录生成一个文件夹:
shellTetris │ game.data │ game.js │ index.html │ love.js │ love.wasm │ └─theme bg.png love.css
其中,game.data
和 game.js
是游戏的生成文件, index.html
就是生成的网页,而 theme
目录下的文件是页面的美化相关的文件。love.js
和 love.wasm
是 love
框架对应的文件。
相关信息
游戏基本上完成了,剩下的东西主要是美化辅助和提升操作手感的方面(阴影和 DAS ),大概率不会更新了……想体验戳这里
各种发布方式(包括网页发布)具体可以请参考:Game Distribution - LOVE (love2d.org)
本文作者:Zerol Acqua
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!