第 6 章 — 实现高分榜

上一页 | 下一页

在本章中,我们将创建一个场景来显示游戏中的十个最高分。我们还将探讨如何将这些分数保存到持久化存储位置。

Composer 可访问数据

在大多数情况下,当您使用 Composer 时,场景保持自包含,这意味着您在给定场景中创建的变量、函数和对象仅隔离在该特定场景中。例如,我们在 `game.lua` 中使用的 `lives` 和 `score` 变量仅与该场景关联。

这通常有利于保持应用程序的整洁和有序,但有时您需要从本质上不知道其存在的不同场景访问一个场景中的变量/对象。例如,在本章中,我们需要访问玩家的分数(`game.lua` 中的 `score`)在我们新的 `highscores.lua` 场景中。

Lua 本身提供了多种在模块之间传递和访问数据的方法,但 Composer 通过以下命令使其更加容易

掌握了这些命令后,让我们更新 `endGame()` 函数

  1. 打开您的 `game.lua` 文件。

  2. 找到 `endGame()` 函数并将其内容**替换**为以下两个命令

local function endGame()
    composer.setVariable( "finalScore", score )
    composer.gotoScene( "highscores", { time=800, effect="crossFade" } )
end

第一个新命令,`composer.setVariable( "finalScore", score )`,创建一个Composer 可访问的名为 `finalScore` 的变量,并将其赋值为 `score` 变量。这样,我们就能够从高分场景中检索该值。

第二个命令只是将应用程序重定向到 `highscores.lua` 场景,而不是菜单场景。

  1. 保存您修改后的 `game.lua` 文件。

您可以传递任何标准 Lua 变量类型(包括表)作为 `composer.setVariable()` 的值。您甚至可以使用它使一个场景中的局部函数在另一个场景中可访问。这种灵活性使 `composer.setVariable()` 和 `composer.getVariable()` 成为 Composer 工具集中最有用的两个 API。

高分榜场景

我们的高分场景将主要具有存储和检索分数、确定十个最高分并显示它们的功能。

首先,复制标准的`scene-template.lua`文件,该文件包含在本章的 源文件中。将此副本重命名为 `highscores.lua`,将其放在您的 `StarExplorer` 项目文件夹中,然后使用您选择的文本编辑器将其打开。

像往常一样,我们将从初始化一些变量开始。将以下代码放在场景可访问的代码区域

-- -----------------------------------------------------------------------------------
-- Code outside of the scene event functions below will only be executed ONCE unless
-- the scene is removed entirely (not recycled) via "composer.removeScene()"
-- -----------------------------------------------------------------------------------

-- Initialize variables
local json = require( "json" )

local scoresTable = {}

local filePath = system.pathForFile( "scores.json", system.DocumentsDirectory )

让我们简要地检查一下这些命令

重要

您可能想知道为什么我们要创建一个**文件**来存储高分数据。为什么不直接将它们存储在 Lua 表中?原因是应用程序开发的基础。基本上,如果应用程序退出/关闭,则应用程序本地内存中存在的变量将被销毁!

本质上,任何需要在应用程序退出/关闭后某个时间点访问的数据都应存储在**持久**状态,而存储持久数据的最简单方法是将其保存到设备上的文件中。此外,此文件必须存储在**持久位置**。

为了确保将 `scores.json` 文件放置在持久位置,我们将 `system.DocumentsDirectory` 指定为刚刚输入的命令的第二个参数。这告诉 Solar2D 在应用程序内部的“documents”目录中创建 `scores.json` 文件。虽然我们可以将文件放在其他位置,但我们在这里不会详细介绍它们 - 只需记住,由常量 `system.DocumentsDirectory` 引用的 documents 目录是唯一可以为从应用程序内部创建的文件提供真正持久存储的地方。换句话说,即使玩家退出了应用程序并且一个月后才再次打开它,`scores.json` 文件仍然存在。

加载数据

现在我们已经初始化了用于存储分数的文件,让我们编写一个函数来检查先前保存的任何分数。当然,目前还没有任何分数,但我们最终将需要此函数。

紧跟您已经添加到场景可访问的代码区域的命令之后,添加以下 `loadScores()` 函数

local filePath = system.pathForFile( "scores.json", system.DocumentsDirectory )


