Playdateでのタイルマップの使い方について記事にします。
開発環境や事前準備は以前の記事を参考にしてください。
目次
コードと実行結果
先にコード例と実行結果を載せます。
まず実行結果は以下の通りになります。
地面のブロックが画面に並べられてプレイヤーがその上を歩いているのが分かります。また動かせるブロックのギミックもあります。動かせるブロックはタイルマップではなく前回の記事で作成したBlock
オブジェクトです。
プロジェクトのフォルダ構成は以下の通りです。
. ├── LICENSE ├── README.md ├── run.sh └── source ├── actors │ ├── actor.lua │ ├── block.lua │ ├── hart.lua │ └── player.lua ├── images │ └── spritesheets │ ├── block │ │ └── idle-table-16-16.png │ ├── hart │ │ └── idle-table-16-16.png │ ├── player │ │ ├── idle-table-16-16.png │ │ └── walk-table-16-16.png │ └── tilemap-table-16-16.png ├── main.lua └── maps └── map.lua
LICENSEとREADME.mdは無視して大丈夫です。前回の記事と違い追加されたのはmaps/map.lua
です。
source/actors/actor.lua
のソースコードは以下の通りです。
---@diagnostic disable: undefined-global class('Actor').extends() function Actor:init(_stageRect, _zindex, _groupIds) self.zindex = _zindex self.stageRect = _stageRect self.groupIds = _groupIds self.currentImagetable = nil self.currentLoop = nil self.currentSprite = nil self.state = 0 self.x = 0 self.y = 0 self.floorPosition = 0 end function Actor:commonSpriteSettings() if self.currentSprite then self.currentSprite:remove() end self.currentSprite = playdate.graphics.sprite.new(self.currentLoop:image()) self.currentSprite:setCenter(0, 0) self.currentSprite:setZIndex(self.zindex) self.currentSprite:setGroups(self.groupIds) self.currentSprite:setCollidesWithGroups(self.groupIds) -- https://sdk.play.date/2.1.1/Inside%20Playdate.html#m-graphics.sprite.setCollideRect self.currentSprite:setCollideRect(0, 0, self.currentSprite:getSize()) self.currentSprite.collisionResponse = playdate.graphics.sprite.kCollisionTypeFreeze self.currentSprite:moveTo(self.x, self.y) end function Actor:move(_ignore) local cx, cy, _, count = self.currentSprite:checkCollisions(self.x, self.y) if count > 0 then if not _ignore then self.currentSprite:moveTo(cx, cy) self.x = cx self.y = cy self.floorPosition = cy -- Keep floor coordinates. end else self.currentSprite:moveTo(self.x, self.y) end end function Actor:setPosition(_cellx, _celly) local spriteWidth, spriteHeight = self.currentSprite:getSize() self.x = _cellx * spriteWidth self.y = _celly * spriteHeight self.currentSprite:moveTo(self.x, self.y) end
source/actors/player.lua
のソースコードは以下の通りです。
import "actors/actor" ---@diagnostic disable: undefined-global class('Player').extends(Actor) function Player:init(_stageRect, _zindex, _groupIds, _jumpPower, _speed) Player.super.init(self, _stageRect, _zindex, _groupIds) self.states = { idle = 1; walk = 2; } self.directions = { right = 1; left = 2; } self.direction = self.directions.right self.idleFrametime = 200 self.walkFrametime = 200 self.idlePath = "images/spritesheets/" .. self.className:lower() .. "/idle" self.walkPath = "images/spritesheets/" .. self.className:lower() .. "/walk" self.jumpping = false self.dropping = false self.jumpReimitHeight = 0 self.jumpPower = _jumpPower or 4 self.speed = _speed or 5 end function Player:turnLeft() self.direction = self.directions.left end function Player:turnRight() self.direction = self.directions.right end function Player:lowestPoint() local _, spriteHeight = self.currentSprite:getSize() self.jumpReimitHeight = (spriteHeight * 2.5) return self.stageRect.height - spriteHeight end function Player:jump() local maxHeight = self.floorPosition - self.jumpReimitHeight if not self.jumpping then self.jumpping = true end if self.jumpping then if not self.dropping then self.y = self.y - self.jumpPower else local y = self.y + self.jumpPower local _, _, _, count = self.currentSprite:checkCollisions(self.x, y) if count <= 0 then self.y = self.y + self.jumpPower else self.dropping = false self.jumpping = false end end if self.y <= maxHeight then self.dropping = true self.y = maxHeight end end end function Player:drop() self.y = self.y + self.jumpPower end function Player:isFloorPresent() local y = self.y + self.jumpPower local _, _, _, count = self.currentSprite:checkCollisions(self.x, y) if count > 0 then return true end return false end function Player:pushBlock(_blocks) local _, _, collsitions, count = self.currentSprite:checkCollisions(self.x, self.y) for i=1, count do local collision = collsitions[i] for j=1, #_blocks do local block = _blocks[j] if collision.other == block.currentSprite then local _, _, _, count = block.currentSprite:checkCollisions(self.x, self.y) if count > 0 then if self.direction == self.directions.left then block.x = block.x - 1 else block.x = block.x + 1 end block:move(true) end end end end end function Player:waitMove() self:drop() self:idle() self:move() end function Player:jumpMove() self:jump() self:move() end function Player:rightMove(_block) self.x = self.x + self.speed self:turnRight() self:walk() if _block then self:pushBlock(_block) end self:move() if not self:isFloorPresent() then self:drop() end end function Player:leftMove(_block) self.x = self.x - self.speed self:turnLeft() self:walk() if _block then self:pushBlock(_block) end self:move() if not self:isFloorPresent() then self:drop() end end function Player:rightJumpMove(_speed) self.x = self.x + self.speed self:turnRight() self:walk() self:move() self:jump() end function Player:leftJumpMove(_speed) self.x = self.x - self.speed self:turnLeft() self:walk() self:move() self:jump() end function Player:idle() if not (self.state == self.states.idle) then self.currentImagetable = playdate.graphics.imagetable.new(self.idlePath) self.currentLoop = playdate.graphics.animation.loop.new(self.idleFrametime, self.currentImagetable, true) self:commonSpriteSettings(idlePath, self.walkFrametime, true) end self.currentSprite.update = function() self.currentSprite:setImage(self.currentLoop:image()) if not self.currentLoop:isValid() then self.currentSprite:remove() end end self.currentSprite:add() self.state = self.states.idle end function Player:walk() if not (self.state == self.states.walk) then self.currentImagetable = playdate.graphics.imagetable.new(self.walkPath) self.currentLoop = playdate.graphics.animation.loop.new(self.walkFrametime, self.currentImagetable, true) self:commonSpriteSettings() end self.currentSprite.update = function() self.currentSprite:setImage(self.currentLoop:image()) if self.direction == self.directions.left then self.currentSprite:setImageFlip("flipX") end if not self.currentLoop:isValid() then self.currentSprite:remove() end end self.currentSprite:add() self.state = self.states.walk end
source/actors/hart.lua
のソースコードは以下の通りです。
import "actors/actor" ---@diagnostic disable: undefined-global class('Hart').extends(Actor) function Hart:init(_stageRect, _zindex, _groupIds) Hart.super.init(self, _stageRect, _zindex, _groupIds) self.states = { idle = 1; } self.idleFrametime = 200 self.idlePath = "images/spritesheets/" .. self.className:lower() .. "/idle" end function Hart:idle() if not (self.state == self.states.idle) then if self.currentSprite then self.currentSprite:remove() end self.currentImagetable = playdate.graphics.imagetable.new(self.idlePath) self.currentLoop = playdate.graphics.animation.loop.new(self.idleFrametime, self.currentImagetable, true) self:commonSpriteSettings() end self.currentSprite.update = function() self.currentSprite:setImage(self.currentLoop:image()) if not self.currentLoop:isValid() then self.currentSprite:remove() end end self.currentSprite:add() self.state = self.states.idle end function Hart:collideWithPlayer(_player) -- https://sdk.play.date/2.1.1/Inside%20Playdate.html#m-graphics.sprite.alphaCollision if _player.currentSprite:alphaCollision(self.currentSprite) then self.currentSprite:remove() end end
source/actors/block.lua
のソースコードは以下の通りです。
import "actors/actor" ---@diagnostic disable: undefined-global class('Block').extends(Actor) function Block:init(_stageRect, _zindex, _groupIds, _weight) Block.super.init(self, _stageRect, _zindex, _groupIds) self.states = { idle = 1; } self.idleFrametime = 200 self.idlePath = "images/spritesheets/" .. self.className:lower() .. "/idle" self.weight = _weight or 5 end function Block:dropMove() self.y = self.y + self.weight self:move() end function Block:idle() if not (self.state == self.states.idle) then if self.currentSprite then self.currentSprite:remove() end self.currentImagetable = playdate.graphics.imagetable.new(self.idlePath) self.currentLoop = playdate.graphics.animation.loop.new(self.idleFrametime, self.currentImagetable, true) self:commonSpriteSettings() end self.currentSprite.update = function() self.currentSprite:setImage(self.currentLoop:image()) if not self.currentLoop:isValid() then self.currentSprite:remove() end end self.currentSprite:add() self.state = self.states.idle end
source/maps/map.lua
のソースコードは以下の通りです。
---@diagnostic disable: undefined-global class('Map').extends() function Map:init(_mapArray, _zindex, _displayWidth, _groudIds) self.mapArray = _mapArray self.zindex = _zindex self.displayWidth = _displayWidth self.groudIds = _groudIds self.path = "images/spritesheets/tilemap" self.x = 0 self.y = 0 self.complete = false local tilemapImage = playdate.graphics.imagetable.new(self.path) self.tilemap = playdate.graphics.tilemap.new() self.tilemap:setImageTable(tilemapImage) local tileWidth, _ = self.tilemap:getTileSize() self.tilemap:setTiles(self.mapArray, (self.displayWidth / tileWidth)) self.currentSprite = playdate.graphics.sprite.new(self.tilemap) self.currentSprite:setCenter(0, 0) self.currentSprite:moveTo(self.x, self.y) self.currentSprite:setZIndex(self.zindex) self.currentSprite:add() end function Map:setCollsions(emptyIds) if not self.complete then -- https://sdk.play.date/2.2.0/Inside%20Playdate.html#f-graphics.sprite.addWallSprites local collisions = playdate.graphics.sprite.addWallSprites(self.tilemap, emptyIds) for i=1, #collisions do collisions[i].collisionResponse = playdate.graphics.sprite.kCollisionTypeFreeze collisions[i]:setGroups(self.groudIds) collisions[i]:setCollidesWithGroups(self.groudIds) end self.complete = true end end
そしてsource/main.lua
のソースコードは以下の通りです。
if not import then import = require end import "CoreLibs/graphics" import "CoreLibs/object" import "CoreLibs/sprites" import "CoreLibs/animation" import "maps/map" import "actors/player" import "actors/hart" import "actors/block" local function clearDisplay() playdate.graphics.clear() playdate.graphics.setBackgroundColor(playdate.graphics.kColorBlack) end local groups = { collision = 1, item = 2 } local mapArray = { -- 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 0 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 1 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 2 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 3 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 4 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 5 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ,1, -- 6 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 1, 1, 1, 1, 1, 1, 1, 1, -- 7 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 12, 12, 1, 1, 1, 1, 1, 1, -- 8 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 12, 1, 1, 1, 1, 1, -- 9 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 10 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 11 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 12, 12, 12, 12, 1, 12, 12, 12, 12, -- 12 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, -- 13 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, -- 14 } local mapEmptyIds = { 1 } local playerGroupIds = { groups.collision } local mapGroupIds = { groups.collision } local hartGroupIds = { groups.item } local blockGroupIds = { groups.collision } local mapIndex = -1 local playerIndex = 1 local hartIndex = 1 local blockIndex = 1 local stageRect = playdate.geometry.rect.new(0, 0, playdate.display.getWidth(), playdate.display.getHeight()) local map = Map(mapArray, mapIndex, stageRect.width, mapGroupIds) local player = Player(stageRect, playerIndex, playerGroupIds) player:idle() player.y = player:lowestPoint() player:setPosition(0, 14) local hart = Hart(stageRect, hartIndex, hartGroupIds) hart:idle() hart:setPosition(1, 6) local block_1 = Block(stageRect, blockIndex, blockGroupIds) block_1:idle() block_1:setPosition(21, 11) local block_2 = Block(stageRect, 1, blockGroupIds) block_2:idle() block_2:setPosition(18, 11) local blocks = { block_1, block_2 } function playdate.update() clearDisplay() -- https://sdk.play.date/2.1.1/Inside%20Playdate.html#f-graphics.sprite.update playdate.graphics.sprite.update() map:setCollsions(mapEmptyIds) if playdate.buttonIsPressed(playdate.kButtonRight) and playdate.buttonIsPressed(playdate.kButtonA) then player:rightJumpMove() elseif playdate.buttonIsPressed(playdate.kButtonLeft) and playdate.buttonIsPressed(playdate.kButtonA) then player:leftJumpMove() elseif playdate.buttonIsPressed(playdate.kButtonRight) then player:rightMove(blocks) elseif playdate.buttonIsPressed(playdate.kButtonLeft) then player:leftMove(blocks) elseif playdate.buttonIsPressed(playdate.kButtonA) then player:jumpMove() else player:waitMove() end block_1:dropMove() block_2:dropMove() hart:collideWithPlayer(player) end
タイルマップの解説
タイルマップの読み込みと設定を行っているのは、map.lua
のMap
クラスです。
local tilemapImage = playdate.graphics.imagetable.new(self.path)
self.tilemap = playdate.graphics.tilemap.new()
self.tilemap:setImageTable(tilemapImage)
tilemapのスプライトシートがあるパスを指定して、ImageTable
オブジェクトとして読み込みます。
そして、Tilemap
オブジェクトを新規に作成して、:setImageTable()
で読み込んだImageTable
オブジェクトを引数に指定することでタイルマップとして扱うことができるようになります。
Tilemap
オブジェクトにImageTable
オブジェクトを読み込ませた時点でPlaydateでは以下のようにタイルマップを扱うよう設定されます。
まずこれは今回の実装例で読み込んだスプライトシートです。
上図のように原点から右方向に1次元配列としてタイルに1,2,3,4…とインデックス番号が割り振られます。今回の実装例では12番の番号が振られたタイルを使いマップを作成しています。
タイルのインデックス番号はカスタマイズも可能みたいです。今回の例では使用していません。
local tileWidth, _ = self.tilemap:getTileSize()
self.tilemap:setTiles(self.mapArray, (self.displayWidth / tileWidth))
タイルマップからマップを作成している個所です。:setTiles(self.mapArray, (self.displayWidth / tileWidth))
にマップを表現している1次元配列と横一列に並べるタイルの最大幅を指定します。
この際の最大幅はピクセル数ではなくタイル数です。そのため、上記の例ではPlaydateの解像幅400pxに対して、1タイルの幅16pxを割り25タイルが指定されています。
折り返しまでの最大幅が分かれば以下の様な配列でマップを表現することが可能です。
local mapArray = { -- 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 0 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 1 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 2 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 3 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 4 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 5 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ,1, -- 6 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 1, 1, 1, 1, 1, 1, 1, 1, -- 7 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 12, 12, 1, 1, 1, 1, 1, 1, -- 8 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 12, 1, 1, 1, 1, 1, -- 9 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 10 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 11 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 12, 12, 12, 12, 1, 12, 12, 12, 12, -- 12 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, -- 13 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, -- 14 }
横25タイル、高さ15タイルの1次元配列です。コードの見た目では2次元配列の形に整形しているだけなのに注意してください。番号は表示するタイルのインデックス番号を表しています。
(コメント箇所はプレイヤーやブロック、ハートを配置する際に座標値を分かりやすくするために振っています。)
self.currentSprite = playdate.graphics.sprite.new(self.tilemap) self.currentSprite:setCenter(0, 0) self.currentSprite:moveTo(self.x, self.y) self.currentSprite:setZIndex(self.zindex) self.currentSprite:add()
Tilemap
オブジェクトもSprite
オブジェクトに変換できます。Sprite
オブジェクトに変換して:add()
メソッドで画面に表示されます。
タイルマップへ衝突検知を設定する
タイルマップもSprite
オブジェクトに変換できているため、setCollideRect()
を設定可能です。ですが1タイルずつsetCollideRect()
すると大変です。衝突検知が必要なタイルのみに衝突判定を行う矩形領域を設定できる便利なメソッドがあります。
function Map:setCollsions(emptyIds) ~ 省略 ~ -- https://sdk.play.date/2.2.0/Inside%20Playdate.html#f-graphics.sprite.addWallSprites local collisions = playdate.graphics.sprite.addWallSprites(self.tilemap, emptyIds) ~ 省略 ~ end
:addWallSprites()
メソッドです。
第2引数のemptyIds
は衝突検知が必要ないタイルのインデックス番号を指定します。配列形式なので複数指定可能です。今回の実装例は1番と12番のタイルのみでマップを構成していて、12番のみ床と壁として機能して欲しいので1番を指定しています。
function playdate.update() ~ 省略 ~ -- https://sdk.play.date/2.1.1/Inside%20Playdate.html#f-graphics.sprite.update playdate.graphics.sprite.update() map:setCollsions(mapEmptyIds) ~ 省略 ~ end
main.lua
からの抜粋です。:addWallSprites()
はsprite.update()
が行われた後に呼び出すようにしています。こうしないとタイルマップに設定される衝突範囲がずれます。map:setCollsions()
メソッドでは:addWallSprites()
を1度のみしか呼び出さないようフラグで制御するよう実装しています。参考にしてください。
衝突検知を設定したタイルの上を歩行させる
前回の記事ではmoveWithCollisions()
メソッドを使用して、Sprite
オブジェクト同士の衝突を検知してめり込まないよう実装していました。ただ、moveWithCollisions()
メソッドは位置補正も行う都合上、衝突した際の挙動を細かく制御できません。そこでタイルの上を歩行させるためにcheckCollisions()
を使用します。
actor.lua
からの抜粋です。
function Actor:move(_ignore) local cx, cy, _, count = self.currentSprite:checkCollisions(self.x, self.y) if count > 0 then if not _ignore then self.currentSprite:moveTo(cx, cy) self.x = cx self.y = cy self.floorPosition = cy -- Keep floor coordinates. end else self.currentSprite:moveTo(self.x, self.y) end end
Sprite
オブジェクトを通常移動させる仕組みはすべてここに集約させています。checkCollisions()
メソッドが返却する戻り値はmoveWithCollisions()
メソッドと一緒です。違いは戻り値を使った位置補正を勝手に行いません。
また、今回はマップの上を歩行させるために衝突検知の種別を以下のように設定しています。
function Actor:commonSpriteSettings() ~ 省略 ~ self.currentSprite.collisionResponse = playdate.graphics.sprite.kCollisionTypeFreeze self.currentSprite:moveTo(self.x, self.y) end
これは、衝突した際に返却する補正座標をどのように算出するかの種別を指定しています。
4種類ほど列挙型が設定されているので作成するゲームに合わせて設定します。今回は、すべてのオブジェクトが衝突した時点での座標があればよいのでkCollisionTypeFreeze
が設定されます。
マップのタイル上の歩行方法のロジックはシンプルで、checkCollisions()
メソッドのcount
の戻り値で1件以上衝突が確認出来たら、戻り値の座標にSprite
オブジェクトを移動させる。衝突が確認できなければ移動しようとしていた。self.x, self.y
の位置にそのまま移動させています。
まとめ
他にも、ジャンプやブロックを動かしたりしていますが記事の主題ではないので解説自体は省略致します。でも今回はリポジトリを用意したのでPlaydateのSDKをインストールされているのであればリポジトリをクローンして挙動を試してみて頂ければと思います。
Playdateでのゲーム開発で気づいたことがあれば自分の為にも随時記事にしようと思います。