rusty_engine/
sprite.rs

1/// Sprites are the images that make up a game
2use bevy::prelude::{Component, Quat, Transform, Vec2, Vec3};
3
4use crate::physics::Collider;
5
6/// A [`Sprite`] is the basic abstraction for something that can be seen and interacted with.
7/// Players, obstacles, etc. are all sprites.
8#[derive(Clone, Component, Debug, PartialEq)]
9pub struct Sprite {
10    /// READONLY: A way to identify a sprite. This must be unique, or else the game will crash.
11    pub label: String,
12    /// READONLY: File used for this sprite's image
13    pub filepath: PathBuf,
14    /// READONLY: File used for this sprite's collider. Note that this file will not exist if the
15    /// sprite does not have a collider, but if you set the `collider` field to a collider and then
16    /// call the `write_collider` method, the file will be written for you!
17    pub collider_filepath: PathBuf,
18    /// SYNCED: Where you are in 2D game space. Positive x is right. Positive y is up. (0.0, 0.0) is the
19    /// center of the screen.
20    pub translation: Vec2,
21    /// SYNCED: Depth of the sprite. 0.0 (back) to 999.0 (front)
22    pub layer: f32,
23    /// SYNCED: Direction you face in radians. See constants UP, DOWN, LEFT, RIGHT
24    pub rotation: f32,
25    /// SYNCED: 1.0 is the normal 100%
26    pub scale: f32,
27    /// Whether or not to calculate collisions
28    pub collision: bool,
29    /// The actual collider for this sprite
30    pub collider: Collider,
31    /// If set to `true`, then the collider shown for this sprite will be regenerated (see also
32    /// [`Engine.show_colliders`](crate::prelude::Engine)). Normally you shouldn't touch this, but
33    /// if you manually replace a `Sprite`'s [`Collider`] in a game logic function, then you need to
34    /// set this to true.
35    pub collider_dirty: bool,
36}
37
38/// Reads the collider file and creates the collider
39fn read_collider_from_file(filepath: &Path) -> Collider {
40    match File::open(filepath) {
41        Ok(fh) => match ron::de::from_reader::<_, Collider>(fh) {
42            Ok(collider) => collider,
43            Err(e) => {
44                eprintln!("failed deserializing collider from file: {}", e);
45                Collider::NoCollider
46            }
47        },
48        Err(e) => {
49            eprintln!("failed to open collider file: {}", e);
50            Collider::NoCollider
51        }
52    }
53}
54
55impl Sprite {
56    /// `label` should be a unique string (it will be used as a key in the hashmap
57    /// [`Engine::sprites`](crate::prelude::Engine)). `file_or_preset` should either be a
58    /// [`SpritePreset`] variant, or a relative path to an image file inside the `assets/`
59    /// directory. If a collider definition exists in a file with the same name as the image file,
60    /// but with the `.collider` extension, then the collider will be loaded automatically. To
61    /// create a collider file you can either run the `collider` example, or
62    /// programmatically create a [`Collider`], set the sprite's `.collider` field to it, and call
63    /// the sprite's `.write_collider()` method.  All presets have collider files already.
64    pub fn new<S: Into<String>, P: Into<PathBuf>>(label: S, file_or_preset: P) -> Self {
65        let label = label.into();
66        let filepath = file_or_preset.into();
67        let mut collider_filepath = filepath.clone();
68        collider_filepath.set_extension("collider");
69        let actual_collider_filepath = PathBuf::from("assets").join(&collider_filepath);
70        let collider = if actual_collider_filepath.exists() {
71            read_collider_from_file(actual_collider_filepath.as_path())
72        } else {
73            eprintln!(
74                "warning: could not find collider file {} -- consider creating one with the `collider` example.",
75                actual_collider_filepath.to_string_lossy()
76            );
77            Collider::NoCollider
78        };
79        Self {
80            label,
81            filepath,
82            collider_filepath,
83            translation: Vec2::default(),
84            layer: f32::default(),
85            rotation: f32::default(),
86            scale: 1.0,
87            collision: false,
88            collider,
89            collider_dirty: true,
90        }
91    }
92
93    /// Do the math to convert from Rusty Engine translation+rotation+scale+layer to Bevy's Transform
94    #[doc(hidden)]
95    pub fn bevy_transform(&self) -> Transform {
96        let mut transform = Transform::from_translation(self.translation.extend(self.layer));
97        transform.rotation = Quat::from_axis_angle(Vec3::Z, self.rotation);
98        transform.scale = Vec3::splat(self.scale);
99        transform
100    }
101
102    /// Attempt to take the current collider and write it to collider_filepath. If there isn't a
103    /// collider, or writing fails, then `false` is returned. Otherwise `true` is returned.
104    pub fn write_collider(&self) -> bool {
105        if self.collider == Collider::NoCollider {
106            return false;
107        }
108        // Bevy's asset system is relative from the assets/ subdirectory, so we must be too
109        let filepath = PathBuf::from("assets").join(self.collider_filepath.clone());
110        let mut fh = match File::create(filepath) {
111            Ok(fh) => fh,
112            Err(e) => {
113                eprintln!("failed creating collider file: {}", e);
114                return false;
115            }
116        };
117
118        let collider_ron = match ron::ser::to_string_pretty(&self.collider, Default::default()) {
119            Ok(r) => r,
120            Err(e) => {
121                eprintln!("failed converting collider to ron: {}", e);
122                return false;
123            }
124        };
125        match fh.write_all(collider_ron.as_bytes()) {
126            Ok(_) => true,
127            Err(e) => {
128                eprintln!("failed writing collider file: {}", e);
129                false
130            }
131        }
132    }
133    /// Add a collider point. `p` is a `Vec2` in worldspace (usually the mouse coordinate). See the
134    /// `collider` example.
135    pub fn add_collider_point(&mut self, mut p: Vec2) {
136        self.collider_dirty = true;
137        // If there isn't a collider, we better switch to one
138        if self.collider == Collider::NoCollider {
139            self.collider = Collider::Poly(Vec::new());
140        }
141        // Add the current point to the collider
142        if let Collider::Poly(points) = &mut self.collider {
143            // untranslate (make p relative to the sprite's position)
144            p -= self.translation;
145            // unscale (make p the same scale as the sprite)
146            p *= 1.0 / self.scale;
147            // unrotate (make p the same rotation as the sprite)
148            let mut p2 = Vec2::ZERO;
149            let sin = (-self.rotation).sin();
150            let cos = (-self.rotation).cos();
151            p2.x = p.x * cos - p.y * sin;
152            p2.y = p.x * sin + p.y * cos;
153            points.push(p2);
154        }
155    }
156    /// Change the last collider point. `p` is a `Vec2` in worldspace (usually the mouse
157    /// coordinate). See the `collider` example.
158    pub fn change_last_collider_point(&mut self, mut p: Vec2) {
159        self.collider_dirty = true;
160        // If there isn't a collider, create one with a "last point" to change
161        if self.collider == Collider::NoCollider {
162            self.collider = Collider::Poly(vec![Vec2::ZERO]);
163        }
164        // Add the current point to the collider
165        if let Collider::Poly(points) = &mut self.collider {
166            // If the collider exists, but doesn't have any points, add a "last point" to modify.
167            if points.is_empty() {
168                points.push(Vec2::ZERO);
169            }
170            // untranslate (make p relative to the sprite's origin instead of the world's origin)
171            p -= self.translation;
172            // unscale (make p the same scale as the sprite)
173            p *= 1.0 / self.scale;
174            // unrotate (make p the same rotation as the sprite)
175            let length = points.len();
176            let p2 = points.get_mut(length - 1).unwrap(); // mutable reference to "last point"
177            let sin = (-self.rotation).sin();
178            let cos = (-self.rotation).cos();
179            p2.x = p.x * cos - p.y * sin;
180            p2.y = p.x * sin + p.y * cos;
181        }
182    }
183}
184
185use std::{
186    array::IntoIter,
187    fs::File,
188    io::Write,
189    path::{Path, PathBuf},
190};
191
192/// Sprite presets using the asset pack all have colliders
193#[derive(Copy, Clone, Debug, PartialEq, Eq)]
194pub enum SpritePreset {
195    RacingBarrelBlue,
196    RacingBarrelRed,
197    RacingBarrierRed,
198    RacingBarrierWhite,
199    RacingCarBlack,
200    RacingCarBlue,
201    RacingCarGreen,
202    RacingCarRed,
203    RacingCarYellow,
204    RacingConeStraight,
205    RollingBallBlue,
206    RollingBallBlueAlt,
207    RollingBallRed,
208    RollingBallRedAlt,
209    RollingBlockCorner,
210    RollingBlockNarrow,
211    RollingBlockSmall,
212    RollingBlockSquare,
213    RollingHoleEnd,
214    RollingHoleStart,
215}
216
217impl SpritePreset {
218    /// Retrieve the asset filepath. You probably won't need to call this method, since the methods
219    /// which create [`Sprite`]s will accept [`SpritePreset`]s and call this method via the
220    /// `impl From<SpritePreset> for PathBuf` implementation.
221    pub fn filepath(&self) -> PathBuf {
222        match self {
223            SpritePreset::RacingBarrelBlue => "sprite/racing/barrel_blue.png",
224            SpritePreset::RacingBarrelRed => "sprite/racing/barrel_red.png",
225            SpritePreset::RacingBarrierRed => "sprite/racing/barrier_red.png",
226            SpritePreset::RacingBarrierWhite => "sprite/racing/barrier_white.png",
227            SpritePreset::RacingCarBlack => "sprite/racing/car_black.png",
228            SpritePreset::RacingCarBlue => "sprite/racing/car_blue.png",
229            SpritePreset::RacingCarGreen => "sprite/racing/car_green.png",
230            SpritePreset::RacingCarRed => "sprite/racing/car_red.png",
231            SpritePreset::RacingCarYellow => "sprite/racing/car_yellow.png",
232            SpritePreset::RacingConeStraight => "sprite/racing/cone_straight.png",
233            SpritePreset::RollingBallBlue => "sprite/rolling/ball_blue.png",
234            SpritePreset::RollingBallBlueAlt => "sprite/rolling/ball_blue_alt.png",
235            SpritePreset::RollingBallRed => "sprite/rolling/ball_red.png",
236            SpritePreset::RollingBallRedAlt => "sprite/rolling/ball_red_alt.png",
237            SpritePreset::RollingBlockCorner => "sprite/rolling/block_corner.png",
238            SpritePreset::RollingBlockNarrow => "sprite/rolling/block_narrow.png",
239            SpritePreset::RollingBlockSmall => "sprite/rolling/block_small.png",
240            SpritePreset::RollingBlockSquare => "sprite/rolling/block_square.png",
241            SpritePreset::RollingHoleEnd => "sprite/rolling/hole_end.png",
242            SpritePreset::RollingHoleStart => "sprite/rolling/hole_start.png",
243        }
244        .into()
245    }
246
247    /// An iterator that iterates through presets. Mostly useful for things like level builders
248    /// when you want to be able to rotate something through each preset.
249    pub fn variant_iter() -> IntoIter<SpritePreset, 20> {
250        static SPRITE_PRESETS: [SpritePreset; 20] = [
251            SpritePreset::RacingBarrelBlue,
252            SpritePreset::RacingBarrelRed,
253            SpritePreset::RacingBarrierRed,
254            SpritePreset::RacingBarrierWhite,
255            SpritePreset::RacingCarBlack,
256            SpritePreset::RacingCarBlue,
257            SpritePreset::RacingCarGreen,
258            SpritePreset::RacingCarRed,
259            SpritePreset::RacingCarYellow,
260            SpritePreset::RacingConeStraight,
261            SpritePreset::RollingBallBlueAlt,
262            SpritePreset::RollingBallBlue,
263            SpritePreset::RollingBallRedAlt,
264            SpritePreset::RollingBallRed,
265            SpritePreset::RollingBlockCorner,
266            SpritePreset::RollingBlockNarrow,
267            SpritePreset::RollingBlockSmall,
268            SpritePreset::RollingBlockSquare,
269            SpritePreset::RollingHoleEnd,
270            SpritePreset::RollingHoleStart,
271        ];
272        SPRITE_PRESETS.into_iter()
273    }
274
275    /// The core logic of both `next` and `prev`
276    fn shifted_by(&self, amount: isize) -> SpritePreset {
277        let len = SpritePreset::variant_iter().len();
278        let index = SpritePreset::variant_iter()
279            .enumerate()
280            .find(|(_, a)| *a == *self)
281            .unwrap()
282            .0;
283        let mut new_index_isize = index as isize + amount;
284        while new_index_isize < 0 {
285            new_index_isize += len as isize;
286        }
287        let new_index = (new_index_isize as usize) % len;
288        SpritePreset::variant_iter().nth(new_index).unwrap()
289    }
290
291    /// Just get the next sprite preset in the list, without dealing with an iterator
292    pub fn next(&self) -> SpritePreset {
293        self.shifted_by(-1)
294    }
295
296    /// Just get the previous sprite preset in the list, without dealing with an iterator
297    pub fn prev(&self) -> SpritePreset {
298        self.shifted_by(1)
299    }
300}
301
302impl From<SpritePreset> for PathBuf {
303    fn from(sprite_preset: SpritePreset) -> Self {
304        sprite_preset.filepath()
305    }
306}