I'm making a game where the player follows a touch. The game has obstacles and pathfinding, and I'm experiencing extreme lag with touches moved. Upon loading the game, everything works, but after a few seconds of dragging my finger around (especially around obstacles) the game freezes to the point where moving a touch will freeze all physics (0 fps) until the touch is ended.
I'm assuming the culprit is my function makeGraph(), which finds a path for the player from player.position:CGPoint() to player.destination:CGPoint(), storing a path in player.goto:[CGPoint()], the update function then takes care of moving to the next goto point.
This function is called every cycle of touchesmoved, so the player can switch directions if the finger moves over an obstacle
My question is: 1) what in this code is causing the unbearable lag, 2) how can I make this code more efficient?
My code:
initializing vars:
var obstacles = [GKPolygonObstacle(points: [float2()])]
var navgraph = GKObstacleGraph()
setting up a graph (called at start of level):
func setupGraph(){
var wallnodes :[SKSpriteNode] = [SKSpriteNode]()
self.enumerateChildNodes(withName: "wall") {
node, stop in
wallnodes.append(node as! SKSpriteNode)
}
self.enumerateChildNodes(withName: "crate") {
node, stop in
wallnodes.append(node as! SKSpriteNode)
}
obstacles = SKNode.obstacles(fromNodeBounds: wallnodes)
navgraph = GKObstacleGraph(obstacles: obstacles, bufferRadius: Float(gridSize/2))
}
touchesmoved: I keep track of multiple touches using strings. only one finger may act as the "MoveFinger." this string is created at touchesbegan and emptied at touchesended
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
touchloop: for touch in touches {
if MoveFinger == String(format: "%p", touch) {
let location = touch.location(in: self)
player.destination = location
makeGraph()
player.moving = true
player.UpdateMoveMode()
continue touchloop
}
// .... more if statements ....
}
}
and graph function (called every touchesmoved cycle):
func makeGraph(){
let startNode = GKGraphNode2D(point: float2(Float(player.position.x), Float(player.position.y)))
let endNode = GKGraphNode2D(point: float2(Float(player.destination.x), Float(player.destination.y)))
let graphcopy = navgraph
graphcopy.connectUsingObstacles(node: startNode)
graphcopy.connectUsingObstacles(node: endNode)
let path = graphcopy.findPath(from: startNode, to: endNode)
player.goto = []
for node:GKGraphNode in path {
if let point2d = node as? GKGraphNode2D {
let point = CGPoint(x: CGFloat(point2d.position.x), y: CGFloat(point2d.position.y))
player.goto.append(point)
}
}
if player.goto.count == 0 {
//if path is empty, go straight to destination
player.goto = [player.destination]
} else {
//if path is not empty, remove first point (start point)
player.goto.remove(at: 0)
}
}
any ideas?
Related
I'm writing a Pong-like game using SpriteKit and Swift 5 for learning purpose. And I'm trying to made the player's paddle (an SKNode rectangle) move across the screen in the x coordinate but couldn't.
I've tried this simple solution:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches {
let location = t.location(in: self)
playerPaddle.position.x = location.x
}
It works but seems like the paddle only moves on clicks (touches), but not drags.
Also I've tried using the UIPanGestureRecognizer, but I don't really understand how to call it, here is the code which I copied from the official Apple Developer's doc and changed a little to match my situation:
#IBAction func panPiece(_ gestureRecognizer : UIPanGestureRecognizer) {
guard gestureRecognizer.view != nil else {return}
let piece = gestureRecognizer.view!
let translation = gestureRecognizer.translation(in: piece.superview)
if gestureRecognizer.state == .began {
self.initialPos = piece.center
}
else if gestureRecognizer.state != .changed {
let newPos = CGPoint(x: initialPos.x + translation.x, y: initialPos.y)
piece.center = newPos
}
}
Your touchesMoved solution works for me, so I'd assume that something else is amiss. Regardless, I can offer an alternative:
I also built Pong to learn Swift and SpriteKit, and I implemented the paddle movement using SKActions. Any time a touch begins, cancel the paddle's current actions and begin moving to the new x location.
If you want to keep it simple to start, then you can have a constant move time, like 0.5 seconds. Otherwise, you can calculate the time to move by getting the absolute distance between the paddle and touch location and dividing by some constant. Changing the constant will change the speed at which the paddle moves.
Here's an example with the constant move time:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if playerPaddle.hasActions() { playerPaddle.removeAllActions() }
for touch in touches {
let newX = touch.location(in: self).x
let oldX = playerPaddle.position.x
playerPaddle.run(.moveBy(x: newX - oldX, y: 0, duration: 0.5))
}
}
My animation model has a total time interval of 45 seconds. I tap on the model and should be able to play it not right from the beginning, but from say, the 15th second.
Can anyone please help me out if you think by any means that this is possible?
EDIT:
As soon as I load my animation model, the SceneKit plays the animation. Now with the key in hand, I crop the animation with the help of a custom method I came across.
By tapping on the model, I enumerate through all the parent/child nodes to stop or remove animation from the scene. So far so good.
The problem appears when I try to add the cropped animation back on to the scene. Nothing really happens, as the scene remains idle without any action.
Am I doing something wrong here?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touchLocation = touches.first!.location(in: sceneView)
// Let's test if a 3D Object was touch
var hitTestOptions = [SCNHitTestOption: Any]()
hitTestOptions[SCNHitTestOption.boundingBoxOnly] = true
let hitResults: [SCNHitTestResult] = sceneView.hitTest(touchLocation, options: hitTestOptions)
let animation = animScene?.entryWithIdentifier("myKey", withClass: CAAnimation.self)
print(" duration is...", animation!.duration)
let animationNew = subAnimation(of:(animation)!, startFrame: 10, endFrame: 360)
print("New duration is...", animationNew.duration)
sceneView.scene.rootNode.enumerateChildNodes { (node, stop) in
node.removeAllAnimations()
}
sceneView.scene.rootNode.enumerateChildNodes { (node, _) in
node.addAnimation(animationNew, forKey: "myKey")
}
}
Suppose, it's the most robust approach for playing animations containing in Collada file:
import SceneKit
func myAnimation(path: String) -> SCNAnimation? {
let scene = SCNScene(named: path)
var animation: SCNAnimationPlayer?
scene?.rootNode.enumerateChildNodes( { (child, stop) in
if let animationKey = child.animationKeys.first {
animation = child.animationPlayer(forKey: animationKey)
// variable pointee: ObjCBool { get nonmutating set }
stop.pointee = true
}
})
return animation?.animation
}
let node = SCNNode()
let animation = myAnimation(path: "animation.dae")
node.addAnimation(animation!, forKey: "FirstAnimationSet")
Seems my .dae file had a number of animations(with multiple id's) and they should be grouped as one. Once I group them together, I get control over the animation and could play them from whichever frame I want.
I am trying to make touch events take the trackpad as the display when moving it.
What I mean in this is:
Consider a Mac laptop display, and imagine it to be exactly the size of the trackpad, or so this is how I want the touch events to be implemented. There was a normalised x y coordinate property before in NSEvent which may have given me this, but it seems to have been deprecated.
Below are touchBegan and touchMoved overrides and what I mean:
override func touchesBegan(with event: NSEvent) {
CGDisplayMoveCursorToPoint(CGMainDisplayID(), event.location(in: overlayScene))
let player = SCNAudioPlayer(source: tick)
sineNode.addAudioPlayer(player)
}
override func touchesMoved(with event: NSEvent) {
let location = event.location(in: overlayScene)
guard let node = overlayScene.nodes(at: location).first else{
if currentSKNode != nil{
currentSKNode = nil
}
return
}
if currentSKNode != node{
currentSKNode = node
print(node.name)
let player = SCNAudioPlayer(source: tick)
sineNode.addAudioPlayer(player)
}
}
Imagine a fgraph reader where x and y axes are centred at exactly width/2 and height/2 of the trackpad. When I touch anywhere on it, this should reflect exactly on the apps window which is set to full screen.
Currently, the mouse cursor seems to go only partially across the display when I make a full left to right move, hence trying to reposition the mouse cursor to the position of the NSView, or in this case the SKScene.
Ok, so it turns out, as Kent mentioned, normalised position is not deprecated, but I was looking into NSEvent not NSTouch.
Here's the code for anyone who would stumble upon the same requirements. Works fine, just need to figure out now how to make this as responsive as possible.
override func touchesMoved(with event: NSEvent) {
DispatchQueue.main.async {
let touch = event.touches(matching: .moved, in: self.sceneView).first
let normalised = touch!.normalizedPosition
let translated = CGPoint(x: self.width*normalised.x - self.widthCenter, y: self.height*normalised.y - self.heightCenter)
guard let node = self.overlayScene.nodes(at: translated).first else{
self.currentSKNode = nil
return
}
if self.currentSKNode != node{
self.currentSKNode = node
let player = SCNAudioPlayer(source: self.tick)
self.sineNode.addAudioPlayer(player)
}
}
}
It's the third time I'm looking for help in this question.
I have a class name BallNode of kind SKShapeNode. Inside my code I also have a function the spawn ball every 3 second from the top side of the screen. Now, I want to set a function that locate the ball position every 1 second, and so, if the ball.position.y > 200 to print a message to the console.
The purpose of this is that if any ball will be at this position (not while it's falling down) the I will call another function. I tried to do it via SKAction, update(_ currentTime: CFTimeInterval), Timer but I didn't succeed and I really have no idea what to do...
update - my current code:
var timeType1: CFTimeInterval = 0.0
var timeType2: CFTimeInterval = 2.0
override func update(_ currentTime: CFTimeInterval) {
if (currentTime - timeType1 > timeType2){
print("time")
self.checkBallsPosition()
self.timeType1 = currentTime
}
self.enumerateChildNodes(withName: "color.BallNode") { node, _ in
self.checkBallsPosition()
}
}
func checkBallsPosition() {
self.enumerateChildNodes(withName: "BALL") { (node: SKNode, nil) in
let x = self.createTopBorder()
x.isHidden = true
let wait2 = SKAction.wait(forDuration: 1)
let action2 = SKAction.run {
let position = Double(node.position.y)
if position < 200 {
}
else if position > 200 {
print("bolbo")
node.removeFromParent()
}
}
self.run(SKAction.sequence([wait2,action2]))
}
}
thats what I try do to so far, as I said the problem is that I want to get the ball last position. because the ball fall down the screen the last position should be when it touch the bottom border of the screen or when it touches another ball. if I set it at update I get the ball position every frame or (as I did) every second - but not the last. another problem is that the ball position can always change depends on another balls (when collision occurs).
update #2 - another functions:
func spawnBalls() {
let wait = SKAction.wait(forDuration: 3)
let action = SKAction.run {
self.createBall()
}
run(SKAction.repeatForever((SKAction.sequence([wait, action]))))
}
func createBall(){
let ball = BallNode(radius: 65)
print(ball.Name)
print(ball._subName!)
ball.position.y = ((frame.size.height) - 200)
let ballXPosition = arc4random_uniform(UInt32(frame.size.width))
ball.position.x = CGFloat(ballXPosition)
ball.physicsBody?.categoryBitMask = PhysicsCategory.ball
ball.physicsBody?.collisionBitMask = PhysicsCategory.ball
ball.physicsBody?.contactTestBitMask = PhysicsCategory.topBorder
ball.delegate = self
addChild(ball)
}
You did not post the entire code you are using. Not knowing how you are spawning your balls, how is the hierarchy and how you are moving them, I can't reproduce it here.
I am not sure if I understand it right, but I believe it might be simpler than what you are doing. See if the code below helps you, I am making the balls fall with SKAction (you can do it with physics also), and checking when they go below zero, when I remove them.
private let ballSpeed : CGFloat = 400
private let ballRadius : CGFloat = 10
private func spawnBall(atPoint pos : CGPoint){
let b = SKShapeNode(circleOfRadius: ballRadius)
b.name = "ball"
b.position = pos
addChild(b)
b.run(SKAction.moveTo(y: -size.height, duration: TimeInterval((pos.y+size.height)/400)))
}
override func update(_ currentTime: TimeInterval) {
super.update(currentTime)
enumerateChildNodes(withName: "ball") { (node, _) in
if node.position.y < 0 {
node.removeFromParent()
}
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches{
let pos = touch.location(in: self)
spawnBall(atPoint: pos)
}
}
I have several programmatically made SKSpriteNode's. Some of them I want to move around, some I want to be static (have a fixed position). When adding physics to the nodes (need that to be able to do collision detection, right?) and set physicsBodyXXXX.dynamic = false they stay in the same position when moving other object over them. That's fine!
But, I'm still able to grab the node I want to be statically positioned, and move them around. How can I mask out the node I don't want to move in touches function? Or is there another solution?
Tried to find a property like static which made the node's position fixed, but can't find it...
Here's my code for auto generating nodes (in override func didMoveToView(view: SKView):
for Character in englishWord{
// Make letters:
let letterToMove = SKSpriteNode(imageNamed: "\(Character)")
//then setting size and position
var physicsBodyLetterToMove = SKPhysicsBody(rectangleOfSize: letterToMove.size)
physicsBodyLetterToMove.affectedByGravity = false
physicsBodyLetterToMove.allowsRotation = false
physicsBodyLetterToMove.dynamic = false
letterToMove.physicsBody = physicsBodyLetterToMove
self.addChild(letterToMove)
// Make empty boxes for the letters:
let letterRecBox = SKSpriteNode(imageNamed: "EmptyBox")
//then setting size and position
var physicsBodyLetterRecBox = SKPhysicsBody(rectangleOfSize: letterRecBox.size)
physicsBodyLetterToMove.affectedByGravity = false
physicsBodyLetterRecBox.dynamic = false
letterRecBox.physicsBody = physicsBodyLetterRecBox
self.addChild(letterRecBox)
}
So the touches func's:
var selected: [UITouch: SKNode] = [:]
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */
selected = [:]
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
selected[touch as UITouch] = nodeAtPoint(location)
}
}
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
for (touch, node) in selected{
if !contains([self], node){
let action = SKAction.moveTo(location, duration: 0.1)
node.runAction(SKAction.repeatAction(action, count: 1))
}
}
}
}
Any idea?
Setting dynamic to false will make the node unaffected by physics. SKActions and touch events are not considered physics so they will still affect your nodes that are not dynamic.
You could do something like:
YourSpriteNode.name = #"staticNode"; //Right after you create it
Then alter your touch method:
for (touch, node) in selected{
if !contains([self], node){
if(![node.name isEqualToString:#"staticNode"])
{
let action = SKAction.moveTo(location, duration: 0.1)
node.runAction(SKAction.repeatAction(action, count: 1))
}
}
}
The newly introduced if statement will prevent any node named "staticNode" from getting moved due to your SKActions. Other nodes will move as expected.
Thanx, that worked.
Only, I had to write it like this:
letterRecBox.name = "staticNode"
...
if !(node.name == "staticNode"){
...then do something...
}