20. Spawning Bonuses
So far the game has been somewhat dull. To add some diversity to the gameplay, bonuses are introduced. They alter the game mechanics in minor ways and provide additional challenge for the player. Parts 3-04 -- 3-09 are dedicated to the bonuses. This part opens the sequence by addressing a question of how the bonuses appear in the game and their basic behavior.
到目前为止游戏有点单调。为了让玩法更丰富,我们引入奖励(bonuses)。它们会以一些小幅度的方式改变游戏机制,为玩家增加挑战。第 3-04 到 3-09 部分都会围绕奖励展开。本节先从奖励如何出现以及它们的基础行为讲起。

A bonus is an object that emerges on destruction of a brick. After it appears, it falls down with constant velocity. If the player catches it with the platform, i.e. on bonus-platform collision, the effect of the bonus is applied.
奖励是砖块被打碎后生成的对象。出现后会以恒定速度下落。如果玩家用平台接住它(即奖励与平台发生碰撞),就会触发奖励效果。
There could be several bonuses on the screen simultaneously. For that reason, basic definitions for the bonuses are similar to the bricks.
屏幕上可能同时存在多个奖励对象,因此奖励的基础定义和砖块比较类似。
Functions, responsible for behavior of a single bonus are shown below.
下面是单个奖励对象的行为函数:
local bonuses = {}
.....
bonuses.radius = 14 --(*2)
bonuses.speed = vector( 0, 100 ) --(*3)
.....
function bonuses.new_bonus( position, bonustype ) --(*1)
return( { position = position,
bonustype = bonustype,
quad = bonuses.bonustype_to_quad( bonustype ) } )
end
function bonuses.draw_bonus( single_bonus ) --(*2)
if single_bonus.quad then
love.graphics.draw(
bonuses.image,
single_bonus.quad,
single_bonus.position.x - bonuses.tile_width / 2,
single_bonus.position.y - bonuses.tile_height / 2 )
end
local segments_in_circle = 16
love.graphics.circle( 'line',
single_bonus.position.x,
single_bonus.position.y,
bonuses.radius,
segments_in_circle )
end
function bonuses.update_bonus( single_bonus ) --(*3)
single_bonus.position = single_bonus.position + bonuses.speed * dt
end
.....
return bonuses(*1): To create a single bonus, it necessary to provide to the constructor a position and a bonus type. Similarly to the bricks, a quad is inferred from the provided bonustype.
(*2): A bonus is represented as a sphere with the radius defined by bonuses.radius variable. In the drawing function, apart from the quad, the circle is drawn with the love.graphics.circle for testing purposes.
(*3): A bonus falls down with a constant speed, defined by bonuses.speed variable.
(*1):创建单个奖励需要提供位置和奖励类型。和砖块一样,quad 会根据 bonustype 推导出来。
(*2):奖励用一个半径由 bonuses.radius 定义的球体表示。在绘制函数里,除了 quad 之外,还用 love.graphics.circle 画一个圆用于测试。
(*3):奖励以恒定速度下落,速度由 bonuses.speed 定义。
There can be several bonus objects in the game simultaneously, and the functions that manage them are following:
游戏里可能同时存在多个奖励对象,管理它们的函数如下:
bonuses.current_level_bonuses = {} --(*1)
function bonuses.update( dt ) --(*2)
for _, bonus in pairs( bonuses.current_level_bonuses ) do
bonuses.update_bonus( bonus )
end
end
function bonuses.draw() --(*2)
for _, bonus in pairs( bonuses.current_level_bonuses ) do
bonuses.draw_bonus( bonus )
end
end
function bonuses.add_bonus( bonus ) --(*3)
table.insert( bonuses.current_level_bonuses, bonus )
end
function bonuses.generate_bonus( position, bonustype ) --(*3)
if bonuses.valid_bonustype( bonustype ) then
bonuses.add_bonus( bonuses.new_bonus( position, bonustype ) )
end
end
function bonuses.valid_bonustype( bonustype ) --(*3)
if bonustype and bonustype > 10 and bonustype < 19 then
return true
else
return false
end
end
function bonuses.clear_current_level_bonuses() --(*4)
for i in pairs( bonuses.current_level_bonuses ) do
bonuses.current_level_bonuses[i] = nil
end
end(*1): The presently active bonuses are stored in the bonuses.current_level_bonuses table.
(*2): draw and update methods iterate over the currently active bonuses and call the corresponding functions for the each individual bonus.
(*3): To create a bonus at the given position with a given bonustype, first it is necessary to check the validity of the bonustype. After that, the constructor for a single bonus can be called, and the resulting object can be inserted into the bonuses.current_level_bonuses table.
(*4): On transitions between the levels, it is necessary to clear the table with active bonuses.
(*1):当前激活的奖励存放在 bonuses.current_level_bonuses 表中。
(*2):draw 和 update 遍历当前奖励列表,并对每个奖励调用相应函数。
(*3):要在指定位置生成指定类型的奖励,先检查 bonustype 是否有效;有效则调用单个奖励的构造函数,并把结果插入 bonuses.current_level_bonuses。
(*4):切换关卡时需要清空当前奖励表。

