Open14

Rust&Bevy勉強日誌

amanitasamanitas

C++歴一年のプログラミング初心者がRustに手を出してしまった備忘録。

自分のスキル(C++)

  • C++で簡単なアプリは作れる
  • 継承使いこなせない
  • テンプレートメタプログラミングわからない
  • デザインパターン勉強中

1/20 Rust一日目

使用教材

感想

rustupやcargoはシンプル操作かつ使いやすく、コンパイルが爆速で良い。
直前にc++環境(boost,cmake)を作ったときはかなり工数が嵩んだので、さすがモダンかつ人気ある言語といった印象。

コンパイル時の指摘は多いと聞いたが、個人的にはc++の警告最大にしていたときと頻度は変わらないように思う。ただ、エラー時の指摘は具体的でわかりやすい。コンパイルが通ってしまうが意図していない挙動の場合にどこを修正すればいいかまでは指摘してくれないので注意が必要(それはそう)

1/21 Rust二日目

使用教材

感想

ジェネリクスに手を出してトレイト境界沼に溺れかける。とにかく日本語情報が(C++と比べて)少ないので英語のドキュメントをうまく読むスキルが必要。

悪戦苦闘の末できたベクタの平均を求めるジェネリクス
mean_for_list.rs
fn sum_of_list<T>(list:&[T])->T
  where T:Clone+std::ops::Add<Output=T>{
    let mut result =list[0].clone();
    let iter = list[1..].iter();
    for item in iter {
      result =result + item.clone();
    }
    result 
}
fn mean<T>(dividend: T,divisor: T)->T
  where T:std::ops::Div<Output=T>{
  dividend/divisor
}

分割した。なおよりスマートな解法を教えてもらった(スマートな解法なら一つの関数にまとめることができます)

Rustfmt,Rustfix,RustClippyは神

1/22 Rust三日目

使用教材

  • Rust sokoban
  • ECS(Entity Component System)を学びつつ倉庫番が出来上がる

感想

もともとECSを学びたいと思いこのドキュメントを見つけたのでRustに手を出したという経緯。
外部クレートの導入と使用宣言のやりかたが身についた。また、はじめはmain.rsに書かせた上で肥大化してきたタイミングでモジュールに分割する方法を教えてくれるので良い感じ。

インデントが大変なことになりがち。

amanitasamanitas

1/23 Rust四日目

使用教材

Amethystは開発終了してしまったらしい。現在はBevyが人気っぽいのでそれを学ぶ。
と思ったが公式チュートリアルがあっさりしていて、使いこなすにはDoc.rsを読み込むスキルが必要のようだ。

ggez+specsとBevy

直近でRust-sokoban(ggez+specs)をやったので、Bevyでの書き方はかなり簡単だと感じた。(無論、Bevyは後発のものなのでより改善されているのは当然ではある)

Rust-sokoban(ggez+specs)での書き方
rust-sokoban.rs
//コンポーネント作成
use specs::{Component, VecStorage};
#[derive(Component)]
#[storage(VecStorage)]
pub struct Component'sName{}

//エンティティ作成
use specs::{Builder,World,WorldExt};

pub fn create_entity_name(world: &mut World, component1: ComponentName...){
  world
    .create_entity()
    .with(component1{...})
    .with(component2{...})
    /*追加するコンポーネントの分だけwith()を書く*/
    .build();
}

//システム作成
use crate::components::*;
use specs::{Entities, System, ...};
use ggez::Context;

pub struct SystemName<'a>{
  pub context:&'a mut Context, 
}
impl<'a> System<'a> for SystemName<'a>{
  type SystemData ={/*書いたり呼んだりするものを宣言...*/};
  fn run(&mut self,data: Self::SystemData){
   /*具体的な処理*/
  }
}
main.rs
use bevy::prelude::*;
//コンポーネント作成
#[derive(Component)]
struct Component1{}

//エンティティ作成関数
fn add_EntityName_function(mut commands: Commands){
  commands.spawn().insert(Component1).insert(Component...);
}

fn main(){
  //add_startup_system()にエンティティ作成関数を渡すことで晴れて実体化
  App::new().add_startup_system(add_EntityName_function).run();
}

実際に手を動かす

トランプのカードを実装してみた。

52枚のカードを作る!
main.rs
use bevy::prelude::*;

//コンポーネント作成
#[derive(Component)]
pub struct Card;

#[derive(Component)]
pub struct Value(u8);

#[derive(Component)]
pub enum Suit{
  Spade,
  Heart,
  Diamond,
  Club,
  None, //Jokerとかでもいい気がする
}

//エンティティ作成
pub fn add_cards_all(mut: commands: Commands){
  let mut count:u8 =0;
  while count < 52{
  let value = count%13+1;
  let suit = match count%4 {
    0 => Suit::Spade,
    1 => Suit::Heart,
    2 => Suit::Diamond,
    3 => Suit::Club,
    _ => Suit::None,
    };
  //具体的なエンティティ追加処理はここから
  commands.spawn().insert(Card).insert(Value(value)).insert(suit);
 //ここまで 
  count += 1;
  }
}

fn main(){
  App::new()
         .add_startup_system(add_cards_all)
         .run();
}

モチベが上がるRust、Bevy関連のURLまとめ

