How can I properly make a SKLabelNode follow a body with Physics? - xcode

I created class whose behavior is really simple: every object of this class will be a square with a name on top and they will float randomly around the screen.
class XSquare: SKNode {
private var squareShape: SKSpriteNode
private var text: SKLabelNode
init() {
// Sets up the square
squareShape = SKSpriteNode(color: .red, size: CGSize(width: 100, height: 100))
// Sets up the text
text = SKLabelNode(text: "Here goes the name")
text.position = CGPoint(x: 0, y: 100)
// Calls the super initializator
super.init()
// Sets up the square
squareShape.physicsBody = SKPhysicsBody(rectangleOf: squareShape.size)
squareShape.physicsBody?.categoryBitMask = squareBitMask
squareShape.physicsBody?.collisionBitMask = wallBitMask
squareShape.physicsBody?.velocity = CGVector(dx: 10, dy: 10)
squareShape.physicsBody?.angularVelocity = CGFloat(5)
squareShape.physicsBody?.allowsRotation = true
squareShape.physicsBody?.pinned = false
squareShape.physicsBody?.affectedByGravity = false
addChild(squareShape)
addChild(text)
}
}
The reason why I used a SKNode for this class is because I wanted to provide a little text right on the top of every square (a name indicator).
Everything looks fine but when I run the code the name stays fixed while the squares move randomly around the screen (probably because I'm not moving the square with SKAction but with PhysicsBody). By the other hand, If I use squareShape.addChild(text) the text will also rotate following the physics of the square.
I'm a newbie using SpriteKit and I'm sure I'm missing something. Can anyone help me understand?

Just have your text node do the opposite angle. If your shape rotates 10 degrees left, you rotate text 10 degrees right.
Now keep in mind it assumes both anchor points are anchored in the center. You may want to consider a second SKNode that you place between your shape and your text if you want to have the text somewhere else. Then you would reverse rotate the new node instead of the text.
Here is an example on how to do that.
class XSquare: SKNode {
private var squareShape: SKSpriteNode
private var text: SKLabelNode
init() {
// Sets up the square
squareShape = SKSpriteNode(color: .red, size: CGSize(width: 100, height: 100))
// Sets up the text
text = SKLabelNode(text: "Here goes the name")
text.position = CGPoint(x: 0, y: 100)
// Calls the super initializator
super.init()
// Sets up the square
squareShape.physicsBody = SKPhysicsBody(rectangleOf: squareShape.size)
squareShape.physicsBody?.categoryBitMask = squareBitMask
squareShape.physicsBody?.collisionBitMask = wallBitMask
squareShape.physicsBody?.velocity = CGVector(dx: 10, dy: 10)
squareShape.physicsBody?.angularVelocity = CGFloat(5)
squareShape.physicsBody?.allowsRotation = true
squareShape.physicsBody?.pinned = false
squareShape.physicsBody?.affectedByGravity = false
addChild(squareShape)
squareShape.addChild(text)
}
}
func didFinishUpdate(){
text.zRotation = -squareShape.zRotation
}

Related

Clock minute-hand disappears when attempting to rotate it

Modus Operandi:
1) Use an UIImageView of a base Clock Image.
2) Add MinuteHand & HourHand sublayers (containing their respective images) to the UIImageView layer.
Problem: both sublayers disappear when attempting to perform a rotation transformation.
Note: 1) I've removed the 'hour' code & ancillary radian calculations to simplify code.
2) The 'center' is the center of the clock. I had adjusted the coordinates to actually pin the hands to the clock's center.
3) The ViewDidLayoutSubviews() appear to be okay. I got the clock + hands.
class ClockViewController:UIViewController {
private let minuteLayer = CALayer()
#IBOutlet weak var clockBaseImageView: UIImageView!
#IBOutlet weak var datePicker: UIDatePicker!
override func viewDidLayoutSubviews() {
guard var minuteSize = UIImage(named: "MinuteHand")?.size,
var hourSize = UIImage(named: "HourHand")?.size
else {
return
}
var contentLayer:CALayer {
return self.view.layer
}
var center = clockBaseImageView.center
// Minute Hand:
minuteLayer.setValue("*** Minute Hand ***", forKey: "id")
minuteSize = CGSize(width: minuteSize.width/3, height: minuteSize.height/3)
minuteLayer.contents = UIImage(named: "MinuteHand")?.cgImage
center = CGPoint(x: 107.0, y: 40.0)
var handFrame = CGRect(origin: center, size: minuteSize)
minuteLayer.frame = handFrame
minuteLayer.contentsScale = clockBaseImageView.layer.contentsScale
minuteLayer.anchorPoint = center
clockBaseImageView.layer.addSublayer(minuteLayer)
}
Here's my problem: Attempting to rotate the minute hand via 0.01 radians:
func set(_ time:Date) {
minuteLayer.setAffineTransform(CGAffineTransform(rotationAngle: .01)) // random value for test.
}
Before rotation attempt:
After attempting to rotate minute hand:
The hand shifted laterally to the right vs rotate.
Why? Perhaps due to the pivot point?
I think this will solve your problem, Take a look and let me know.
import GLKit // Importing GLKit Framework
func set(_ time:Date) {
minuteLayer.transform = CGAffineTransformMakeRotation(CGFloat(GLKMathDegreesToRadians(0.01)))
}
Note: this solution doesn't solve the issue about rotating a CALayer. Instead, it bypasses the issue by replacing the layer with a subview and rotating the subview via:
func set(_ time:Date) {
minuteView.transform = CGAffineTransform(rotationAngle: 45 * CGFloat(M_PI)/180.0)
}
Here's the result:
Still, it would be nice to know how to rotate a CALayer.

CustomView looks odd in my project, but good in the playground

So I've created a custom NSButton to have a beautiful radio button, but I'm experiencing a very weird bug.
My radio button looks good in the playground, but when I add it to my project, it looks odd.
Here are screenshots:
Left = in the playground.
Right = in my project.
As you can see, on the right (in my project), the blue dot looks horrible, it's not smooth, same thing for the white circle (it's less visible with the dark background).
In my project, the NSShadow on my CALayer is also flipped, even if the geometryFlipped property on my main (_containerLayer_) CALayer is set to true. -> FIXED: see #Bannings answer.
import AppKit
extension NSColor {
static func colorWithDecimal(deviceRed deviceRed: Int, deviceGreen: Int, deviceBlue: Int, alpha: Float) -> NSColor {
return NSColor(
deviceRed: CGFloat(Double(deviceRed)/255.0),
green: CGFloat(Double(deviceGreen)/255.0),
blue: CGFloat(Double(deviceBlue)/255.0),
alpha: CGFloat(alpha)
)
}
}
extension NSBezierPath {
var CGPath: CGPathRef {
return self.toCGPath()
}
/// Transforms the NSBezierPath into a CGPathRef
///
/// :returns: The transformed NSBezierPath
private func toCGPath() -> CGPathRef {
// Create path
let path = CGPathCreateMutable()
var points = UnsafeMutablePointer<NSPoint>.alloc(3)
let numElements = self.elementCount
if numElements > 0 {
var didClosePath = true
for index in 0..<numElements {
let pathType = self.elementAtIndex(index, associatedPoints: points)
switch pathType {
case .MoveToBezierPathElement:
CGPathMoveToPoint(path, nil, points[0].x, points[0].y)
case .LineToBezierPathElement:
CGPathAddLineToPoint(path, nil, points[0].x, points[0].y)
didClosePath = false
case .CurveToBezierPathElement:
CGPathAddCurveToPoint(path, nil, points[0].x, points[0].y, points[1].x, points[1].y, points[2].x, points[2].y)
didClosePath = false
case .ClosePathBezierPathElement:
CGPathCloseSubpath(path)
didClosePath = true
}
}
if !didClosePath { CGPathCloseSubpath(path) }
}
points.dealloc(3)
return path
}
}
class RadioButton: NSButton {
private var containerLayer: CALayer!
private var backgroundLayer: CALayer!
private var dotLayer: CALayer!
private var hoverLayer: CALayer!
required init?(coder: NSCoder) {
super.init(coder: coder)
self.setupLayers(radioButtonFrame: CGRectZero)
}
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
let radioButtonFrame = CGRect(
x: 0,
y: 0,
width: frameRect.height,
height: frameRect.height
)
self.setupLayers(radioButtonFrame: radioButtonFrame)
}
override func drawRect(dirtyRect: NSRect) {
}
private func setupLayers(radioButtonFrame radioButtonFrame: CGRect) {
//// Enable view layer
self.wantsLayer = true
self.setupBackgroundLayer(radioButtonFrame)
self.setupDotLayer(radioButtonFrame)
self.setupHoverLayer(radioButtonFrame)
self.setupContainerLayer(radioButtonFrame)
}
private func setupContainerLayer(frame: CGRect) {
self.containerLayer = CALayer()
self.containerLayer.frame = frame
self.containerLayer.geometryFlipped = true
//// Mask
let mask = CAShapeLayer()
mask.path = NSBezierPath(ovalInRect: frame).CGPath
mask.fillColor = NSColor.blackColor().CGColor
self.containerLayer.mask = mask
self.containerLayer.addSublayer(self.backgroundLayer)
self.containerLayer.addSublayer(self.dotLayer)
self.containerLayer.addSublayer(self.hoverLayer)
self.layer!.addSublayer(self.containerLayer)
}
private func setupBackgroundLayer(frame: CGRect) {
self.backgroundLayer = CALayer()
self.backgroundLayer.frame = frame
self.backgroundLayer.backgroundColor = NSColor.whiteColor().CGColor
}
private func setupDotLayer(frame: CGRect) {
let dotFrame = frame.rectByInsetting(dx: 6, dy: 6)
let maskFrame = CGRect(origin: CGPointZero, size: dotFrame.size)
self.dotLayer = CALayer()
self.dotLayer.frame = dotFrame
self.dotLayer.shadowColor = NSColor.colorWithDecimal(deviceRed: 46, deviceGreen: 146, deviceBlue: 255, alpha: 1.0).CGColor
self.dotLayer.shadowOffset = CGSize(width: 0, height: 2)
self.dotLayer.shadowOpacity = 0.4
self.dotLayer.shadowRadius = 2.0
//// Mask
let maskLayer = CAShapeLayer()
maskLayer.path = NSBezierPath(ovalInRect: maskFrame).CGPath
maskLayer.fillColor = NSColor.blackColor().CGColor
//// Gradient
let gradientLayer = CAGradientLayer()
gradientLayer.frame = CGRect(origin: CGPointZero, size: dotFrame.size)
gradientLayer.colors = [
NSColor.colorWithDecimal(deviceRed: 29, deviceGreen: 114, deviceBlue: 253, alpha: 1.0).CGColor,
NSColor.colorWithDecimal(deviceRed: 59, deviceGreen: 154, deviceBlue: 255, alpha: 1.0).CGColor
]
gradientLayer.mask = maskLayer
//// Inner Stroke
let strokeLayer = CAShapeLayer()
strokeLayer.path = NSBezierPath(ovalInRect: maskFrame.rectByInsetting(dx: 0.5, dy: 0.5)).CGPath
strokeLayer.fillColor = NSColor.clearColor().CGColor
strokeLayer.strokeColor = NSColor.blackColor().colorWithAlphaComponent(0.12).CGColor
strokeLayer.lineWidth = 1.0
self.dotLayer.addSublayer(gradientLayer)
self.dotLayer.addSublayer(strokeLayer)
}
private func setupHoverLayer(frame: CGRect) {
self.hoverLayer = CALayer()
self.hoverLayer.frame = frame
//// Inner Shadow
let innerShadowLayer = CAShapeLayer()
let ovalPath = NSBezierPath(ovalInRect: frame.rectByInsetting(dx: -10, dy: -10))
let cutout = NSBezierPath(ovalInRect: frame.rectByInsetting(dx: -1, dy: -1)).bezierPathByReversingPath
ovalPath.appendBezierPath(cutout)
innerShadowLayer.path = ovalPath.CGPath
innerShadowLayer.shadowColor = NSColor.blackColor().CGColor
innerShadowLayer.shadowOpacity = 0.2
innerShadowLayer.shadowRadius = 2.0
innerShadowLayer.shadowOffset = CGSize(width: 0, height: 2)
self.hoverLayer.addSublayer(innerShadowLayer)
//// Inner Stroke
let strokeLayer = CAShapeLayer()
strokeLayer.path = NSBezierPath(ovalInRect: frame.rectByInsetting(dx: -0.5, dy: -0.5)).CGPath
strokeLayer.fillColor = NSColor.clearColor().CGColor
strokeLayer.strokeColor = NSColor.blackColor().colorWithAlphaComponent(0.22).CGColor
strokeLayer.lineWidth = 2.0
self.hoverLayer.addSublayer(strokeLayer)
}
}
let rbFrame = NSRect(
x: 87,
y: 37,
width: 26,
height: 26
)
let viewFrame = CGRect(
x: 0,
y: 0,
width: 200,
height: 100
)
let view = NSView(frame: viewFrame)
view.wantsLayer = true
view.layer!.backgroundColor = NSColor.colorWithDecimal(deviceRed: 40, deviceGreen: 40, deviceBlue: 40, alpha: 1.0).CGColor
let rb = RadioButton(frame: rbFrame)
view.addSubview(rb)
I'm using the exact same code on both my project and in the playground.
Here is a zip containing the playground and the project.
Just to be clear: I want to know why circles drawings are smooth in the playground but not in projects. (See #Bannings answer, it's more obvious with his screenshots)
Took time but I think I finally figured out everything or almost everything.
First some science : Circles or arcs can't be represented through Bézier curves. That's a property of Bézier curves as stated here : https://en.wikipedia.org/wiki/Bézier_curve
So when using NSBezierPath(ovalInRect:) you are in fact generating a Bézier curve approximating a circle. This can lead to a difference in appearance and how the shape renders. Still this shouldn't be a problem in our case because the difference is between two Bézier curves (the one in Playground and the one in real OS X project), but still I find it interesting to note in case you think the circle is not perfect enough.
Second as stated in this question (How to draw a smooth circle with CAShapeLayer and UIBezierPath) there are differences in the way the antialiasing will apply to your path depending on where the path is used. NSView's drawRect: being the place where the path antialiasing will be the best and CAShapeLayer being the worst.
Also I found that the CAShapeLayer documentation has a note saying this :
Shape rasterization may favor speed over accuracy. For example, pixels with multiple intersecting path segments may not give exact results.
Glen Low's answer to the question I previously mentioned seem to work fine in our case :
layer.rasterizationScale = 2.0 * self.window!.screen!.backingScaleFactor;
layer.shouldRasterize = true;
See the differences here :
Another solution is to leverage corner radiuses instead of Bézier paths in order to simulate a circle, and this time it's pretty accurate :
Finally my assumption on the difference between the Playground & the real OS X project is that Apple configured Playground so that some optimizations are turned off so the path even thought drawn using CAShapeLayer gets the best anti-aliasing possible. After all you're prototyping, performances are not really important especially on drawing operations.
I'm not sure to be right on this but I think it wouldn't be surprising. If anyone have any source I'd happily add it.
To me the best solution if you really need the best circle possible is to use corner radiuses.
Also as stated by #Bannings in another answer to this post. Shadows are reversed due to the fact playground render in a different coordinate system. See his answer to fix this.
I just fixed it by replace this line code:
self.dotLayer.shadowOffset = CGSize(width: 0, height: 2)
with:
self.dotLayer.shadowOffset = CGSize(width: 0, height: -2)
and replace innerShadowLayer.shadowOffset = CGSize(width: 0, height: 2) with:
innerShadowLayer.shadowOffset = CGSize(width: 0, height: -2)
I think you will get the same result like this:
Left = in the playground.
Right = in my project.
It seems the Playground is showing the bezier path in LLO coordinate system, you can visit the link:
https://forums.developer.apple.com/message/39277#39277

Swift Activity Indicator and Label

I'm trying to do a simple task of creating a activity indicator with a label that says, "loading" or "saving" or whatever I program it to say when it is running. I can not seem to figure out how to get it to be directly under my activity indicator though, right now it is right along the side of it and I want to to be centered below it.
Here is my code:
public func show(viewController : UIViewController) {
Async.main {
self.spinner = UIActivityIndicatorView(frame: CGRectMake(0, 0, self.size, self.size))
self.activityLabel = UILabel(frame: CGRectMake(0,0,200,200))
if let spinner = self.spinner {
spinner.activityIndicatorViewStyle = self.style
let screenSize: CGRect = UIScreen.mainScreen().bounds
spinner.center = CGPoint (x: screenSize.width/2 , y: screenSize.height/2)
spinner.hidesWhenStopped = true
viewController.view.addSubview(spinner)
spinner.startAnimating()
self.activityLabel?.center = CGPoint (x: screenSize.width/2 , y: screenSize.height/1.9 )
self.activityLabel?.text = self.textmessage
viewController.view.addSubview(self.activityLabel!)
}
}
}
Thanks for any help!
I think the problem you are having with the label being misaligned is the fact that you didn't set the label's textAlignment to .center. You need to add:
self.activityLabel.textAlignment = .center
That should put the text inside the label directly under the spinner.

What is happening to my sprite when using init(rect:inTexture:)?

I'm writing an isometric game using SpriteKit and swift, and I'm having trouble with sprites for my sheep. They have a basic AI that randomly moves them around, but when it draws I get two big black horizontal lines instead of a sheep. I'm using init(rect:inTexture:) to take the first sub-image of my sprite sheet. I'm only doing one sheep to test my code. Here's my code:
//
// Animal.swift
// Anointed
//
// Created by Jacob Jackson on 4/26/15.
// Copyright (c) 2015 ThinkMac Innovations. All rights reserved.
//
import Foundation
import SpriteKit
/* DEFINES AN ANIMAL */
class Animal : SKSpriteNode {
let animalName: String //name
let descript: String //description
let spriteSheetName: String //image name for item icon
var deltaT = 0.0
var startTime = 0.0
init( name: String, desc: String, sheetName: String ) {
animalName = name
descript = desc
spriteSheetName = sheetName
let texture = SKTexture(rect: CGRect(x: 0, y: 0, width: 32, height: 32), inTexture: SKTexture(imageNamed: spriteSheetName)) //make a texture for the animal's initial state
super.init(texture: texture, color: SKColor.clearColor(), size: texture.size()) //sets up tex, bgcolor, and size
}
func updateAI( time: CFTimeInterval ) {
if startTime == 0.0 {
startTime = time
} else {
deltaT = time - startTime
}
if deltaT > 2 {
moveRandom()
deltaT = 0.0
startTime = time
}
}
func moveRandom() {
var direction = random() % 4
switch( direction ) {
case 0: moveUP()
break
case 1: moveRT()
break
case 2: moveDN()
break
case 3: moveLT()
break
default:
break
}
}
func moveUP() {
let moveUp = SKAction.moveByX(0, y: 64, duration: 1.0)
self.runAction(moveUp)
}
func moveRT() {
let moveRt = SKAction.moveByX(32, y: 0, duration: 1.0)
self.runAction(moveRt)
}
func moveLT() {
let moveLt = SKAction.moveByX(-32, y: 0, duration: 1.0)
self.runAction(moveLt)
}
func moveDN() {
let moveDn = SKAction.moveByX(0, y: -64, duration: 1.0)
self.runAction(moveDn)
}
/* FROM APPLE. APPARENTLY NECESSARY IF I'M INHERITING FROM SKSpriteNode */
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Then, to create the one sheep for a test:
var sheep1 = Animal(name: "Sheep", desc: "A White Sheep", sheetName: "whiteSheep")
var CaveStable = Location(n:"Cave Stable", d:"A small stable inside a cave, near an Inn", g:tempGrid, a:[sheep1]) //uses temporary grid defined above as the "Cave Stable" location where Jesus is born
Then, to place the sheep randomly based on an array of animals for each "location" (like a level):
for x in 0...theGame.currentLocation.animals.count-1 {
var animal = theGame.currentLocation.animals[x]
var pos = twoDToIso(CGPoint(x: (random() % (theGame.currentLocation.grid.count-1))*64, y: (random() % (theGame.currentLocation.grid[0].count-1))*64))
animal.position = CGPoint(x: pos.x, y: pos.y + animal.size.height / 2)
world.addChild(animal)
}
Then, in my scene code:
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
theGame.currentLocation.animals[0].updateAI(currentTime)
/* other stuff below here */
My whiteSheep sprite sheet looks like this:
Finally, this is what it looks like when the game is running:
The black lines move randomly, like the sheep should be doing - but what is going on with the graphics? Weirdness. Anybody have an idea what's going on?
The initialiser rect:inTexture: expects unit coordinates (0-1). So that line of code should be:
let spriteSheet = SKTexture(imageNamed: spriteSheetName)
let texture = SKTexture(rect: CGRect(x: 0, y: 0, width: 32/spriteSheet.size().width, height: 32/spriteSheet.size().height), inTexture: spriteSheet)

SKSpriteNode that the correct size

I am trying to draw a SKSpriteNode that is 30 tall and has the width of the viewport. This is the code (inside SKScene):
func floor() -> SKSpriteNode{
let floor = SKSpriteNode(color: SKColor.greenColor(), size: CGSizeMake(self.size.width, 20))
floor.position = CGPointMake(0, 0)
floor.physicsBody = SKPhysicsBody(rectangleOfSize: floor.size)
floor.physicsBody.dynamic = false
return floor
}
The sprite is added to the scene like this:
override func didMoveToView(view: SKView!){
if (!contentCreated){
self.createContents()
contentCreated = true
}
}
func createContents() {
self.backgroundColor = SKColor.blackColor()
self.scaleMode = SKSceneScaleMode.AspectFill
self.addChild(self.floor())
}
The sprite is 30 tall (seemingly), but the length seems to be half of the viewport in width instead of the full width. The code that creates this scene is:
var mainScene = MainScene(size: self.view.frame.size)
spriteView.presentScene(mainScene)
This code is inside a ViewController.
Does anyone know what might be going on?
The default anchorPoint of a sprite node is { 0.5, 0.5 }, which could result in the code above positioning only half of your sprite on the screen. Try setting the anchorPoint to { 0.0, 0.0 } and see if that helps.

Resources