Este es el capítulo 1 de mi serie de tutoriales Rust-Bevy “The Impatient Programmer’s Guide to Bevy and Rust: Build a Video Game from Scratch”. Únete a nuestra comunidad para recibir actualizaciones sobre las nuevas ediciones de esta serie.El código fuente de este capítulo está disponible aquí. Únete a nuestra comunidad Disponible aquí At the end of this chapter, you’ll be able to achieve the results in the GIF below: Instrucciones Setup Instalación Si no se ha establecido Por favor, siga el para ponerlo en marcha. El descanso Guía Oficial Create a fresh project: cargo new bevy_game cd bevy_game Open and add Bevy: Cargo.toml [dependencies] bevy = "0.16.1" Yo voy a asumir que usted tiene experiencia en la programación en cualquier lenguaje como JavaScript o Python. Pensar en sistemas ¿Qué necesitamos para construir un juego simple que permita al jugador moverse desde la entrada del teclado? Sistema Mundial: Crea un mundo de juego donde el jugador puede moverse. Sistema de entrada: monitorear la entrada del teclado y traducirla en movimiento del jugador. Vemos el patrón anterior muy común en la construcción de juegos, el patrón de creación / configuración y el patrón de monitoreo de los cambios en el estado del juego y la actualización de las entidades del mundo del juego. Configuración: Prueba al enemigo con diferentes comportamientos, carga espritos enemigos y animaciones, configura la salud del enemigo y los valores de daño. : Move enemies toward the player, check if enemies are hit by bullets, remove dead enemies and spawn new enemies at intervals. Update En el núcleo de Bevy, tenemos este simple Este patrón fundamental es lo que hace que Bevy sea poderoso y fácil de entender - una vez que se capte este concepto, la construcción con bevy se vuelve más fácil. setup and update system Imagínese una función llamada configuración, que le da "comandos" como argumento, y le da la capacidad de generar o generar cualquier cosa que desee. // Pseudo code, doesn't compile fn setup(mut commands: Commands) { // Create anything you want commands.spawn() } What’s mut? En Rust necesitamos mencionar explícitamente que estamos planeando cambiar este valor. Por defecto, Rust trata los valores declarados como sólo leídos. Rust dice que planeamos cambiar el valor. Rust utiliza este conocimiento para prevenir toda una clase de errores. mut mut y ? And what’s this mut commands: Commands Es de la biblioteca bevy, que te permite añadir cosas a tu mundo del juego. Rust le gusta los tipos explícitos, así que permite que el compilador sepa que este parámetro es la interfaz de comandos de Bevy. Saltar la pista y Rust no puede estar seguro de lo que aparece, por lo que bloquea la construcción. :Commands What’s a type? Un tipo le dice a Rust qué tipo de valor está manejando: números, texto, temporizadores, comandos Bevy, etc. Una vez que el compilador conoce el tipo, puede comprobar que cada operación que realice en él tiene realmente sentido. Isn’t this too much work? Previene errores y le ayuda con el rendimiento, por ejemplo: si intenta agregar una cadena a un número, el compilador lo detiene antes de que el juego se ejecute. a es sólo dos números, por lo que Rust almacena exactamente dos números, sin sorpresa espacio extra, que mantiene las cosas rápido cuando tienes miles de entidades de juego. Vec2 Cuando creas un objeto Vec2 en JavaScript o Python, no solo almacenas dos números.El tiempo de ejecución agrega metadatos de tipo, información de propiedad y referencias de prototipo, convirtiendo tu estructura de datos simple de 8 bytes en ~48 bytes de memoria. El sistema de tipo de Rust funciona en el tiempo de compilación.Cuando declara una estructura Vec2 con dos campos f32, eso es exactamente lo que se almacena: sólo 8 bytes, sin metadatos adicionales.El compilador ya conoce los tipos, por lo que no se necesita información de tipo de tiempo de ejecución. Con 1000 entidades de juego, esta diferencia se vuelve dramática: Language Memory Usage Overhead Rust 8KB None Dynamic language ~48KB+ 6x overhead El descanso 8 kB Ninguno Lenguaje dinámico · 48 KB + 6x sobre la cabeza Los diseños de memoria más pequeños y previsibles significan una mejor utilización del cache de la CPU, lo que se traduce directamente en tasas de cuadro más rápidas en los juegos donde se están procesando miles de entidades cada cuadro. Instalar una cámara What should we setup first? Necesitamos una cámara porque nada muestra en la pantalla sin uno.El mundo puede existir en los datos, pero la cámara decide lo que realmente se dibuja. //Pseudo code, don't use this yet fn setup(mut commands: Commands) { commands.spawn(Camera2d) } What’s Camera2d? es el paquete de cámara 2D integrado de Bevy. Spañalo y obtendrás una vista listo para ir de tu escena 2D. Camera2d What’s a bundle? Un paquete es sólo un grupo de componentes que a menudo engendran juntos. por ejemplo, Bevy que envuelve la posición, la textura, el tono de color y la visibilidad; el germen de ese paquete da una entidad de sprite en una llamada. SpriteBundle Registrar nuestra función de configuración Necesitamos registrar nuestra función de configuración con bevy para ser activada en Startup. Actualiza tu con el siguiente código. 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? Es como un kit de arranque, cosas que se alcanzan constantemente al construir un juego. , de , componentes, ayudantes matemáticos, etc. bevy::prelude App Commands What’s App::new()…? creates the game application, rendimiento de cargas, entrada, audio y otros sistemas, Registra nuestra función de inicio, y Control de manos a la curva principal de Bevy. App::new() add_plugins(DefaultPlugins) add_systems(Startup, setup) run() Why register a startup function? What does it do? Los sistemas de arranque se ejecutan una vez antes del primer cuadro. los usamos para configurar cámaras, reproductores y otras cosas que deberían existir tan pronto como se abra la ventana. What’s bevy main loop? Después del arranque, Bevy entra en su ciclo principal: encuesta la entrada, ejecuta sus sistemas, actualiza el mundo y hace un cuadro. Vamos a correrlo cargo run Yup, sólo hemos configurado la cámara, ahora añadamos nuestro jugador. Establecer el jugador ¡Ahora vamos a crear nuestro jugador! and Bueno, en Bevy, todo es un con Estos sistemas pueden funcionar. Setup System Update System Entity Components Piense en una entidad como una identificación única (como un número de seguridad social) y los componentes como los datos adjuntos a ella. Para nuestro jugador, necesitamos componentes como velocidad de movimiento, salud o habilidades especiales. . struct es uno de los bloques de construcción del núcleo de hierro. agrupa datos similares juntos. Aquí declaramos un vacío estruct para que el tipo mismo actúe como una etiqueta que podemos adjuntar a la entidad del jugador. Más tarde podemos añadir cosas como la salud del jugador. struct Player // Place this before main function in main.rs #[derive(Component)] struct Player; Why tag? La etiqueta marca una entidad para una búsqueda posterior. is attached to only our hero, systems can ask Bevy, “give me the entity with the Player tag” and work with just that one. Player What’s this #[derive(component)]? Le dice a Rust que aplique el código de macro de componentes a esta estructura.Una macro es la forma en que Rust genera el código de plantilla predefinido para usted. inyecta automáticamente las necesidades de la placa de calentamiento de Bevy, para que pueda almacenar y encontrar las entidades, salvándonos de copiar el mismo código de cola en todas partes. Tomaremos una mirada más de cerca a las macros más adelante en la serie. derive #[derive(Component)] Player What’s a component, and why should the player be a component? Un componente es una pieza de datos anexada a una entidad. Posición, velocidad, salud, e incluso la idea de “este es el jugador” todos viven en componentes. un componente que podemos consultar para esa entidad más tarde, agregar más componentes (como la salud o el inventario), y dejar que los sistemas de Bevy seleccionen exactamente las entidades que necesitan actualizar. Player Por ahora, representaremos a nuestro personaje en la pantalla usando el símbolo “@”. // 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, )); } toma ese tuple y lo trata como un paquete de componentes. Esta única llamada agrega el texto que queremos mostrar, sus configuraciones de fuente, el color, la posición y el que identifica a la entidad. commands.spawn(( ... )) Player What’s a tuple? Un tuple es una lista ordenada de valores escritos en paréntesis. Mantiene dos valores lado a lado sin necesidad de crear una estructura. (Text2d::new("@"), TextColor(Color::WHITE)) Whats an entity? Una entidad es la identificación única que usa Bevy para unir los componentes. por sí misma no contiene datos, pero una vez que se adhieren componentes a ella, esa identificación representa algo en su mundo del juego. Cada paquete produce una nueva entidad con un ID único y los componentes listados, que se pueden imaginar de esta manera: Entity Components it carries #42 Camera2d #43 , , , , Text2d("@") TextFont TextColor Transform Player #42 Camera2d #43 , de , , de , de Text2d("@") TextFont TextColor Transform Player Once the queue flushes, those entities live in the world, ready for systems to discover them by the tags (components) they carry. We will be using this later when we want to do things like moving or animating them, or making them attack enemies, etc. Implementación del movimiento de los jugadores Ahora vamos a crear nuestro Pensemos en lo que necesitamos para mover a un jugador: Update System Entrada de teclado - para saber qué teclas se presionan - to make movement smooth regardless of frame rate Time Posición del jugador - para mover realmente al jugador In other game engines, you’d spend time manually connecting these systems together. But here’s the magic of Bevy - you just ask for what you need in your function parameters, and Bevy automatically provides it! Escribamos nuestra función de movimiento del jugador. // 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 o Recursos son piezas de información de todo el juego que no están vinculadas a ninguna entidad. gives you the game’s master clock, so every system reads the same “time since last frame” value. Res<Time> Explain Single<&mut Transform, With>? Pide probar exactamente una entidad que tenga un componente y también lleva el tag. The parte significa que tenemos la intención de modificar esa transformación o posición del jugador (recordemos que añadimos un componente de transformación en la función de configuración).Si existiera más de un jugador, este extractor se quejaría, lo cual es perfecto para un juego de un héroe. Single<&mut Transform, With<Player>> Transform Player &mut Transform What’s Vec2::ZERO? is a two-dimensional vector with both values set to zero: . We use it as the starting direction before reading keyboard input. Vec2::ZERO Vec2 { x: 0.0, y: 0.0 } What’s this KeyCode::… pattern? , de ... son enums (cubrirán enums más tarde) que representan claves específicas en el teclado. Simplemente pregunta a Bevy si esa clave está retenida durante el marco actual. KeyCode::ArrowLeft KeyCode::ArrowRight input.pressed(KeyCode::ArrowLeft) Ignoramos la dirección cero para que el jugador se detenga cuando no se presionan las teclas. Convierte el vector a longitud 1 para que el movimiento diagonal no sea más rápido que el movimiento recto. indica cuántos píxeles por segundo se deben mover, y Devuelve el tiempo de cuadro – el número de segundos desde el cuadro anterior – por lo que multiplicarlos da la distancia que deberíamos recorrer esta actualización. normalize() speed time.delta_secs() Registrar el sistema de movimiento // 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? runs every frame, so we plug it into the El Calendario. es el estadio integrado de Bevy que se estrella una vez por ciclo de juego después de que se haya completado el arranque. 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? Un sistema es simplemente una función de Rust que entregas a Bevy con una nota pegajosa que dice "me ejecute durante el inicio" o "me ejecute en cada actualización", y el motor obedece de manera duvidosa. Let’s run it. Introducción a los gráficos Sprite We’ll use the Universal LPC SpriteSheet Generator to give our character some personality. You can remix body parts, clothes, and colors at and export a full spritesheet. Este enlace Para este proyecto, la espritesheet ya está incluida en la Desplácese los archivos de imagen proporcionados en para que Bevy pueda encontrarlos cuando el juego se ejecute. Directorios dentro de la carpeta de src. repo src/assets assets Refactoring Code Necesitamos añadir más código y agregar a main.rs hará que sea difícil leer y entender. Update your main.rs file to the following, also create another file player.rs Pulsar en la module, keeping our main file slim. Splitting code like this makes it easier to grow the project without one big file doing everything. 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); } Se introduce una configuración global en la aplicación para que cada cuadro comience con un fondo blanco. .insert_resource(ClearColor(Color::WHITE)) es el cargador de Bevy para texturas, audio y otros activos. Así se ve por dentro Es donde viven nuestros espíritus. AssetPlugin file_path src/assets Creación del módulo de jugador Ahora que tenemos nuestra estructura básica de aplicaciones en su lugar, es hora de traer a la vida a nuestro personaje! módulo para manejar todo lo relacionado con nuestro pequeño aventurero. Esto mantiene nuestro código organizado y hace que sea más fácil añadir más características más tarde. player.rs This module will hold all the constants, components, and systems that make our player move, animate, and interact with the world. Add following code in 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 Estas constantes dan nombres a los números que reutilizamos para la hoja de cálculo y las matemáticas de movimiento: tamaño de ladrillo, cuadros por fila, velocidad de marcha y la rapidez con la que avanza la animación. El marcador aquí mantiene todos los tipos específicos del jugador en un módulo. Player Definir las direcciones de los jugadores // 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? Un enum enumera un puñado de valores permitidos.Nuestro personaje sólo se enfrenta a cuatro direcciones, por lo que enum captures those options in one place. Facing Why can’t I use a struct here? Una estructura necesitaría un montón de booleanos como , de , y tendrías que mantenerlos en sincronía. Esto hace que sea complicado. Enum garantiza que solo eliges una dirección. facing_up:true facing_left:false When to decide between using enum and struct? Use a struct when you need several fields, like a position with and , o estadísticas de jugadores con salud y resistencia. Use un enum cuando elige una opción de una lista, como qué herramienta el jugador usa (espada, arco, varita) o qué pantalla de menú está activa. x y Why the purpose of adding Debug, Clone, Copy, PartialEq, Eq macros? Imprimimos el rostro para los logos, y hacer trivial duplicar el valor, y / permiten comprobar la igualdad cuando comparamos direcciones. Debug Clone Copy PartialEq Eq Why can’t I copy through simple assignment, why should I add these macros? Por defecto, Rust mueve valores en lugar de copiarlos, por lo que el compilador le hace optar. (y el como un ayudante) dice “es barato, sigue adelante y duplica”. and let us compare two facings directly, which is how we detect when the player changes direction. Copy Clone PartialEq Eq What do you mean by rust moves values, instead of copying? Cuando asigna la mayoría de los valores en Rust, la variable vieja deja de poseerla o, en otras palabras, muere. Mantenga ambas variables válidas. Copy Why does the old variable stop owning or die when assigned? Rust impone que cada valor tiene un único propietario para que la memoria pueda ser liberada de forma segura. desempacaremos las reglas de propiedad y emprender en capítulos posteriores. This is a bit going over my head! Sí, no te preocupes, tenemos muchos más capítulos por seguir y a medida que pases por ellos, vamos a abordar esto. Animation System Components When we have a spritesheet with multiple frames (like our 9-frame walking animation), we need a way to control how fast those frames play. Without timing control, our character would either be frozen on one frame or cycling through frames so fast it looks like a blur! Piensa en ello como en un flipbook: si giras las páginas demasiado despacio, la animación se verá choppy. nos da un control preciso sobre este timing, asegurando que la animación caminando de nuestro personaje se vea suave y natural a la velocidad correcta. AnimationTimer wraps a Bevy so each player entity knows when to advance to the next animation frame. Each tick represents one slice of time; once the timer hits its interval we move to the next sprite in the sheet. 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? Deja que nuestro engranaje finja ser el interior Cuando lo leemos, y Lo mismo ocurre con las cartas, por lo que podemos es sin extraer manualmente el valor interno primero. Deref Timer DerefMut timer.tick(time.delta()) AnimationTimer So we are renaming the Timer to AnimationTimer? Estamos envueltos en el temporizador, no lo renombramos. como una pequeña caja que contiene un , más una etiqueta que dice "este pertenece a la animación del jugador." Cuando engendramos a un jugador creamos una nueva y póngalo en esa caja, para que cada jugador pueda tener su propio temporizador si necesitábamos varios héroes. AnimationTimer Timer Timer So it’s an instance of AnimationTime? Y sí, es una estructura tuple que contiene 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 remembers which way the player points, whether they are moving, and whether they just started or stopped. Systems read this to choose animation rows and reset frames when movement changes. AnimationState Descargar El Jugador 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. 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)), )); } Establece la dirección de partida y señala que el personaje está vacío en este momento y fue vacío en el último marco. AnimationState { facing, moving: false, was_moving: false } crea un reloj repetitivo que incendia todos los seconds to advance the spritesheet. AnimationTimer(Timer::from_seconds(ANIM_DT, TimerMode::Repeating)) ANIM_DT ? What’s an AssetServer El es el cargador y gestor de archivos de Bevy que gestiona la carga y el caché de activos de juego como imágenes, sonidos y modelos 3D. AssetServer Cuando llamas , no carga inmediatamente el archivo en la memoria. En su lugar, devuelve un manejo que puede usar más adelante. Esto se llama "carga perezosa" - la carga real del archivo ocurre en el fondo, y Bevy le notificará cuando esté listo. asset_server.load("path/to/sprite.png") Este método es eficaz porque: Varias entidades pueden compartir el mismo sprite sin cargarlo varias veces Los activos solo se cargan cuando son realmente necesarios Bevy puede optimizar y cargar activos por lotes para un mejor rendimiento If an asset fails to load, it won’t crash your entire game En nuestro caso, solicita la hoja y devuelve un manejo para rastrear su estado de carga. asset_server.load("sprites/player.png") Sistema de movimiento Ahora que tenemos a nuestro jugador engendrado con todos los componentes necesarios, necesitamos actualizar nuestro sistema de movimiento existente para rastrear la dirección a la que se enfrenta el jugador. Esto es crucial para que el sistema de animación sepa qué fila de la hoja de esprites utilizar - cada dirección ( arriba, abajo, izquierda, derecha) corresponde a una fila diferente en nuestro atlas de esprites. This updated system will detect the movement direction and update the En consecuencia, nuestro sistema de animación puede elegir la línea de sprite correcta para las animaciones de caminar en cada dirección. 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; } } La consulta pide la prueba para la única entidad etiquetada , giving us mutable access to its y Construimos un vector de dirección a partir de las teclas presionadas, lo normalizamos para que la entrada diagonal no sea más rápida, y movemos al jugador por velocidad × tiempo de cuadro.La lógica de cara compara la fuerza horizontal vs. vertical para decidir de qué manera debe verse el sprite. Player Transform AnimationState Implementación de animación Ahora tenemos todas las piezas en su lugar - nuestro jugador puede moverse en diferentes direcciones, y estamos siguiendo en qué dirección se enfrentan.La pieza final es el sistema de animación que actualiza realmente los marcos de sprite para crear la animación caminando. Este sistema toma la información de dirección de nuestro sistema de movimiento y utiliza el temporizador de animación para circular a través de los marcos de sprite a la velocidad correcta. maneja la lógica compleja de cambiar entre diferentes líneas de animación cuando el jugador cambia de dirección, y asegura transiciones suaves entre los estados de caminar y inactividad. // 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, } } ambos verifica el resultado y nombra las piezas que necesitamos. Si la consulta tiene éxito, el código se vincula , de , and Así que podemos usarlos más tarde. Si falla (no hay jugador, o más de uno), golpeamos el rama y la salida inmediata. Rust utiliza el type for this: means “query returned exactly one result,” significa “algo sobre esa consulta no coincidió”. let Ok((mut anim, mut timer, mut sprite)) = query.single_mut() else { return; }; anim timer sprite else Result Ok Err Después de eso nos Es un , que es el tipo de Rust "puede haber un valor". significa que el atlas de textura existe y podemos ajustarlo; significa que aún no se ha cargado, por lo que salta y deja que el siguiente cuadro lo pruebe de nuevo.Es el mismo patrón que usaría al comprobar un mapa o caché: solo use el valor cuando la búsqueda devuelva algo. match Option Some(atlas) None Trae el estado de animación, el temporizador y el manejo de sprite para el jugador. Descubre qué fila del atlas coincide con la cara actual, snaps a esa fila cuando cambia la dirección, y utiliza el temporizador para pasar por las columnas a un ritmo constante. Cuando el movimiento se detiene, resetamos el temporizador para que la animación descanse en el último marco mostrado. animate_player Crear el Plugin del jugador // 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)); } } En Bevy, un plugin es simplemente un struct que sabe cómo registrar sistemas, recursos y activos. por , le damos a Bevy una lista de verificación: cada vez que se añade este plugin a la aplicación, ejecute el código dentro para configurar todo lo que necesita la característica del jugador. de convertirse en un tangle de llamadas específicas de jugadores. PlayerPlugin Plugin PlayerPlugin main.rs El El método es la lista de verificación.Bevy nos pasa una mutable , y nos enfocamos en los sistemas que nos importan. Está programado en Por lo tanto, el sprite aparece tan pronto como el juego se lanza. and Entra en la programa para que ejecuten cada cuadro – manipular la entrada y la animación en lockstep. En el Se activa automáticamente todo el flujo de jugadores. 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? Sí, el “Cualquier plugin debe proporcionar una function.” 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. Plugin build(&self, app: &mut App) What’s a trait? Un rasgo es un contrato que describe qué métodos debe proporcionar un tipo. Título: “Dame una funcionamiento para que pueda registrar sus sistemas”. , nos conectamos al proceso de arranque de Bevy e inyectamos nuestro propio código de configuración. se comporta como cualquier otro plugin de Bevy, pero instala nuestros sistemas específicos para el jugador. Plugin build PlayerPlugin PlayerPlugin La integración final // 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(); } Vamos a correrlo. cargo run Este post fue publicado originalmente en mi blog. Este artículo fue publicado originalmente en . Mi blog Conecte conmigo en LinkedIn, sígueme en X para las últimas actualizaciones. LinkedIn X