Shaping up for action: CGPath and UIBezierPath
Shaping up for action: CGPath and UIBezierPath 관련
Like I already explained, we're going to keep an array of the user's swipe points so that we can draw a shape resembling their slicing. To make this work, we're going to need four new methods, two of which you've met already. They are: touchesBegan()
, touchesMoved()
, touchesEnded()
and redrawActiveSlice()
. You already know how touchesBegan()
and touchesMoved()
works, and the other "touches" methods all work the same way.
First things first: add this new property to your class so that we can store swipe points:
var activeSlicePoints = [CGPoint]()
We're going to tackle the two easiest methods first: touchesMoved()
and touchesEnded()
. All the touchesMoved()
method needs to do is figure out where in the scene the user touched, add that location to the slice points array, then redraw the slice shape, so that's easy enough:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
activeSlicePoints.append(location)
redrawActiveSlice()
}
When the user finishes touching the screen, touchesEnded()
will be called. I'm going to make this method fade out the slice shapes over a quarter of a second. We could remove them immediately but that looks ugly, and leaving them sitting there for no reason would rather destroy the effect. So, fading it is – add this touchesEnded()
method:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
activeSliceBG.run(SKAction.fadeOut(withDuration: 0.25))
activeSliceFG.run(SKAction.fadeOut(withDuration: 0.25))
}
You haven't used the fadeOut(withDuration:)
action before, but I think it's pretty obvious what it does!
So far this is all easy stuff, but we're going to look at an interesting method now: touchesBegan()
. One we’ve read out the touch from the UITouch
set, this needs to do several things:
- Remove all existing points in the
activeSlicePoints
array, because we're starting fresh. - Get the touch location and add it to the
activeSlicePoints
array. - Call the (as yet unwritten)
redrawActiveSlice()
method to clear the slice shapes. - Remove any actions that are currently attached to the slice shapes. This will be important if they are in the middle of a
fadeOut(withDuration:)
action. - Set both slice shapes to have an alpha value of 1 so they are fully visible.
We can convert that to code with ease – in fact, I've put numbered comments in the code below so you can match them up to the points above:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
// 1
activeSlicePoints.removeAll(keepingCapacity: true)
// 2
let location = touch.location(in: self)
activeSlicePoints.append(location)
// 3
redrawActiveSlice()
// 4
activeSliceBG.removeAllActions()
activeSliceFG.removeAllActions()
// 5
activeSliceBG.alpha = 1
activeSliceFG.alpha = 1
}
So, there's some challenge there but not a whole lot. Where it gets interesting is the redrawActiveSlice()
method, because this is going to use a UIBezierPath
that will be used to connect our swipe points together into a single line.
As with the previous method, let's take a look at what redrawActiveSlice()
needs to do:
- If we have fewer than two points in our array, we don't have enough data to draw a line so it needs to clear the shapes and exit the method.
- If we have more than 12 slice points in our array, we need to remove the oldest ones until we have at most 12 – this stops the swipe shapes from becoming too long.
- It needs to start its line at the position of the first swipe point, then go through each of the others drawing lines to each point.
- Finally, it needs to update the slice shape paths so they get drawn using their designs – i.e., line width and color.
To make this work, you're going to need to know that an SKShapeNode
object has a property called path
which describes the shape we want to draw. When it's nil
, there's nothing to draw; when it's set to a valid path, that gets drawn with the SKShapeNode
's settings. SKShapeNode
expects you to use a data type called CGPath
, but we can easily create that from a UIBezierPath
.
Drawing a complex path using UIBezierPath
is a cinch: we'll use its move(to:)
method to position the start of our lines, then loop through our activeSlicePoints
array and call the path's addLine(to:)
method for each point.
To stop the array storing more than 12 slice points, we’re going new method called removeFirst()
, which lets us remove a certain number of items from the start of an array. In this case we know we want at most 12, so we can subtract 12 from our current count to see how many excess we have, and pass that to removeFirst()
.
I'm going to insert numbered comments into the code again to help you match up the goals with the code more easily:
func redrawActiveSlice() {
// 1
if activeSlicePoints.count < 2 {
activeSliceBG.path = nil
activeSliceFG.path = nil
return
}
// 2
if activeSlicePoints.count > 12 {
activeSlicePoints.removeFirst(activeSlicePoints.count - 12)
}
// 3
let path = UIBezierPath()
path.move(to: activeSlicePoints[0])
for i in 1 ..< activeSlicePoints.count {
path.addLine(to: activeSlicePoints[i])
}
// 4
activeSliceBG.path = path.cgPath
activeSliceFG.path = path.cgPath
}
At this point, we have something you can run: press Cmd+R to run the game, then tap and swipe around on the screen to see the slice effect – I think you'll agree that SKShapeNode
is pretty powerful!
Before we're done with the slice effect, we're going to add one more thing: a "swoosh" sound that plays as you swipe around. You've already seen the playSoundFileNamed()
method of SKAction
, but we're going to use it a little differently here.
You see, if we just played a swoosh every time the player moved, there would be 100 sounds playing at any given time – one for every small movement they made. Instead, we want only one swoosh to play at once, so we're going to set to true a property called isSwooshSoundActive
, make the waitForCompletion
of our SKAction
true, then use a completion closure for runAction()
so that isSwooshSoundActive
is set to false.
So, when the player first swipes we set isSwooshSoundActive
to be true, and only when the swoosh sound has finished playing do we set it back to false again. This will allow us to ensure only one swoosh sound is playing at a time.
First, give your class this new property:
var isSwooshSoundActive = false
Now we need to check whether that's false when touchesMoved()
is called, and, if it is false, call a new method called playSwooshSound()
. Add this to code just before the end of touchesMoved()
:
if !isSwooshSoundActive {
playSwooshSound()
}
I've provided you with three different swoosh sounds, all of which are effectively the same just at varying pitches. The playSwooshSound()
method needs to set isSwooshSoundActive
to be true (so that no other swoosh sounds are played until we're ready), play one of the three sounds, then when the sound has finished set isSwooshSoundActive
to be false again so that another swoosh sound can play.
By playing our sound with waitForCompletion
set to true, SpriteKit automatically ensures the completion closure given to runAction()
isn't called until the sound has finished, so this solution is perfect.
func playSwooshSound() {
isSwooshSoundActive = true
let randomNumber = Int.random(in: 1...3)
let soundName = "swoosh\(randomNumber).caf"
let swooshSound = SKAction.playSoundFileNamed(soundName, waitForCompletion: true)
run(swooshSound) { [weak self] in
self?.isSwooshSoundActive = false
}
}
If you try running the game now you should hear only one swipe sound at a time.