SpriteKit:关于非常规网格圆角的建议?

SpriteKit: suggestions for rounding corners of unconventional grid?

目标是使非常规网格的角变圆,如下所示:

https://s-media-cache-ak0.pinimg.com/564x/50/bc/e0/50bce0cb908913ebc2cf630d635331ef.jpg

https://s-media-cache-ak0.pinimg.com/564x/7e/29/ee/7e29ee80e957ec22bbba630ccefbfaa2.jpg

不同于传统网格的四个角网格,这些网格有多个需要圆角的角。

蛮力方法是识别带有外露角的图块,然后使用不同的背景图像或通过在代码中剪切角来将这些角圆化。

有没有更简洁的方法?

网格是为 SpriteKit SKScene 中的 iOS 应用渲染的。

唯一想到的是当一个节点接触到另一个节点时,在那一刻及时评估所述节点的显示,并更改受其影响的邻居。

这真的很有趣 question.You 可以用不同的方法构建您的矩阵,但您肯定必须每次解决每个图块背景中 4 个角的变化。

假设您从这样的 GameViewController 开始(没有加载 SKS 文件并且 anchorPoint 等于零):

import UIKit
import SpriteKit
class GameViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        guard let view = self.view as! SKView? else { return }
        view.ignoresSiblingOrder = true
        view.showsFPS = true
        view.showsNodeCount = true
        let scene = GameScene(size:view.bounds.size)
        scene.scaleMode = .resizeFill
        scene.anchorPoint = CGPoint.zero
        view.presentScene(scene)
    }
}

我的想法是构建这样一个矩阵:

import SpriteKit
class GameScene: SKScene {
    private var sideTile:CGFloat = 40
    private var gridWidthTiles:Int = 5
    private var gridHeightTiles:Int = 6
    override func didMove(to view: SKView) {
        self.drawMatrix()
    }
    func drawMatrix(){
        var index = 1
        let matrixPos = CGPoint(x:50,y:150)
        for i in 0..<gridHeightTiles {
            for j in 0..<gridWidthTiles {
                let tile = getTile()
                tile.name = "tile\(index)"
                addChild(tile)
                tile.position = CGPoint(x:matrixPos.x+(sideTile*CGFloat(j)),y:matrixPos.y+(sideTile*CGFloat(i)))
                let label = SKLabelNode.init(text: "\(index)")
                label.fontSize = 12
                label.fontColor = .white
                tile.addChild(label)
                label.position = CGPoint(x:tile.frame.size.width/2,y:tile.frame.size.height/2)
                index += 1
            }
        }
    }
    func getTile()->SKShapeNode {
        let tile = SKShapeNode(rect: CGRect(x: 0, y: 0, width: sideTile, height: sideTile), cornerRadius: 10)
        tile.fillColor = .gray
        tile.strokeColor = .gray
        return tile
    }
}

输出:

现在我们可以为矩阵的每个图块构建背景。 我们可以制作相同的图块节点,但颜色不同(可能比图块颜色更清晰)并且没有角半径。如果我们将这个背景分成 4 个部分,我们有:

  • 左侧 - 底部背景图块
  • 左侧 - 顶部背景图块
  • 右 - 底部背景图块
  • 右侧 - 顶部背景图块

典型背景图块的代码:

func getBgTileCorner()->SKShapeNode {
   let bgTileCorner = SKShapeNode(rect: CGRect(x: 0, y: 0, width: sideTile/2, height: sideTile/2))
   bgTileCorner.fillColor = .lightGray
   bgTileCorner.strokeColor = .lightGray
   bgTileCorner.lineJoin = .round
   bgTileCorner.isAntialiased = false
   return bgTileCorner
}

现在有了 SKSCropNode 我们可以使用背景图块和图块只获得角:

func getCorner(at angle:String)->SKCropNode {
        let cropNode = SKCropNode()
        let tile = getTile()
        let bgTile = getBgTileCorner() 
        cropNode.addChild(bgTile)
        tile.position = CGPoint.zero
        let tileFrame = CGRect(x: 0, y: 0, width: sideTile, height: sideTile)
        switch angle {
            case "leftBottom": bgTile.position = CGPoint(x:tile.position.x,y:tile.position.y)
            case "rightBottom": bgTile.position = CGPoint(x:tile.position.x+tileFrame.size.width/2,y:tile.position.y)
            case "leftTop": bgTile.position = CGPoint(x:tile.position.x,y:tile.position.y+tileFrame.size.height/2)
            case "rightTop": bgTile.position = CGPoint(x:tile.position.x+tileFrame.size.width/2,y:tile.position.y+tileFrame.size.height/2)
            default:break
        }
        tile.fillColor = self.backgroundColor
        tile.strokeColor = self.backgroundColor
        tile.lineWidth = 0.0
        bgTile.lineWidth = 0.0
        tile.blendMode = .replace
        cropNode.position = CGPoint.zero
        cropNode.addChild(tile)
        cropNode.maskNode = bgTile
        return cropNode
    }

