The only reason I'm attempting this programmatically is so I can access the locations of each tile, making it possible to manipulate the grid. I've looked at about 15 different tutorials using other programming languages, but even with that knowledge, I'm still having a very difficult time creating one in Swift.
I've tried creating a nested for loop but I'm not even sure if the code inside the loops make logical sense for creating an isometric grid, I just took it off of one of the tutorials I found:
func generateGrid() {
for (var i = 0; i < 5; i++) {
for (var j = 5; j >= 0; j--){
tile.position = CGPoint(x: (j * Int(tile.size.height) / 2) +
(i * Int(tile.size.width) / 2),
y: (i * Int(tile.size.height) / 2) -
(j * Int(tile.size.width) / 2))
addChild(tile)
}
}
}
Then I tried to call this in the "didMoveToView" function, but obviously you can't add the same node more than once.
Please give me any direction that you can.
Here's an example of how to create an isometric grid in SpriteKit:
Define the grid settings
let tileHeight:CGFloat = 16.0
let tileWidth:CGFloat = 32.0
let numRows = 5
let numCols = 6
Create grid elements and add them to the scene at the appropriate locations
let size = CGSizeMake(tileWidth, tileHeight)
for row in 1...numRows {
for col in 1...numCols {
// Create a new tile object
let tile = newTile(size)
// Convert (col,row) to (x,y)
tile.position = tilePosition(col, row: row)
self.addChild(tile)
}
}
This function converts a grid position to x and y scene coordinates. Use this to create the grid and to add buildings on the grid.
func tilePosition(col:Int, row:Int) -> CGPoint {
let x = (CGFloat(row) * tileWidth / 2.0) + (CGFloat(col) * tileWidth / 2.0)
let y = (CGFloat(col) * tileHeight / 2.0) - (CGFloat(row) * tileHeight / 2.0)
return CGPointMake(x+100.0, y+100.0)
}
This function creates a new diamond-shaped SKShapeNode
func newTile(size:CGSize) -> SKShapeNode {
let shape = SKShapeNode()
let path = UIBezierPath()
path.move(to: CGPoint(x:0, y:size.height / 2.0))
path.addLine(to: CGPoint(x:size.width / 2.0, y:0))
path.addLine(to: CGPoint(x:0, y:-size.height / 2.0))
path.addLine(to: CGPoint(x:-size.width / 2.0, y:0))
path.close()
shape.path = path.cgPath
shape.lineWidth = 1.0
shape.fillColor = SKColor.gray
return shape
}
This function determines the appropriate z position to ensure that buildings placed on the grid will be rendered in the correct order.
func buildingZPosition(col:Int, row:Int) -> CGFloat {
return CGFloat(row*numCols + numCols-col)
}
Figure 1. Isometric Grid Created with the Above Code
To create an isometric grid like so
one has to create a two loops. One of them iterates through rows and the other iterates through columns. Notice that the center of every other column is shifted up by half of the height of the tile and the same for every other row. To compensate for this we check if the row and column is even or odd and adjust it accordingly. A potential function would look something like this:
func generateGrid() {
for i in 0..<5 {
// Every other line needs to be shifted
var offsetX = 0
if i % 2 == 0 {
offsetX = 0
} else {
offsetX = tile.size.width / 2.0
}
for j in 0..<5 {
var tile = Tile()
// Every other line needs to be shifted
var offsetY = 0
if i % 2 == 0 {
offsetX = 0
} else {
offsetX = tile.size.height / 2.0
}
tile.position = CGPoint(x: (i * tile.size.width) - offsetX, y: (j * tile.size.height) - offsetY)
addChild(tile)
}
}
}
Related
I'm using iOS Mapbox SDK and I need to find the center coordinate in a polygon because I want to add a marker in the center coordinate. How can I do this in Swift?
func drawPolygonFeature(shapes: [MGLShape & MGLFeature]) {
let shapeSource = MGLShapeSource(identifier: "MultiPolygonShapeSource", shapes: shapes, options: nil)
let lineStyleLayer = MGLLineStyleLayer(identifier: "LineStyleLayer", source: shapeSource)
lineStyleLayer.lineColor = NSExpression(forConstantValue: UIColor.purple)
lineStyleLayer.lineOpacity = NSExpression(forConstantValue: 0.5)
lineStyleLayer.lineWidth = NSExpression(forConstantValue: 4)
DispatchQueue.main.async(execute: {[weak self] in
guard let self = self else { return }
self.mapView.style?.addSource(shapeSource)
self.mapView.style?.addLayer(lineStyleLayer)
let multiPolygonFeature = shapes.first as? MGLMultiPolygonFeature
if let centerCoordinate = multiPolygonFeature?.polygons.first?.coordinate {
self.mapView.centerCoordinate = centerCoordinate
// but centerCoordinate var does not contain the center coordinate
}
})
}
A solution depends on your requirements. If it is required that the center is within the polygon, the solution provided by Paul van Roosendaal is perfect.
However, in many cases it is better if the center can also be outside of the polygon. Think, e.g. of a polygon that looks like a nearly closed ring. In this case, it may be more natural that the center is roughly in the center of the ring, and the center is computed as the centroid of the polygon.
In the cited wiki post, this reference discusses how to compute it, and shows a number of implementations in different languages.
I have translated the Java version into Swift, and added an example:
func signedPolygonArea(polygon: [CGPoint]) -> CGFloat {
let nr = polygon.count
var area: CGFloat = 0
for i in 0 ..< nr {
let j = (i + 1) % nr
area = area + polygon[i].x * polygon[j].y
area = area - polygon[i].y * polygon[j].x
}
area = area/2.0
return area
}
func polygonCenterOfMass(polygon: [CGPoint]) -> CGPoint {
let nr = polygon.count
var centerX: CGFloat = 0
var centerY: CGFloat = 0
var area = signedPolygonArea(polygon: polygon)
for i in 0 ..< nr {
let j = (i + 1) % nr
let factor1 = polygon[i].x * polygon[j].y - polygon[j].x * polygon[i].y
centerX = centerX + (polygon[i].x + polygon[j].x) * factor1
centerY = centerY + (polygon[i].y + polygon[j].y) * factor1
}
area = area * 6.0
let factor2 = 1.0/area
centerX = centerX * factor2
centerY = centerY * factor2
let center = CGPoint.init(x: centerX, y: centerY)
return center
}
let point0 = CGPoint.init(x: 1, y: 1)
let point1 = CGPoint.init(x: 2, y: 2)
let point2 = CGPoint.init(x: 4, y: 3)
let point3 = CGPoint.init(x: 4, y: 5)
let point4 = CGPoint.init(x: 3, y: 4)
let point5 = CGPoint.init(x: 2, y: 4)
let point6 = CGPoint.init(x: 1, y: 5)
let point7 = CGPoint.init(x: 3, y: 2)
let polygon = [point0, point1, point2, point3, point4, point5, point6, point7]
let center = polygonCenterOfMass(polygon: polygon)
I think you can find all the info you need here: https://blog.mapbox.com/a-new-algorithm-for-finding-a-visual-center-of-a-polygon-7c77e6492fbc
It links to a javascript module (https://github.com/mapbox/polylabel), but I expect you can easily rewrite it.
For sake of not just sharing a url I copied the most relevant info from the blog post here:
The basic principle is use of quadtrees. The main concept is to recursively subdivide a two-dimensional space into four quadrants.
Start with a few large cells covering the polygon. Recursively subdivide them into four smaller cells, probing cell centers as candidates and discarding cells that can’t possibly contain a solution better than the one we already found.
How do we know if a cell can be discarded? Let’s consider a sample square cell over a polygon:
If we know the distance from the cell center to the polygon (dist above), any point inside the cell can’t have a bigger distance to the polygon than dist + radius, where radius is the radius of the cell. If that potential cell maximum is smaller than or equal to the best distance of a cell we already processed (within a given precision), we can safely discard the cell.
For this assumption to work correctly for any cell regardless whether their center is inside the polygon or not, we need to use signed distance to polygon — positive if a point is inside the polygon and negative if it’s outside.
I am plotting an array of CGFloat values within a UIBezierPath. Now I want to Normalise the values so the max value of the array will occupy the whole height.
So far
let scaleFactor = (waveView.frame.height) * readFile.maxValue
for point in readFile.points {
let nextPoint = CGPoint(x: soundPath.currentPoint.x + sampleSpace, y: middleY - (point * scaleFactor) - 1.0)
soundPath.addLine(to: nextPoint)
soundPath.move(to: nextPoint)
}
But it does not seem to work...
EDIT
ReadFile:
class ReadFile {
public enum ReadFileError:Error{
case errorReading(String)
}
/* Player Interval Measured in Miliseconds (By now..) */
var beginPosition:Int32 = 0
var endPosition:Int32 = 0
/* Sample Rate -> Default 48KHz */
var sampleRate:Double = 48000
var samplesSeconds:CGFloat = 5
var maxValue:CGFloat = 0
var points:[CGFloat] = []
}
And
sampleSpace = 0.2
Thank you Andy for your answer but I have finally figured it out.
I am drawing a sound wave so it has positives and negatives values.
heightMax = waveView.frame.height/2
Applying a rule of three (Spanish translation) I end up with this:
func drawSoundWave(windowLength:Int32){
// Drawing code
print("\(logClassName): Drawing!!!")
print("\(logClassName): points COUNT = \(readFile.points.count)")
let soundPath = UIBezierPath()
soundPath.lineWidth = lineWidth
soundPath.move(to: CGPoint(x:0.0 , y: middleY))
print("\(logClassName) max ")
for point in readFile.points{
let normalizedHeight:CGFloat = (waveView.frame.height * point) / (2 * readFile.maxValue)
let nextPoint = CGPoint(x: soundPath.currentPoint.x + sampleSpace, y: middleY - (normalizedHeight))
soundPath.addLine(to: nextPoint)
soundPath.move(to: nextPoint)
}
let trackLayer = CAShapeLayer()
trackLayer.path = soundPath.cgPath
waveView.layer.addSublayer(trackLayer)
trackLayer.strokeColor = UIColor.red.cgColor
trackLayer.lineWidth = lineWidth
trackLayer.fillColor = UIColor.green.cgColor
trackLayer.lineCap = kCALineCapRound
}
where
let normalizedHeight:CGFloat = (waveView.frame.height * point) / (2 * readFile.maxValue)
is the normalize value given a readFile.maxValue and waveView.frame.height
I am using the following code to try to creating a CGPoint for a ball along the top of the screen:
func randomBallPosition() -> CGPoint {
let random = CGFloat((arc4random_uniform(8) + 1) / 10)
let randomX = CGFloat(self.frame.size.width * random)
let staticY = CGFloat(self.frame.size.height * 0.95)
return CGPoint(x: randomX, y: staticY)
}
However the ball is always placed at (0,y) and I'm unsure why.
You just need to divide by 10 after converting your integer to CGFloat:
let random = CGFloat((arc4random_uniform(8) + 1)) / 10
In my code I am trying to have it so the entity moves a certain number of times, and delays between each movement.
These movements are essential small teleports to allow for the movement distance to be slowed or speed up (that is what the RGBA part of the code does by using the green color at a pixel of another image that is not displayed).
The problem I am getting is that when a entity is moved.
It moves the correct distance, but all at once. Thus if I think I am telling it to move 10 units over 10 seconds, it waits 10 seconds then moves 10 units.
This is a problem as it looks bad and makes it so the entity "overshoots" where it is selected to go (that is what the "if" statement in the loop does, it stops the entity when it gets close).
Note: This function is within a class that is a SKNode.
Here is the code (note: "moveTo" is defined outside of the function):
func moveGo(){
var dX: CGFloat
var dY: CGFloat
var magnitudeOfVector: Double
var originalX: CGFloat = self.position.x
var originalY: CGFloat = self.position.y
dX = ((moveTo?.x)! - (self.position.x))
dY = ((moveTo?.y)! - (self.position.y))
let wantedX: CGFloat = (moveTo?.x)!
let wantedY: CGFloat = (moveTo?.y)!
magnitudeOfVector = sqrt(((Double)(dX) * (Double)(dX) + (Double)(dY) * (Double)(dY)))
dY = dY/(CGFloat)(magnitudeOfVector)
dX = dX/(CGFloat)(magnitudeOfVector)
var currentX = self.position.x
var currentY = self.position.y
for _ in 1...100{
SKAction.wait(forDuration: 0.1)
if let (r,g,b,a) = pixel(in: image, at: self.position) {
print ("Red \(r), Green: \(g), Blue :\(b), Alpha: \(a)")
stepDebuff = 2*(CGFloat)(r)/255
}
dX = ((moveTo?.x)! - (originalX))
dY = ((moveTo?.y)! - (originalY))
dY = dY/(CGFloat)(magnitudeOfVector)
dX = dX/(CGFloat)(magnitudeOfVector)
dY = dY * defaultStepSize * stepDebuff
dX = dX * defaultStepSize * stepDebuff
currentX += dX
currentY += dY
self.position = CGPoint(x: currentX, y: currentY)
if abs(wantedX - currentX) < 2 && abs(wantedY - currentY) < 2{
if abs(wantedX - currentX) < 0.01 && abs(wantedY - currentY) < 0.01{
dY = 0
dX = 0
} else{
dX = ((moveTo?.x)! - (self.position.x))
dY = ((moveTo?.y)! - (self.position.y))
}
}
print("\(magnitudeOfVector)")
print("\(dX)")
print("\(currentX)")
}
}
The number of loops is arbitrary. defaultStepSize = 1.
I have the following code:
func createScene(){
count += 1
let sphereGeom = SCNSphere(radius: 1.5)
sphereGeom.firstMaterial?.diffuse.contents = UIColor.redColor()
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: 0, y: 0))
let radius = 3.0
var radians = Double(0)
var yPosition = Float(5.4)
while count <= 20 {
if radians >= 2{
radians -= 2
}
let sphereNode = SCNNode(geometry: sphereGeom)
let angle = Double(radians * M_PI)
let xPosition = Float(radius * cos(angle))
let zPosition = Float(radius * sin(angle))
sphereNode.position = SCNVector3(xPosition, yPosition, zPosition)
let cgX = CGFloat(xPosition)
let cgY = CGFloat(yPosition)
path.addQuadCurveToPoint(CGPoint(x: cgX, y: cgY), controlPoint: CGPoint(x: (cgX / 2), y: (cgY / 2)))
path.addLineToPoint(CGPoint(x: (cgX - (cgX * 0.01)), y: cgY))
path.addQuadCurveToPoint(CGPoint(x: 1, y: 0), controlPoint: CGPoint(x: (cgX / 2), y: ((cgY / 2) - (cgY * 0.01))))
let shape = SCNShape(path: path, extrusionDepth: 3.0)
shape.firstMaterial?.diffuse.contents = UIColor.blueColor()
let shapeNode = SCNNode(geometry: shape)
shapeNode.eulerAngles.y = Float(-M_PI_4)
self.rootNode.addChildNode(shapeNode)
count += 1
radians += 0.5556
yPosition -= 1.35
self.rootNode.addChildNode(sphereNode)
}
I want to add a Bezier path connecting each sphere to the next one, creating a spiral going down the helix. For some reason, when I add this code, the shape doesn't even appear. But when I use larger x and y values, I see the path fine, but it is no way oriented to the size of the spheres. I don't understand why it disappears when I try to make it smaller.
Your SCNShape doesn't ever get extruded. Per Apple doc,
An extrusion depth of zero creates a flat, one-sided shape.
With larger X/Y values your flat shape happens to become visible. You can't build a 3D helix with SCNShape, though: the start and end planes of the extrusion are parallel.
You'll have to use custom geometry, or approximate your helix with a series of elongated SCNBox nodes. And I bet someone out there knows how to do this with a shader.