Zooming Weirdness in SpriteKit - swift

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 :)

Related

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

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?

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.

GPUImage2 use SolidColorGenerator as a RectangleGenerator

Similar to the simpleVideoFilter and face detection, I would like to highlight a portion of my GPUImage2 video feed with colored rectangles. The SolidColorGenerator seems like a good start with two exceptions. First, I am unsure how to transpose the SolidColorGenerator output to the right place on my original image. Second, I am unsure how to add N rectangles to my original image. In the mean time I have had success with the LineGenerator but I think a rectangle would look better.
Bonus:
Rectangles with rounded corners and a single pixel border.
What worked for me was writing a RectangleGenerator. The key to the new RectangleGenerator was to draw GL_TRIANGLES.
glDrawArrays(GLenum(GL_TRIANGLES), 0, GLsizei(positions.count) * 2)
One optimization that could be made would be to use GL_Elements and GL_TRIANGLE_STRIP, though I was not able to figure that out.
The full solution should support border, borderWidth and cornerRadius. For now simple rectangles will suffice. Full code below.
let vertex = "attribute vec4 position; void main(){ gl_Position = position; }"
let fragment = "uniform lowp vec3 fillColor; void main(){ gl_FragColor = vec4(fillColor, 1.0); }"
public class RectangleGenerator: ImageGenerator {
let rectangleShader:ShaderProgram
var uniformSettings = ShaderUniformSettings()
public var fillColor:Color = Color.green { didSet { uniformSettings["fillColor"] = fillColor } }
public override init(size:Size) {
rectangleShader = crashOnShaderCompileFailure("RectangleGenerator"){try sharedImageProcessingContext.programForVertexShader(vertex, fragmentShader:fragment)}
super.init(size:size)
({fillColor = Color.green})()
}
public func renderRectangles(_ positions:[Position]) {
guard positions.count > 3 else { return }
imageFramebuffer.activateFramebufferForRendering()
rectangleShader.use()
uniformSettings.restoreShaderSettings(rectangleShader)
clearFramebufferWithColor(Color.transparent)
guard let positionAttribute = rectangleShader.attributeIndex("position") else { fatalError("A position attribute was missing from the shader program during rendering.") }
let convertedPositions = positions.flatMap{$0.toGLArray()}
glVertexAttribPointer(positionAttribute, 2, GLenum(GL_FLOAT), 0, 0, convertedPositions)
glBlendEquation(GLenum(GL_FUNC_ADD))
glBlendFunc(GLenum(GL_ONE), GLenum(GL_ONE))
glEnable(GLenum(GL_BLEND))
glDrawArrays(GLenum(GL_TRIANGLES), 0, GLsizei(positions.count) * 2)
glDisable(GLenum(GL_BLEND))
notifyTargets()
}
}
Happy Coding

Very slow minesweeper recursive algorithm in Swift

