编辑
2022-07-30
个人项目
00
请注意,本文编写于 847 天前,最后修改于 36 天前,其中某些信息可能已经过时。

目录

配置
Lua
LÖVE
VSCode
测试
编写游戏
基础
配置文件
运行
任务目标
实现
场地参考
方块表达
方块碰撞
延迟锁定
出块序列
暂存
发布
创建 .love 文件
针对 windows 平台构建
网页发布

啊,我真的懒得写了。明明应该好好学 Python 的,怎么学起 Lua 了……

其实,我是看到别人用 LuaLÖVE 框架做了俄罗斯方块,自己也想做一个,才研究了一下 Lua (明明 Python 也可以做啊)。

配置

Lua

Lua官网上下载即可,然后配置环境变量,将包含 Lua 的文件夹路径加入环境变量中。之后就可以在命令行执行:

shell
lua54

显示相关信息,则说明 Lua 配置成功。要注意的是,下载的版本不同,执行的命令可能也不同,我的版本是 lua-5.4.2 。这一问题在后面配置 VSCode 时还会遇到。

LÖVE

LÖVE这里下载,配置环境变量。将包含 LÖVE 的文件夹路径加入环境变量中。之后在命令行执行:

shell
love

会启动默认的 LÖVE 程序。

VSCode

没错,我还是想用 VSCode ,所以就需要配置一下了。

VSCode 需要使用相应的拓展。我使用的是 Code RunnerLove2D 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 的情况。

lua
print("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 函数,可以修改默认值,关闭不需要的模块:

lua
function love.conf(t) -- 修改默认值 t.window.width = 1024 t.window.height = 768 -- 关闭不需要的模块 t.modules.joystick = false t.modules.physics = false end

以下是 11.311.4 的全部默认值:

lua
function 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 代表七种方块。这样,以后既可以拓展格子的类型,又可以绘制彩色的场地。

从数据结构的角度,先消除在线性表靠后数据,再消除在线性表靠前的数据,操作要更方便一些。因此,消行的顺序就很关键。

方块表达

方块,以及方块与其他要素的交互是最麻烦的一部分。

方块表达时,采用以下的方式。

各方块的边界框

方块的边界框是包含四个方向形状的外接矩形。除了 IO 的边界框是 4×4 和 2×2 以外,其他方块的边界框是 3×3 ,若要确定每个 mino 的位置,需要知道此刻边界框的位置和方块的朝向。在我们这次的编写中,我们通过边界框左上角的场地坐标,确定方块的位置。此外,在方块新生成的时候, IO 是居中的,而其他方块是偏左的,它们的边界框左上角坐标也不相同。这些生成时的初始坐标,记录在一个表中。

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。而暂存计数重置的时机,应当是一个方块成功锁定后,这是毫无疑问的。

可是,锁延计数是什么时候重置呢?在没有暂存功能的情况下,锁延计数在方块成功锁定后,或者新生成方块时重置都是没有问题的。但是引入了暂存功能后,暂存方块会导致方块的锁延计数重置。因此,如果将锁延计数在方块成功锁定后重置,会导致暂存后新方块的锁延计数未重置。所以锁延计数应当在生成方块时更新。

换个角度想想,暂存后的方块,本质就是新生成了一个方块,这和方块锁定后新生成方块的原理应当一致。而锁延计数会在方块锁定后和暂存后重置,因此在生成方块时重置锁延计数能很好地解决这两种情况。

此外,在暂存中无方块和暂存中有方块时,对序列的处理有一定的区别。前者是将当前块暂存,从序列中新取一块作为当前块;后者是将当前块与暂存块交换。

发布

创建 .love 文件

将游戏文件变为 .exe 文件的步骤比较简单:

  • 将游戏文件加入 .zip 压缩文件中
  • 将后缀更改为.love

此文件就可以通过以下方式运行:

shell
love xxx.love

针对 windows 平台构建

  • love.exe.zip 文件合并为一个 .exe 文件
  • .dll 文件、许可 license.txt 和 合并得到的 .exe 文件放在同一个目录下,即可运行游戏

网页发布

推荐使用 Davidobot/love.js,该作者目前还保持着更新,适用于 11.4 版本

通过以下方式安装:

shell
npm i love.js

shell
npm -g i love.js

然后构建兼容版本:

shell
love.js game.love game -c

如果命令无法正常执行(比如说,打开了 love.js 文件),则可以使用以下命令:

shell
love.js.cmd game.love game -c

即可完成构建。该命令会在当前目录生成一个文件夹:

shell
Tetris │ game.data │ game.js │ index.html │ love.js │ love.wasm │ └─theme bg.png love.css

其中,game.datagame.js 是游戏的生成文件, index.html 就是生成的网页,而 theme 目录下的文件是页面的美化相关的文件。love.jslove.wasmlove 框架对应的文件。

相关信息

游戏基本上完成了,剩下的东西主要是美化辅助和提升操作手感的方面(阴影和 DAS ),大概率不会更新了……想体验戳这里

各种发布方式(包括网页发布)具体可以请参考:Game Distribution - LOVE (love2d.org)

本文作者:Zerol Acqua

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!