超入門!Rustで作るアセット管理ツール~アセット命名規則チェッカー~
はじめに
ゲーム開発において、アセット管理は地味ながら重要な業務の一つです。プロジェクトが大きくなるにつれ、手作業での管理は困難になり、ミスのリスクも高まります。
アセットとは、ゲームで使用される画像、音声、3Dモデルなどのデータファイルの総称です。例えば、キャラクターの画像、背景音楽、効果音、ステージの3Dモデルなどが該当します。
本記事では、Rustを使用してアセット名の命名規則管理を自動化するツールの作成方法を解説します。
想定読者
- ゲーム開発の効率化に興味がある開発者
- Rustに興味ある方
- 自動化ツールの作成経験が少ない方
なぜRustなのか
アセット管理ツールの開発にRustを選択する理由を簡単に触れておきます。
記事の内容がぶれないようにあえて具体的な処理の話など、専門的な観点を除いた点で紹介します。
💡 他の言語との比較
- Python:書きやすいが実行速度が遅い
- JavaScript:手軽だがメモリ管理が甘い
- C++:高速だが安全性に課題。学習コストは高め
- Rust:高速で安全、ただし学習コストは高め
-
高いパフォーマンス
- 大量のアセットを効率的に処理
例:数千枚の画像を数秒で処理 - メモリ使用の最適化
例:不要なメモリ確保を避け、リソースを効率的に使用 - 並列処理の容易な実装
例:複数のCPUコアを使って同時に処理
- 大量のアセットを効率的に処理
-
クロスプラットフォーム
-
シンプルなバイナリ配布
- 依存ライブラリが静的リンク
- Pythonのような実行環境不要
-
高い移植性
- Windows/Mac/Linuxで同じコードが動作
- 環境差異を抽象化するAPIを提供
-
シンプルなバイナリ配布
-
将来性と実績
-
大手企業での採用実績
- Discord:全サーバーのメモリ使用量を50%削減
- Dropbox:同期エンジンのパフォーマンス改善
-
活発なコミュニティ
- 7年連続でStack Overflowで最も愛されている言語
- 月間1億以上のクレートダウンロード
-
大手企業での採用実績
アセット管理の課題
ゲーム開発におけるアセット管理では、主に次のような課題があります。
-
命名規則の統一
- 複数のアーティストやデザイナーが作成するアセット
例:player_character.png
vsPlayerCharacter.png
vsplayer-character.png
- プラットフォームごとの命名制限
例:Windowsでは特定の文字が使用できない - 大文字小文字の揺れ
例:Background
vsbackground
- 複数のアーティストやデザイナーが作成するアセット
-
フォーマット変換
- 作業用フォーマットからゲーム用フォーマットへの変換
例:PhotoshopのPSDファイルからPNGへの変換 - プラットフォームごとの最適化
例:モバイル向けの圧縮設定 - メタデータの付与
例:画像サイズ、作成日時、作者情報の管理
- 作業用フォーマットからゲーム用フォーマットへの変換
-
バージョン管理
- アセットの更新履歴の追跡
例:いつ、誰が、どのように変更したか - 変更の影響範囲の特定
例:テクスチャの変更がどのモデルに影響するか - バックアップの管理
例:古いバージョンの保存と復元
- アセットの更新履歴の追跡
アセット命名規則チェッカーを作成
今回は、ファイル名の命名規則と拡張子を検証するツールを動作させてみます。
開発環境の準備
Rustの環境構築から順を追って説明します。
- Rustのインストール
-
Windows の場合
https://rustup.rs/ からRustupインストーラーをダウンロードして実行 -
macOS / Linux の場合
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
- 新しいプロジェクトの作成
# 新しいディレクトリを作成して移動
mkdir file_checker
cd file_checker
# Cargoプロジェクトを初期化
cargo init
- 必要な依存関係の追加
コードで使用しているwalkdir
とregex
クレートをCargo.toml
に追加する必要があります。
Cargo.toml
を以下のように編集してください:
[package]
name = "file_checker"
version = "0.1.0"
edition = "2021"
[dependencies]
walkdir = "2.3"
regex = "1.5"
- コードの実装
下記コードを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()),
}
}
}
}
- プログラムの実行
# プロジェクトディレクトリで実行
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