【Unity×NFCカード】で脱出ゲームを作る

# はじめに

[総コンAdventCalender2024](https://adventar.org/calendars/10574) の6日目の記事です！

この記事では，
1.Unityを用いた **NFCカード** を読み取る方法
2.Untiyを用いた **交通系ICカード** を読み取る方法
3.脱出ゲームの制作過程
を記述します．

この記事で用いた方法の利点として，PCSC-Sharpを用いるため，
Windows, MacOSX, Linux(ubuntu)のOSで動作します．

## 制作した脱出ゲームの概要とGitHubリポジトリ
PCとNFCカードリーダーを2台ずつ用意して，
"もの"を読み込んで謎解きをする脱出ゲームを制作しました．
2チームに分かれて協力しながら進めていきます．

<iframe width="560" height="315" src="https://www.youtube.com/embed/MNnho2pWu8U?si=ib_u9ndw1Jsw6KxX" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

制作したUnityファイルのソースコードは以下のGitHubリポジトリに置いておきます．
[AmanatsuTouko/EscapeGameUsingUDPAndNFC - GitHub](https://github.com/AmanatsuTouko/EscapeGameUsingUDPAndNFC)

## 対象読者
- NFCカードを読み込んで，固有ID(UID/IDm)を取得したい人
- Unity(.Net)に組み込んで実装したい人

## 実行環境と使用したライブラリ
- Unity 2022.3.11f1
- Visual Studio 2022
- [PCSC-Sharp](https://github.com/danm-de/pcsc-sharp)
- Windows10 / MaxOS Sonoma 14.6 / ubuntu 20.04

## 物理的に準備するもの
- [NFCカードリーダー](https://www.amazon.co.jp/dp/B09K48NHPY)
- [NFCカード](https://www.amazon.co.jp/dp/B08WRV4NHM)
- 交通系ICカード(Suicaなど)
- （* NFCコインタグ）
- （* 脱出ゲームに使う小物）
(*) は任意です．NFCの読み取りだけであれば必要ありません．
(リンク先は実際に使用したamazonの商品リンクです．)

# NFCについての基礎知識
<details><summary>NFCについての基礎知識（クリックして展開）</summary>

まず，NFCカードを読み取る前に必要な予備知識を簡単に記述します．

NFC(Near Field Communication)とは 近距離無線通信 を意味します．
例を挙げると，交通系ICやクレジットカード、スマートフォン端末などに搭載されています．（社員証や，アーケードゲームのデータ保存用カード，amiiboなどにも使われています．）

## 通信の規格
主に4種類あり，以下の特徴を持ちます．
- ISO14443 Type-A：比較的安価
- FeliCa Type-F ：交通系ICで使用．セキュリティ面で優れ，処理速度が早い
- ISO14443 Type-B：セキュリティ面で優れる
- ISO15693 ：メモリ少なめだが安価．通信距離が近傍型(約10～70cm の交信距離)

（参考：[NFCとは?-NFC基礎知識|トッパンインフォメディア](https://www.toppan-im.co.jp/ic/nfc/)）

## 通信の仕組み
NFCカードの構成要素は，ICチップとアンテナです．
通信の流れは以下になるようです．
- 1.ICカードをかざすことで、磁界による誘導電流が発生
  - これを起電力とすることでICチップを起動させる
- 2.相互認証ののち、データの読み書きを行う
  - 2-1.リーダーから電磁波を送信
  - 2-2.ライターのアンテナで電磁波を受信、電磁波を返す
  - 2-3.リーダーで受信する

相互に認証を行った後に通信を行うことだけ覚えておきましょう．

</details>
（記事の本題とそこまで関連がないので折りたたんであります）

# UnityでNFCカードを読み取る方法の概要
今回は以下の方法で読み取っていきます．

- 1.PCSC-Sharpをダウンロード
    PCSC-Sharpは，PCSCライブラリ[*1]をC#(.NET)向けにラッパーしたライブラリ
    WindowsとUnixの両方で実行できる[*2]

- 2.PCSC-Sharpを **dllファイル** にする

- 3.**Unityのプラグイン** として読み込む

- 4.NFCカードリーダーへの通信，NFCカードから情報を取得するコードを書く

[*1] PCSC(Personal Computer / Smart Card) : ICカードをWindows環境上で相互利用できるようにするためのインターフェース仕様
[*2] Windows向けにwinscard.dll，Unix向けにPC/SC liteをラップしている

## 1.PCSC-Sharpをダウンロード
[PCSC-Sharp - GitHub](https://github.com/danm-de/pcsc-sharp)
こちらのリンク先からPCSC-Sharpをダウンロードしてください．
自分がやった際は，[version 6.2.0](https://github.com/danm-de/pcsc-sharp/releases/tag/RELEASE_6_2_0) にて行いましたが，最新版で問題ないと思われます．
[Releasesのtag一覧](https://github.com/danm-de/pcsc-sharp/tags)から，最新版をクリックしてソースコードをzipでダウンロードして展開します．

![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865650/ea52f8ca-30a1-aadc-2b12-798cc143df18.png)


## 2.PCSC-SharpをDLLにする

### ターゲットフレームワーク(.Net Standard2.0)の指定

Windowsでの手順を示します．

Visual Studio 2022などのVSを開き，プロジェクト/ソリューションを開くを選択して，pcsc-sharp.sln を開きます．

次に，ビルド対象のターゲットフレームワークのバージョンの指定をします．
ソリューションエクスプローラーからPCSCを右クリックして，プロパティを開きます．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865650/56e64207-af91-c7a9-9b17-7988cb2024df.png)

（参考：[対象となる .NET Framework を指定する - Visual Studio (Windows) | Microsoft Learn](https://learn.microsoft.com/ja-jp/visualstudio/ide/visual-studio-multi-targeting-overview?view=vs-2022)）

今回は .Net Standard2.0 向けのビルドを行いたいので，
ターゲットフレームワークの欄に netstandard2.0 が入っていれば問題ありません．

![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865650/ae76c1f5-c157-1cf2-eb37-9b97e0be1ea0.png)

:::note warn
注意
使用するUnityがどの.Net Frameworkに対応しているかを確認する必要があります．
Unity2022以外を用いる場合は，.Net Standard2.0に対応していない可能性があります．
:::

ちなみに，大体のバージョンは，.Net Standard 2.1 に対応しているようです．
（参考：[Unity - Manual: .NET profile support](https://docs.unity3d.com/6000.0/Documentation/Manual/dotnet-profile-support.html)）

### ビルドを行う

画面上部の ビルド -> ソリューションのビルド を選択してビルドを行います．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865650/3c8d9cce-57af-ea58-6d04-c0f6f672c45a.png)

参考記事
- [Visual Studio2022でDLLファイルを作成する手順｜Mizeee](https://note.com/mizeee7956/n/n33dd4ad39b88)
- [Unity で使えるライブラリのソリューションとプロジェクト（とテスト）を dotnet CLI で生成する](https://zenn.dev/agate_pris/scraps/76d3494ccecdbb)
- [【Unity】.NetFrameWorkのバージョン変更 - たまごネコは夢を見る](https://ashutaru.hatenablog.com/entry/2019/03/02/132424)

## 3.DLLをUnityにプラグインとして読み込む

### Untiyの.NetStandardのバージョンの指定

書き出した dll の .net standard のバージョンに合わせて Unity側で .net standard のバージョン指定をする必要があります．

File -> Build Settings -> Player Settings -> Playerの項目内にある Other Settingsの欄から
Configuration -> Api Compatibility Level が .Net Standard 2.1 であることを確認します．

![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865650/6234c789-6dd1-920d-99bb-dc5e036b7983.png)

参考：[【Unity】.NetFrameWorkのバージョン変更 - たまごネコは夢を見る](https://ashutaru.hatenablog.com/entry/2019/03/02/132424)

### dllファイルの読み込み

Assets直下に"Plugins"というフォルダを作成します．
先ほど作成したdllファイルは，
```
pcsc-sharp-RELEASE_6_2_0\src\PCSC\obj\Debug\netstandard2.0\PCSC.dll
```
のパスにビルドされています．
このファイルを Plugins フォルダにドラッグ&ドロップでコピーします．

![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865650/09640d80-c8b9-c054-3dd8-4361e9d7b91c.png)

適当なスクリプトを作成し，using PCSC; と記述してimport Errorが出なければdllをUnityから使うことができる準備が整いました．

同様に，
PCSC.Iso7816.dll と PCSC.Reactive.dll も Pluginsに入れておきましょう．
2つとも以下のパスに生成されています．
```
pcsc-sharp-RELEASE_6_2_0\src\PCSC.Iso7816\obj\Debug\netstandard2.0\PCSC.Iso7816.dll
pcsc-sharp-RELEASE_6_2_0\src\PCSC.Reactive\obj\Debug\netstandard2.0\PCSC.Reactive.dll
```

## 4.NFCカードとの通信を行うコード

### 4-1.NFCカードの固有IDを取得しよう

#### UIDの取得方法の流れ
- 1.ISO7816規格の APDU(Application Protocol Data Unit)コマンド を用いて
カードリーダーからNFCカードへ情報を送信
- 2.NFCカードがデータに応じて情報を返却
- 3.返却されたレスポンスデータのBit列を文字列に戻すことでUIDが取得できる

#### APDUコマンドについて

APDUコマンドは，カードリーダーが送る場合と，NFCカードが送る場合とで2種類のタイプを使います．それぞれ見ていきましょう．

**Command APDU** (NFCカードリーダーが送る情報)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865650/8ba3534f-c9b5-d0e1-75e5-f805f52be1bd.png)

ここで，図の用語の意味はこちらになります．
- CLA: Class of Instruction(命令クラス)
- INS: Instruction Code(命令コード)
- P1 / P2 : Parameter(引数)
- Lc : Length of data field(データの長さ)
- Data Field : Cardへ送るデータ
- Le : 期待する返却データの長さ

**Response APDU** (NFCカードが返却する情報)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865650/d08c04ee-9bfd-5c27-7dbc-db4264014760.png)

SWとは，Status Wordを意味します．
SW1とSW2を合わせて正常にデータの受信ができたかなどの状態を表します．
例：SW1 = 0x90, SW2 = 0x00 で正常に操作が完了したことを意味します．

Status word の一覧はこちらのサイトでまとめてくれていました．
[APDUコマンドの返り値をメッセージで表示する | 晴耕雨読](https://tex2e.github.io/blog/protocol/apdu-return-status-msg)

（参考：[APDU Application Protocol Data Unit プロトコル - Smart Card Guy](https://smartcardguy.hatenablog.jp/entry/2018/08/11/153334)）

#### UIDを取得するコード
実際に，UIDを取得するコードを記述していきましょう．

以下をCommand APDUに指定して送信します．
- CLA(命令クラス) : 0xFF
- INS(命令コード) : Instruction.GetData
- P1/P2(引数) : 0x00
- Le(期待されるレスポンスの長さ) : 0

StatusWordに0x9000(正常)が返ってきたときにバイト列から戻すことでUIDを取得できます．

<details><summary>UIDを取得するソースコード(一部)</summary>

[GitHubのソースコード全文のリンク](https://github.com/AmanatsuTouko/EscapeGameUsingUDPAndNFC/blob/main/Assets/Scripts/NFCReader.cs#L169)

以下は，処理のコアとなる部分を切り出しています，
実際に用いる場合は，GitHubのソースコードを参考にしてください．

```GetUID.cs
ICardReader reader = context.ConnectReader(mainReaderName, SCardShareMode.Shared, SCardProtocol.Any);

// ISO7816のAPDUコマンドを使用してカードからデータを読み取る
var apdu = new CommandApdu(IsoCase.Case2Short, reader.Protocol) {
    CLA = 0xFF,
    Instruction = InstructionCode.GetData,
    P1 = 0x00,
    P2 = 0x00,
    Le = 0 // We don't know the ID tag size
};

using (reader.Transaction(SCardReaderDisposition.Leave)) {
    Debug.Log("Retrieving the UID .... ");

    var sendPci = SCardPCI.GetPci(reader.Protocol);
    var receivePci = new SCardPCI(); // IO returned protocol control information.

    var receiveBuffer = new byte[256];
    var command = apdu.ToArray();

    var bytesReceived = reader.Transmit(
        sendPci,             // Protocol Control Information (T0, T1 or Raw)
        command,             // command APDU
        command.Length,
        receivePci,          // returning Protocol Control Information
        receiveBuffer,
        receiveBuffer.Length // data buffer
    );

    var responseApdu = new ResponseApdu(receiveBuffer, bytesReceived, IsoCase.Case2Short, reader.Protocol);
    Debug.Log(
        string.Format("SW1: {0:X2}, SW2: {1:X2}\nUid: {2}",
        responseApdu.SW1,
        responseApdu.SW2,
        responseApdu.HasData ? BitConverter.ToString(responseApdu.GetData()) : "No uid received")
    );
    
    // UUIDを返却する
    // 操作が正常に完了した かつ データを持っているとき
    if(responseApdu.HasData && responseApdu.SW1 == 0x90 && responseApdu.SW2 == 0x00)
    {
        return BitConverter.ToString(responseApdu.GetData());
    }
    else
    {
        Debug.LogError("NFCカードのデータの読み取り操作が正常に完了しませんでした．");
        return "";
    }
}
```

</details>

#### 動かしてみた結果
<iframe width="560" height="315" src="https://www.youtube.com/embed/U7R3zRfvK1Q?si=NSGok1sDknPaMgdh" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

### 4-2.交通系ICかどうかを判別して残高を読み出す

次に，読み込んだカードが交通系ICかどうかを判別します．
UIDを読み取った時と同様にAPDUコマンドを用いて判定を行います．

交通系ICかどうかの判定と残高の読み出しには，**SelectFile** と **ReadBinary** という2つのAPDUコマンドを用います．SelectFileモードでは，どのアプリケーションのデータを読み出すかをSelectFileコマンドで指定し，どの領域のデータを読み出すかをReadBinaryコマンドで指定します．SuicaなどのFelicaでは，1枚のカードに複数のサービスを記憶させることができるので，そのための構造だと思われます．

交通系ICかどうかを判別して，残高を読み出すコードは以下になります．
[NFCReader.cs](https://github.com/AmanatsuTouko/EscapeGameUsingUDPAndNFC/blob/main/Assets/Scripts/NFCReader.cs#L227)
以下に解説を記述していきます．

#### 交通系ICかどうかを判別する

以下のSelectFileコマンドを、APDUコマンドとして送信し，
（Application Protocolを選択してどのファイルを読み込むかを選択する）
SW1に0x90，SW2に0x00の正常ステータスのレスポンスが返ってきたら交通系ICと判断できます．

![スクリーンショット 2024-12-05 17.23.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865650/b94d94d7-be20-ea6d-0a47-9989bc328c47.png)

APDUコマンドの意味は以下です．
- INS(命令コード)：0X04 SelectFileモード
- P1：0x00 エリア数を指定
- P2：0x01 サービス数を指定

#### 交通系ICの残高を読み出す

以下のReadBinaryコマンドを，APDUコマンドとして送信します．

![スクリーンショット 2024-12-05 17.26.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865650/f9c1bbfa-76ea-b9e2-7628-18eb85ec3181.png)

ここで，APDUコマンドの意味は以下です．
- INS(命令コード)：0XB0 ReadBinatyモード
- P1：0x00 読み出すBlock開始位置の指定
- P2：0x00 Blockリストの指定 Block No.の指定

残高データは，受信したレスポンスのByte列の 10-11バイト目にあるので，整数値にパースすることで残高が取得できます．

ここで，P2に0x01を指定すると，履歴の2件目が取得できます．
[最大で100件目まで取得できる](https://www.jreast.co.jp/mobilesuica/use/sf/chk_account.html)ようです．

ちなみに，受信したレスポンスの残高データは2Byte(16bit)なので，設計上は0円〜65,535円まで保存できそうです．（仕様上は20,000円まで）また，履歴についても，P2を0x00~0xFFまで指定できるなら，設計上は255件まで保存できそうですね．

#### 動かしてみた結果

<iframe width="403" height="716" src="https://www.youtube.com/embed/xsnIDZiwjaA" title="交通系ICカードかどうかを判別する" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

# 作った脱出ゲームの概要
これ以降の記事の内容は，実際に作ったシステムと作ってみた感想になります．

## ゲーム内容
脱出ゲームの企画と謎の制作は，友人の tofu くんにやってもらいました．
PCとNFCカードリーダーを2台ずつ用意して，ものを読み込んで謎解きをする脱出ゲームです．2チームに分かれて協力しながら進めていきます．

![GameOverview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865650/7e2f98e1-e758-94ed-7d3d-6923d532579a.png)

## 用いたもの
[上で書いた準備するもの](#準備するもの)に加えて，以下のものを脱出ゲーム用に用いました．
- [NFCコインタグ](https://www.amazon.co.jp/dp/B0C5WFZ1TD)
- Wi-Fiルーター(スマホテザリングでも可)
- 謎解きに用いる小物（NFCコインタグを貼り付ける）

## 実装上の工夫点

### ScriptableObjectでNFCカードのUIDを管理
ScriptableObjectを用いてカードの種類とUIDの管理をしていました．

### UDP通信で2台のPCの通信処理を記述
通信は，同じローカルネットワークに接続して指定したプライベートIDに向けて
"どのUIDのカードを読み込んだか"を送受信しあっていました．

[UDP送信を行うためのコード](https://github.com/AmanatsuTouko/EscapeGameUsingUDPAndNFC/blob/main/Assets/Scripts/UdpSender.cs)，[UDP受信を待機するためのコード](https://github.com/AmanatsuTouko/EscapeGameUsingUDPAndNFC/blob/main/Assets/Scripts/UdpReciever.cs)

また，Remote Procedure Call(RPC, 別のPCで任意の処理を呼び出す)を実装する必要があったので，簡易的なものを実装しました．

送信クライアント
- 実行したい関数名と引数をjsonにまとめて、バイト列にして送信

受信クライアント
- 受信したjsonをデシリアライズして、静的な関数を実行する

という処理を行ってRPCを実現しました．

[RPCのコード](https://github.com/AmanatsuTouko/EscapeGameUsingUDPAndNFC/blob/main/Assets/Scripts/Invoker.cs)

# おわりに

ハードを用いた脱出ゲームを作ってみましたが，iso規格やらの知らない知識が多く非常に苦労しましたが面白くもありました．皆さんもぜひ，NFCを用いた面白そうなゲームの作成をしてみてください！

# 参考文献

https://www.toppan-im.co.jp/ic/nfc/

https://ja.komoju.com/blog/nfc/

https://www.sony.co.jp/Products/felica/about/scheme.html

https://www.hummingheads.co.jp/reports/feature/1306/130603_01.html

https://atmarkit.itmedia.co.jp/ait/articles/1303/25/news054.html

https://tex2e.github.io/blog/protocol/jpki-mynumbercard-with-apdu

https://smartcardguy.hatenablog.jp/entry/2018/08/11/153334