天天看点

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

预览游戏

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

love2d游戏引擎重要函数

详情:

  • 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
      

项目结构

项目结构图如下