我正在尝试在场景周围移动SCNNode
,约束为GKGridGraph
。按照PacMan的想法思考但是用3D。
我有ControlComponent
来处理我的SCNNode的移动。逻辑应该是这样的......
queuedDirection
属性。isMoving
标志为false,请调用move()方法使用GKGridGraph评估下一步行动
3.1如果实体可以使用direction
属性移动到GKGridGraphNode,nextNode = nodeInDirection(direction)
3.2如果实体可以使用queuedDirection
属性nextNode = nodeInDirection(queuedDirection)
3.3如果实体无法移动到具有任一方向的节点,请将isMoving
标志设置为false并返回。
moveTo
操作runBlock
方法move()
moveTo
和runBlock
操作作为序列应用于SCNNode 我已经粘贴在下面的全班中。但我会解释我先遇到的问题。上述逻辑有效,但只是间歇性的。有时动画停止工作几乎immediatley,有时它运行长达一分钟。但在某些时候,由于某些原因,它只是停止工作 - setDirection()
将触发,move()
将触发,SCNNode将在指定方向上移动一次,然后move()
方法停止被召唤。
我不是100%确信我目前的方法是正确的,所以我很高兴听到是否有更惯用的SceneKit / GameplayKit方法来做到这一点。
这是完整的课程,但我认为重要的一点是setDirection()
和move()
方法。
import GameplayKit
import SceneKit
enum BRDirection {
case Up, Down, Left, Right, None
}
class ControlComponent: GKComponent {
var level: BRLevel!
var direction: BRDirection = .None
var queuedDirection: BRDirection?
var isMoving: Bool = false
var speed: NSTimeInterval = 0.5
//----------------------------------------------------------------------------------------
init(level: BRLevel) {
self.level = level
super.init()
}
//----------------------------------------------------------------------------------------
func setDirection( nextDirection: BRDirection) {
self.queuedDirection = nextDirection;
if !self.isMoving {
self.move()
}
}
//----------------------------------------------------------------------------------------
func move() {
let spriteNode: SCNNode = (self.entity?.componentForClass(NodeComponent.self)!.node)!
var nextNode = nodeInDirection( direction )
if let _ = self.queuedDirection {
let attemptedNextNode = nodeInDirection(self.queuedDirection! )
if let _ = attemptedNextNode {
nextNode = attemptedNextNode
self.direction = self.queuedDirection!
self.queuedDirection = nil
}
}
// Bail if we don't have a valid next node
guard let _ = nextNode else {
self.direction = .None
self.queuedDirection = nil
self.isMoving = false
return
}
// Set flag
self.isMoving = true;
// convert graphNode coordinates to Scene coordinates
let xPos: Float = Float(nextNode!.gridPosition.x) + 0.5
let zPos: Float = Float(nextNode!.gridPosition.y) + 0.5
let nextPosition: SCNVector3 = SCNVector3Make(xPos, 0, zPos)
// Configure actions
let moveTo = SCNAction.moveTo(nextPosition, duration: speed)
let repeatAction = SCNAction.runBlock( { _ in self.move() } )
let sequence = SCNAction.sequence([ moveTo, repeatAction ])
spriteNode.runAction( sequence )
}
//----------------------------------------------------------------------------------------
func getCurrentGridGraphNode() -> GKGridGraphNode {
// Acces the node in the scene and gett he grid positions
let spriteNode: SCNNode = (self.entity?.componentForClass(NodeComponent.self)!.node)!
// Account for visual offset
let currentGridPosition: vector_int2 = vector_int2(
Int32( floor(spriteNode.position.x) ),
Int32( floor(spriteNode.position.z) )
)
// return unwrapped node
return level.gridGraph.nodeAtGridPosition(currentGridPosition)!
}
//----------------------------------------------------------------------------------------
func nodeInDirection( nextDirection:BRDirection? ) -> GKGridGraphNode? {
guard let _ = nextDirection else { return nil }
let currentGridGraphNode = self.getCurrentGridGraphNode()
return self.nodeInDirection(nextDirection!, fromNode: currentGridGraphNode)
}
//----------------------------------------------------------------------------------------
func nodeInDirection( nextDirection:BRDirection?, fromNode node:GKGridGraphNode ) -> GKGridGraphNode? {
guard let _ = nextDirection else { return nil }
var nextPosition: vector_int2?
switch (nextDirection!) {
case .Left:
nextPosition = vector_int2(node.gridPosition.x + 1, node.gridPosition.y)
break
case .Right:
nextPosition = vector_int2(node.gridPosition.x - 1, node.gridPosition.y)
break
case .Down:
nextPosition = vector_int2(node.gridPosition.x, node.gridPosition.y - 1)
break
case .Up:
nextPosition = vector_int2(node.gridPosition.x, node.gridPosition.y + 1)
break;
case .None:
return nil
}
return level.gridGraph.nodeAtGridPosition(nextPosition!)
}
}
答案 0 :(得分:4)
我必须回答我自己的问题。首先,这是一个糟糕的问题,因为我试图做错事。我犯的两个主要错误是
updateWithDeltaTime
方法。 这就是使用GameplayKit的实体组件结构来构建代码和行为的方式。我试着告诉所有价格最后如何融合在一起。
该组件负责管理代表我的游戏角色的实际SCNNode。我已经移动了代码,用于将字符设置为ControlComponent
以及此组件的动画。
class NodeComponent: GKComponent {
let node: SCNNode
let animationSpeed:NSTimeInterval = 0.25
var nextGridPosition: vector_int2 {
didSet {
makeNextMove(nextGridPosition, oldPosition: oldValue)
}
}
init(node:SCNNode, startPosition: vector_int2){
self.node = node
self.nextGridPosition = startPosition
}
func makeNextMove(newPosition: vector_int2, oldPosition: vector_int2) {
if ( newPosition.x != oldPosition.x || newPosition.y != oldPosition.y ){
let xPos: Float = Float(newPosition.x)
let zPos: Float = Float(newPosition.y)
let nextPosition: SCNVector3 = SCNVector3Make(xPos, 0, zPos)
let moveTo = SCNAction.moveTo(nextPosition, duration: self.animationSpeed)
let updateEntity = SCNAction.runBlock( { _ in
(self.entity as! PlayerEntity).gridPosition = newPosition
})
self.node.runAction(SCNAction.sequence([moveTo, updateEntity]), forKey: "move")
}
}
}
请注意,每次设置组件gridPosition
属性时,都会调用makeNextMove
方法。
我最初的例子是试图做太多。现在,该组件的唯一责任是评估其下一个gridPosition
实体的NodeComponent
。请注意,由于updateWithDeltaTime
,它将在调用该方法时评估下一步移动。
class ControlComponent: GKComponent {
var level: BRLevel!
var direction: BRDirection = .None
var queuedDirection: BRDirection?
init(level: BRLevel) {
self.level = level
super.init()
}
override func updateWithDeltaTime(seconds: NSTimeInterval) {
self.evaluateNextPosition()
}
func setDirection( nextDirection: BRDirection) {
self.queuedDirection = nextDirection
}
func evaluateNextPosition() {
var nextNode = self.nodeInDirection(self.direction)
if let _ = self.queuedDirection {
let nextPosition = self.entity?.componentForClass(NodeComponent.self)?.nextGridPosition
let targetPosition = (self.entity! as! PlayerEntity).gridPosition
let attemptedNextNode = self.nodeInDirection(self.queuedDirection)
if (nextPosition!.x == targetPosition.x && nextPosition!.y == targetPosition.y){
if let _ = attemptedNextNode {
nextNode = attemptedNextNode
self.direction = self.queuedDirection!
self.queuedDirection = nil
}
}
}
// Bail if we don't have a valid next node
guard let _ = nextNode else {
self.direction = .None
return
}
self.entity!.componentForClass(NodeComponent.self)?.nextGridPosition = nextNode!.gridPosition
}
func getCurrentGridGraphNode() -> GKGridGraphNode {
// Access grid position
let currentGridPosition = (self.entity as! PlayerEntity).gridPosition
// return unwrapped node
return level.gridGraph.nodeAtGridPosition(currentGridPosition)!
}
func nodeInDirection( nextDirection:BRDirection? ) -> GKGridGraphNode? {
guard let _ = nextDirection else { return nil }
let currentGridGraphNode = self.getCurrentGridGraphNode()
return self.nodeInDirection(nextDirection!, fromNode: currentGridGraphNode)
}
func nodeInDirection( nextDirection:BRDirection?, fromNode node:GKGridGraphNode? ) -> GKGridGraphNode? {
guard let _ = nextDirection else { return nil }
guard let _ = node else { return nil }
var nextPosition: vector_int2?
switch (nextDirection!) {
case .Left:
nextPosition = vector_int2(node!.gridPosition.x + 1, node!.gridPosition.y)
break
case .Right:
nextPosition = vector_int2(node!.gridPosition.x - 1, node!.gridPosition.y)
break
case .Down:
nextPosition = vector_int2(node!.gridPosition.x, node!.gridPosition.y - 1)
break
case .Up:
nextPosition = vector_int2(node!.gridPosition.x, node!.gridPosition.y + 1)
break;
case .None:
return nil
}
return level.gridGraph.nodeAtGridPosition(nextPosition!)
}
}
这里的一切都在一起。课堂上有很多内容,所以我只发布相关的内容。
class GameViewController: UIViewController, SCNSceneRendererDelegate {
var entityManager: BREntityManager?
var previousUpdateTime: NSTimeInterval?;
var playerEntity: GKEntity?
override func viewDidLoad() {
super.viewDidLoad()
// create a new scene
let scene = SCNScene(named: "art.scnassets/game.scn")!
// retrieve the SCNView
let scnView = self.view as! SCNView
// set the scene to the view
scnView.scene = scene
// show statistics such as fps and timing information
scnView.showsStatistics = true
scnView.delegate = self
entityManager = BREntityManager(level: level!)
createPlayer()
configureSwipeGestures()
scnView.playing = true
}
func createPlayer() {
guard let playerNode = level!.scene.rootNode.childNodeWithName("player", recursively: true) else {
fatalError("No player node in scene")
}
// convert scene coords to grid coords
let scenePos = playerNode.position;
let startingGridPosition = vector_int2(
Int32( floor(scenePos.x) ),
Int32( floor(scenePos.z) )
)
self.playerEntity = PlayerEntity(gridPos: startingGridPosition)
let nodeComp = NodeComponent(node: playerNode, startPosition: startingGridPosition)
let controlComp = ControlComponent(level: level!)
playerEntity!.addComponent(nodeComp)
playerEntity!.addComponent(controlComp)
entityManager!.add(playerEntity!)
}
func configureSwipeGestures() {
let directions: [UISwipeGestureRecognizerDirection] = [.Right, .Left, .Up, .Down]
for direction in directions {
let gesture = UISwipeGestureRecognizer(target: self, action: Selector("handleSwipe:"))
gesture.direction = direction
self.view.addGestureRecognizer(gesture)
}
}
func handleSwipe( gesture: UISwipeGestureRecognizer ) {
let controlComp = playerEntity!.componentForClass(ControlComponent.self)!
switch gesture.direction {
case UISwipeGestureRecognizerDirection.Up:
controlComp.setDirection(BRDirection.Up)
break
case UISwipeGestureRecognizerDirection.Down:
controlComp.setDirection(BRDirection.Down)
break
case UISwipeGestureRecognizerDirection.Left:
controlComp.setDirection(BRDirection.Left)
break
case UISwipeGestureRecognizerDirection.Right:
controlComp.setDirection(BRDirection.Right)
break
default:
break
}
}
// MARK: SCNSceneRendererDelegate
func renderer(renderer: SCNSceneRenderer, updateAtTime time: NSTimeInterval) {
let delta: NSTimeInterval
if let _ = self.previousUpdateTime {
delta = time - self.previousUpdateTime!
}else{
delta = 0.0
}
self.previousUpdateTime = time
self.entityManager!.update(withDelaTime: delta)
}
}
我在此Ray Wenderlich tutorial之后提到了这个提示。从本质上讲,它是一个保持所有实体和组件的桶,以简化管理和更新它们的工作。我非常推荐给该教程一个更好的理解。
class BREntityManager {
var entities = Set<GKEntity>()
var toRemove = Set<GKEntity>()
let level: BRLevel
//----------------------------------------------------------------------------------
lazy var componentSystems: [GKComponentSystem] = {
return [
GKComponentSystem(componentClass: ControlComponent.self),
GKComponentSystem(componentClass: NodeComponent.self)
]
}()
//----------------------------------------------------------------------------------
init(level:BRLevel) {
self.level = level
}
//----------------------------------------------------------------------------------
func add(entity: GKEntity){
if let node:SCNNode = entity.componentForClass(NodeComponent.self)!.node {
if !level.scene.rootNode.childNodes.contains(node){
level.scene.rootNode.addChildNode(node)
}
}
entities.insert(entity)
for componentSystem in componentSystems {
componentSystem.addComponentWithEntity(entity)
}
}
//----------------------------------------------------------------------------------
func remove(entity: GKEntity) {
if let _node = entity.componentForClass(NodeComponent.self)?.node {
_node.removeFromParentNode()
}
entities.remove(entity)
toRemove.insert(entity)
}
//----------------------------------------------------------------------------------
func update(withDelaTime deltaTime: NSTimeInterval) {
// update components
for componentSystem in componentSystems {
componentSystem.updateWithDeltaTime(deltaTime)
}
// remove components
for curRemove in toRemove {
for componentSystem in componentSystems {
componentSystem.removeComponentWithEntity(curRemove)
}
}
toRemove.removeAll()
}
}
可以随时调用ControlComponent.setDirection()
方法。
如果实体或组件实现updateWithDeltaTime
方法,则应该每帧调用它。我花了一些时间来弄清楚如何使用SceneKit来实现这一点,因为大多数GameplayKit示例都是为SpriteKit设置的,并且SKScene有一个非常方便的updatet方法。
对于SceneKit,我们必须为SCNSceneRendererDelegate
设置GameVierwController SCNView
。然后,使用rendererUpdateAtTime
方法,我们可以在实体管理器上调用updateAtDeltaTime
,它处理每帧对所有实体和组件调用相同的方法。
注意您必须手动将playing
属性设置为true才能生效。
现在我们转到实际动画。 ControlComponent
使用NodeComponents
属性评估每个帧direction
下一个网格位置应该是什么(您可以忽略queuedDirection
属性,它是一个实现细节)。
这意味着每帧都会调用NodeComponents.makeNextMove()
方法。只要newPosition
和oldPosition
不相同,就会应用动画。每当动画结束时,节点的gridPosition
都会更新。
现在,为什么我的动画我的SCNNode的初始方法不起作用,我不知道。但至少它迫使我更好地理解如何使用GameplayKit的实体/组件设计。