Rust

  • Rust by Example 日本語版
    すこし手を動かした後なのでわかりやすく感じる。プログラミング慣れしているひとはThe Book飛ばしてこれからでもよさそう
  • Are we game yet?
    Rustでゲームを作る際に便利なエコシステムの紹介、Rust製ゲームの紹介、Rustでゲームを作るためのチュートリアルなどが乗っている。

Bevy

amanitasamanitas

1/24 Rust五日目

カードの中身を表示するシステム

Rustのパターンマッチが便利だなぁと感じる

main.rs
fn print_cards(query: Query<(&Suit,&Value)>){
  for (suit, value) in query.iter() {
    let s_string = match suit{
      Suit::Spade => "♠",
      Suit::Heart => "♥",
      Suit::Diamond => "♦",
      Suit::Club => "♣",
      _ => " ",
    };
    let v_string = match value{
      Value(1_u8)  => "A".to_string(),
      Value(2..=9)  => value.0.to_string(),
      Value(10_u8) => "T".to_string(),
      Value(11_u8) => "J".to_string(),
      Value(12_u8) => "Q".to_string(),
      Value(13_u8) => "K".to_string(),
      _ => " ".to_string(),
    };
    print!("[{},{}]",s_string,v_string);
  }
}

Plugin化する

BevyはAppに追加したいものをプラグインとしてまとめることができる
使い方は簡単で、Pluginトレイトを継承してfn build(&self, app:&mut App)を実装するだけ。

card_plugin.rs
pub struct CardPlugin;
impl Plugin for CardPlugin{
  fn build(&self, app: &mut App){
  app.add_startup_system(add_card_deck)
       .add_system(print_cards);
  }
}
//以下コンポーネントやシステム関数を羅列

メイン関数がスリムであることは重要なので、ばしばしプラグイン化していこう。
main関数で呼び出すのは簡単で、

main.rs
use bevy::prelude::*;
mod card_plugin;
use  card_plugin::CardPlugin;

fn main(){
  App::new().add_plugin(CardPlugin).run();
}

プラグインが増えてきたらlib.rsに書きまとめてmain関数をスッキリさせることができる。

Bevy 0.6 リリースノートを読もう

Bevy0.6がリリースされたのは2022/01/08とつい最近である。なので英語情報ですらBevy0.6準拠の情報でない場合が多々ある。そのため、古い構文か最新の構文かを判断するためにリリースノートを読むと良い。

といっても長いし英語なのでところどころピックアップ

Bevy0.6→App::new()  Bevy0.5→App:build()

build()と書いてある記事はちらほらあり、build()ではコンパイルが通らないので注意

main.rs
fn main(){
  //bevy 0.5
  App::build().run();
  //bevy 0.6
  App::new().run;
}

#[derive(Component)]の仕様変更

より書きやすくなったり、ストレージの指定が簡単になったらしい(クソ雑魚英語リアン)

main.rs
//普通のコンポーネントは"Table"ストレージらしい
#[derive(Component)]
struct Hoge;

//ストレージを変更する場合こう書く
#[derive(Component)]
#[component(storage = "SparseSet")]
struct Foo;

//この書き方でも可
struct Hyaaaa;
impl Component for Hyaaaa{
    type Storage = TableStorage;
}

新しいBevyBookの制作が進行中

Bevyコミュニティの活気強さが感じられますね、楽しみです。

amanitasamanitas

1/25 Rust6日目

Event

Eventは異なるシステム間でメッセージを送りあい連携できる

event.rs
//イベントの宣言
struct EventName(/*送るアイテム、Entityが一般的?*/);

//イベント発生を伝える側
fn event_check(mut event: EventWriter<EventName>,...){
  if /*何かの条件を満たしているときに*/{
    event.send(EventName(send_item));
  }
}

//イベント発生したときに処理をする側
fn event_action(mut event: EventReader<EventName>){
  for item in event.iter(){
   //処理
  }
}

//イベント登録
fn main(){
  App::new()
         .add_event::<EventName>()
         .add_system(event_chack)
         .add_system(event_action)
         .run();
}

イベントの発生を知らせる側はEventWriter<EventName>を引数に取り、.send(書き込み)
イベント発生に応じて処理する側はEventReader<EventName>を引数にとり、send内容を取得できる。

EntityIDでクエリに問い合わせる

if let Ok(component) = query.get(entity_id)

if let Ok(component) = query.get_mut(entity_id)

引数で対象のコンポーネントを指定したうえでエンティティIDを問い合わせる。

event.rs
fn level_reset(
  mut event: EventWriter<LevelReset>,
  mut query: Query<&mut PlayerExp>
){
  for entity in event.iter(){
    if let Ok(mut exp) =query.get_mut(entity.0){
      exp.0 =0;
      }
   }
}

EventにEntityを持たせるとこんな感じでイベント処理側でのみミュータブルアクセスができて、いい感じ。

最近の課題

Rustのユニットテストやデバッグをもう少し学びたい。

今までデバッグをまともにやったことがないのでノウハウを学ぶところから。

amanitasamanitas

1/26 Rust七日目(for Bevy)

VSCodeの拡張機能をrust-analyzerに変更した。

昨日少し試していて、
あまりにもおせっかいが過ぎる(主にドキュメントを自動で表示してコード画面を圧迫する)ので導入を決めあぐねていた。
機能としては公式提供のRust拡張機能よりもさらに手厚い。
Rust公式の機能ですらしつこく未使用変数に波線を使ってくるので本当につらい。
rust-analyzerはその引数が何であるかの注釈を入れてくるのだが、Bevy使用時にそれをさせるとコードが果てしなく横に伸びて画面外に文字が消えていく。