典型角的输出

let corner = getCorner(at: "leftBottom")
addChild(corner)
corner.position = CGPoint(x:50,y:50)

现在我们可以用每个方块的角重建 drawMatrix 函数:

func drawMatrix(){
        var index = 1
        let matrixPos = CGPoint(x:50,y:150)
        for i in 0..<gridHeightTiles {
            for j in 0..<gridWidthTiles {
                let tile = getTile()
                tile.name = "tile\(index)"
                let bgTileLB = getCorner(at:"leftBottom")
                let bgTileRB = getCorner(at:"rightBottom")
                let bgTileLT = getCorner(at:"leftTop")
                let bgTileRT = getCorner(at:"rightTop")
                bgTileLB.name = "bgTileLB\(index)"
                bgTileRB.name = "bgTileRB\(index)"
                bgTileLT.name = "bgTileLT\(index)"
                bgTileRT.name = "bgTileRT\(index)"
                addChild(bgTileLB)
                addChild(bgTileRB)
                addChild(bgTileLT)
                addChild(bgTileRT)
                addChild(tile)
                tile.position = CGPoint(x:matrixPos.x+(sideTile*CGFloat(j)),y:matrixPos.y+(sideTile*CGFloat(i)))
                let label = SKLabelNode.init(text: "\(index)")
                label.fontSize = 12
                label.fontColor = .white
                tile.addChild(label)
                label.position = CGPoint(x:tile.frame.size.width/2,y:tile.frame.size.height/2)
                bgTileLB.position = CGPoint(x:tile.position.x,y:tile.position.y)
                bgTileRB.position = CGPoint(x:tile.position.x,y:tile.position.y)
                bgTileLT.position = CGPoint(x:tile.position.x,y:tile.position.y)
                bgTileRT.position = CGPoint(x:tile.position.x,y:tile.position.y)
                index += 1
            }
        }
}

输出:

非常类似于您的屏幕截图(这是两个磁贴示例:)

现在,当您想要移除瓷砖时,您可以决定要移除或保留哪个角,因为对于每个瓷砖,您还有相对的 4 个角:

输出:

好的,网格创建过程与此无关。您只需要某种方法来区分网格中的空白点和填充点。在我的示例中,我有一个类型为 .blank 或 .regular 的 Tile 对象。您需要拥有所有 15 张图像(您可以将样式更改为任何您喜欢的样式,但它们必须按相同的顺序排列并且必须以 1..15 为前缀)。它使用位计算来确定将哪个图像用作背景,并将背景图像偏移 x 和 y 的 1/2 平铺大小。除此之外,它是非常自我解释的。这些背景图片是我在开发这个时创建的测试图片,所以请随意使用它们。

struct GridPosition {

    var col: Int = 0
    var row: Int = 0
}

class GameScene: SKScene {

    private var backgroundLayer = SKNode()
    private var tileLayer = SKNode()
    private var gridSize: CGSize = CGSize.zero
    private var gridRows: Int = 0
    private var gridCols: Int = 0
    private var gridBlanks = [Int]()
    private var tiles = [[Tile]]()
    var tileSize: CGFloat = 150

    override func didMove(to view: SKView) {

        backgroundLayer.zPosition = 1
        addChild(backgroundLayer)

        tileLayer.zPosition = 2
        addChild(tileLayer)

        gridRows = 8
        gridCols = 11
        gridBlanks = [0,1,3,4,5,6,7,9,10,11,12,13,15,16,17,19,20,21,22,23,31,32,33,36,40,43,56,64,67,69,70,71,72,73,75,77,78,79,82,85,86,87]

        createGrid()

        createBackgroundTiles()
    }

    func createGrid() {

        for row in 0 ..< gridRows {

            var rowContent = [Tile]()

            for col in 0 ..< gridCols {

                let currentTileLocation: Int = row * gridCols + col
                var tile: Tile

                if gridBlanks.contains(currentTileLocation) {
                    tile = Tile(row: row, col: col, type: .blank, tileSize: tileSize)
                }
                else {
                    tile = Tile(row: row, col: col, type: .regular, tileSize: tileSize)
                }

                tile.position = positionInGrid(column: col, row: row)
                tile.zPosition = CGFloat(100 + gridRows - row)
                tileLayer.addChild(tile)
                rowContent.append(tile)
            }

            tiles.append(rowContent)
        }
    }

    func tileByGridPosition(_ gridPos: GridPosition) -> Tile {
        return (tiles[Int(gridPos.row)][Int(gridPos.col)])
    }

