How to detect SCNPhysics Intersection between two GKEntities(SCNNodes) in a GKComponent for in SceneKit - swift

I am attempting to detect the intersection of two scnnodes in SceneKit. They are registered as GKEntities. I want to associate a component to both entities that will detect the intersection of their physics bodies using SCNPhysicsContactDelegate, but I don't want to do this in the view controller as I know it is not best practice and it would make it difficult to register that as a component. Any help would be appreciated.
Thanks for the assistance,
Montreaux
import Foundation
import SpriteKit
import GameplayKit
import SceneKit
class MeleeComponent: GKComponent, SCNPhysicsContactDelegate {
let damage: CGFloat
let destroySelf: Bool
let damageRate: CGFloat
var lastDamageTime: TimeInterval
let aoe: Bool
// let sound: SKAction
let entityManager: EntityManager
init(damage: CGFloat, destroySelf: Bool, damageRate: CGFloat, aoe: Bool, entityManager: EntityManager) {
self.damage = damage
self.destroySelf = destroySelf
self.damageRate = damageRate
self.aoe = aoe
// self.sound = sound
self.lastDamageTime = 0
self.entityManager = entityManager
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func update(deltaTime seconds: TimeInterval) {
super.update(deltaTime: seconds)
// Get required components
guard let teamComponent = entity?.component(ofType: TeamComponent.self),
let spriteComponent = entity?.component(ofType: SpriteComponent.self) else {
return
}
// Loop through enemy entities
var aoeDamageCaused = false
let enemyEntities = entityManager.entitiesForTeam(teamComponent.team.oppositeTeam())
for enemyEntity in enemyEntities {
// Get required components
guard let enemySpriteComponent = enemyEntity.component(ofType: SpriteComponent.self),
let enemyHealthComponent = enemyEntity.component(ofType: HealthComponent.self) else {
continue
}
// Check for intersection
if (spriteComponent.node.frame.intersects(enemySpriteComponent.node.frame)) {
// Check damage rate
if (CGFloat(CACurrentMediaTime() - lastDamageTime) > damageRate) {
// Cause damage
// spriteComponent.node.parent?.run(sound)
if (aoe) {
aoeDamageCaused = true
} else {
lastDamageTime = CACurrentMediaTime()
}
// Subtract health
enemyHealthComponent.takeDamage(damage)
// Destroy self
if destroySelf {
entityManager.remove(entity!)
}
}
}
}
if (aoeDamageCaused) {
lastDamageTime = CACurrentMediaTime()
}
}
}

In SCNPhysicsWorld, There are a few APIs can help you.
such as
func contactTestBetween(_ bodyA: SCNPhysicsBody, _ bodyB: SCNPhysicsBody, options: [SCNPhysicsWorld.TestOption : Any]? = nil) -> [SCNPhysicsContact]
It's similar to the code in your example:
if (spriteComponent.node.frame.intersects(enemySpriteComponent.node.frame))
But it's in SceneKit and will give you more information about the contact.

Related

VIO error callback - barcode detected 3d - Swift

I want to show 3d modeling when reading Qr code and when it matches. But after reading the Qr code, the camera cannot see the surface, and when I look at the output, I get the error "VIO error callback: 161457.637109, 1, Frame processing rate has fallen below pre-set threshold". The camera works very slowly, I think there is a situation with this. Despite reading the barcode, 3d modeling does not turn on after reading the barcode.
enum FunctionMode {
case none
case placeObject(String)
case measure
}
class ARKitTestViewController: UIViewController {
#IBOutlet var sceneView: ARSCNView!
#IBOutlet weak var crosshair: UIView!
#IBOutlet weak var messageLabel: UILabel!
#IBOutlet weak var trackingInfo: UILabel!
var currentMode: FunctionMode = .none
var objects: [SCNNode] = []
// Current touch location
private var currTouchLocation: CGPoint?
let sequenceHandler = VNSequenceRequestHandler()
var isObjectAdded: Bool = false
var isQRCodeFound: Bool = false
var viewCenter:CGPoint = CGPoint()
override func viewDidLoad() {
super.viewDidLoad()
runARSession()
trackingInfo.text = ""
messageLabel.text = ""
viewCenter = CGPoint(x: view.bounds.width / 2.0, y: view.bounds.height / 2.0)
}
#IBAction func didTapReset(_ sender: Any) {
removeAllObjects()
}
func removeAllObjects() {
for object in objects {
object.removeFromParentNode()
}
objects = []
}
// MARK: - barcode handling
func searchQRCode(){
guard let frame = sceneView.session.currentFrame else {
return
}
let handler = VNImageRequestHandler(ciImage: CIImage(cvPixelBuffer: frame.capturedImage), options: [.properties : ""])
//DispatchQueue.global(qos: .userInteractive).async {
do {
try handler.perform([self.barcodeRequest])
} catch {
print(error)
}
//}
}
lazy var barcodeRequest: VNDetectBarcodesRequest = {
return VNDetectBarcodesRequest(completionHandler: self.handleBarcodes)
}()
func handleBarcodes(request: VNRequest, error: Error?) {
//print("handleBarcodes called")
guard let observations = request.results as? [VNBarcodeObservation]
else { fatalError("unexpected result type from VNBarcodeRequest") }
guard observations.first != nil else {
/*DispatchQueue.main.async {
print("No Barcode detected.")
}*/
return
}
// Loop through the found results
for result in request.results! {
print("Barcode detected")
// Cast the result to a barcode-observation
if let barcode = result as? VNBarcodeObservation {
if let payload = barcode.payloadStringValue {
let screenCentre : CGPoint = CGPoint(x: self.sceneView.bounds.midX, y: self.sceneView.bounds.midY)
let hitTestResults = sceneView.hitTest(screenCentre, types: [.existingPlaneUsingExtent])
//check payload
if let hitResult = hitTestResults.first {
// Get Coordinates of HitTest
let transform : matrix_float4x4 = hitResult.worldTransform
let worldCoord : SCNVector3 = SCNVector3Make(transform.columns.3.x, transform.columns.3.y, transform.columns.3.z)
let plane = SCNPlane(width: 0.1, height: 0.1)
let material = SCNMaterial()
material.diffuse.contents = UIColor.red
plane.materials = [material]
// Holder node
let node = SCNNode()
//node.transform = SCNMatrix4MakeRotation(-Float.pi / 2, 1, 0, 0)
//node.geometry = plane
sceneView.scene.rootNode.addChildNode(node)
node.position = worldCoord
//check payload
if(payload == "target_1"){
//Add 3D object
let objectScene = SCNScene(named: "Models.scnassets/candle/candle.scn")!
if let objectNode = objectScene.rootNode.childNode(withName: "candle", recursively: true) {
node.addChildNode(objectNode)
}
}
if(payload == "target_2"){
//Add 3D object
let objectScene = SCNScene(named: "Models.scnassets/lamp/lamp.scn")!
if let objectNode = objectScene.rootNode.childNode(withName: "lamp", recursively: true) {
node.addChildNode(objectNode)
}
}
isQRCodeFound = true
}
}
}
}
}
// MARK: - AR functions
func runARSession() {
// Registers ARKitTestViewController as ARSCNView delegate. You’ll use this later to render objects.
sceneView.delegate = self
// Uses ARWorldTrackingConfiguration to make use of all degrees of movement and give the best results. Remember, it supports A9 processors and up.
let configuration = ARWorldTrackingConfiguration()
// Turns on the automatic horizontal plane detection. You’ll use this to render planes for debugging and to place objects in the world.
configuration.planeDetection = .horizontal
// This turns on the light estimation calculations. ARSCNView uses that automatically and lights your objects based on the estimated light conditions in the real world.
configuration.isLightEstimationEnabled = true
// run(_:options) starts the ARKit session along with capturing video. This method will cause your device to ask for camera capture permission. If the user denies this request, ARKit won’t work.
sceneView.session.run(configuration)
// ASRCNView has an extra feature of rendering feature points. This turns it on for debug builds.
#if DEBUG
sceneView.debugOptions = ARSCNDebugOptions.showFeaturePoints
#endif
}
//Function that gives the user some feedback of the current tracking status.
func updateTrackingInfo() {
// You can get the current ARFrame thanks to the currentFrame property on the ARSession object.
guard let frame = sceneView.session.currentFrame else {
return
}
// The trackingState property can be found in the current frame’s ARCamera object. The trackingState enum value limited has an associated TrackingStateReason value which tells you the specific tracking problem.
switch frame.camera.trackingState {
case .limited(let reason):
switch reason {
case .excessiveMotion:
trackingInfo.text = "Limited Tracking: Excessive Motion"
case .insufficientFeatures:
trackingInfo.text =
"Limited Tracking: Insufficient Details"
default:
trackingInfo.text = "Limited Tracking"
}
default:
trackingInfo.text = "Good tracking conditions"
}
// You turned on light estimation in the ARWorldTrackingConfiguration, so it’s measured and provided in each ARFrame in the lightEstimate property.
guard
let lightEstimate = frame.lightEstimate?.ambientIntensity
else {
return
}
// ambientIntensity is given in lumen units. Less than 100 lumens is usually too dark, so you communicate this to the user.
if lightEstimate < 100 {
trackingInfo.text = "Limited Tracking: Too Dark"
}
}
}
extension ARKitTestViewController: ARSCNViewDelegate {
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
DispatchQueue.main.async {
if let planeAnchor = anchor as? ARPlaneAnchor {
#if DEBUG
let planeNode = createPlaneNode(center: planeAnchor.center, extent: planeAnchor.extent)
node.addChildNode(planeNode)
#endif
// else means that ARAnchor is not ARPlaneAnchor subclass, but just a regular ARAnchor instance you added in touchesBegan(_:with:)
} else {
// currentMode is a ARKitTestViewController property already added in the starter. It represents the current UI state: placeObject value if the object button is selected, or measure value if the measuring button is selected. The switch executes different code depending on the UI state.
switch self.currentMode {
case .none:
break
// placeObject has an associated string value which represents the path to the 3D model .scn file. You can browse all the 3D models in Models.scnassets.
case .placeObject(let name):
// nodeWithModelName(_:) creates a new 3D model SCNNode with the given path name. It’s a helper function provided with the starter project.
let modelClone = nodeWithModelName(name)
// Append the node to the objects array provided with the starter.
self.objects.append(modelClone)
// Finally, you add your new object node to the SCNNode provided to the delegate method.
node.addChildNode(modelClone)
// You’ll implement measuring later.
case .measure:
break
}
}
}
}
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
DispatchQueue.main.async {
if let planeAnchor = anchor as? ARPlaneAnchor {
// Update the child node, which is the plane node you added earlier in renderer(_:didAdd:for:). updatePlaneNode(_:center:extent:) is a function included with the starter that updates the coordinates and size of the plane to the updated values contained in ARPlaneAnchor.
updatePlaneNode(node.childNodes[0], center: planeAnchor.center, extent: planeAnchor.extent)
}
}
}
func renderer(_ renderer: SCNSceneRenderer, didRemove node: SCNNode,
for anchor: ARAnchor) {
guard anchor is ARPlaneAnchor else { return }
// Removes the plane from the node if the corresponding ARAnchorPlane has been removed. removeChildren(inNode:) was provided with the starter project as well.
removeChildren(inNode: node)
}
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
DispatchQueue.main.async {
// Updates tracking info for each rendered frame.
self.updateTrackingInfo()
if(!self.isQRCodeFound){
self.searchQRCode()
}
// If the dot in the middle hit tests with existingPlaneUsingExtent type, it turns green to indicate high quality hit testing to the user.
if let _ = self.sceneView.hitTest(
self.viewCenter,
types: [.existingPlaneUsingExtent]).first {
self.crosshair.backgroundColor = UIColor.green
} else {
self.crosshair.backgroundColor = UIColor(white: 0.34, alpha: 1)
}
}
}
func session(_ session: ARSession, didFailWithError error: Error) {
print("ARSession error: \(error.localizedDescription)")
let message = error.localizedDescription
messageLabel.text = message
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
if self.messageLabel.text == message {
self.messageLabel.text = ""
}
}
}
// sessionWasInterrupted(_:) is called when a session is interrupted, like when your app is backgrounded.
func sessionWasInterrupted(_ session: ARSession) {
print("Session interrupted")
let message = "Session interrupted"
messageLabel.text = message
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
if self.messageLabel.text == message {
self.messageLabel.text = ""
}
}
}
func sessionInterruptionEnded(_ session: ARSession) {
print("Session resumed")
let message = "Session resumed"
messageLabel.text = message
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
if self.messageLabel.text == message {
self.messageLabel.text = ""
}
}
// When sessionInterruptionEnded(_:) is called, you should remove all your objects and restart the AR session by calling the runSession() method you implemented before. removeAllObjects() is a helper method provided with the starter project.
removeAllObjects()
runARSession()
}
}