つらい。

でも便利。

アセット

デフォルトはルート直下のassetsフォルダの中を読み込んでくれる。正しいディレクトリ構造はこう

package
 |_cargo.toml
 |_src
 |_assets
    |_fonts
       |_ fontdata.ttf
       |_ fontdata.otf

フォントはttfでもotfでも大丈夫。

Official Examples

BevyCheatBookを一通り読み終わったので
Official Examplesがちょっと理解しながら読めるようになった。写経している。

ボタンUIの実装

ボタンUI実装例をみていてなんとなくエンティティの親子関係の感覚がつかめた。

ボタン作成の基本は用意されているButtonBundleをcommandsに渡すだけ

button.rs
fn setup(mut commands){
  commands.spawn_bundle(ButtonBundle{});
}

ButtonBundleのパラメータのなかから指定したいものは指定する。フィールドにもっている
Styleの設定項目も多い。

Styleの項目
  • display: Display // enum Display{Flex, None };
    フレックスボックスレイアウトを使用するか否か
    柔軟にサイズが伸び縮みして画面にフィットしてくれるらしい
  • position_type: PositionType // enum PositionType{Relative,Absolute};
    ほかのノードに対して相対的配置をするか、絶対的配置をするか
  • direction: Direction //enum Direction{Inherit, LeftToRightm RightToLeft};
    ノードのコンテンツが進むべき方向(継承、左から右へ、右から左へ)
  • flex_direction: FlexDirection //enum {Row,Column,RowReverse,ColumnReverse}
    列と行どちらのレイアウトを採用するか(Reverseは折り返し)
  • flex_wrap: FlexWrap //enum {NoWrap,Wrap,WrapReverse}
    ノードの折り返し方向
  • align_items: AlignItems //{FlexStart,FlexEnd,Center,Baseline,Strech}
    十字軸に沿ったアイテムの配置方法
  • align_self: AlignSelf //{Auto, /上と同じ項目/}
    上と同じだが、この項目のみの設定,Autoを選ぶとAlignItemsの値を参照する?
  • align_content: AlignContent//{/align_itemsと同じ項目+/,SpaceBetween,SpaceAround}
    flex_wrapがFlexWrap::Wrap に設定され、かつ複数行の項目がある場合にのみ適用される各行の配置方法
  • justify_content: JustifyContent//{FlexStart,FlexEnd,Center,SpaceBetween,SpaceAround,SpaceEvenly}
    主軸に沿ったアイテム配置方法の位置を指定 (SpaceEvenlyはアイテム間が等間隔になる?)
  • position: Rect<Val>
    Rectで指定されたノードの位置
  • margin: Rect<Val>
    ノードのマージン(外側の余白)
  • padding: Rect<Val>
    ノードのパディング(内側の余白)
  • border: Rect<Val>
    ノードの境界線
  • flex_grow: f32
    フレックスボックスのアイテムにスペースがある場合、どの程度大きくするかの定義
  • flex_shrink: f32
    十分なスペースがない場合の縮小方法
  • flex_basis: Val
    アイテムの初期サイズ
  • size: Size<Val>
    フレックスボックスのサイズ
  • min_size: Size<Val>
    フレックスボックスの最小サイズ
  • max_size: Size<Val>
    フレックスボックスの最大サイズ
  • aspect_ratio: Option<f32>
    フレックスボックスのアスペクト比
  • overflow: Overflow//{Visible Hidden}
    オーバーフローの処理方法
amanitasamanitas

1/27 Rust8日目

main.rsとlib.rsと時々ディレクトリ化のモジュール

いまだにディレクトリに封じたモジュールをlib.rsでどう書けばいいんだっけと悩むのでメモ。

ディレクトリ構造

package_name
 |_Cargo.toml 
 |_src
    |_main.rs
    |_lib.rs
    |_plugins.rs
    |_plugins
       |_module_name.rs

pluginsディレクトリの中身のmodule_nameモジュールを使いたい場合、ディレクトリと同じ名前のplugins.rsでmodule_nameを宣言する。

plugins.rs
pub mod module_name;

lib.rsはpluginsをモジュール宣言する。

lib.rs
pub mod plugins;

main.rsではpackage_nameから宣言する

main.rs
use package_name::plugins::module_name::*;
よく検索でヒットするモジュール関連のRustの古い仕様

mod.rsは古い

lib.rsが強いらしい

extern crate package_name;は不要

main.rsでこれを書く必要はないらしい

pluginsディレクトリと同名の.rsを用意してそこでモジュールを宣言する必要がある、というのがちょっと迂遠でしばしば忘れて混乱のもとになる。もしかしたらもっとスマートな方法があるかもしれないが...

amanitasamanitas

1/28

いいかげんゲームっぽいものを作らないとモチベーションが下がってくるのでBevyでマップ作成&表示をする方法を考える。

Transformsを読む

日本語訳

Official Example:parenting.rs

構成

トランスフォーム(Transforms)とは、ゲーム世界にオブジェクトを配置するためのもの。以下の三要素からなる

  • トランスレーション(translation) 位置や座標 オブジェクトを移動させる
  • ローテーション(rotation) 回転 回転
  • スケール(scale) サイズ調整 見た目の大小を調整

コンポーネント

  • Transform,
  • GlobalTransform

Transform

三要素を含む構造体。基本的にこれを操作する。
クエリを使用してシステムからアクセスする。

