Can I make an array of SK nodes of which one is selected randomly and brought from the top to bottom of the screen. For example say I have 25 or so different platforms that will be falling out of the sky on a portrait iPhone. I need it to randomly select one of the platforms from the array to start and then after a certain amount of time/ or pixel space randomly select another to continue the same action until reaching the bottom etc. Im new to swift but have a pretty decent understanding of it. I haven't been able to find out how to create an array of SKsprite nodes yet either. Could someone help with this?
So far the only way I've been able to get any sort of effect similar to what I've wanted is by placing each of the nodes off the screen and adding them to a dictionary and making them move like this
class ObstacleStatus {
var isMoving = false
var timeGapForNextRun = Int(0)
var currentInterval = Int(0)
init(isMoving: Bool, timeGapForNextRun: Int, currentInterval: Int) {
self.isMoving = isMoving
self.timeGapForNextRun = timeGapForNextRun
self.currentInterval = currentInterval
}
func shouldRunBlock() -> Bool {
return self.currentInterval > self.timeGapForNextRun
}
and
func moveBlocks(){
for(blocks, ObstacleStatus) in self.blockStatuses {
var thisBlock = self.childNodeWithName(blocks)
var thisBlock2 = self.childNodeWithName(blocks)
if ObstacleStatus.shouldRunBlock() {
ObstacleStatus.timeGapForNextRun = randomNum()
ObstacleStatus.currentInterval = 0
ObstacleStatus.isMoving = true
}
if ObstacleStatus.isMoving {
if thisBlock?.position.y > blockMaxY{
thisBlock?.position.y -= CGFloat(self.fallSpeed)
}else{
thisBlock?.position.y = self.origBlockPosistionY
ObstacleStatus.isMoving = false
}
}else{
ObstacleStatus.currentInterval++
}
}
}
using this for the random function
func randomNum() -> Int{
return randomInt(50, max: 300)
}
func randomInt(min: Int, max:Int) -> Int {
return min + Int(arc4random_uniform(UInt32(max - min + 1)))
}
All this has been doing for me is moving the pieces down at random timed intervals often overlapping them, But increasing the min or max of the random numbers doesn't really have an affect on the actual timing of the gaps. I need to be able to specify a distance or time gap.
One of many possible solutions is to create a falling action sequence which calls itself recursively until no more platform nodes are left. You can control the mean "gap time" and the range of its random variation. Here is a working example (assuming the iOS SpriteKit game template):
import SpriteKit
extension Double {
var cg: CGFloat { return CGFloat(self) }
}
extension Int {
var cg: CGFloat { return CGFloat(self) }
}
func randomInt(range: Range<Int>) -> Int {
return range.startIndex + Int(arc4random_uniform(UInt32(range.endIndex - range.startIndex)))
}
extension Array {
func randomElement() -> Element? {
switch self.count {
case 0: return nil
default: return self[randomInt(0..<self.count)]
}
}
func apply<Ignore>(f: (T) -> (Ignore)) {
for e in self { f(e) }
}
}
class GameScene: SKScene {
var screenWidth: CGFloat { return UIScreen.mainScreen().bounds.size.width }
var screenHeight: CGFloat { return UIScreen.mainScreen().bounds.size.height }
let PlatformName = "Platform"
let FallenPlatformName = "FallenPlatform"
func createRectangularNode(#x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat) -> SKShapeNode {
let rect = CGRect(x: x, y: y, width: width, height: height)
let path = UIBezierPath(rect: rect)
let node = SKShapeNode(path: path.CGPath)
return node
}
func createPlatformNodes(numNodes: Int, atHeight: CGFloat) -> [SKShapeNode] {
var padding = 20.cg
let width = (screenWidth - padding) / numNodes.cg - padding
padding = (screenWidth - width * numNodes.cg) / (numNodes.cg + 1)
let height = width / 4
var nodes = [SKShapeNode]()
for x in stride(from: padding, to: numNodes.cg * (width + padding), by: width + padding) {
let node = createRectangularNode(x: x, y: atHeight, width: width, height: height)
node.fillColor = SKColor.blackColor()
node.name = PlatformName
nodes.append(node)
}
return nodes
}
func createFallingAction(#by: CGFloat, duration: NSTimeInterval, timeGap: NSTimeInterval, range: NSTimeInterval = 0) -> SKAction {
let gap = SKAction.waitForDuration(timeGap, withRange: range)
// let fall = SKAction.moveToY(toHeight, duration: duration) // moveToY appears to have a bug: behaves as moveBy
let fall = SKAction.moveByX(0, y: -by, duration: duration)
let next = SKAction.customActionWithDuration(0) { [unowned self]
node, time in
node.name = self.FallenPlatformName
self.fallNextNode()
}
return SKAction.sequence([gap, fall, next])
}
func fallNextNode() {
if let nextNode = self[PlatformName].randomElement() as? SKShapeNode {
let falling = createFallingAction(by: screenHeight * 0.7, duration: 1, timeGap: 2.5, range: 2) // mean time gap and random range
nextNode.runAction(falling)
} else {
self.children.apply { ($0 as? SKShapeNode)?.fillColor = SKColor.redColor() }
}
}
override func didMoveToView(view: SKView) {
self.backgroundColor = SKColor.whiteColor()
for platform in createPlatformNodes(7, atHeight: screenHeight * 0.8) {
self.addChild(platform)
}
fallNextNode()
}
}
Related
I am building a messanger view in iOs (Swift) app with help of UICollectionView inside a UIViewController. I am taking inspiration from MessageKit and I was able to set everything properly with simple dynamic cell height. When I hide the keyboard and the collection view bottom inset is reduced while the collection view is scrolled to bottom, it logically drags cells in to the view from top (scrolls down). I am not sure if it is somehow in conflict with keyboard hiding animation, but if this cause the collection view scroll to much and therefore display cells that have not been in the view, they appear not as scrolled in, but with some strange layout animation. It happens only while hiding keyboard && collectionView is at the bottom. Please check the gif:
link to gif
Building everything on UITableView did work, but I aim for collection view due to future features. I tried to use even a fixed cell height in a CollectionViewFlowDelegate, but it has the same effect as dynamically calculated heights.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
I am setting the UICollectionView inset the same way as in the MessageKit:
private func requiredScrollViewBottomInset(forKeyboardFrame keyboardFrame: CGRect) -> CGFloat {
let intersection = chatCollectionView.frame.intersection(keyboardFrame)
if intersection.isNull || (chatCollectionView.frame.maxY - intersection.maxY) > 0.001 {
messagesCollectionView.frame.maxY when dealing with undocked keyboards.
return max(0, additionalBottomInset - automaticallyAddedBottomInset)
} else {
return max(0, intersection.height + additionalBottomInset - automaticallyAddedBottomInset)
}
}
#objc private func handleKeyboardDidChangeState(_ notification: Notification) {
guard !isMessagesControllerBeingDismissed else { return }
guard let keyboardStartFrameInScreenCoords = notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? CGRect else { return }
guard !keyboardStartFrameInScreenCoords.isEmpty || UIDevice.current.userInterfaceIdiom != .pad else {
// WORKAROUND for what seems to be a bug in iPad's keyboard handling in iOS 11: we receive an extra spurious frame change
// notification when undocking the keyboard, with a zero starting frame and an incorrect end frame. The workaround is to
// ignore this notification.
return
}
guard let keyboardEndFrameInScreenCoords = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
let keyboardEndFrame = view.convert(keyboardEndFrameInScreenCoords, from: view.window)
let newBottomInset = requiredScrollViewBottomInset(forKeyboardFrame: keyboardEndFrame)
let differenceOfBottomInset = newBottomInset - messageCollectionViewBottomInset
if maintainPositionOnKeyboardFrameChanged && differenceOfBottomInset >/*!=*/ 0 {
let contentOffset = CGPoint(x: chatCollectionView.contentOffset.x, y: chatCollectionView.contentOffset.y + differenceOfBottomInset)
chatCollectionView.setContentOffset(contentOffset, animated: false)
}
messageCollectionViewBottomInset = newBottomInset
}
internal func requiredInitialScrollViewBottomInset() -> CGFloat {
print("accessory view for initial bottom inset: \(inputAccessoryView)")
guard let inputAccessoryView = inputAccessoryView else { return 0 }
return max(0, inputAccessoryView.frame.height + additionalBottomInset - automaticallyAddedBottomInset)
}
As I could not find any related topic regarding this scrolling upon keyboard hiding, I am not sure if this is reusableCell issue or animation conflict?
EDIT
So the partial solution is to invalidate layout only if the width change, this will prevent keyboard invalidating it while hiding:
open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return collectionView?.bounds.width != newBounds.width
}
But it also prevents sticky headers to get invalidated and therefore resign being sticky. I went deeper into invalidationContext as this looked as potential full solution, although I get the same behaviour.
open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
invalidateLayout(with: invalidationContext(forBoundsChange: newBounds))
return collectionView?.bounds.width != newBounds.width
}
open override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
let context = super.invalidationContext(forBoundsChange: newBounds)
guard let flowLayoutContext = context as? UICollectionViewFlowLayoutInvalidationContext else { return context }
let indexes: [IndexPath] = (collectionView?.indexPathsForVisibleSupplementaryElements(ofKind: UICollectionView.elementKindSectionHeader))!
print(indexes)
flowLayoutContext.invalidateSupplementaryElements(ofKind: UICollectionView.elementKindSectionHeader, at: indexes)
print(context.invalidatedSupplementaryIndexPaths)
return flowLayoutContext
}
Print statements clearly state that only headers are invalidated, and for the rest I am returning false. But it behaves exactly the same as in the gif (see the link - unfortunately I have reputation not high enough yet to add it directly here). Thank you for any comments!
After long investigation I noticed that the same phenomen happens also at the bottom in particular cases. I am not sure it it is bug or custom layout is necessary, but with the simple flow layout I solved the issue by setting collectionView contraints beyound the display edge.
This force to call displaying cell earlier and gives some time to lay cell approprietely even hide keyboard animation is used. Of coarse collectionView top and bottom contentInset has to be set in viewDidLoad and handle any contentInset change during runtime accordingly
Hope it help!
EDIT:
I ended up using CADisplaLink for scrolling of the collectionView when the keyboard shows and setting the bottom inset without animation. This case the inset to change exactly in the moment, when scroll function is scrolling the collection, so there is no jump and each frame has correct layout. For the keyboard hide it is quite simple - setting bottom inset in keyboardWillHide cause the collection to adjust offset upon releasing the finger also outside animation block and therefore is the layout also correct.
Following code is inspired by multiple answers here and by MessakeKit on GitHub
let scrollController = ScrollController()
override func viewDidLoad() {
super.viewDidLoad()
scrollController.scrollView = self.chatCollectionView
scrollController.decelerationAllowed = true
scrollController.keepUserInteractionDuringScroll = true
}
#objc private func keyboardWillShowOrHide(_ notification: Notification) {
guard !ignoreKeyboardNotification, !insertingMessage, !isViewControllerBeingDismissed, viewAlreadyAppeared else { return }
guard let keyboardEndFrameInScreenCoords = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
let keyboardEndFrame = view.convert(keyboardEndFrameInScreenCoords, from: view.window)
let newBottomInset = requiredScrollViewBottomInset(forKeyboardFrame: keyboardEndFrame)
let deltaBottomInset = newBottomInset - messageCollectionViewBottomInset
let differenceOfBottomInset = min(deltaBottomInset, collectionViewContentHightIntersectSafeArea())
ignoreScrollButtonHide = true
let scrollButtonOriginY = keyboardEndFrameInScreenCoords.origin.y - (self.scrollToBottomButtonMargin + self.scrollButton.frame.size.height)
UIView.performWithoutAnimation {
let contentOffset = chatCollectionView.contentOffset
self.messageCollectionViewBottomInset = newBottomInset
self.chatCollectionView.contentOffset = contentOffset
}
if !chatCollectionView.isTracking {
if differenceOfBottomInset != 0 {
let beginOffsetY = self.scrollController.endContentOffset == CGPoint.zero ? self.chatCollectionView.contentOffset.y : self.scrollController.endContentOffset.y
let negativeScroll = differenceOfBottomInset < 0//if difference is negative, it should not scroll pass the topScrollOffset
let setContentOffsetY = negativeScroll ? max(beginOffsetY + differenceOfBottomInset, topScrollOffset()) : beginOffsetY + differenceOfBottomInset
let contentOffset = CGPoint(x: 0.0, y: setContentOffsetY)
self.scrollController.setContentOffset(contentOffset, with: .easeOut, duration: self.animationsDuration)
}
}
UIView.animate(withDuration: 0, delay: 0, options: [.curveEaseOut, .allowAnimatedContent, .overrideInheritedOptions, .beginFromCurrentState], animations: {
self.chatCollectionView.verticalScrollIndicatorInsets.bottom = newBottomInset
self.scrollButton.frame.origin.y = scrollButtonOriginY
self.adjustLangSelectionViews()
}, completion: { (complete) in
self.ignoreScrollButtonHide = false
if self.chatCollectionView.isDecelerating == false { //do not hide if still scrolling
self.pinnedHeaderIsHidden(hidden: true)
}
})
}
I unfortunately did not saved, where I got the scrollController code for the CADisplayLink. If author could contact me, Iĺl be more than happy to put the link to the source here.
class ScrollController {
//CADisplayLink
private let scrollAnimationDuration: TimeInterval = 0.25
private let scrollApproximationTolerance: Double = 0.00000001
private let scrollMaximumSteps: Int = 10
var displayLink: CADisplayLink? // Display link used to trigger event to scroll the view
private var timingFunction: CAMediaTimingFunction? // Timing function of an scroll animation
private var duration: CFTimeInterval = 0 // Duration of an scroll animation
private var animationStarted = false // States whether the animation has started
private var beginTime: CFTimeInterval = 0 // Time at the begining of an animation
private var beginContentOffset = CGPoint.zero // The content offset at the begining of an animation
private var deltaContentOffset = CGPoint.zero // The delta between the scroll contentOffsets
var endContentOffset = CGPoint.zero // The delta between the scroll contentOffsets
var scrollView: UIScrollView!
var keepUserInteractionDuringScroll = false
var delegateClass: UIScrollViewDelegate?
var decelerationAllowed: Bool = false
func setContentOffset(_ contentOffset: CGPoint, with timingFunction: CAMediaTimingFunction?, duration: CFTimeInterval, deceleratingAllowed: Bool? = false) {
self.duration = duration
self.timingFunction = timingFunction
deltaContentOffset = CGPointMinus(contentOffset, scrollView.contentOffset)
endContentOffset = contentOffset
decelerationAllowed = deceleratingAllowed!
if displayLink == nil {
displayLink = CADisplayLink(target: self, selector: #selector(updateContentOffset(_:)))
displayLink?.add(to: .main, forMode: .common)
scrollView.isUserInteractionEnabled = keepUserInteractionDuringScroll
} else {
beginTime = 0.0
}
}
#objc func updateContentOffset(_ displayLink: CADisplayLink?) {
if beginTime == 0.0 {
beginTime = self.displayLink?.timestamp ?? 0
beginContentOffset = scrollView.contentOffset
} else {
let deltaTime = (displayLink?.timestamp ?? 0) - beginTime
// Ratio of duration that went by
let progress = CGFloat(deltaTime / duration)
if progress < 1.0 {
// Ratio adjusted by timing function
let adjustedProgress = CGFloat(timingFunctionValue(timingFunction, Double(progress)))
if 1 - adjustedProgress < 0.001 {
stopDisplayLink()
} else {
updateProgress(adjustedProgress)
}
} else {
stopDisplayLink()
}
}
}
func updateProgress(_ progress: CGFloat) {
let currentDeltaContentOffset = CGPointScalarMult(progress, deltaContentOffset)
scrollView.contentOffset = CGPointAdd(beginContentOffset, currentDeltaContentOffset)
}
func stopDisplayLink() {
displayLink?.isPaused = true
beginTime = 0.0
if !decelerationAllowed {
scrollView.setContentOffset(endContentOffset, animated: false)
} else {
scrollView.contentOffset = CGPointAdd(beginContentOffset, deltaContentOffset)
}
endContentOffset = CGPoint.zero
displayLink?.invalidate()
displayLink = nil
scrollView.isUserInteractionEnabled = true
if delegateClass != nil {
delegateClass!.scrollViewDidEndScrollingAnimation?(scrollView)
}
}
func CGPointScalarMult(_ s: CGFloat, _ p: CGPoint) -> CGPoint {
return CGPoint(x: s * p.x, y: s * p.y)
}
func CGPointAdd(_ p: CGPoint, _ q: CGPoint) -> CGPoint {
return CGPoint(x: p.x + q.x, y: p.y + q.y)
}
func CGPointMinus(_ p: CGPoint, _ q: CGPoint) -> CGPoint {
return CGPoint(x: p.x - q.x, y: p.y - q.y)
}
func cubicFunctionValue(_ a: Double, _ b: Double, _ c: Double, _ d: Double, _ x: Double) -> Double {
return (a*x*x*x)+(b*x*x)+(c*x)+d//Double(d as? c as? b as? a ?? 0.0)
}
func cubicDerivativeValue(_ a: Double, _ b: Double, _ c: Double, _ d: Double, _ x: Double) -> Double {
/// Derivation of the cubic (a*x*x*x)+(b*x*x)+(c*x)+d
return (3 * a * x * x) + (2 * b * x) + c
}
func rootOfCubic(_ a: Double, _ b: Double, _ c: Double, _ d: Double, _ startPoint: Double) -> Double {
// We use 0 as start point as the root will be in the interval [0,1]
var x = startPoint
var lastX: Double = 1
// Approximate a root by using the Newton-Raphson method
var y = 0
while y <= scrollMaximumSteps && fabs(lastX - x/*Float(lastX - x)*/) > scrollApproximationTolerance {
lastX = x
x = x - (cubicFunctionValue(a, b, c, d, x) / cubicDerivativeValue(a, b, c, d, x))
y += 1
}
return x
}
func timingFunctionValue(_ function: CAMediaTimingFunction?, _ x: Double) -> Double {
var a = [Float](repeating: 0.0, count: 2)
var b = [Float](repeating: 0.0, count: 2)
var c = [Float](repeating: 0.0, count: 2)
var d = [Float](repeating: 0.0, count: 2)
function?.getControlPoint(at: 0, values: &a)
function?.getControlPoint(at: 1, values: &b)
function?.getControlPoint(at: 2, values: &c)
function?.getControlPoint(at: 3, values: &d)
// Look for t value that corresponds to provided x
let t = rootOfCubic(Double(-a[0] + 3 * b[0] - 3 * c[0] + d[0]), Double(3 * a[0] - 6 * b[0] + 3 * c[0]), Double(-3 * a[0] + 3 * b[0]), Double(a[0]) - x, x)
// Return corresponding y value
let y = cubicFunctionValue(Double(-a[1] + 3 * b[1] - 3 * c[1] + d[1]), Double(3 * a[1] - 6 * b[1] + 3 * c[1]), Double(-3 * a[1] + 3 * b[1]), Double(a[1]), t)
return y
}
Hope it helps!
I want to trace the path where a bullet will move in my SpriteKit GameScene.
I'm using "enumerateBodies(alongRayStart", I can easily calculate the first collision with a physics body.
I don't know how to calculate the angle of reflection, given the contact point and the contact normal.
I want to calculate the path, over 5 reflections/bounces, so first I:
Cast a ray, get all the bodies it intersects with, and get the closest one.
I then use that contact point as the start of my next reflection/bounce....but I'm struggling with what the end point should be set to....
What I think I should be doing is getting the angle between the contact point and the contact normal, and then calculating a new point opposite to that...
var points: [CGPoint] = []
var start: CGPoint = renderComponent.node.position
var end: CGPoint = crossHairComponent.node.position
points.append(start)
var closestNormal: CGVector = .zero
for i in 0...5 {
closestNormal = .zero
var closestLength: CGFloat? = nil
var closestContact: CGPoint!
// Get the closest contact point.
self.physicsWorld.enumerateBodies(alongRayStart: start, end: end) { (physicsBody, contactPoint, contactNormal, stop) in
let len = start.distance(point: contactPoint)
if closestContact == nil {
closestNormal = contactNormal
closestLength = len
closestContact = contactPoint
} else {
if len <= closestLength! {
closestLength = len
closestNormal = contactNormal
closestContact = contactPoint
}
}
}
// This is where the code is just plain wrong and my math fails me.
if closestContact != nil {
// Calculate intersection angle...doesn't seem right?
let v1: CGVector = (end - start).normalized().toCGVector()
let v2: CGVector = closestNormal.normalized()
var angle = acos(v1.dot(v2)) * (180 / .pi)
let v1perp = CGVector(dx: -v1.dy, dy: v1.dx)
if(v2.dot(v1perp) > 0) {
angle = 360.0 - angle
}
angle = angle.degreesToRadians
// Set the new start point
start = closestContact
// Calculate a new end point somewhere in the distance to cast a ray to, so we can repeat the process again
let x = closestContact.x + cos(angle)*100
let y = closestContact.y + sin(-angle)*100
end = CGPoint(x: x, y: y)
// Add points to array to draw them on the screen
points.append(closestContact)
points.append(end)
}
}
I guess you are looking for something like this right?
1. Working code
First of all let me post the full working code. Just create a new Xcode project based SpriteKit and
In GameViewController.swift set
scene.scaleMode = .resizeFill
Remove the usual label you find in GameScene.sks
Replace Scene.swift with the following code
>
import SpriteKit
class GameScene: SKScene {
override func didMove(to view: SKView) {
self.physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
}
var angle: CGFloat = 0
override func update(_ currentTime: TimeInterval) {
removeAllChildren()
drawRayCasting(angle: angle)
angle += 0.001
}
private func drawRayCasting(angle: CGFloat) {
let colors: [UIColor] = [.red, .green, .blue, .orange, .white]
var start: CGPoint = .zero
var direction: CGVector = CGVector(angle: angle)
for i in 0...4 {
guard let result = rayCast(start: start, direction: direction) else { return }
let vector = CGVector(from: start, to: result.destination)
// draw
drawVector(point: start, vector: vector, color: colors[i])
// prepare for next iteration
start = result.destination
direction = vector.normalized().bounced(withNormal: result.normal.normalized()).normalized()
}
}
private func rayCast(start: CGPoint, direction: CGVector) -> (destination:CGPoint, normal: CGVector)? {
let endVector = CGVector(
dx: start.x + direction.normalized().dx * 4000,
dy: start.y + direction.normalized().dy * 4000
)
let endPoint = CGPoint(x: endVector.dx, y: endVector.dy)
var closestPoint: CGPoint?
var normal: CGVector?
physicsWorld.enumerateBodies(alongRayStart: start, end: endPoint) {
(physicsBody:SKPhysicsBody,
point:CGPoint,
normalVector:CGVector,
stop:UnsafeMutablePointer<ObjCBool>) in
guard start.distanceTo(point) > 1 else {
return
}
guard let newClosestPoint = closestPoint else {
closestPoint = point
normal = normalVector
return
}
guard start.distanceTo(point) < start.distanceTo(newClosestPoint) else {
return
}
normal = normalVector
}
guard let p = closestPoint, let n = normal else { return nil }
return (p, n)
}
private func drawVector(point: CGPoint, vector: CGVector, color: SKColor) {
let start = point
let destX = (start.x + vector.dx)
let destY = (start.y + vector.dy)
let to = CGPoint(x: destX, y: destY)
let path = CGMutablePath()
path.move(to: start)
path.addLine(to: to)
path.closeSubpath()
let line = SKShapeNode(path: path)
line.strokeColor = color
line.lineWidth = 6
addChild(line)
}
}
extension CGVector {
init(angle: CGFloat) {
self.init(dx: cos(angle), dy: sin(angle))
}
func normalized() -> CGVector {
let len = length()
return len>0 ? self / len : CGVector.zero
}
func length() -> CGFloat {
return sqrt(dx*dx + dy*dy)
}
static func / (vector: CGVector, scalar: CGFloat) -> CGVector {
return CGVector(dx: vector.dx / scalar, dy: vector.dy / scalar)
}
func bounced(withNormal normal: CGVector) -> CGVector {
let dotProduct = self.normalized() * normal.normalized()
let dx = self.dx - 2 * (dotProduct) * normal.dx
let dy = self.dy - 2 * (dotProduct) * normal.dy
return CGVector(dx: dx, dy: dy)
}
init(from:CGPoint, to:CGPoint) {
self = CGVector(dx: to.x - from.x, dy: to.y - from.y)
}
static func * (left: CGVector, right: CGVector) -> CGFloat {
return (left.dx * right.dx) + (left.dy * right.dy)
}
}
extension CGPoint {
func length() -> CGFloat {
return sqrt(x*x + y*y)
}
func distanceTo(_ point: CGPoint) -> CGFloat {
return (self - point).length()
}
static func - (left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x - right.x, y: left.y - right.y)
}
}
2. How does it work?
Lets have a look at what this code does. We'll start from the bottom.
3. CGPoint and CGVector extensions
These are just simple extensions (mainly taken from Ray Wenderlich's repository on GitHub) to simplify the geometrical operations we are going to perform.
4. drawVector(point:vector:color)
This is a simple method to draw a vector with a given color starting from a given point.
Nothing fancy here.
private func drawVector(point: CGPoint, vector: CGVector, color: SKColor) {
let start = point
let destX = (start.x + vector.dx)
let destY = (start.y + vector.dy)
let to = CGPoint(x: destX, y: destY)
let path = CGMutablePath()
path.move(to: start)
path.addLine(to: to)
path.closeSubpath()
let line = SKShapeNode(path: path)
line.strokeColor = color
line.lineWidth = 6
addChild(line)
}
5. rayCast(start:direction) -> (destination:CGPoint, normal: CGVector)?
This method perform a raycasting and returns the ALMOST closest point where the ray enter in collision with a physics body.
private func rayCast(start: CGPoint, direction: CGVector) -> (destination:CGPoint, normal: CGVector)? {
let endVector = CGVector(
dx: start.x + direction.normalized().dx * 4000,
dy: start.y + direction.normalized().dy * 4000
)
let endPoint = CGPoint(x: endVector.dx, y: endVector.dy)
var closestPoint: CGPoint?
var normal: CGVector?
physicsWorld.enumerateBodies(alongRayStart: start, end: endPoint) {
(physicsBody:SKPhysicsBody,
point:CGPoint,
normalVector:CGVector,
stop:UnsafeMutablePointer<ObjCBool>) in
guard start.distanceTo(point) > 1 else {
return
}
guard let newClosestPoint = closestPoint else {
closestPoint = point
normal = normalVector
return
}
guard start.distanceTo(point) < start.distanceTo(newClosestPoint) else {
return
}
normal = normalVector
}
guard let p = closestPoint, let n = normal else { return nil }
return (p, n)
}
What does it mean ALMOST the closets?
It means the the destination point must be at least 1 point distant from the start point
guard start.distanceTo(point) > 1 else {
return
}
Ok but why?
Because without this rule the ray gets stuck into a physics body and it is never able to get outside of it.
6. drawRayCasting(angle)
This method basically keeps the local variables up to date to properly generate 5 segments.
private func drawRayCasting(angle: CGFloat) {
let colors: [UIColor] = [.red, .green, .blue, .orange, .white]
var start: CGPoint = .zero
var direction: CGVector = CGVector(angle: angle)
for i in 0...4 {
guard let result = rayCast(start: start, direction: direction) else { return }
let vector = CGVector(from: start, to: result.destination)
// draw
drawVector(point: start, vector: vector, color: colors[i])
// prepare next direction
start = result.destination
direction = vector.normalized().bounced(withNormal: result.normal.normalized()).normalized()
}
}
The first segment has starting point equals to zero and a direction diving my the angle parameter.
Segments 2 to 5 use the final point and the "mirrored direction" of the previous segment.
update(_ currentTime: TimeInterval)
Here I am just calling drawRayCasting every frame passing the current angle value and the increasing angle by 0.001.
var angle: CGFloat = 0
override func update(_ currentTime: TimeInterval) {
removeAllChildren()
drawRayCasting(angle: angle)
angle += 0.001
}
6. didMove(to view: SKView)
Finally here I create a physics body around the scene in order to make the ray bounce over the borders.
override func didMove(to view: SKView) {
self.physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
}
7. Wrap up
I hope the explanation is clear.
Should you have any doubt let me know.
Update
There was a bug in the bounced function. It was preventing a proper calculation of the reflected ray.
It is now fixed.
I am new to programming and trying to learn Cocoa for Mac OS using Xcode 9.3 and Swift 4.1. I’ve worked through books on C++ and Objective-C. Now I am working through the Big Nerd Ranch book, Cocoa Programming for Mac OS, 5th edition which uses Swift 2.0 (on my own). There have been challenges trying to work through the differences from Swift 2.0 to 4.1, and I don’t know if this issue if part of that.
To sum up the exercise, I created a die. Part of the exercise is to make the die change its number by double clicking in the die. I did that, and it worked. Now I need to duplicate the die twice for a total of three dice.
I followed the steps to copy the die (highlight the dieView and command-D) and I now have three dice. Each view accepts First Responder and each view subsequently accepts keyboard input to change the number showing on the die. I can select the key window with the mouse and the highlight shows which window is active. However, the double-click to “roll” the dice only works on the original die, not the other two. How can that be when they are exact duplicates? And since they were copied, how can I get the double-click to work in the other two dice since they should have the same code?
I also tried moving the original die to the third position, moving the two new dice over and now it is still the die in the first position that works with the double-click, not the last two in line. When I move the dice into a stacked position, one above the other, above the other, at first I had them on the left side of the window, and the double-click did not work on any of them. I resized the window so they were all stacked on the left side, and the double-click only works on the bottom die.
I know this has something to do with the custom view window the dice are in, but shouldn’t mouse clicks register anywhere in that window? Obviously the clicks are being registered, because I can change the key window to each of the dice. It’s just the double-click feature that isn’t working correctly.
Here is my dieView code:
import Cocoa
#IBDesignable class DieView: NSView {
var intValue: Int? = 1 {
didSet {
needsDisplay = true
}
}
var pressed: Bool = false {
didSet {
needsDisplay = true
}
}
var dieShape = NSBezierPath()
override var intrinsicContentSize: NSSize {
return NSSize(width: 20, height: 20)
}
override func draw(_ dirtyRect: NSRect) {
let backgroundColor = NSColor.lightGray
backgroundColor.set()
NSBezierPath.fill(bounds)
drawDieWithSize(size: bounds.size)
}
func metricsForSize(size: CGSize) -> (edgeLength: CGFloat, dieFrame: CGRect) {
let edgeLength = min(size.width, size.height)
let padding = edgeLength/10.0
let drawingBounds = CGRect(x: 0, y: 0, width: edgeLength, height: edgeLength)
var dieFrame = drawingBounds.insetBy(dx: padding, dy: padding)
if pressed {
dieFrame = dieFrame.offsetBy(dx: 0, dy: -edgeLength/40)
}
return (edgeLength, dieFrame)
}
func drawDieWithSize(size: CGSize) {
if let intValue = intValue {
let (edgeLength, dieFrame) = metricsForSize(size: size)
let cornerRadius: CGFloat = edgeLength/5.0
let dotRadius = edgeLength/12.0
let dotFrame = dieFrame.insetBy(dx: dotRadius * 2.5, dy: dotRadius * 2.5)
// The glint must be within the dot.
let glintFrame = dotFrame
NSGraphicsContext.saveGraphicsState()
let shadow = NSShadow()
shadow.shadowOffset = NSSize(width: 0, height: -1)
//shadow.shadowBlurRadius = edgeLength/20
shadow.shadowBlurRadius = (pressed ? edgeLength/100 : edgeLength/20)
shadow.set()
// Draw the rounded shape of the die profile:
// Challenge use color Gradient - commented portions are used to make white die and were removed to make code more readable in this post
let gradient = NSGradient(starting: NSColor.red, ending: NSColor.blue)
dieShape =
NSBezierPath(roundedRect: dieFrame, xRadius: cornerRadius, yRadius: cornerRadius)
gradient?.draw(in: dieShape, angle: 1.0)
// Challlenge - use stroke() to add a border the die
NSColor.black.set()
dieShape.lineWidth = 4
dieShape.stroke()
NSGraphicsContext.restoreGraphicsState()
// Shadow will not apply to subequent drawing commands
// ready to draw the dots.
// Nested Function to make drawing dots cleaner:
func drawDot(u: CGFloat, v: CGFloat) {
let dotOrigin = CGPoint(x: dotFrame.minX + dotFrame.width * u,
y: dotFrame.minY + dotFrame.height * v)
let dotRect =
CGRect(origin: dotOrigin, size: CGSize.zero).insetBy(dx: -dotRadius, dy: -dotRadius)
// The dots will be black:
NSColor.black.set()
NSBezierPath(ovalIn: dotRect).fill()
}
// nested function to draw a glint in each dot
func drawGlint(u: CGFloat, v: CGFloat) {
let glintOrigin = CGPoint(x: glintFrame.minX + glintFrame.width * u,
y: glintFrame.minY + glintFrame.height * v)
let glintRect =
CGRect(origin: glintOrigin,
size: CGSize(width: 3.5, height: 3.5)).insetBy(dx: -0.5, dy: -0.5)
// Glints will be white
NSColor.white.set()
NSBezierPath(rect: glintRect).fill()
}
// If intVlaue is in range...
if intValue >= 1 && intValue <= 6 {
// Draw the dots:
if intValue == 1 || intValue == 3 || intValue == 5 {
drawDot(u: 0.5, v: 0.5) // Center dot
drawGlint(u: 0.55, v: 0.55)
}
if intValue >= 2 && intValue <= 6 {
drawDot(u: 0, v: 1) // upper left
drawGlint(u: 0.05, v: 1.05)
drawDot(u: 1, v: 0) // Lower right
drawGlint(u: 1.05, v: 0.05)
}
if intValue >= 4 && intValue <= 6 {
drawDot(u: 1, v: 1) // Upper right
drawGlint(u: 1.05, v: 1.05)
drawDot(u: 0, v: 0) // lower left
drawGlint(u: 0.05, v: 0.05)
}
if intValue == 6 {
drawDot(u: 0, v: 0.5) // Mid left/right
drawGlint(u: 0.05, v: 0.55)
drawDot(u: 1, v: 0.5)
drawGlint(u: 1.05, v: 0.55)
}
} else {
let paraStyle = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
paraStyle.alignment = .center
let font = NSFont.systemFont(ofSize: edgeLength * 0.5)
let attrs = [NSAttributedStringKey.foregroundColor: NSColor.black,
NSAttributedStringKey.font : font,
NSAttributedStringKey.paragraphStyle: paraStyle ]
let string = "\(intValue)" as NSString
string.drawCentered(in: dieFrame, attributes: attrs)
}
}
}
func randomize() {
intValue = Int(arc4random_uniform(5)) + 1
}
// MARK: - Mouse Events
override func mouseDown(with event: NSEvent) {
if dieShape.contains(event.locationInWindow) {
Swift.print("mouseDown CLICKCOUNT: \(event.clickCount)")
let dieFrame = metricsForSize(size: bounds.size).dieFrame
let pointInView = convert(event.locationInWindow, from: nil)
pressed = dieFrame.contains(pointInView)
}
}
override func mouseDragged(with event: NSEvent) {
Swift.print("mouseDragged")
}
override func mouseUp(with event: NSEvent) {
if dieShape.contains(event.locationInWindow) {
Swift.print("mouseUp clickCount: \(event.clickCount)")
if event.clickCount == 2 {
randomize()
}
pressed = false
}
}
// MARK: - First Responder
override func drawFocusRingMask() {
NSBezierPath.fill(bounds)
}
override var focusRingMaskBounds: NSRect {
return bounds
}
override var acceptsFirstResponder: Bool { return true }
override func becomeFirstResponder() -> Bool {
return true
}
override func resignFirstResponder() -> Bool {
return true
}
// MARK: Ketboard Events
override func keyDown(with event: NSEvent) {
interpretKeyEvents([event])
}
override func insertText(_ insertString: Any) {
let text = insertString as! String
if let number = Int(text) {
intValue = number
}
}
override func insertTab(_ sender: Any?) {
window?.selectNextKeyView(sender)
}
override func insertBacktab(_ sender: Any?) {
window?.selectPreviousKeyView(sender)
}
}
After a quick glance, my guess is the problem is in if dieShape.contains(event.locationInWindow).
diaShape is in local (view) coordinate. But event.locationInWindow is in window coordinates. You need to first convert the window coordinates into local coordinates before you can test the hit point.
Refer to the documentation for locationInWindow:
let eventLocation = event.locationInWindow
let localPoint = self.convert(eventLocation, from: nil)
if dieShape.contains(localPoint)
...
If your view is close to the origin of the window, the difference between window and view coordinates is small enough that it might work, but the ones further away won't.
Is it possible to create an SKAction for SKSpriteNode in SpriteKit that generates the same effect as "Photoshop" with the Edit->Transform->Distort option?
Example:
I solve with this implementation:
Swift 5
extension SKSpriteNode {
func addSkew(value: CGFloat = -1){
var effectNode = SKEffectNode()
effectNode.shouldRasterize = true
effectNode.shouldEnableEffects = true
effectNode.addChild(SKSpriteNode(texture: texture))
effectNode.zPosition = 1
let transform = CGAffineTransform(a: 1 , b: 0,
c: value, d: 1,
tx: 0 , ty: 0)
let transformFilter = CIFilter(name: "CIAffineTransform")!
transformFilter.setValue(transform, forKey: "inputTransform")
effectNode.filter = transformFilter
addChild(effectNode)
texture = nil
}
}
You can create a skew using a 1x1 warp mesh. This is supported in iOS10.0+.
This extension receives the skew angle in degrees, and distorts around the anchor point of the given sprite.
Swift 4.2
extension SKWarpGeometryGrid {
public static var skewPosGridZero:[float2] {
get {
return [float2(0.0, 0.0), float2(1.0, 0.0),
float2(0.0, 1.0), float2(1.0, 1.0)]
}
}
public static func skewXPosGrid(_ skewX: CGFloat, node:SKSpriteNode? = nil) -> [float2] {
let anchorY:Float = Float(node?.anchorPoint.y ?? 0.5)
var skewPosGrid = skewPosGridZero
let offsetX = Float(tan(skewX.degToRad()) * (node == nil ? 1.0 : (node!.size.height/node!.size.width)) )
skewPosGrid[2][0] += offsetX * (1.0 - anchorY)
skewPosGrid[3][0] += offsetX * (1.0 - anchorY)
skewPosGrid[0][0] -= offsetX * anchorY
skewPosGrid[1][0] -= offsetX * anchorY
return skewPosGrid
}
public static func skewYPosGrid(_ skewY: CGFloat, node:SKSpriteNode? = nil) -> [float2] {
let anchorX:Float = Float(node?.anchorPoint.x ?? 0.5)
var skewPosGrid = skewPosGridZero
let offsetY = Float(tan(skewY.degToRad()) * (node == nil ? 1.0 : (node!.size.width/node!.size.height)) )
skewPosGrid[1][1] += offsetY * (1.0 - anchorX)
skewPosGrid[3][1] += offsetY * (1.0 - anchorX)
skewPosGrid[0][1] -= offsetY * anchorX
skewPosGrid[2][1] -= offsetY * anchorX
return skewPosGrid
}
public static func skewX(_ angle: CGFloat, node:SKSpriteNode? = nil) -> SKWarpGeometryGrid {
return SKWarpGeometryGrid(columns: 1, rows: 1, sourcePositions: skewPosGridZero, destinationPositions: skewXPosGrid(angle, node:node))
}
public static func skewY(_ angle: CGFloat, node:SKSpriteNode? = nil) -> SKWarpGeometryGrid {
return SKWarpGeometryGrid(columns: 1, rows: 1, sourcePositions: skewPosGridZero, destinationPositions: skewYPosGrid(angle, node:node))
}
public static func skewZero() -> SKWarpGeometryGrid {
return SKWarpGeometryGrid(columns: 1, rows: 1)
}
}
Example animation:
let spriteNode = SKSpriteNode(imageNamed: "tex")
spriteNode.anchorPoint = CGPoint(x:0.25, y:1.0)
let skewA = SKWarpGeometryGrid.skewX(-45.0, node: spriteNode)
let skewB = SKWarpGeometryGrid.skewX(45.0, node: spriteNode)
spriteNode.warpGeometry = skewB
if let skewActionA = SKAction.warp(to: skewA, duration: 3.0),
let skewActionB = SKAction.warp(to: skewB, duration: 3.0){
// Individual easing
skewActionA.timingMode = .easeInEaseOut
skewActionB.timingMode = .easeInEaseOut
spriteNode.run(SKAction.repeatForever(SKAction.sequence([skewActionA,skewActionB])))
}
The list of available SKAction's is here: https://developer.apple.com/reference/spritekit/skaction
There is none to do exactly what you describe. Instead, you can export multiple sprite images from a photo editing tool like Photoshop, and use an animation action like class func animate(with: [SKTexture], timePerFrame: TimeInterval).
This is a little more work, but should achieve the desired effect.
I have nodes spawning every 0.2-5.0 seconds on my screen like so:
override func didMoveToView(view: SKView) {
backgroundColor = UIColor.whiteColor()
runAction(SKAction.repeatActionForever(
SKAction.sequence([
SKAction.runBlock(blackDots),
SKAction.waitForDuration(1.0)])))
}
func random() -> CGFloat {
return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
}
func random(min min: CGFloat, max: CGFloat) -> CGFloat {
return random() * (max - min) + min
}
func blackDots() {
let dot = SKSpriteNode(imageNamed: "first#2x")
dot.size = CGSizeMake(75, 75)
dot.name = "dotted"
dot.position = CGPointMake(500 * random(min: 0, max: 1), 500 * random(min: 0, max: 1))
addChild(dot)
}
However, when they are spawned, some intersect and lay on top of one another? Is there a way to prevent this? Thanks in advance.
Here's how to check if a node exists at a particular position.
You might want to throw the check into a loop so that if the position is taken, it will retry with a newly generated point. Otherwise you'll get some dots that just won't show. Just depends on what you're doing with them.
override func didMoveToView(view: SKView) {
backgroundColor = UIColor.whiteColor()
runAction(SKAction.repeatActionForever(
SKAction.sequence([
SKAction.runBlock(blackDots),
SKAction.waitForDuration(1.0)])))
}
func random() -> CGFloat {
return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
}
func random(min min: CGFloat, max: CGFloat) -> CGFloat {
return random() * (max - min) + min
}
func blackDots() {
let dot = SKSpriteNode(imageNamed: "first#2x")
dot.size = CGSizeMake(75, 75)
dot.name = "dotted"
let position = CGPointMake(500 * random(min: 0, max: 1), 500 * random(min: 0, max: 1))
if positionIsEmpty(position) {
dot.position = position
addChild(dot)
}
}
func positionIsEmpty(point: CGPoint) -> Bool {
self.enumerateChildNodesWithName("dotted", usingBlock: {
node, stop in
let dot = node as SKSpriteNode
if (CGRectContainsPoint(dot.frame, point)) {
return false
}
})
return true
}
here is this in swift 3.1 please note that this did take me several hours to recreate successfully also a for-loop or TimerInterval won't run this properly it has to be a sequence of actions
like this
let wait = SKAction.wait(forDuration: 1)
let spawn = SKAction.run {
//be sure to call your spawn function here
example()
}
let sequence = SKAction.sequence([wait, spawn])
self.run(SKAction.repeatForever(sequence))
if this is done right it should work like I have it
func example() {
//the person node is equal to an SKSpriteNode subclass
person = people()
func random() -> CGFloat {
//this is the random spawn generator increasing or decreasing these two numbers will change how far or how close they spawn together
return CGFloat(Float(arc4random_uniform(UInt32(500 - 343))))
}
func random(min: CGFloat, max: CGFloat) -> CGFloat {
return random() * (max - min) + min
}
func spawnPeople() {
//note that this is set to spawn them on the x-axis but can easily be changed for the y-axis
let position = CGPoint(x: 2 * random(min: 0, max: 1),y: -290)
if positionIsEmpty(point: position) {
//set the position of your node and add it to the scene here any actions you want to add to the node will go here
person.position = position
self.addChild(person)
//example
person.run(parallax1)
}
}
func positionIsEmpty(point: CGPoint) -> Bool {
if (person.frame.contains(point)) {
print("failed")
return false
}
print("success")
return true
}
//make sure to call the spawn(name)() function down here or the code will not run
spawnPeople()
}
also I have all of this code in my didMoveToView function