版本记录
版本号 | 时间 |
---|---|
V1.0 | 2020.08.07 星期五 |
前言
Core Haptics
是iOS13
的新的SDK,接下来几篇我们就一起看一下这个专题。感兴趣的可以看下面几篇文章。
1. Core Haptics框架详细解析(一) —— 基本概览(一)
2. Core Haptics框架详细解析(二) —— 一个简单示例(一)
源码
1. Swift
首先看下工程组织结构
接着就是sb
中的内容
下面就是代码了
1. Constants.swift
import CoreGraphics
enum ImageName {
static let background = "Background"
static let ground = "Ground"
static let water = "Water"
static let vineTexture = "VineTexture"
static let vineHolder = "VineHolder"
static let crocMouthClosed = "CrocMouthClosed"
static let crocMouthOpen = "CrocMouthOpen"
static let crocMask = "CrocMask"
static let prize = "Pineapple"
static let prizeMask = "PineappleMask"
}
enum SoundFile {
static let backgroundMusic = "CheeZeeJungle.caf"
static let slice = "Slice.caf"
static let splash = "Splash.caf"
static let nomNom = "NomNom.caf"
}
enum Layer {
static let background: CGFloat = 0
static let crocodile: CGFloat = 1
static let vine: CGFloat = 1
static let prize: CGFloat = 2
static let foreground: CGFloat = 3
}
enum PhysicsCategory {
static let crocodile: UInt32 = 1
static let vineHolder: UInt32 = 2
static let vine: UInt32 = 4
static let prize: UInt32 = 8
}
enum GameConfiguration {
static let vineDataFile = "VineData.plist"
static let canCutMultipleVinesAtOnce = false
}
enum Scene {
static let particles = "Particle.sks"
}
2. VineNode.swift
import UIKit
import SpriteKit
class VineNode: SKNode {
private let length: Int
private let anchorPoint: CGPoint
private var vineSegments: [SKNode] = []
init(length: Int, anchorPoint: CGPoint, name: String) {
self.length = length
self.anchorPoint = anchorPoint
super.init()
self.name = name
}
required init?(coder aDecoder: NSCoder) {
length = aDecoder.decodeInteger(forKey: "length")
anchorPoint = aDecoder.decodeCGPoint(forKey: "anchorPoint")
super.init(coder: aDecoder)
}
func addToScene(_ scene: SKScene) {
// add vine to scene
zPosition = Layer.vine
scene.addChild(self)
// create vine holder
let vineHolder = SKSpriteNode(imageNamed: ImageName.vineHolder)
vineHolder.position = anchorPoint
vineHolder.zPosition = 1
addChild(vineHolder)
vineHolder.physicsBody = SKPhysicsBody(circleOfRadius: vineHolder.size.width / 2)
vineHolder.physicsBody?.isDynamic = false
vineHolder.physicsBody?.categoryBitMask = PhysicsCategory.vineHolder
vineHolder.physicsBody?.collisionBitMask = 0
// add each of the vine parts
for i in 0..<length {
let vineSegment = SKSpriteNode(imageNamed: ImageName.vineTexture)
let offset = vineSegment.size.height * CGFloat(i + 1)
vineSegment.position = CGPoint(x: anchorPoint.x, y: anchorPoint.y - offset)
vineSegment.name = name
vineSegments.append(vineSegment)
addChild(vineSegment)
vineSegment.physicsBody = SKPhysicsBody(rectangleOf: vineSegment.size)
vineSegment.physicsBody?.categoryBitMask = PhysicsCategory.vine
vineSegment.physicsBody?.collisionBitMask = PhysicsCategory.vineHolder
}
// set up joint for vine holder
// swiftlint:disable force_unwrapping
let joint = SKPhysicsJointPin.joint(
withBodyA: vineHolder.physicsBody!,
bodyB: vineSegments[0].physicsBody!,
anchor: CGPoint(
x: vineHolder.frame.midX,
y: vineHolder.frame.midY))
scene.physicsWorld.add(joint)
// set up joints between vine parts
for i in 1..<length {
let nodeA = vineSegments[i - 1]
let nodeB = vineSegments[i]
let joint = SKPhysicsJointPin.joint(
withBodyA: nodeA.physicsBody!,
bodyB: nodeB.physicsBody!,
anchor: CGPoint(
x: nodeA.frame.midX,
y: nodeA.frame.minY))
scene.physicsWorld.add(joint)
// swiftlint:enable force_unwrapping
}
}
func attachToPrize(_ prize: SKSpriteNode) {
// align last segment of vine with prize
// swiftlint:disable force_unwrapping
let lastNode = vineSegments.last!
lastNode.position = CGPoint(
x: prize.position.x,
y: prize.position.y + prize.size.height * 0.1)
// set up connecting joint
let joint = SKPhysicsJointPin.joint(
withBodyA: lastNode.physicsBody!,
bodyB: prize.physicsBody!,
anchor: lastNode.position)
prize.scene?.physicsWorld.add(joint)
// swiftlint:enable force_unwrapping
}
}
3. GameViewController.swift
import UIKit
import SpriteKit
import GameplayKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Configure the view.
// swiftlint:disable:next force_cast
let skView = self.view as! SKView
skView.showsFPS = true
skView.showsNodeCount = true
skView.ignoresSiblingOrder = true
// Create and configure the scene.
let scene = GameScene(size: CGSize(width: 375, height: 667))
scene.scaleMode = .aspectFill
// Present the scene.
skView.presentScene(scene)
}
}
4. VineData.swift
import UIKit
struct VineData: Decodable {
let length: Int
let relAnchorPoint: CGPoint
}
5. GameScene.swift
import SpriteKit
import AVFoundation
import CoreHaptics
class GameScene: SKScene {
// swiftlint:disable implicitly_unwrapped_optional
private var particles: SKEmitterNode?
private var crocodile: SKSpriteNode!
private var prize: SKSpriteNode!
private var hapticManager: HapticManager?
private static var backgroundMusicPlayer: AVAudioPlayer!
private var sliceSoundAction: SKAction!
private var splashSoundAction: SKAction!
private var nomNomSoundAction: SKAction!
// swiftlint:enable implicitly_unwrapped_optional
private var isLevelOver = false
private var didCutVine = false
private var swishTimestamp: TimeInterval = 0
override func didMove(to view: SKView) {
hapticManager = HapticManager()
setUpPhysics()
setUpScenery()
setUpPrize()
setUpVines()
setUpCrocodile()
setUpAudio()
}
// MARK: - Level setup
private func setUpPhysics() {
physicsWorld.contactDelegate = self
physicsWorld.gravity = CGVector(dx: 0.0, dy: -9.8)
physicsWorld.speed = 1.0
}
private func setUpScenery() {
let background = SKSpriteNode(imageNamed: ImageName.background)
background.anchorPoint = CGPoint(x: 0, y: 0)
background.position = CGPoint(x: 0, y: 0)
background.zPosition = Layer.background
background.size = CGSize(width: size.width, height: size.height)
addChild(background)
let water = SKSpriteNode(imageNamed: ImageName.water)
water.anchorPoint = CGPoint(x: 0, y: 0)
water.position = CGPoint(x: 0, y: 0)
water.zPosition = Layer.foreground
water.size = CGSize(width: size.width, height: size.height * 0.2139)
addChild(water)
}
private func setUpPrize() {
prize = SKSpriteNode(imageNamed: ImageName.prize)
prize.position = CGPoint(x: size.width * 0.5, y: size.height * 0.7)
prize.zPosition = Layer.prize
prize.physicsBody = SKPhysicsBody(circleOfRadius: prize.size.height / 2)
prize.physicsBody?.categoryBitMask = PhysicsCategory.prize
prize.physicsBody?.collisionBitMask = 0
prize.physicsBody?.density = 0.5
addChild(prize)
}
// MARK: - Vine methods
private func setUpVines() {
// load vine data
let decoder = PropertyListDecoder()
guard
let dataFile = Bundle.main.url(
forResource: GameConfiguration.vineDataFile,
withExtension: nil),
let data = try? Data(contentsOf: dataFile),
let vines = try? decoder.decode([VineData].self, from: data)
else {
return
}
for (i, vineData) in vines.enumerated() {
let anchorPoint = CGPoint(
x: vineData.relAnchorPoint.x * size.width,
y: vineData.relAnchorPoint.y * size.height)
let vine = VineNode(length: vineData.length, anchorPoint: anchorPoint, name: "\(i)")
vine.addToScene(self)
vine.attachToPrize(prize)
}
}
// MARK: - Croc methods
private func setUpCrocodile() {
crocodile = SKSpriteNode(imageNamed: ImageName.crocMouthClosed)
crocodile.position = CGPoint(x: size.width * 0.75, y: size.height * 0.312)
crocodile.zPosition = Layer.crocodile
crocodile.physicsBody = SKPhysicsBody(
texture: SKTexture(imageNamed: ImageName.crocMask),
size: crocodile.size)
crocodile.physicsBody?.categoryBitMask = PhysicsCategory.crocodile
crocodile.physicsBody?.collisionBitMask = 0
crocodile.physicsBody?.contactTestBitMask = PhysicsCategory.prize
crocodile.physicsBody?.isDynamic = false
addChild(crocodile)
animateCrocodile()
}
private func animateCrocodile() {
let duration = Double.random(in: 2...4)
let open = SKAction.setTexture(SKTexture(imageNamed: ImageName.crocMouthOpen))
let wait = SKAction.wait(forDuration: duration)
let close = SKAction.setTexture(SKTexture(imageNamed: ImageName.crocMouthClosed))
let sequence = SKAction.sequence([wait, open, wait, close])
crocodile.run(.repeatForever(sequence))
}
private func runNomNomAnimation(withDelay delay: TimeInterval) {
crocodile.removeAllActions()
let closeMouth = SKAction.setTexture(SKTexture(imageNamed: ImageName.crocMouthClosed))
let wait = SKAction.wait(forDuration: delay)
let openMouth = SKAction.setTexture(SKTexture(imageNamed: ImageName.crocMouthOpen))
let sequence = SKAction.sequence([closeMouth, wait, openMouth, wait, closeMouth])
crocodile.run(sequence)
}
// MARK: - Touch handling
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
didCutVine = false
hapticManager?.startSwishPlayer()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let startPoint = touch.location(in: self)
let endPoint = touch.previousLocation(in: self)
// check if vine cut
scene?.physicsWorld.enumerateBodies(
alongRayStart: startPoint,
end: endPoint) { body, _, _, _ in
self.checkIfVineCut(withBody: body)
}
// produce some nice particles
showMoveParticles(touchPosition: startPoint)
// update haptic player intensity
let distance = CGVector(dx: abs(startPoint.x - endPoint.x), dy: abs(startPoint.y - endPoint.y))
let distanceRatio = CGVector(dx: distance.dx / size.width, dy: distance.dy / size.height)
let intensity = Float(max(distanceRatio.dx, distanceRatio.dy)) * 100
hapticManager?.updateSwishPlayer(intensity: intensity)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
particles?.removeFromParent()
particles = nil
hapticManager?.stopSwishPlayer()
}
private func showMoveParticles(touchPosition: CGPoint) {
// swiftlint:disable force_unwrapping
if particles == nil {
particles = SKEmitterNode(fileNamed: Scene.particles)
particles!.zPosition = 1
particles!.targetNode = self
addChild(particles!)
}
particles!.position = touchPosition
// swiftlint:enable force_unwrapping
}
// MARK: - Game logic
private func checkIfVineCut(withBody body: SKPhysicsBody) {
if didCutVine && !GameConfiguration.canCutMultipleVinesAtOnce {
return
}
guard let node = body.node else {
return
}
// if it has a name it must be a vine node
if let name = node.name {
// snip the vine
node.removeFromParent()
// fade out all nodes matching name
enumerateChildNodes(withName: name) { node, _ in
let fadeAway = SKAction.fadeOut(withDuration: 0.25)
let removeNode = SKAction.removeFromParent()
let sequence = SKAction.sequence([fadeAway, removeNode])
node.run(sequence)
}
crocodile.removeAllActions()
crocodile.texture = SKTexture(imageNamed: ImageName.crocMouthOpen)
animateCrocodile()
run(sliceSoundAction)
didCutVine = true
}
}
private func switchToNewGame(withTransition transition: SKTransition) {
let delay = SKAction.wait(forDuration: 1)
let sceneChange = SKAction.run {
let scene = GameScene(size: self.size)
self.view?.presentScene(scene, transition: transition)
}
run(.sequence([delay, sceneChange]))
}
// MARK: - Audio & Haptics
private func setUpAudio() {
if GameScene.backgroundMusicPlayer == nil {
guard let backgroundMusicURL = Bundle.main.url(
forResource: SoundFile.backgroundMusic,
withExtension: nil) else {
return
}
do {
let theme = try AVAudioPlayer(contentsOf: backgroundMusicURL)
GameScene.backgroundMusicPlayer = theme
} catch {
// couldn't load file :[
}
GameScene.backgroundMusicPlayer.numberOfLoops = -1
}
if !GameScene.backgroundMusicPlayer.isPlaying {
GameScene.backgroundMusicPlayer.play()
}
guard let manager = hapticManager else {
sliceSoundAction = .playSoundFileNamed(
SoundFile.slice,
waitForCompletion: false)
nomNomSoundAction = .playSoundFileNamed(
SoundFile.nomNom,
waitForCompletion: false)
splashSoundAction = .playSoundFileNamed(
SoundFile.splash,
waitForCompletion: false)
return
}
setupHaptics(manager)
}
private func setupHaptics(_ manager: HapticManager) {
let sliceHaptics = SKAction.run {
manager.playSlice()
}
if manager.sliceAudio != nil {
sliceSoundAction = sliceHaptics
} else {
sliceSoundAction = .group([
.playSoundFileNamed(SoundFile.slice, waitForCompletion: false),
sliceHaptics
])
}
let nomNomHaptics = SKAction.run {
manager.playNomNom()
}
if manager.nomNomAudio != nil {
nomNomSoundAction = nomNomHaptics
} else {
nomNomSoundAction = .group([
.playSoundFileNamed(SoundFile.nomNom, waitForCompletion: false),
nomNomHaptics
])
}
let splashHaptics = SKAction.run {
manager.playSplash()
}
if manager.splashAudio != nil {
splashSoundAction = splashHaptics
} else {
splashSoundAction = .group([
.playSoundFileNamed(SoundFile.splash, waitForCompletion: false),
splashHaptics
])
}
}
}
extension GameScene: SKPhysicsContactDelegate {
override func update(_ currentTime: TimeInterval) {
if isLevelOver {
return
}
if prize.position.y <= 0 {
isLevelOver = true
run(splashSoundAction)
switchToNewGame(withTransition: .fade(withDuration: 1.0))
}
}
func didBegin(_ contact: SKPhysicsContact) {
if isLevelOver {
return
}
if (contact.bodyA.node == crocodile && contact.bodyB.node == prize)
|| (contact.bodyA.node == prize && contact.bodyB.node == crocodile) {
isLevelOver = true
// shrink the pineapple away
let shrink = SKAction.scale(to: 0, duration: 0.08)
let removeNode = SKAction.removeFromParent()
let sequence = SKAction.sequence([shrink, removeNode])
prize.run(sequence)
run(nomNomSoundAction)
runNomNomAnimation(withDelay: 0.15)
// transition to next level
switchToNewGame(withTransition: .doorway(withDuration: 1.0))
}
}
}
6. Haptics.swift
import CoreHaptics
class HapticManager {
let hapticEngine: CHHapticEngine
var sliceAudio: CHHapticAudioResourceID?
var nomNomAudio: CHHapticAudioResourceID?
var splashAudio: CHHapticAudioResourceID?
var swishPlayer: CHHapticAdvancedPatternPlayer?
// Failable initializer: the game will ignore haptics if the manager is nil
init?() {
// Check if the device supports haptics and fail the initializer if it doesn't
let hapticCapability = CHHapticEngine.capabilitiesForHardware()
guard hapticCapability.supportsHaptics else {
return nil
}
// Try to ceate the engine, fail the initializer if it fails
do {
hapticEngine = try CHHapticEngine()
} catch let error {
print("Haptic engine Creation Error: \(error)")
return nil
}
do {
try hapticEngine.start()
} catch let error {
print("Haptic failed to start Error: \(error)")
}
hapticEngine.isAutoShutdownEnabled = true
hapticEngine.resetHandler = { [weak self] in
self?.handleEngineReset()
}
// Setup our audio resources
setupResources()
}
private func handleEngineReset() {
print("Engine is resetting...")
do {
try hapticEngine.start()
setupResources()
createSwishPlayer()
} catch {
print("Failed to restart the engine: \(error)")
}
}
private func setupResources() {
do {
if let path = Bundle.main.url(forResource: "Slice", withExtension: "caf") {
sliceAudio = try hapticEngine.registerAudioResource(path)
}
if let path = Bundle.main.url(forResource: "NomNom", withExtension: "caf") {
nomNomAudio = try hapticEngine.registerAudioResource(path)
}
if let path = Bundle.main.url(forResource: "Splash", withExtension: "caf") {
splashAudio = try hapticEngine.registerAudioResource(path)
}
} catch {
print("Failed to load audio: \(error)")
}
createSwishPlayer()
}
// MARK: - Dynamic Swish Player
private func createSwishPlayer() {
let swish = CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
],
relativeTime: 0,
duration: 60)
do {
let pattern = try CHHapticPattern(events: [swish], parameters: [])
swishPlayer = try hapticEngine.makeAdvancedPlayer(with: pattern)
} catch let error {
print("Swish player error: \(error)")
}
}
func startSwishPlayer() {
do {
try hapticEngine.start()
try swishPlayer?.start(atTime: CHHapticTimeImmediate)
} catch {
print("Swish player start error: \(error)")
}
}
func stopSwishPlayer() {
do {
try swishPlayer?.stop(atTime: CHHapticTimeImmediate)
} catch {
print("Swish player stop error: \(error)")
}
}
func updateSwishPlayer(intensity: Float) {
let intensity = CHHapticDynamicParameter(
parameterID: .hapticIntensityControl,
value: intensity,
relativeTime: 0)
do {
try swishPlayer?.sendParameters([intensity], atTime: CHHapticTimeImmediate)
} catch let error {
print("Swish player dynamic update error: \(error)")
}
}
// MARK: - Play Haptic Patterns
func playSlice() {
do {
let pattern = try slicePattern()
try playHapticFromPattern(pattern)
} catch {
print("Failed to play slice: \(error)")
}
}
func playNomNom() {
do {
let pattern = try nomNomPattern()
try playHapticFromPattern(pattern)
} catch {
print("Failed to play nomNom: \(error)")
}
}
func playSplash() {
do {
let pattern = try splashPattern()
try playHapticFromPattern(pattern)
} catch {
print("Failed to play splash: \(error)")
}
}
private func playHapticFromPattern(_ pattern: CHHapticPattern) throws {
try hapticEngine.start()
let player = try hapticEngine.makePlayer(with: pattern)
try player.start(atTime: CHHapticTimeImmediate)
}
}
// MARK: - Haptic Patterns
extension HapticManager {
private func slicePattern() throws -> CHHapticPattern {
let slice = CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8)
],
relativeTime: 0,
duration: 0.5)
let snip = CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
],
relativeTime: 0.08)
let curve = CHHapticParameterCurve(
parameterID: .hapticIntensityControl,
controlPoints: [
.init(relativeTime: 0, value: 0.2),
.init(relativeTime: 0.08, value: 1.0),
.init(relativeTime: 0.24, value: 0.2),
.init(relativeTime: 0.34, value: 0.6),
.init(relativeTime: 0.5, value: 0)
],
relativeTime: 0)
var events = [slice, snip]
if let audioResourceID = sliceAudio {
let audio = CHHapticEvent(audioResourceID: audioResourceID, parameters: [], relativeTime: 0)
events.append(audio)
}
return try CHHapticPattern(events: events, parameterCurves: [curve])
}
private func nomNomPattern() throws -> CHHapticPattern {
let rumble1 = CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
],
relativeTime: 0,
duration: 0.15)
let rumble2 = CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.1)
],
relativeTime: 0.3,
duration: 0.3)
let crunch1 = CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
],
relativeTime: 0)
let crunch2 = CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
],
relativeTime: 0.3)
var events = [rumble1, rumble2, crunch1, crunch2]
if let audioResourceID = nomNomAudio {
let audio = CHHapticEvent(audioResourceID: audioResourceID, parameters: [], relativeTime: 0)
events.append(audio)
}
return try CHHapticPattern(events: events, parameters: [])
}
private func splashPattern() throws -> CHHapticPattern {
let splish = CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.1)
],
relativeTime: 0)
let splash = CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.1),
CHHapticEventParameter(parameterID: .attackTime, value: 0.1),
CHHapticEventParameter(parameterID: .releaseTime, value: 0.2),
CHHapticEventParameter(parameterID: .decayTime, value: 0.3)
],
relativeTime: 0.1,
duration: 0.6)
var events = [splish, splash]
if let audioResourceID = splashAudio {
let audio = CHHapticEvent(audioResourceID: audioResourceID, parameters: [], relativeTime: 0)
events.append(audio)
}
return try CHHapticPattern(events: events, parameters: [])
}
}
后记
本篇主要讲述了
Core Haptics
的一个简单示例,感兴趣的给个赞或者关注~~~
网友评论