2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

iOSAdvent Calendar 2022

Day 12

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

Last updated at Posted at 2022-12-11

Xcode-14.1 iOS-16.0

はじめに

その1〜その5の番外編です。

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

実装

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

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

Multipeer ConnectivityでのP2P通信(Swift)

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

ゲーム側

MultipeerConnectivity の操作をするクラスを作成します(データのやりとりめんどくさいので今回は NSNotification を使いました)。

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)
        // 1対1想定なので停止
        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 で通知を受信できるよう修正。

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 追加。

struct ContentView: View {

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

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

コントローラー側

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

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)
        // 1対1想定
        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)
        // 1対1想定なので停止
        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)作成。

struct BrowserView: UIViewControllerRepresentable{

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

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

    }
}

画面作成。

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)
    }
}

おわりに

これで2つの iOS 端末が必要な豪華なアプリができました:clap:

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

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?