もしエンティティに親が存在する場合、親に対して相対的な操作になる。
つまり、親の操作は子に伝搬する。

GlobalTransform

絶対的な位置を表す。エンティティが親を持たない場合、これはTransformと同じ値を持つがユーザーはこの値を書き換えるべきではない(Bevyシステムの内部で管理されている)

Transform構造体の定義の仕方

三要素は以下のように定義できる

entity.rs
            transform : Transform{
                translation : Vec3::new(0.0,0.0,10.0), 
                rotation : Quat::from_rotation_z(0.0),
                scale : Vec3::splat(2.0),
            },

translationは(x,y,z)の順にfloatで定義する。z値が大きいほど手前に表示される。
Bevyのx,yは数学のxy座標と向きが同じ,xは右にyは上に伸びていく。

Asset画像の読み込み

なるべく似たような、あるいはアニメーションさせるデータを使うときは一枚の画像をスライスして使うとよいので、画像読み込み~エンティティ作成の関数の基本は以下の様になる

entity.rs
fn maptile_setup(
    mut commands: Commands, //いつもの
    asset_server: Res<AssetServer>, //アセット使用のため
    mut texture_atlases: ResMut<Assets<TextureAtlas>>, //アニメーションとかマップタイルとかはテクスチャアトラスで管理できる
) {
    //画像そのものに対するハンドルを取得
    let texture_handle = asset_server.load("textures/player.png");
   //画像をスライスしたもの(TextureAtlas)を作成
    let texture_atlas = 
      TextureAtlas::from_grid(
          texture_handle, 
          Vec2::new(/*一マスの大きさ*/16.0, 16.0), /*列数*/7,/*行数*/ 1);
    //テクスチャアトラスのハンドルを作成
    let texture_atlas_handle = texture_atlases.add(texture_atlas);

   //いつもの
    commands
        .spawn_bundle(SpriteSheetBundle {
            texture_atlas: texture_atlas_handle,
            transform : Transform{
                translation : Vec3::new(0.0,0.0,10.0),
                rotation : Quat::from_rotation_z(0.0),
                scale : Vec3::splat(2.0),
            },
            ..Default::default()
        });
}

マップを作るにはどう処理するか考える

一番無難かつ直感的なのは、二次元配列を用意してループを回す。
ただ、一般的な二次元配列は左上から始まり右下に増えていくのでtranslation:Vec3::new(x,y,0.0)とやってしまうと左に90度回転してしまうはず。
それを加味して元MAPデータを作ってもよいがもう少しスマートな解法がありそう

ひとまずここまで

画像
画像は公式のアセットを使わせてもらっている。

main.rs
use bevy::prelude::*;
fn main(){
  App::new()
    .init_resource::<TilePng>() 
    .add_plugins(DefaultPlugins)
    .add_startup_system(tile_load.label("load"))
    .add_startup_system(map_setup.after("load"))
    .add_startup_system(player_setup)
    .run();
}
それぞれの実装

ハンドルリソース

#[derive(Component)]
struct TilePng{
  handle: Handle<TextureAtlas>,
}

fn tile_load(
 mut tile_png: ResMut<TilePng>,
 asset_server: Res<AssetServer>,
 mut texture_atlases: Res<Assets<TextureAtlas>>,
){
  let texture_handle = asset_server.load("asset-path")l
  let texture_atlas = TextureAtlas::from_grid(texture_handle,Vec2::new(16.0, 16.0), 4, 1);
  tile_png.handle = texture_atlases.add(texture_atlas);
}

アセット読み込みとタイルエンティティの登録を分割するためにハンドルをリソースに登録した。

マップタイルセット

fn map_setup(
    mut commands:Commands,
    texture_atlas_handle: Res<TilePng>,
){
    commands.spawn_bundle(OrthographicCameraBundle::new_2d());
    for x in -4..4 {
        for y in -4..4{
            let position = Vec2::new(x as f32, y as f32);
            let tile_size = Vec2::splat(48.0);//scaleで拡大するので16.0*3.0
            commands.spawn_bundle(SpriteSheetBundle{
                texture_atlas: texture_atlas_handle.handle.as_weak(),
                transform: Transform{
                    translation : (position * tile_size).extend(0.0),
                    rotation : Quat::from_rotation_z(0.0),
                    scale : Vec3::splat(3.0),                    
                },
                ..Default::default()                
            });
        }
    }
}
as_weak()はハンドルへの弱参照だが正直弱参照じゃなくてもいいかなという気がする。
for文は0スタートではなくプラスマイナス0になるように回してなるべく中央に表示させているが,この実装では応用が聞かないので別の方法を考え中

土日は積んでいるゲームを消化するので勉強は一旦中断かな

amanitasamanitas

1/30

昨日は某モンスター捕獲ゲームを遊びまくっていたのでVsCodeを開いてすらいなかった。

ダンジョンマップ生成システム(ただの四角形フロア)

最初はシンプルに、ランダムな大きさの四角形を生成して縁は壁に、それ以外は床になる二次元配列を生成する。また、ダンジョン、というか最終目標はローグライクなダンジョンゲーム作成なので、床のどこかに階段を一つ生成する。

乱数を用いたいのでrandクレートを使用。

