I am trying to start with the basics of SpriteKit and draw a rounded rectangle
import Foundation
import SpriteKit
class UINode: SKShapeNode {
override init() {
super.init()
self.path = UIBezierPath(roundedRect: CGRect(x: -5, y: -2, width: 10, height: 4), cornerRadius: 1).cgPath
self.position = CGPoint(x: 0, y: 0)
self.fillColor = UIColor.red
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
The respective Scene containing that SKShapeNode contains a SKCameraNode, which can be moved and scaled for panning/zooming. That works fine.
However the output of that Shape is very strange, as you can see in the attachment. This is the result of the above code.
I have no idea what's going on. If I reduce the cornerRadius to zero, I get a rectangular shape with the same strange white border and artifacts. But I except a simple rounded Rect without any blurring, second color or artifacts (thin lines)
What am I doing wrong?
Rendering result
Related
I cannot figure out how to accomplish what seems to be a very simple task. I have a macOS cocoa application which consists of one window with a custom view inside. The view is 600x500 points. Inside this view is a subwiew that spans the whole width of the main view, but only a small fraction of the view's heigth. Its vertical origin is at about y=200.
The subview draws a rect starting 20 points off the left and right border, about 30 points in heigth. It also uses its layer with a CAEmitter layer as sublayer. This emitter's position can be controlled by the user, so its position will be updated by a function called from outside the view.
What I want:
When the user resizes the window, it should only scale proportionally (I managed to to that), so that the window's content as a whole gets resized. No subviews should get rearranged in any way, they all should keep their size and positions relative to each other, just getting bigger/smaller.
I do not get this to work.
What I have tried so far:
With no additional properties set I can resize the window, but the
subiew does not scale at all, it sticks to its position in the
superview.
I set subView.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor).isActive = true and the same for the trailingAnchor, additionally I set subView.autoresizingMask = [.width] Now the width of the rect drawn in the subview gets resized, but the rect's edges stay away 20 points from the border. I want this distance to be scaled as well.
The rect's heigth stays fixed. When I add -heigth to the subview's autoresizing mask the heigth gets distorted in a riculously disproportianal way (the distance to top and bottom margins are kept constant, so although the subview's heigth is only about 30 at start the rect gets blown up on resizing.
The emitter does not get repositioned. Its horizontal position gets calculated on user interaction and is relative to the view's borders, after resizing it sticks to its absloute position. I solved that by calling the calculating function from the draw() method, but that does not seem right to me
There are so many entry points in the cocoa framework regarding sizes, anchors, constraints etc., I simply do not know where to start, so any help would be appreciated.
UPDATE: I added code stripped down to the min:
class MyViewController: NSViewController {
public init () {
super.init(nibName: nil, bundle: nil)
self.view = MyView(frame: NSRect(x: 0, y: 0, width: 600, height: 500))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class MyView: NSView {
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
let subView = MySubView(frame: NSRect(x: 0, y: frame.height/3, width: frame.width, height: frame.height/10))
self.addSubview(subView)
subView.autoresizingMask = [.width, .height]
subView.translatesAutoresizingMaskIntoConstraints = true
subView.trailingAnchor.constraint(greaterThanOrEqualTo: self.trailingAnchor).isActive = true
subView.leadingAnchor.constraint(greaterThanOrEqualTo: self.leadingAnchor).isActive = true
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class MySubView: NSView {
override func draw(_ dirtyRect: NSRect) {
let context = NSGraphicsContext.current!.cgContext
context.setStrokeColor(.black)
context.setFillColor(CGColor.init(red: 0.7, green: 0.7, blue: 0.7, alpha: 1))
context.setLineWidth(2)
let borderRect = CGRect(x: constant.sideMargin, y: 5, width: frame.width-2*constant.sideMargin, height: frame.height-10)
let roundedRect = CGPath.init(roundedRect: borderRect, cornerWidth: 5, cornerHeight: 5, transform: nil)
context.addPath(roundedRect)
context.fillPath()
context.strokePath()
context.addPath(roundedRect)
context.strokePath()
}
}
As explained in the View Programming Guide, NSView will scale and offset its contents if its bounds is different from its frame. Just set bounds to {600, 500} when frame changes. The subviews of the view don't need constraints because their coordinates don't change.
MyView setup code:
// frame changed notitication
self.postsFrameChangedNotifications = true
self.frameChangeObserver = NotificationCenter.default.addObserver(forName: NSView.frameDidChangeNotification,
object: self, queue: OperationQueue.main) { (note) in
self.setBoundsSize(NSSize(width:600, height: 500))
}
let subView = MySubView(frame: NSRect(x: 0, y: bounds.height/3, width: bounds.width, height: bounds.height/10))
// no constraints
subView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(subView)
I'm trying to make infinite scrolling terrain by programmatically adding/deleting textured tiles as the player pans the view. New tiles should only be added next to existing tiles that have an open edge. To detect if a tile has an open edge, I plan to attach a small physics body that sticks out from all 4 sides of the tiles to act as a sensor. If the sensor contacts any other sensors, we know that edge of the tile is not open.
The problem I'm having is that the sensors do not always stay aligned with the tiles. To show this problem, I created a SpriteKit project with the code below.
Touch behavior includes a gesture recognizer in the GameScene class which causes the invisible Handle object to move. When the gesture ends, I use the handle's physics body to give it a little velocity on this line:
handle.physicsBody?.applyImpulse(CGVector(dx: velocity.x * multiplier, dy: -velocity.y * multiplier))
I'm also creating a Tile object (big green square below) and adding it as a child of the invisible handle. That's great, now all child tiles I add will move along with their parent handle.
Whenever a tile is instantiated, a Sensor object (small red square below) is created and added as a child of the tile. That's also great, now all sensors will move along with their parent tile which in turn moves with its parent, the invisible handle. There's just one problem...
When I pan the screen, both the green tile and its red sensor (shown below) move together in unison, as expected. When I release my pan gesture, the extra kick of velocity I give to the handle also carries over to its child tile, also as expected. But that velocity does not affect the child sensor of the tile. As soon as I release the gesture, the sensor stops dead on the screen while the tile continues moving along with the handle until they both slow to a halt. The desired behavior is for the sensor to keep moving along with its parent tile.
Here's a link to a video that might show what's happening better than I can describe it:
https://youtu.be/ccJKdZv-NsM
I can't understand why the tile is staying in sync with its parents motion but the sensor is not doing the same. Thanks for any insight into this problem.
GameScene.swift:
import SpriteKit
class GameScene: SKScene {
let handle = Handle()
let startTile = Tile()
override func didMove(to view: SKView) {
self.backgroundColor = .white
self.physicsWorld.gravity = CGVector(dx: 0, dy: 0)
self.addChild(handle)
startTile.position.x = handle.anchorPoint.x
startTile.position.y = handle.anchorPoint.y
handle.addChild(startTile)
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanFrom))
panGestureRecognizer.cancelsTouchesInView = false
panGestureRecognizer.delaysTouchesEnded = false
self.view!.addGestureRecognizer(panGestureRecognizer)
}
func handlePanFrom(_ recognizer: UIPanGestureRecognizer) {
if recognizer.state == .changed {
var translation = recognizer.translation(in: recognizer.view)
translation = CGPoint(x: translation.x, y: -translation.y)
self.panForTranslation(translation)
recognizer.setTranslation(.zero, in: recognizer.view)
} else if recognizer.state == .ended {
let velocity = recognizer.velocity(in: self.view)
let multiplier = CGFloat(0.5)
handle.physicsBody?.applyImpulse(CGVector(dx: velocity.x * multiplier, dy: -velocity.y * multiplier))
}
}
func panForTranslation(_ translation: CGPoint) {
let position = handle.position
let newPosition = CGPoint(x: position.x + translation.x, y: position.y + translation.y)
handle.position = newPosition
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
handle.physicsBody?.isResting = true
}
}
Handle class:
import SpriteKit
class Handle : SKSpriteNode {
init() {
super.init(texture: nil, color: .clear, size: CGSize(width: 1, height: 1))
self.physicsBody = SKPhysicsBody(rectangleOf: self.size)
self.physicsBody?.mass = 1
self.physicsBody?.linearDamping = 2
self.physicsBody?.categoryBitMask = 0
self.physicsBody?.contactTestBitMask = 0
self.physicsBody?.collisionBitMask = 0
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Tile class:
import SpriteKit
class Tile : SKSpriteNode {
init() {
super.init(texture: nil, color: .green, size: CGSize(width: 300, height: 300))
let sensorA = Sensor()
self.addChild(sensorA)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Sensor class:
import SpriteKit
class Sensor : SKSpriteNode {
init() {
super.init(texture: nil, color: .red, size: CGSize(width: 50, height: 50))
self.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 50, height: 50))
self.physicsBody?.categoryBitMask = 0b1
self.physicsBody?.contactTestBitMask = 0b1
self.physicsBody?.collisionBitMask = 0
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
UPDATE:
The accepted answer provided by Whirlwind solved the problem I had with the child separating from the parent. I believe that the cause of the problem became clear in the comments of that answer.
My understanding of it is that the red square did not move because it has its own physics body which is not receiving any velocity after the handle stops moving. While the handle object (and its child tile) keeps moving because it does have velocity. So it sounds like the red box's own physics body was holding it back.
I don't really have a time to get into why your code does some things, but if you want to move another physics body along with handle's physics body, then you could pin it to it.
I will just modify your code to make it work, but you should worry by yourself about encapsulation. First make a sensor variable inside of Tile class visible to the outside world, so you can use it later in your scene:
class Tile : SKSpriteNode {
let sensorA = Sensor()
init() {
super.init(texture: nil, color: .green, size: CGSize(width: 300, height: 300))
self.addChild(sensorA)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Then in you scene pin sensor to a handle:
let pin = SKPhysicsJointPin.joint(withBodyA: self.startTile.sensorA.physicsBody!, bodyB: self.handle.physicsBody!, anchor: CGPoint.zero)
startTile.sensorA.physicsBody?.allowsRotation = false
self.physicsWorld.add(pin)
I guess this is what you wanted:
How can I create a simple line that I can control its originating point, but let the user determine where it ends?
I have a UIView on my storyboard and on top of this I want to insert a line. The line would be restricted to be within this particular view and would start vertically centered and from the left side.
I have buttons with a - and a + to allow the user to increase its length or decrease it.
I suppose the easiest method will be to a add another UIView (call it lineView) inside your main view, set it's width to whatever you want the width of your line to be and hook it up with an #IBOutlet. Then in your #IBAction methods for - and +, change the height of lineView
I found the answer here, How to draw a line between two points over an image in swift 3?
class LineView : UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.init(white: 0.0, alpha: 0.0)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
context.setStrokeColor(UIColor.blue.cgColor)
context.setLineWidth(3)
context.beginPath()
context.move(to: CGPoint(x: 5.0, y: 5.0)) // This would be oldX, oldY
context.addLine(to: CGPoint(x: 50.0, y: 50.0)) // This would be newX, newY
context.strokePath()
}
}
}
let imageView = UIImageView(image: #imageLiteral(resourceName: "image.png")) // This would be your mapView, here I am just using a random image
let lineView = LineView(frame: imageView.frame)
imageView.addSubview(lineView)
I am trying to create a mario style rotating platform that stays horizontal.
What I have done so far is create a simple class for this for testing purposes.
class PlatformRound: SKNode {
let platform: Platform
// MARK: - Init
init(barSize: CGSize, color: SKColor, pos: CGPoint) {
/// Base
let base = SKShapeNode(circleOfRadius: 6)
//base.name = Platform.Name.normal
base.fillColor = SKColor.darkGrayColor()
base.strokeColor = base.fillColor
base.position = pos
let rotatingAction = SKAction.rotateByAngle(CGFloat(-M_PI), duration: 8)
base.runAction(SKAction.repeatActionForever(rotatingAction))
/// Bar
let bar = Platform(size: barSize, color: color, pos: CGPointMake(0, 0 - (barSize.height / 2)), ofType: .Normal)
bar.zPosition = -200
/// Platform that supposed to stay horizontal
let platformSize = CGSizeMake(40, GameplayConfig.World.platformHeight)
let platformPos = CGPointMake(0, 0 - (bar.size.height / 2))
platform = Platform(size: platformSize, color: color, pos: platformPos, ofType: .Normal)
super.init()
addChild(base)
base.addChild(bar)
bar.addChild(platform)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I am creating a roundBase that I can rotate. I than create a bar that goes down from the base, that is added to the base node. Finally I create the platform that is supposed to stay horizontal at all times.
I am using another Platform subclass to create the bar and platform, but they are not relevant to this question.
How can I make the platform stay horizontal. I tried the following which didnt work.
1) In update in my gameScene I constantly update the platform position or zRotation
platformRound.platform.zRotation = ...
2) Create a zRotation property that gets set once the platform is added and than use that property to constantly update the zRotation.
3) Tried playing around with physicsJoints
Im sure there is a easy way that I am missing. I would appreciate any help.
This should work:
class GameScene: SKScene{
override func didMoveToView(view: SKView) {
backgroundColor = .blackColor()
let centerSprite = SKSpriteNode(color: .whiteColor(), size: CGSize(width: 10, height: 10))
centerSprite.zPosition = 3
let platform = SKSpriteNode(color: .orangeColor(), size: CGSize(width: 70, height: 20))
platform.zPosition = 2
platform.name = "platform"
let container = SKNode()
container.position = CGPoint(x: frame.midX, y: frame.midY)
container.addChild(centerSprite) //Just for debugging
container.addChild(platform)
let radius = 120
let chain = SKSpriteNode(color: .grayColor(), size: CGSize(width: 3, height: radius))
chain.position = CGPoint(x: 0, y: radius/2)
container.addChild(chain)
platform.position = CGPoint(x: 0, y: radius)
let rotatingAction = SKAction.rotateByAngle(CGFloat(-M_PI), duration: 8)
container.runAction(SKAction.repeatActionForever(rotatingAction), withKey: "rotating")
addChild(container)
}
override func didEvaluateActions() {
self.enumerateChildNodesWithName("//platform") { node,stop in
if let parent = node.parent{
node.zRotation = -parent.zRotation
}
}
}
}
What I did, is that I have added platform node into container node and applied rotation to that container. Later on, in didEvaluateActions I've adjusted the rotation of platform (to have a negative value of its parent's zRotation). And that's it.
Here is the result of what I am seeing:
The adjusting is needed, because otherwise the platform will end-up rotating along with its parent (notice how white, center sprite is being rotated along with container node).
I have a custom NSView with two NSClipViews. I want to add a vertical scroller for each clipview. The snippet below shows a NSScroller, but it is not drawing any knob or buttons. Interaction is also not possible.
NSScroller is a small class, so I am not sure what I am missing here.
required init?(coder: NSCoder)
{
super.init(coder: coder)
let width = NSScroller.scrollerWidthForControlSize(NSControlSize.RegularControlSize, scrollerStyle: NSScrollerStyle.Overlay)
let scroller = NSScroller(frame: NSRect(x: 10, y: 10, width: width, height: 500))
self.addSubview(scroller)
scroller.doubleValue = 0.5
scroller.knobProportion = 0.1
scroller.needsLayout = true //no effect
scroller.needsDisplay = true //no effect
}
The missing line:
scroller.enabled = true