How to add a 30 second timer to end a game round? - swift

I am currently experimenting with some code that I found on the internet about a game where you have to click on one set of items and avoid clicking on the other. I am currently trying to add a timer to the game so that it lasts of a total of 30 seconds but I am really struggling to do so as I am quite inexperienced with this programming language.
import UIKit
import QuartzCore
import SceneKit
class GameViewController: UIViewController, SCNSceneRendererDelegate {
var gameView:SCNView!
var SceneGame:SCNScene!
var NodeCamera:SCNNode!
var targetCreationTime:TimeInterval = 0
override func viewDidLoad() {
super.viewDidLoad()
View_in()
initScene()
initCamera()
}
func View_in(){
gameView = self.view as! SCNView
gameView.allowsCameraControl = true
gameView.autoenablesDefaultLighting = true
gameView.delegate = self
}
func initScene (){
SceneGame = SCNScene()
gameView.scene = SceneGame
gameView.isPlaying = true
}
func initCamera(){
NodeCamera = SCNNode()
NodeCamera.camera = SCNCamera()
NodeCamera.position = SCNVector3(x:0, y:5, z:10)
SceneGame.rootNode.addChildNode(NodeCamera)
}
func createTarget(){
let geometry:SCNGeometry = SCNPyramid( width: 1, height: 1, length: 1)
let randomColor = arc4random_uniform(2
) == 0 ? UIColor.green : UIColor.red
geometry.materials.first?.diffuse.contents = randomColor
let geometryNode = SCNNode(geometry: geometry)
geometryNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
if randomColor == UIColor.red {
geometryNode.name = "enemy"
}else{
geometryNode.name = "friend"
}
SceneGame.rootNode.addChildNode(geometryNode)
let randomDirection:Float = arc4random_uniform(2) == 0 ? -1.0 : 1.0
let force = SCNVector3(x: randomDirection, y: 15, z: 0)
geometryNode.physicsBody?.applyForce(force, at: SCNVector3(x: 0.05, y: 0.05, z: 0.05), asImpulse: true)
}
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
if time > targetCreationTime{
createTarget()
targetCreationTime = time + 0.6
}
cleanUp()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first!
let location = touch.location(in: gameView)
let hitList = gameView.hitTest(location, options: nil)
if let hitObject = hitList.first{
let node = hitObject.node
if node.name == "friend" {
node.removeFromParentNode()
self.gameView.backgroundColor = UIColor.black
}else {
node.removeFromParentNode()
self.gameView.backgroundColor = UIColor.red
}
}
}
func cleanUp() {
for node in SceneGame.rootNode.childNodes {
if node.presentation.position.y < -2 {
node.removeFromParentNode()
}
}
}
override var shouldAutorotate: Bool {
return true
}
override var prefersStatusBarHidden: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .allButUpsideDown
} else {
return .all
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
}

You could use a Timer object, documented here. Just set up the timer when you want the game to start, probably once you've finished all your initializations. When you set up the timer, just wait for it to call back to your code when it finishes and run whatever logic you want to use to terminate your game.

EDIT
Create a variable representing the time you want your game will end:
var time: CGFloat = 60
Then, add an SCNAction to your scene so that each second it will decrease this variable value, for example in the viewDidLoad:
//One second before decrease the time
let wait = SCNAction.wait(forDuration: 1)
//This is the heart of this answer
// An action that reduce the time and when it is less than 1 (it reached zero) do whatever you want
let reduceTime = SCNAction.run{ _ in
self.time -= 1
if self.time < 1 {
// Do whatever you want
// for example show a game over scene or something else
}
}
}
SceneGame.rootNode.run(SCNAction.repeatForever(SCNAction.sequence([wait,reduceTime])))
If you want, you can show the remaining time by using SKLabel on an HUD, which is an SKScene used as overlay.
You can check this tutorial for how to create an HUD
As well, you can use an SCNText, this is the documentation about it

Related

tap gesture recognizer Xcode

