UIKit 和 SpriteKit 坐标系之间的转换

Convert between UIKit and SpriteKit coordinate systems

我是iOS编程新手,对SpriteKit几乎没有任何经验,所以如果这个问题很荒谬,请原谅我。

我一直在尝试用二维数组制作一个基本网格,我更愿意从左上角开始使用它 0, 0

在研究了 UIKit 和 SpriteKit 之间坐标系的差异后,我遇到了 this answer about Converting Between View and Scene Coordinates 但它似乎并没有像我想象的那样改变 y 值。我猜我没有正确使用它,或者这不是它的本意,我不知道。

当我尝试这个时:

let convertedCoordinates = convert(cellCoordinates, to: grid)
print(cellCoordinates.y, convertedCoordinates.y)

好像对y值没有影响

我发现当我在 let cellCoordinates = CGPoint(x: cx, y: cy) 行中更改为“y: -cy”时 然后它似乎确实按照我希望的方式工作,但我不知道这是否是唯一的解决方案,或者这样做是否会在更复杂的情况下按预期工作。

这是我正在使用的代码:

import SpriteKit
import GameplayKit

class GameScene: SKScene {

   override func didMove(to view: SKView) {
      var background: SKShapeNode!
      background = SKShapeNode(rectOf: CGSize(width: frame.size.width, height: frame.size.height))
      background.fillColor = SKColor.lightGray
      self.addChild(background)

      let margin = CGFloat(50)
      let width = frame.size.width - margin
      let height = frame.size.height - margin

      let centerX = frame.midX - width / 2
      let centerY = frame.midY - height / 2

      var grid: SKShapeNode!
      grid = SKShapeNode(rectOf: CGSize(width: width, height: height))
      grid.strokeColor = SKColor.clear
      self.addChild(grid)

      let numRows = 2
      let numCols = 3

      let cellWidth = width / CGFloat(numCols)

      for r in 0..<numRows {
         for c in 0..<numCols {

            let cx = centerX + (cellWidth / 2) + (CGFloat(c) * cellWidth)
            let cy = centerY + (cellWidth / 2) + (CGFloat(r) * cellWidth)

            //***
            let cellCoordinates = CGPoint(x: cx, y: cy)
            //***

            let cellNode = SKShapeNode(rectOf: CGSize(width: cellWidth, height: cellWidth))

            let convertedCoordinates = convert(cellCoordinates, to: grid)
            print(cellCoordinates.y, convertedCoordinates.y)

            cellNode.strokeColor = SKColor.black
            cellNode.lineWidth = 5
            cellNode.fillColor = SKColor.darkGray
            cellNode.position = convertedCoordinates

            let textNode = SKLabelNode(text: String("\(r),\(c)"))
            textNode.fontName = "Menlo"
            textNode.fontSize = 60
            textNode.verticalAlignmentMode = .center
            textNode.position = convertedCoordinates

            grid.addChild(cellNode)
            grid.addChild(textNode)
         }
      }
   }
}

与其说是实施答案,不如说这是哲学答案。至于以某种方式翻转 SpriteKit 的坐标系,嗯,你将不断地与它作斗争。最好接受系统的现状。

虽然你的问题的本质更多的是模型和视图的分离。当你说

I would prefer to work with it from top-left being 0, 0

您的意思是,您在心理上将游戏视为左上角为 0,0 的单元格网格。那很好很自然。那是你的游戏模型。但是你在代码中写了什么?

let cx = centerX + (cellWidth / 2) + (CGFloat(c) * cellWidth)
let cy = centerY + (cellWidth / 2) + (CGFloat(r) * cellWidth)
let cellCoordinates = CGPoint(x: cx, y: cy)
let convertedCoordinates = convert(cellCoordinates, to: grid)

这是你努力摆脱的观点。你有一个抽象模型网格,你用 r,c 索引,左上角有 0,0,其坐标以单位步长向下和向右增加。然后是模型的视图,这可能取决于屏幕分辨率、纵横比、设备方向等等。如果您将两者在思想上分开,您通常会发现可以将两个系统之间的转换隔离到一个小接口。在那些地方,您可能需要执行一些操作,例如缩放轴或翻转其中一个轴,或者沿一个方向拉伸事物以匹配纵横比。

在这种情况下,如果您从左上角首选的 0,0 开始您的心智模型并思考游戏的运作方式,通常会以单元格为单位。好的,这表明二维数组或数组数组可能是自然的。也许这些单元格最终会成为您游戏中的 class。他们可能会有一个存储 SpriteKit 节点的节点 属性。你可能会得到这样的结果:

struct boardPosition {
  let row: Int
  let col: Int
}

class Cell {
  let pos: boardPosition
  let node: SKNode

  init(pos: boardPosition, in board: Board) {
    self.pos = pos
    node = SKShapeNode(...)
    board.node.addChild(node)
  }
}

class Board {
  let cells: [[Cell]]
  let node: SKNode

  init(numRows: Int, numColumns: Int) {
    ...
  }

  func movePiece(from: boardPosition, to: boardPosition) {
    let piece = cell[from.row][from.col].removePiece()
    cell[to.row][to.col].addPiece(piece)
  }
}

游戏的运作将取决于您的心智模型。单元格的 SKNode 节点的 y 坐标恰好随着行索引的增加而减小的事实将被完全掩盖。

将所有适用的节点和场景的锚点设置为0,1,使其挂载到左上角并设置你的世界节点(如果你没有,我建议添加它,它是一个基本的SKNode你用来放置所有游戏节点的,允许你使用一个单独的节点来处理不适用于游戏世界的东西,比如平视显示器和叠加层)yScale 到 -1 使 y 向下而不是向上增加。

编辑:

在处理SKShapeNodes时,除非你有一个模糊的形状,否则你不必担心图像被反转。为晦涩的形状设计CGPath时,只需翻转即可。

shape.path = shape.path!.copy(using:CGAffineTransform(scaleX:1,y:-1))

更大的问题是SKShapeNode没有锚点。相反,您需要移动整个 CGPath

为此,添加以下行:

shape.path = shape.path!.copy(using:CGAffineTransform(translationX:shape.frame.width/2,y:shape.frame.height/2))

如果以后处理SKSprite节点.... 这将导致您的资产上下颠倒,因此您需要做的就是在导入之前翻转您的资产,使用辅助节点翻转 y 轴,或为所有节点分配 -1 的 yScale。在垂直导入之前翻转所有资产是最便宜的方法,我相信你也可以在 xcassets 中翻转它,但我需要在我再次使用 MacOS 时验证这一点。