如何在树莓派上使用 LÖVE 游戏引擎编程游戏

当您不再满足于拖放式编程后,下一步该怎么做?我们将解释如何开始使用 LÖVE 游戏引擎,它使用脚本语言 Lua。
533 位读者喜欢这篇文章。
Raspberries with pi symbol overlay

Dwight Sipler 在 Flickr 上

树莓派 以向孩子们介绍开源软件和编程而闻名。树莓派是对专业级计算的一种经济实惠且实用的入门方式,它伪装成可破解的乐趣。在让幼儿开始编程方面做得最好的应用程序一直是 Mitch Resnick 的 Scratch(幸运的是,当 Scratch 2 切换到非开源的 Adobe Air 时,Pi 基金会对其进行了 fork),但一个不可避免的问题是,当人们不再满足于拖放式编程后,应该进阶到什么程度。

在像 Scratch 这样的拖放式入门之后,有很多候选者可以作为下一个级别的编程选择。有出色的 PyGame,有一个名为 Processing 的 Java 子集,强大的 Godot 引擎,以及许多其他引擎。诀窍是找到一个框架,它既足够容易,可以缓解从拖放的即时满足感过渡,又足够复杂,可以准确地代表专业程序员整天实际做的事情。

一个特别强大的游戏引擎叫做 LÖVE。LÖVE 使用脚本语言 Lua,Lua 不像 Python 那样受到关注,但它在现代视频游戏行业中被 大量嵌入(无论是字面上还是比喻上)。几乎所有主要的视频游戏工作室都将 Lua 列为必备技能,它也是专有的 Unity 和 Unreal 游戏引擎中的一个选项,并且它通常在现实世界中各种意想不到的地方出现。

长话短说,Lua 是一种值得学习的语言,特别是如果您计划进入游戏开发领域。就 LÖVE 引擎而言,它在功能方面与 PyGame 框架一样出色,而且因为它没有 IDE,所以它非常轻量级,并且在某种程度上,比 Godot 之类的引擎更容易学习。

更好的是,LÖVE 在树莓派上原生运行,但它的项目可以开源,并在 Lua 运行的任何平台上运行。这包括 Linux、Windows 和 Mac,也包括 Android,甚至包括非常封闭的 iOS。换句话说,LÖVE 也是一个不错的移动开发入门平台。

Mr. Rescue,一款在 itch.io 上提供的开源游戏https://open-source.net.cn/sites/default/files/love_rescue.png" title="Mr. Rescue,一款在 itch.io 上提供的开源游戏" typeof="foaf:Image" width="520" height="400">

Mr. Rescue,一款在 itch.io 上提供的开源游戏

既然我已经说服您 LÖVE 是树莓派上 Scratch 的下一步进阶,那么让我们深入了解一下它是如何工作的。

安装 LÖVE

与往常一样,在树莓派上安装 LÖVE 只需一个简单的命令


$ sudo apt install love2d

如果您在树莓派上运行 Fedora,请使用 dnf 代替


$ sudo dnf install love2d

无论哪种方式,软件包管理系统都会拉取 Lua、SDL 和 LÖVE 运行所需的其他依赖项。

Hello, World!

LÖVE 引擎中没有什么可看的。它实际上是一个框架,这意味着您可以使用任何您喜欢的文本编辑器。首先要尝试的是“hello world”程序,以确保它可以启动,并向您介绍 Lua 语言和 LÖVE 框架的基本语法。打开一个文本编辑器并输入以下文本


cwide = 520
chigh = 333

love.window.setTitle(' Hello Wörld ')
love.window.setMode(cwide, chigh)

function love.load()
	love.graphics.setBackgroundColor(177, 106, 248)
	-- font = love.graphics.setNewFont("SkirtGirl.ttf", 72)
end

function love.draw()
love.graphics.setColor(255, 33, 78)
	love.graphics.print("Hello World", cwide/4, chigh/3.33)
end

将其另存为 main.lua

一个可分发的 LÖVE 包只是一个标准的 zip 文件,扩展名为 .love。主文件必须始终命名为 main.lua,并且必须位于 zip 文件的顶层。像这样创建它


$ zip hello.love main.lua

并启动它


$ love ./hello.love

