
Unity + Twilio で作る代替現実ゲームアプリ開発 - 電話発信と自動音声読み上げを数行で導入
はじめに
本記事では、 Unity と Twilio を組み合わせて、セキュリティドア風の代替現実ゲームアプリを作成します。このゲームでは、ユーザーが電話番号を入力すると、実際に入力した番号に電話がかかってきます。電話に出ると、自動音声によってパスコードが読み上げられ、そのパスコードをゲーム内で入力することでクリアとなります。まるでゲームの世界と現実のスマホがリンクしたような体験が得られます。
本記事では、実際に手を動かしながらゲームアプリを作っていきます。 Twilio を使うことで、一見複雑そうな 電話発信+自動音声読み上げの実装 がいかに簡単に導入できるか、この記事を通して体験してみてください。
Twilio とは
Twilio は、通話・ SMS ・チャットなどの通信機能を API として提供するクラウドプラットフォームです。従来であれば複雑な実装が必要だった通信の機能を、数行のコードで実装できるのが大きな特徴です。
対象読者
- Unity での基本的な開発経験がある方
- アプリケーションに電話・ SMS 機能を追加したい方
- 現実世界とバーチャルの世界を繋ぐゲームデザインに関心がある方
参考
実装
Twilio 側の準備
後ほど Unity で使用する Twilio の認証情報を取得します。Twilio Console で以下の情報を控えておきます。
- Account SID
- Auth Token
- 取得した電話番号
Unity 側の準備
プロジェクトの作成
- Unity Hub で新しいプロジェクトを作成
- テンプレートは「Universal 2D」を選択
- プロジェクト名は「TwilioVoiceDemo」など任意の名前
Twilio C# SDK のインストール
Twilio C# SDK を Unity プロジェクトにインストールします。今回は NuGetForUnity を使用します。
- NuGetForUnity のページで unitypackage をダウンロード
- Unity プロジェクトにインポート
- 上部メニューから「NuGet」→「Manage NuGet Packages」を選択
- 「Browse」タブで「Twilio」を検索
- 「Twilio」パッケージをインストール
Hierarchy にオブジェクトを追加
ゲームの UI を作成していきます。
メイン UI パネル
- Hierarchy で右クリック > UI > Canvas を作成
- Canvas の子として Empty GameObject を作成し「SecurityDoorPanel」と命名
- RectTransform を Stretch に設定し、 Left, Top, Right, Bottom をすべて 0 に設定
- Image コンポーネントを追加し、背景色を設定
電話番号・パスコード表示エリア
SecurityDoorPanel の子として以下を作成します。
- 「Your Phone Number」ボタン (phoneNumberModeButton)
- 電話番号表示用 TextMeshPro (phoneNumberDisplay)
- 「Passcode」ボタン (passcodeModeButton)
- パスコード表示用 TextMeshPro (passcodeDisplay)
ダイヤルパッド
- SecurityDoorPanel の子として Empty GameObject を作成し「DialPad」と命名
- Grid Layout Group コンポーネントを追加
- Cell Size: (80, 80)
- Spacing: (10, 10)
- Constraint: Fixed Column Count
- Constraint Count: 3
- 12 個のボタンを作成し、以下の順序で配置
1 2 3 4 5 6 7 8 9 C 0 E
Game Clear UI
- Canvas の子として Empry GameObject を作成し「GameClearPanel」と命名
- GameClearPanel の子として TextMeshPro を作成
- GameClearPanel オブジェクトを Disable 化
Manager オブジェクト
ゲームの動作を制御するための各 Object を作成します。以下の Empty GameObject を追加します。
- AudioManager
- GameManager
- TwilioManager
スクリプトの作成
ゲームの動作を制御するスクリプトを書いていきます。
TwilioManager - Twilio API との通信
TwilioManager.cs
using UnityEngine;
using Twilio;
using Twilio.Rest.Api.V2010.Account;
using Twilio.Types;
using System;
public class TwilioManager : MonoBehaviour
{
[Header("Twilio Settings")]
public string accountSid = "YOUR_ACCOUNT_SID";
public string authToken = "YOUR_AUTH_TOKEN";
public string twilioPhoneNumber = "YOUR_TWILIO_PHONE_NUMBER";
void Start()
{
// Twilio クライアントを初期化
TwilioClient.Init(accountSid, authToken);
}
public void MakeCallWithTwiML(string toPhoneNumber, string twimlContent, Action<bool, string> onComplete)
{
try
{
// TwiML を直接渡して通話を作成
var call = CallResource.Create(
twiml: new Twiml(twimlContent),
to: new PhoneNumber(toPhoneNumber),
from: new PhoneNumber(twilioPhoneNumber)
);
// 成功時のコールバック
onComplete?.Invoke(true, call.Sid);
}
catch (Exception e)
{
// 失敗時のコールバック
onComplete?.Invoke(false, e.Message);
}
}
}
GameManager - ゲームロジック管理
GameManager.cs
using UnityEngine;
using System;
public class GameManager : MonoBehaviour
{
private string generatedPasscode;
private bool isCallMade = false;
// UI に通知するためのイベント
public event Action<string> OnPasscodeGenerated;
public event Action<bool> OnPasscodeVerified;
public event Action<bool, string> OnCallCompleted;
public void InitiateCall(string phoneNumber, TwilioManager twilioManager)
{
if (isCallMade) return;
// 4桁のランダムパスコードを生成
generatedPasscode = GeneratePasscode();
// 音声読み上げ用の TwiML を作成
string twimlContent = CreatePasscodeTwiML(generatedPasscode);
// Twilio 経由で通話を開始
twilioManager.MakeCallWithTwiML(phoneNumber, twimlContent, (success, message) => {
if (success)
{
isCallMade = true;
OnPasscodeGenerated?.Invoke(generatedPasscode);
OnCallCompleted?.Invoke(true, "Call initiated successfully");
}
else
{
OnCallCompleted?.Invoke(false, message);
}
});
}
public bool VerifyPasscode(string inputPasscode)
{
if (!isCallMade) return false;
// 入力されたパスコードと生成されたパスコードを比較
bool isCorrect = inputPasscode == generatedPasscode;
OnPasscodeVerified?.Invoke(isCorrect);
return isCorrect;
}
private string GeneratePasscode()
{
System.Random random = new System.Random();
return random.Next(1000, 9999).ToString();
}
private string CreatePasscodeTwiML(string passcode)
{
// パスコードを1桁ずつ区切って読み上げやすくする
string spacedPasscode = string.Join(" ", passcode.ToCharArray());
// TwiML で日本語音声読み上げを定義
return $@"<?xml version=""1.0"" encoding=""UTF-8""?>
<Response>
<Say voice=""alice"" language=""ja-JP"">
セキュリティパスコードは {spacedPasscode} です。
もう一度繰り返します。{spacedPasscode} です。
</Say>
</Response>";
}
public void ResetGame()
{
generatedPasscode = "";
isCallMade = false;
}
public bool IsCallMade()
{
return isCallMade;
}
}
SecurityDoorController - UI の制御
SecurityDoorController.cs
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Collections;
public enum InputMode
{
PhoneNumber,
Passcode
}
public class SecurityDoorController : MonoBehaviour
{
[Header("Phone Number UI")]
public TextMeshProUGUI phoneNumberDisplay;
public Button phoneNumberModeButton;
[Header("Passcode UI")]
public TextMeshProUGUI passcodeDisplay;
public Button passcodeModeButton;
[Header("Dial Pad")]
public Button[] dialButtons; // 1-9, C, 0, E の順序
[Header("Game Clear UI")]
public GameObject gameClearPanel;
[Header("Dependencies")]
public TwilioManager twilioManager;
public GameManager gameManager;
public AudioManager audioManager;
[Header("UI Colors")]
public Color selectedModeColor = Color.green;
public Color unselectedModeColor = Color.gray;
private string phoneNumber = "+81";
private string passcode = "";
private InputMode currentInputMode = InputMode.PhoneNumber;
private string[] dialPadValues = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "C", "0", "E"};
void Start()
{
SetupDialPadButtons();
SetupModeButtons();
SetupGameManagerEvents();
UpdateDisplays();
UpdateModeSelection();
}
void SetupModeButtons()
{
phoneNumberModeButton.onClick.AddListener(() => SetInputMode(InputMode.PhoneNumber));
passcodeModeButton.onClick.AddListener(() => SetInputMode(InputMode.Passcode));
}
void SetupGameManagerEvents()
{
// GameManager のイベントを購読
gameManager.OnPasscodeGenerated += OnPasscodeGenerated;
gameManager.OnPasscodeVerified += OnPasscodeVerified;
gameManager.OnCallCompleted += OnCallCompleted;
}
void SetupDialPadButtons()
{
for (int i = 0; i < dialButtons.Length; i++)
{
int index = i;
dialButtons[i].onClick.AddListener(() => OnDialPadPressed(dialPadValues[index]));
}
}
void OnDialPadPressed(string value)
{
audioManager.PlayDialPadSound();
switch (value)
{
case "C": // クリアボタン
ClearCurrentInput();
break;
case "E": // 実行ボタン
ExecuteCurrentAction();
break;
default: // 数字ボタン
if (char.IsDigit(value[0]))
{
AddDigit(value);
}
break;
}
}
void SetInputMode(InputMode mode)
{
currentInputMode = mode;
UpdateModeSelection();
}
void UpdateModeSelection()
{
// 選択中のモードをボタンの色で表示
phoneNumberModeButton.GetComponent<Image>().color =
currentInputMode == InputMode.PhoneNumber ? selectedModeColor : unselectedModeColor;
passcodeModeButton.GetComponent<Image>().color =
currentInputMode == InputMode.Passcode ? selectedModeColor : unselectedModeColor;
}
void AddDigit(string digit)
{
switch (currentInputMode)
{
case InputMode.PhoneNumber:
if (phoneNumber.Length < 15)
{
phoneNumber += digit;
UpdateDisplays();
}
break;
case InputMode.Passcode:
if (passcode.Length < 4)
{
passcode += digit;
UpdateDisplays();
}
break;
}
}
void ClearCurrentInput()
{
switch (currentInputMode)
{
case InputMode.PhoneNumber:
phoneNumber = "+81";
break;
case InputMode.Passcode:
passcode = "";
break;
}
UpdateDisplays();
}
void ExecuteCurrentAction()
{
switch (currentInputMode)
{
case InputMode.PhoneNumber:
MakeCall(); // 通話開始
break;
case InputMode.Passcode:
VerifyPasscode(); // パスコード検証
break;
}
}
void UpdateDisplays()
{
phoneNumberDisplay.text = phoneNumber;
// パスコードは * で表示
string displayPasscode = "";
for (int i = 0; i < 4; i++)
{
if (i < passcode.Length)
displayPasscode += "*";
else
displayPasscode += "-";
}
passcodeDisplay.text = displayPasscode;
}
void MakeCall()
{
if (phoneNumber.Length > 3 && !gameManager.IsCallMade())
{
gameManager.InitiateCall(phoneNumber, twilioManager);
}
}
void VerifyPasscode()
{
if (passcode.Length == 4)
{
gameManager.VerifyPasscode(passcode);
}
}
// GameManager からのイベントハンドラー
void OnPasscodeGenerated(string generatedPasscode)
{
// 通話成功時、自動的にパスコード入力モードに切り替え
SetInputMode(InputMode.Passcode);
}
void OnCallCompleted(bool success, string message)
{
if (!success)
{
Debug.LogError($"Call failed: {message}");
}
}
void OnPasscodeVerified(bool isCorrect)
{
if (isCorrect)
{
// 正解時: 成功音とゲームクリア演出
audioManager.PlaySuccessSound();
GameClear();
}
else
{
// 不正解時: エラー音と画面点滅
audioManager.PlayErrorSound();
StartCoroutine(ShowIncorrectPasscodeEffect());
}
}
IEnumerator ShowIncorrectPasscodeEffect()
{
// パスコード表示を赤く点滅
Color originalColor = passcodeDisplay.color;
passcodeDisplay.color = Color.red;
yield return new WaitForSeconds(0.5f);
passcodeDisplay.color = originalColor;
passcode = "";
UpdateDisplays();
}
void GameClear()
{
audioManager.PlayDoorOpenSound();
StartCoroutine(SlideOutUI());
}
IEnumerator SlideOutUI()
{
// UI を左にスライドアウト
RectTransform panelRect = GetComponent<RectTransform>();
Vector3 startPos = panelRect.anchoredPosition;
Vector3 endPos = startPos + new Vector3(-Screen.width, 0, 0);
float elapsed = 0f;
float duration = 1.0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = elapsed / duration;
panelRect.anchoredPosition = Vector3.Lerp(startPos, endPos, t);
yield return null;
}
// ゲームクリア UI をフェードイン表示
if (gameClearPanel != null)
{
gameClearPanel.SetActive(true);
StartCoroutine(FadeInGameClear());
}
}
IEnumerator FadeInGameClear()
{
CanvasGroup canvasGroup = gameClearPanel.GetComponent<CanvasGroup>();
if (canvasGroup == null)
canvasGroup = gameClearPanel.AddComponent<CanvasGroup>();
canvasGroup.alpha = 0f;
float elapsed = 0f;
float duration = 0.5f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
canvasGroup.alpha = elapsed / duration;
yield return null;
}
canvasGroup.alpha = 1f;
}
void OnDestroy()
{
// イベントの購読解除
if (gameManager != null)
{
gameManager.OnPasscodeGenerated -= OnPasscodeGenerated;
gameManager.OnPasscodeVerified -= OnPasscodeVerified;
gameManager.OnCallCompleted -= OnCallCompleted;
}
}
}
AudioManager - 音声の制御
AudioManager.cs
using UnityEngine;
public class AudioManager : MonoBehaviour
{
[Header("Audio Clips")]
public AudioClip dialPadSound;
public AudioClip doorOpenSound;
public AudioClip successSound;
public AudioClip errorSound;
[Header("Audio Sources")]
public AudioSource sfxAudioSource;
void Start()
{
// AudioSource が設定されていない場合は自動で作成
if (sfxAudioSource == null)
{
GameObject sfxObject = new GameObject("SFX AudioSource");
sfxObject.transform.SetParent(transform);
sfxAudioSource = sfxObject.AddComponent<AudioSource>();
}
}
public void PlayDialPadSound()
{
if (dialPadSound != null && sfxAudioSource != null)
{
sfxAudioSource.PlayOneShot(dialPadSound);
}
}
public void PlayDoorOpenSound()
{
if (doorOpenSound != null && sfxAudioSource != null)
{
sfxAudioSource.PlayOneShot(doorOpenSound);
}
}
public void PlaySuccessSound()
{
if (successSound != null && sfxAudioSource != null)
{
sfxAudioSource.PlayOneShot(successSound);
}
}
public void PlayErrorSound()
{
if (errorSound != null && sfxAudioSource != null)
{
sfxAudioSource.PlayOneShot(errorSound);
}
}
}
動作確認
各スクリプトを、対応する GameObject にアタッチし、以下の設定を行います。
- TwilioManager のインスペクタで Twilio の認証情報を入力
- SecurityDoorController のインスペクタで UI 要素と Manager への参照を設定
- AudioManager のインスペクタの Audio Clips にサウンドをアタッチ
Unity エディタの Play ボタンを押してゲームが動作することを確認します。
- 電話番号を入力 (自分の携帯電話番号)
- E ボタンを押して自動音声を発信
- 電話に出てパスコードを聞く
- Passcode ボタンを押しパスコードを入力
- E ボタンで正しいパスコードを入力しゲームクリア
まとめ
この記事では、 Unity と Twilio を組み合わせたセキュリティドア風ゲームアプリを作成しました。電話がかかり、自動音声が読み上げられるという、複雑な通話機能がわずか数行のコードで実現できるのが、 Twilio の魅力です。本記事が、ARG (代替現実ゲーム) での演出など現実との通信を導入したいというユースケースにおいて参考になれば幸いです。