I want to make a 3d game on Xcode, SceneKit. To test it out, I started a simple project about a box, that if you tapped, moves in X direction. My question is how do you make the tap gesture work in a 3d game? I've been trying to add it but it just doesn't work. Here is all the code that I've written in case I'm doing something wrong:
import UIKit
import QuartzCore
import SceneKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene(named:"art.scnassets/Box.scn")
let newscene = self.view as! SCNView
newscene.scene = scene
let tap = UITapGestureRecognizer(target: self, action: #selector(taps(tap:)));newscene.addGestureRecognizer(tap)
tap.numberOfTapsRequired = 1
tap.numberOfTouchesRequired = 1
}
override var shouldAutorotate: Bool {
return true
}
override var prefersStatusBarHidden: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .allButUpsideDown
} else {
return .all
}
}
#objc func taps(tap:UITapGestureRecognizer){
let newscene = self.view as! SCNView
let scene = SCNScene(named: "art.scnassets/Box.scn")
let box = scene?.rootNode.childNode(withName: "Box", recursively: true)
let place = tap.location(in: newscene)
let tapped = newscene.hitTest(place, options: [:])
if tapped.count > 0{
box?.runAction(SCNAction.repeatForever(SCNAction.moveBy(x: 5, y: 0, z: 0, duration: 1)))
}
}
}
Try this...
#objc func handleTap(recognizer: UITapGestureRecognizer)
{
if(data.isNavigationOff == true) { return } // No panel select if Add, Update, EndWave, or EndGame
if(gameMenuTableView.isHidden == false) { return } // No panel if game menu is showing
let location: CGPoint = recognizer.location(in: gameScene)
if(data.isAirStrikeModeOn == true)
{
let projectedPoint = gameScene.projectPoint(SCNVector3(0, 0, 0))
let scenePoint = gameScene.unprojectPoint(SCNVector3(location.x, location.y, CGFloat(projectedPoint.z)))
gameControl.airStrike(position: scenePoint)
}
else
{
let hitResults = gameScene.hitTest(location, options: hitTestOptions)
for vHit in hitResults
{
if(vHit.node.name?.prefix(5) == "Panel")
{
// May have selected an invalid panel or auto upgrade was on
if(gameControl.selectPanel(vPanel: vHit.node.name!) == false) { return }
return
}
}
}
}
If Airstrike - Tap drops a bomb on the screen where you touched, translating it to 3D
Else hittest looks for panels which may return multiple results

Animated 3D models using ARKit. Swift

I added purchased 3D zombie model and I want to add it multiple times so there is a group of zombies chasing the player (his camera). The DAE file (zombie) I have has couple of animations (attack, walk, run and so on). I added a couple of these zombie models and they all appear on the positions I wanted them to appear (previously determined coordinates stored in zombies array)
When they appear on the screen they do all the animations as it is presented in DAE file.
And now my question: Is it possible to add this DAE zombie model but display it in a specified frame? For example 2-122? My code so far (I deleted what was unnecessary)
import UIKit
import CoreLocation
import SceneKit
import ARKit
#available(iOS 11.0, *)
class VRViewController: UIViewController, CLLocationManagerDelegate
{
/* VARIABLES */
var number:Int = 10
var speed:Int = 4
var zombies = [[AnyObject]]()
var timer = Timer()
var nodes = [SCNNode]()
let configuration = ARWorldTrackingConfiguration()
#IBOutlet weak var sceneView: ARSCNView!
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
// Run the view's session
sceneView.session.run(configuration)
}
override func viewDidLoad()
{
super.viewDidLoad()
// Set the view's delegate
sceneView.delegate = self as? ARSCNViewDelegate
// Create a new scene
let scene = SCNScene()
// Set the scene to the view
sceneView.scene = scene
sceneView.autoenablesDefaultLighting = true
/* sceneView.debugOptions = [.showConstraints, .showLightExtents, ARSCNDebugOptions.showFeaturePoints, ARSCNDebugOptions.showWorldOrigin] */
}
override func viewDidAppear(_ animated: Bool)
{
setZombies()
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(VRViewController.restartSession)), userInfo: nil, repeats: true)
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
}
/* MAIN FUNCTIONS */
func setZombies()
{
for i in 0..<number
{
let zombiePlane = zombies[i][0] as! CGPoint
let thetaPlane = zombies[i][1] as! Double
let xPlane = abs(zombiePlane.x - center.x)
let yPlane = abs(zombiePlane.y - center.y)
let distance = sqrt((xPlane*xPlane) + (yPlane*yPlane))
var theta3D:Double = thetaPlane * (180/Double.pi) - diff2D - 90 /* degrees */
theta3D = theta3D * (Double.pi/180) /* radians */
let x3D = Float(distance) * Float(cos(theta3D))
let z3D = Float(distance) * Float(sin(theta3D))
addZombies(i:i, x: x3D, y: -1.5, z: z3D)
}
}
func addZombies(i:Int, x: Float, y: Float, z: Float) {
guard let zombieScene = SCNScene(named: "art.scnassets/zombie/StrongZombie.DAE") else { return }
let zombieNode = SCNNode()
let zombieSceneChildNodes = zombieScene.rootNode.childNodes
for childNode in zombieSceneChildNodes {
zombieNode.addChildNode(childNode)
}
zombieNode.position = SCNVector3(x, y, z)
zombieNode.scale = SCNVector3(0.1, 0.1, 0.1)
sceneView.scene.rootNode.addChildNode(zombieNode)
nodes.append(zombieNode)
}
func restartSession()
{
/* self.sceneView.session.run(configuration, options: [.resetTracking]) */
for i in 0..<number
{
let theta3D = atan2(nodes[i].position.z, nodes[i].position.x)
let movement = (Float(speed)/5)
print(speed)
let distance = sqrt((nodes[i].position.x)*(nodes[i].position.x) + (nodes[i].position.z)*(nodes[i].position.z)) - movement
let x3D = Float(distance) * Float(cos(theta3D))
let z3D = Float(distance) * Float(sin(theta3D))
nodes[i].position = SCNVector3(x:x3D, y:-10, z:z3D)
}
}
/* SESSION FUNCTIONS */
func session(_ session: ARSession, didFailWithError error: Error)
{
// Present an error message to the user
}
func sessionWasInterrupted(_ session: ARSession)
{
// Inform the user that the session has been interrupted, for example, by presenting an overlay
}
func sessionInterruptionEnded(_ session: ARSession)
{
// Reset tracking and/or remove existing anchors if consistent tracking is required
}
}

