第1章 游戏的基本元素

本章内容:love2d的游戏循环以及基本的输入、输出。

游戏的基本元素

教程开启之前,笔者假定你对lua已经有了初步了解,因为在教程中,我不会再讲解关于lua的基础使用。所谓初步了解是指,起码了解lua的数据类型,函数,遍历,条件语句。对自定义迭代器,metatable,与c互动方面暂时不做要求。如有可能,希望了解一些初步的oop(面向对象编程)和eop(面向组件编程)的概念。因为我们未来是写程序的,而写脚本,因此对于程序架构需要有一定的了解。

好啦,言归正传,下面我们将开启对游戏的另一个视角,从如何玩游戏到如何做游戏。本章的主要目的是让你更深的了解何为游戏,以及用love2d我们有哪些基本的做游戏的工具。

游戏的基本元素

游戏,换个说法可以说是一种有趣的人机互动。
有趣就意味着你的游戏得有“游戏性”,也就是让人觉得“好玩”,或者起码让人坚持玩下去。
人机互动意味着起码你需要有用户输入,而输出对于现在一般电脑来讲,主要是声音和图像的输出。当然手机有摄像头是输入,有震动是输出。
人机互动还意味着输入和输出的反馈关系,好的输入会产生正向的反馈,比如得分的画面和声音,不好的输入会产生负反馈,比如gameover。而游戏的“好玩”主要就是反馈的感觉如何上体现的。本章我们将用love实现一下简单的这种反馈。
本次我们仅仅学一下游戏最基本的元素,更复杂的元素我们在后面再慢慢道来。

love引擎循环

想要实现游戏,就需要了解love引擎是如何运作的。