map.rs
use rand::Rng;
fn map()-> Vec<Vec<u32>>{
  const MAX_WIDTH:u32 =20;
  const MAX_HEIGHT:u32=20;

  let mut rng = rand::thread_rng();
  let width = rng.gen_range(4..=MAX_WIDTH);
  let height= rng.gen_range(4..=MAX_HEIGHT);

  let step = (rng.gen_range(1..width),rng.gen_range(1..height));

  let mut map:Vec<Vec<u32>> = Vec::new();
  for x in 0..=width{
    let mut columns:Vec<u32> =Vec::new();
    for y in 0..=height{
      let tile_type:u32 = match (x,y) {
        (x,y) if (x,y) ==step => 1, // (_,_)if (x,y)==stepと同じ
        (0,_)|(_,0) => 2,
        (x,_) if x ==width => 2,
        (_,y) if y ==height => 2,
        (_,_) => 0,
      };
      columns.push(tile_type);
    }
    map.push(columns);
  }
  map
}

テスト時のprintln!マクロ結果はcargo test -- --nocaptureで表示できる。

map_test.rs
#[cfg(test)]
#[test]
fn test_map_check(){
  let mapped = map();
  for x in mapped.iter(){
    for y in x.iter(){
      print!("{} ",*y);
    }
    println!("");
  }
}
// 生成例
2 2 2 2 2 2 2 2 2 2 
2 0 0 0 0 0 0 0 0 2 
2 0 0 0 0 0 0 0 0 2 
2 0 0 0 1 0 0 0 0 2 
2 0 0 0 0 0 0 0 0 2 
2 2 2 2 2 2 2 2 2 2 

かんがえる

壁or床の二択なら0or1のビット列で表現できるので、階段生成は別の関数に分離するべきようにおもう。まあ今はこのままで。

matchで左辺に変数名を入れたとき新しい変数として宣言される挙動に少し手間取った。

描画

rustでfor文を回しつつ添字番号もついでにほしい場合、.iter().enumerate()でイテレータとカウンタ番号が取り出せる。

map_setup.rs
fn map_setup(
    mut commands:Commands,
    texture_atlas_handle: Res<TileAtlasHandles>,
  ){
    let map_data =map_make();
    for (x,vec) in map_data.iter().enumerate() {
        for (y,item) in vec.iter().enumerate(){
  
            let position = Vec2::new(x as f32, y as f32);
            let tile_size = Vec2::splat(32.0);
            commands.spawn_bundle(SpriteSheetBundle{
                texture_atlas: texture_atlas_handle.handle.as_weak(),
                transform: Transform{
                    translation : (position * tile_size).extend(0.0),
                    rotation : Quat::from_rotation_z(0.0),
                    scale : Vec3::splat(2.0),                    
                },
                sprite: TextureAtlasSprite{
                    index : *item as usize,
                    ..Default::default()
                },
                ..Default::default()                
            })
            .insert(match item {
                2 => TileType::Wall,
                _ => TileType::Floor,
            });
        }
    }
  }

ちゃんとしたマップチップを用意しよう

amanitasamanitas

1/31

マップチップ自作した


ドット絵制作は苦手意識が強かったのだが、無理に使い慣れないドット絵専用ソフトをつかて描こうとしたのが良くなかっただけで普通に描けば普通に完成した。一気にダンジョンっぽくなりモチベが上がって良い感じ。

Roguelike Tutorial - In Rust

Rustでローグライクゲームを作るチュートリアルの本があった。存在した。とりあえずこれを読みつつBevyで自分なりに実装していこう。RLTKも気になるけれど...

amanitasamanitas

2/1

キー入力を受け取る

Bevyでキーボード入力を受け取る方法は以下の通り

move_input.rs
fn key_input(keyboard_input: Res<Input<KeyCode>>)
{
  if keyboard_input.just_pressed(KeyCode::Up){
    println!("pressed Up");
 }
}

KeyCode::/*キーネーム*/が押された場合trueを返す。この関数の中にプレイヤーの位置を操作する処理を書いても良いが、なるべくシステムは小分けにしたいので最終的にはこのようにした

move_input.rs
fn player_move_input(
    keyboard_input: Res<Input<KeyCode>>,
    mut move_event: EventWriter<MoveEvent>, 
){
    if !keyboard_input.is_changed(){
        return;
    }else if keyboard_input.just_pressed(KeyCode::Left){
        move_event.send(MoveEvent(Dimension::Left));
    }else if keyboard_input.just_pressed(KeyCode::Right){
        move_event.send(MoveEvent(Dimension::Right));
    }else if keyboard_input.just_pressed(KeyCode::Up){
        move_event.send(MoveEvent(Dimension::Up));
    }else if keyboard_input.just_pressed(KeyCode::Down){
        move_event.send(MoveEvent(Dimension::Down));
    }
}

有効なインプットがあればMoveEventを発生させ、その中に方向を示す列挙型を入れる。

プレイヤーの移動方向を決定する

MoveEventとプレイヤーの位置を受け取り、次の位置を仮決定する。

next_position.rs
fn player_next_position(
  mut move_event: EventReader<MoveEvent>,
  query: Query<&Transform,With<Player>>,
  mut next_pos: ResMut<NextPos>,    
){
  for mv in move_event.iter(){
    let x_move = match mv.0{
      Dimension::Left => -1.0*TILE_WIDTH,
      Dimension::Right => 1.0*TILE_WIDTH,
      _ => 0.0,
    };
    let y_move = match mv.0{
      Dimension::Up => 1.0*TILE_HEIGHT,
      Dimension::Down => -1.0*TILE_HEIGHT,
      _ => 0.0,
    };
    let transform = query.single();
    let trans = transform.translation;
    next_pos.x=Some(trans.x+x_move);
    next_pos.y=Some(trans.y+y_move);
    return;
  }
}