Creating a stopwatch in swift

I have tried searching other answers and I cannot find one that applies to my scenario. I am writing a game in swift and want to create a stopwatch that determines how the long the player is playing. When the user touches down, the stopwatch will start and when a certain action happens, then the timer stops and resets. I'd like to use minutes, seconds, milliseconds (i.e. 00:00.00).
Currently, the time function kind of works. It doesn't start at 0, it starts at the current seconds in time (I know thats when I start it, but I don't know how to start it at 0). It also only updates when I touch the screen, I need it to count from 00:00.00 and update on its own until the cancel action is fired.
Thank you for your time.
Here is what I have so far:
class GameScene: SKScene {
var activeTimer = SKLabelNode()
//var bestTime = SKLabelNode()
var startTime = TimeInterval()
//var currentTime = TimeInterval()
//var countingTime = TimeInterval()
var updateTimerAction = SKAction()
override func didMove(to view: SKView) {
activeTimer = self.childNode(withName: "active_timer") as! SKLabelNode
//bestTime = self.childNode(withName: "best_time") as! SKLabelNode
startTime = TimeInterval(Calendar.current.component(.second, from:Date()))
updateTimerAction = SKAction.sequence([SKAction.run(updateTimer), SKAction.wait(forDuration: 1.0)])
}
func startGame() {
// Other code
startGameTimer()
}
func resetGame() {
// Other code
stopGameTimer()
}
func startGameTimer() {
run(SKAction.repeatForever(updateTimerAction), withKey: "timer")
}
func updateTimer() {
activeTimer.text = stringFromTimeInterval(interval: startTime) as String
}
func stringFromTimeInterval(interval: TimeInterval) -> NSString {
let ti = NSInteger(interval)
let ms = Int((interval.truncatingRemainder(dividingBy: 1)) * 1000)
let seconds = ti % 60
let minutes = (ti / 60) % 60
return NSString(format: "%0.2d:%0.2d.%0.2d",minutes,seconds,ms)
}
func stopGameTimer() {
removeAction(forKey: "timer")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
startGame()
for touch in touches {
// other code
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
// other code
}
}
override func update(_ currentTime: TimeInterval) {
updateTimer()
if <action> {
// stop timer and reset game
resetGame()
}
}
Instead of using a TimeInterval object, it might be better to use a Timer object which calls a function that increments the value of a label (or an internal variable) from 0 onwards accordingly after every 1 second(or can be made lesser). And for stopping the clock just call the Timer object's invalidate function.
Check the documentation for more info: https://developer.apple.com/documentation/foundation/timer
The following lines of code come from an actual macOS game that I submitted to Mac App Store some 3 weeks ago. It counts down a number to 0. In your case, change
self.currentSeconds -= 1
to
self.currentSeconds += 1
timeBackNode is an empty node that holds SKLabelNode objects timeNode0 and timeNode1. So create it when the game starts with didMove.
var currentSeconds = Int() // currentSeconds
var timeNode0 = SKLabelNode() // timeNode0
var timeNode1 = SKLabelNode() // timeNode1
func setupTimeGoal() {
enumerateChildNodes(withName: "//TimerBack", using: { timeBackNode, _ in
let goal = self.timeDict["num"] as! Int
self.currentSeconds = goal // In your case, goal is 0 since you are going to count up.
self.timeNode0 = SKLabelNode(fontNamed: "Your font's font name")
self.timeNode0.text = self.makeTimeWithSeconds(secs: goal)
self.timeNode0.fontSize = 26
self.timeNode0.fontColor = SKColor.black
self.timeNode0.horizontalAlignmentMode = .center
self.timeNode0.position = CGPoint(x: 1, y: -22)
timeBackNode.addChild(self.timeNode0)
self.timeNode1 = SKLabelNode(fontNamed: "Your font's font name")
self.timeNode1.text = self.makeTimeWithSeconds(secs: goal) // this function translate a counting number into a time code like 00:00:00
self.timeNode1.fontSize = 26
self.timeNode1.fontColor = SKColor.cyan
self.timeNode1.horizontalAlignmentMode = .center
self.timeNode1.position = CGPoint(x: 0, y: -23)
timeBackNode.addChild(self.timeNode1)
})
}
func repeatTime() {
let waitAction = SKAction.wait(forDuration: 1.0)
let completionAction = SKAction.run {
if self.currentSeconds == 0 && self.gameState == .playing {
self.removeAction(forKey: "RepeatTime")
self.proceedLoss()
return
}
self.currentSeconds -= 1
self.timeNode0.text = self.makeTimeWithSeconds(secs: self.currentSeconds)
self.timeNode1.text = self.makeTimeWithSeconds(secs: self.currentSeconds)
}
let seqAction = SKAction.sequence([waitAction, completionAction])
let repeatAction = SKAction.repeatForever(seqAction)
self.run(repeatAction, withKey: "RepeatTime")
}
Here is what I ended up doing. While El Tomato's answer appears to work, given the setup of my code I went a little different route. After talking with some other friends, I chose to user a timer due to the design of my application. If I happen to run into any problems using a Timer object I will update this question.
The code below will start on a touch, update in the format: "00:00.00" and stop when the action is fired. This time will pause until another touch and the timer will then start from zero.
class GameScene: SKScene {
var seconds = 0
var timer = Timer()
var timeStarted = Bool()
var activeTimer = SKLabelNode()
//var bestTime = SKLabelNode()
override func didMove(to view: SKView) {
activeTimer = self.childNode(withName: "active_timer") as! SKLabelNode
//bestTime = self.childNode(withName: "best_time") as! SKLabelNode
timeStarted = false
}
func startGame() {
startGameTimer()
timeStarted = true
}
func resetGame() {
timeStarted = false
stopGameTimer()
seconds = 0
}
func startGameTimer() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(updateTimer)), userInfo: nil, repeats: true)
}
func updateTimer() {
seconds += 1
activeTimer.text = timeString(time: TimeInterval(seconds))
}
func timeString(time:TimeInterval) -> String {
let hours = Int(time) / 3600
let minutes = Int(time) / 60 % 60
let seconds = Int(time) % 60
return String(format:"%02i:%02i.%02i", hours, minutes, seconds)
}
func stopGameTimer() {
timer.invalidate()
removeAction(forKey: "timer")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if !timeStarted {
startGame()
}
for touch in touches {
// other code
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
// other code
}
}
override func update(_ currentTime: TimeInterval) {
if timeStarted {
updateTimer()
}
if <action> {
// stop timer and reset game
resetGame()
}
}
}