Hello Worldhttps://open-source.net.cn/sites/default/files/love_hello.png" title="Hello World" typeof="foaf:Image" width="544" height="390">

代码非常容易理解。cwidechigh 变量是全局变量,整个脚本都可以使用,它们设置游戏世界的宽度和高度。在第一段代码(称为函数)中,设置了背景颜色,在第二个函数中,“hello world”被打印到屏幕上。

以两条破折号 (--) 开头的行是注释。在 .love 包中包含一个 .ttf 文件并取消注释 setNewFont 行,将以该字体呈现文本。您不必使用 Skirt Girl 字体,当然;只需从 FontLibrary.org 获取一种字体并相应地调整代码即可。

$ zip hello.love main.lua SkirtGirl.ttf

并启动它


$ love ./hello.love

基础知识

Scratch 和专业编程语言之间的一个主要区别在于,使用 Scratch,您可以学习一些基本原理,然后随机探索,直到发现所有其他功能。

对于像 Lua 这样的低级编程语言,您首先学习的东西不是您可以复制粘贴以生成可玩游戏的东西。您学习如何使用该语言,然后查找可以完成您希望游戏具有的功能的函数。在您了解语言的工作原理之前,偶然发现一个可工作的游戏世界仍然很困难。

幸运的是,您可以在构建游戏开始部分的同时学习该语言。在此过程中,您会学到一些技巧,这些技巧可以在以后重用,以使您的游戏以您想要的方式工作。

首先,您需要了解 LÖVE 引擎核心中的三个主要函数

  • function love.load() 是在您启动 LÖVE 游戏时触发的函数(在其他语言中称为 initvoid setup() 函数)。它用于为您的游戏世界设置基础。function love.load() 只执行一次,因此它只包含在整个游戏中持续存在的代码。它或多或少相当于 Scratch 中的当绿旗被点击块和舞台脚本区域。
  • function love.update(dt) 是在游戏过程中不断更新的函数。love.update(dt) 函数中的任何内容都会在游戏进行时刷新。如果您玩过任何游戏,无论是《玩具熊的五夜后宫》还是《上古卷轴》或任何其他游戏,并且您监控了您的帧率 (fps),那么帧数滴答作响时发生的一切都将在更新循环中(不是字面上,因为这些游戏不是用 LÖVE 制作的,而是类似于更新函数)。
  • function love.draw() 是一个函数,它使引擎实例化您在游戏的 love.load() 函数中创建的图形组件。您可以加载精灵或创建一座山,但如果您不绘制它,那么它永远不会在您的游戏中显示出来。

这三个函数是您在 LÖVE 中创建任何元素的基础。您也可以创建自己的新函数。首先,让我们探索 LÖVE 框架。

精灵

“Hello, World!” 的基础知识是一个很好的起点。同样的基本的三个函数仍然是应用程序的基本骨架。但是,与在屏幕上渲染简单的文本不同,这次我们使用来自 freesvg.org 的图形创建一个精灵。

此示例的代码示例和资源可在 git 存储库中找到。如果您想继续学习,请克隆它


$ git clone https://notabug.org/seth/lessons_love2d.git

LÖVE 中的大多数对象都存储在数组中。数组有点像 Scratch 中的列表;它是一堆放入公共容器中的特征。通常,当您在 LÖVE 中创建精灵时,您会创建一个数组来保存有关精灵的属性,然后在 love.load 中列出您希望对象拥有的属性。

love.draw 函数中,精灵被绘制到屏幕上。


fish  = {}
cwide = 520
chigh = 333
    
love.window.setMode(cwide, chigh)
love.window.setTitle(' Collide ')
    
function love.load()
	fish.x    = 0
	fish.y    = 0
	fish.img  = love.graphics.newImage('images/fish.png')
end
    
function love.update(dt)
        -- this is a comment
end
    
function love.draw()
    	love.graphics.draw(fish.img, fish.x, fish.y, 0, 1, 1, 0, 0)
end

鱼的天然捕食者是南极洲最凶猛的生物之一:企鹅。以与创建鱼精灵相同的方式创建企鹅精灵,并添加一个 player.speed,这表示企鹅在我们设置玩家控件后将移动多少像素