how to access entity from code - gameplay kit

I have a node I added in the scene. Also in the scene I give it a component to bounce. The component looks like this:
class BounceComponent: GKComponent, ComponentSetupProt {
var bounce: SKAction?
var node: SKSpriteNode?
#GKInspectable var diff: CGFloat = 0.2
#GKInspectable var startScale: CGFloat = 1
#GKInspectable var duration: TimeInterval = 0.5
override init() {
super.init()
comps.append(self)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func setup() {
print("setup")
node = entity?.component(ofType: GKSKNodeComponent.self)!.node as? SKSpriteNode
setupBounce()
}
func setupBounce() {
...
}
func performBounce() {
print("performing")
node?.run(bounce!)
}
}//
In the didMove function on my scene file, it calls the components setup(). This works fine. I'm trying to call the function performBounce() when I click on the button...
if (play?.contains(pos))! {
print("test")
if let _ = play?.entity {
print("we have an entity")
}
play?.entity?.component(ofType: BounceComponent.self)?.performBounce()
}
When I click, the only thing it prints is "test" and the entity is nil. I was under the assumption that when you add a node to the editor, it also sets up the entity for that node, so I'm not sure why it is nil. Curious if anyone could shed some light on this?
Thanks for any help!
The entity on the SKSpriteNode is a weak reference, you need to make sure you retain your entities from your gameplaykit object
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Load 'GameScene.sks' as a GKScene. This provides gameplay related content
// including entities and graphs.
if let scene = GKScene(fileNamed: "Main") {
// Get the SKScene from the loaded GKScene
if let sceneNode = scene.rootNode as? Main {
sceneNode.entities = scene.entities // add this
// Set the scale mode to scale to fit the window
sceneNode.scaleMode = .aspectFill
// Present the scene
if let view = self.view as! SKView? {
view.presentScene(sceneNode)
view.showsDrawCount = true
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
}
}
}
class Main: Base {
var entities = [GKEntity]() // add this

How do edit a sprite based on its name?

So in my game i have a function that spawns coins,they are given the name "coin", Now I have no way to reference the coins,example to kill them or move them.So what I'm trying to do is make a reference to be able to use in my code to just change its zPosition.
Everytime I run my app and have a function run that uses the coinRef [ex. to change the zPosition], the app crashes with the error:
'Thread 1 EXC_BAD_INSTRUCTION (code=EXC_1386_INVOP, subcode=0x0)'
Heres my code:
let coinRef: SKSpriteNode = self.childNodeWithName("coin")! as! SKSpriteNode
func hideCoins() {
coinRef.zPosition = -1
}
func showCoins() {
coinRef.zPosition = 101
}
func killCoins() {
coinRef.removeFromParent()
}
Looking at what you write
So in my game i have a function that spawns coins,they are given the name "coin"
it looks like there are multiple coins in your scene. As you can imagine a single name coin is not enough to univocally identify more then 1 coin :)
We'll need a way do identity multiple coins.
1. The Coin class
class Coin: SKSpriteNode {
private static var lastID: UInt = 0
let id:UInt
init() {
self.id = Coin.lastID++
let texture = SKTexture(imageNamed: "coin")
super.init(texture: texture, color: UIColor.clearColor(), size: texture.size())
self.name = "coin"
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
As you can see Coin has an internal mechanism to assign a new id to each new instance. You can use this id to reference the coins in your scene.
let coin0 = Coin()
coin0.id // 0
let coin1 = Coin()
coin1.id // 1
let coin2 = Coin()
coin2.id // 2
2. Managing your coins
class GameScene: SKScene {
func retrieveCoin(id:UInt) -> Coin? {
return children.filter { ($0 as? Coin)?.id == id }.first as? Coin
}
func hideCoin(id:UInt) {
retrieveCoin(id)?.hidden = true
}
func showCoin(id:UInt) {
retrieveCoin(id)?.hidden = true
}
func deleteCoin(id:UInt) {
retrieveCoin(id)?.removeFromParent()
}
}
The retrieveCoin method returns (if does exist) a coin with the specified id. Otherwise nil is returned.
The hideCoin and showCoin do change the hidden property to change its visibility.
Finally deleteCoin remove from the scene the Coin with the specified id.
Try this. Initialise coinRef before the didMoveToView function, and then give coinRef its value in the didMoveToView function.
class scene : SKScene {
let coinRef: SKSpriteNode = SKSpriteNode()
override func didMoveToView(view: SKView) {
coinRef: SKSpriteNode = self.childNodeWithName("coin")! as! SKSpriteNode
}
func hideCoins() {
coinRef.zPosition = -1
}
func showCoins() {
coinRef.zPosition = 101
}
func killCoins() {
coinRef.removeFromParent()
}
}

Why does my SCNAction sequence stop working intermittently?

I'm trying to move an SCNNode around a scene, constrained to a GKGridGraph. Think along the lines of PacMan but in 3D.
I have a ControlComponent which handles the movement of my SCNNode. The logic should go like this...
Set the queuedDirection property.
If the isMoving flag is false, call the move() method
Use the GKGridGraph to evaluate the next move
3.1 If the entity can move to the GKGridGraphNode using the direction property, nextNode = nodeInDirection(direction)
3.2 If the entity can move to the GKGridGraphNode using the queuedDirection property nextNode = nodeInDirection(queuedDirection)
3.3 If the entity can not move to a node with either direction, set the isMoving flag to false and return.
Create the moveTo action
Create a runBlock which calls the move() method
Apply the moveTo and runBlock actions as a sequence to the SCNNode
I've pasted in the full class below. But I'll explain the problem I'm having first. The above logic works, but only intermitently. Sometimes the animation stops working almost immediatley, and sometimes it runs for up to a minute. But at some point, for some reason, it just stops working - setDirection() will fire, move() will fire , the SCNNode will move once space in the specified direction and then the move() method just stops being called.
I'm not 100% convinced my current approach is correct so I'm happy to hear if there's a more idiomatic SceneKit/GameplayKit way to do this.
Here's the full class, but I think the important bit's are the setDirection() and move() methods.
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!)
}
}
I'll have to answer my own question. First off, it's a bad question because I'm trying to do things incorrectly. The two main mistakes I made were
My compnent was trying to do too much
I wasn't using the updateWithDeltaTime method.
This is how the code and behaviour should be structured using GameplayKit's entity component structure. I'll try to epxlain how all the prices fit together at the end.
NodeComponent
This component is responsible for managing the actual SCNNode that represents my game character. I've moved the code for animating the character out of the ControlComponent and into this component.
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")
}
}
}
Note that every time the components gridPosition property is set, the makeNextMove method is called.
Control Component
My initial example was trying to do too much. This compnents sole responsibility now is to evaluate the next gridPosition for it's entity's NodeComponent. Note that because of updateWithDeltaTime, it will evaluate the next move as often as that method is called.
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!)
}
}
GameViewController
Here's where everything pieces together. There's a lot going on in the class, so I'll post only what's relevant.
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)
}
}
Entity Manager
I picked up this tip following this Ray Wenderlich tutorial. Essentially, it's a bucket to keep all your entities and components in to simplify to work of managing and updating them. I highly reccomend giving that tutorial a walthrough to understand this better.
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()
}
}
So how does all this fit together
The ControlComponent.setDirection() method can be called at any time.
If an entity or component implements an updateWithDeltaTime method, it should be called every frame. It took me a little while to figure out how to do this with SceneKit as most GameplayKit example's are set up for SpriteKit, and SKScene has a very conventient updatet method.
For SceneKit, We have to make the GameVierwController the SCNSceneRendererDelegate for the SCNView. Then, using the rendererUpdateAtTime method, we can call updateAtDeltaTime on the Entity Manager which handles calling the same method on all entities and components every frame.
Note You have to manually set the playing property to true for this to work.
Now we move to the actual animation. ControlComponent is evaluating what the NodeComponents next grid position should be every frame, using the direction property (You can ignore the queuedDirection property, it's an implementation detail).
This means that the NodeComponents.makeNextMove() method is also being called every frame. Whenever newPosition and oldPosition are not the same, the animation is applied. And whenever the animation finishes, the node's gridPosition is updated.
Now, as to why my intial method for animating my SCNNode didn't work, I've no idea. But at least it forced me to better understand how to use GameplayKit's Entity/Component design.

NSImage in a CALayer and Liveness

I'm missing something here. I got Liveness to work in Interface Builder. I can draw the image in the NSView with NSImage.draw rect. So the image loads correctly. However when I put this inside a CALayer it doesn't show up.
Did I miss something about behaviour on NSView? CALayer? Layer Hosting? Or something else?
Here's the code of the view:
import Foundation
import AppKit
import QuartzCore
#IBDesignable public class CircularImageView: NSView {
var imageLayer: CALayer?
#IBInspectable public var edgeInset: CGFloat = 10
public var image: NSImage? {
didSet {
if let newImage = image {
imageLayer?.contents = newImage
}
}
}
// MARK: New in this class
private func prepareLayer() {
self.layer = CALayer()
self.wantsLayer = true
}
private func drawImage() {
// What am I doing wrong here?
var newImageLayer = CALayer()
newImageLayer.contentsGravity = kCAGravityResizeAspect
if let imageToSet = image {
newImageLayer.contents = imageToSet
}
let insetBounds = CGRectInset(self.bounds, edgeInset, edgeInset)
newImageLayer.frame = insetBounds
newImageLayer.backgroundColor = NSColor(calibratedWhite: 0.8, alpha: 1).CGColor
self.layer!.addSublayer(newImageLayer)
imageLayer = newImageLayer
}
private func test(){
image?.drawInRect(self.bounds)
}
// MARK: NSView stuff
public override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
prepareLayer()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
prepareLayer()
}
public override func viewWillDraw() {
super.viewWillDraw()
drawImage()
}
public override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
// Load default test image.
println("\(self): prepareForInterfaceBuilder")
let processInfo = NSProcessInfo.processInfo()
let environment = processInfo.environment
let projectSourceDirectories : AnyObject = environment["IB_PROJECT_SOURCE_DIRECTORIES"]!
let directories = projectSourceDirectories.componentsSeparatedByString(":")
if directories.count != 0 {
let firstPath = directories[0] as! String
let imagePath = firstPath.stringByAppendingPathComponent("CircularView/Bz1dSvR.jpg")
let image = NSImage(contentsOfFile: imagePath)
image!.setName("Test Image")
self.image = image
}
}
}
Thanks in advance.
Okay, guys I found the answer.
I did two things wrong in this code. I was working on a layer hosting view and I simply needed a layer-backed view. I didn't new there was a difference. And before adding an NSImage to the CALayer's contents I needed to embrace it with lockFocus() and unlockFocus().
Here's the full code that solved the issue.
import Foundation
import AppKit
#IBDesignable public class CircularImageView: NSView {
var imageLayer: CALayer?
#IBInspectable public var edgeInset: CGFloat = 10
public var image: NSImage? {
didSet {
if let newImage = image {
imageLayer?.contents = newImage
}
}
}
// MARK: New in this class
private func prepareLayer() {
// I had to remove my own created layer.
self.wantsLayer = true
}
private func drawImage() {
var newImageLayer = CALayer()
newImageLayer.contentsGravity = kCAGravityResizeAspect
if let imageToSet = image {
// I didn't lock the focus on the imageToSet.
imageToSet.lockFocus()
newImageLayer.contents = imageToSet
// I didn't unlock the focus either.
imageToSet.unlockFocus()
}
let insetBounds = CGRectInset(self.bounds, edgeInset, edgeInset)
newImageLayer.frame = insetBounds
newImageLayer.backgroundColor = NSColor(calibratedWhite: 0.8, alpha: 1).CGColor
self.layer!.addSublayer(newImageLayer)
imageLayer = newImageLayer
}
// MARK: NSView stuff
public override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
prepareLayer()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
prepareLayer()
}
public override func viewWillDraw() {
super.viewWillDraw()
drawImage()
}
public override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
// Load default test image.
println("\(self): prepareForInterfaceBuilder")
let processInfo = NSProcessInfo.processInfo()
let environment = processInfo.environment
let projectSourceDirectories : AnyObject = environment["IB_PROJECT_SOURCE_DIRECTORIES"]!
let directories = projectSourceDirectories.componentsSeparatedByString(":")
if directories.count != 0 {
let firstPath = directories[0] as! String
let imagePath = firstPath.stringByAppendingPathComponent("CircularView/Bz1dSvR.jpg")
let image = NSImage(contentsOfFile: imagePath)
image!.setName("Test Image")
self.image = image
}
}
}