I am making a IOS game in Swift using Spritekit, the player has a triangular mesh, that I create with this code:
let viewpath = CGPathCreateMutable();
CGPathMoveToPoint(viewpath, nil, 200, 0);
CGPathAddLineToPoint(viewpath, nil, 0, 400);
CGPathAddLineToPoint(viewpath, nil, 400, 400);
CGPathAddLineToPoint(viewpath, nil, 200, 0);
let playerviewshape = SKShapeNode(path: viewpath);
playerviewshape.fillColor = SKColor(red: 1, green: 1, blue: 1, alpha: 0.1)
playerviewshape.strokeColor = SKColor(red: 1, green: 1, blue: 1, alpha: 0.2)
playerview = SKSpriteNode(texture: view.textureFromNode(playerviewshape));
playerview.zPosition = 0;
playerview.position = CGPoint(x: 0, y: 200);
player.addChild(playerview);
I want to know if any enemies are inside the triangle, So in the update loop I use if(enemy.intersectsNode(playerview)) { however this only checks if it is inside the node's frame, Which is a rectangle around the triangle. I have tried using the triangle as a SKShapeNode and I have also tried using containsPoint instead of intersectsNode.
How can I change the frame/the way I detect intersections, so it will only detect the sprites if they are inside the triangle?
I write some code that can be useful to starting to set your physics:
(P.S. I dont know which type was player so I use playerview just to make an example..)
enum CollisionTypes: UInt32 {
case Playerview = 1 // my hero
case WarField = 2 // the warfield
case Enemy1 = 4 // simple enemies type
case Enemy2 = 8 // 2 level enemies type
case Enemy3 = 16 // boss enemies type
case Enemy4 = 32 // boss of the boss enemies type
}
playerview.physicsBody = SKPhysicsBody(texture: texture, size: texture.size())
playerview.physicsBody!.categoryBitMask = CollisionTypes.Playerview.rawValue
playerview.physicsBody!.contactTestBitMask = CollisionTypes.Enemy1.rawValue | CollisionTypes.Enemy2.rawValue | CollisionTypes.Enemy3.rawValue | CollisionTypes.Enemy4.rawValue
playerview.physicsBody!.collisionBitMask = 0
func didBeginContact(contact: SKPhysicsContact) {
if (contact.bodyA.categoryBitMask == CollisionTypes.Playerview.rawValue &&
contact.bodyB.categoryBitMask == CollisionTypes.Enemy1.rawValue) {
print("The collision was between the playerview and a enemy1")
}
...
}
Related
Since a while I am looking around to find answers to my Issue, but cannot find anything that really helps, or explains what is happening. I also over and over checked everything I found on SO, but couldn't find the answer.
The Issue is happening continuously when displaying Objects in the AR World. iEx I place an object to a Plane on the floor, which is my invisible Shadow Plane. Then it depends all on the viewing angle from the device. To clarify, I added to images, which has just a slightly different viewing angle. Have a look what is happening to the shadows:
I would like to have a good shadow all the time and not such artefacts as you can see.
Note: I already played around using the shadowSampleCount, the Bias, and all the other options, that should help to get a proper, low rendering cost shadow.
Here are is the extract of the relevant code for Lighting and Plane, Material, etc
For the SCNLight:
class func directionalLight() -> SCNLight {
let light = SCNLight()
light.type = .directional
light.castsShadow = true
light.color = UIColor.white
light.shadowMode = .deferred
light.shadowSampleCount = 8
light.shadowRadius = 1
// light.automaticallyAdjustsShadowProjection = false
light.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.75)
light.categoryBitMask = -1
return light
}
and how I add it:
func setupLights() {
lightDirectionNode.light = Lighting.directionalLight()
// lightDirectionNode.eulerAngles = SCNVector3(-66.degreesToRadians, 0, 0)
lightDirectionNode.eulerAngles = SCNVector3(0, 90.degreesToRadians, 45.degreesToRadians)
sceneView.scene.rootNode.addChildNode(lightDirectionNode)
}
For the SCNPlane:
class func shadowPlane() -> SCNNode {
let objectShape = SCNPlane(width: 200, height: 200)
objectShape.heightSegmentCount = 2
objectShape.widthSegmentCount = 2
objectShape.cornerRadius = 100
objectShape.cornerSegmentCount = 16
let objectNode = SCNNode(geometry: objectShape)
objectNode.geometry?.firstMaterial?.diffuse.contents = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
objectNode.geometry?.firstMaterial?.colorBufferWriteMask = SCNColorMask(rawValue: 0)
objectNode.physicsBody = Physics.floorPhysicsBody(shape: objectShape)
objectNode.name = "floor"
objectNode.renderingOrder = -10 // renderingOrder // 0
return objectNode
}
and how I add it:
func setupShadowPlane() {
let shadowPlane = NodeFactory.shadowPlane()
// Set the Node's properties
shadowPlane.position = SCNVector3(x: (focusSquare.lastPosition?.x)!, y: (focusSquare.lastPosition?.y)!, z: (focusSquare.lastPosition?.z)!)
shadowPlane.eulerAngles = SCNVector3(-90.degreesToRadians, 0.0, 0.0)
sceneView.scene.rootNode.addChildNode(shadowPlane)
}
What am I doing wrong? Can anyone help?
There are 3 more instance properties to take into consideration:
var shadowRadius: CGFloat { get set }
var shadowCascadeCount: Int { get set }
var shadowCascadeSplittingFactor: CGFloat { get set }
If you don't setup these ones they definitely cause rendering artifacts.
let lightNode = SCNNode()
lightNode.light = SCNLight()
// POSITION OF DIRECTIONAL LIGHT ISN'T IMPORTANT.
// ONLY DIRECTION IS CRUCIAL FOR DIRECTIONAL LIGHTS.
lightNode.rotation = SCNVector4(x: 0, y: 0, z: 0, w: 1)
lightNode.light!.type = .directional
lightNode.light!.castsShadow = true
lightNode.light?.shadowMode = .deferred
/* THREE INSTANCE PROPERTIES TO SETUP */
lightNode.light?.shadowRadius = 3.25
lightNode.light?.shadowCascadeCount = 3
lightNode.light?.shadowCascadeSplittingFactor = 0.09
lightNode.light?.shadowColor = UIColor(white: 0, alpha: 0.75)
scene.rootNode.addChildNode(lightNode)
And one more thing – when Auto Adjust is off:
Light, like a camera, has near and far clipping planes for setup.
lightNode.light?.zNear = 0
lightNode.light?.zFar = 1000000 // Far Clipping Plane is important
Hope this helps.
I have a SpriteKit scene with a sprite. The sprite has a physics body derived from the texture's alpha to get an accurate physics shape like so:
let texture_bottle = SKTexture(imageNamed:"Bottle")
let sprite_bottle = SKSpriteNode(texture: texture_bottle)
physicsBody_bottle = SKPhysicsBody(texture: texture_bottle, size: size)
physicsBody_bottle.affectedByGravity = false
sprite_bottle.physicsBody = physicsBody_bottle
root.addChild(sprite_bottle)
....
func touchesBegan(_ touches: Set<UITouch>?, with event: UIEvent?, touchLocation: CGPoint!) {
let hitNodes = self.nodes(at: touchLocation)
}
When a user taps the screen, how can I detect if they actually touched within the physics body shape (not the sprite's rect)?
You "can't" (Not easily)
UITouch commands are based on CGRects, so let hitNodes = self.nodes(at: touchLocation) is going to be filled with any node who's frame intersects with that touch.
This can't be avoided, so the next step is to determine pixel accuracy from the nodes that registered as "hit". The first thing you should do is convert the touch position to local coordinates to your sprite.
for node in hitNodes
{
//assuming touchLocation is based on screen coordinates
let localLocation = node.convertPoint(touchLocation,from:scene)
}
Then from this point you need to figure out which method you want to use.
If you need speed, then I would recommend creating a 2D boolean array that behaves as a mask, and fill this array with false for transparent areas and true for opaque areas. Then you can use localLocation to point to a certain index of the array (Remember to add anchorPoint * width and height to your x and y values then cast to int)
func isHit(node: SKNode,mask: [[Boolean]],position:CGPoint) -> Boolean
{
return mask[Int(node.size.height * node.anchorPoint.y + position.y)][Int(node.size.width * node.anchorPoint.x + position.x)]
}
If speed is not of concern, then you can create a CGContext, fill your texture into this context, and then check if the point in the context is transparent or not.
Something like this would help you out:
How do I get the RGB Value of a pixel using CGContext?
//: Playground - noun: a place where people can play
import UIKit
import XCPlayground
extension CALayer {
func colorOfPoint(point:CGPoint) -> UIColor
{
var pixel:[CUnsignedChar] = [0,0,0,0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.PremultipliedLast.rawValue)
let context = CGBitmapContextCreate(&pixel, 1, 1, 8, 4, colorSpace,bitmapInfo.rawValue)
CGContextTranslateCTM(context, -point.x, -point.y)
self.renderInContext(context!)
let red:CGFloat = CGFloat(pixel[0])/255.0
let green:CGFloat = CGFloat(pixel[1])/255.0
let blue:CGFloat = CGFloat(pixel[2])/255.0
let alpha:CGFloat = CGFloat(pixel[3])/255.0
//println("point color - red:\(red) green:\(green) blue:\(blue)")
let color = UIColor(red:red, green: green, blue:blue, alpha:alpha)
return color
}
}
extension UIColor {
var components:(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
var r:CGFloat = 0
var g:CGFloat = 0
var b:CGFloat = 0
var a:CGFloat = 0
getRed(&r, green: &g, blue: &b, alpha: &a)
return (r,g,b,a)
}
}
//get an image we can work on
var imageFromURL = UIImage(data: NSData(contentsOfURL: NSURL(string:"https://www.gravatar.com/avatar/ba4178644a33a51e928ffd820269347c?s=328&d=identicon&r=PG&f=1")!)!)
//only use a small area of that image - 50 x 50 square
let imageSliceArea = CGRectMake(0, 0, 50, 50);
let imageSlice = CGImageCreateWithImageInRect(imageFromURL?.CGImage, imageSliceArea);
//we'll work on this image
var image = UIImage(CGImage: imageSlice!)
let imageView = UIImageView(image: image)
//test out the extension above on the point (0,0) - returns r 0.541 g 0.78 b 0.227 a 1.0
var pointColor = imageView.layer.colorOfPoint(CGPoint(x: 0, y: 0))
let imageRect = CGRectMake(0, 0, image.size.width, image.size.height)
UIGraphicsBeginImageContext(image.size)
let context = UIGraphicsGetCurrentContext()
CGContextSaveGState(context)
CGContextDrawImage(context, imageRect, image.CGImage)
for x in 0...Int(image.size.width) {
for y in 0...Int(image.size.height) {
var pointColor = imageView.layer.colorOfPoint(CGPoint(x: x, y: y))
//I used my own creativity here - change this to whatever logic you want
if y % 2 == 0 {
CGContextSetRGBFillColor(context, pointColor.components.red , 0.5, 0.5, 1)
}
else {
CGContextSetRGBFillColor(context, 255, 0.5, 0.5, 1)
}
CGContextFillRect(context, CGRectMake(CGFloat(x), CGFloat(y), 1, 1))
}
}
CGContextRestoreGState(context)
image = UIGraphicsGetImageFromCurrentImageContext()
where you would eventually call colorOfPoint(point:localLocation).cgColor.alpha > 0 to determine if you are touching a node or not.
Now I would recommend you make colorOfPoint an extension of SKSpriteNode, so be creative with the code posted above.
func isHit(node: SKSpriteNode,position:CGPoint) -> Boolean
{
return node.colorOfPoint(point:localLocation).cgColor.alpha > 0
}
Your final code would look something like this:
hitNodes = hitNodes.filter
{
node in
//assuming touchLocation is based on screen coordinates
let localLocation = node.convertPoint(touchLocation,from:node.scene)
return isHit(node:node,mask:mask,position:localLocation)
}
OR
hitNodes = hitNodes.filter
{
node in
//assuming touchLocation is based on screen coordinates
let localLocation = node.convertPoint(touchLocation,from:node.scene)
return isHit(node:node,position:localLocation)
}
which is basically filtering out all nodes that were in the detected by the frame comparison, leaving you pixel perfect touched nodes.
Note: The code from the separate SO link may need to be converted to Swift 4.
I am attempting to generate a 3d voxel style island and am generating bricks and placing them on the scene. However, when I set the z axis on the camera node to more than 150, objects disappear behind the white background.
import Cocoa
import SceneKit
import PlaygroundSupport
let view = SCNView()
let scene = SCNScene()
view.scene = scene
view.frame = CGRect(x: 0, y: 0, width: 650, height: 650)
public func buildIsland(size: Int, image: NSImage, scene: SCNScene){
//Start building the island
var blocks = 0
for x in 0...size {
for y in 0...size {
//Create Block
var block = SCNBox(width: 10, height: 10, length: 10, chamferRadius: 0)
var color = SCNMaterial()
color.diffuse.contents = CGColor.init(red: 0, green: 1, blue: 0, alpha: 1)
block.materials[0] = color
var node = SCNNode(geometry: block)
node.position = SCNVector3(x/2, y/2, 0)
scene.rootNode.addChildNode(node)
blocks = blocks + 1
}
}
}
view.autoenablesDefaultLighting = true
var cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 200)
scene.rootNode.addChildNode(cameraNode)
buildIsland(size: 4, image: NSImage(), scene: scene)
view.allowsCameraControl = true
PlaygroundPage.current.liveView = view
Configure the zFar property of the camera to avoid clipping when the camera is further away. (The default value is 100)
For example:
cameraNode.camera?.zFar = 500
You can play with this property to get everything visible while trying to keep good performance.
Currently, when running this code, a colored ball runs from the top of the screen to the bottom. When it reaches the bottom it respawns at the top and drops again. I need the ball to change to a random color each time it spawns again. I have a colorChange action but it still doesn't change the color of the ball when it runs.
import SpriteKit
class GameScene: SKScene {
let ball = SKShapeNode(circleOfRadius: 20)
let label = SKLabelNode(fontNamed: "Futura")
let colorChange = SKAction.colorizeWithColor(.blueColor(), colorBlendFactor: 1, duration: 1)
let randomRed = Int(arc4random_uniform(255))
let randomGreen = Int(arc4random_uniform(255))
let randomBlue = Int(arc4random_uniform(255))
override func didMoveToView(view: SKView) {
let sceneBody = SKPhysicsBody(edgeLoopFromRect: self.frame)
self.physicsBody = sceneBody
//Ball Transition
let ballTransition = SKAction.sequence([SKAction.fadeInWithDuration(1)])
ball.runAction(ballTransition)
ball.fillColor = UIColor(red: 100, green: 100, blue: 0, alpha: 1)
ball.physicsBody = SKPhysicsBody(circleOfRadius: 25)
ball.physicsBody?.affectedByGravity = false
ball.position = CGPoint(x: self.frame.size.width/2, y: CGFloat(self.frame.size.height*1))
ballMovement()
self.addChild(ball)
self.addChild(label)
}
func ballMovement() {
let moveBall = SKAction.moveToY(0, duration: 3)
let goBackUp = SKAction.moveToY(self.frame.size.height, duration:0)
let keepFalling = SKAction.sequence([moveBall, goBackUp, colorChange])
ball.runAction(SKAction.repeatActionForever(keepFalling))
//Label Sprite
label.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
label.fontColor = SKColor.redColor()
label.fontSize = 30
}
override func update(currentTime: CFTimeInterval) {
label.text = "\(ball.position.y)"
if ball.position.y < 26 {
ball.position = CGPoint(x: self.frame.size.width/2, y: CGFloat(self.frame.size.height*1))
}
}
}
Instead of using an SKAction to change the color, just do
ball.color = UIColor...
To generate a random color check this out: How to make a random background color with Swift
and use that to generate a random color
ball.color = .randomColor()
I suggest you don't use an SKShape for an important piece that moves because I find that they can slow down the app and cause problems. I would use an SKSpritenode instead for the ball. If you then make the ball default as all white, you can change the colour by doing this:
ball.colorBlendFactor = 1
ball.color = SKColor(hue: randomNumberFunctionBetweenZeroAndOne(), saturation: 0.75, contrast: 0.5)
The hue should be a random value between 0 and 1.
If you put this inside a function, you could call the function to change the colour of the ball inside and SKAction or anywhere else.
The documentation in XCode clearly states that hitTesting a geometry in SceneKit can be done with SCNRender, SCNView or the SCNNode themselves when one plans to test a 3D line segment. I have a use for SCNScene with its nodes without a renderer or a view, therefore I am planning to use SCNNode hitTesting. I create a SCNScene, put a SCNNode in it and test a simple ray that goes through, but I always get an empty hitList and I don't understand why:
import Swift
import SceneKit
let boxGeometry = SCNBox(width: 1.0, height: 1.0, length: 1.0, chamferRadius: 0)
let boxNode = SCNNode(geometry: boxGeometry)
var scene = SCNScene()
scene.rootNode.addChildNode(boxNode)
let from = SCNVector3(x: 0, y: -2, z: 0)
let to = SCNVector3(x: 0, y: 2 , z: 0)
var hits = scene.rootNode.hitTestWithSegmentFromPoint(from, toPoint: to, options:nil) // this is always empty
if hits != nil {
if hits!.count > 0 {
var hit = (hits!.first as! SCNHitTestResult).node as SCNNode
}
}
I have tried passing various forms of options but nothing changes.
SCNHitTestFirstFoundOnlyKey: yes or no does not change anything
SCNHitTestSortResultsKey: yes or no does not change anything
SCNHitTestClipToZRangeKey: invalid for SCNNode
SCNHitTestBackFaceCullingKey: yes or no does not change anything
SCNHitTestBoundingBoxOnlyKey: yes or no does not change anything
SCNHitTestRootNodeKey: rootNOde of scene or boxNode does not change
anything
SCNHitTestIgnoreHiddenNodesKey: yes or no does not change anything
What am I doing wrong?
I have found the answer, which is either a bug or a feature: using SCNScene and its nodes SCNNode for 3D hitTesting, in particular the method: "hitTestWithSegmentFromPoint(toPoint:options:)" does not return a hit unless the scene is included in an SCNView. It appears it cannot be used offscreen. My guess is yours for why this is the case, although I can imagine it has something to do with performing some of these quite expensive calculations on the graphics card.
I have tested this using the GameView SCNScene starter project. The critical line is self.gameView!.scene = scene
override func awakeFromNib(){
let scene = SCNScene()
let boxGeometry = SCNBox(width: 1.0, height: 1.0, length: 1.0, chamferRadius: 0.0)
let boxNode = SCNNode(geometry: boxGeometry)
boxNode.position=SCNVector3(x: 0, y: 0, z: 0)
scene.rootNode.addChildNode(boxNode)
// create and add a camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
// place the camera
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
// create and add a light to the scene
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = SCNLightTypeOmni
lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
scene.rootNode.addChildNode(lightNode)
// create and add an ambient light to the scene
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = SCNLightTypeAmbient
ambientLightNode.light!.color = NSColor.darkGrayColor()
scene.rootNode.addChildNode(ambientLightNode)
// set the scene to the view
// uncomment this to fail
self.gameView!.scene = scene
// allows the user to manipulate the camera
self.gameView!.allowsCameraControl = true
// show statistics such as fps and timing information
self.gameView!.showsStatistics = true
// configure the view
self.gameView!.backgroundColor = NSColor.blackColor()
let hitList = scene.rootNode.hitTestWithSegmentFromPoint(SCNVector3(x:-10,y:0,z:0), toPoint: SCNVector3(x:10,y:0,z:0), options:[SCNHitTestBackFaceCullingKey:false, SCNHitTestSortResultsKey:true, SCNHitTestIgnoreHiddenNodesKey:false])
if hitList?.count > 0 {
println("Hit found: \n\n\( hitList![0] )") // assign self.gameView!.scene = scene to reach this point.
} else {
println("No hit") // uncomment self.gameView!.scene = scene to reach this point.
}
}
I've also had trouble with hitTestWithSegmentFromPoint.
I was calling it in viewDidLoad() and it returned a 0 elements array, though I was sure there was a hit.
Calling it in viewDidAppear() (or later) solved my problem.