はじめに
その1〜その5の番外編です。
今回はこんな感じでコントローラーで操作できるようにします
振動パックも実装したのでもはや64🤥 pic.twitter.com/iHLEU4Blnj
— am10 (@am103141592) September 26, 2022
実装
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 端末が必要な豪華なアプリができました
(ストアの審査通るのかは知りません)