首先要理解游戏循环,大家要知道,游戏,窗体等图形界面,程序都不是一闪而过,得出了结构程序就自动退出了,而是一直出于一个循环状态,在循环的某个间隙中,我们把程序的控制权交给程序,而其他时间它都处于休眠的状态。而在这个间隙中,我们要完成程序本身的所有工作,比如检测某个按钮是否被按下,画面如何更新等等。
理解到游戏程序是个循环,我们就可以进行下一步了。
作为一个循环,我们必须在循环之前做一些必要的设置。比如布置好一个场景,生成几个按钮等等。
在循环过程中,我们需要根据用户输入以及一些预先设定好的方式对场景进行变更。
既然场景变更了,那么它的画面表现也要随之变更。
至于如何设置,如何变更,如何绘图,love提供了几个回调函数。所谓回调函数的意义是,平时它不工作,直到特定的条件下,由条件的检测者来触发这些函数。另外,在检测者触发函数时,会传入一些参数。这些参数根据检测者的不同而不同。一句话,所有回调函数是不应该被用户(你们)主动调用的,而是系统(love引擎)来调用他们,而调用的时机也是系统控制的。
love.load函数,是在循环开始前的必要设置时触发。
love.update函数,是在循环时,每当程序拥有控制权时触发。
(程序合适拥有控制权?程序更新的快慢也叫帧率,也就是每秒更新的频率,对于游戏玩家来讲一点都不陌生,简称fps,而每帧之间的时间间隔叫帧时间,简称dt。
提高帧率有助于让画面流畅,降低帧率可以降低cpu或gpu的使用率从而节能。如果开启垂直同步,则默认60帧每秒。垂直同步相关信息请咨询百度。
love.draw函数,是在循环时,游戏逻辑更新后,画面更新时触发。
进阶:对于老手来讲,请自行参阅wiki中的love.run,它是love运行时的一个回调,它控制着love的游戏循环。你可以更深的了解love是如何更新引擎的,同时也可以自行修改,使之更符合自身需求。
同时,love还有其他的回调,即在其他条件下可以触发的回调。比如下面将介绍的键盘按下,鼠标按下等。(其他回调可以参阅wiki中回调部分)
换一句话将,做游戏就是要把对应的功能放到对应回调中就可以了。

love引擎的输入和输出

输入

输入就是让电脑知道你想做什么,人物走停跑跳?车子拐弯?开枪?都需要电脑来读取你的输入。当然,输入也不局限于键盘和鼠标,他们是最基本的输入工具,还有比如游戏杆,触摸屏,重力感应器(手机,还有现在常见的vr设备),红外感应器(比如红外枪),还有主机相关的一堆杂七杂八的设备。下面介绍love2d提供了那些api(编程接口)来帮助用户判断这些输入。
键盘按下

1
love.keyreleased() --回调

键盘按住

1
love.keyboard.isDown() --状态函数

鼠标按下

1
love.mousereleased() --回调

鼠标按住

1
love.mouse.isDown() --状态函数

我们发现,有些函数是回调函数,有些函数是状态函数,这里说明一下。回调函数,上面已经介绍了,是在某个状态下就立即触发的,它并不依赖游戏循环。也就是说即使在两帧的间隙中,有按下这个动作,控制者(love内部的事件系统)就会立即call这个函数,从而让你有机会执行到函数内部的内容。而状态函数是一个查询键盘状态的函数,可以任意场合被用户主动调用。
任意键(比如鼠标,键盘)有2种状态和2个回调。状态就是要么一个键是被按下的,要么这个键不被按下。而事件有按下事件和弹起事件,这两个事件会触发相应的回调函数pressed和released。
进阶:一般而言,一个按键处于按下状态时,系统内部会有一个变量来标记你的某个按键处于按下状态。当这个按键不再处于按下状态时,我们检测这个键是否被标记为按下,然后就会触发这个键的弹起事件。有时候,我们不想使用love的按键回调,可以手动的去模拟这么个过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
local lctrlDown = false
function love.update()
if love.keyboard.isDown("lctrl") and not lctrlDown then
lctrlDown = true
-- lctrl pressed event
elseif lctrlDown then
lctrlDown = false
-- lctrl released event
end
if lctrlDown then
-- hold lctrl
end
end

输出

输出就是让用户(玩家)能够体验到游戏输入的反馈结果。实际方式也有很多,最常见的就是画面反馈了,然后还有声音,震动,以及其他设备对你感官的刺激(污)。下面介绍的是最基本的画面绘图方式。

设置颜色

1
love.graphics.setColor()

设置颜色换一个说法是对某个绘制对象进行染色。如果不做任何设置为白色。颜色有4个参数 r g b a,相关内容请咨询百度。
进阶:alpha值是如何影响各个绘制对象的需要参阅wiki的blendMode部分。类似于ps的图层混合方式。

矢量绘图:

1
2
3
4
5
6
7
love.graphics.points() --画点
love.graphics.line() --画线
love.graphics.rectangle() --画矩形(圆角)
love.graphics.circle() --画圆
love.graphics.polygon() --画多边形
love.graphics.arc() --画弧形
love.graphics.ellipse() --画椭圆

矢量绘图或者我更喜欢角顶点绘图,是最简单的绘图,在你学习时会经常使用到,或者某些特殊风格的游戏也会直接用到。他们的特点就是你给出一个图形的顶点,然后告诉引擎要填充还是要画线,就可以画出图像了。当然,对于点和线没有填充的意义。一个额外的知识就是,实际上我们画圆(椭圆)并不是圆,而是一些正多边形,边数越多,越圆。所以才会有画圆的最后一个参数。当然大多数时候留空默认就行了。
进阶:聪明的你一定知道如何画一个outlined描边多边形了。描边图形往往更加生动好看些,因为人观察东西主要是观察轮廓。

1
2
3
4
love.graphics.setColor(255,55,55,155)
love.graphics.circle("fill",300,300,100)
love.graphics.setColor(255,155,155,255)
love.graphics.circle("line",300,300,100)

创建图像对象以及绘制图像

1
2
love.graphics.newImage()
love.graphics.draw()

这里要说明的是,对于任何游戏引擎,我们知道的图片,实际上仅仅是数据,都不可以直接用来直接绘图的。而是有一个导入过程。而把图片的数据转化为可以被绘制的对象(在wiki里的那些drawable object)需要使用newImage函数。把他们呈现在画面上用draw函数。我们有时我们听说要减少drawcall提高效率就是指这些(也包括矢量绘图)。
关于draw的参数自己查阅wiki。
love的绘制使用的是屏幕坐标系,也是就是屏幕左上角作为坐标原点,y轴正方向向下,x轴正方向向右。也许初学者对此很不适应,只能慢慢调整了。另外所有图片的锚点默认的也是左上角。(关于锚点是什么意思,可以参考ps的旋转锚点,word图片编辑时也有锚点。)

设置字体以及绘制文字

1
2
3
love.graphics.newFont()
love.graphics.print()
love.graphics.setFont()

与图片制作图片对象一样,绘制文字首先要加载字体,同时规定字体大小。然后要设置这个字体为当前字体才能使用。

在具体学习之前,我十分希望你们自己先wiki一下上面所介绍的api。自己看看这些函数都需要填哪些参数,绘图对象有哪些方法等等,虽然我们可能并不马上用得到。

love编程时间

ok,那么我们开始学习制作第一个love案例,让我们来试试。

设计阶段

在任何编程操作之前,我们都要先进行设计,设计的过程可以在电脑上,纸上或者其他设计工具。在设计阶段,我们要弄清楚,我们最终的作品是什么样的,拥有那些元素,他们的输入和输出方法有哪些,基本的游戏循环涉及哪些元素,文件架构如何,需要引用那些外部资源及使用哪些库文件等等一系列问题。可能目前看起来上面的东西有点复杂,好在我们最开始时不用涉及太多问题,只需要弄清我们将要编程的对象是什么,它有哪些动作,有什么样的画面表现即可。

好的,我们开始设计阶段,首先罗列一个清单:

  1. 我们的游戏对象是一个有一些线条装饰,并且附带一个图片的圆,我们命名为test。
  2. 键盘按住ws时,这个圆每帧横向移动1个像素位置(像素是什么?自己百度吧。)
  3. 键盘按下ad时,这个圆每次纵向移动100个像素。
  4. 鼠标任意键点击,随机重选位置。
  5. test需要画love-ball图片,需要一个线框圆圈以及一个第透明度的方块还要把文字写在下面。
    好啦,以上就是我们的设计内容,是不是很简单。上面几步我们基本实现了互动,只是还没有涉及“游戏性”的部分,作为一个简单的例子,已经足够啦。

实现阶段

首先,我们新建一个main.lua文件,然后敲入我们的可以用到的回调。

然后,根据设计,我们建立一个test对象,是的,lua table是我们最理想的对象形式,它融合了数组、构造、稀疏表等功能,他可以包容数字,字符,表以及函数等等。总之用它没错^^
我们给test一些属性,比如他的位置x,y,它的图像对象image,它的文字对象“hello lovers!”我们后面图像和位置都需要更新他们。
我们知道,设置部分要写在love.load里面,于是有一下代码

1
2
3
4
5
6
7
8
function love.load()
test = {
x = 400, --屏幕中央
y = 300,
image = love.graphics.newImage("images/love-ball.png"),
text = "hello lovers!"
}
end

关于lua的书写方面,我建议大家养成比较好的书写习惯,尽量避免用中文作为变量名(虽然这并不产生任何错误),比如合理的变量名,合理的空格和折行。总之好看就好。代码如果很乱的话,自己看也看不清,更何况别人了。

实现键盘按住,在update中使用isDown函数,也就是每帧看看键盘是否按住,如果按住则进行纵向移动。
实现键盘按下,则不属于更新部分,因为无论是否有Update更新,这个回调都单独响应。于是在love.keyrelease下输入平移的判断。
实现鼠标点击随机移动,跟键盘按键类似。这里使用了随机函数,love.math.random,请自行查阅wiki。
插一句,love的窗体默认状态下是800*600垂直同步。你可以通过love.conf来初始设置。以及love.graphics.setMode来动态调整。
update的回调函数有个参数叫dt,我们之前说的帧时间,我们暂时不用。可以使用love.timer.getDelta来获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function love.update(dt)
if love.keyboard.isDown("w") then
test.y = test.y - 1 --上
elseif love.keyboard.isDown("s") then
test.y = test.y + 1 --下
end
end
function love.keyreleased(key)
if key == "a" then
test.x = test.x - 100 --左
elseif key == "d" then
test.x = test.x + 100 --右
end
end
function love.mousereleased(key)
test.x = love.math.random(1,800) -- 1到800间随机取
test.y = love.math.random(1,600)
end

接下来,我们让test呈现在我们屏幕上,绘制可能遇到的参数包括,x,y位置,r旋转,sx,sy按坐标轴缩放,ox,oy锚点位置,kx,ky平行畸变(暂时不要管他)
进阶:如果知道4*4Matrix的童鞋应该能够理解,这些东西实际上只是矩阵的简单化。当然你也可以用矩阵方式重新做这些。
注意,锚点位置也会影响缩放中心和旋转中心,所以仅仅使用手动平移的画会有问题的。

1
2
3
4
5
6
7
8
9
10
11
function love.draw()
love.graphics.setColor(255, 255, 255, 255)
love.graphics.draw(test.image,test.x,test.y,0,1,1,32,32) --图片的宽高是64*64,如何查?请查阅image对象
--love.graphics.draw(test.image,test.x - 32,test.y - 32) --同样的效果,但是不方便未来的拓展。
love.graphics.setColor(255, 0, 0, 255)
love.graphics.circle("line", test.x, test.y, 100) --圆的中心当然是圆心了
love.graphics.setColor(0, 255, 255, 50)
love.graphics.rectangle("fill", test.x - 100, test.y - 100, 200, 200) --矩形的起点是他的左上角
love.graphics.setColor(255, 255, 255, 255)
love.graphics.print(test.text, test.x-100, test.y+100) --字的锚点也是左上角
end

如果会用ps或则其他绘图软件的人,其实应该很容易理解,可以想象每个drawcall相当于ps中的一个图层,可以对图层进行颜色设置。而后面的图层会覆盖前面的图层,图层间也有相应的混合模式,矢量绘图实际上就是绘制路径,而绘制图片对象就相当于一个普通的像素图层。只不过我们用代码来控制他们绘制的位置。而且是实时控制的。是不是这样就会很容易理解游戏绘图的本质了?
当然,要注意的是,love在每次绘图时,都会用背景颜色重新填充整个屏幕,所以需要每一帧都重画。至于画一次静态图,就不需要每次都重绘,就需要用到canvas,这里先不做讲解,有兴趣的可以自己试试。

上面的代码就包含了,做一个游戏所需的互动内容。是不是很简单? 我们现在实现了第一步。接下来的一课,我们希望把游戏互动变得更加复杂和更有意思。就要在update里动手脚了。

作业

任意替换本课的图片内容,文字,颜色,矢量绘图,互动方式等
用矢量绘图绘制一个安卓机器人或者其他的东西。
在PS中绘制一些东西,在love里重新绘制,理解love绘图。
试试其他移动游戏对象的方式。我们下节课会具体讲解。

本课代码

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
function love.load()
test = {
x = 400,
y = 300,
image = love.graphics.newImage("images/love-ball.png"),
text = "hello lovers!"
}
end
function love.update(dt)
if love.keyboard.isDown("w") then
test.y = test.y - 1
elseif love.keyboard.isDown("s") then
test.y = test.y + 1
end
end
function love.draw()
love.graphics.setColor(255, 255, 255, 255)
love.graphics.draw(test.image,test.x,test.y,0,1,1,32,32)
love.graphics.setColor(255, 0, 0, 255)
love.graphics.circle("line", test.x, test.y, 100)
love.graphics.setColor(0, 255, 255, 50)
love.graphics.rectangle("fill", test.x - 100, test.y - 100, 200, 200)
love.graphics.setColor(255, 255, 255, 255)
love.graphics.print(test.text, test.x-100, test.y+100)
end
function love.keyreleased(key)
if key == "a" then
test.x = test.x - 100
elseif key == "d" then
test.x = test.x + 100
end
end
function love.mousereleased(key)
test.x = love.math.random(1,800)
test.y = love.math.random(1,600)
end