SpriteKitで2D横スクロールゲームをつくろう（番外編）

![Xcode-14.1](https://img.shields.io/badge/Xcode-14.1-brightgreen) ![iOS-16.0](https://img.shields.io/badge/iOS-16.0-brightgreen) 

## はじめに
その１〜その５の番外編です。

* [その１](https://qiita.com/am10/items/df42c1f0091e6c4705e2)
* [その２](https://qiita.com/am10/items/d53d1cdf1b973a9f56cc)
* [その３](https://qiita.com/am10/items/8729f77f3ee512a86052)
* [その４](https://qiita.com/am10/items/90e3a2f58ae88e4ad6c4)
* [その５](https://qiita.com/am10/items/deacceda00c034ac29bb)

今回はこんな感じでコントローラーで操作できるようにします:wink:

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">振動パックも実装したのでもはや64🤥 <a href="https://t.co/iHLEU4Blnj">pic.twitter.com/iHLEU4Blnj</a></p>&mdash; am10 (@am103141592) <a href="https://twitter.com/am103141592/status/1574372961381134336?ref_src=twsrc%5Etfw">September 26, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## 実装
MultipeerConnectivity を使ってコントローラーとの接続をおこないます（iOS 同士なら便利です。Android とかと接続したい場合は BLE 使うといいかもです）。

MultipeerConnectivity の細かい部分は下記を参考にどうぞ。

[Multipeer ConnectivityでのP2P通信（Swift）](https://qiita.com/am10/items/e56c2bc6eaab75bc9c8c)

ゲーム側とコントローラー側で２つのプロジェクトを用意します。

### ゲーム側
MultipeerConnectivity の操作をするクラスを作成します（データのやりとりめんどくさいので今回は `NSNotification` を使いました）。

```swift
import MultipeerConnectivity

final class P2PManager: NSObject {

    // サービスタイプは適切なものを設定してください
    private let serviceType = "hoge-fuga-game"
    private var session: MCSession!
    private var advertiser: MCNearbyServiceAdvertiser!

    override init() {
        super.init()
        let peerID = MCPeerID(displayName: "Game")
        session = MCSession(peer: peerID)
        session.delegate = self

        advertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceType)
        advertiser.delegate = self
        advertiser.startAdvertisingPeer()
    }

    private func send(value: Int) {
        guard let data = "\(value)".data(using: .utf8) else {
            assertionFailure("データ送信非対応")
            return
        }
        do {
            try session.send(data, toPeers: session.connectedPeers, with: .reliable)
        } catch let error {
            print(error.localizedDescription)
        }
    }
}

extension P2PManager: MCSessionDelegate {

    func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
        print(state)
        print(peerID.displayName)
    }

    func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
        guard let string = String(data: data, encoding: .utf8),
              let value = Int(string) else {
            assertionFailure("データ受信非対応")
            return
        }
        NotificationCenter.default.post(name: .P2PDidReceiveValue, object: value)
    }

    func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
        assertionFailure("非対応")
    }

    func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
        assertionFailure("非対応")
    }

    func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
        assertionFailure("非対応")
    }
}

extension P2PManager: MCNearbyServiceAdvertiserDelegate {

    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
        invitationHandler(true, session)
        // １対１想定なので停止
        advertiser.stopAdvertisingPeer()
    }

    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: Error) {
        print(error.localizedDescription)
    }
}

extension NSNotification.Name {

    static let P2PDidReceiveValue: NSNotification.Name = .init(rawValue: "P2PDidReceiveValue")
}
```

info.plist に下記を追加します。

```
   <key>NSLocalNetworkUsageDescription</key>
    <string>ローカルネットワークを使う理由</string>
    <key>NSBonjourServices</key>
    <array>
        <string>_hoge-fuga-game._tcp</string>
        <string>_hoge-fuga-game._udp</string>
    </array>
```

SKScene で通知を受信できるよう修正。

```swift
override func didMove(to view: SKView) {
    NotificationCenter.default.addObserver(forName: .P2PDidReceiveValue, object: nil, queue: .main) { [weak self] notification in
        switch notification.object as? Int {
        case 1:
            self?.runAction()
        case 2:
            self?.jumpAction()
        default:
            break
        }
    }
}

func runAction() {
    if status == .jumping || status == .prepareJump {
        return
    }
    // リピートすると終了タイミング取るのが難しいのでアクション変更してます
    player.run(.group([
        .animate(with: runTextures, timePerFrame: 0.2),
        .moveBy(x: 5, y: 0, duration: 0.2)
    ]))
}

func jumpAction() {
    if status == .jumping || status == .prepareJump {
        return
    }
    status = .prepareJump
    let action = SKAction.group([
        .animate(with: jumpTextures, timePerFrame: 0.2),
        .sequence([
            .wait(forDuration: 0.2),
            .applyImpulse(.init(dx: 3, dy: 13), duration: 0.2)
        ])
    ])
    player.run(action) {
        self.player.run(.setTexture(self.runTextures[1]))
    }
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
        self.status = .jumping
    }
}
```

View に `P2PManager` 追加。

```swift
struct ContentView: View {

    // これ追加
    private let p2pManager = P2PManager()

    var body: some View {
        return GeometryReader {
            SpriteView(scene: GameScene(size: $0.size))
        }
    }
}
```

### コントローラー側
MultipeerConnectivity の操作をするクラスを作成。`MCBrowserViewController` を使うためにめんどくさかったのでシングルトンにしてます。

```swift
import MultipeerConnectivity

final class P2PManager: NSObject {
    static let shared = P2PManager()
    // サービスタイプは適切なものを設定してください
    private let serviceType = "hoge-fuga-game"
    private var session: MCSession!
    private var advertiser: MCNearbyServiceAdvertiser!

    var isConnected: Bool {
         return !session.connectedPeers.isEmpty
    }

    private override init() {
        super.init()
        let peerID = MCPeerID(displayName: "GameCon")
        session = MCSession(peer: peerID)
        advertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceType)
        advertiser.delegate = self
        advertiser.startAdvertisingPeer()
    }

    func send(value: Int) {
        guard let data = "\(value)".data(using: .utf8) else {
            assertionFailure("データ送信非対応")
            return
        }
        do {
            try session.send(data, toPeers: session.connectedPeers, with: .reliable)
        } catch let error {
            print(error.localizedDescription)
        }
    }

    func makeBrowserViewController() -> MCBrowserViewController {

        let vc = MCBrowserViewController(serviceType: serviceType, session: session)
        // １対１想定
        vc.maximumNumberOfPeers = 2
        vc.delegate = self
        return vc
    }
}

extension P2PManager: MCBrowserViewControllerDelegate {

    func browserViewControllerDidFinish(_ browserViewController: MCBrowserViewController) {
        browserViewController.dismiss(animated: true)
        advertiser.stopAdvertisingPeer()
    }

    func browserViewControllerWasCancelled(_ browserViewController: MCBrowserViewController) {
        browserViewController.dismiss(animated: true)
        advertiser.stopAdvertisingPeer()
    }

    func browserViewController(_ browserViewController: MCBrowserViewController, shouldPresentNearbyPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) -> Bool {
        return true
    }
}

extension P2PManager: MCNearbyServiceAdvertiserDelegate {

    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
        invitationHandler(true, session)
        // １対１想定なので停止
        advertiser.stopAdvertisingPeer()
    }

    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: Error) {
        print(error.localizedDescription)
    }
}
```

info.plist に下記を追加します。

```
   <key>NSLocalNetworkUsageDescription</key>
    <string>ローカルネットワークを使う理由</string>
    <key>NSBonjourServices</key>
    <array>
        <string>_hoge-fuga-game._tcp</string>
        <string>_hoge-fuga-game._udp</string>
    </array>
```

端末検索用の画面（MCBrowserViewController）作成。

```swift
struct BrowserView: UIViewControllerRepresentable{

    func makeUIViewController(context: Context) -> MCBrowserViewController {
        return P2PManager.shared.makeBrowserViewController()
    }

    func updateUIViewController(_ uiViewController: MCBrowserViewController, context: Context) {

    }
}
```

画面作成。

```swift
import SwiftUI

struct ContentView: View {

    private let p2pManager = P2PManager.shared
    @State private var isActive = false

    var body: some View {
        HStack {
            Button {
                send(value: 2)
            } label: {
                Image(systemName: "arrow.up")
            }

            Button {
                send(value: 1)
            } label: {
                Image(systemName: "arrow.right")
            }
        }.fullScreenCover(isPresented: $isActive) {
            BrowserView()
        }.onAppear {
            if !p2pManager.isConnected {
                isActive = true
            }
        }
    }

    private func send(value: Int) {
        p2pManager.send(value: value)
    }
}
```

## おわりに
これで２つの iOS 端末が必要な豪華なアプリができました:clap:

（ストアの審査通るのかは知りません:stuck_out_tongue_winking_eye:）