Love2D游戏引擎制作贪吃蛇游戏

代码地址如下:
http://www.demodashi.com/demo/15051.html

Love2D游戏引擎制作贪吃蛇游戏

内附有linux下的makefile,windows下的生成方法请查看:

for windows

预览游戏

love2d游戏引擎重要函数

详情:

love2d wiki

  • love.load:当游戏开始时被调用且仅调用一次

  • love.draw:回调函数,每帧更新一次游戏画面

  • love.update:回调函数,每帧更新一次游戏状态

  • love.keypressed:回调函数,当有按键被按下时触发

  • love.filesystem.load:加载一个lua脚本文件但不执行

!其他的函数在用到时再做解释

版本区别以及初始化资源

!首先要注意的是,本次使用的游戏引擎时love 0.9版本,与最新的love 11.x版本稍有区别。在0.9版本中颜色使用0~255来表示,而在11.x版本中是0~1来表示。

因为需要制作的游戏非常小,所以我们将所用到的资源在第一时间将其初始化并加载到内存中,以便使用。使用到的资源主要有:

  • 字体

  • 颜色

  • 声音

  • 窗口大小与块大小

  • 标题

  • 边框

所用到的函数:

  • love.window.setMode:设置窗口大小,以及样式

  • love.window.setTitle:设置标题

  • love.graphics.newFont:加载字体文件,大小自定义,返回Font类型

  • love.audio.newSource:加载音效文件

代码如下:

function love.load ()
	-- 块大小,窗口宽高,标题
	cellSize = 20
	width = 20 * 40
	height = 20 * 25
	title = 'SNAKE !'

	-- 设置窗口大小和标题
	love.window.setMode (width, height)
	love.window.setTitle (title)

	-- 加载不同大小字体
	fonts = {
		pixies100 = love.graphics.newFont ('Fonts/Pixies.TTF', 100),
		pixies30 = love.graphics.newFont ('Fonts/Pixies.TTF', 30),
		pixies10 = love.graphics.newFont ('Fonts/Pixies.TTF', 10)
	}

	-- 加载音效资源
	sounds = {
		showMenu = love.audio.newSource ('Sounds/showMenu.wav', 'stream'),
		switchOption = love.audio.newSource ('Sounds/switchOption.wav', 'stream'),
		eatFood = love.audio.newSource ('Sounds/eatFood.wav', 'stream'),
		collided = love.audio.newSource ('Sounds/collided.wav', 'stream'),
		gameOver = love.audio.newSource ('Sounds/gameOver.wav', 'stream')
	}

	-- 边框数据
	border = {
		1, 1,
		width-1, 1,
		width-1, height-1,
		1, height-1,
		1, 1
	}

	-- 颜色数据
	colors = {
		darkGray = { 0.3, 0.3, 0.3, 1 },
		beiga = { 0.98, 0.91, 0.76, 1 },
		white = { 1, 1, 1, 1 },
		paleTurquoise = { 0.7, 1, 1, 1 },
	}

	SwitchScence ('Menu')
end

场景与其切换

!首先我们需要实现一个简单的场景切换函数,因为一个游戏总是有多个场景

  1. 先将love2d引擎的主要回调函数赋值nil以免之后出现错误
  2. 加载新场景的lua脚本
  3. 执行新场景的lua脚本

代码如下:

function SwitchScence (scence)
	-- 将重要的函数赋予空值,以免冲突
	love.update = nil
	love.draw = nil
	love.keypressed = nil

	-- 将需要的场景加载进来,并执行load函数
	love.filesystem.load ('Scences/'..scence..'.lua') ()
	love.load ()
end

-- 切换到初始化场景
SwitchScence ('Init')

绘制开始界面

在这里我们需要认识一些绘图函数:

  • love.graphics.setFont:设置当期字体

  • love.graphics.setColor:设置当前颜色

  • love.graphics.rectangle:绘制矩形

  • love.graphics.line:绘制直线

  • love.graphics.print:在窗口上输出

