My question is
When i touch black arrow areas i still touch whole picture, but i want only touch and have action when i touch red area only.
I tried to give a name to spriteNode plane = childNode(withName: "play") but it take/touch all image frame not only alphaBody.
red and black areas
I did some searches on google but no positive results.
Many thanks.
a really simple solution could be to put hit test areas on your plane object (I've left them slightly red for the example but you would make them transparent). I created my plane class in the editor and dragged it as a reference into my scene, but you could easily just program the hit zones from scratch as well.
Then in your plane class add them to the plane and detect touches within the plane class itself. It's not exact but not that much farther off then your alpha tracing was in your picture.
A nice bonus of this method is that you can isolate this touches to areas of the plane. Some uses for that could be...
touch left/right wing to turn that direction
touch body to refuel
touch wings to change guns
here is the code I used in my Plane class
class Plane: SKSpriteNode {
private var background: SKSpriteNode!
private var wingsHitTest: SKSpriteNode!
private var bodyHitTest: SKSpriteNode!
private var smallWingsHitTest: SKSpriteNode!
init() {
super.init(texture: nil, color: .clear, size: CGSize.zero)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
isUserInteractionEnabled = true
self.color = .clear
if let background = self.childNode(withName: "//background") as? SKSpriteNode {
self.background = background
}
if let wingsHitTest = self.childNode(withName: "//wingsHitTest") as? SKSpriteNode {
self.wingsHitTest = wingsHitTest
}
if let bodyHitTest = self.childNode(withName: "//bodyHitTest") as? SKSpriteNode {
self.bodyHitTest = bodyHitTest
}
if let smallWingsHitTest = self.childNode(withName: "//smallWingsHitTest") as? SKSpriteNode {
self.smallWingsHitTest = smallWingsHitTest
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first as UITouch? {
let touchLocation = touch.location(in: self)
if wingsHitTest.contains(touchLocation) {
print("hit in the wings")
}
else if smallWingsHitTest.contains(touchLocation) {
print("hit in the back wings")
}
else if bodyHitTest.contains(touchLocation) {
print("hit in the body")
}
}
}
}
It seems like the behavior you want is not included in SpriteKit so you will have to implement it yourself.
Quoting from the link above:
You will have to find out the locationInNode(spriteNode) of the UITouch object object to get the coordinates within the sprite and then read the alpha value from the sprite Image (not trivial) or precompute bitmasks (ones and zeros) from your images and read the value at the corresponding point in the bitmask for the sprite
So you will need to load your image as UIImage or CGImage and check whether the color at the pixels where the touch occurred was transparent or not. You can find more information on how to do that in this question: How to get pixel color from SKSpriteNode or from SKTexture?
Related
I am pretty new to SpriteKit so I may be missing something quite obvious.
I am attempting to create an interactive map of the US. I have loaded PNG images for each state and placed a couple of them into the main SKScene using the scene editor.
My goal is wanting to detect when each state is tapped by the user. I initially wrote some code that would find the nodes within a touch location however, this meant that the user could tap the frame and it would be counted as a valid tap. I only want to register touches that happen within the state texture and not the frame. Reading online it was suggested to use SKPhysicsBody to register when a tap takes place. So I changed my code to the following.
class GameScene: SKScene {
override func didMove(to view: SKView) {}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location: CGPoint = self.convertPoint(fromView: touch.location(in: self.view))
let body = self.physicsWorld.body(at: location)
if let state = body?.node, let name = state.name {
state.run(SKAction.run({
var sprite = self.childNode(withName: name) as! SKSpriteNode
sprite.color = UIColor.random()
sprite.colorBlendFactor = 1
}))
}
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
}
}
Now, if I choose the Bounding circle body type everything works as expected (shown above in the screenshot). When I click within the boudning circle it runs the SKAction otherwise it does nothing. However, when I change the body type to Alpha mask (the body type I want) it suddenly stops detecting the state. In fact, it returns the SKPhysicsBody for the MainScene entity.
Any advice on how to fix this would be greatly appreciated.
Thanks.
i can reproduce this behavior (bug?) when using the scene editor. however it goes away if you skip the sks file and initialize your sprites in code. (i acknowledge that setting locations for 50 states is more tedious this way.)
class GameScene: SKScene {
let ok = SKSpriteNode(imageNamed: "ok")
let nm = SKSpriteNode(imageNamed: "nm")
let tx = SKSpriteNode(imageNamed: "tx")
override func didMove(to view: SKView) {
tx.name = "Texas"
nm.name = "New Mexico"
ok.name = "Oklahoma"
//set up physics bodies w/ alpha mask
tx.physicsBody = SKPhysicsBody(texture: tx.texture!, size: tx.texture!.size())
tx.physicsBody?.affectedByGravity = false
nm.physicsBody = SKPhysicsBody(texture: nm.texture!, size: nm.texture!.size())
nm.physicsBody?.affectedByGravity = false
ok.physicsBody = SKPhysicsBody(texture: ok.texture!, size: ok.texture!.size())
ok.physicsBody?.affectedByGravity = false
//then position your sprites and add them as children
}
}
I am working with SceneKit and ARKit. I have made a collectionView with an array of emoji's. Now I want the user to be able to select the emoji from collectionView and when he/she touches the screen that selected emoji will be placed in 3D.
How can I do that? I think I have to create a function for the Node, but still my idea is blurry in the mind and I am not very much clear.
As far as any emoji is a 2D element, it's better to use a SpriteKit framework to upload them, not a SceneKit. But, of course, you might choose a SceneKit as well. So, there are two ways you can work with emojis in ARKit:
Using SpriteKit. In that case all the 2D sprites you spawn in ARSKView are always face the camera. So, if the camera moves around a definite point of real scene, all the sprites are rotates about their pivot point facing a camera.
Using SceneKit. In ARSCNView you can use all your sprites as a texture for 3D geometry. This texture could be for a plane, cube, sphere, or any custom model, it's up to you. To make, for example, a plane (with emojis texture on it) to face a camera use SCNBillboardConstraint constraint.
Here's how you code in ViewController might look like:
// Element's index coming from `collectionView`
var i: Int = 0
func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {
let emojiArray = ["🐶","🦊","🐸","🐼","🐹"]
let emojiNode = SKLabelNode(text: emojiArray[i])
emojiNode.horizontalAlignmentMode = .center
emojiNode.verticalAlignmentMode = .center
return emojiNode
}
...and in Scene.swift file:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let sceneView = self.view as? ARSKView else { return }
if let currentFrame = sceneView.session.currentFrame {
var translation = matrix_identity_float4x4
translation.columns.3.z = -0.75 // 75 cm from camera
let transform = simd_mul(currentFrame.camera.transform, translation)
let anchor = ARAnchor(transform: transform)
sceneView.session.add(anchor: anchor)
}
}
Or, if you use hit-testing, your code might look like this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let sceneView = self.view as? ARSKView else { return }
if let touchLocation = touches.first?.location(in: sceneView) {
if let hit = sceneView.hitTest(touchLocation, types: .featurePoint).first {
sceneView.session.add(anchor: ARAnchor(transform: hit.worldTransform))
}
}
}
If you'd like to create an UICollectionView overlay containing emojis to choose from, read the following post.
If you'd like to create an SKView overlay, containing emojis to choose from, read the following post.
I want to implement a spritenode with a "colored highlight". The "highlight can be of a number of colors. This is done in sandbox app to try things out.
I have considered using SKTextures to represent the "highlight" of the node. However, with this method, I will have to upload a number of "highlight" files which must be "pre-attached" to the host spritenode png file. So, if I have 5 spritenode "host" png files and 5 "highlight" png files, then I will have a total of 5x5=25 png files that represents all the possible combinations. That will be way too many files to prepare and maintained.
So, I rather make the spritenode "host" have a child spritenode which is the highlight.
Both the host and highlight spritenode need to move together and the highlight spritenode is 1 zPosition infront of the "host" spritenode. So, the cgposition of the 2 spritenodes should be the same.
I create a Class file for the host spritenode.
class Host : SKSpriteNode {
var highlight = SKSpriteNode()
init(strPieceName : String) {
let texture = SKTexture(imageNamed: strPieceName)
super.init(texture: texture, color: UIColor.clear, size: texture.size())
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func addHighlight(withColour strColour : String) {
highlight = SKSpriteNode(imageNamed: strColour)
addChild(highlight)
}
}
In the GameScene class, I call the addHighlight function in touchesBegan
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches {
let cgPointTouched = t.location(in: self)
let skNodeTouched : SKNode = self.atPoint(cgPointTouched)
switch skNodeTouched.name {
case "Highlight":
host.highlight.position = skNodeTouched.position
host.highlight.anchorPoint = CGPoint(x: 0.5, y: 0.5)
host.highlight.size = CGSize(width: 0.2, height: 0.2)
host.highlight.alpha = 1
host.highlight.zPosition = 3
addChild(host.highlight)
}
}
}
The problem with this code is that although the highlight spritenode "follows" the host spritenode, the screen appearance of the position of highlight spritenode is not "on top" of the host spritenode. But strangely, the cgposition of the 2 spritenodes are the same.
I think it has something to coordinates systems of the GameScene and the Highlight class not being the same but I cannot figure out how to solve this and why this is happening.
Not sure why you are doing all this complicated stuff, but you are not suppose to touch the position. Leave it at 0,0, and never add it to the scene. Instead leave it on the sprite, and it will always follow your sprite.
Of course, you can always use colorBlendFactor to blend the sprite color with your texture to create the highlight
Your class should look like this:
class Host : SKSpriteNode {
var highlight : SKSpriteNode!
func addHighlight(withColour strColour : String) {
highlight.removeFromParent()
highlight = SKSpriteNode(imageNamed: strColour)
//highlight.anchorPoint = CGPoint(x: 0.5, y: 0.5)
highlight.size = CGSize(width: 0.2, height: 0.2)
//highlight.alpha = 1
highlight.zPosition = 3
highlight.moveToParent(self)
}
}
There is no need to add extra inits that do exactly the same thing as other inits
Your touch code should look like this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches {
let cgPointTouched = t.location(in: self)
let skNodeTouched : SKNode = self.atPoint(cgPointTouched)
switch skNodeTouched.name {
case "Highlight":
host.addHighlight(withColour:"whatevermycoloris")
}
}
}
Of course, you can always avoid the extra node by just doing:
class Host : SKSpriteNode {
func addHighlight(withColour : UIColor) {
color = withColour
colorBlendFactor = 0.75 //Change this intensity to 1 to add more color
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches {
let cgPointTouched = t.location(in: self)
let skNodeTouched : SKNode = self.atPoint(cgPointTouched)
switch skNodeTouched.name {
case "Highlight":
host.addHighlight(withColour:UIColor.yellow)
}
}
}
Do you know the puzzle game „voi“? That is a game which works with color-XOR-logic. That means: black + black = white.
https://www.youtube.com/watch?v=Aw5BdVcAtII
Is there any way to do the same color logic with two sprite nodes in sprit kit?
Thanks.
Of course, it's possible to do that in Sprite Kit.
Problem:
Let's say you have 2 black squares, squareA and squareB. The user can drag these two squares wherever he wants to. He can drag only one square at a time. You want to color the intersect area to white whenever the two squares intersect.
Initial Setup:
At the top of your scene, there are a few variables that we need to create:
private var squareA: SKSpriteNode?
private var squareB: SKSpriteNode?
private var squares = [SKSpriteNode]()
private var selectedShape: SKSpriteNode?
private var intersectionSquare: SKShapeNode?
squareA and squareB are just the 2 squares that we initially have on screen.
squares is an array and it will store all the squares that are showing on screen.
selectedShape will help us keeping track of the square that is currently being dragged.
intersectionSquare is a white square that represents the intersection area between the two black squares.
Then initialize squareA and squareB, and add them to the squares array like so:
squareA = SKSpriteNode(color: .black, size: CGSize(width: 190.0, height: 190.0))
if let squareA = self.squareA {
squareA.position = CGPoint(x: -200, y: 200)
squareA.name = "Square A"
squares.append(squareA)
self.addChild(squareA)
}
// Do the same for squareB or any other squares that you have on screen..
Note: As you can see, I gave it a name here just to make it easier to differentiate them during the testing phase.
Detect when user is dragging a square:
Now, you need to detect when the user is dragging a square. To do this, you can use:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchDown(atPoint: t.location(in: self)) }
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchMoved(toPoint: t.location(in: self)) }
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchUp(atPoint: t.location(in: self)) }
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchUp(atPoint: t.location(in: self)) }
}
These are just helper methods that are going to make our life easier.
Then, you need to setup touchDown, touchMoved and touchUp methods:
func touchDown(atPoint pos : CGPoint) {
let touchedNode = self.nodes(at: pos)
guard let selectedSquare = touchedNode.first as? SKSpriteNode else {
return
}
selectedShape = selectedSquare
}
func touchMoved(toPoint pos : CGPoint) {
guard let selectedSquare = self.selectedShape else {
return
}
selectedSquare.position = pos
checkIntersectionsWith(selectedSquare)
}
func touchUp(atPoint pos : CGPoint) {
selectedShape = nil
}
To explain you in more details what is going on here:
In the touchDown method:
Well, we need the user to be able to drag only one square at a time. Using the nodes(at:) method, it's easy to know which square was touched, and we can know set our selectedShape variable to be equal to the square that was touched.
In the touchMoved method:
Here we are basically just moving the selectedShape to the position the user moves his finger at. We also call the checkIntersectionsWith() method that we will setup in a second.
In the touchUp method:
The user released his finger from the screen, so we can set the selectedShape to nil.
Change the color of the intersection frame:
Now the most important part to make your game actually look like the one you want to make, is how to change the color of the intersection frame to white when two black squares are intersecting ?
Well, you have different possibilities here, and here is one possible way of doing it:
private func checkIntersectionsWith(_ selectedSquare: SKSpriteNode) {
for square in squares {
if selectedSquare != square && square.intersects(selectedSquare) {
let intersectionFrame = square.frame.intersection(selectedSquare.frame)
intersectionSquare?.removeFromParent()
intersectionSquare = nil
intersectionSquare = SKShapeNode(rect: intersectionFrame)
guard let interSquare = self.intersectionSquare else {
return
}
interSquare.fillColor = .white
interSquare.strokeColor = .clear
self.addChild(interSquare)
} else if selectedSquare != square {
intersectionSquare?.removeFromParent()
intersectionSquare = nil
}
}
}
Every time the checkIntersectionsWith() method is called, we are iterating through the nodes that are inside our squares array, and we check, using the frame's intersection() method, if the selected square intersects with any of these (except itself). If it does, then we create a white square, named intersectionSquare, and set its frame to be equal to the intersection frame.
And to save up your memory usage, you can delete the square from the scene and set intersectionSquare to nil if there is no intersection at all.
Final result:
The final result would look like this:
That's just a rapid draft that I made to show you on you could approach the problem, and obviously there are many things that you could add or improve (apply this to a situation where you have not only 2 but many squares on screen, or create a kind of magnetism effect for when your user release his finger from the screen, etc) but I hope at least it will put you on the right track for your project :)
I am trying to develop a game which need to have a common background across all the scenes using SpriteKit and Swift. Since the background is common and its actions need to be continuous, I created a custom singleton subclass of SKSpriteNode like this:
class BackgroundNode: SKSpriteNode {
static let sharedBackground = BackgroundNode()
private init()
{
let texture = SKTexture(imageNamed: "Background")
super.init(texture: texture, color: UIColor.clearColor(), size: texture.size())
addActors()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private func addActors() {
addClouds()
}
private func addClouds() {
addCloud1()
}
private func addCloud1() {
let cloud1 = SKSpriteNode(imageNamed: "Cloud1")
cloud1.size = CGSizeMake(0.41953125*self.size.width, 0.225*self.size.height)
cloud1.position = CGPointMake(-self.size.width/2, 0.7*self.size.height)
self.addChild(cloud1)
let moveAction = SKAction.moveTo(CGPointMake(self.size.width/2, 0.7*self.size.height), duration: 20)
cloud1.runAction(SKAction.repeatActionForever(moveAction))
}
}
And from the GameScene class I am adding this node to the current view like this:
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
BackgroundNode.sharedBackground.size = CGSizeMake(self.size.width, self.size.height)
BackgroundNode.sharedBackground.position = CGPointMake(self.size.width/2, self.size.height/2)
addChild(BackgroundNode.sharedBackground)
}
}
The background image is showing up correctly, but the cloud is not getting added. As of the code above The cloud should appear out of the screen and animate into and then again out of the screen through the other edge, but to verify if it is getting added, I even tried adding the cloud to the center of the screen without any animations. Still the cloud didn't show up. What can be the issue here? And how to fix it?
EDIT
I figured out that the child is actually getting added but is getting added and moving through some points far above the screen. I also figured out that it MIGHT have something to do with anchor point of the cloud, but whatever value I set as anchor point, the cloud always remains on the top right corner of the screen. What can I do about the anchor points to make the clouds appear as it should(considering the lower left corner as (0, 0) is what I want)
Solved the issue. The problem was that I had to manually set the anchor points of the Scene and the node. Setting the anchor point of both the Scene and the node to (0, 0) solved the issue. The new code looks as follows:
GameScene
override func didMoveToView(view: SKView) {
anchorPoint = CGPointMake(0, 0) //Added this
BackgroundNode.sharedBackground.size = CGSizeMake(self.size.width, self.size.height)
BackgroundNode.sharedBackground.position = CGPointMake(0, 0)
addChild(BackgroundNode.sharedBackground)
}
BackgroundNode
private init()
{
let texture = SKTexture(imageNamed: "Background")
super.init(texture: texture, color: UIColor.clearColor(), size: texture.size())
anchorPoint = CGPointMake(0, 0) //Added this
addActors()
}