I'm working with Swift 3 and Xcode.
I'm creating an iOS game that is basically a Minesweeper, but there are no squares but hexagons, so each hexagon can have up to 6 mines in their surrounding.
I created a recursive algorithm, so that when the player touches an hexagon, if it's not a bomb, then it call a recursive function called "reveal" which :
- if one ore more mine in the surrounding and the touched hexagon is still hidden (by hidden I mean we don't know if it's a mine or not), reveal the hexagon & set the number of surrounding mine's label, and stop the function
- if no mine in the surrounding, for each nearby hexagon that is hidden, call the reveal function.
So here's what my code looks like :
class Hexagon: SKShapeNode
{
var mine: Bool
var hide: Bool
var proximityMines: Int
init(mine: Bool = false, proximityMines: Int = 0, hide: Bool = true)
{
self.mine = mine // if it's a mine
self.proximityMines = proximityMines // number of surrounding mines (that I calculated using a function after I generated the level)
self.hide = hide // if the hexagon is still hidden
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
func reveal(hexagon: Hexagon)
{
if hexagon.proximityMines == 0 && hexagon.hide == true // if there are no mines in the surrounding
{
hexagon.hide = false // we update the value of this hexagon
setNumberLabel(hexagon: hexagon) // we set the .proximityMines number as a label (here 0)
for proxyHexagon in proximityHexagons(hexagon: hexagon) // for each surrounding hexagon ...
{
if proxyHexagon.hide == true // ... that is still hidden
{
reveal(hexagon: proxyHexagon) // we call this function again
}
}
}
else if hexagon.proximityMines != 0 && hexagon.hide == true // else if there are mines in the surrounding
{
hexagon.hide = false // update
setNumberLabel(hexagon: hexagon) // set label
}
}
the proximityHexagons(hexagon: Hexagon) function returns an array containing all surrounding hexagons of a given hexagon.
So I really checked my algorithm again and again, and I really think it's the good one.
But the fact is that when I create a level with 0 or a really low amount of mine, and I click on an hexagon, it takes something like 2 seconds for the recursive function to update all the empty hexagons.
My map contains more or less 260 hexagons, and I debugged the number of calls of reveal() and it's about the same amount.
So why is it taking so much time ? I don't think the iPhone 6 can't handle this amount of operations ! I tried it on my iPhone, not an emulator.
Do you have any idea ?
Ok I've been thinking about this because it sounds like a fun problem. I didn't look up any minesweeper solvers, so I might be way out in left field, but here is how I would approach your problem.
First you have to give every mine an index, and you need to know the pattern of that index such that you can do a little math to get the surrounding indices of every mine. If the rows have identical numbers, and the numbering is sequential across rows, then the surrounding indices are:
[index - 1, index + 1,
index - rowCount, index - rowCount - 1,
index + rowCount, index + rowCount + 1]
Then I would make a class that holds a set of all the safe spots on the map that you had when you built the puzzle. I'll call it SafetyManager.
class SafetyManager {
var safeSpots: Set<Int> = all your safe spots
func indices(surrounding index: Int) -> Set<Int> {
return [index - 1, index + 1,
index - rowCount, index - rowCount - 1,
index + rowCount, index + rowCount + 1]
}
func safePlaces(around hexagon: Int) -> Set<Int> {
let allIndices = indices(surrounding: hexagon)
let safe = allIndices.intersection(safeSpots)
safeSpots.subtract(safe)
return safe
}
}
It's got two important functions, one calculates the surrounding indices, the second filters the safe spots. I'm using sets so we can quickly determine the intersection between the safe spots and the surrounding spots.
Next we need a class that would be instantiated when a move is made so we can do the recursion. Lets call it CheckManager.
class CheckManager {
var checked : [Int]
var unchecked : Set<Int>
init(firstHex: Hexagon, surroundingSafeSpots: Set<Int>) {
checked = [firstHex.index]
unchecked = surroundingSafeSpots
}
func nextUnchecked() -> Int? {
guard !unchecked.isEmpty else { return nil }
let next = unchecked.removeFirst()
checked += [next]
return next
}
func pleaseTake(these indices: Set<Int>) {
unchecked.formUnion(indices)
}
}
You initialize it with your first hexagon, or hex index, and the surrounding safespots that the safety manager would give you, if you get no safe spots from the SafetyManager, no need to instantiate.
It keeps a set of checked spots and unchecked spots. Two important functions, the second you use to give it newly acquired safe spots from the safety manager to be added to the unchecked list. The other returns an optional Int? of the next safe spot to check the surroundings of.
Then to do the recursion, something like this..
func check(spot: Hexagon) {
let safe = safetyMan.safePlaces(around: spot.index)
guard safe.count > 0 else { .. }
let checkMan = CheckManager(firstHex: spot, surroundingSafeSpots: safe)
while let i = checkMan.nextUnchecked() {
let safeSpots = safetyMan.safePlaces(around: i)
checkMan.pleaseTake(these: safeSpots)
} // goes until unchecked is empty
for spot in checkMan.checked {
// get the hex and reveal
}
}
You could keep a dictionary of [Int: Hexagon] to quickly grab the hex for a given index. I haven't tested this so I'm not sure if it works well, or at all or has some improper syntax. It would also probably be a lot faster to use multithreading. Fun problem. Good luck.
Okay, I managed to solve my problem.
The problem was the proximityHexagons function that was taking a lot of time. In fact, each time I called this function, he made 6 complex calculations and added the surrounding hexagons in an array, so it was taking a lot of time.
Here's what it looked like :
func proximityHexagons(hexagon: Hexagon) -> Array<Hexagon>
{
var array = [Hexagon]()
var nodeArray = [[Hexagon]]()
nodeArray.append(nodes(at: CGPoint(x: hexagon.position.x, y: hexagon.position.y + hexagon.height)).filter({$0 is Hexagon}) as! [Hexagon])
nodeArray.append(nodes(at: CGPoint(x: hexagon.position.x + hexagon.width * 3/4, y: hexagon.position.y + hexagon.height / 2)).filter({$0 is Hexagon}) as! [Hexagon])
nodeArray.append(nodes(at: CGPoint(x: hexagon.position.x + hexagon.width * 3/4, y: hexagon.position.y - hexagon.height / 2)).filter({$0 is Hexagon}) as! [Hexagon])
nodeArray.append(nodes(at: CGPoint(x: hexagon.position.x, y: hexagon.position.y - hexagon.height)).filter({$0 is Hexagon}) as! [Hexagon])
nodeArray.append(nodes(at: CGPoint(x: hexagon.position.x - hexagon.width * 3/4, y: hexagon.position.y - hexagon.height / 2)).filter({$0 is Hexagon}) as! [Hexagon])
nodeArray.append(nodes(at: CGPoint(x: hexagon.position.x - hexagon.width * 3/4, y: hexagon.position.y + hexagon.height / 2)).filter({$0 is Hexagon}) as! [Hexagon])
// first, for each 6 directions, I'm adding in an array every nodes that are Hexagon, and then adding all of theses arrays in another bigger one
for node in nodeArray // for each hexagon array in the big array
{
if node.count != 0 // if there is an hexagon
{
array.append(node.first!) // we set the hexagon in the final array
}
}
return array // we return the array containing all surrounding hexagons
}
I prefer checking the surrounding hexagons with the nodes(at: Point) function because my levels aren't always regular maps, they can have a weird positioning and twiz_'s func indices(surrounding index: Int) function could not work.
So I kept my function, but I call it once at the beginning of the level and store in a new variable in my hexagon class all the surrounding hexagons of each hexagon:
class Hexagon: SKShapeNode
{
var mine: Bool
var hide: Bool
var proximityMines: Int
var proxyHexagons: [Hexagon] // here
init(mine: Bool = false, proximityMines: Int = 0, hide: Bool = true, proxyHexagons: [Hexagon] =
[Hexagon]())
{
self.mine = mine
self.proximityMines = proximityMines
self.hide = hide
self.proxyHexagons = proxyHexagons
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
And then, in the reveal function, instead of calling the proximityHexagons function, I use the .proxyHexagons array of the hexagon, like this :
func reveal(hexagon: Hexagon)
{
if hexagon.proximityMines == 0 && hexagon.hide == true
{
hexagon.hide = false
setNumberLabel(hexagon: hexagon)
for proxyHexagon in hexagon.proxyHexagons // here
{
if proxyHexagon.hide == true
{
reveal(hexagon: proxyHexagon)
}
}
}
else if hexagon.proximityMines != 0 && hexagon.hide == true
{
hexagon.hide = false
setNumberLabel(hexagon: hexagon)
}
}
And now my function is way faster, I manage to reveal all 260 hexagons in 0.001 secs instead of the old 2.81 secs.

Swift Enumerate out of Range

I'm working on making a tile-based adventure game and I'm trying to generate tile maps. I put together some code that removes inland tiles from the list of possible new tile locations. I have a method that detects if a tile is coastal.
for (index, tile) in landTiles.enumerate() {
let coastal = isCoastal(tile.y, x: tile.x)
if coastal {
coastTiles.append(tile)
} else {
print(landTiles.count)
print(index)
landTiles.removeAtIndex(index)
}
}
When I run this code I'm getting an error: index out of range message. I believe this is because the indexes are getting confused when I delete an item from landTiles. How could I fix this?
You are removing elements from an array while you enumerate it.
And this is an anti-pattern.
What should you do instead?
The Tile class
First of all, the Tile class should have a isCoastal computed property. Something like this
class Tile {
let x: Int
let y: Int
var isCoastal: Bool { /* your logic goes here */ }
init(x:Int, y:Int) {
self.x = x
self.y = y
}
}
Filtering
Now given an array of Tile(s)
var landTiles: [Tile] = ...
you can extract the ones having isCoastal true
let coastTiles = landTiles.filter { $0.isCoastal }
and overwrite the original array with the ones having isCoastal false.
landTiles = landTiles.filter { !$0.isCoastal }
That's it.