PONOS Advent Calendar 2025の19日目の記事です。
昨日は、@tequila0725さんの自作キーボードをつくってみたでした。
はじめに
前回のアドベントカレンダーで、RustのゲームエンジンBevyでマスタデータを扱ってみるの記事を書かせていただきました。CSVを読み込んでマスタデータとして読み込んでみましたが、今度は2Dゲームでステージ等を構成する際に使用することがあるTilemapについてBevyエンジンで実現をしてみました。Tilemapは、ゲーム開発などの分野で、小さな画像(タイル)を敷き詰めて、背景やステージのような大きなマップを効率的に作成するためのシステムになります。
自分の実現方法については1つの方法として参考にしていただければと思います。
前回公開した記事について、RustのゲームエンジンBevyに触れてみるを公開しております。こちらにBevyエンジンの特徴など記述しておりますのでご覧になってはいかがでしょうか。
実装
実装環境としてはローカル(macOS)で実施することを前提しています。
| 開発環境 | バージョン |
|---|---|
| MacOS(Sequoia) | 15.3.2 |
| Rust(rustc、cargo) | 1.91.1 |
| Bevy | 0.17.3 |
事前準備
開発端末にRustを導入しなくてはいけませんが、導入する方法は2019年アドベントカレンダーの17日目の記事で記載しておりますので、今回も省略いたします。
プロジェクトの作成
$ cargo new bevy_tilemap
生成されたプロジェクトに必要なファイルを設置していきます。実装したソースコードについては、以下のように配置していきます。
├──Cargo.toml
├──README.md
├──assets
│ ├──stage_atlas.png
│ └──stage.csv
└──src
├──camera.rs
├──csv.rs
├──main.rs
└──stage.rs
実装する
まずは、Tilemapを実装するにあたって以下画像を用意しました。プロジェクトにassetsディレクトリを作成し、そこに画像(stage_atlas.png)を配置しておきます。
こちらの画像は、128x64のサイズで1マス32x32を8個繋げたアトラス画像になっております。このアトラス画像をTilemapでどのように描画するかについて、以下のようなCSVファイルを設置します。
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
-1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,-1
-1,3,3,3,3,2,2,2,2,2,3,3,3,3,3,3,3,2,2,2,2,2,3,3,3,3,3,3,3,2,2,2,2,2,3,3,3,3,3,-1
-1,3,3,3,2,1,0,0,0,1,2,3,3,3,3,3,2,1,0,0,0,1,2,3,3,3,3,3,2,1,0,0,0,1,2,3,3,3,3,-1
-1,3,3,2,2,1,0,0,0,1,2,2,3,3,3,2,2,1,0,0,0,1,2,2,3,3,3,2,2,1,0,0,0,1,2,2,3,3,3,-1
-1,3,3,3,2,1,0,0,0,1,2,3,3,3,3,3,2,1,0,0,0,1,2,3,3,3,3,3,2,1,0,0,0,1,2,3,3,3,3,-1
-1,3,3,3,3,2,2,2,2,2,3,3,3,3,3,3,3,2,2,2,2,2,3,3,3,3,3,3,3,2,2,2,2,2,3,3,3,3,3,-1
-1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,-1
-1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,-1
-1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,-1
-1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,-1
-1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,-1
-1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,-1
-1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,-1
-1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,-1
-1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,-1
-1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,-1
-1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,-1
-1,4,4,4,4,5,5,5,5,6,6,6,6,7,7,7,7,7,7,7,7,7,7,7,7,7,7,6,6,6,6,5,5,5,5,4,4,4,4,-1
-1,4,4,4,5,5,5,6,6,6,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,6,6,6,5,5,5,4,4,4,-1
-1,4,4,5,5,6,6,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,6,6,5,5,4,4,-1
-1,4,5,6,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,6,5,4,-1
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
「-1」は何も描画しないという設定で、それ以外はアトラス画像のどの部分を表示するかのindexの番号となっております。画像の左上から0で右下が7という設定で実装します。
各種追加したコードを記述します。
[package]
name = "bevy_tilemap"
version = "0.1.0"
edition = "2024"
[dependencies]
bevy = "0.17.3"
csv = "1.4.0"
use bevy::{
prelude::{App, default, DefaultPlugins, PluginGroup, PreStartup, Startup, Window, WindowPlugin},
window::{PresentMode, WindowResolution, WindowTheme},
};
mod camera;
mod csv;
mod stage;
fn main() {
App::new()
.add_plugins(
// アプリケーションのウィンドウ設定
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Bevy Tilemap".into(),
resolution: WindowResolution::new(1280, 720),
resizable: false,
present_mode: PresentMode::AutoVsync,
window_theme: Some(WindowTheme::Dark),
..default()
}),
..default()
})
)
// カメラの生成
.add_systems(PreStartup, camera::create)
// ステージの生成
.add_systems(Startup, stage::create)
.run();
}
use bevy::prelude::{Camera2d, Commands};
/// カメラの生成
///
/// * `command` - Worldの構造変更を実行するためのコマンド
pub fn create(mut command: Commands) {
// 2Dカメラを生成
command.spawn(Camera2d::default());
}
use std::error::Error;
use std::fs;
/// CSVファイルを読み込んでVecのString型を取得
///
/// * `file_path` - CSVファイルのパス
/// * `is_header` - ヘッダーありで取得するか
pub fn read(file_path: &str, is_header: bool) -> Result<Vec<csv::StringRecord>, Box<dyn Error>> {
// 読んだCSVファイルを一時的に保管するベクタ型
let mut records = Vec::new();
// CSVファイルを読み込む。
let csv_text = fs::read_to_string(file_path)?;
// ヘッダーありで取得するかしないかを判定して読み込む。
let mut reader = if is_header {
// CSVライブラリは通常以下のように読み込むが、1行目が読み込まない設定になっているため、読み込むように変更する。
csv::ReaderBuilder::new().has_headers(false).from_reader(csv_text.as_bytes())
} else {
// CSVライブラリで読み込むとヘッダーなしで読み込まれる。
csv::Reader::from_reader(csv_text.as_bytes())
};
// 読み込んだCSVの内容を1行ずつ読み込む。
for string_record in reader.records() {
let record: csv::StringRecord = string_record?;
records.push(record);
}
Ok(records)
}
use bevy::{prelude::*, sprite::Anchor};
use crate::csv;
/// ステージの生成
///
/// * `command` - Worldの構造変更を実行するためのコマンド
/// * `asset_server` - アセットの読み込みするアセットサーバ
/// * `texture_atlases` - テクスチャの位置を検索するために使用するテクスチャアトラス
pub fn create(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
) {
// ステージのアトラス画像を取得する。
let stage_atlas: Handle<Image> = asset_server.load("stage_atlas.png");
// ステージテクスチャー画像のアトラス形式にする。
let tilemap_size = 32.0;
let texture_atlas_layout = TextureAtlasLayout::from_grid(UVec2::splat(tilemap_size as u32), 4, 2, None, None);
// ステージテクスチャー画像のアトラスを取り扱いできるようにする。
let handle_texture_atlas_layout = texture_atlases.add(texture_atlas_layout);
// パスを指定してCSVからレコード群を読み込む。
let records = match csv::read("assets/stage.csv", true) {
Ok(result) => result,
Err(err) => panic!("{}{:?}", "Error Message: ", err),
};
// ステージのタイルマップ情報をベクタ型として取得する。
let mut stage_tilemap: Vec<Vec<i32>> = Vec::new();
for record in records {
stage_tilemap.push(record.deserialize(None).unwrap());
}
// タイルマップの表示する座標情報を初期化する。
let mut tilemap_x = -640.0;
let mut tilemap_y = 360.0;
// ステージを表示するタイルマップ情報を格納する変数を初期化する。
let mut stage_tilemaps: Vec<(Sprite, Anchor, Transform)> = Vec::new();
// CSVから読み込んでステージ情報からステージを表示するタイルマップ群情報を構築する。
for (_index_x, x_coordinate_points) in stage_tilemap.iter().enumerate() {
for (_index_y, y_coordinate_point) in x_coordinate_points.iter().enumerate() {
// 0未満の数値は除外する。
if *y_coordinate_point < 0 {
tilemap_x = tilemap_x + tilemap_size;
continue;
}
// ステージバンドルにスプライト情報および衝突情報を保管する。
// 保管したものをステージバンドル群に挿入する。
stage_tilemaps.push((
// ステージのスプライト情報を設定
Sprite {
image: stage_atlas.clone(),
texture_atlas: Some(TextureAtlas {
layout: handle_texture_atlas_layout.clone(),
index: *y_coordinate_point as usize
}),
custom_size: Some(Vec2::splat(tilemap_size)),
image_mode: SpriteImageMode::Tiled {
tile_x: true,
tile_y: true,
stretch_value: 0.5
},
..default()
},
// Spriteのアンカーの位置
Anchor::TOP_LEFT,
// 位置情報を設定
Transform {
translation: Vec3::new(tilemap_x, tilemap_y, 0.0),
..default()
}
));
// X軸に座標をスプライトの倍率を考慮して加算する。
tilemap_x = tilemap_x + tilemap_size;
}
// Y軸に座標をスプライトのサイズを考慮して加算し、X軸をリセットする。
tilemap_x = -640.0;
tilemap_y = tilemap_y - tilemap_size;
}
commands
.spawn_batch(stage_tilemaps);
}
実装内容
main
main関数では、bevyエンジンで必要なプラグインの読み込みやシステムを追加して実行する処理を記載しています。
csv::read
csvモジュールのread関数では、アトラス画像をどのように表示するかのCSVを読み込んでいます。CSVの読み込みについては、前回記述した記事「RustのゲームエンジンBevyでマスタデータを扱ってみる」で、少し詳しく記載しておりますのでこちらもご確認いただけると幸いです。
camera::create
cameraモジュールのcreate関数では、bevyエンジンが用意している2Dのカメラのエンティティを生成します。こちらは生成するだけの処理としていますが、カメラの操作等をこちらのモジュールで記載すると見やすいかもしれません。
stage::create
stageモジュールのcreate関数が、今回の記事で実施したかったアトラス画像をTilemapで表示する実装が記載されております。この中でアトラス画像の読み込み、CSVモジュールを使用してのCSVファイル読み込みを行なっております。アトラス画像は、16x16のサイズで分割(from_grid)する設定を行います。CSVからstage_tilemapsのVec型変数に、生成すべきステージのエンティティ群を登録していきます。それらエンティティは1つずつに、Sprite(表示すべき画像)、Anchor(座標の基準点))、Transform(表示する画像の位置)のコンポーネントを定義し、タプルで紐づけております。
動作確認
$ cargo run
以下のように、アトラス画像がCSVの設定の通りに表示されていれば成功です。
おわりに
今回は、2Dゲームのステージを構成する際に非常に便利なTilemapのシステムですが、Bevyのエンジンで実現してみました。Bevy公式サイトのサンプルを参考に実装しておりまして、Tilemapを実現する参考にしていただければと思います。
Tilemapは2Dアクションや2DのRPGに使用されることが多いかと思いますが、そこで必要になってくるものとして当たり判定(コリジョン)をどうするかが課題になってくるかと思います。次回は、その当たり判定について言及できたらと思います!
次回は、@mon3612さんになります。お楽しみに!