!绘制比较简单,其他详情都在代码里有详细注释,要注意的是我绘制选项的方法。options的有效长度并不是#options,而是options.count记录的选项数量

代码如下:

-- 游戏标题,以及绘制位置
local gameName = {
	text = title,
	textX = cellSize * 12,
	textY = cellSize * 6
}

-- 选项:开始和退出
local options = {
	{
		text = "START",

		textX = cellSize * 18,
		textY = cellSize * 15 - 5,

		border = {
			cellSize*16, cellSize*14,
			cellSize*24, cellSize*14,
			cellSize*24, cellSize*17,
			cellSize*16, cellSize*17,
			cellSize*16, cellSize*14
		}
	},
	{
		text = "QUIT",

		textX = cellSize * 19 - 10,
		textY = cellSize * 19 - 5,

		border = {
			cellSize*16, cellSize*18,
			cellSize*24, cellSize*18,
			cellSize*24, cellSize*21,
			cellSize*16, cellSize*21,
			cellSize*16, cellSize*18
		}
	},

	-- 一些其他属性
	count = 2,
	selected = 1
}

function love.load ()
	-- 加载并播放背景音乐
	sounds.showMenu:play ()

	-- 设置米色和蓝色的透明程度为0,为了之后的动画效果
	colors.beiga[4] = 0
	colors.paleTurquoise[4] = 0
end

function love.draw ()
	-- 灰色背景
	love.graphics.setColor (colors.darkGray)
	love.graphics.rectangle (
		'fill',
		0,
		0,
		width,
		height
	)

	-- 白色边框
	love.graphics.setColor (colors.white)
	love.graphics.line (border)

	-- 渐显效果
	if colors.beiga[4] < 1 then
		colors.beiga[4] = colors.beiga[4] + 0.01
		colors.paleTurquoise[4] = colors.paleTurquoise[4] + 0.01
	end

	-- 设置字体,在指定位置画出米色标题
	love.graphics.setFont (fonts.pixies100)
	love.graphics.setColor (colors.beiga)
	love.graphics.print (gameName.text, gameName.textX, gameName.textY)

	-- 设置字体
	love.graphics.setFont (fonts.pixies30)

	-- 绘制所有选项
	for i = 1, options.count do
		if i == options.selected then
			love.graphics.setColor (colors.paleTurquoise)
		else
			love.graphics.setColor (colors.beiga)
		end

		-- 绘制选项边框和字体
		love.graphics.line (options[i].border)
		love.graphics.print (options[i].text, options[i].textX, options[i].textY)
	end
end

function love.keypressed (key)
	-- 上下箭头选择选项,回车按键确认选项
	if key == 'up' then
		-- 关闭切换选项的声音并重新播放
		if sounds.switchOption.isPlaying then
			sounds.switchOption:stop ()
		end
		sounds.switchOption:play ()

		-- 切换当前选项索引
		options.selected = options.selected - 1
		if options.selected <= 0 then
			options.selected = options.count
		end
	elseif key == 'down' then
		-- 同上
		if sounds.switchOption.isPlaying then
			sounds.switchOption:stop ()
		end
		sounds.switchOption:play ()

		options.selected = options.selected + 1
		if options.selected > options.count then
			options.selected = 1
		end
	elseif key == 'return' then
		-- 关闭显示界面声音
		if sounds.showMenu.isPlaying then
			sounds.showMenu:stop ()
		end

		-- 对应不同选项作出不同回应
		if options.selected == 1 then
			SwitchScence ('GameStart')
		elseif options.selected == 2 then
			love.event.quit ()
		end
	end
end

实现游戏主体

游戏的实现方法,主要知道两个方面:

  • 蛇的移动方式:根据方向获取下一个头的位置,若没有吃到食物就将蛇尾删除,达到移动效果
-- 下一个蛇头位置
local nextX = snake.body[1].x
local nextY = snake.body[1].y

