I would like to apply a rotation to multiple nodes (selected nodes in my game) using the UIRotationGesture, based on the center point of all those nodes. I can already rotate a single node simply changing it's zRotation.
The problem with multiple nodes is that it changes position and zRotation based on a center node, and I cannot seem to understand how to manage that.
I would like something like this:
What I have to rotate a single node is this:
During the rotation gesture
theRotation = CGFloat(sender.rotation) + self.offset
theRotation = theRotation * -1
node.rotation = theRotation
After the rotation gesture
self.offset = theRotation * -1
Would you have an idea on how set the correct position and angle for my nodes during the rotation?
What I tried:
I tried to add a node in the center (where the white dot is in my pictures, which represents the center) and change the parent of my nodes to be this one, then apply the zRotation on this node, and then replace the right parents. This did not work as I cannot seem to change a parent (my nodes disappear), this is another one of my Stack Questions.
I tried to change the anchor point of my nodes to fit the center point and than rotate them using theRotation. It did not work as I cannot seem to set the anchor point at the center position (that I have). I tried changing the coordinates system of the center's position to fit the node's one, but this is still not working. node.convertPoint(center, fromNode: Self) gives me coordinated like -58;-74 when it's about -1;-.5 (or something like that). I do not understand this.
So now I am thinking to calculate the position and rotation myself, as those did not work, but I would need an idea on how to calculate those as I am not very good with trigonometry/linear algebra, sadly enough.
Thank you for you help!
How I calculate my center:
var maxX = nodesSelected[0].position.x
var minX = nodesSelected[0].position.x
var maxY = nodesSelected[0].position.y
var minY = nodesSelected[0].position.y
for node in nodesSelected{
if node.position.x > maxX{
maxX = node.position.x
}
if node.position.x < minX{
minX = node.position.x
}
if node.position.y > maxY{
maxY = node.position.y
}
if node.position.y > maxY{
minY = node.position.y
}
}
return CGPoint(x: (maxX-minX)/2+minX, y: (maxY-minY)+minY/2)
How I calculate the radius of the rotation (distance between a node and the center):
extension CGPoint {
func distance(point: CGPoint) -> CGFloat {
return abs(CGFloat(hypotf(Float(point.x - x), Float(point.y - y))))
}
How I get my rotation:
sender.rotation
Given a rotationAngle, you can calculate the new position of each node with the code below, you need to know a bit of trigonometry to understand the code.
Here I have an array of SKShapeNode that I called dots (It would be the equivalent of your green nodes in the image). And the centralDot would be your central SKSpriteNode.
for dot in dots {
let dx = dot.position.x - centralDot!.position.x // Get distance X from center
let dy = dot.position.y - centralDot!.position.y // Get distance Y from center
let current_angle = atan(dy / dx) // Current angle is the arctan of dy / dx
let next_angle = current_angle - rotationAngle // Sum how much you want to rotate in radians
// the new x is: center + radius*cos(x)
// the new y is: center + radius*sin(y)
// if dx < 0 you need to get the oposite value of the position
let new_x = dx >= 0 ? centralDot!.position.x + rotationRadius * cos(next_angle) : centralDot!.position.x - rotationRadius * cos(next_angle)
let new_y = dx >= 0 ? centralDot!.position.y + rotationRadius * sin(next_angle) : centralDot!.position.y - rotationRadius * sin(next_angle)
let new_point = CGPoint(x: new_x, y: new_y)
let action = SKAction.moveTo(new_point, duration: 0.2)
dot.runAction(action)
}
Hope this helps
Update:
The first code didn't helped, so I tried another one. This one worked better on my tests.
for i in 0..<dots.count {
let dot = dots[i]
let angle = rotationAngle + CGFloat(M_PI_2 * Double(i))
let new_x = rotationRadius * cos(angle) + centralDot!.position.x
let new_y = rotationRadius * sin(angle) + centralDot!.position.y
let new_point = CGPoint(x: new_x, y: new_y)
let action = SKAction.moveTo(new_point, duration: 1/60)
dot.runAction(action)
}
rotationRadius is a constant, the distance you want between the center and the green node.
Related
I am currently facing the problem that I want to calculate the angle in radians from the camera's position to a target position. However, this calculation needs to take into account the heading of the camera as well.
For example, when the camera is facing away from the object the function should return π. So far the function I have written works most of the time. However when the user gets close to the X and Z axis the arrow does not point to the target any more, rather it points slightly to the left or right depending if you are at positive or negative X and z space.
Currently, I'm not sure why my function does not work. The only explanation I would have for this behavior is gimbal lock. However I'm not quite sure how to implement the same function using quaternions.
I also attached some photos to this post that the issue is a little bit more clear.
Here is the function I'm using right now:
func getAngle() -> Float {
guard let pointOfView = self.sceneView.session.currentFrame else { return 0.0 }
let cameraPosition = pointOfView.camera.transform.columns.3
let heading = getUserVector()
let distance = SCNVector3Make(TargetPosition.x - cameraPosition.x ,TargetPosition.y - cameraPosition.y - TargetPosition.y,TargetPosition.z - cameraPosition.z)
let heading_scalar = sqrtf(heading.x * heading.x + heading.z * heading.z)
let distance_scalar = sqrtf(distance.z * distance.z + distance.z * distance.z)
let x = ((heading.x * distance.x) + (heading.z * distance.z) / (heading_scalar * distance_scalar))
let theta = acos(max(min(x, 1), -1))
if theta < 0.35 {
return 0
}
if (heading.x * (distance.z / distance_scalar) - heading.z * (distance.x/distance_scalar)) > 0{
return theta
}
else{
return -theta
}
}
func getUserVector() -> (SCNVector3) { // (direction)
if let frame = self.sceneView.session.currentFrame {
let mat = SCNMatrix4(frame.camera.transform) // 4x4 transform matrix describing camera in world space
let dir = SCNVector3(-1 * mat.m31, -1 * mat.m32, -1 * mat.m33) // orientation of camera in world space
print(mat)
return dir
}
return SCNVector3(0, 0, -1)
}
Consider the following image as an example. The arrow in the top right corner should be pointing straight up to follow the line to the center object but instead it is pointing slightly to the left. As I am aligned with the z-axis the same behavior happens when aligning with the x-axis.
I figured out the answer to my problem the solution was transforming the object into the prospective of the camera and then simply taking the atan2 to get the angle in between the camera and object hope this post will help future readers!
func getAngle() -> Float {
guard let pointOfView = self.sceneView.session.currentFrame else { return 0.0 }
let cameraPosition = pointOfView.camera.transform
let targetPosition = simd_float4x4(targetNode.transform)
let newTransform = simd_mul(cameraPosition.inverse, targetPosition).columns.3
let theta = atan2(newTransform.z, newTransform.y)
return theta + (Float.pi / 2)
}
I use the following function to append physicsbodies on tiles from a SKTileMapNode:
static func addPhysicsBody(to tileMap: SKTileMapNode, and tileInfo: String){
let tileSize = tileMap.tileSize
let halfWidth = CGFloat(tileMap.numberOfColumns) / 2 * tileSize.width
let halfHeight = CGFloat(tileMap.numberOfRows) / 2 * tileSize.height
for row in 0..<tileMap.numberOfColumns{
for column in 0..<tileMap.numberOfRows{
let tileDefinition = tileMap.tileDefinition(atColumn: column, row: row)
let isCorrectTile = tileDefinition?.userData?[tileInfo] as? Bool
if isCorrectTile ?? false && tileInfo == "wall"{
let x = CGFloat(column) * tileSize.width - halfWidth
let y = CGFloat(row) * tileSize.height - halfHeight
let tileNode = SKNode()
tileNode.position = CGPoint(x: x, y: y)
tileNode.physicsBody = SKPhysicsBody.init(rectangleOf: tileSize, center: CGPoint(x: tileSize.width / 2, y: tileSize.height / 2))
tileNode.physicsBody!.isDynamic = false
tileNode.physicsBody!.restitution = 0.0
tileNode.physicsBody!.categoryBitMask = Constants.PhysicsCategories.wall
tileNode.physicsBody!.collisionBitMask = Constants.PhysicsCategories.player | Constants.PhysicsCategories.npc | Constants.PhysicsCategories.enemy
nodesForGraph.append(tileNode)
tileMap.addChild(tileNode)
}
}
}
}
However if I use this, I have a physicsbody per tile. I want to connect physicsbodies to bigger ones to get a better performance. I know that this can be with init(bodies: [SKPhysicsBody]). But how can I do that?
How can I find out which body is next to another body to group them?
The physicsbodies in the tileMap aren't all next to each other. Some are big blocks of physicsbodies, some are single physicsbodies with no bodies next to them. So I can't simply put every physicsbody in an array and group them.
Here's an image that shows how it looks like at the moment.
I hope the explanation is clear enough. If not, I will try to explain it better.
Has anyone done this before and can point me in the right direction? I would appreciate any help.
EDIT:
Before I tried this:
static var bodies = [SKPhysicsBody]()
static func addPhysicsBody(to tileMap: SKTileMapNode, and tileInfo: String){
let tileSize = tileMap.tileSize
let halfWidth = CGFloat(tileMap.numberOfColumns) / 2 * tileSize.width
let halfHeight = CGFloat(tileMap.numberOfRows) / 2 * tileSize.height
for column in 0..<tileMap.numberOfColumns{
for row in 0..<tileMap.numberOfRows{
let tileDefinition = tileMap.tileDefinition(atColumn: column, row: row)
let isCorrectTile = tileDefinition?.userData?[tileInfo] as? Bool
if isCorrectTile ?? false && tileInfo == "wall"{
let x = CGFloat(column) * tileSize.width - halfWidth
let y = CGFloat(row) * tileSize.height - halfHeight
let tileNode = SKNode()
tileNode.position = CGPoint(x: x, y: y)
tileNode.physicsBody = SKPhysicsBody.init(rectangleOf: tileSize, center: CGPoint(x: tileSize.width / 2, y: tileSize.height / 2))
tileNode.physicsBody!.isDynamic = false
tileNode.physicsBody!.restitution = 0.0
tileNode.physicsBody!.categoryBitMask = Constants.PhysicsCategories.wall
tileNode.physicsBody!.collisionBitMask = Constants.PhysicsCategories.player | Constants.PhysicsCategories.npc | Constants.PhysicsCategories.enemy
//nodesForGraph.append(tileNode)
bodies.append(tileNode.physicsBody!)
tileMap.addChild(tileNode)
}
}
}
tileMap.physicsBody = SKPhysicsBody(bodies: bodies)
}
But when I do this, the physicsbodies are totally messed up..
I recommend applying a line sweep algorithm to merge the tiles together.
You can do this in four steps;
Iterate through the position of the tiles in your SKTileMap.
Find the tiles that are adjacent to one another.
For each group of adjacent tiles, collect:
a down-left corner coordinate and
an up-right corner coordinate.
Draw a square, and move on to the next group of tiles until you run out of tile coordinates.
The first step: creating an array containing all of your position nodes.
func tilephysics() {
let tilesize = tileMap.tileSize
let halfwidth = CGFloat(tileMap.numberOfColumns) / 2.0 * tilesize.width
let halfheight = CGFloat(tileMap.numberOfRows) / 2.0 * tilesize.height
for col in 0 ..< tileMap.numberOfColumns {
for row in 0 ..< tileMap.numberOfRows {
if (tileMap.tileDefinition(atColumn: col, row: row)?.userData?.value(forKey: "ground") != nil) {
let tileDef = tileMap.tileDefinition(atColumn: col, row: row)!
let tile = SKSpriteNode()
let x = round(CGFloat(col) * tilesize.width - halfwidth + (tilesize.width / 2))
let y = round(CGFloat(row) * tilesize.height - halfheight + (tilesize.height / 2))
tile.position = CGPoint(x: x, y: y)
tile.size = CGSize(width: tileDef.size.width, height: tileDef.size.height)
tileArray.append(tile)
tilePositionArray.append(tile.position)
}
}
}
algorithm()
}
The second and third step: finding adjacent tiles, collecting the two corner coordinates, and adding them to an array:
var dir = [String]()
var pLoc = [CGPoint]()
var adT = [CGPoint]()
func algorithm(){
let width = tileMap.tileSize.width
let height = tileMap.tileSize.height
let rWidth = 0.5 * width
let rHeight = 0.5 * height
var ti:Int = 0
var ti2:Int = 0
var id:Int = 0
var dl:CGPoint = CGPoint(x: 0, y: 0)
var tLE = [CGPoint]()
var tRE = [CGPoint]()
for t in tilePositionArray {
if (ti-1 < 0) || (tilePositionArray[ti-1].y != tilePositionArray[ti].y - height) {
dl = CGPoint(x: t.x - rWidth, y: t.y - rHeight)
}
if (ti+1 > tilePositionArray.count-1) {
tLE.append(dl)
tRE.append(CGPoint(x: t.x + rWidth, y: t.y + rHeight))
} else if (tilePositionArray[ti+1].y != tilePositionArray[ti].y + height) {
if let _ = tRE.first(where: {
if $0 == CGPoint(x: t.x + rWidth - width, y: t.y + rHeight) {id = tRE.index(of: $0)!}
return $0 == CGPoint(x: t.x + rWidth - width, y: t.y + rHeight)}) {
if tLE[id].y == dl.y {
tRE[id] = CGPoint(x: t.x + rWidth, y: t.y + rHeight)
} else {
tLE.append(dl)
tRE.append(CGPoint(x: t.x + rWidth, y: t.y + rHeight))
}
} else {
tLE.append(dl)
tRE.append(CGPoint(x: t.x + rWidth, y: t.y + rHeight))
}
}
ti+=1
}
The fourth step: drawing a rectangle and moving on to the next shape:
for t in tLE {
let size = CGSize(width: abs(t.x - tRE[ti2].x), height: abs(t.y - tRE[ti2].y))
let loadnode = SKNode()
loadnode.physicsBody = SKPhysicsBody(rectangleOf: size)
loadnode.physicsBody?.isDynamic = false
loadnode.physicsBody?.affectedByGravity = false
loadnode.physicsBody?.restitution = 0
loadnode.physicsBody?.categoryBitMask = 2
loadnode.position.x = t.x + size.width / 2
loadnode.position.y = t.y + size.height / 2
scene.addChild(loadnode)
ti2 += 1
}
}
Apply these steps correctly, and you should see that your tiles are merged together in large squares; like so:
Screenshot without visuals for comparison
Screenshot without visuals showing the physicsbodies
I had a lot of fun solving this problem. If I have helped you, let me know.
I only recently started coding and am looking for new challenges. Please reach out to me if you have challenges or projects I could possibly contribute to.
As Knight0fDragon pointed out, there is no way to do exactly what you have asked. Unfortunately, tile maps in SpriteKit leave much to be desired. But you might try this technique to reduce the number of physics bodies.
Idea #1 - Manually Draw Your Physics Bodies
Create your tile map in the editor. Just paint your tile textures onto the map; don't assign any physics bodies to them. Then keep working in the editor to drag Color Sprites (SKSpriteNodes) over parts of your map that need a physics body. Shape the nodes to make the largest rectangle possible for areas that need physics bodies. This works best for for large, flat surfaces like walls, floors, ceilings, platforms, crates, etc. It's tedious but you end up with far fewer physics bodies in your simulation than if you automatically assign bodies to all tiles like you are doing.
Idea #2 - Use No Physics Bodies
This idea would probably require even more work, but you could potentially avoid using physics bodies altogether. First, create your tile map in the editor. Analyze your map to identify which tiles mark a barrier, beyond which the player should not cross. Assign a user data identifier to that type of tile. You would need different categories of identifiers for different types of barriers, and you may also need to design your artwork to fit this approach.
Once your barrier tiles are sufficiently identified, write code which checks the user data value for the tile currently occupied by the player sprite and restrict the sprite's movement accordingly. For example, if the player enters a title that marks an upper boundary, your movement code would not allow the player sprite to move up. Likewise, if the player enters a tile that marks the leftmost boundary, your movement code will not let the player travel left.
You can check out this related post where I basically suggest the same ideas. Unfortunately, SpriteKit's tile maps have no perfect solution for this problem.
I'm working on a top-down space game built using Swift and SceneKit with the following setup:
SCNNode representing a spaceship
Rotation is constrained to the y axis; values range from -M_PI_2 to M_PI + M_PI_2
Movement is constrained to the x and z axes.
Game controller thumbstick input
Values range from -1.0 to 1.0 on the x and y axes.
When the game controller's thumbstick changes position, the spaceship should rotate using the physics body to match the thumbstick's radian.
The target radian of the thumbstick can be calculated with the following:
let targetRadian = M_PI_2 + atan2(-y, -x)
The current radian of the node can be obtained with the following:
let currentRadian = node.presentationNode.rotation.w * node.presentationNode.rotation.y
NSTimeInterval deltaTime provides the time in seconds since the last rotation calculation.
How can the node be rotated using angularVelocity, applyTorque, or another physics method to reach the targetRadian?
The difference between the targetRadian and the currentRadian ranged from 0.0 to -2π depending on the value of currentRadian. This equation will determine the shortest direction to turn, .Clockwise or .CounterClockwise, to reach the targetRadian:
let turnDirection = (radianDifference + (M_PI * 2)) % (M_PI * 2) < M_PI ? RotationDirection.CounterClockwise : RotationDirection.Clockwise
Using applyTorque, there is a possibility to over-rotate past the targetRadian resulting in a wobbling effect, like a compass magnetizing toward a point, as the rotation changes direction back and forth to reach the targetRadian. The following, while not a perfect solution, dampened the effect:
let turnDampener = abs(radianDifference) < 1.0 ? abs(radianDifference) : 1.0
The complete solution is thus:
enum RotationDirection: Double {
case Clockwise = -1.0
case CounterClockwise = 1.0
}
func rotateNodeTowardDirectionalVector(node: SCNNode, targetDirectionalVector: (x: Double, y: Double), deltaTime: NSTimeInterval) {
guard abs(targetDirectionalVector.x) > 0.0 || abs(targetDirectionalVector.y) > 0.0 else { return }
let currentRadian = Double(node.presentationNode.rotation.w * node.presentationNode.rotation.y)
let targetRadian = M_PI_2 + atan2(-targetDirectionalVector.y, -targetDirectionalVector.x)
let radianDifference = targetRadian - currentRadian
let π2 = M_PI * 2
let turnDirection = (radianDifference + π2) % π2 < M_PI ? RotationDirection.CounterClockwise : RotationDirection.Clockwise
let absRadianDifference = abs(radianDifference)
let turnDampener = absRadianDifference < 1.0 ? absRadianDifference : 1.0
node.physicsBody?.applyTorque(SCNVector4Make(0, CGFloat(turnDirection.rawValue), 0, CGFloat(deltaTime * turnDampener)), impulse: true)
}
I'm new to SpriteKit game development. I'm trying give a boss character the ability to cast fireballs in multiple directions (16 fireballs all at once, 360 degree/16 = 22.5 degree apart).
I know how to get him to fire at a certain position by providing the player's current position, but how to get him to fire at 16 different angles regardless of player's position?
Thanks for any help in advance.
First, set up a loop over the angles
let numAngles = 16
var angle:CGFloat = 0
var angleIncr = CGFloat(2 * M_PI) / CGFloat(numAngles)
let strength:CGFloat = 50
for _ in 0..<numAngles {
...
angle += angleIncr
}
In the loop, convert the angle to the corresponding vector components and then create a vector
let dx = strength * cos (angle)
let dy = strength * sin (angle)
let vector = CGVectorMake (dx, dy)
and create a new fireball and apply an impulse to its physics body
let fireball = ...
fireball.position = player.position
fireball.zRotation = angle
// Add a physics body here
fireball.physicsBody?.appyImpulse (vector)
I'm not sure what code you have in place. for shooting. but ill give this a shot. angles in spritekit are in radians and a there are 2*pi radians in a circle. so you just need to do something like this
let fireballs = 16
let threeSixty = CGFloat(M_PI*2)
for i in 1...fireballs {
let angle = (CGFloat(i) / CGFloat(fireballs)) * threeSixty
// do something useful with your angle
}
So what I am trying to do is make it so that a physics impulsee seems to have the same effect on all devices. So basically if I can figure out A way to do the following I will be able to accomplish my goal.
First lets simplify things by taking out all gravity.
Basically I need to calculate the impulse it will take to get a physics object on the far left of the screen to get to the far right of the screen in the same amount of time no matter how big the screen size is.
The reason I ask is I am making a movement system based on the magnitude and angle of a swipe. However I want it to play the same way on every device. I am calculating magnitude by
(distance (in virtual points)) / (Time spent making gesture)
Then i am applying it as a physics impulse.
This is the code I am working with:
func Jump(angle: CGFloat, strength: CGFloat)
{
if (Ready == true)
{
var rangle:CGFloat = angle * CGFloat(M_PI / 180)
var translate:CGPoint = CGPoint(x: 1, y: 0)
var vx:CGFloat = ((translate.x * cos(rangle)) - (translate.y * sin(angle)))
var vy:CGFloat = ((translate.y * cos(rangle)) + (translate.x * sin(rangle)))
vx *= width
vy *= height
vx *= (strength)
vy *= (strength)
vx /= 4000
vy /= 4000
print("Applying Impulse VX: ")
print(vx)
print(" , VY: ")
print(vy)
println(" )")
var velx = Cavity.physicsBody?.velocity.dx
var vely = Cavity.physicsBody?.velocity.dy
Cavity.physicsBody?.velocity = CGVector(dx: CGFloat(velx!) / 2, dy: CGFloat(vely!) / 2)
Cavity.physicsBody?.applyImpulse(CGVectorMake(vx, vy))
//Cavity.physicsBody?.applyImpulse(CGVectorMake(1000 / width, 1000 / height))
}
}
So basically I want it to be so that if a strength of 1 or 2 is passed it will make the same looking result on all devices.
What you can do is make the strength relative to the screen size.
strengthAdjustment = (1/375*UIScreen.mainScreen().bounds.width)
This uses the iPhone 6 screen (4.7") width (375 pts) to make the strength = 1.
With an iPhone 5s the screen will be only 320 pts which and will only require 0.8533 of the impulse strength to move the width of the screen in the same amount of time.
Hopefully this helps you out.