Can you check if a SKSpriteNode is indirectly connected to another? - swift

I'm working on a game where the player moves around a space station, consisting of tiles (called parts, inheriting SKSpriteNode). One of these parts is the 'Source'.
Here is a helpful image.
In the game, asteroids may hit the space station, and cause tiles to be destroyed. When this happens, I need to check if all the tiles are still connected to the source. If they are not, they need to be removed from the station (can't have floating tiles!). Useful point: Each tile has a name, "T|Empty" or for the source, "T|Source". Also, the obtainConnectedTiles function just finds the tiles up, down, left, and right of the part.
What I've tried
func checkIfConnectedToSource(_ part: SpaceStationPart) -> Bool {
var foundSource = false
let splitArray1 = part.name?.components(separatedBy: "|")
if splitArray1?[1] == "Source" {
foundSource = true
}
while !foundSource {
let connections = part.obtainConnectedTiles(scene: self)
if connections.count != 0 {
for i in 0..<connections.count {
let splitArray = connections[i].name?.components(separatedBy: "|")
if splitArray?[1] != "Source" {
if checkIfConnectedToSource(connections[i]) {
foundSource = true
}
} else {
foundSource = true
}
}
} else {
print("Connections was 0!")
}
}
return foundSource
}
I tried this, but it threw "EXC_BAD_ACCESS" on the line when I find the components of the name, although I'm not sure why.
The tiles are also always in a grid, 100 points apart from each other.
So, the question boils down to: How can I check if an SKSpriteNode is connected to another SKSpriteNode, sometimes indirectly?

Related

Using swift built in partition to manage elements in an array

iOS 14, Swift 5.x
I watched this excellent WWDC from 2018
https://developer.apple.com/videos/play/wwdc2018/223/
And I wrote a shapes editor... and have been trying to use partition as Dave in the video says you should. I got the first three to work, but the last one I had to use a loop- cannot for the life of me figure out how to get it to work with partition.
Can someone see how I might do this?
The first method moves the selected object to the end of the list, works perfectly.
func bringToFrontEA() {
let subset = objects.partition(by: { $0.selected })
let selected = objects[subset...]
let unselected = objects[..<subset]
let reordered = unselected + selected
objects = Array(reordered)
}
The second method moves the selected object to the front of the list. Works prefectly.
func sendToBackEA() {
let subset = objects.partition(by: { !$0.selected })
let selected = objects[subset...]
let unselected = objects[..<subset]
let reordered = unselected + selected
objects = Array(reordered)
}
The third method moves the element just one element back in the list. Works perfectly.
func sendBackEA() {
if let i = objects.firstIndex(where: { $0.selected }) {
if i == 0 { return }
let predecessor = i - 1
let shapes = objects[predecessor...].partition(by: { !$0.selected })
let slice = objects[predecessor...]
let row = objects[..<predecessor]
let selected = Array(slice[..<shapes])
let unselected = Array(slice[shapes...])
objects = row + selected + unselected
}
}
The last method moves the element forward in the list, works perfectly... but unlike the other methods it will not scale as described in the WWDC video.
func bringForwardEA() {
let indexes = objects.enumerated().filter { $0.element.selected == true }.map{$0.offset}
for i in indexes {
if objects[i+1].unused {
return
}
objects.swapAt(i+1, i)
}
}
Objects is an array of shapes with a property indicating if it is selected or not. I want to exchange the loop in the last method by using a partition as I did in the first three. It needs to work for one or more selected shapes.
Looking at the WWDC video, it appears that what you are calling sendBackEA is what WWDC calls bringForward, and what you are calling bringForwardEA is what WWDC calls sendBack.
Just like how you move the first selected element forward one index (index decreases) in sendBackEA, then move all the other selected elements to immediately after that first selected element. bringForwardEA should do the reverse: move the last selected element backward one index (index increases), then move all the other selected elements to immediately before the last selected element. (See circa 19:10 in the video)
You seem to have confused yourself by trying to increase the indices of all the selected index by 1. This obviously cannot be done with a partition in general.
Also note that partition(by:) already modifies the collection, you don't need to get each partition, then recombine.
Your 4 methods can be written like this:
func bringToFrontEA() {
objects.partition(by: { $0.selected })
}
func sendToBackEA() {
objects.partition(by: { !$0.selected })
}
func sendBackEA() {
if let i = objects.indices.first(where: { objects[$0].selected }) {
if i == 0 { return }
let predecessor = i - 1
objects[predecessor...].partition(by: { !$0.selected })
}
}
func bringForwardEA() {
if let i = objects.indices.last(where: { objects[$0].selected }) {
if i == objects.indices.last { return }
let successor = i + 1
objects[...successor].partition(by: { !$0.selected })
}
}
Notice the symmetry between sendBackEA and bringForwardEA.

SKSpriteNode isn't responding when I try to hide it (Spoiler: SKAction timing issue)

OK, so I've been working on doing an RPG-style dialog box for a project, and while most of it is going smoothly, the one thing that's tripping me up right now is the little icon in the corner of the box to let you know there's more.
I tried to figure out how to get draw the shape, but not having any luck getting Core Graphics to draw triangles I decided to just use a PNG image of one instead. The code below shows everything relevant to how it's been set up and managed.
That being figured out, I'm now trying to get it to hide the marker when updating the box and show it again afterward. Here's what I've tried so far:
Method 1: Use .alpha = 0 to hide it from view during updates, restore with .alpha = 1
Method 2: Remove it from the node tree
Method 3: Place it behind the box background (located at .zPosition = -1)
The result has been consistent across all 3 methods: The triangle just stays in place, unresponsive when invoked.
class DialogBox: SKNode {
private var continueMarker = SKSpriteNode(imageNamed: "continueTriangle") // The triangle that shows in the lower-right to show there's more to read
init() {
/// Setup and placement. It appears in the proper position if I draw it and don't try to hide anything
continueMarker.size.width = 50
continueMarker.size.height = 25
continueMarker.position = CGPoint(x: ((width / 2) - (continueMarker.size.width * 0.9)), y: ((continueMarker.size.height * 0.9) - (height - margin)))
addChild(continueMarker)
}
func updateContent(forceAnimation: Bool = false) {
/// Determine what content to put into the box
hideContinueMarker()
/// Perform the content update in the box (which works as it should)
showContinueMarker()
}
func showContinueMarker() {
// continueMarker.alpha = 1 /// Method 1: Use .alpha to hide it from view during updates
// if (continueMarker.parent == nil) { // Method 2: Remove it from the tree
// addChild(continueMarker)
// }
continueMarker.zPosition = -2 /// Method 3: place it behind the box background (zPosition -1)
}
func hideContinueMarker() {
// continueMarker.alpha = 0 /// Method 1
// if (continueMarker.parent != nil) { /// Method 2
// continueMarker.removeFromParent()
// }
continueMarker.zPosition = 2 /// Method 3
}
}
OK, so while typing this one up I had some more ideas and ended up solving my own problem, so I figured I'd share the solution here, rather than pull a DenverCoder9 on everyone.
On the plus side, you get a look at a simple way to animate text in SpriteKit! Hooray!
In a final check to make sure I wasn't losing my mind, I added some print statements to showContinueMarker() and hideContinueMarker() and noticed that they always appeared simultaneously.
What's that mean? SKAction is likely at fault. Here's a look at the code for animating updates to the box:
private func animatedContentUpdate(contentBody: String, speaker: String? = nil) {
if let speaker = speaker {
// Update speaker box, if provided
speakerLabel.text = speaker
}
var updatedText = "" // Text shown so far
var actionSequence: [SKAction] = []
for char in contentBody {
updatedText += "\(char)"
dialogTextLabel.text = updatedText
// Set up a custom action to update the label with the new text
let updateLabelAction = SKAction.customAction(withDuration: animateUpdateSpeed.rawValue, actionBlock: { [weak self, updatedText] (node, elapsed) in
self?.dialogTextLabel.text = updatedText
})
// Queue up the action so we can run the batch afterward
actionSequence.append(updateLabelAction)
}
/// HERE'S THE FIX
// We needed to add another action to the end of the sequence so that showing the marker again didn't occur concurrent with the update sequence.
let showMarker = SKAction.customAction(withDuration: animateUpdateSpeed.rawValue, actionBlock: { [weak self] (node, elapsed) in
self?.showContinueMarker()
})
// Run the sequence
actionSequence.append(showMarker)
removeAction(forKey: "animatedUpdate") // Cancel any animated updates already in progress
run(SKAction.sequence(actionSequence), withKey: "animatedUpdate") // Start the update
}
In case you missed it in the big block there, here's the specific bit in isolation
let showMarker = SKAction.customAction(withDuration: animateUpdateSpeed.rawValue, actionBlock: { [weak self] (node, elapsed) in
self?.showContinueMarker()
})
Basically, we needed to add showing the triangle as an action at the end of the update sequence instead of just assuming it would occur after the update since the function was invoked at a later time.
And since all 3 methods work equally well now that the timing has been fixed, I've gone back to the .alpha = 0 method to keep it simple.

ARKit limit display distance of a node

I would like to create a node in sceneView, that is displayed at normal position in the scene, until user get too close or too far from it. Then it should be displayed at the same direction from the user, but with restricted distance. So far best I found is SCNDistanceConstraint, which limits this distance, but the problem is, that this constraint after it moved the node, this node stays in this new place. So for example, I want to limit the node to be displayed not closer then one meter from camera. I'm getting closer to the node, and it's being pushed away, but then when I get camera back, this node should return to it's original position - for now it stays where it was pushed. Is there some easy way to get such behavior?
Im not entirely sure I have understood what you mean, but it seems you always want your SCNNode to be positioned 1m away from the camera, but keeping its other x, y values?
If this is the case then you can do something like this:
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
//1. Get The Current Node On Screen & The Camera Point Of View
guard let nodeToPosition = currentNode, let pointOfView = augmentedRealityView.pointOfView else { return }
//2. Set The Position Of The Node 1m Away From The Camera
nodeToPosition.simdPosition.z = pointOfView.presentation.worldPosition.z - 1
//3. Get The Current Distance Between The SCNNode & The Camera
let positionOfNode = SCNVector3ToGLKVector3(nodeToPosition.presentation.worldPosition)
let positionOfCamera = SCNVector3ToGLKVector3(pointOfView.presentation.worldPosition)
let distanceBetweenNodeAndCamera = GLKVector3Distance(positionOfNode, positionOfCamera)
print(distanceBetweenNodeAndCamera)
}
I have added in part three, so you could use the distance to do some additional calculations etc.
Hope this points you in the right direction...
The answer above is not exactly what I need - I want object to be displayed like it was just placed in normal position, so I can get closer and farer to/from it, but limit how close/far I can get from that object. When I'm beyond that limit, object should start move to be always within given distance range from camera. Anyway I think I have found some right direction in this. Instead of assigning position, I'm creating a constraint that constantly updates position of my node to be either in given position if it's in given range from user, or if not, adjusts this position to fit in that range:
private func setupConstraint() {
guard let mainNodeDisplayDistanceRange = mainNodeDisplayDistanceRange else {
constraints = nil
position = requestedPosition
return
}
let constraint = SCNTransformConstraint.positionConstraint(inWorldSpace: true) { (node, currentPosition) -> SCNVector3 in
var cameraPositionHorizontally = (self.augmentedRealityView as! AugmentedRealityViewARKit).currentCameraPosition
cameraPositionHorizontally.y = self.requestedPosition.y
let cameraToObjectVector = self.requestedPosition - cameraPositionHorizontally
let horizontalDistanceFromCamera = Double((cameraToObjectVector).distanceHorizontal)
guard mainNodeDisplayDistanceRange ~= horizontalDistanceFromCamera else {
let normalizedDistance = horizontalDistanceFromCamera.keepInRange(mainNodeDisplayDistanceRange)
let normalizedPosition = cameraPositionHorizontally + cameraToObjectVector.normalizeHorizontally(toDistance: normalizedDistance)
return normalizedPosition
}
return self.requestedPosition
}
constraints = [constraint]
}
internal var requestedPosition: SCNVector3 = .zero {
didSet {
setupConstraint()
}
}
This starts to work fine, but I still need to find a way to animate this.

