😎

超入門!Rustで作るアセット管理ツール~アセット命名規則チェッカー~

2024/12/23に公開

はじめに

ゲーム開発において、アセット管理は地味ながら重要な業務の一つです。プロジェクトが大きくなるにつれ、手作業での管理は困難になり、ミスのリスクも高まります。

アセットとは、ゲームで使用される画像、音声、3Dモデルなどのデータファイルの総称です。例えば、キャラクターの画像、背景音楽、効果音、ステージの3Dモデルなどが該当します。

本記事では、Rustを使用してアセット名の命名規則管理を自動化するツールの作成方法を解説します。

想定読者

  • ゲーム開発の効率化に興味がある開発者
  • Rustに興味ある方
  • 自動化ツールの作成経験が少ない方

なぜRustなのか

アセット管理ツールの開発にRustを選択する理由を簡単に触れておきます。
記事の内容がぶれないようにあえて具体的な処理の話など、専門的な観点を除いた点で紹介します。

💡 他の言語との比較

  • Python:書きやすいが実行速度が遅い
  • JavaScript:手軽だがメモリ管理が甘い
  • C++:高速だが安全性に課題。学習コストは高め
  • Rust:高速で安全、ただし学習コストは高め
  1. 高いパフォーマンス

    • 大量のアセットを効率的に処理
      例:数千枚の画像を数秒で処理
    • メモリ使用の最適化
      例:不要なメモリ確保を避け、リソースを効率的に使用
    • 並列処理の容易な実装
      例:複数のCPUコアを使って同時に処理
  2. クロスプラットフォーム

    • シンプルなバイナリ配布
      • 依存ライブラリが静的リンク
      • Pythonのような実行環境不要
    • 高い移植性
      • Windows/Mac/Linuxで同じコードが動作
      • 環境差異を抽象化するAPIを提供
  3. 将来性と実績

    • 大手企業での採用実績
      • Discord:全サーバーのメモリ使用量を50%削減
      • Dropbox:同期エンジンのパフォーマンス改善
    • 活発なコミュニティ
      • 7年連続でStack Overflowで最も愛されている言語
      • 月間1億以上のクレートダウンロード

アセット管理の課題

ゲーム開発におけるアセット管理では、主に次のような課題があります。

  1. 命名規則の統一

    • 複数のアーティストやデザイナーが作成するアセット
      例:player_character.png vs PlayerCharacter.png vs player-character.png
    • プラットフォームごとの命名制限
      例:Windowsでは特定の文字が使用できない
    • 大文字小文字の揺れ
      例:Background vs background
  2. フォーマット変換

    • 作業用フォーマットからゲーム用フォーマットへの変換
      例:PhotoshopのPSDファイルからPNGへの変換
    • プラットフォームごとの最適化
      例:モバイル向けの圧縮設定
    • メタデータの付与
      例:画像サイズ、作成日時、作者情報の管理
  3. バージョン管理

    • アセットの更新履歴の追跡
      例:いつ、誰が、どのように変更したか
    • 変更の影響範囲の特定
      例:テクスチャの変更がどのモデルに影響するか
    • バックアップの管理
      例:古いバージョンの保存と復元

アセット命名規則チェッカーを作成

今回は、ファイル名の命名規則と拡張子を検証するツールを動作させてみます。

開発環境の準備

Rustの環境構築から順を追って説明します。

  1. Rustのインストール
  • Windows の場合
    https://rustup.rs/ からRustupインストーラーをダウンロードして実行

  • macOS / Linux の場合

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  1. 新しいプロジェクトの作成
# 新しいディレクトリを作成して移動
mkdir file_checker
cd file_checker

# Cargoプロジェクトを初期化
cargo init
  1. 必要な依存関係の追加
    コードで使用しているwalkdirregexクレートをCargo.tomlに追加する必要があります。
    Cargo.tomlを以下のように編集してください:
[package]
name = "file_checker"
version = "0.1.0"
edition = "2021"

[dependencies]
walkdir = "2.3"
regex = "1.5"
  1. コードの実装
    下記コードをsrc/main.rsに保存します
use regex::Regex;
use std::path::Path;
use walkdir::WalkDir;

struct AssetChecker {
    name_pattern: Regex,
    allowed_extensions: Vec<String>,
}

impl AssetChecker {
    fn new(pattern: &str, extensions: Vec<String>) -> Self {
        AssetChecker {
            name_pattern: Regex::new(pattern).unwrap(),
            allowed_extensions: extensions,
        }
    }

