Last active
October 31, 2023 00:49
-
-
Save overlair/b500460a3190d608d7d2a8108c8c0d0c to your computer and use it in GitHub Desktop.
SpriteKit-Coordinate-Conversion
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SpriteKit | |
import SwiftUI | |
import Combine | |
enum ControlUpdate { | |
case tap(UITapGestureRecognizer) // print SpriteKit coordinate | |
case doubleTap // reset camera | |
case pan(UIPanGestureRecognizer) // move camera | |
} | |
struct ExampleView: View { | |
@State var isPaused = false | |
var messages = PassthroughSubject<ControlUpdate, Never>() | |
var body: some View { | |
GeometryReader { geo in | |
SpriteKitView(isPaused: $isPaused, | |
size: geo.size, | |
messages: messages) | |
.overlay(gestures) | |
} | |
} | |
@ViewBuilder var gestures: some View { | |
ExampleGestureRepresentable(messages: messages) | |
.frame(maxWidth: .infinity, maxHeight: .infinity) | |
} | |
} | |
struct SpriteKitView: View { | |
@Binding var isPaused: Bool | |
let size: CGSize | |
let messages: PassthroughSubject<ControlUpdate, Never> | |
var scene: SKScene{ | |
let scene = ExampleScene(size: size, | |
messages: messages) | |
scene.size = size | |
scene.scaleMode = .fill | |
return scene | |
} | |
var body: some View { | |
SpriteView(scene: scene, | |
isPaused: isPaused) | |
.frame(width: size.width, height: size.height) | |
} | |
} | |
struct ExampleGestureRepresentable: UIViewRepresentable { | |
let messages: PassthroughSubject<ControlUpdate, Never> | |
func makeUIView(context: Context) -> some UIView { | |
let v = UIView(frame: .zero) | |
let tap = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.tap)) | |
v.addGestureRecognizer(tap) | |
let pan = UIPanGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.pan)) | |
v.addGestureRecognizer(pan) | |
let doubleTap = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.doubleTap)) | |
doubleTap.numberOfTapsRequired = 2 | |
v.addGestureRecognizer(doubleTap) | |
return v | |
} | |
class Coordinator: NSObject, UIGestureRecognizerDelegate { | |
let messages: PassthroughSubject<ControlUpdate, Never> | |
init(messages: PassthroughSubject<ControlUpdate, Never>) { | |
self.messages = messages | |
} | |
@objc func tap(gesture: UITapGestureRecognizer) { | |
messages.send(.tap(gesture)) | |
} | |
@objc func doubleTap(gesture: UITapGestureRecognizer) { | |
messages.send(.doubleTap) | |
} | |
@objc func pan(gesture: UIPanGestureRecognizer) { | |
messages.send(.pan(gesture)) | |
} | |
} | |
func makeCoordinator() -> Coordinator { | |
Coordinator(messages: messages) | |
} | |
func updateUIView(_ uiView: UIViewType, context: Context) {} | |
} | |
class ExampleScene: SKScene { | |
let messages: PassthroughSubject<ControlUpdate, Never> | |
init(size: CGSize, | |
messages: PassthroughSubject<ControlUpdate, Never>) { | |
self.messages = messages | |
super.init(size: size) | |
} | |
required init?(coder aDecoder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
override func sceneDidLoad() { | |
super.sceneDidLoad() | |
setup() | |
} | |
var cancellables = Set<AnyCancellable>() | |
private func setup() { | |
messages | |
.sink(receiveValue: handle) | |
.store(in: &cancellables) | |
// setup nodes | |
let shape: CGPath = .init(roundedRect: .init(origin: .zero, | |
size: .init(width: 50,height: 50)), | |
cornerWidth: 12, | |
cornerHeight: 12, | |
transform: nil) | |
let tapNode = SKShapeNode(path: shape, centered: true) | |
self.shapeNode = tapNode | |
self.shapeNode?.position = origin | |
self.shapeNode?.fillColor = .blue | |
addChild(tapNode) | |
let node = SKShapeNode(path: shape, centered: true) | |
node.position = origin | |
node.fillColor = .orange | |
addChild(node) | |
camera = sceneCamera | |
sceneCamera.position = origin | |
addChild(sceneCamera) | |
} | |
var sceneCamera: SKCameraNode = SKCameraNode() | |
var shapeNode: SKShapeNode? = nil | |
var origin: CGPoint { | |
CGPoint(x: size.width / 2.0, y: size.height / 2.0) | |
} | |
var dragOrigin: CGPoint = .zero | |
private func handle(_ message: ControlUpdate) { | |
switch message { | |
case .tap(let gesture): | |
let location = gesture.location(in: gesture.view) | |
let point = convertPoint(fromView: location) | |
moveNode(node: shapeNode, | |
to: point, | |
at: 0.3, | |
with: .easeOut) | |
case .pan(let pan): | |
switch pan.state { | |
case .began: | |
dragOrigin = self.camera?.position ?? .zero | |
case .changed: | |
let translation = pan.translation(in: pan.view) | |
let point = CGPoint(x: dragOrigin.x - translation.x, | |
y: dragOrigin.y + translation.y) | |
moveNode(node: sceneCamera, | |
to: point) | |
case .ended, .cancelled: | |
dragOrigin = .zero | |
default: break | |
} | |
break | |
case .doubleTap: | |
moveNode(node: sceneCamera, | |
to: origin, | |
at: 0.3, | |
with: .easeInEaseOut) | |
break | |
} | |
} | |
func moveNode(node: SKNode?, | |
to point: CGPoint, | |
at duration: CGFloat = 0.0, | |
with timing: SKActionTimingMode = .linear) { | |
let move = SKAction.move(to: point, duration: duration) | |
move.timingMode = timing | |
node?.run(move, withKey: "moveNode") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I've created an Xcode project and put the code in
ContentView.swift
and replaced all theContentView
withExampleView
.The preview is running. I usually try across iPhones and iPads to make sure that the code is responsive. I want to avoid any layout or positioning that is tied to a specific screen or ratio.
Could you please elaborate on what is the code supposed to do so I can match the intent with the code?
Right now, I can:
Thank you!