Multiple circles drawn SpriteKit

I'm working on simple game which will based on paiting background by player. In left and right corner there will be buttons which will move character to left or right. I've already implemented that (character is moving and lefts painted background behind), but with adding another circles fps's drops really fast. Is there any solution to that?
import SpriteKit
class GameScene: SKScene {
var playerDot:PlayerDot = PlayerDot(imageNamed:"player")
var isTurningLeft:Bool = false
var isTurningRight:Bool = false
var lastLocation:CGPoint = CGPoint()
override func didMoveToView(view: SKView) {
/* Setup your scene here */
let myLabel = SKLabelNode(fontNamed:"Helvetica")
myLabel.name = "left"
myLabel.text = "Left"
myLabel.fontSize = 30
myLabel.horizontalAlignmentMode = .Left
myLabel.position = CGPoint(x:CGRectGetMinX(self.frame), y:CGRectGetMinY(self.frame))
self.addChild(myLabel)
let myLabel2 = SKLabelNode(fontNamed:"Helvetica")
myLabel2.name = "right"
myLabel2.text = "Right"
myLabel2.fontSize = 30
myLabel2.horizontalAlignmentMode = .Right
myLabel2.position = CGPoint(x:CGRectGetMaxX(self.frame), y:CGRectGetMinY(self.frame))
self.addChild(myLabel2)
playerDot.position = CGPoint(x:CGRectGetMaxX(self.frame)/2, y:CGRectGetMinY(self.frame))
self.addChild(playerDot)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
let location = touch.locationInNode(self)
if let theName = self.nodeAtPoint(location).name {
if theName == "left" {
isTurningLeft = true
isTurningRight = false
}
else if theName == "right" {
isTurningRight = true
isTurningLeft = false
}
}
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
let location = touch.locationInNode(self)
if let theName = self.nodeAtPoint(location).name {
if theName == "left" {
isTurningLeft = false
}
else if theName == "right" {
isTurningRight = false
}
}
}
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
let location = touch.locationInNode(self)
if let theName = self.nodeAtPoint(location).name {
if theName == "left" {
isTurningLeft = false
}
else if theName == "right" {
isTurningRight = false
}
}
}
}
override func update(currentTime: CFTimeInterval) {
if(isTurningLeft){
playerDot.increaseAngle()
} else if (isTurningRight){
playerDot.decreaseAngle()
}
//calculates new character position based on angle of movement changed
playerDot.updatePosition()
drawCircle()
}
func drawCircle(){
if(distanceFromCGPoints(lastLocation, b: playerDot.position)>2){
let circle = SKShapeNode(circleOfRadius: 10 )
circle.position = playerDot.position
circle.fillColor = SKColor.orangeColor()
circle.strokeColor = SKColor.orangeColor()
self.addChild(circle)
lastLocation = playerDot.position
}
}
func distanceFromCGPoints(a:CGPoint,b:CGPoint)->CGFloat{
return sqrt(pow(a.x-b.x,2)+pow(a.y-b.y,2));
}
func random() -> CGFloat {
return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
}
func random(min min: CGFloat, max: CGFloat) -> CGFloat {
return random() * (max - min) + min
}
}
EDIT:
drawCircle with SKShapeNode replaced with SKSpriteNote
func drawCircle(){
if(distanceFromCGPoints(lastLocation, b: playerDot.position)>2){
let playerDot2 = SKSpriteNode(imageNamed:"player")
playerDot2.position = playerDot.position
self.addChild(playerDot2)
lastLocation = playerDot.position
}
}
If you are having frame rate drops then some parts of your code are slowing down execution too much. You need to optimise your code. Use Instruments (Time Profiler) to find out which lines/functions are causing problems speed wise. If you have never used it before read up on it and use it a couple times to get the gist, then I recommend watching the WWDC video Profiling In-Depth
Based on your edit (switching to SKSpriteNode), this is how you could do it:
Declare a texture as a property of your scene. This way, you load it once and reuse it later:
let yourTexture = SKTextureAtlas(named: "yourAtlas").textureNamed("yourTexture")
Then in your drawCircle method:
func drawCircle(){
if(distanceFromCGPoints(lastLocation, b: playerDot.position)>2){
let playerDot2 = SKSpriteNode(texture: yourTexture)
playerDot2.position = playerDot.position
self.addChild(playerDot2)
lastLocation = playerDot.position
}
}
This way (by using SKSpriteNode instead of SKShapeNode) you have reduced number of draw calls required to render all those circles. Also because you are reusing the same texture, instead of allocating it every time using imageNamed you have reduced greatly an amount of memory that app consumes. Now, SpriteKit can render hundreds of nodes #60 fps if those nodes are rendered in batches. Still there is a limit before fps start dropping. And that depends on a device.

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.