Zooming Weirdness in SpriteKit

I am building an app using Swift and SpriteKit. The app loads an SKSpriteNode that is exactly the size of the screen and places it to exactly cover the screen. However, the user can move and zoom this SKSpriteNode as much as they like. I care about the absolute position of the SKSpriteNode because if the node is moved too much I like to bring it back to center, or "snap" it to the edges of the screen. (The SKSpriteNode will eventually hold a map).
When the node has a scale of exactly 1.0, the position values work as I expect them to. However, when I zoom the node (using a pinch gesture) and the scale grows above 1.0, the position values do not make sense. Here are some images to illustrate what I mean.
Here, you can see that the un-zoomed node has the exact same dimensions and position as the scene it is placed in. X and Y are both 0 for each the Map as well as the scene:
However, as you can see in this next image, when I zoom the Map to be a larger SKSpriteNode, and move it so that the lower left corner of the Map is exactly placed in the lower left corner of the screen, the X and Y values of the zoomed node are off. I would expect the X to be 0 and the Y to be 0 but they are not.
What is happening here, and how do I calculate the absolute position of a zoomed node? And when I say "absolute" position, perhaps what I mean is the position relative to the scene?? Anyway, your thoughts are appreciated.
I figured this out, although it wasn't easy. It turned out to be a case of "AnchorPoint Dorkitis". The primary symptom of this condition is writing code based on a false assumption, and in my case, the assumption that did me in was this:
The (X,Y) point of my node was the lower left corner of the node. (false)
I knew all along that I set my AnchorPoints (of both the scene as well as the SKSpriteNode) to (x: 0.5, y: 0.5) but it had not sunk in for me yet that this also was altering where on the screen the (X,Y) coordinate actually lived (hint: the center).
For those who are interested, I worked out a nice set of computed properties to return the X, Y, height, width, top, bottom, left, and right values for my scene and my SKSpriteNode. I share you them, here:
var map_x: Int {
get {
return Int(self.position.x)
}
}
var map_y: Int {
get {
return Int(self.position.y)
}
}
var map_h: Int {
get {
return Int(self.frame.height)
}
}
var map_w: Int {
get {
return Int(self.frame.width)
}
}
var map_t: Int {
get {
return Int(self.position.y + self.frame.height/2)
}
}
var map_b: Int {
get {
return Int(self.position.y - self.frame.height/2)
}
}
var map_l: Int {
get {
return Int(self.position.x - self.frame.width/2)
}
}
var map_r: Int {
get {
return Int(self.position.x + self.frame.width/2)
}
}
var scene_x: Int {
get {
return Int((scene?.position.x)!)
}
}
var scene_y: Int {
get {
return Int((scene?.position.y)!)
}
}
var scene_h: Int {
get {
return Int((scene?.frame.height)!)
}
}
var scene_w: Int {
get {
return Int((scene?.frame.width)!)
}
}
var scene_t: Int {
get {
return Int((scene?.position.y)! + (scene?.frame.height)!/2)
}
}
var scene_b: Int {
get {
return Int((scene?.position.y)! - (scene?.frame.height)!/2)
}
}
var scene_l: Int {
get {
return Int((scene?.position.x)! - (scene?.frame.width)!/2)
}
}
var scene_r: Int {
get {
return Int((scene?.position.x)! + (scene?.frame.width)!/2)
}
}
I could have made them CGFloat values and not wrapped them in Int() but I needed ints elsewhere in the code so I figured may as well do that here. I think the next step for me is to figure out how to do this in fewer lines of code :)

