📖

Udonarium x Cloudflare R2 によるボードゲームのテストプレイ環境

に公開

はじめに

私はしばしばボードゲームのテストプレイをオンラインで実施しています。よく使われるツールにUdonariumがあると思いますが、各カードに1枚ずつ画像を貼り付けるのは非常に手間がかかります。
この記事では、Udonariumで使われているXMLファイルの構造を解析し、特定のディレクトリ下にある画像をまとめてUdonariumに取り込む方法を説明します。

TL;DR

  • Udonariumで使われているXMLファイルの構造を理解すると効率的にカードを作成できる
  • Cloudflare R2を用いた画像ディレクトリのホスティングで大量の画像を管理
  • Python スクリプトを使ってXMLを自動生成しUdonarium上でデッキを一括作成

XMLファイルの構造

XMLファイルは以下の構造で作成する必要があります。

<?xml version='1.0' encoding='utf-8'?>
<card-stack location.name="table" location.x="825" location.y="1200" poxZ="0" rotate="0" zindex="167" owner="" isShowTotal="true">
  <data name="card-stack">
    <data name="image">
      <data type="image" name="imageIdentifier"></data>
    </data>
    <data name="common">
      <data name="name">cards_stack</data>
    </data>
    <data name="detail"></data>
  </data>
  <node name="cardRoot">
    <card location.name="table" location.x="875" location.y="1525" poxZ="0" state="0" rotate="0" owner="" zindex="0">
      <data name="card">
        <data name="image">
          <data type="image" name="imageIdentifier">0</data>
          <data type="image" name="front">表面画像アドレス</data>
          <data type="image" name="back">裏面画像アドレス</data>
        </data>
        <data name="common">
          <data name="name">カード</data>
          <data name="size">2</data>
        </data>
        <data name="detail"></data>
      </data>
    </card>
    <card location.name="table" location.x="875" location.y="1525" poxZ="0" state="0" rotate="0" owner="" zindex="0">
      <data name="card">
        <data name="image">
          <data type="image" name="imageIdentifier">1</data>
          <data type="image" name="front">表面画像アドレス</data>
          <data type="image" name="back">裏面画像アドレス</data>
        </data>
        <data name="common">
          <data name="name">カード</data>
          <data name="size">2</data>
        </data>
        <data name="detail"></data>
      </data>
    </card>
  </node>
</card-stack>

ここで、各画像のアドレスはウェブ上にホスティングしておく必要があります。
実際のUdonariumで使用されているデータをダウンロードすると、アドレスの代わりにハッシュ値が入っていますが、このハッシュ値はUdonarium上で生成されるものであり、SHA256で同様のハッシュ化をしたローカル画像ファイル名で圧縮したzipファイルを作成しても動作しません。

画像のホスティング

カードのイメージをUdonariumで使用するためには、ウェブ上にホスティングする必要があります。ここでは、主要なホスティング方法を比較してみましょう。

ホスティング方法の比較

ホスティング方法 無料枠 設定の手軽さ 一括アップロード URL取得の容易さ 長期安定性
Dropbox 2GB ★★★★★ ★★★★ ★★ ★★★
Google Drive 15GB ★★★★ ★★★★ ★★ ★★★
GitHub 1GB/リポジトリ ★★★ ★★★ ★★★ ★★★★
Cloudflare R2 10GB ★★★ ★★★★★ ★★★★★ ★★★★★
自前サーバー - ★★★★★ ★★★★★ ★★★★★

おすすめは、Cloudflare R2へのホスティングです。無料で10GBまで画像をホストでき、カスタムドメインも設定可能です[1]

Cloudflare R2とは

Cloudflare R2は、Cloudflareが提供するオブジェクトストレージサービスです。Amazon S3と互換性があり、以下のような特徴があります:

  • 出力帯域の料金がない(データ転送料金なし)
  • 保存容量は10GBまで無料
  • 高速なグローバルCDNによる配信
  • APIリクエストは月100万回まで無料
  • Workers KVと連携可能
  • カスタムドメインの設定が可能

