チャットで人狼をするアプリを2013年から運用しています。
Clubhouseが利用していることで話題になったagora.ioのSDKを使って、ボイスチャットで人狼ができたらおもしろいんじゃないかと思い、実装することにしました。
やりたいこと
人狼アプリという性質上、次のような実装を目指します。
- 昼間の時間は生存している人のみが発言できる
- 死亡した人は生存者の会話は聞けるが発言はできない
- 死亡した人は死亡した人同士でいつでも会話できる
- 人狼が複数いる場合、夜の時間は人狼同士で会話できる
- ゲームに参加していない人は、生存している人の会話を観覧(?)できる
agora.ioについて
agora.ioは手軽にビデオチャットやボイスチャットができるようになるサービスです。
サービスの登録方法、SDKの導入方法につきましては割愛します。
今回はiOSアプリに導入してみました。
マルチチャンネル接続する
agoraについての記事はだいたいシングルチャンネル接続について解説しているようです。
今回の要件ではマルチチャンネル接続をしたかったのですが、あまりドキュメントがなかったので解説していきます。
こちらは公式ドキュメントのビデオのほうでマルチチャンネル接続する方法です。
ボイスの場合も基本的には同じです。
注意点としては、ChannelProfile
を .liveBroadcasting
にしないといけません。
初期化コードは次のようにしました。
self.agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: agoraAppId, delegate: self)
self.agoraKit?.disableVideo()
self.agoraKit?.setAudioProfile(self.audioProfile, scenario: self.audioScenario)
self.agoraKit?.setChannelProfile(.liveBroadcasting)
self.agoraKit?.enableAudioVolumeIndication(200, smooth: 3, report_vad: true)
生存者のチャンネルにジョインする
生存者のチャンネルは、昼の間は生存者はbroadcaster
、それ以外の人はaudience
としてジョインします。
マルチチャンネルの場合、誰かがすでに作成したチャンネルにジョインするのではなく、すべての人が自分でチャンネルを作成してそこにジョインします。
チャンネル名が同じ場合は同じチャンネルとして扱われますので、チャンネルがたくさんできてしまうというわけではありません。
生存者のチャンネルにジョインするコードは以下のようになりました。
private func doJoinAliveChannel(channelId: String, playerId: String, isAudience: Bool, token: String) {
guard let agoraKit = self.agoraKit else {
return
}
if let channel = agoraKit.createRtcChannel(channelId) {
channel.setRtcChannelDelegate(self)
if isAudience {
channel.setClientRole(.audience)
print("Joining Alive channel as audience.")
} else {
channel.setClientRole(.broadcaster)
print("Joining Alive channel as broadcaster.")
}
channel.publish()
let mediaOptions = AgoraRtcChannelMediaOptions()
mediaOptions.autoSubscribeAudio = true
let result = channel.join(byUserAccount: playerId, token: token, options: mediaOptions)
if result != 0 {
print("Error joining channel: \(channelId) \(result)")
return
}
self.aliveChannel = channel
}
}
死者の場合は同じように死者のチャンネルにbroadcaster
としてジョインします。
マルチチャンネルなので同時にジョインできます。
夜の時間になったら、一旦生存者チャンネルをクローズして、人狼の人のみを人狼チャンネルに接続させます。
delegateを設定する
こちらが今回一番苦労した部分です。
公式ドキュメントはだいたいシングルチャンネルについて書かれているので、試行錯誤しました。
delegateは、agoraの動作、およびシングルチャンネルに関するdelegateのAgoraRtcEngineDelegate
と、マルチチャンネルの各チャンネルに対するメッセージを受け取る AgoraRtcChannelDelegate
に分かれています。
それぞれの役割について解説します。
AgoraRtcEngineDelegate
マルチチャンネルの場合、使用するのは以下の3つです
// agoraエンジンでWarningがあった場合
func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) {
print("warning: \(warningCode.rawValue)")
}
// agoraエンジンでerrorがあった場合
func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) {
print("error: \(errorCode.rawValue)")
}
大体の場合、UIで誰が発言したかのインジケーターを表示したいと思いますが、その場合はこちらを実装します。
agoraの初期化の際に enableAudioVolumeIndication
を設定しておいてください。
func rtcEngine(_ engine: AgoraRtcEngineKit, reportAudioVolumeIndicationOfSpeakers speakers: [AgoraRtcAudioVolumeInfo], totalVolume: Int) {
for speaker in speakers {
// speaker.uid の人が speaker.volume の音量で発言した旨の表示をここで行う
}
}
AgoraRtcChannelDelegate
マルチチャンネルの場合、ほとんどのメッセージはこちらのdelegateを使います。
Local user (自分) に関するメッセージ
// Local user(自分)がチャンネルにジョインした
func rtcChannelDidJoin(_ rtcChannel: AgoraRtcChannel, withUid uid: UInt, elapsed: Int) {
guard let channelId = rtcChannel.getId() else {
return
}
print("Joined: \(channelId) \(uid)")
}
// Local user(自分)が再びチャンネルにジョインした
func rtcChannelDidRejoin(_ rtcChannel: AgoraRtcChannel, withUid uid: UInt, elapsed: Int) {
guard let channelId = rtcChannel.getId() else {
return
}
print("ReJoined: \(channelId) \(uid)")
}
// Local user(自分)がチャンネルから離れた (呼ばれない?)
func rtcChannelDidLeave(_ rtcChannel: AgoraRtcChannel, with stats: AgoraChannelStats) {
print("DidLeave: \(rtcChannel.getId() ?? "") \(stats)")
}
// Local user(自分)がネットワークなどの原因で切断された
func rtcChannelDidLost(_ rtcChannel: AgoraRtcChannel) {
guard let channelId = rtcChannel.getId() else {
return
}
print("DidLost: \(channelId)")
}
Remote user (他人) に関するメッセージ
// Remote userにwarningが起きた
func rtcChannel(_ rtcChannel: AgoraRtcChannel, didOccurWarning warningCode: AgoraWarningCode) {
print("channel: \(rtcChannel.getId() ?? ""), warning: \(warningCode.rawValue)")
}
// Remote userにerrorが起きた
func rtcChannel(_ rtcChannel: AgoraRtcChannel, didOccurError errorCode: AgoraErrorCode) {
print("channel: \(rtcChannel.getId() ?? ""), Error: \(errorCode.rawValue)")
}
broadcaster
の人がジョインしたときはこちらが呼ばれます。 audience
の人がジョインした時にはメッセージは来ないようなので、agoraのReal-time Messaging SDKなどを利用して自前でやる必要があります。
// broadcaster の人がJoinした
func rtcChannel(_ rtcChannel: AgoraRtcChannel, didJoinedOfUid uid: UInt, elapsed: Int) {
guard let channelId = rtcChannel.getId() else {
return
}
print("didJoinedOfUid: \(channelId) \(uid)")
var error: AgoraErrorCode = .noError
if let info = agoraKit?.getUserInfo(byUid: uid, withError: &error) {
if error == .noError, let userAccount = info.userAccount {
// joinした時の処理をここで行う
}
}
}
// remote userの人がオフラインになった
func rtcChannel(_ rtcChannel: AgoraRtcChannel, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) {
guard let channelId = rtcChannel.getId() else {
return
}
print("didOfflineOfUid: \(channelId) \(uid) reason \(reason.rawValue)")
var error: AgoraErrorCode = .noError
if let info = agoraKit?.getUserInfo(byUid: uid, withError: &error) {
if error == .noError, let userAccount = info.userAccount {
//オフラインになったときの処理をここで行う
}
}
}
Remote userの人がマイクをミュートしたり解除したりした場合に呼ばれます。
Joinedが呼ばれずにいきなりこちらが呼ばれる場合もあるので、そちらも想定して実装しましょう。
func rtcChannel( _ rtcChannel: AgoraRtcChannel, remoteAudioStateChangedOfUid uid: UInt,
state: AgoraAudioRemoteState, reason: AgoraAudioRemoteStateReason, elapsed: Int
) {
guard let channelId = rtcChannel.getId() else {
return
}
print("remoteAudioStateChangedOfUid: \(channelId) \(uid) reason \(reason.rawValue)")
if state == .stopped || state == .starting {
let isMute = (state == .stopped)
// MuteのUI表示などの処理をここで行う。
// Joinedが呼ばれなかった人の処理もここで。
}
}
終了時の処理
チャンネルを終了する時の処理です
self.aliveChannel?.leave()
self.aliveChannel?.unpublish()
self.aliveChannel?.destroy()
人狼アプリについて
今回実装したアプリはこちらです。
要望が多ければAndroid版やWeb版にも実装していきたいです。