もし進行先に壁がある場合、プレイヤーを移動させてはならないのであくまで仮決定。
ひとまずリソースとして次の位置を書き込んでおく。

進行先のタイルを取得

next_tile.rs
fn next_tile(
  query: Query<(&Transform,&TileType)>,
  next_pos: Res<NextPos>,
  mut next_tile: ResMut<NextTile>,
){
  if next_pos.x ==None || next_pos.y ==None{
      return
  }

  let next = (next_pos.x.unwrap(),next_pos.y.unwrap());
  for (tile_pos,tile_type) in query.iter(){
    if (tile_pos.translation.x,tile_pos.translation.y)== next {
      next_tile.0 = match tile_type{
        TileType::Floor => Some(TileType::Floor),
        TileType::Step => Some(TileType::Step),
        TileType::Wall => Some(TileType::Wall),
      };
      return          
    }
  }
}

次のプレイヤーの位置と等しいタイルの位置を調べ、そのタイルを返す。

用意した情報を元にプレイヤーを移動させる

move_system.rs
fn player_move_system(
    mut query: Query<&mut Transform,With<Player>>,
    mut step_ev: EventWriter<StepOnEvent>,
    next_pos: Res<NextPos>,
    next_tile: Res<NextTile>,
){
  let next = match next_tile.0{
    Some(TileType::Wall) => TileType::Wall,
    Some(TileType::Floor) => TileType::Floor,    
    Some(TileType::Step) => TileType::Step,
    _ => return,        
  };
  match next{
    TileType::Wall =>{
      return
    },
    TileType::Floor =>{
      let mut transform = query.single_mut();
      let translation =&mut transform.translation;
      translation.x =next_pos.x.unwrap();
      translation.y =next_pos.y.unwrap();
      return
    }
    TileType::Step =>{
      let mut transform = query.single_mut();
      let translation =&mut transform.translation;
      translation.x =next_pos.x.unwrap();
      translation.y =next_pos.y.unwrap();
      step_ev.send(StepOnEvent(true));
      return
    }
  }
}

壁ならば移動処理を行わず早期リターン、床か階段なら移動する。階段の場合はStepOnEventを発生させる。

階段イベントは未実装なので貧相な処理
step_on.rs
fn step_on(mut step_ev: EventReader<StepOnEvent>){
  for ev in step_ev.iter(){
    if ev.0 {
        println!("Player on the Step");
    }else{
        return
    }
  }
}

リソースリセット

後片付けもしておく

resource_reset.rs
fn resource_reset(
  mut next_pos: ResMut<NextPos>,
  mut next_tile: ResMut<NextTile>,
){
  next_pos.x =None;
  next_pos.y =None;
  next_tile.0 =None; 
}

システム実行順序をラベル付け

実行順序を支持しないと移動できたりできなかったりするので、ラベルで指示。

player_plugin.rs
pub struct PlayerPlugin;
impl Plugin for PlayerPlugin{
  fn build(&self,app: &mut App){
    app
      .add_startup_system(player_setup)
      .insert_resource(NextPos{x:None,y:None})
      .insert_resource(NextTile(None))      
      .add_event::<MoveEvent>()
      .add_event::<StepOnEvent>()
      .add_system(player_move_input.label("input"))
      .add_system(player_next_position.label("pos_set").after("input"))
      .add_system(next_tile.label("set_end").after("pos_set"))
      .add_system(player_move_system.label("move").after("set_end"))
      .add_system(player_move_animate.label("move").after("set_end"))
      .add_system(step_on.label("move_end").after("move"))
      .add_system(move_res_reset.after("move_end"));
  }
}

MoveイベントではなくMoveStateを発生させるほうが良いのかも。

amanitasamanitas

2/2

テスト駆動開発についての本を呼んだりしつつ、rustのtestを使いこなせるように練習。

プレイヤーの開始位置を決定する

0/1/2の二次元配列から床の二次元座標タプルをベクタにし、ベクタのサイズで乱数を降る

player_spon.rs
fn player_pos(map: &Vec<Vec<u32>>)->(usize,usize){
   let mut floors: Vec<(usize,usize)> = Vec::new();
   for (x, vec) in map.iter().enumerate(){
     for (y, item) in vec.iter().enumerate(){
      if *item == 0 {
        floors.push((x,y));
      }
    }
  }

  //実際のプレイヤー位置決定処理はここから
  let mut rng = rand::thread_rng();
  let pos_index = rng.gen_range(0..floors.len());
  let pos = floors[pos_index];
  return pos
}

床のx,y座標のタプル配列はリソース化しても良さそうな感じがする。タイルエンティティ作成は多分時間がかかるので(?)元のマップデータの二次元座標を用意する時点で作っておくべきなのかも。

エネミー位置

ついでにエネミーの数も乱数で決める。ただ、現状のコードでは位置の重複を取り除いていない。

enemys_pos.rs
fn enemys_pos.rs(floors:&Vec<(usize,usize)>) ->Vec<(usize,usize)>{
  let mut rng = rand::thread_rng();
  let enemys = rng.gen_range(1..MAX_ENEMY);
  let mut enemys_pos:Vec<(usize,usize)> = Vec::new();
  loop {
    let pos_index = rng.gen_range(0..floors.len());
    let pos = floors[pos_index];
    enemys_pos.push(pos);
    if enemys_pos.len() == enemy_spon {
      break
    }
  }
  enemys_pos
}