    fn check_file(&self, path: &Path) -> Result<(), String> {
        if path.file_name().is_some() {
            // ファイル名から拡張子を除いた部分を取得
            let name_without_ext = path
                .file_stem()
                .map(|s| s.to_string_lossy())
                .unwrap_or_default();

            // 拡張子を除いたファイル名でパターンマッチング
            if !self.name_pattern.is_match(&name_without_ext) {
                return Err(format!(
                    "Invalid file name: {} (without extension)",
                    name_without_ext
                ));
            }

            // 拡張子のチェック
            if let Some(ext) = path.extension() {
                let ext_str = ext.to_string_lossy().to_lowercase();
                if !self.allowed_extensions.contains(&ext_str.to_string()) {
                    return Err(format!("Invalid extension: {}", ext_str));
                }
            }
        }
        Ok(())
    }
}
fn main() {
    // チェッカーの初期化(例:小文字とアンダースコアのみを許可)
    let checker = AssetChecker::new(
        r"^[a-z][a-z0-9_]*$",
        vec!["png".to_string(), "jpg".to_string()],
    );

    // 指定ディレクトリ以下のファイルを再帰的にチェック
    for entry in WalkDir::new("./checktarget")
        .into_iter()
        .filter_map(|e| e.ok())
    {
        let path = entry.path();
        if path.is_file() {
            match checker.check_file(path) {
                Ok(()) => println!("OK: {}", path.display()),
                Err(e) => println!("Error: {} for file {}", e, path.display()),
            }
        }
    }
}

  1. プログラムの実行
# プロジェクトディレクトリで実行
cargo run

コードの詳細な解説

使用クレート(外部ライブラリ)の説明

use regex::Regex;         // 正規表現を扱うためのクレート
use std::path::Path;      // ファイルパスを扱うための標準ライブラリ
use walkdir::WalkDir;     // ディレクトリを再帰的に走査するためのクレート

構造体の定義

struct AssetChecker {
    name_pattern: Regex,             // ファイル名のパターンを保持
    allowed_extensions: Vec<String>, // 許可する拡張子のリストを保持
}
  • Regex: 正規表現パターンを保持する型
  • Vec<String>: 文字列のベクター(可変長配列)

構造体のメソッド実装

impl AssetChecker {
    // コンストラクタ
    fn new(pattern: &str, extensions: Vec<String>) -> Self {
        AssetChecker {
            name_pattern: Regex::new(pattern).unwrap(),
            allowed_extensions: extensions,
        }
    }
}
  • new: 構造体の新しいインスタンスを作成する関数
  • pattern: &str: 文字列スライス(参照)として正規表現パターンを受け取る
  • unwrap(): エラーが発生した場合にパニックする(本番環境では適切なエラーハンドリングが必要)

ファイルチェックのメソッド

fn check_file(&self, path: &Path) -> Result<(), String> {
  • &self: メソッドが構造体のインスタンスを参照として使用
  • Result<(), String>: 成功時は空のタプル()、失敗時はStringを返す

ファイル名の処理

let name_without_ext = path
    .file_stem()           // パスからファイル名(拡張子なし)を取得
    .map(|s| s.to_string_lossy())  // OSStringをUnicodeに変換
    .unwrap_or_default();  // 失敗時はデフォルト値を使用

パターンマッチングとエラー処理

if !self.name_pattern.is_match(&name_without_ext) {
    return Err(format!("Invalid file name: {} (without extension)", name_without_ext));
}
  • is_match: 正規表現パターンとマッチするか確認
  • format!: 文字列フォーマット(printfに相当)

拡張子チェック

if let Some(ext) = path.extension() {
    let ext_str = ext.to_string_lossy().to_lowercase();
    if !self.allowed_extensions.contains(&ext_str.to_string()) {
        return Err(format!("Invalid extension: {}", ext_str));
    }
}
  • if let: パターンマッチングの簡略形
  • to_lowercase(): 大文字小文字を区別しないチェック

メイン関数

fn main() {
    let checker = AssetChecker::new(
        r"^[a-z][a-z0-9_]*$",  // 正規表現パターン
        vec!["png".to_string(), "jpg".to_string()], // 許可する拡張子
    );
  • r"...": Rawストリング(エスケープシーケンスを無視)
  • ^[a-z]: 小文字アルファベットで始まる
  • [a-z0-9_]*: 小文字、数字、アンダースコアの0回以上の繰り返し
  • $: 文字列の終端

ディレクトリ走査

for entry in WalkDir::new("./checktarget")
    .into_iter()
    .filter_map(|e| e.ok())
  • WalkDir: 再帰的なディレクトリ走査
  • filter_map: イテレータを変換し、Some値のみを抽出

結果の出力

match checker.check_file(path) {
    Ok(()) => println!("OK: {}", path.display()),
    Err(e) => println!("Error: {} for file {}", e, path.display()),
}
  • match: パターンマッチング(switch文に相当)
  • path.display(): パスを表示用の文字列に変換

まとめ

本記事では、Rustを使用したアセット命名規則チェッカーの実装を通じて、以下の点について学びました。

  • Rustを使用したツール開発の基本的なワークフロー
  • 外部クレート(walkdir、regex)を活用した実用的なプログラミング
  • ゲーム開発における自動化ツールの重要性

このツールはを元に以下のような拡張も検討してみてください。

  • より複雑な命名規則のサポート
  • 設定ファイルによるルールの外部化
  • CI/CDパイプラインへの組み込み

この記事で紹介したコードは非常にシンプルなものですが、Rustの特徴である安全性と高いパフォーマンスの一端を感じていただけたのではないでしょうか。私自身、このような小さな自動化ツールの開発から始めることで、少しずつその魅力に引き込まれていきました。

もし興味を持っていただけたなら、ぜひこのコードを土台に、ご自身のプロジェクトで困っていることを解決するツールを作ってみてください。Rustコミュニティは初心者に優しく、困ったときは力になってくれるはずです。

Discussion