Jest to rozdział 1 mojej serii samouczków Rust-Bevy "The Impatient Programmer's Guide to Bevy and Rust: Build a Video Game from Scratch". Dołącz do naszej społeczności, aby zostać zaktualizowanym o nowych wydaniach w tej serii. Dołącz do naszej społeczności Dostępne tutaj At the end of this chapter, you’ll be able to achieve the results in the GIF below: Instrukcje Setup ustawienia If you haven’t setup Mimo to, proszę o śledzenie Aby go ustawić. Spokój Oficjalny przewodnik Create a fresh project: cargo new bevy_game cd bevy_game Open and add Bevy: Cargo.toml [dependencies] bevy = "0.16.1" Przypuszczam, że masz doświadczenie w programowaniu w dowolnym języku, takim jak javascript lub python. Myślenie w systemach Czego potrzebujemy, aby zbudować prostą grę, która pozwala graczowi przejść od wejścia klawiatury? System światowy: Stwórz świat gry, w którym gracz może się poruszać. System wejścia: Monitoruj wejście klawiatury i przetłumacz go na ruch odtwarzacza. Widzimy powyższy wzorzec bardzo powszechny w budowaniu gier, wzorzec tworzenia / ustawiania i wzorzec monitorowania zmian stanu gry i aktualizacji podmiotów świata gry. Konfiguracja: Wygraj wroga z różnymi zachowaniami, załaduj sprity i animacje wroga, skonfiguruj wartości zdrowia i uszkodzeń wroga. Aktualizacja: Przenieś wrogów w kierunku gracza, sprawdź, czy wrogowie są uderzeni kulami, usuń martwych wrogów i rodź nowych wrogów w odstępach czasu. Podstawową cechą tego modelu jest to, że mamy prostą Ten podstawowy wzorzec sprawia, że Bevy jest zarówno potężny, jak i łatwy do zrozumienia - gdy tylko zrozumiesz tę koncepcję, budowanie z bevy staje się łatwiejsze. setup and update system Wyobraź sobie funkcję o nazwie setup, która daje ci „polecenia” jako argument i daje ci zdolność do tworzenia lub tworzenia wszystkiego, co chcesz. // Pseudo code, doesn't compile fn setup(mut commands: Commands) { // Create anything you want commands.spawn() } What’s mut? W Rust musimy wyraźnie wspomnieć, że planujemy zmienić tę wartość. Domyślnie Rust traktuje deklarowane wartości jako tylko odczytane. mówi Rust planujemy zmienić wartość. Rust wykorzystuje tę wiedzę, aby zapobiec całej klasie błędów. mut mut ? And what’s this mut commands: Commands Jest to z biblioteki bevy, która pozwala dodać rzeczy do swojego świata gry. Rust lubi wyraźne typy, więc pozwala kompilatorowi wiedzieć, że ten parametr jest interfejsem polecenia Bevy. Przesunięcie wskazówki i Rust nie może być pewien, co się pojawia, więc blokuje budowę. :Commands What’s a type? Typ informuje Rust o tym, z jakim typem wartości masz do czynienia — liczby, tekst, zegarki, polecenia Bevy itp. Po zapoznaniu się z typem kompilator może sprawdzić, czy każda operacja wykonana na nim ma sens. Isn’t this too much work? Zapobiega błędom i pomaga w wydajności, np.: Jeśli spróbujesz dodać strunę do liczby, kompilator zatrzymuje cię przed rozpoczęciem gry. a jest tylko dwa numery, więc Rust przechowuje dokładnie dwa numery, bez zaskoczenia dodatkowego miejsca, który utrzymuje rzeczy szybko, gdy masz tysiące jednostek gry. Vec2 Kiedy tworzysz obiekt Vec2 w JavaScript lub Python, nie przechowujesz tylko dwóch liczb. Czas działania dodaje metadane typu, informacje o właściwości i odniesienia do prototypów - przekształcając prostą strukturę danych o 8 bajtach w ~48 bajtów pamięci. Gdy deklarujesz strukturę Vec2 z dwoma polami f32, jest to dokładnie to, co jest przechowywane - tylko 8 bajtów, bez dodatkowych metadanych. W przypadku 1000 jednostek gier różnica ta staje się dramatyczna: Language Memory Usage Overhead Rust 8KB None Dynamic language ~48KB+ 6x overhead Spokój 8Kb Nikt Dynamiczny język ~48 KB + 6x nad głową Małe, przewidywalne układy pamięci oznaczają lepsze wykorzystanie pamięci podręcznej procesora, co bezpośrednio przekłada się na szybsze współczynniki ramki w grach, w których przetwarzasz tysiące podmiotów na każdy frame. ustawienie kamery What should we setup first? Świat może istnieć w danych, ale kamera decyduje, co rzeczywiście jest rysowane. //Pseudo code, don't use this yet fn setup(mut commands: Commands) { commands.spawn(Camera2d) } What’s Camera2d? to wbudowany zbiór kamer 2D firmy Bevy. Spraw, aby uzyskać gotowy do obejrzenia widok Twojej sceny 2D. Camera2d What’s a bundle? Pakiet jest po prostu grupą komponentów, które często rodzi się razem. który pakuje pozycję, teksturę, odcień koloru i widoczność; rodzenie tego zbioru daje jednostkę sprite w jednym wezwaniu. SpriteBundle Zarejestruj naszą funkcję konfiguracji Musimy zarejestrować naszą funkcję ustawień z bevy, aby być uruchomiony na Startup. Aktualizacja Twoja z następującym kodem. 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? jest jak zestaw startowy, rzeczy, do których ciągle dotykasz podczas budowania gry - , komponenty, pomocniki matematyczne itp. bevy::prelude App Commands What’s App::new()…? tworzenie aplikacji do gier, przesyłanie obciążeń, wejście, audio i inne systemy, rejestruje naszą funkcję startu oraz kontrolę rąk do głównego łańcucha Bevy. App::new() add_plugins(DefaultPlugins) add_systems(Startup, setup) run() Why register a startup function? What does it do? Używamy ich do ustawiania kamer, odtwarzaczy i innych rzeczy, które powinny istnieć, gdy tylko otworzy się okno. What’s bevy main loop? Po uruchomieniu Bevy wchodzi w jego główny obwód: ankietuje wejścia, uruchamia systemy, aktualizuje świat i renderuje ramy. Będziemy jeździć cargo run Tak, tylko skonfigurowaliśmy kamerę, teraz dodajmy naszego odtwarzacza. Ustawianie gracza Stwórzmy teraz naszego gracza!Pamiętaj o naszych i No dobrze, w Bevy, wszystko jest z Z tymi systemami można pracować. Setup System Update System Entity Components Pomyśl o jednostce jako o unikalnym identyfikatorze (takim jak numer zabezpieczenia społecznego) i komponentach jako o danych dołączonych do niej. Dla naszego gracza potrzebujemy komponentów takich jak prędkość ruchu, zdrowie lub specjalne zdolności. . struct jest jednym z podstawowych bloków budowlanych rdzy. Gromadzi podobne dane razem. struktur tak, że sam typ działa jako tag, który możemy dołączyć do podmiotu gracza. struct Player // Place this before main function in main.rs #[derive(Component)] struct Player; Why tag? Tag oznacza jednostkę do późniejszego wyszukiwania. ponieważ Jest przywiązany tylko do naszego bohatera, systemy mogą poprosić Bevy, "daj mi jednostkę z tagem gracza" i pracować tylko z tym. Player What’s this #[derive(component)]? makro jest sposobem Rust do generowania wstępnie zdefiniowanego kodu szablonu dla Ciebie. automatycznie wstrzykuje płyta grzewcza Bevy potrzeba, więc może przechowywać i znaleźć Oszczędzając nas przed kopiowaniem tego samego kodu kleju wszędzie. Przyjrzymy się bliżej makrom później w serii. derive #[derive(Component)] Player What’s a component, and why should the player be a component? Komponent to fragment danych przyłączony do jednostki. Pozycja, prędkość, zdrowie, a nawet idea „to jest gracz” żyją w komponentach. Możemy zapytać o ten składnik później, dodać więcej składników (takich jak zdrowie lub zapas) i pozwolić systemom Bevy wybrać dokładnie te jednostki, które muszą zaktualizować. Player Na razie będziemy reprezentować naszą postać na ekranie za pomocą symbolu „@”. // 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, )); } Ten pojedynczy połączenie dodaje tekst, który chcemy pokazać, jego ustawienia czcionek, kolor, pozycję i identyfikator, który identyfikuje daną jednostkę. commands.spawn(( ... )) Player What’s a tuple? Tuple to uporządkowana lista wartości zapisanych w parentezach. Rust śledzi każdą pozycję, więc Trzyma dwie wartości obok siebie bez konieczności tworzenia struktury. (Text2d::new("@"), TextColor(Color::WHITE)) Whats an entity? Jednostka jest unikalnym identyfikatorem, który Bevy używa do łączenia składników. Sam w sobie nie zawiera żadnych danych, ale gdy do niej dołączysz składniki, to identyfikator reprezentuje coś w twoim świecie gier. Każdy zbiór generuje nową jednostkę z unikalnym identyfikatorem i wymienionymi komponentami, które można przedstawić w następujący sposób: Entity Components it carries #42 Camera2d #43 , , , , Text2d("@") TextFont TextColor Transform Player #42 Camera2d # 43 , , , , Text2d("@") TextFont TextColor Transform Player Gdy kolejka wypłynie, te jednostki żyją w świecie, gotowe dla systemów do odkrycia ich przez znaczniki (komponenty), które noszą. będziemy używać tego później, gdy chcemy robić rzeczy, takie jak poruszanie się lub animowanie ich, lub sprawiają, że atakują wrogów itp. Wdrażanie ruchu graczy Stwórzmy teraz nasze Pomyśl o tym, co musimy zrobić, aby przenieść gracza: Update System Wprowadzenie klawiatury - aby wiedzieć, które klawisze są naciśnięte Czas - aby ruch był płynny niezależnie od prędkości ramy Pozycja gracza - aby faktycznie przenieść gracza W innych silnikach do gier spędzasz czas na ręcznym połączeniu tych systemów.Ale oto magia Bevy - po prostu prosiłeś o to, czego potrzebujesz w swoich parametrach funkcji, a Bevy automatycznie dostarcza to! Napiszmy naszą funkcję gracza ruchu. // 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 lub zasoby to fragmenty informacji o całej grze, które nie są powiązane z żadną pojedynczą jednostką. daje mistrzowy zegar gry, więc każdy system odczytuje tę samą wartość „czas od ostatniego kadru”. Res<Time> Explain Single<&mut Transform, With>? Prowadzimy analizę jednego podmiotu, który ma component and also carries the Tagi: The część oznacza, że zamierzamy zmodyfikować tę transformację lub pozycję gracza (Pamiętaj, że dodaliśmy transform komponent w funkcji konfiguracji). Single<&mut Transform, With<Player>> Transform Player &mut Transform What’s Vec2::ZERO? jest wektorem dwuwymiarowym z obydwoma wartościami ustawionymi na zero: Używamy go jako kierunku wyjścia przed przeczytaniem wejścia klawiatury. Vec2::ZERO Vec2 { x: 0.0, y: 0.0 } What’s this KeyCode::… pattern? , ... są enum (zostaną pokryte później), które reprezentują konkretne klawisze na klawiaturze. Po prostu pyta Bevy, czy ten klucz jest zatrzymywany podczas bieżącego ramy. KeyCode::ArrowLeft KeyCode::ArrowRight input.pressed(KeyCode::ArrowLeft) Ignorujemy kierunek zerowy, więc gracz stoi, gdy nie naciśnięte są klucze. Konwertuje wektor na długość 1, więc ruch diagonalny nie jest szybszy niż ruch prosty. określa, ile pikseli na sekundę do poruszania się, i 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() Rejestracja systemu ruchu // 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? Każda z nich jest włączona, więc włączamy ją do w harmonogramie . Jest to wbudowana faza Bevy, która zapala się raz na cyklu gry po zakończeniu uruchamiania. 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? System to tylko funkcja Rust, którą przekazujesz Bevy z klejową notatką, która mówi „Uruchom mnie podczas uruchamiania” lub „Uruchom mnie przy każdej aktualizacji”, a silnik należycie przestrzega. Będziemy to biegać. Dodawanie grafiki Sprite Będziemy używać uniwersalnego generatora LPC SpriteSheet, aby dać naszemu bohaterowi pewną osobowość. i eksportować pełną spritesheet. Ten link W ramach tego projektu już włączono do Drop podane pliki obrazów do więc Bevy może je znaleźć, gdy gra zostanie uruchomiona. Znajduje się on w folderze src. repo src/assets assets Reaktywowanie kodu Musimy dodać więcej kodu i dodanie do main.rs będzie trudne do przeczytania i zrozumienia. Zaktualizuj plik wtyczki.pl do następującego, a także utwórz inny plik wtyczki.pl Ciągnie się w Rozdzielanie kodu w ten sposób ułatwia rozwój projektu bez jednego dużego pliku, który robi wszystko. 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); } W aplikacji wprowadza się globalne ustawienie, dzięki czemu każda rama zaczyna się od białego tła. .insert_resource(ClearColor(Color::WHITE)) jest ładowarką Bevy dla tekstur, dźwięku i innych zasobów. so it looks inside Właśnie tam żyją nasze sprity. AssetPlugin file_path src/assets Tworzenie modułu gracza Teraz, gdy mamy podstawową strukturę aplikacji, nadszedł czas, aby przywrócić do życia naszą postać! Moduł do obsługi wszystkiego, co związane z naszym małym przygodowcą.To utrzymuje nasz kod zorganizowany i ułatwia dodanie więcej funkcji później. player.rs Ten moduł będzie zawierał wszystkie stałe, komponenty i systemy, które sprawiają, że nasz gracz porusza się, animuje i interakcji ze światem. Dodaj następujący kod w 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 Konstanty te nadają nazwy liczbom, które ponownie używamy do spritesheet i matematyki ruchu: rozmiar płytki, ramy na wiersz, prędkość chodzenia i jak szybko postępuje animacja. marker utrzymuje wszystkie typy specyficzne dla gracza w jednym module. Player Definicja kierunków graczy // 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? Enum wymienia garstkę dozwolonych wartości. Nasza postać zawsze zmierza tylko w czterech kierunkach, więc Enum rejestruje te opcje w jednym miejscu. Facing Why can’t I use a struct here? Będziemy potrzebować jakiegoś boeinga, jak , , i musisz je synchronizować. co komplikuje. Enum gwarantuje, że wybierzesz tylko jeden kierunek. facing_up:true facing_left:false When to decide between using enum and struct? Użyj struktury, gdy potrzebujesz kilku pól, takich jak pozycja z i , lub statystyki graczy ze zdrowiem i wytrzymałością.Użyj enum, gdy wybierzesz jedną opcję z listy, na przykład, które narzędzie gracz używa (miecz, łuk, łuk) lub który ekran menu jest aktywny. x y Why the purpose of adding Debug, Clone, Copy, PartialEq, Eq macros? Pozwól nam wydrukować oblicze dla logów, i Uczynić to trivialnym, aby powtórzyć wartość, i / Umożliwia kontrolę równości podczas porównywania kierunków. Debug Clone Copy PartialEq Eq Why can’t I copy through simple assignment, why should I add these macros? Domyślnie Rust przenosi wartości zamiast ich kopiowania, więc kompilator sprawia, że opt-in. (i jak pomocnik) mówi „to tanie, idź do przodu i zduplikuj go”. i Porównajmy dwa twarze bezpośrednio, co jest sposobem, w jaki wykrywamy, gdy gracz zmienia kierunek. Copy Clone PartialEq Eq What do you mean by rust moves values, instead of copying? Gdy przydzielasz większość wartości w Rust, stara zmienna przestaje ją posiadać lub w inny sposób umiera. Obie zmienne pozostają ważne. Copy Why does the old variable stop owning or die when assigned? Rust zapewnia, że każda wartość ma jednego właściciela, aby pamięć mogła zostać bezpiecznie uwolniona. This is a bit going over my head! Tak, nie martw się, mamy jeszcze wiele rozdziałów do obejrzenia, a gdy przechodzisz przez nie, zajmiemy się tym. Komponenty systemu animacji Gdy mamy spritesheet z wieloma ramkami (jak nasza 9-ramkowa animacja chodząca), potrzebujemy sposobu, aby kontrolować, jak szybko te ramy grają. Pomyśl o tym jak o flipbooku - jeśli obrócisz strony zbyt wolno, animacja wygląda choppy. daje nam precyzyjną kontrolę nad tym momentem, zapewniając, że animacja chodzenia naszego bohatera wygląda gładko i naturalnie w odpowiedniej prędkości. AnimationTimer Wrzucaj do dowodu Tak więc każda jednostka gracza wie, kiedy przejść do następnej ramki animacji.Każda kropka reprezentuje jeden kawałek czasu; po tym, jak timer uderzy w jego odstępy, przechodzimy do następnego spritu w arkuszu. 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? Pozwól naszemu wkrętowi udawać, że jest wewnętrznym Czytając z niej, i To samo dotyczy pisarzy, więc możemy on bez ręcznego wyciągania wartości wewnętrznej. Deref Timer DerefMut timer.tick(time.delta()) AnimationTimer So we are renaming the Timer to AnimationTimer? Zamykamy zegarek, a nie przemianujemy go. Jest to mała skrzynka, która zawiera , plus etykieta, która mówi „to należy do animacji gracza”. i wkleić go do tego pudełka, aby każdy gracz mógł mieć swój własny timer, jeśli potrzebujemy wielu bohaterów. AnimationTimer Timer Timer So it’s an instance of AnimationTime? Tak tak, Jest to kompozycja, która zawiera a Tworzymy jeden, gdy rodzimy odtwarzacz, aby każda jednostka mogła przenosić własne dane timerowe. Ten wzorzec pojawia się, gdy chcesz dodać dodatkowe znaczenie do istniejącego typu bez pisania zupełnie nowego API. AnimationTimer Timer Pamięta, w jaki sposób gracz wskazuje, czy się porusza i czy właśnie zaczął lub zatrzymał.Systemy czytają to, aby wybrać linie animacji i zresetować ramy, gdy ruch się zmienia. AnimationState Sprawia, że gracz Ładujemy spritesheet przez , utwórz układ atlasu tekstury, aby Bevy znał siatkę, i wybierz ramy startowe dla bohatera, który patrzy w dół. 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)), )); } ustawia kierunek startu i flagi, że postać jest pusty teraz i był pusty ostatnie ramy. AnimationState { facing, moving: false, was_moving: false } tworzy powtarzający się zegarek, który zapala każdy sekundy, aby wyprzedzić spritesheet. AnimationTimer(Timer::from_seconds(ANIM_DT, TimerMode::Repeating)) ANIM_DT ? What’s an AssetServer o jest załadowcą plików i menedżerem firmy Bevy, który obsługuje ładowanie i przechowywanie w pamięci podręcznej aktywów gier, takich jak obrazy, dźwięki i modele 3D. AssetServer Kiedy zadzwonisz , nie natychmiast ładuje pliku do pamięci. Zamiast tego zwraca uchwyt, który można użyć później. To się nazywa "leniwe ładowanie" - faktyczne ładowanie pliku odbywa się w tle, a Bevy powiadomi Cię, kiedy jest gotowy. asset_server.load("path/to/sprite.png") Takie podejście jest skuteczne, ponieważ: Wiele podmiotów może udostępniać ten sam sprit bez wielokrotnego ładowania Aktywa są ładowane tylko wtedy, gdy są naprawdę potrzebne Bevy może zoptymalizować i obciążać aktywa dla lepszej wydajności Jeśli składnik aktywów nie zostanie załadowany, nie spowoduje to awarii całej gry W naszym przypadku, żąda arkusza sprites i zwraca uchwyt, aby śledzić jego stan ładowania. asset_server.load("sprites/player.png") System ruchu Teraz, gdy nasz gracz ma wszystkie niezbędne komponenty, musimy zaktualizować nasz istniejący system ruchu, aby śledzić, w którym kierunku gracz zmierza.To jest kluczowe dla systemu animacji, aby wiedzieć, który wiersz arkusza sprite do użycia - każdy kierunek (w górę, w dół, w lewo, w prawo) odpowiada innej linii w naszym atlasie sprite. Ten zaktualizowany system wykryje kierunek ruchu i aktualizuje odpowiednio, dzięki czemu nasz system animacji może wybrać właściwy wiersz sprite dla animacji chodzenia w każdym kierunku. 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; } } Zapytanie prosi o dowód dla pojedynczej jednostki oznaczonej Zapewniamy mutację dostępu do i Budujemy wektor kierunkowy z naciśniętych klawiszy, normalizujemy go tak, aby wejście diagonalne nie było szybsze, a następnie przenosimy odtwarzacza przez prędkość × czas ramy. Logika twarzy porównuje siłę poziomą vs siłę pionową, aby zdecydować, w jaki sposób powinien wyglądać sprite. Player Transform AnimationState Wdrożenie animacji Teraz mamy wszystkie kawałki na miejscu - nasz gracz może poruszać się w różnych kierunkach, a my śledzimy, w którym kierunku się zmierzają. System ten pobiera informacje o kierunku z naszego systemu ruchu i wykorzystuje timer animacji do przechodzenia przez ramy sprite z odpowiednią prędkością. obsługuje złożoną logikę przełączania się między różnymi liniami animacji, gdy gracz zmienia kierunek, i zapewnia płynne przejścia między chodzeniem a stanami spaceru. // 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, } } obie sprawdzają wynik i nazywają potrzebne kawałki. jeśli zapytanie się uda, kod wiąże , , oraz Jeśli to się nie uda (nie ma żadnego gracza lub więcej niż jeden), uderzamy w odcinka i natychmiast wyjść. Rust używa Typ dla tego: oznacza „wniosek zwrócił dokładnie jeden wynik”, oznacza „coś o tym zapytaniu nie pasowało”. let Ok((mut anim, mut timer, mut sprite)) = query.single_mut() else { return; }; anim timer sprite else Result Ok Err Później my Jest to , which is Rust’s “maybe there is a value” type. oznacza, że atlas tekstury istnieje i możemy go dostosować; oznacza, że nie został jeszcze załadowany, więc przepuszczamy i pozwalamy następnemu kadrowi spróbować ponownie. Jest to ten sam wzorzec, którego używasz podczas sprawdzania mapy lub pamięci podręcznej: użyj wartości tylko wtedy, gdy wyszukiwanie coś zwróci. match Option Some(atlas) None przyciąga stan animacji, timer i sprite dla odtwarzacza. Określa, który wiersz atlasu pasuje do bieżącego obrazu, śni się do tego wiersza, gdy kierunek się zmienia, i wykorzystuje timer, aby przejść przez kolumny w stałym tempie. Kiedy ruch się zatrzymuje, resetujemy timer, aby animacja spoczywała na ostatnim wyświetlonym ramy. animate_player Tworzenie pluginów dla graczy // 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)); } } jest naszym „modułem gracza” w formie wtyczki. w Bevy wtyczka jest po prostu strukturą, która wie, jak rejestrować systemy, zasoby i aktywa. dla , dajemy Bevy listę kontrolną: za każdym razem, gdy ten wtyczka jest dodawana do aplikacji, uruchom kod wewnątrz, aby ustawić wszystko, czego potrzebuje funkcja odtwarzacza. Zaczęło się od połączenia z konkretnymi graczami. PlayerPlugin Plugin PlayerPlugin main.rs o Metoda jest listą kontrolną.Bevy przekazuje nam mutable Zacznijmy od systemów, o które dbamy. Zaplanowano w Tak więc sprite pojawia się zaraz po uruchomieniu gry. i Wejdź do harmonogram, aby wykonać każdą ramkę – obsługę wejść i animacji w lockstep. Wejście automatycznie wyłącza cały przepływ gracza. 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? Tak, to The „Wszystkie pluginy muszą dostarczać Wdrażamy tę cechę, więc Rust oczekuje od nas dostarczenia ciała. Bevy nazywa tę metodę, gdy ładuje wtyczkę, dlatego dodajemy do niej wszystkie nasze systemy. Plugin build(&self, app: &mut App) What’s a trait? Cechą jest umowa opisująca, jakie metody musi zapewnić typ. Trójka mówi: „Daj mi funkcji, dzięki czemu mogę zarejestrować twoje systemy.” poprzez wdrożenie tej cechy dla , złączyliśmy się z procesem uruchamiania firmy Bevy i wstrzykiwaliśmy własny kod konfiguracyjny. zachowuje się jak każdy inny wtyczka Bevy, ale instaluje nasze systemy specyficzne dla gracza. Plugin build PlayerPlugin PlayerPlugin Ostateczna integracja // 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(); } Będziemy to biegać. cargo run Ten artykuł został pierwotnie opublikowany na moim blogu. Ten artykuł został pierwotnie opublikowany na moim blogu. Więcej rozdziałów nadchodzących, pozostań nastawiony.Połącz się ze mną na LinkedIn, śledź mnie na X, aby uzyskać najnowsze aktualizacje. Linkedin → X