I'm learning to create a game using SprikeKit and the game has something to do with sushi train.
I have something like 20 dishes that follows along a rectangular path in order to simulate the look that all dishes are on a moving conveyor belt.
I've tried with a scene editor to place a couple sprite nodes and made them move on a square path like so.
let dish_1 = self.childNode(withName: "dish_1");
let dish_2 = self.childNode(withName: "dish_2");
let square = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 200, height: 200));
let followSquare = SKAction.follow(square.cgPath, asOffset: true, orientToPath: false, duration: 5.0);
dish_1?.run(SKAction.sequence([followSquare]))
dish_2?.run(SKAction.sequence([followSquare]))
At the moment, the two dishes are using the same square and the square position is relative to each dish so they look like they are on two different rectangular path as their starting point is different.
Is this the reasonable way to simulate the look I'm after?
The reason I'm asking this is that to make all dishes look like they are moving on a same path, I will need to tweak x, y positions of each dish and it will be like 20 dishes.
I was wondering if it would be possible to use physics kit and let one rectangle node makes the all dishes move along the path as long as they are within the rectangle node area.
Well I would have probably done it in a similar way, and I think using SKAction.follow is a solution here.
But there are a few improvements that you could do in your code. The first is that you don't need to write your SKAction as a sequence because it is composed of only one action, so you could simplify your code to this:
let squarePath = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 200, height: 200))
let followAction = SKAction.follow(path: squarePath.cgPath, duration: 5.0)
dish?.run(followAction)
Now an other thing that is important is, in order to save your memory usage, instead of recreating a new dish every time (20 times), you could just use copy() on your SKSpriteNode to create many different copies of it. Depending on what you need to achieve, you can of course custom each copy, changing its color, or position, etc. This would be way more efficient.
You can create a copy of your SKSpriteNode like so:
import GameplayKit
if let dishCopy = dish?.copy() as? SKSpriteNode {
dishCopy.position = CGPoint(x: GKRandomDistribution(lowestValue: 0, highestValue: 200).nextInt(), y: GKRandomDistribution(lowestValue: 0, highestValue: 500).nextInt())
self.addChild(dishCopy)
}
For the position of each dish copy, you can of course adjust to your own values, but here I used GameplayKit and its very useful random values generator.
Update:
If you want to set a unique name to each of your dishes while still using copy(), you have several options.
One possibility would be to generate a unique name using a counter that you would increment, another possibility would be using currentTime in the update method for example.
But another more elegant way of doing this, and to keep tracks of where are all your copies, is to create an array that will store them all.
At the top of your scene subclass, declare your dishes array like so:
var dishes = [SKSpriteNode]()
At the moment, it's still empty, so you need to add each copy to the array at the time you create each of them:
// Create (safely) a dish copy:
if let dishCopy = dish?.copy() as? SKSpriteNode {
// Add the dish copy to the dishes array:
dishes.append(dishCopy)
// And add the dish copy to the world node:
worldNode?.addChild(dishCopy)
}
If you want to create many copies like this, you could use a for loop, or you could play with the update(_ currentTime:) method by specifying that you want to create a new sushi dish every 5 seconds for example :).
And well, if at any time you need to access any of these copies, just do so by accessing the dishes array.
For example, this is how you would remove all the copies (you would have to deal with the original dish if you added it to the scene though) :
// And just iterate through all of your dish copies:
for dish in dishes {
dish.removeFromParent()
// Do whatever you need here :)
}
It's pretty straightforward and it allows you to control perfectly all the objects and/or copies that you added to your worldNode.
Just let me know if you have any other questions I'd be glad to help !
By the way tonight I will actually go to the sushi restaurant you're talking about in your question, the one with the little train.. Nice coincidence ! :D
asOffset does not seem correct. According to the documentation this is exactly the opposite behavior you want.
If YES, the points in the path are relative offsets to the node’s starting position. If NO, the points in the node are absolute coordinate values.
Setting this to true would give each dish their own rectangle path to follow with coordinates relative to their starting position, as you describe in the question:
v First dish
v Second dish
O--O-----.---.
| | | |
| | | |
'--'-----'---'
^^ Second dish's path
^^ First dish's path
Instead, I would imagine you need to set up the conveyor belt in absolute coordinates, then initialize each dish's starting point to be different - again in absolute coordinates - and finally have each dish follow said path:
v First dish (starting point x=0; y=0)
v Second dish (starting point x=20; y=0)
O--O-----.
| |
| |
'--------'
^^ Single, absolute path
(If you're simulating a sushi conveyor belt, they move clockwise and the nomenclature "first" and "second" above are technically backwards. Swapping this is left as an exercise to the reader. :-P)
Related
Objective: There is a SCNCylinder object in my scene. I want to be able to drag one end of the cylinder and rotate the cylinder in any direction, while keeping the other end in the same position.
Currently, I am calling localRotate(by: SCNQuaternion) to first rotate the node, then calculate the position offset needed to move the cylinder so that the other end can go back to its original position.
How can I achieve the objective in one step instead of what I am doing now?
The pivot property is what you're looking for. Or, since modern SceneKit often works better / makes nicer Swift / interoperates easier with ARKit when you use SIMD types, the simdPivot property.
Note this bit in the docs:
Changing the pivot transform alters these behaviors in many useful ways. You can:
Offset the node’s contents relative to its position. For example, by setting the pivot to a translation transform you can position a node containing a sphere geometry relative to where the sphere would rest on a floor instead of relative to its center.
Move the node’s axis of rotation. For example, with a translation transform you can cause a node to revolve around a faraway point instead of rotating around its center, and with a rotation transform you can tilt the axis of rotation.
Similarly, for a cylinder, you can make its pivot a transform matrix that translates the origin by half its height, giving it an "anchor point" (for position and rotation changes) at one end instead of in the center. Something like this (untested):
let cylinder = SCNCylinder(radius: /*...*/, height: /*...*/)
let cylinderNode = SCNNode(geometry: cylinder)
cylinderNode.simdPivot = float4x4(translation: cylinder.height / 2)
extension float4x4 {
init(translation vector: float3) {
self.init(float4(1, 0, 0, 0),
float4(0, 1, 0, 0),
float4(0, 0, 1, 0),
float4(vector.x, vector.y, vector.z, 1))
}
}
More generally, whenever you're using a scene-graph / transform-hierarchy based graphics framework, any time you find yourself doing math depending on one transform (rotation, translation, etc) to affect another, it's always good to check for API that can do that math for you — because doing that kind of math is what transform hierarchy is all about.
And if there's not an API fairly specific to what you need, remember that the hierarchy itself is good for making dependent transforms. For example, if you want one node to follow a circular orbit around another, you don't need to set its position using sines and cosines... just make it the child of another node, and rotate that other node.
In this case, pivot is a convenience equivalent to using the node hierarchy. You could just as well create an intermediate node and move the cylinder within it (something like this):
let cylinder = SCNCylinder(radius: /*...*/, height: /*...*/)
let cylinderNode = SCNNode(geometry: cylinder)
let offsetNode = SCNNode()
offsetNode.addChildNode(cylinderNode)
cylinderNode.simdPosition.y = cylinder.height / 2
offsetNode.position = /*...*/ // set world-space position of end of cylinder
offsetNode.eulerAngles.x = /*...*/ // rotate cylinder around its end
I experienced that for some NSBezierPaths SCNShape seems to be unable to draw a shape.
The path is created only using line(to:).
//...set up scene...
//Create path (working)
let path = NSBezierPath()
path.move(to: CGPoint.zero)
path.line(to: NSMakePoint(0.000000, 0.000000))
path.line(to: NSMakePoint(0.011681, 0.029526))
// more points ...
path.close()
// Make a 3D shape (not working)
let shape = SCNShape(path: path, extrusionDepth: 10)
shape.firstMaterial?.diffuse.contents = NSColor.green
let node = SCNNode(geometry: shape)
root.addChildNode(node)
For verifying that the general process of creating a SCNShape is correct, I also drew a blue shape that only differs by having different points. The blue shape gets drawn, the green shape doesn't.
You can find a playground containing the full example here. In the example you should be able to see a green and a blue shape in assistant editor. But only the blue shape gets drawn.
Do you have any idea why the green shape is not shown?
The short story: your path has way more points than it needs to, leading you to unexpected, hard to find geometric problems.
Note this bit in the documentation:
The result of extruding a self-intersecting path is undefined.
As it turns out, somewhere in the first 8 or so points, your "curve" makes enough of a turn the wrong way that the line closing the path (between the first point in the path 0,0, and the last point 32.366829, 29.713470) intersects the rest of the path. Here's an attempt at making it visible by excluding all but the first few points and the last point from a playground render (see that tiny little zigzag in the bottom left corner):
And at least on some SceneKit versions/renderers, when it tries to make a mesh out of a self-intersecting path it just gives up and makes nothing.
However, you really don't need that many points to make your path look good. Here it is if you use 1x, 1/5x, and 1/10x as many points:
If you exclude enough points overall, and/or skip the few at the beginning that make your curve zag where it should zig, SceneKit renders the shape just fine:
Some tips from diagnosing the problem:
When working with lots of coordinate data like this, I like to use ExpressibleByArrayLiteral so I can easily build an array of lots of points/vectors/etc:
extension CGPoint: ExpressibleByArrayLiteral {
public init(arrayLiteral elements: CGFloat...) {
precondition(elements.count == 2)
self.init(x: elements.first!, y: elements.last!)
}
}
var points: [CGPoint] = [
[0.000000, 0.000000],
[0.011681, 0.029526],
// ...
]
That gets me an array (and a lot less typing out things like NSPointMake over and over), so I can slice and dice the data to figure out what's wrong with it. (For example, one of my early theories was that there might be something about negative coordinates, so I did some map and min() to find the most-negative X and Y values, then some more map to make an array where all points are offset by a constant amount.)
Now, to make paths using arrays of points, I make an extension on NSBezierPath:
extension NSBezierPath {
convenience init(linesBetween points: [CGPoint], stride: Int = 1) {
precondition(points.count > 1)
self.init()
move(to: points.first! )
for i in Swift.stride(from: 1, to: points.count, by: stride) {
line(to: points[i])
}
}
}
With this, I can easily create paths from not just entire arrays of points, but also...
paths that skip parts of the original array (with the stride parameter)
let path5 = NSBezierPath(linesBetween: points, stride: 5)
let path10 = NSBezierPath(linesBetween: points, stride: 10)
(This is handy for generating playground previews a bit more quickly, too.)
paths that use some chunk or slice of the original array
let zigzag = NSBezierPath(linesBetween: Array(points.prefix(to:10)) + [points.last!])
let lopOffBothEnds = NSBezierPath(linesBetween: Array(points[1 ..< points.count < 1]))
Or both... the winning entry (in the screenshot above) is:
let path = NSBezierPath(linesBetween: Array(points.suffix(from: 10)), stride: 5)
You can get a (marginally) better render out of having more points in your path, but an even better way to do it would be to make a path out of curves instead of lines. For extra credit, try extending the NSBezierPath(linesBetween:) initializer above to add curves by keeping every nth point as part of the path while using a couple of the intermediary points as control handles. (It's no general purpose auto trace algorithm, but might be good enough for cases like this.)
In no way does this compare to Rikster's answer, but there is another way to prevent this kind of problem. It's a commercial way, and there's probably freeware apps that do similar, but this is one I'm used to using, that does this quite well.
What is 'this' that I'm talking about?
The conversion of drawings to code, by an app called PaintCode. This will allow you to see your paths and be sure they have none of the conflicts that Rickster pointed out are your issue.
Check it out here: https://www.paintcodeapp.com/
Other options are listed in answers here: How to import/parse SVG into UIBezierpaths, NSBezierpaths, CGPaths?
I have found a few other questions and answers similar to this, but none of them quite work perfect for me.
My vision is to have a horizontal scrollable view at the bottom of the screen where I can scroll through what will look like cards in a hand. Ideally, I could eventually make the card in the middle scaled up a bit and give it a highlighted look to show the user which card is selected. Even better would be if I could figure out how to keep the scroll view resizing to fir the number of sprites (cards) in the view.
Anyways, I am still very new to XCode and Swift, so it is hard for me to take what I find and change it. But, I am hoping to learn fast.
What I understand so far is that a UIScrollView could overlay the scene and with a moveable spritenode I could scroll through the view. The view would then translate the coordinates somehow to the SpriteKit Scene to move the sprites that will look like they are in the view. I think that's how it works. Any help would be great. I am pretty stuck. <3
You have to make your own logic that takes place in touchesMoved() using a global/member variable.
Unfortunately, a lot of gamedev and SK is math and logic.. You have to come up with your own problems and solutions.. There is no manual because the possibilities in programming and Swift are endless :)
Moving the cards:
Basically, you compare each touch location to the last one, and this becomes a "delta value" that you can use to perform actions.
Example, if I touch in the center of the screen, my touch location is 0,0 (or whatever your anchorpoint is set to). If I move my finger right, then I'm now at say 25, 0... This creates a "delta value" of +25x.
With that delta value, you can perform various actions such as moveBy for all the cards... so if I have a deltaX of +25, then I need to move all of the card nodes to the right (by a certain amount that you will determine according to your preferences). If I have a deltaX of -25, I move the cards to the left by a certain amount.
Where you do the actual moving is up to you--you could put a function in update() or touchesMoved() that constantly moves the cards a certain direction at a certain rate of that deltaX value..
Ok that was a mouthful... Maybe this will help:
for touch in touches {
myGlobalDeltaX = myDeltaXFunc(currentTouch: touch)
myMoveFunc(cards: allTheCards, byDeltaX: myGlobalDeltaX)
- You can search on how to make a Delta function, but it really is just the same thing from Algebra.
- myMoveFunc can be something as simple as iterating through all of your card nodes then running .moveBy on them at the same time.
Middle detection:
To detect which card is in the center, you would put in touchesEnded() or update() a call to check the name / identity of the node in the center of the screen... so something like
// `self` here refers to your GameScene class' instance, which is just an `SKScene` object
let centerX = self.frame.midX
let centerY = self.frame.midY
let center = CGPoint(x: centerX, y: centerY)
let centerNode = self.nodes(at: center)
You would obviously want to change centerX and centerY to wherever it is you want the middle card to be :) Right now, this is just in the dead-center of the screen.
Once you have a centerNode, you would then just need to do whatever function you have created to "select" it.
let selectedCard = centerNode
mySelectionFunc(middleCard: selectedCard)
This may look like a lot, but I drew out the steps to make understanding it a bit easier.. You can do all of this in one line if desired.
mySelectionFunc(middleCard: self.nodes(at: CGPoint(x: self.frame.x, y: self.frame.y)))
Hope this helps some!
I am trying to create a basic snake game using Swift and Spritekit. I have a sprite moving on the screen and when i swipe it starts moving in the swipe direction. I do this in the update method where I change the position of the sprite based on the swipe direction, a set speed and a fixed duration.
e.g. direction.x * blockMovePerSecond * CGFloat(actionDuration)
I have the sprites following each other, however, as soon as i swipe and the first sprite changes direction, the one following it moves diagonally instead of first on the x-axis and then the y-axis like a normal snakes game.
I tried the following options:
Tried keeping the distance that the sprite moves on each update equal
to the distance between the two sprites. However, the sprite runs off
the screen when i do that.
Made the first sprite create a path for the rest of the sprites to follow. However, the second sprite
runs off the screen and never shows up again. I think the problem is
because the path is open.
Here's my move method code for the sprites following the first sprite (snake head):
{
var temp:NSValue = lastNodeLocation // the lastnodelocation is from first sprite location
var tempNodeDirection = lastNodeDirection
let actionDuration = 1.0
let distanceToMoveThisFrame = tempNodeDirection.CGPointValue() * blockMovePerSecond * CGFloat(actionDuration)
var currentPosition = NSValue(CGPoint: blocksOfAlphabets[i].position)
let beforeDistance = CGPoint(x: temp.CGPointValue().x - currentPosition.CGPointValue().x, y: temp.CGPointValue().y - currentPosition.CGPointValue().y)
lastNodeLocation = NSValue(CGPoint: blocksOfAlphabets[i].position)
// move node to new location
var moveAction = SKAction.moveTo(temp.CGPointValue(), duration: 1.0)
node.runAction(moveAction)
}
Can somebody please help?
Thanks
Shuchi
Well your problem is that the runAction-methods don't wait to complete. So if you call another runAction-method while a sprite is already running an action, the new action will be started immediately.
You can work with sequences to finish the x-axis first: SKAction:sequence
I think the easiest thing to do here is to keep an array of snake sprite components (let's call these sprites a "Rib").
Every 5 seconds add a new Rib to the end of the array to make the snake grow.
Then, each half second in the update() method (or however long you want it to be):
check for any swipes that have happened since the last update and change direction of the first rib (the snake head) in the direction of the swipe.
now run through the snake array in reverse and set the location of node (n) to the node position of node (n-1).
There are other ways you could do it but I think this would be the easiest.
Is there a way of getting the closest nodes to a node?
I'm just about to write a method to iterate all nodes and calculate distances etc... but wondered if there is a better way?
I have 30 nodes and need the 2 nearest nodes to each of the 30 nodes (if that makes sense).
As of iOS 10, you can use the Spatial Partitioning features of GampelayKit. In 2D either GKQuadtree or GKRTree depending on your needs.
From the docs:
Quadtrees and R-trees have different performance tradeoffs for different tasks: quadtrees can be faster when objects are more uniformly distributed in space or when their positions change frequently, and R-trees can be faster when searching for all objects in a given region.
Add your enemies to the tree:
let minX = Float(enemy.frame.minX)
let minY = Float(enemy.frame.minY)
let maxX = Float(enemy.frame.maxX)
let maxY = Float(enemy.frame.maxY)
var enemiesRTree = GKRTree(maxNumberOfChildren: 3)
enemiesRTree.addElement(enemy,
boundingRectMin: vector2(minX, minY),
boundingRectMax: vector2(maxX, maxY),
splitStrategy: GKRTreeSplitStrategy.linear)
Then you can search by area.
let enemiesInProximity = enemiesRTree.elements(
inBoundingRectMin: vector2(0, 0),
rectMax: vector2(100, 100))
You can then create the search area e.g. relative to the player's position.
I believe that your approach is the most appropriate for this situation. If you stored all the nodes in an array style there may be a relatively efficient way to do what you mentioned above.
In any situation, you still may be tasked with having three nodes closest. If this is not a problem, you use a method that takes a nodes position and then use for loop to project "invisible circles" of increasing radius until a circle finally contains exactly 2 nodes. Then just return those nodes.