fish  = {}
player= {}
cwide = 520
chigh = 333
    
love.window.setMode(cwide, chigh)
love.window.setTitle(' Collide ')
    
function love.load()
  fish.x    = 0
  fish.y    = 0
  fish.img  = love.graphics.newImage('images/fish.png')
  player.x     = 100
  player.y     = 100
  player.img   = love.graphics.newImage('images/tux.png')
  player.speed = 10
end
    
function love.update(dt)
        -- this is a comment
end
    
function love.draw()
    	love.graphics.draw(fish.img,  fish.x,  fish.y,  0,1,1,0, 0)
        love.graphics.draw(player.img,player.x,player.y,0,1,1,0, 0)
end

为了保持整洁,请将 png 文件放在 images 目录中。为了测试目前的游戏,请压缩 main.luapng 文件


$ zip game.zip main.lua -r images
$ mv game.zip game.love
$ love ./game.love

我们当前的角色阵容。https://open-source.net.cn/sites/default/files/love_sprites.png" title="我们当前的角色阵容。" typeof="foaf:Image" width="375" height="256">

我们当前的角色阵容

移动

在 LÖVE 中实现移动有几种方法,包括用于鼠标、操纵杆和键盘的函数。我们已经为玩家的 xy 位置以及玩家移动的像素数建立了变量,因此使用 if/then 语句来检测按键,然后使用简单的数学重新定义保存玩家精灵位置的变量


player     = {}
fish       = {}
cwide      = 520
chigh      = 333
    
love.window.setMode(cwide, chigh)
love.window.setTitle(' Collide ')

function love.load()
	fish.x    = 0
	fish.y    = 0
	fish.img  = love.graphics.newImage( 'images/fish.png' )
	player.x     = 100
	player.y     = 100
	player.img   = love.graphics.newImage('images/tux.png')
	player.speed = 10
end

function love.update(dt)
	if love.keyboard.isDown("right") then
	    player.x = player.x+player.speed

	elseif love.keyboard.isDown("left") then
	    player.x = player.x-player.speed

	elseif love.keyboard.isDown("up")  then
	    player.y = player.y-player.speed    

	elseif love.keyboard.isDown("down") then
	    player.y = player.y+player.speed
        end
end

function love.draw()
	love.graphics.draw(player.img,player.x,player.y,0,1,1,0, 0)
	love.graphics.draw(fish.img, fish.x, fish.y, 0, 1, 1, 0, 0)    
end

测试代码以确认移动是否按预期工作。记住在测试前重新压缩所有文件,这样您就不会意外地测试旧版本的游戏。

为了使移动更有意义,我们可以创建函数来根据按下的键更改精灵面向的方向。这相当于 Scratch 代码块的这些类型

在 Scratch 中查看和移动https://open-source.net.cn/sites/default/files/scratch-right.png" title="在 Scratch 中查看和移动" typeof="foaf:Image" width="314" height="192">

在 Scratch 中查看和移动

使用 ImageMagick 生成玩家精灵的翻转版本


$ convert tux.png -flop tuxleft.png

然后编写一个函数来交换图像。您需要两个函数:一个用于交换图像,另一个用于将其恢复到原始状态


function rotate_left()
	player.img = love.graphics.newImage('images/tuxleft.png')
end

function rotate_right()
	player.img = love.graphics.newImage('images/tux.png' )
end

在适当的地方使用这些函数


function love.update(dt)
	if love.keyboard.isDown("right") then
	    player.x = player.x+player.speed
            rotate_right()

	elseif love.keyboard.isDown("left") then
	    player.x = player.x-player.speed
            rotate_left()
	    
	elseif love.keyboard.isDown("up")  then
	    player.y = player.y-player.speed    

	elseif love.keyboard.isDown("down") then
	    player.y = player.y+player.speed
        end
end

自动移动

使用精灵的 x 值的相同约定,并使用一点数学知识,您可以使鱼自动在屏幕上从一边移动到另一边。这相当于在 Scratch 中执行此操作

Scratch 中的自动移动https://open-source.net.cn/sites/default/files/scratch-edge.png" title="Scratch 中的自动移动" typeof="foaf:Image" width="314" height="192">

Scratch 中的自动移动