手っ取り早く重複を取り除くなら、ベクタではなくハッシュセットが多分良い。

amanitasamanitas

2/3

プレイヤーの移動とカメラの移動を連動させる

本当はプレイヤーとカメラの間にparent/children関係を作るのが良いのだが、ちょっと難しかったのでプレイヤーが移動したらカメラも移動させるシステムを作成した。

player_camera.rs
#[derive(Component)]
struct PlayerCamera;

fn camera_setup(mut commands: Commands){
      commands.spawn_bundle(OrthographicCameraBundle::new_2d())
            .insert(PlayerCamera);
}

fn player_camera(
  mut camera: Query<&mut Transform,(With<PlayerCamera>,Without<Player>)>,
  player: Query<&Transform,With<Player>>,
){
  let player = player.get_single();
  if let Ok(player_pos) = player{
    let next_pos = player_pos.translation;
    let mut camera = camera.single_mut();
    camera.translation.x = next_pos.x;
    camera.translation.y = next_pos.y;
  }
}

システム関数で別々のクエリを取得したい場合、Withoutでうまく競合しないようにする必要がある。

カメラ移動システムの登録

ラベル付けを行い、プレイヤーの移動処理が終わった後にカメラの移動処理を行うようにする。

system.rs
app.add_system(player_move_system.label("move"))
     .add_system(player_camera.after("move"));

この順番でないと、プレイヤーが移動した時に一瞬1マス先に進む残像がチラつく。

実例

ローグライクてきなゲームのダンジョン処理フローを考える

  1. プレイヤーがダンジョンを選ぶ
  • ダンジョンデータをロード
  • ダンジョン名,難易度,エネミーリスト,アイテムリスト,使用マップタイル,...
  1. フロア初期処理
    2.1. マップデータ配列を作成(リソース)
    • マップ描画(エンティティ作成)
    • 床配列を作成(リソース)
      • プレイヤー初期位置を生成
        • プレイヤー描画(エンティティ作成)
      • エネミー数/位置/種類を生成
        • エネミー描画(エンティティ作成)
      • アイテム数/位置/種類を生成
        • アイテム描画(エンティティ作成)
  2. ターン処理
    3.1. プレイヤーの行動
    • 方向キーならばMoveEvent
      • 移動先が階段ならばターン終了時にStepEvent
      • 移動先がアイテムならばターン終了時にItemEvent
    • 攻撃キーならばAttackEvent
      • 攻撃先にエネミーがいるならばダメージ処理
      • 攻撃先のエネミーが倒れたら経験値処理
    • 脱出の場合はDungeonExitEvent
      3.2. エネミー1..xの行動
    • 攻撃
    • 徘徊
    • 逃走
      3.3. ターン終了時処理
    • StepEventならば現在フロア終了処理
    • ItemEventならばアイテムを獲得する
    • 3.ターン処理の先頭に戻る
  3. フロア終了処理
    - inputシステムをロック
    - 画面を非表示(カメラ消去?)
    - アイテム・エネミーエンティティを削除
    4.1. StepEventならば現在階層を+1してフロア初期処理の先頭へ
    4.2. DungeonExitEventならば終了

実装を考える

まず、ダンジョン内のみの特殊なシステムが多いことから,StateとしてDungeonモードを実装できそう

dungeon.rs
pub struct DungeonPlugin;
impl Plugin for DungeonPlugin{
  fn build(&self,app: &mut App){
    app
      .add_state(GameState::WorldMap)
      .add_system_set(
        SystemSet::on_update(GameState::WorldMap)
          .with_system(dungeon_select) //この関数がGameState::Dungeonに変更する
      )
      .add_system_set(
        SystemSet::on_exit(GameState::WorldMap)
          .with_system(data_save) //ダンジョン生成に必要な情報などをリソース化
      )
      .add_system_set(
        SystemSet::on_enter(GameState::Dungeon)
          .with_system(dungeon_data_load) //上記で用意された情報を読み込む
      )
      .add_system_set(
        SystemSet::on_update(GameState::Dungeon)
          .with_system(...)
      )
      .add_system_set(
        SystemSet::on_exit(GameState::Dungeon)
          .with_system(data_save) //ダンジョン終了処理
      )
      .add_system(...);
  }
}

on_updateの中ではフロア初期化→ターン処理→フロア終了処理→初期化のループを行いたいが、on_updateの中に初期化処理を入れてしまうと毎フレーム初期化してしまう。

Stateのスタックで解決できるはず。

こんなかんじ?