-- 当方向队列中的方向大于1时除去第一个方向(当前方向)
if #directionQueue > 1 then
	table.remove (directionQueue, 1)
end

-- 根据方向作出改动
if directionQueue[1] == 'right' then
	nextX = nextX + 1
	if nextX > limit.x then
        nextX = 0
	end
    elseif directionQueue[1] == 'left' then
        nextX = nextX - 1
        if nextX < 0 then
            nextX = limit.x
        end
    elseif directionQueue[1] == 'down' then
	   nextY = nextY + 1
	   if nextY > limit.y then
		  nextY = 0
	   end
    elseif directionQueue[1] == 'up' then
        nextY = nextY - 1
        if nextY < 0 then
            nextY = limit.y
        end
    end

    -- 蛇是否可以移动(没有与自身相撞)
    local canMove = true
    for index, pair in ipairs (snake.body) do
        if index ~= #snake.body
        and nextX == pair.x
        and nextY == pair.y then
            canMove = false
        end
    end

	-- 当蛇可以移动时
	if canMove then
        -- 将新位置加在蛇身的头,并检测是否吃到了食物
		table.insert (snake.body, 1, { x = nextX, y = nextY })
		if nextX == food.x and nextY == food.y then
            -- 播放吃到食物的音效(关闭之前的音效)
			if sounds.eatFood.isPlaying then
				sounds.eatFood:stop ()
			end
			sounds.eatFood:play ()

			-- 分数加一,并生成新的食物位置
			currentScore.score = currentScore.score + 1
				CreateFood ()
			else
			-- 没有吃到食物则删去蛇身的尾部,达到移动的目的
			table.remove (snake.body)
		end
	else
	-- 蛇死亡,并播放相撞的音效
		snake.alive = false
		sounds.collided:play ()
	end
end

  • 方向队列的引入:主要是解决键位冲突的问题
