跳转至内容

07. Storing Levels as Strings

In this appendix a method to use text strings to define and store levels is demonstrated.

本附录展示一种用文本字符串来定义和存储关卡的方法。

In the part 1-06 levels are represented by 2d Lua tables. This is acceptable for small maps, but quickly becomes inconvenient as level size and complexity grows. A widely used technique is to store levels as text strings. This can be a good solution for medium-sized maps, where tables are already inefficient and specialized tools such as Tiled are overkill.

第 1-06 部分中,我们用二维 Lua 表来表示关卡。对于小地图这完全可行,但关卡尺寸和复杂度一上来,这种方式就会很不方便。一个广泛使用的技巧是用文本字符串来存储关卡。对于中等规模的地图,这是个不错的方案:表结构已经显得低效,而像 Tiled 这样的专用工具又显得过于重。

For Arkanoid, it is natural to represent a level as a multiline string. Each line corresponds to a row of bricks, and each character in this line - to specific position. To define multiline strings, Lua provides special syntax - double square brackets. Let's agree that the hash symbol '#' denotes a brick, and the space ' ' or the underscore '_' denote empty space. That way it is possible to use underscores to visually indicate size of the map.

对于 Arkanoid 来说,用多行字符串来表示关卡非常自然。每一行对应一行砖块,而该行中的每个字符对应一个具体位置。Lua 用双中括号来定义多行字符串。我们约定 '#' 表示砖块,空格 ' ' 或下划线 '_' 表示空位。这样就可以用下划线直观地表示地图的尺寸。

lua
levels.sequence = {}
levels.sequence[1] = [[  --(*1)
___________

# # ### # #
# # #   # #
### ##   #
# # #    #
# # ###  #
___________
]]

levels.sequence[2] = [[
___________

##  # # ###
# # # # #
###  #  ##
# #  #  #
###  #  ###
___________
]]

(*1): Care should be taken not to put any whitespaces or any other characters after opening brackets, because Lua will treat it as an additional line that will be added to the level data (I've put several spaces and a comment here to illustrate the point, but they are not present in the actual code). Depending on the way it's parsed, it may or may not cause errors.

(*1):要注意在双中括号的开头不要额外放空白或其它字符,否则 Lua 会把它当作额外的一行加进关卡数据里(我在这里加了几个空格和注释只是为了说明问题,实际代码里没有)。根据解析方式不同,这可能会导致错误,也可能不会。

To construct levels from such data, it is necessary to iterate over each line and each character of each line. Lua doesn't allow to iterate over contents of the string directly, but it has built-in capabilities for pattern matching and regular expressions, which can be used instead. In this case, gmatch string method is a good choice: for a given string and a regexp pattern, it allows to iterate over each substring in the string that matches the pattern. Typically it is used in constructions with the for-loop, such as

要根据这些数据构建关卡,就需要遍历每一行以及每一行中的每个字符。Lua 不能直接遍历字符串内容,但提供了内建的模式匹配能力(类似正则),可以用它来实现。在这里,字符串方法 gmatch 很合适:给定一个字符串和模式,它可以遍历字符串中每个匹配该模式的子串。通常会配合 for 循环使用,比如:

lua
for pattern_match in some_string:gmatch( pattern ) do
   print( pattern_match )
end

To extract a single line from a multiline string, a '(.-)\n' pattern can be used. Dot '.' matches arbitrary character, minus '-' means "match the previous character or class of characters zero or more times, as few times as possible", brackets '()' denote a group and '\n' stands for the new line character. Literally '(.-)\n' reads as "match shortest sequence of arbitrary characters of lengths zero or more with newline at the end". So, effectively, '(.-)' matches everything from the beginning of the line to the nearest end-of-line character, which is precisely a single line of a multiline string. To iterate over each character of the extracted line, a call to gmatch('.') is enough: the dot '.' matches single arbitrary character.

要从多行字符串中提取单行,可以用 '(.-)\n' 这个模式。点号 '.' 匹配任意字符,减号 '-' 表示“尽可能短地匹配前一个字符或字符类 0 次或多次”,括号 '()' 表示分组,'\n' 表示换行。'(.-)\n' 可以理解为“匹配最短的、以换行结尾的任意字符序列”。因此 '(.-)' 实际上匹配的是从行首到最近的行尾,这正好就是一行。要遍历该行中的每个字符,只要对这行调用 gmatch('.') 就行了,因为 '.' 匹配单个任意字符。

The iteration should be performed in the bricks.construct_level function, where depending on the extracted symbol certain actions are performed:

遍历应该放在 bricks.construct_level 中,并根据提取到的字符执行相应操作:

lua
function bricks.construct_level( level_bricks_arrangement )
   bricks.no_more_bricks = false
   local row_index = 0
   for row in level_bricks_arrangement:gmatch( '(.-)\n' ) do          --(*1)
      row_index = row_index + 1
      local col_index = 0
      for bricktype in row:gmatch('.') do                             --(*2)
         col_index = col_index + 1
         if bricktype == '#' then                                     --(*3)
            local new_brick_position_x = bricks.top_left_position_x +
               ( col_index - 1 ) *
               ( bricks.brick_width + bricks.horizontal_distance )
            local new_brick_position_y = bricks.top_left_position_y +
               ( row_index - 1 ) *
               ( bricks.brick_height + bricks.vertical_distance )
            local new_brick = bricks.new_brick( new_brick_position_x,
                                                new_brick_position_y )
            table.insert( bricks.current_level_bricks, new_brick )
         end
      end
   end
end

(*1): Iteration over each line of the string representing level. An extracted line is assigned to the row variable.
(*2): Iteration over each character of a single line.
(*3): Dispatch on type of the extracted character.

(*1):遍历代表关卡的字符串的每一行,提取出来的行赋值给 row
(*2):遍历单行中的每个字符。
(*3):根据提取字符的类型做分发处理。