local function loadScores()

    local file = io.open( filePath, "r" )

    if file then
        local contents = file:read( "*a" )
        io.close( file )
        scoresTable = json.decode( contents )
    end

    if ( scoresTable == nil or #scoresTable == 0 ) then
        scoresTable = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
    end
end

剖析此函数,我们完成了以下操作

如果您想让事情更有趣,请以“计算机”获得的十个默认分数开始游戏,并挑战玩家击败它们!例如

scoresTable = { 10000, 7500, 5200, 4700, 3500, 3200, 1200, 1100, 800, 500 }

保存数据

保存数据与读取数据一样简单。在之前的函数之后场景可访问的代码区域,添加一个 `saveScores()` 函数

    if ( scoresTable == nil or #scoresTable == 0 ) then
        scoresTable = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
    end
end


local function saveScores()

    for i = #scoresTable, 11, -1 do
        table.remove( scoresTable, i )
    end

    local file = io.open( filePath, "w" )

    if file then
        file:write( json.encode( scoresTable ) )
        io.close( file )
    end
end

此函数如下保存高分数据

显示高分

在向玩家显示分数之前,我们需要稍微操作一下 `scoresTable` 表。具体来说,我们需要将最新的分数添加到表中,然后将表条目从高到低排序。

此场景的所有工作都可以在 `scene:create()` 中完成,因此让我们将注意力集中在该函数上

  1. 在我们处理分数数据之前,我们需要加载现有的分数。让我们通过调用 `loadScores()` 函数来做到这一点
-- create()
function scene:create( event )

    local sceneGroup = self.view
    -- Code here runs when the scene is first created but has not yet appeared on screen

    -- Load the previous scores
    loadScores()
end
  1. 接下来,让我们将玩家最新的分数插入到 `scoresTable` 表中。请注意,我们是通过调用 `composer.getVariable()` 并使用唯一参数 `“finalScore”` 来获取其值的。这展示了 `composer.setVariable()`命令的强大功能 - 添加到 `game.lua` -用于设置一个Composer 可访问的我们现在可以在此场景 (`highscores.lua`) 中访问的变量。之后,我们立即将其值重置为 `0`,因为我们不再需要进一步记录它。
    -- Load the previous scores
    loadScores()
    
    -- Insert the saved score from the last game into the table, then reset it
    table.insert( scoresTable, composer.getVariable( "finalScore" ) )
    composer.setVariable( "finalScore", 0 )
end
  1. 目前,我们不知道我们的分数在现有高分中排名如何。它甚至可能没有超过十个最高分中的最低分!为了检查这一点,让我们对表格的值进行**排序**,从高到低。这将自动将我们的分数放在正确的顺序。如果它不够高,无法进入前十名,它将保留在第 11 位,但由于我们只显示十个分数,因此您不会在屏幕上看到它。

要对表格进行排序,我们使用 Lua 的 `table.sort()` 命令。为此,我们必须为它提供要排序的表格和一个比较函数 (`compare()`) 的引用,该函数确定项目是否需要交换位置。这里我们将 `compare()` 函数直接编写在 `scene:create()` 内部,因为它不需要在其他地方访问。

    -- Insert the saved score from the last game into the table, then reset it
    table.insert( scoresTable, composer.getVariable( "finalScore" ) )
    composer.setVariable( "finalScore", 0 )

    -- Sort the table entries from highest to lowest
    local function compare( a, b )
        return a > b
    end
    table.sort( scoresTable, compare )
end

`compare()` 函数本身接受 `table.sort()` 提供的两个值。由于我们正在对数字表格进行排序,因此两个参数 `a` 和 `b` 将是数值。该函数比较这两个值,如下所示“`a` 大于 `b` 吗?”如果为 true,则返回 `true`,并且 `table.sort()` 知道它需要交换这些值。本质上,当 `table.sort()` 过程完成时,我们的 `scoresTable` 值将从高到低排序。

  1. 现在表格已排序,让我们通过调用 `saveScores()` 函数将数据保存回 `scores.json`。
    -- Sort the table entries from highest to lowest
    local function compare( a, b )
        return a > b
    end
    table.sort( scoresTable, compare )

    -- Save the scores
    saveScores()
end
  1. 接下来,让我们创建场景背景和一个文本对象 — 这里没有什么特别的;现在您已经是专家了!
    -- Save the scores
    saveScores()

    local background = display.newImageRect( sceneGroup, "background.png", 800, 1400 )
    background.x = display.contentCenterX
    background.y = display.contentCenterY
    
    local highScoresHeader = display.newText( sceneGroup, "High Scores", display.contentCenterX, 100, native.systemFont, 44 )
end
  1. 接下来,让我们使用一个简单的 `for` 循环从 `1` 到 `10` 来显示分数。对于每个分数,我们将首先通过计算局部变量 `yPos` 来设置预期的 **y** 位置。这将使分数在屏幕上向下排列,并均匀分布。
    local highScoresHeader = display.newText( sceneGroup, "High Scores", display.contentCenterX, 100, native.systemFont, 44 )

    for i = 1, 10 do
        if ( scoresTable[i] ) then
            local yPos = 150 + ( i * 56 )

        end
    end
end

对于每个分数行,我们将显示**两个**文本对象。左边将是一个从 **1)** 到 **10)** 的排名数字。它的右边将是实际分数。

    for i = 1, 10 do
        if ( scoresTable[i] ) then
            local yPos = 150 + ( i * 56 )

            local rankNum = display.newText( sceneGroup, i .. ")", display.contentCenterX-50, yPos, native.systemFont, 36 )
            rankNum:setFillColor( 0.8 )
            rankNum.anchorX = 1

            local thisScore = display.newText( sceneGroup, scoresTable[i], display.contentCenterX-30, yPos, native.systemFont, 36 )
            thisScore.anchorX = 0
        end
    end