    func positionInGrid(column: Int, row: Int) -> CGPoint {

        let startX = 0 - CGFloat(gridCols / 2) * tileSize
        let startY = 0 - CGFloat(gridRows / 2) * tileSize + tileSize / 2

        return CGPoint(

            x: startX + CGFloat(column) * tileSize,
            y: startY + CGFloat(row) * tileSize)
    }

    func createBackgroundTiles() {

        for row in 0...gridRows {

            for col in 0...gridCols {

                let topLeft = (col > 0) && (row < gridRows) && tileByGridPosition(GridPosition(col: col - 1, row: row)).type == .regular
                let bottomLeft = (col > 0) && (row > 0) && tileByGridPosition(GridPosition(col: col - 1, row: row - 1)).type == .regular
                let topRight = (col < gridCols) && (row < gridRows) && tileByGridPosition(GridPosition(col: col, row: row)).type == .regular
                let bottomRight = (col < gridCols) && (row > 0) && tileByGridPosition(GridPosition(col: col, row: row - 1)).type == .regular

                // The tiles are named from 0 to 15, according to the bitmask that is made by combining these four values.
                let value = Int(NSNumber(value: topLeft)) | Int(NSNumber(value: topRight)) << 1 | Int(NSNumber(value: bottomLeft)) << 2 | Int(NSNumber(value: bottomRight)) << 3

                // Values 0 (no tiles)
                if value != 0 {

                    var gridPosition = positionInGrid(column: col, row: row)
                    gridPosition.x -= tileSize / 2
                    gridPosition.y -= tileSize / 2

                    let backgroundNode = SKSpriteNode(imageNamed: ("background_tile_\(value)"))
                    backgroundNode.size = CGSize(width: tileSize, height: tileSize)
                    backgroundNode.alpha = 0.8
                    backgroundNode.position = gridPosition
                    backgroundNode.zPosition = 1
                    backgroundLayer.addChild(backgroundNode)
                }
            }
        }
    }
}

class Tile: SKSpriteNode {

    private var row = 0
    private var col = 0
    var type: TileType = .blank

    init(row: Int, col: Int, type: TileType, tileSize: CGFloat) {

        super.init(texture: nil ,color: .clear, size:CGSize(width: tileSize, height: tileSize))

        self.type = type
        size = self.size

        let square = SKSpriteNode(color: type.color, size: size)
        square.zPosition = 1
        addChild(square)
    }
}

我们所做的是布置瓷砖,然后调用此函数来舍入暴露瓷砖的节点。

// Rounds corners of exposed tiles. UIKit inverts coordinates so top is bottom and vice-versa.
fileprivate func roundTileCorners() {
    // Get all tiles
    var tiles = [TileClass]()
    tileLayer.enumerateChildNodes(withName: ".//*") { node, stop in
        if node is TileClass {
            tiles.append(node as! TileClass)
        }
    }

    // Round corners for each exposed tile
    for t in tiles {
        // Convert tile's position to root coordinates
        let convertedPos = convert(t.position, from: t.parent!)

        // Set neighbor positions
        var leftNeighborPos = convertedPos
        leftNeighborPos.x -= tileWidth
        var rightNeighborPos = convertedPos
        rightNeighborPos.x += tileWidth
        var topNeighborPos = convertedPos
        topNeighborPos.y += tileHeight
        var bottomNeighborPos = convertedPos
        bottomNeighborPos.y -= tileHeight

        // Set default value for rounding
        var cornersToRound : UIRectCorner?

        // No neighbor below & to left? Round bottom left.
        if !isTileAtPoint(point: bottomNeighborPos) && !isTileAtPoint(point: leftNeighborPos) {
            cornersToRound = cornersToRound?.union(.topLeft) ?? .topLeft
        }

        // No neighbor below & to right? Round bottom right.
        if !isTileAtPoint(point: bottomNeighborPos) && !isTileAtPoint(point: rightNeighborPos) {
            cornersToRound = cornersToRound?.union(.topRight) ?? .topRight
        }

        // No neightbor above & to left? Round top left.
        if !isTileAtPoint(point: topNeighborPos) && !isTileAtPoint(point: leftNeighborPos) {
            cornersToRound = cornersToRound?.union(.bottomLeft) ?? .bottomLeft
        }

        // No neighbor above & to right? Round top right.
        if !isTileAtPoint(point: topNeighborPos) && !isTileAtPoint(point: rightNeighborPos) {
            cornersToRound = cornersToRound?.union(.bottomRight) ?? .bottomRight
        }

        // Any corners to round?
        if cornersToRound != nil {
            t.roundCorners(cornersToRound: cornersToRound!)
        }
    }
}

// Returns true if a tile exists at <point>. Assumes <point> is in root node's coordinates.
fileprivate func isTileAtPoint(point: CGPoint) -> Bool {
    return nodes(at: point).contains(where: {[=10=] is BoardTileNode })
}