dungeon.rs
pub struct DungeonPlugin;
impl Plugin for DungeonPlugin{
  fn build(&self,app: &mut App){
    app
      .add_state(GameState::WorldMap)
      .add_event::<Event>() //色んなイベント
      .add_system_set(
        SystemSet::on_update(GameState::WorldMap)
          .with_system(dungeon_select)
      )
      .add_system_set(
        SystemSet::on_exit(GameState::WorldMap)
          .with_system(data_save)
      )
      .add_system_set(
        SystemSet::on_enter(GameState::Dungeon) // 初回起動
          .with_system(dungeon_data_load.label("data_set"))
          .with_system(floorstate_push.after("data_set")) //FloorをPush
      )//Dungeonの上にFloorステートが乗るイメージ
      .add_system_set(
        SystemSet::on_enter(GameState::Floor)
          .with_system(map_make) //データをつくって
          .with_system(free_floor)  //床配列をつくって
          .with_system(pos_set)  //いろんな初期位置を決めて
          .with_system(map_draw) //色々描画(エンティティ作成)
      )
      .add_system_set(
        SystemSet::on_update(GameState::Floor)
          .with_system(player_input)//基本はプレイヤーの入力待ち
          .with_system(step_on_event)//
          .with_system(dungeon_exit_event)
    //この2つのイベントがFloorやDungeonを終了するトリガーになる
    //Dungeonステートは消えているわけではないので、受信できるはず...
      )
      .add_system_set(
        SystemSet::on_exit(GameState::Floor)
          .with_system(floor_data_save)
      )
      .add_system_set(
   //WorldMapステートに戻すか,またFloorステートをpopするか
  // on_resumeはスタックで非アクティブから復帰する時に一度だけ呼ばれる
        SystemSet::on_resume(GameState::Floor)
          .with_system(next_state)
      )
      .add_system_set(
        SystemSet::on_exit(GameState::Dungeon) //終了処理
          .with_system(dungeon_data_load.label("data_set"))
      );
  }
}

bevyのテストでworldを自作すればなんやかんやテスト出来るらしいが、ちょっと難しかったので(いつもテスト難しいって言ってるな)もうちょっと勉強。

amanitasamanitas

2/4

TOMLを学ぶ

TOML.io
TOMLファイルの書き方をそろそろ学ぼうと思ったので学びます(jsonファイルで設定記述したくない民なので)

bevyのドキュメントを読み続けたおかげで公式サイトの情報が英語だけど読める……読めるぞ!となった。読み終わった後に右上で日本語表示に切替可能であることに気がついて泣いた。

コメント

プログラミング言語のコメントはだいたい//とか#でTOMLはシンプルなので#一個で良い。

# comments

文字列

一行ならば"ダブルクオーテーションで囲む"
複数列ならば"""ダブルクオーテーション3個で囲む"""

basic_strings = " string "
many_strings = """
strings
many strings
str str str"""

最初の"""直後の改行は無視される。
基本的なエスケープシーケンス\nとかは上記で有効なので、ファイルパスなどは'シングルクオーテーションで囲む'
改行したい場合は'''シングル3つで囲む'''

file_path = '\file\hoge\huges'
non_escape= '''
エスケープしない以外は"""で囲むのと同じ
'''

変数宣言

基本形は変数名 = 数値リテラルまたは文字列

fileversion = 1
filename = "test toml"
saved_value = 0.01
is_valid = true 

プログラミングにある基本的な数値型はだいたいある。

配列

配列の中ではコンマの存在が許される
配列を持つ配列も許される。
何なら配列の中の要素型が異なっていても許される

nanikano_array = [ 0, 0, 0 ]
string_array = [
  "string1",
  "string2", #このコンマは書いても書かなくても許される
]
float_array = [
  0.1, 0.2, 0.3, #配列の中なので許される
]

テーブル

変数をまとめる。つぎのテーブルが宣言されるまでの範囲をそのテーブルと紐付ける。
あと変数A.変数Bという書き方でも変数Aに関連する変数Bみたいな関連付けが可能。

[player]
name = "Kinoko"
level = 1
hp = 10

[enemy]
name = "Takenoko"

これは以下と大体同じっぽい?

player.name = "Kinoko"
player.level = 1
player.hp =10

enemy.name ="Takenoko"

インラインテーブル

テーブルやドットだと冗長になるが一行で表せそうな時はこれで書くとシンプル。最後の要素にコンマをつけることは許されない。

player = {name ="Kinoko", level =1, hp =10 }

Rustのtoml関連クレート

Taplo

TOML公式WIKIに記載がある、TOML最新版(v1.0.0)に対応しているパーサー

toml-rs

対応バージョンは0.5、serdeとの連携ができるらしい

amanitasamanitas

Bevy0.7 リリースノートを読む

https://bevyengine.org/news/bevy-0-7/
最近プログラミングさぼってお絵かきばっかりやってたので脳をRustに戻す

システム順序付けが簡単になった

ラベルとして関数名を打ち込めるようになったのでいちいちラベル名を考えなくて良くなった

例えばダンジョンのマップを生成するシステムを作るとき、
タイルチップを読み込む処理の後にタイルを並べてマップを生成する、
みたいな順番で処理をしてほしかったりするので0.6ではこんな感じに書いた

//0.6
pub struct MapPlugin;
impl Plugin for MapPlugin{
    fn build(&self, app: &mut App){
        app
          .add_startup_system(tile_load.label("load"))
          .add_startup_system(map_setup.after("load"))
}

Bevy0.7では.after(/*関数名*/)で順序付けが出来るようになった

//0.7
pub struct MapPlugin;
impl Plugin for MapPlugin{
    fn build(&self, app: &mut App){
        app
          .add_startup_system(tile_load)
          .add_startup_system(map_setup.after(tile_load))
}

簡単でスッキリしたね

構造体をデフォルト値で初期化するのがひたすら楽になった

今まで通りに描いても良いがさらに短くなった

//0.6
 commands.spawn_bundle(sumple_bundle{
  ..Default::default()
})

//0.7
commands.spawn_bundle(sumple_bundle{
  ..default()
})