Cloudflare R2の設定方法

  1. Cloudflareにアカウント登録する
  2. 左メニューからR2を選択し、サービスを有効化
  3. バケットを作成(名前は英数字と-(ハイフン)のみ使用可能)
  4. 「公開アクセス」の設定をONにする
  5. (オプション) Workers経由でカスタムドメインを設定

CLIで画像を一括アップロードする方法

Cloudflare R2にフォルダごと画像を一括アップロードするには、rcloneを利用するのが効率的です。rcloneはさまざまなクラウドストレージサービスとの連携に対応した強力なコマンドラインツールです。

1. rcloneのインストール

まず、rcloneをインストールします:

# macOSの場合
brew install rclone

# Windowsの場合
winget install -e --id Rclone.Rclone

# Ubuntuの場合
sudo apt install rclone

2. rcloneの設定

rcloneでCloudflare R2を使用するには、設定ファイルを作成する必要があります:

# 設定ウィザードを起動
rclone config

以下の手順で進めてください:

  1. nを入力して新しいリモート接続を作成
  2. リモート名を入力(例:r2
  3. ストレージタイプとしてs3を選択
  4. プロバイダーとしてCloudflareを選択
  5. アクセスキーIDとシークレットアクセスキーを入力(Cloudflare R2のAPIトークンから取得)
  6. 他の設定はデフォルトのままでOK
  7. 設定を確認してyを入力

あるいは、直接設定ファイルを編集することもできます。通常、設定ファイルは以下の場所にあります:

  • Linux/macOS: ~/.config/rclone/rclone.conf
  • Windows: %USERPROFILE%\.config\rclone\rclone.conf

次のような内容を追加します:

[r2]
type = s3
provider = Cloudflare
access_key_id = あなたのアクセスキーID
secret_access_key = あなたのシークレットアクセスキー
endpoint = https://あなたのアカウントID.r2.cloudflarestorage.com
acl = private

3. 画像を一括アップロード

設定が完了したら、rclone copyコマンドを使ってローカルフォルダの画像を一括アップロードできます:

# シンプルな例
rclone copy ./cards r2:your-bucket-name

# 詳細な進捗を表示
rclone copy ./cards r2:your-bucket-name --progress

# 公開アクセス権を設定
rclone copy ./cards r2:your-bucket-name --acl public-read

./cardsはローカルの画像フォルダ、r2:your-bucket-nameはCloudflare R2のバケット名です。

4. 便利なオプション

rcloneにはさまざまな便利なオプションがあります:

  • --include "*.{jpg,png}" - 特定の拡張子のファイルのみをアップロード
  • --exclude "*.tmp" - 特定のファイルを除外
  • --checksum - チェックサムでファイルが同一か確認(ファイルサイズと更新日時だけでなく)
  • --dry-run - 実際にはアップロードせずに何が行われるかを確認
  • --transfers=N - 並列アップロード数を指定(デフォルトは4)

例えば:

# JPGとPNGファイルのみをアップロード、8並列で高速化
rclone copy ./cards r2:your-bucket-name --include "*.{jpg,png}" --transfers=8 --progress

これにより、画像フォルダ内のすべてのJPGとPNG画像がCloudflare R2バケットに一括アップロードされます。rclone copyはファイルをそのままコピーするので、アップロード後のURLは以下のようになります:

https://your-bucket-name.your-subdomain.r2.cloudflarestorage.com/ファイル名

XML自動作成スクリプト

以下のPythonスクリプトを使用すると、特定ディレクトリ内の画像ファイルからUdonarium用XMLを自動生成できます:

import glob
import os
import xml.etree.ElementTree as ET
from xml.dom import minidom

# 設定
CARD_DIR = "./cards"  # カード画像のディレクトリ
BACK_IMAGE = "https://yourbackend.com/card-back.jpg"  # カード裏面の画像URL
R2_BASE_URL = "https://your-bucket.your-subdomain.workers.dev/"  # CloudflareのR2 URL

def create_card_xml():
    # ルート要素の作成
    card_stack = ET.Element("card-stack", {
        "location.name": "table",
        "location.x": "825",
        "location.y": "1200",
        "poxZ": "0",
        "rotate": "0",
        "zindex": "167",
        "owner": "",
        "isShowTotal": "true"
    })
    
    # メタデータ部分
    data_card_stack = ET.SubElement(card_stack, "data", {"name": "card-stack"})
    data_image = ET.SubElement(data_card_stack, "data", {"name": "image"})
    ET.SubElement(data_image, "data", {"type": "image", "name": "imageIdentifier"})
    data_common = ET.SubElement(data_card_stack, "data", {"name": "common"})
    ET.SubElement(data_common, "data", {"name": "name"}).text = "cards_stack"
    ET.SubElement(data_card_stack, "data", {"name": "detail"})
    
    # カードのルートノード
    card_root = ET.SubElement(card_stack, "node", {"name": "cardRoot"})
    
    # 画像ファイルの取得
    card_images = glob.glob(os.path.join(CARD_DIR, "*.jpg")) + \
                  glob.glob(os.path.join(CARD_DIR, "*.png"))
    
    # 各カードの作成
    for i, img_path in enumerate(card_images):
        filename = os.path.basename(img_path)
        front_url = R2_BASE_URL + filename
        
        card = ET.SubElement(card_root, "card", {
            "location.name": "table",
            "location.x": "875",
            "location.y": "1525", 
            "poxZ": "0",
            "state": "0",
            "rotate": "0",
            "owner": "",
            "zindex": "0"
        })
        
        data_card = ET.SubElement(card, "data", {"name": "card"})
        data_image = ET.SubElement(data_card, "data", {"name": "image"})
        ET.SubElement(data_image, "data", {"type": "image", "name": "imageIdentifier"}).text = str(i)
        ET.SubElement(data_image, "data", {"type": "image", "name": "front"}).text = front_url
        ET.SubElement(data_image, "data", {"type": "image", "name": "back"}).text = BACK_IMAGE
        
        data_common = ET.SubElement(data_card, "data", {"name": "common"})
        ET.SubElement(data_common, "data", {"name": "name"}).text = "カード" + str(i+1)
        ET.SubElement(data_common, "data", {"name": "size"}).text = "2"
        ET.SubElement(data_card, "data", {"name": "detail"})
    
    # XMLの整形と出力
    rough_string = ET.tostring(card_stack, 'utf-8')
    reparsed = minidom.parseString(rough_string)
    pretty_xml = reparsed.toprettyxml(indent="  ")
    
    with open('udonarium_cards.xml', 'w', encoding='utf-8') as f:
        f.write(pretty_xml)
    
    print(f"合計{len(card_images)}枚のカードを含むXMLを生成しました。")

if __name__ == "__main__":
    create_card_xml()

使用方法

  1. 上記のスクリプトをPythonファイル(例:create_cards.py)として保存
  2. CARD_DIRBACK_IMAGER2_BASE_URLの変数を自分の環境に合わせて修正
  3. スクリプトを実行してudonarium_cards.xmlを生成
  4. 生成されたXMLファイルをUdonariumにインポート

まとめ

Udonariumでカードデッキを一括作成するには、XMLファイルの構造を理解し、画像をWeb上にホスティングすることが重要です。Cloudflare R2を使うことで、無料で大量の画像を効率的に管理できます。今回紹介したPythonスクリプトを使えば、数百枚のカードも数分で一括取り込み可能になります。

また、このスクリプトは拡張性が高いため、カードごとの個別設定(名前や属性など)を追加したい場合は、CSVファイルからメタデータを読み込むなどの機能を追加することも検討してみてください。

脚注
  1. Cloudflare R2の料金プラン ↩︎

Discussion