LÖVE 中没有 if on edge, bounce,但通过检查鱼的 x 值是否已到达屏幕的最左侧(即 0 像素)或最右侧(与画布的宽度相同,即 cwide 变量),您可以确定精灵是否已超出边缘。

如果鱼在最左侧,则表示它已到达屏幕边缘,因此您需要增加鱼的位置,使其向右移动。如果鱼在最右侧,则减小鱼的位置,使其向左移动。


function automove(obj,x,y,ox,oy)
        if obj.x == cwide then
	    local edgeright = 0
	elseif obj.x == 0 then
	    local edgeright = 1
end

if edgeright == 1 then
	    obj.x = obj.x + x
	else
	    obj.x = obj.x - x
	end
    end

在这种情况下,有一个事件序列很重要。在第一个 if 块中,您检查 x 的值并分配一个临时(局部)变量来指示鱼接下来需要去哪里。然后在第二个单独的 if 块中,您移动鱼。为了获得奖励积分,尝试在一个 if 语句中完成所有操作,看看您是否可以理解为什么它会失败。为了获得更多奖励积分,看看您是否可以找到另一种移动鱼的方式。

要实现鱼的移动函数,请在 love.update 循环的底部调用该函数


automove(fish,1,0,fish.img:getWidth(),fish.img:getHeight() )

如果您测试脚本,您会注意到鱼在碰到右边缘时会完全移出屏幕。它这样做是因为精灵的 x 值基于其左上角像素。我将把它作为一个练习留给您,让您找出在检查鱼的位置时应该从 cwide 中减去哪个变量。

碰撞检测

视频游戏都与碰撞有关。当事物相互碰撞时,无论是倒霉的英雄一头扎进熔岩坑,还是坏人被魔法弹击中,都应该发生一些事情。

在检测碰撞之前,让我们确定当发生碰撞时我们希望发生什么。因为您已经知道如何更改精灵的外观,我们将创建两个函数:一个将鱼变成企鹅抓住它后剩下的骨头,另一个在企鹅不在附近时将鱼变回活鱼。


function falive()
        fish.img = love.graphics.newImage('images/fish.png')
end

function fdead()
        fish.img = love.graphics.newImage('images/fishbones.png')
end

设置好这些函数后,就可以计算碰撞了。

在 Scratch 中,有一些代码块可以检查两个精灵是否接触。

Scratch 碰撞https://open-source.net.cn/sites/default/files/scratch-collision.png" title="Scratch 碰撞" typeof="foaf:Image" width="338" height="148">

Scratch 碰撞

原则上,相同的概念适用于 LÖVE。有几种不同的方法可以检测碰撞,包括外部库,如 HClove.physics,但两者之间的一个很好的折衷方案是自定义函数来检测精灵边界的重叠。


function CheckCollision(x1,y1,w1,h1, x2,y2,w2,h2)
        return x1 < x2+w2 and
	    x2 < x1+w1 and
	    y1 < y2+h2 and
	    y2 < y1+h1
end

数学很复杂,但如果您花时间思考一下,它是有道理的。目标是检测两个图像是否重叠。如果图像重叠,则可以认为它们发生了碰撞。这是一个两个框* 重叠的图示,并带有示例 y 值以保持简单

两个框https://open-source.net.cn/sites/default/files/overlap_no.png" title="两个框" typeof="foaf:Image" width="507" height="649">

两个框

从函数中取一行并计算数字


y2  < y1 + h1
110 < 0  + 100

这显然是一个假语句,所以函数必须返回 false。换句话说,这些框没有碰撞。

现在看看两个重叠框的相同逻辑

重叠框https://open-source.net.cn/sites/default/files/overlap.png" title="重叠框" typeof="foaf:Image" width="507" height="460">


y2  < y1 + h1
50  < 0  + 100

这一个显然是真的。假设函数中的所有语句也为真(并且它们会是真的,如果我费心添加 x 值),那么函数返回 true

为了利用碰撞检查,请使用 if 语句对其进行评估。您现在知道,如果 CheckCollision 函数返回 true,则会发生碰撞。CheckCollision 函数是通用编写的,因此在调用它时,您需要为其提供适当的值,以便它知道哪个对象是哪个对象。