function love.keypressed (key)
	-- 空格键暂停游戏
	if key == 'space' then
		paused = not paused
	end

	-- 没有暂停时
	if not paused then
		-- 记录方向键的按下顺序,同方向或相反方向的不记录
		if key == 'right'
		and directionQueue[#directionQueue] ~= 'right'
		and directionQueue[#directionQueue] ~= 'left' then
			table.insert (directionQueue, 'right')
		elseif key == 'left'
		and directionQueue[#directionQueue] ~= 'left'
		and directionQueue[#directionQueue] ~= 'right' then
			table.insert (directionQueue, 'left')
		elseif key == 'down'
		and directionQueue[#directionQueue] ~= 'down'
		and directionQueue[#directionQueue] ~= 'up' then
			table.insert (directionQueue, 'down')
		elseif key == 'up'
		and directionQueue[#directionQueue] ~= 'up'
		and directionQueue[#directionQueue] ~= 'down' then
			table.insert (directionQueue, 'up')
		end
	end
end

代码如下:

-- 游戏窗口与记分窗口的分界线
local boundary = {
	cellSize*30, 0,
	cellSize*30, height
}

-- 当前分数的信息
local currentScore = {
	text = 'SCORE',
	score = 0,

	-- 文字的绘图位置
	textX = cellSize * 33,
	textY = cellSize * 2,

	-- 分数的绘图位置
	scoreX = cellSize * 34,
	scoreY = cellSize * 5
}

-- 最高分的信息
local highScore = {
	text = 'HIGH SCORE',
	score = 0,

	-- 同上
	textX = cellSize * 31,
	textY = cellSize * 12,

	scoreX = cellSize * 34,
	scoreY = cellSize * 15
}

-- 提示信息
local notes = {
	{
		text = 'ARROW KEY TO MOVE',
		textX = cellSize * 34,
		textY = cellSize * 22
	},
	{
		text = 'ENTER KEY TO PAUSE',
		textX = cellSize * 34,
		textY = cellSize * 23
	}
}

-- 游戏窗口的限制
local limit = { x = 29, y = 24 }

-- 蛇的初始化信息
local snake = {
	-- 蛇身
	body = {
		{ x = 2, y = 0 },
		{ x = 1, y = 0 },
		{ x = 0, y = 0 }
	},

	-- 速度与状态
	speed = 0.1,
	alive = true,
}

-- 食物的位置
local food = { x = nil, y = nil }

-- 方向队列,用于记录键盘按下的顺序以免产生冲突
local directionQueue = { 'right' }

-- 计时器,暂停状态以及最高分文件
local timer = 0
local paused = false
local file = nil

-- 用于生成食物的可存在位置
local function CreateFood ()
	local foodPosition = {}

	-- 遍历整个窗口,将可生成食物的位置记录在foodPosition表里
	for i = 0, limit.x do
		for j = 0, limit.y do
			local possible = true

			-- 是否与蛇身冲突
			for index, pair in ipairs (snake.body) do
				if i == pair.x and j == pair.y then
					possible = false
				end
			end

			if possible then
				table.insert (foodPosition, { x = i, y = j })
			end
		end
	end

	-- 生成随机食物位置
	local index = love.math.random (#foodPosition)
	food.x, food.y = foodPosition[index].x, foodPosition[index].y
end

function love.load ()
	file = love.filesystem.newFile ('HighScore.txt')
	file:open ('r')
	highScore.score = file:read ()
	file:close ()

	-- 没有透明度
	colors.beiga[4] = 1
	colors.paleTurquoise[4] = 1

	CreateFood ()
end

function love.draw ()
	-- 绘制背景
	love.graphics.setColor (colors.darkGray)
	love.graphics.rectangle (
		'fill',
		0,
		0,
		width,
		height
	)

	-- 绘制白色边框和边界线
	love.graphics.setColor (colors.white)
	love.graphics.line (border)
	love.graphics.line (boundary)

	-- 设置字体和颜色,并在指定位置绘制当前分数信息和最高分信息
	love.graphics.setFont (fonts.pixies30)
	love.graphics.setColor (colors.beiga)
	love.graphics.print (currentScore.text, currentScore.textX, currentScore.textY)
	love.graphics.print (currentScore.score, currentScore.scoreX, currentScore.scoreY)
	love.graphics.setColor (colors.paleTurquoise)
	love.graphics.print (highScore.text, highScore.textX, highScore.textY)
	love.graphics.print (highScore.score, highScore.scoreX, highScore.scoreY)

	-- 蛇生存和死亡时使用不同的颜色绘制
	if snake.alive then
		love.graphics.setColor (colors.paleTurquoise)
	else
		love.graphics.setColor (colors.beiga)
	end

	-- 绘制蛇身,蛇头另绘
	for index, pair in ipairs (snake.body) do
		if index == 1 then
			love.graphics.rectangle (
				'fill',
				cellSize*pair.x,
				cellSize*pair.y,
				cellSize,
				cellSize
			)
		end
		love.graphics.rectangle (
			'fill',
			cellSize*pair.x+1,
			cellSize*pair.y+1,
			cellSize-1*2,
			cellSize-1*2
		)
	end

	-- 绘制食物
	love.graphics.setColor (colors.beiga)
	love.graphics.rectangle (
		'fill',
		cellSize*food.x+1,
		cellSize*food.y+1,
		cellSize-1*2,
		cellSize-1*2
	)

	-- 如果是暂停状态,则绘制暂停字样
	if paused then
		love.graphics.print ('PAUSED !', cellSize*12, cellSize*11)
	end

	-- 设置字体和颜色并绘制提示信息
	love.graphics.setFont (fonts.pixies10)
	love.graphics.setColor (colors.beiga)
	for i = 1, #notes do
		love.graphics.print (notes[i].text, notes[i].textX, notes[i].textY)
	end
end

function love.update (dt)
	-- 使用计时器
	timer = timer + dt

	-- 当蛇生存时
	if snake.alive then
		-- 根据蛇的速度更新游戏
		if timer > snake.speed then
			timer = timer - snake.speed

			-- 没有暂停时
			if not paused then
				-- 下一个蛇头位置
				local nextX = snake.body[1].x
				local nextY = snake.body[1].y

				-- 当方向队列中的方向大于1时除去第一个方向(当前方向)
				if #directionQueue > 1 then
					table.remove (directionQueue, 1)
				end

				-- 根据方向作出改动
				if directionQueue[1] == 'right' then
					nextX = nextX + 1
					if nextX > limit.x then
						nextX = 0
					end
				elseif directionQueue[1] == 'left' then
					nextX = nextX - 1
					if nextX < 0 then
						nextX = limit.x
					end
				elseif directionQueue[1] == 'down' then
					nextY = nextY + 1
					if nextY > limit.y then
						nextY = 0
					end
				elseif directionQueue[1] == 'up' then
					nextY = nextY - 1
					if nextY < 0 then
						nextY = limit.y
					end
				end

				-- 蛇是否可以移动(没有与自身相撞)
				local canMove = true
				for index, pair in ipairs (snake.body) do
					if index ~= #snake.body
					and nextX == pair.x
					and nextY == pair.y then
						canMove = false
					end
				end

				-- 当蛇可以移动时
				if canMove then
					-- 将新位置加在蛇身的头,并检测是否吃到了食物
					table.insert (snake.body, 1, { x = nextX, y = nextY })
					if nextX == food.x and nextY == food.y then
						-- 播放吃到食物的音效(关闭之前的音效)
						if sounds.eatFood.isPlaying then
							sounds.eatFood:stop ()
						end
						sounds.eatFood:play ()

						-- 分数加一,并生成新的食物位置
						currentScore.score = currentScore.score + 1
						CreateFood ()
					else
						-- 没有吃到食物则删去蛇身的尾部,达到移动的目的
						table.remove (snake.body)
					end
				else
					-- 蛇死亡,并播放相撞的音效
					snake.alive = false
					sounds.collided:play ()
				end
			end
		end
	-- 等待一秒
	elseif timer >= 1 then
		-- 存储最高分
		if currentScore.score > tonumber (highScore.score) then
			file:open ('w')
			file:write (tostring (currentScore.score))
			file:close ()
		end

		-- 切换到游戏结束场景
		SwitchScence ('GameOver')
	end
end

function love.keypressed (key)
	-- 回车键暂停游戏
	if key == 'return' then
		paused = not paused
	end

	-- 没有暂停时
	if not paused then
		-- 记录方向键的按下顺序,同方向或相反方向的不记录
		if key == 'right'
		and directionQueue[#directionQueue] ~= 'right'
		and directionQueue[#directionQueue] ~= 'left' then
			table.insert (directionQueue, 'right')
		elseif key == 'left'
		and directionQueue[#directionQueue] ~= 'left'
		and directionQueue[#directionQueue] ~= 'right' then
			table.insert (directionQueue, 'left')
		elseif key == 'down'
		and directionQueue[#directionQueue] ~= 'down'
		and directionQueue[#directionQueue] ~= 'up' then
			table.insert (directionQueue, 'down')
		elseif key == 'up'
		and directionQueue[#directionQueue] ~= 'up'
		and directionQueue[#directionQueue] ~= 'down' then
			table.insert (directionQueue, 'up')
		end
	end
end

实现最高分的保存与读取

游戏存档目录:

  • Windows XP: C:\Documents and Settings\user\Application Data\LOVE\ or %appdata%\LOVE\

  • Windows Vista and 7,8: C:\Users\user\AppData\Roaming\LOVE or %appdata%\LOVE\

  • Linux: $XDG_DATA_HOME/love/ or ~/.local/share/love/

  • Mac: /Users/user/Library/Application Support/LOVE/

!写文件只能在存档目录

最高分读取:

file = love.filesystem.newFile ('HighScore.txt')
file:open ('r')
highScore.score = file:read ()
file:close ()

最高分保存:

file:open ('w')
file:write (tostring (currentScore.score))
file:close ()

绘制游戏结束界面

游戏结束界面的绘制与开始界面大致相同,这里不再赘述

代码如下:

local gameOver = {
	text = 'GAME OVER !',
	textX = cellSize * 6,
	textY = cellSize * 6
}

-- 选项:开始和退出
local options = {
	{
		text = "BACK",

		textX = cellSize * 13 - 15,
		textY = cellSize * 17 - 5,

		border = {
			cellSize*10, cellSize*16,
			cellSize*18, cellSize*16,
			cellSize*18, cellSize*19,
			cellSize*10, cellSize*19,
			cellSize*10, cellSize*16
		}
	},
	{
		text = "RETRY",

		textX = cellSize * 24,
		textY = cellSize * 17 - 5,

		border = {
			cellSize*22, cellSize*16,
			cellSize*30, cellSize*16,
			cellSize*30, cellSize*19,
			cellSize*22, cellSize*19,
			cellSize*22, cellSize*16
		}
	},

	-- 一些其他属性
	count = 2,
	selected = 1
}

function love.load ()
	sounds.gameOver:play ()

	-- 设置米色和蓝色的透明程度为0,为了之后的动画效果
	colors.beiga[4] = 0
	colors.paleTurquoise[4] = 0
end

function love.draw ()
	-- 灰色背景
	love.graphics.setColor (colors.darkGray)
	love.graphics.rectangle (
		'fill',
		0,
		0,
		width,
		height
	)

	-- 白色边框
	love.graphics.setColor (colors.white)
	love.graphics.line (border)

	-- 渐显效果
	if colors.beiga[4] < 1 then
		colors.beiga[4] = colors.beiga[4] + 0.01
		colors.paleTurquoise[4] = colors.paleTurquoise[4] + 0.01
	end

	-- 设置字体,在指定位置画出米色标题
	love.graphics.setFont (fonts.pixies100)
	love.graphics.setColor (colors.beiga)
	love.graphics.print (gameOver.text, gameOver.textX, gameOver.textY)

	-- 设置字体
	love.graphics.setFont (fonts.pixies30)

	for i = 1, options.count do
		if i == options.selected then
			love.graphics.setColor (colors.paleTurquoise)
		else
			love.graphics.setColor (colors.beiga)
		end

		love.graphics.line (options[i].border)
		love.graphics.print (options[i].text, options[i].textX, options[i].textY)
	end
end

function love.keypressed (key)
	-- 上下箭头选择选项,回车按键确认选项
	if key == 'left' then
		if sounds.gameOver.isPlaying then
			sounds.gameOver:stop ()
		end

		if sounds.switchOption.isPlaying then
			sounds.switchOption:stop ()
		end
		sounds.switchOption:play ()

		options.selected = options.selected - 1
		if options.selected <= 0 then
			options.selected = options.count
		end
	elseif key == 'right' then
		if sounds.gameOver.isPlaying then
			sounds.gameOver:stop ()
		end

		if sounds.switchOption.isPlaying then
			sounds.switchOption:stop ()
		end
		sounds.switchOption:play ()

		options.selected = options.selected + 1
		if options.selected > options.count then
			options.selected = 1
		end
	elseif key == 'return' then
		if sounds.gameOver.isPlaying then
			sounds.gameOver:stop ()
		end

		if options.selected == 1 then
			SwitchScence ('Menu')
		elseif options.selected == 2 then
			SwitchScence ('GameStart')
		end
	end
end

项目结构

项目结构图如下

Love2D游戏引擎制作贪吃蛇游戏

代码地址如下:
http://www.demodashi.com/demo/15051.html

注:本文著作权归作者,由demo大师代发,拒绝转载,转载需要作者授权

posted on 2019-03-09 22:34  demo例子集  阅读(3206)  评论(0编辑  收藏  举报

导航