end

以上大多数代码应该很简单,但我们在**锚点**中引入了一个重要的新概念。默认情况下,Solar2D 将任何显示对象的**中心**放置在给定的 **x** 和 **y** 坐标处。但是,有时您需要沿其边缘对齐一系列对象 — 在这里,如果每个排名数字都右对齐并且每个分数都左对齐.

为了实现这一点,请注意我们设置了每个对象的 `anchorX` 属性。此属性通常介于 `0`(左)和 `1`(右)之间,默认为 `0.5`(中心)。由于我们希望每个排名数字都右对齐与其他数字对齐,我们将 `anchorX` 设置为 `1`,对于每个分数数字,我们将 `anchorX` 设置为 `0` 以进行左对齐。

当然,Solar2D 也支持使用 `anchorY` 属性的垂直锚点。与其水平对应物类似,此属性通常介于 `0`(顶部)和 `1`(底部)之间,默认为 `0.5`(中心)。锚点甚至可以设置在 `0` 到 `1` 范围之外,尽管这种用法不太常见。将 `anchorX` 或 `anchorY` 设置为小于 `0` 或大于 `1` 的值会将锚点概念上放置在对象边缘边界之外的空间中的某个位置,这在某些情况下可能很有用。

  1. 最后,让我们创建一个按钮(文本对象)以返回菜单。这就像您在菜单场景中创建的文本按钮一样,它的 `"tap"` 事件侦听器将调用一个基本的 `gotoMenu()` 函数,我们将在接下来编写它。
        end
    end

    local menuButton = display.newText( sceneGroup, "Menu", display.contentCenterX, 810, native.systemFont, 44 )
    menuButton:setFillColor( 0.75, 0.78, 1 )
    menuButton:addEventListener( "tap", gotoMenu )
end

离开场景

回到场景可访问的代码区域,我们需要添加处理 `menuButton` 对象的 `"tap"` 事件的函数。这很简单

local function gotoMenu()
    composer.gotoScene( "menu", { time=800, effect="crossFade" } )
end


-- -----------------------------------------------------------------------------------
-- Scene event functions
-- -----------------------------------------------------------------------------------

场景清理

与 `game.lua` 场景类似,让我们在其自身的 `scene:hide()` 函数中移除 `highscores.lua` 场景。当玩家返回菜单场景时,这将从内存中移除该场景。

在 `scene:hide()` 函数中,添加以下突出显示的行

-- hide()
function scene:hide( event )

    local sceneGroup = self.view
    local phase = event.phase

    if ( phase == "will" ) then
        -- Code here runs when the scene is on screen (but is about to go off screen)

    elseif ( phase == "did" ) then
        -- Code here runs immediately after the scene goes entirely off screen
        composer.removeScene( "highscores" )
    end
end

我们的高分场景到此结束!保存您修改后的 `highscores.lua` 和 `game.lua` 文件,然后重新启动模拟器。现在您应该能够玩游戏并看到您的分数保存在高分场景中并进行了排序。

此场景相对简单,但如果您的代码未按预期工作,请将其与本章源文件中捆绑的 `highscores.lua` 文件进行比较。

章节概念

我们在本章中介绍了几个更重要的概念

命令/属性 描述
system.pathForFile() 使用系统定义的目录作为基准生成绝对路径。系统定义的目录。
io.open() 打开文件以进行读取或写入。
io.close() 关闭打开的文件句柄。
file:read() 根据给定的指定读取内容的格式读取文件。
file:write() 将其每个参数的值写入文件。
json.decode() 解码 JSON 编码的数据结构并返回一个包含数据的 Lua 表格。
json.encode() 将 Lua 表格转换为JSON 编码的字符串。
composer.setVariable() 设置在一个场景中声明的变量,使其在整个 Composer 应用程序中都可访问。
composer.getVariable() 允许您检索通过 composer.setVariable() 设置的任何变量的值。
table.sort() 按给定顺序对表格元素进行排序。
object.anchorX 允许您控制显示对象沿 **x** 方向对齐的属性。
object.anchorY 允许您控制显示对象沿 **y** 方向对齐的属性。