大多数值都是直观的。您需要 LÖVE 使用正在检查碰撞的每个对象的 xy 位置以及对象的大小。在这种情况下,唯一特殊的值是根据碰撞状态交换的值。对于这些值,硬编码死鱼的大小而不是活鱼的大小,否则碰撞状态将在碰撞检测的中间发生变化。事实上,如果您想看到它出现故障,您可以先以错误的方式进行操作


if CheckCollision(fish.x,fish.y,fish.img:getWidth(),fish.img:getHeight(), player.x,player.y,player.img:getWidth(),player.img:getHeight() ) then
        fdead()
    else
	falive()
    end

正确的方法是获取较小的精灵图像的大小。您可以使用 ImageMagick 执行此操作


$ identify images/fishbones.png
images/fishbones.png PNG 150x61 [...]

然后使用适当的尺寸硬编码“热点”


if CheckCollision(fish.x,fish.y,150,61, player.x,player.y,player.img:getWidth(),player.img:getHeight() ) then
        fdead()
    else
	falive()
end

硬编码碰撞检测区域的副作用是,当企鹅接触到鱼的边缘时,鱼不会被吞食。为了获得更精确的碰撞检测,请探索 HC 库或 love.physics

最终代码

它不是一个真正的游戏,但它演示了视频游戏的重要元素


player     = {}
fish       = {}
cwide      = 520
chigh      = 333

love.window.setTitle(' Hello Game Wörld ')
love.window.setMode(cwide, chigh)

function love.load()
	fish.x    = 0
	fish.y    = 0
	fish.img  = love.graphics.newImage( 'images/fish.png' )
	player.x     = 150
	player.y     = 150
	player.img   = love.graphics.newImage('images/tux.png')
	player.speed = 10
    end

function love.update(dt)
	if love.keyboard.isDown("right") then
	    player.x = player.x+player.speed

	elseif love.keyboard.isDown("left") then
	    player.x = player.x-player.speed

	elseif love.keyboard.isDown("up")  then
	    player.y = player.y-player.speed    

	elseif love.keyboard.isDown("down") then
	    player.y = player.y+player.speed
        end

if CheckCollision(fish.x,fish.y,151,61, player.x,player.y,player.img:getWidth(),player.img:getHeight() ) then
	    fdead()
	else
	    falive()
end

automove(fish,1,0,fish.wide,fish.high)
end

function love.draw()
	love.graphics.draw(player.img,player.x,player.y,0,1,1,0, 0)
	love.graphics.draw(fish.img, fish.x, fish.y, 0, 1, 1, 0, 0)    
end

function automove(obj,x,y,ox,oy)
	if obj.x == cwide-fish.img:getWidth() then
	    edgeright = 0
	    elseif obj.x == 0 then
	    edgeright = 1
end

if edgeright == 1 then
	    obj.x = obj.x + x
	else
	    obj.x = obj.x - x
	end
   end

function CheckCollision(x1,y1,w1,h1, x2,y2,w2,h2)
	return x1 < x2+w2 and
	    x2 < x1+w1 and
	    y1 < y2+h2 and
	    y2 < y1+h1
	end

function rotate_left()
	player.img = love.graphics.newImage('images/tuxleft.png')
    end

function rotate_right()
	player.img = love.graphics.newImage('images/tux.png' )
    end

function falive()
	fish.img = love.graphics.newImage('images/fish.png')
    end

function fdead()
	fish.img = love.graphics.newImage('images/fishbones.png')
    end

从这里,您可以使用您学到的原则来创建更令人兴奋的作品。碰撞是视频游戏中大多数交互的基础,无论是触发与 NPC 的对话、管理战斗、拾取物品、设置陷阱还是几乎任何其他内容,因此如果您掌握了这一点,剩下的就是重复和努力。

所以去制作一个视频游戏吧!与朋友分享,在移动设备上玩,并且一如既往地保持升级。

Seth Kenlon
Seth Kenlon 是一位 UNIX 极客、自由文化倡导者、独立多媒体艺术家和 D&D 爱好者。他曾在电影和计算机行业工作,通常同时进行。

1 条评论

非常好的文章。感谢分享。

您是开源新手吗?

浏览我们的资源集合。

© . All rights reserved.