「The Impatient Programmer's Guide to Bevy and Rust: Build a Video Game from Scratch」シリーズの第1章です。 コミュニティに参加して、このシリーズの新しいリリースについて更新してください。この章のソースコードはこちらで入手できます。 コミュニティに参加 利用可能 ここ At the end of this chapter, you’ll be able to achieve the results in the GIF below: 指示 セットアップ セットアップ あなたが設定していない場合 それでも、どうぞフォローしてください。 を設定するため。 リラックス 公式ガイド Create a fresh project: cargo new bevy_game cd bevy_game Open and add Bevy: Cargo.toml [dependencies] bevy = "0.16.1" 私はあなたがJavaScriptやPythonのような1つの言語でプログラミングの経験を持っていると仮定します。 システムで考える プレーヤーがキーボードの入力から移動できるシンプルなゲームを構築するために必要なものは何ですか? 世界システム:プレイヤーが移動できるゲームの世界を作成します。 入力システム:キーボードの入力を監視し、プレイヤーの動きに翻訳します。 上記のパターンは、ゲームの構築、作成/セットアップのパターン、ゲームの状態の変化を監視し、ゲーム世界のエンティティを更新するパターンで非常に一般的です。 セットアップ:異なる行動で敵をカバーし、敵のスプリットやアニメーションをロードし、敵の健康と損傷の値を設定します。 アップデート:敵をプレイヤーに向かって移動し、敵が弾丸で撃たれるかどうかをチェックし、死んだ敵を取り除き、間隔で新たな敵を生み出す。 ベビーのコアでは、このシンプルな この基本的なパターンは、Bevyを強力で理解しやすいものにします - 一度このコンセプトを把握すると、ビビーで構築が容易になります。 setup and update system setupと呼ばれる関数を想像して、議論として「コマンド」を与え、あなたが望むものを創造または生成する能力をあなたに与えます。 // Pseudo code, doesn't compile fn setup(mut commands: Commands) { // Create anything you want commands.spawn() } What’s mut? Rust では、この値を変更する予定であることを明示的に言及する必要があります。 Rust は、この値を変更する予定です。Rust は、この知識を使用して、バグの全クラスを防止します。 mut mut ♪ ? And what’s this mut commands: Commands それは、あなたがあなたのゲームの世界にものを追加することを可能にする bevy ライブラリからです。 このパラメータが Bevy のコマンド インターフェイスであることをコンパイラに知らせる. Skip the hint and Rust can not be sure what appears, so it blocks the build. :Commands What’s a type? A type tells Rust what type of value you are handling—number, text, timers, Bevy commands, and so on. 一旦コンパイラがその type を知ると、あなたがそれに実行するすべての操作が実際に意味があることを確認できます。 Isn’t this too much work? エラーを防止し、パフォーマンスをサポートします:例えば、数字に文字列を追加しようとすると、コンパイラはゲームが実行される前にあなたを停止します。 A Rust は 2 つの数字しかありませんので、Rust は 2 つの数字を正確に保存し、何の驚きもなく、何千ものゲームエンティティを持っているときに物事を速く保ちます。 Vec2 Vec2 オブジェクトを JavaScript または Python で作成すると、2 つの数字だけを保存するのではなく、タイプメタデータ、プロパティ情報、プロトタイプ参照を追加し、シンプルな 8 バイトのデータ構造を ~48 バイトのメモリに変換します。 Rustのタイプシステムはコンパイルタイムで動作します。あなたが2つのf32フィールドを持つVec2構造を宣言するとき、それはちょうど8バイトだけ、追加のメタデータはありません。コンパイラはすでに種類を知っているので、ランタイムタイプ情報は必要ありません。 1000のゲームエンティティで、この違いは劇的になります。 Language Memory Usage Overhead Rust 8KB None Dynamic language ~48KB+ 6x overhead リラックス 8KB 誰も ダイナミック言語 48KB+ 6xオーバーヘッド これはメモリの使用だけでなく、パフォーマンスの問題です。より小さい、予測可能なメモリレイアウトはより良いCPUキャッシュ利用を意味し、これはゲームで何千ものエンティティを各フレームで処理しているゲームでより高速なフレームレートに直接転換します。 カメラをセットアップ What should we setup first? 世界はデータの中に存在するかもしれないが、カメラは実際に何が描かれるかを決定する。 //Pseudo code, don't use this yet fn setup(mut commands: Commands) { commands.spawn(Camera2d) } What’s Camera2d? ベビーの内蔵の2Dカメラバンドルです. Spawn it and you get a ready-to-go view of your 2D scene. Camera2d What’s a bundle? バンドルは、あなたがしばしば一緒に産むコンポーネントのグループにすぎません. For example, Bevy ships a that packs position, texture, color tint, and visibility; spawning that bundle gives a sprite entity in one call. ポジション、テクスチャー、色の色彩、および可視性をパッケージします。 SpriteBundle 設定機能の登録 We need to register our setup function with bevy to be triggered on Startup. 私たちは、Startupで起動するためのベビーで設定機能を登録する必要があります。 あなたの更新 次のコードで src/main.rs // Replace your main.rs with the following code. use bevy::prelude::*; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .run(); } fn setup(mut commands: Commands) { commands.spawn(Camera2d); } What’s bevy::prelude? スタートアップキットのように、ゲームを構築するときに絶え間なく手に入れるもの。 で、 コンポーネント、数学アシスタント等 bevy::prelude App Commands What’s App::new()…? ゲームアプリを作成し、 Loads rendering、input、audio、その他のシステム スタートアップ機能を登録し、そして、 コントロールはBevyのメインループにします。 App::new() add_plugins(DefaultPlugins) add_systems(Startup, setup) run() Why register a startup function? What does it do? スタートアップシステムは、最初のフレームの前に一度実行されます。カメラ、プレーヤー、および窓を開くと存在すべき他のものをセットアップするためにそれらを使用します。 What’s bevy main loop? 起動後、Bevyは主なループに入ります:入力を調査し、システムを実行し、世界を更新し、フレームを表示します。 走ろう 走ろう cargo run 空の画面? はい、私たちはカメラをセットアップしたばかりで、今度はプレーヤーを追加しましょう。 プレイヤーをセットアップ 今すぐプレイヤーを作ろう! 覚えておきましょう! そして ベビーでは、すべてが一つの 同 そういうシステムで働ける。 Setup System Update System Entity Components エンティティをユニークなID(ソーシャルセキュリティ番号など)として考え、コンポーネントをそれに付属するデータとして考えましょう. プレイヤーにとっては、移動速度、健康、または特別な能力のようなコンポーネントが必要です. Rust では、これらのコンポーネントを作成する最適な方法は、 . struct 鉄のコアビルドブロックの1つです. 同様のデータを組み合わせています. ここでは空白を宣言します. struct で、そのタイプ自体がタグとして機能するので、プレイヤーのエンティティに付加することができます. Later we can add things like player health. struct Player // Place this before main function in main.rs #[derive(Component)] struct Player; Why tag? Tag marks an entity for later lookup. なぜなら 私たちのヒーローだけに付属している場合、システムはBevyに「プレーヤータグを持つエンティティを私に与えてください」と尋ねることができ、そのエンティティティと共に働くことができます。 Player What’s this #[derive(component)]? この構造にコンポーネントマクロコードを付加するように Rust に言います. A macro is Rust's way of generating pre-defined template code for you. Bevy が必要とするボイラープレートを自動的に注入するので、それを保存し、探すことができます。 エンティティは、同じコードをどこにでもコピーするのを防ぎ、シリーズの後半にマクロを詳しく見ていきます。 derive #[derive(Component)] Player What’s a component, and why should the player be a component? A component is a piece of data attached to an entity. Position, speed, health, and even the idea of “this is the player” all live in components. 位置、スピード、健康さえ、「これはプレーヤーだ」というアイデアさえ、すべてコンポーネントで生きている。 このコンポーネントは、後でそのエンティティについて尋ねることができ、より多くのコンポーネント(健康や在庫など)を追加し、Bevyのシステムが更新する必要があるエンティティを正確に選ぶことができます。 Player 今のところ、私たちは「@」シンボルを使用してスクリーン上で私たちのキャラクターを表します。 // Replace existing setup function in main.rs with the following code fn setup(mut commands: Commands) { commands.spawn(Camera2d); // Code Update Alert // Append the following lines to your setup function. commands.spawn(( Text2d::new("@"), TextFont { font_size: 12.0, font: default(), ..default() }, TextColor(Color::WHITE), Transform::from_translation(Vec3::ZERO), Player, )); } この単一呼び出しは、表示したいテキスト、そのフォント設定、色、位置、および エンティティを識別するタグ commands.spawn(( ... )) Player What’s a tuple? tuple は、パネルで書かれた値の順番のリストです. Rust は各ポジションを追跡します。 構造を作成する必要なしに2つの値を隣接的に保持します。 (Text2d::new("@"), TextColor(Color::WHITE)) Whats an entity? Entity は、Bevy がコンポーネントを結びつけるために使用するユニークな ID です. By itself it holds no data, but once you attach components to it, that ID represents something in your game world. エンティティは、コンポーネントにコンポーネントを結びつけると、その ID はゲームの世界で何かを表します。 各バンドルは、ユニークなIDとリストされたコンポーネントを含む新しいエンティティを生成し、このように描くことができます。 Entity Components it carries #42 Camera2d #43 , , , , Text2d("@") TextFont TextColor Transform Player #42 Camera2d #43 , , , で、 Text2d("@") TextFont TextColor Transform Player 列がフラッシュすると、それらの実体は世界に住み、システムが彼らが持っているタグ(コンポーネント)によってそれらを発見する準備ができています。 Implementing Player Movement さて、私たちの創造を プレイヤーの動きのために!プレイヤーを動かすために必要なことを考える: Update System キーボード入力 - どのキーを押すかを知る Time - to make movement smoothly regardless of frame rate. フレームレートに関係なく動きをスムーズにします。 - to actually move the player Player position 他のゲームエンジンでは、これらのシステムを手動で接続するのに時間を費やしますが、Bevyの魔法はここにあります - あなたはあなたの機能パラメータで必要なものを求めるだけで、Bevyは自動的にそれを提供します! 移動プレーヤーの機能を書きましょう。 // Append this code to main.rs fn move_player( // "Bevy, give me keyboard input" input: Res<ButtonInput<KeyCode>>, // "Bevy, give me the game timer" time: Res<Time>, // "Bevy, give me the player's position" mut player_transform: Single<&mut Transform, With<Player>>, ) { let mut direction = Vec2::ZERO; if input.pressed(KeyCode::ArrowLeft) { direction.x -= 1.0; } if input.pressed(KeyCode::ArrowRight) { direction.x += 1.0; } if input.pressed(KeyCode::ArrowUp) { direction.y += 1.0; } if input.pressed(KeyCode::ArrowDown) { direction.y -= 1.0; } if direction != Vec2::ZERO { let speed = 300.0; // pixels per second let delta = direction.normalize() * speed * time.delta_secs(); player_transform.translation.x += delta.x; player_transform.translation.y += delta.y; } } What’s Res? Res または Resources は、ゲーム全体の情報の一部であり、単一のエンティティに関連付けられていない。 ゲームのマスター時計を表示するので、各システムは同じ「前回のフレーム以来の時間」値を読み取ります。 Res<Time> Explain Single<&mut Transform, With>? asks Bevy for exactly one entity that has a コンポーネントも持ち歩くし、 タグ: THE 一部は、その変換またはプレイヤーの位置を変更することを意図していることを意味します(設定機能に変換コンポーネントを追加したことを覚えておいてください)。 Single<&mut Transform, With<Player>> Transform Player &mut Transform What’s Vec2::ZERO? is a two-dimensional vector with both values set to zero: キーボードの入力を読む前にスタート方向として使用します。 Vec2::ZERO Vec2 { x: 0.0, y: 0.0 } What’s this KeyCode::… pattern? で、 ... enums (will cover enums later) that represent specific keys on the keyboard. キーボード上の特定のキーを表します。 単に、このキーが現在のフレーム中に保持されているかどうかをBevyに尋ねます。 KeyCode::ArrowLeft KeyCode::ArrowRight input.pressed(KeyCode::ArrowLeft) 我々はゼロ方向を無視するので、プレーヤーはキーが押されていないときに立ち止まります。 ベクターを長さ1に変換するので、直線の動きは直線の動きよりも速くない。 1秒間にどれだけのピクセルを動かすか、そして、 returns the frame time—the number of seconds since the previous frame—so multiplying them gives the distance we should travel this update. Finally we add that delta to the player’s transform translation to move the sprite on screen. normalize() speed time.delta_secs() 移動システムの登録 // Update main function fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(Update, move_player) // Line update alert .run(); } Why is move_player added as Update? What’s Update? あらゆるフレームを実行するので、私たちはそれを接続します。 日程 Bevyの組み込みステージは、スタートが完了した後、ゲームループごとに1回点火します。 move_player Update Update So does systems mean functions we want bevy to execute at a particular time, like initial setup or on every game loop update? A system is just a Rust function you hand to Bevy with a sticky note that says “run me during Startup” or “execute me on every Update,” and the engine dutifully obeyes. システムは単にRust機能で、Bevyに「スタートアップ中に私を走らせる」または「毎回のアップデートで私を走らせる」という粘着したメモを渡します。 走りましょう。 Sprite Graphicsの追加 We will use the Universal LPC SpriteSheet Generator to give our character some personality. You can remix body parts, clothes, and colors at 完全なスプレッドシートを輸出します。 this link このプロジェクトでは、スプリットシートはすでに、 . Drop the provided image files into ゲームが始まるときにBevyがそれらを見つけることができます。 directory inside the src folder. レポ src/assets assets Refactoringコード 我々はより多くのコードを追加する必要があり、 main.rs に追加すると読み取りや理解が困難になります。 main.rs ファイルを次のよう更新し、また別の player.rs ファイルを作成します。 Pulls in the このようにコードを分割すると、1 つの大きなファイルがすべてを行うことなくプロジェクトを成長させやすくなります。 mod player; player.rs //main.rs use bevy::prelude::*; mod player; fn main() { App::new() .insert_resource(ClearColor(Color::WHITE)) // We have updated the bg color to white .add_plugins( DefaultPlugins.set(AssetPlugin { // Our assets live in `src/assets` for this project file_path: "src/assets".into(), ..default() }), ) .add_systems(Startup, setup_camera) .run(); } fn setup_camera(mut commands: Commands) { commands.spawn(Camera2d); } グローバル設定をアプリに落とすので、各フレームは白い背景で始まります。 .insert_resource(ClearColor(Color::WHITE)) is Bevy’s loader for textures, audio, and other assets. We tweak its 内側に見えるように , which is where our sprites live. AssetPlugin file_path src/assets Building the Player Module 私たちの基本的なアプリ構造が整った今、私たちのキャラクターを生かす時が来ました! 私たちは専用のアプリを作成します。 このモジュールは、私たちの小さな冒険者に関連するすべてのことを処理します。これは私たちのコードを整理し、後により多くの機能を追加することを容易にします。 player.rs このモジュールは、プレイヤーを動かし、アニメーションし、世界と相互作用させるすべての常数、コンポーネント、システムを保持します。 Player.rs で次のコードを追加する // player.rs use bevy::prelude::*; // Atlas constants const TILE_SIZE: u32 = 64; // 64x64 tiles const WALK_FRAMES: usize = 9; // 9 columns per walking row const MOVE_SPEED: f32 = 140.0; // pixels per second const ANIM_DT: f32 = 0.1; // seconds per frame (~10 FPS) #[derive(Component)] struct Player; // Moved from main.rs to here これらの常数は、スプリットシートと動きの数学のために再利用する数値に名前を与えます: テーブルのサイズ、行当たりのフレーム、歩行速度、およびアニメーションがどのように速く進むか。 marker here keeps all player-specific types in one module. ここでは、すべてのプレイヤー特定のタイプを1つのモジュールに保持します。 Player Defining Player Directions // Append these lines of code to player.rs #[derive(Component, Debug, Clone, Copy, PartialEq, Eq)] enum Facing { Up, Left, Down, Right, } What’s an enum? A enum lists a handful of permitted values. Our character only ever faces four directions. 私たちのキャラクターは常に4つの方向に直面します。 enum は、これらのオプションを 1 か所でキャプチャします。 Facing Why can’t I use a struct here? A struct would need a bunch of booleans like ブーレンの群れのような構造が必要になります。 で、 , and you would have to keep them in sync. That makes it complicated. Enum guarantees you only pick one direction. それはそれを複雑にします。 facing_up:true facing_left:false When to decide between using enum and struct? 複数のフィールドが必要な場合、例えば、 そして , またはプレイヤーの統計と健康および耐久性を使用して、リストから1つのオプションを選択するときに、例えばプレイヤーが使用するツール(剣、弓、棒)またはどのメニュー画面がアクティブであるかのように、エノムを使用します。 x y Why the purpose of adding Debug, Clone, Copy, PartialEq, Eq macros? lets us print the facing for logs, そして 価値を二重化することを trivialにする、および / 方針を比較する際に平等性チェックを許可します。 Debug Clone Copy PartialEq Eq Why can’t I copy through simple assignment, why should I add these macros? デフォルトでは、Rust はそれらをコピーする代わりに値を移動させるので、コンパイラはあなたをオプトインさせます。 (そして 助手として)は「安い、前進し、それを複製する」と言います。 そして 2つの面を直接比較し、プレイヤーが方向を変える時を検出する方法です。 Copy Clone PartialEq Eq What do you mean by rust moves values, instead of copying? Rust でほとんどの値を割り当てると、古い変数がそれを所有するのをやめるか、つまり死ぬ。 両方の変数を有効にします。 Copy Why does the old variable stop owning or die when assigned? Rust は、それぞれの値が単一の所有者であることを強制し、メモリが安全に解放されるようにします。 This is a bit going over my head! はい、心配しないでください、我々はまだまだ多くの章を持っており、あなたがそれらを通過するにつれて、我々はこれに取り組むでしょう。 アニメーションシステムのコンポーネント 私たちが複数のフレームを持つスプリットシート(9フレームのウォーキングアニメーションのように)を持っているとき、私たちは、これらのフレームがどれだけ速くプレイするかを制御する方法が必要です。 フリップブックのように考える - あなたがページをあまりにもゆっくりと振り回す場合、アニメーションはスムーズに見えます. あなたがあまりにも速く振り回す場合、あなたは何が起こっているかを見ることができません。 このタイミングを正確にコントロールし、キャラクターの歩くアニメーションが正しいスピードで滑らかで自然に見えるようにします。 AnimationTimer タイトル: Wraps A Bevy それぞれのプレイヤーのエンティティは、いつ次のアニメーションフレームに進むべきかを知っています. Each tick represents one slice of time; once the timer hits its interval we move to the next sprite in the sheet. それぞれのキックは時間の1枚を表します。 AnimationTimer Timer // Append these lines of code to player.rs #[derive(Component, Deref, DerefMut)] struct AnimationTimer(Timer); #[derive(Component)] struct AnimationState { facing: Facing, moving: bool, was_moving: bool, } What’s Deref, DerefMut macros doing? Let our wrapper pretend to be the inner それを読んだ時、そして、 同じことが書かれているので、呼び出せばいいのです。 は without manually pulling out the inner value first. Deref Timer DerefMut timer.tick(time.delta()) AnimationTimer So we are renaming the Timer to AnimationTimer? We're wrapping the timer, not renaming it. 考える 持っている小さな箱のように、 , plus a label that says “this one belongs to the player animation.” When we spawn a player we create a fresh それをその箱に入れて、各プレイヤーが複数のヒーローを必要とする場合に独自のタイマーを持ることができるようにします。 AnimationTimer Timer Timer So it’s an instance of AnimationTime? はい、 is a tuple struct は a を含む。 . We build one when we spawn the player so each entity can carry its own timer data. This pattern shows up whenever you want to attach extra meaning to an existing type without writing a brand-new API. AnimationTimer Timer プレイヤーがどの方向にポイントを示しているか、動いているか、動きが始まったばかりか、止まったばかりかを覚えています。 AnimationState プレイヤーをスワイプする We load the spritesheet through the , create a texture atlas layout so Bevy knows the grid, and pick the starting frame for a hero facing down. Then we spawn an entity with the sprite, transform at the origin, our marker components, and the timer that will drive animation. テクスチャー・アトラスのレイアウトを作成して、Bevyがグリッドを知り、ヒーローが下を向くためのスタートフレームを選択します。 AssetServer // Append these lines of code to player.rs fn spawn_player( mut commands: Commands, asset_server: Res<AssetServer>, mut atlas_layouts: ResMut<Assets<TextureAtlasLayout>>, ) { // Load the spritesheet and build a grid layout: 64x64 tiles, 9 columns, 12 rows let texture = asset_server.load("male_spritesheet.png"); let layout = atlas_layouts.add(TextureAtlasLayout::from_grid( UVec2::splat(TILE_SIZE), WALK_FRAMES as u32, // columns used for walking frames 12, // at least 12 rows available None, None, )); // Start facing down (towards user), idle on first frame of that row let facing = Facing::Down; let start_index = atlas_index_for(facing, 0); commands.spawn(( Sprite::from_atlas_image( texture, TextureAtlas { layout, index: start_index, }, ), Transform::from_translation(Vec3::ZERO), Player, AnimationState { facing, moving: false, was_moving: false }, AnimationTimer(Timer::from_seconds(ANIM_DT, TimerMode::Repeating)), )); } スタート方向を設定し、キャラクターが今空っぽであり、空っぽだった最後のフレームを旗にします。 AnimationState { facing, moving: false, was_moving: false } 毎回火をつける繰り返しのストップウォッチを作成します。 スプレッドシートを進めるための2秒。 AnimationTimer(Timer::from_seconds(ANIM_DT, TimerMode::Repeating)) ANIM_DT ? What’s an AssetServer THE Bevyのファイルローダーとマネージャーは、画像、音声、3Dモデルなどのゲーム資産のロードとキャッシュを処理します。 AssetServer あなたが呼ぶとき , it does not immediately load the file into memory. instead, it returns a handle that you can use later. This is called "lazy loading" - the actual file loading occurs in the background, and Bevy will notify you when it's ready. それは後で使用できるハンドルを返します。 asset_server.load("path/to/sprite.png") このアプローチは効果的であるため: 複数のエンティティが同じスプラットを複数回ロードすることなく共有できます。 資産は、実際に必要なときにのみ充電されます。 Bevyはパフォーマンスを向上させるために、バッチロード資産を最適化できます。 資産が充電しない場合、それはあなたのゲーム全体を崩壊させません。 私のケースでは、 spritesheet を要求し、そのロード状態を追跡するためのハンドルを返します。 asset_server.load("sprites/player.png") 移動システム 今、我々のプレーヤーが必要なすべてのコンポーネントを持っているので、我々はプレーヤーが向かっている方向を追跡するために既存の動きシステムを更新する必要があります。これは、スプリットシートのどの行を使用するかを知るためのアニメーションシステムにとって重要です - 各方向(上、下、左、右)は我々のスプリットアトラスで異なる行に対応します。 この更新システムは、動きの方向を検出し、動きを更新します。 したがって、私たちのアニメーションシステムは、各方向の歩行アニメーションのための正しいスプライトラインを選択することができます。 AnimationState // Append these lines of code to player.rs fn move_player( input: Res<ButtonInput<KeyCode>>, time: Res<Time>, mut player: Query<(&mut Transform, &mut AnimationState), With<Player>>, ) { let Ok((mut transform, mut anim)) = player.single_mut() else { return; }; let mut direction = Vec2::ZERO; if input.pressed(KeyCode::ArrowLeft) { direction.x -= 1.0; } if input.pressed(KeyCode::ArrowRight) { direction.x += 1.0; } if input.pressed(KeyCode::ArrowUp) { direction.y += 1.0; } if input.pressed(KeyCode::ArrowDown) { direction.y -= 1.0; } if direction != Vec2::ZERO { let delta = direction.normalize() * MOVE_SPEED * time.delta_secs(); transform.translation.x += delta.x; transform.translation.y += delta.y; anim.moving = true; // Update facing based on dominant direction if direction.x.abs() > direction.y.abs() { anim.facing = if direction.x > 0.0 { Facing::Right } else { Facing::Left }; } else { anim.facing = if direction.y > 0.0 { Facing::Up } else { Facing::Down }; } } else { anim.moving = false; } } The query asks Bevy for the single entity tagged. このクエリは、タグ付けされた単一のエンティティのためのBevyを要求します。 , giving us mutable access to its そして . 我々は圧迫されたキーから方向ベクターを構築し、それを正常化して、直径入力が速くないようにし、プレーヤーを速度×フレームタイムで動かす。 Player Transform AnimationState アニメーション実施 今、我々のプレイヤーはさまざまな方向に移動することができ、我々は彼らが向かっている方向を追跡している。最後の部分は、実際にスプライトフレームを更新して歩くアニメーションを作成するアニメーションシステムです。 このシステムは、私たちの動きシステムから方向情報を取得し、アニメーションタイマーを使用してスプライトフレームを適切な速度でサイクリングします。 // Append these lines of code to player.rs fn animate_player( time: Res<Time>, mut query: Query<(&mut AnimationState, &mut AnimationTimer, &mut Sprite), With<Player>>, ) { let Ok((mut anim, mut timer, mut sprite)) = query.single_mut() else { return; }; let atlas = match sprite.texture_atlas.as_mut() { Some(a) => a, None => return, }; // Compute the target row and current position in the atlas (column/row within the 9-column row) let target_row = row_zero_based(anim.facing); let mut current_col = atlas.index % WALK_FRAMES; let mut current_row = atlas.index / WALK_FRAMES; // If the facing changed (or we weren't on a walking row), snap to the first frame of the target row if current_row != target_row { atlas.index = row_start_index(anim.facing); current_col = 0; current_row = target_row; timer.reset(); } let just_started = anim.moving && !anim.was_moving; let just_stopped = !anim.moving && anim.was_moving; if anim.moving { if just_started { // On tap or movement start, immediately advance one frame for visible feedback let row_start = row_start_index(anim.facing); let next_col = (current_col + 1) % WALK_FRAMES; atlas.index = row_start + next_col; // Restart the timer so the next advance uses a full interval timer.reset(); } else { // Continuous movement: advance based on timer cadence timer.tick(time.delta()); if timer.just_finished() { let row_start = row_start_index(anim.facing); let next_col = (current_col + 1) % WALK_FRAMES; atlas.index = row_start + next_col; } } } else if just_stopped { // Not moving: keep current frame to avoid snap. Reset timer on transition to idle. timer.reset(); } // Update previous movement state anim.was_moving = anim.moving; } // Returns the starting atlas index for the given facing row fn row_start_index(facing: Facing) -> usize { row_zero_based(facing) * WALK_FRAMES } fn atlas_index_for(facing: Facing, frame_in_row: usize) -> usize { row_start_index(facing) + frame_in_row.min(WALK_FRAMES - 1) } fn row_zero_based(facing: Facing) -> usize { match facing { Facing::Up => 8, Facing::Left => 9, Facing::Down => 10, Facing::Right => 11, } } 両方とも結果をチェックし、必要なパーツを名前付けます。 クエリが成功すると、コードが結びつきます。 で、 そして、 それが失敗した場合(プレイヤーなし、または1人以上)、我々は Rust は Rust を使い、Rust は Rust type for this: 「クエリが正確に1つの結果を返した」という意味。 つまり「そのクエリに関して何かが一致しなかった」ということです。 let Ok((mut anim, mut timer, mut sprite)) = query.single_mut() else { return; }; anim timer sprite else Result Ok Err その後、我々 アン ルストの「もしかしたら値があるかもしれない」型です。 テクスチャ・アトラスが存在し、私たちはそれを調整することができます。 つまり、まだロードされていないので、次のフレームに再度試してみるようにします。それは、マップやキャッシュをチェックするときに使用する同じパターンです:検索が何かを返すときにのみ値を使用します。 match Option Some(atlas) None プレーヤーのためのアニメーション状態、タイマー、スプライトハンドルを引っ張ります. それは、アトラスのどの行が現在の面と一致するかを計算し、方向が変わるときにその行にスナップし、タイマーを使用してステップを安定したペースでコラムを通過します. 動きが止まるとき、我々はタイマーをリセットし、アニメーションが表示された最後のフレームに落ちるようにします. ヘルパー機能は正しい行とフレームのインデックスに面をマップし、数学が読み取れるようにします。 animate_player プレイヤープラグインの作成 // Append these lines of code to player.rs pub struct PlayerPlugin; impl Plugin for PlayerPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, spawn_player) .add_systems(Update, (move_player, animate_player)); } } Bevyでは、プラグインは、システム、リソース、および資産を登録する方法を知っている構造にすぎません。 のために , we give Bevy a checklist: whenever this plugin is added to the app, run the code inside to set up everything the player feature needs. This keeps プレイヤー特有の呼び出しのバグになることから。 PlayerPlugin Plugin PlayerPlugin main.rs THE 方法はチェックリストです. Bevy passes us a mutable. そして、私たちは私たちが大切にしているシステムにボルトします。 予定されている in ゲームが始まるとすぐにスプレートが現れます。 そして GO IN THE 各フレームを実行するようにスケジュール - 入力とアニメーションを lockstep で処理します。 入 プレイヤー全体の流れを自動的にスイッチします。 build App spawn_player Startup move_player animate_player Update PlayerPlugin App::new() So this build function is an in-built structure, which I have to write? うーん、The trait says “any plugin must provide a We implement that trait, so Rust expects us to supply the body. Bevy calls this method when it loads the plugin, which is why we add all our systems inside it. 私たちはその特性を実装するので、Rustは私たちに体を供給することを期待しています。 Plugin build(&self, app: &mut App) What’s a trait? A trait is a contract describing what methods a type must provide. Bevy’s trait says “give me a あなたのシステムを登録できるように機能します」その機能を実装することで、 , we hook into Bevy's startup process and inject our own setup code. 特性により、異なるタイプが行動を共有することができます。 他のBevyプラグインと同様に動作しますが、プレイヤー特定のシステムをインストールします。 Plugin build PlayerPlugin PlayerPlugin 最終統合 // Update the main function in main.rs use crate::player::PlayerPlugin; fn main() { App::new() .insert_resource(ClearColor(Color::WHITE)) .add_plugins( DefaultPlugins.set(AssetPlugin { file_path: "src/assets".into(), ..default() }), ) .add_systems(Startup, setup_camera) .add_plugins(PlayerPlugin) // Update this line .run(); } 走りましょう。 cargo run この記事は当初、私のブログに掲載されました。 この記事は当初、私のブログに掲載されました。 より多くの章が来るように、調整しておいてください LinkedIn で私とつながり、最新の更新のために X で私にフォローしてください。 リンク X