A tileset definition for the bonuses is analogous to the bricks: quads in the tileset are indexed with a two-digit number where the first digit specifies the row, and the second - the column. Valid bonustypes are from 11 to 18.
奖励的 tileset 定义与砖块类似:用两位数字索引 quad,第一位表示行,第二位表示列。有效的 bonustype 为 11 到 18。
bonuses.image = love.graphics.newImage( "img/800x600/bonuses.png" )
bonuses.tile_width = 64
bonuses.tile_height = 32
bonuses.tileset_width = 512
bonuses.tileset_height = 32
function bonuses.bonustype_to_quad( bonustype )
if bonustype == nil or bonustype <= 10 or bonustype >= 20 then
return nil
end
local row = math.floor( bonustype / 10 )
local col = bonustype % 10
local x_pos = bonuses.tile_width * ( col - 1 )
local y_pos = bonuses.tile_height * ( row - 1 )
return love.graphics.newQuad(
x_pos, y_pos,
bonuses.tile_width, bonuses.tile_height,
bonuses.tileset_width, bonuses.tileset_height )
endAfter the basic definitions are ready, it is necessary to incorporate the bonuses into the game: insert the calls to the bonuses.update and bonuses.draw methods into the corresponding gamestates callbacks and so on. To avoid missing something, it helps to trace the occurrences of the bricks table in the gamestate modules. Since the bricks and the bonuses are alike, bonuses occur mostly in the same places as the bricks. It turns out, that only the "game" state needs modifications.
基础定义完成后,需要把奖励系统整合进游戏:在对应 gamestate 的回调里调用 bonuses.update 和 bonuses.draw 等。为了不漏掉,可以追踪 gamestate 模块里 bricks 表出现的位置。因为砖块和奖励很像,奖励通常也出现在与砖块相同的位置。最终发现只需要改动 “game” 状态。
.....
local bricks = require "bricks"
local bonuses = require "bonuses"
local walls = require "walls"
.....
function game.update( dt )
.....
bricks.update( dt )
bonuses.update( dt )
walls.update( dt )
.....
end
.....Now it is necessary to address a question of how to actually add new bonuses into the game. Depending on a desired game mechanics, this could be done in a variety of different ways. I want the bonuses to appear only after a destruction of a brick. A simple way to do this is to associate each brick with a certain bonustype and call the bonus constructor when the brick is destroyed. The bricks are defined in the level files in a form of a table. It is natural to add a table with the bonustypes into each level file. It should have the same size as the bricks table and should hold bonustypes associated with each brick.
接下来要解决的问题是:如何真正把新奖励加入游戏。根据想要的机制可以有多种做法。我希望奖励只在砖块被击毁时出现。一个简单的方法是给每块砖关联一个 bonustype,并在砖块被摧毁时调用奖励构造函数。砖块在关卡文件里以表的形式定义,因此可以在每个关卡文件里再加一个 bonustype 表,尺寸与砖块表一致,用来存放每块砖的奖励类型。
return {
bricks = {
{51, 51, 00, 00, 00, 00, 51, 51},
{51, 00, 00, 00, 00, 00, 00, 51},
{00, 00, 00, 00, 00, 00, 00, 00},
{00, 00, 00, 00, 00, 00, 00, 00},
{00, 00, 00, 00, 00, 00, 00, 00},
{21, 21, 22, 23, 24, 25, 26, 26},
{31, 31, 32, 33, 34, 35, 36, 36},
{41, 41, 42, 43, 44, 45, 46, 46},
{11, 11, 12, 13, 14, 15, 16, 16},
{00, 00, 00, 00, 00, 00, 00, 00},
{00, 00, 00, 00, 00, 00, 00, 00}
},
bonuses = {
{00, 00, 00, 00, 00, 00, 00, 00},
{00, 00, 00, 00, 00, 00, 00, 00},
{00, 00, 00, 00, 00, 00, 00, 00},
{00, 00, 00, 00, 00, 00, 00, 00},
{00, 00, 00, 00, 00, 00, 00, 00},
{17, 11, 12, 13, 14, 15, 16, 18},
{17, 11, 12, 13, 14, 15, 16, 18},
{17, 11, 12, 13, 14, 15, 16, 18},
{17, 11, 12, 13, 14, 15, 16, 18},
{00, 00, 00, 00, 00, 00, 00, 00},
{00, 00, 00, 00, 00, 00, 00, 00}
}
}Since each brick is going to hold an associated bonustype, it is necessary to add an appropriate argument to the bricks constructor bricks.new_brick( ....., bonustype ). The bricks for the current level are constructed in the bricks.construct_level function. Apart from parsing the brick types from the level file, now it is also necessary to extract the bonustypes and pass them to the bricks constructor.
既然每块砖都要带一个 bonustype,就需要在 bricks.new_brick( ....., bonustype ) 中增加这个参数。当前关卡的砖块由 bricks.construct_level 构建,所以除了读取砖块类型外,还要从关卡文件中取出 bonustype 并传给砖块构造函数。
function bricks.construct_level( level )
.....
for row_index, row in ipairs( level.bricks ) do
for col_index, bricktype in ipairs( row ) do
if bricktype ~= 0 then
.....
local new_brick_position = vector( new_brick_position_x,
new_brick_position_y )
local bonustype = level.bonuses[ row_index ][ col_index ]
local new_brick = bricks.new_brick( new_brick_position,
bricks.brick_width,
bricks.brick_height,
bricktype,
bonustype )
table.insert( bricks.current_level_bricks, new_brick )
end
end
end
end
function bricks.new_brick( position, width, height, bricktype, bonustype )
return( { position = position,
width = width or bricks.brick_width,
height = height or bricks.brick_height,
bricktype = bricktype,
quad = bricks.bricktype_to_quad( bricktype ),
bonustype = bonustype } )
endAfter the bonustypes are associated with the bricks, it is necessary to spawn the corresponding bonuses on bricks destruction. Since the brick destruction is done in the bricks.brick_hit_by_ball function, it needs an access to the bonuses table. I provide it as an argument, which results in necessity to add it to several other collisions-related functions.
砖块关联 bonustype 之后,就需要在砖块被摧毁时生成对应的奖励。砖块摧毁发生在 bricks.brick_hit_by_ball 中,所以该函数需要访问 bonuses 表。我把 bonuses 作为参数传进去,这会导致要在一些碰撞相关函数里新增该参数。
function game.update( dt )
.....
collisions.resolve_collisions( ball, platform, walls, bricks, bonuses )
.....
end
function collisions.resolve_collisions( ball, platform,
walls, bricks, bonuses )
.....
collisions.ball_bricks_collision( ball, bricks, bonuses )
.....
end
function collisions.ball_bricks_collision( ball, bricks, bonuses )
.....
if overlap then
ball.brick_rebound( shift_ball )
bricks.brick_hit_by_ball( i, brick, shift_ball, bonuses )
end
.....
endIf the brick is to be destroyed, bonuses.generate_bonus is called. It receives the bonus position and the bonustype, extracted from the brick properties. This function checks the validity of the bonustype and if it is valid, adds the corresponding bonus into the bonuses.current_level_bonuses table. Non-valid bonustypes are simply ignored.
如果砖块要被销毁,就调用 bonuses.generate_bonus。它接收奖励位置和从砖块属性中取出的 bonustype。该函数会先检查 bonustype 是否有效,有效就把对应奖励加入 bonuses.current_level_bonuses;无效则直接忽略。
function bricks.brick_hit_by_ball( i, brick, shift_ball, bonuses )
if bricks.is_simple( brick ) then
bonuses.generate_bonus(
vector( brick.position.x + brick.width / 2,
brick.position.y + brick.height / 2 ),
brick.bonustype )
table.remove( bricks.current_level_bricks, i )
simple_break_sound:play()
elseif
.....
elseif bricks.is_cracked( brick ) then
bonuses.generate_bonus(
vector( brick.position.x + brick.width / 2,
brick.position.y + brick.height / 2 ),
brick.bonustype )
table.remove( bricks.current_level_bricks, i )
armored_break_sound:play()
elseif bricks.is_heavyarmored( brick ) then
.....
end
end