one score at a time, update method

I'm creating a simple game with SpriteKit (mostly for learning), and I got a question about score adding. some background: I'm checking if a sprite (SKShapeNode) contains another one, if true, I'm checking their color, if it is the same color, the player should get 1 score. I wrote this function:
func onMatch(){
for ring in mColorRings {
if(mPlayer.contains(ring.position)){
if mPlayer.fillColor.isEqual(ring.fillColor) {
score += 1
mScoreLbl.text = "\(score)"
}
}
}
}
which works, the problem is, I'm calling this function inside the update method. as the update method runs a lot, it calls my function a lot of time and as long as mPlayer contains ring it is adding 1 score to the player. How can I avoid that ?
This depends on your game mechanics. If the ring is supposed to give you a score one time then disappear you can safely remove it within that if test. If you want the ring to stay put and maybe be reused later you can add a boolean to the ring class called something like "scoreGiven" and redo your if test to something like this:
func onMatch(){
for ring in mColorRings {
if !ring.scoreGiven{
if(mPlayer.contains(ring.position)){
if mPlayer.fillColor.isEqual(ring.fillColor) {
score += 1
mScoreLbl.text = "\(score)"
ring.scoreGiven = true
}
}
}else if(!mPlayer.contains(ring.position)){
ring.scoreGiven = false
}
}
This is just an example, but note the "not"s in the updated if statements
Ok, so this is what i suggest:
var playerPassed = false
func onMatch(){
for ring in mColorRings {
if(mPlayer.contains(ring.position)) {
if ((mPlayer.fillColor.isEqual(ring.fillColor)) && playerPassed == false) {
score += 1
mScoreLbl.text = "\(score)"
playerPassed = true
}
}
}
}
You're creating a bool and you check if that is false(default) and if so, the block executes and when it does, the bool is set to true and the condition will be false and no longer return true.