SpriteKit: suggestions for rounding corners of a non-traditional grid?

The goal is to round the corners of an unconventional grid like the following:

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

Instead of a grid with four corners like a regular grid, these grids have multiple corners that need to be rounded.

A brute force approach would be to identify tiles with corners open around those corners, either with a different background or by cutting off the corners in the code.

Is there a cleaner approach?

The grid is displayed for an iOS app in SpriteKit SKScene.

+3


source to share


4 answers


What we did was lay out the tiles, then call this function to round off the nodes of the open tiles.



// 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 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 coordinates.
fileprivate func isTileAtPoint(point: CGPoint) -> Bool {
    return nodes(at: point).contains(where: {$0 is BoardTileNode })
}

      

0


source


This is a really interesting question. You can create your matrix with different approaches, but for sure you should allow changes in 4 corners of the background for each fragment every time.

Suppose you start with GameViewController

like this (no download files SKS

and anchorPoint

zero):

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)
    }
}

      

My idea is to build a matrix like this:

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
    }
}

      

Output

enter image description here

We can now build a background for each tile in our matrix. We can make the same tile node, but with a different color (possibly more transparent than the tile color) and no corner radius. If we divide this background into 4 parts, we have:

  • bottom left tile background
  • left top background tile
  • on the right - the bottom tile of the background
  • right top background tile

Code for a typical background tile:

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
}

      

Now with SKSCropNode

we can only get the angle using the background and tile:

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
    }

      



Conclusion for a typical angle:

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

      

enter image description here

Now we can rearrange the function drawMatrix

with corners for each tile:

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
            }
        }
}

      

Output

enter image description here

Very similar to your screenshots (these are two examples of shingles :)

enter image description here

enter image description here

Now, when you want to remove a tile, you can decide which corner you want to remove or keep, because for each tile, you also have relative 4 corners:

Output

enter image description here

+4


source


Okay, the meshing process doesn't really apply to this. You just need to differentiate between an empty spot in the grid and a filled spot in some way. In my example, I have a Tile object of type .blank or .regular. You need to have all 15 images (you can change the style to whatever you want, although they must be in the same order and they must be prefixed with 1..15). It uses bit computation to determine which image to use as the background and offsets the background image by 1/2 the tile size for x and y. Also, it's pretty self-explanatory. These background images were my tester images that I created during development, so feel free to use them.

Without background  with background

1 2 3 4 five 6 7 8 nine ten eleven 12 13 fourteen 15

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)
    }
}

      

+1


source


The only thing that comes to mind is when one node touches another node, at that point in time evaluates the display of the specified node, and also changes the neighbors it affects.

0


source







All Articles