Through the code before, have come up with a according to the key direction of movement of the rectangle, if we put a spacecraft texture loaded into a rectangle, was a moving ship, texture loading if they achieve very tedious, fortunately, SDL help us deal with the details, as long as do a man, I also can quickly load textures.

The elves

Now let’s talk about sprites. If you’ve been in game development, you should understand what sprites are, but I’m going to talk nonsense. If you want to simulate a fire breathing dragon effect, if only to make a 3 d game, in order to we can, of course, to write a particle animation simulation fire, but 2 d game can don’t need so complicated, a dragon with a picture, according to the picture several regions, which has shut up the status of the pictures, state of mouth spray fire image, and then according to different area, You can show the effect of dragon breathing fire.

Here are some images to get a sense of what this means.

This picture contains nine small pictures, the middle is the spaceship normal state, if the keyboard click up, let the previous rectangle show the first row of the second picture, if click left will show the second row of the first picture, other operations similar to understand.

Cargo. Toml configuration changed, because image related stuff is coming up.

[dependencies.sdl2]
version = "0.29"
default-features = false
features = ["image"]
Copy the code

Loading textures

But bite by bite, familiarize yourself with loading textures first. Now I’m going to create a ship, so I’m going to change the name, RectView doesn’t make sense, so I’m going to kill it, and I’m going to change it to ShipView, which is a ShipView, and I’m going to change the contents a little bit, but it’s essentially the same, just the name.

use sdl2::render::{Texture, TextureQuery};
struct Ship {
    rect: Rectangle,
    tex: Texture,
}

pub struct ShipView {
    player: Ship,
}
Copy the code

Next, change the impl RectView to impl ShipView.

impl ShipView {
    pub fn new(phi: &mut Phi) -> Self {
        let tex = phi.canvas
        .load_texture(Path::new("assets/spaceship.png"))
        .unwrap();
        let TextureQuery { width, height, .. } = texture.query();
        Self {
            player: Ship {
                // width: u32, height: u32
                rect: Rectangle {
                    x: 64.0,
                    y: 64.0,
                    w: width as f64,
                    h: height as f64,
                },
                tex
            },
        }
    }
}
Copy the code

Going back to the Render function of ShipView and changing the render operation

impl View for ShipView {
    fn render(&mut self, context: &mut Phi) -> ViewAction {
        ...
        phi.canvas.copy(
        &mut self.player.tex,
        Rectangle {
            x: 0.0, y: 0.0,
            w: self.player.rect.w,
            h: self.player.rect.h,
        }.to_sdl(),
        self.player.rect.to_sdl()); }}Copy the code

Now that I’ve rendered the entire image, I’m going to take nine smaller images, show only one of them, and take the one in the middle. Because the whole thing is 129 times 117, which is exactly nine, so each of the smaller ones is 43 times 39, and since the size of the smaller ones is fixed let’s define constants, because this thing is fixed.

const SHIP_W = 43.0;
const SHIP_H = 39.0;

// ...

impl ShipView {
    pub fn new(phi: &mut Phi) -> Self {
        let tex = phi.canvas
        .load_texture(Path::new("assets/spaceship.png"))
        .unwrap();
        Self {
            player: Ship {
                rect: Rectangle {
                    x: 64.0,
                    y: 64.0,
                    w: SHIP_W,
                    h: SHIP_H,
                },
                tex,
            },
        }
    }
}

// ...

phi.canvas.copy(
    &mut self.player.tex,
    Rectangle {
        x: SHIP_W, y: SHIP_H,
        w: self.player.rect.w,
        h: self.player.rect.h,
    }.to_sdl(),
    self.player.rect.to_sdl());
Copy the code

This shows the middle of the grid of the small picture, the display effect should be good, is a little ugly, yellow background has not been dealt with, and, how to achieve the above mentioned wizard figure?

Get into the business

Each time you copy an area of the image, you can display that area. In fact, you can copy all the areas at once, and then switch to the corresponding area according to the keyboard events.

struct Ship {
    rect: Rectangle,
    sprites: Vec<Sprite>,
    current: ShipFrame,
}

#[derive(Clone)]
pub struct Sprite {
    tex: Rc<RefCell<Texture>>,
    src: Rectangle,
}

impl Sprite {
    pub fn new(texture: Texture) -> Self {
        let TextureQuery { width, height, .. } = texture.query();
        Self {
            tex: Rc::new(RefCell::new(texture)),
            src: Rectangle {
                w: width as f64,
                h: height as f64,
                x: 0.0,
                y: 0.0,}}}pub fn load(canvas: &Renderer, path: &str) - >Option<Sprite> {
        canvas.load_texture(Path::new(path)).ok().map(Sprite::new)
    }
}
Copy the code

The Sprite structure has an odd thing, Rc

>. I’m using Rc as a smart pointer, so explain that. Rc stands for reference counter, and if you’ve had any iOS development experience you’ll understand that memory in Objective-C is managed by reference counting. Rc is similar. In general, a variable can know that it has a value, and if a value has multiple owners, that’s where Rc comes in. If a value has one owner, the counter is 1, if it has two, the counter is 2, and if it has zero, the value can be cleaned up. The current situation is that the Ship structure contains multiple Sprite instances, but we only use one image, so we use Rc to refer to the same resource. Also, because the phi.canvas.copy argument needs to be self, we won’t change the image, but we need to bypass the borrow checker, and RefCell comes in handy. Now lay out the small image area into ShipView.

impl ShipView {
    pub fn new(phi: &mut Phi) -> Self {
        let sprite_sheet = Sprite::load(&phi.canvas, "assets/spaceship.png")
        .unwrap();
        let mut sprites = Vec::with_capacity(9);
        for y in 0.3 {
            for x in 0.3 {
                sprites.push(
                    sprite_sheet
                        .region(Rectangle {
                            w: SHIP_W,
                            h: SHIP_H,
                            x: SHIP_W * x as f64,
                            y: SHIP_H * y as f64,
                        })
                        .unwrap(),
                )
            }
        }
        Self {
            player: Ship {
                rect: Rectangle {
                    x: 64.0,
                    y: 64.0,
                    w: 32.0,
                    h: 32.0,
                },
                sprites,
                current: ShipFrame::MidNorm,
            },
        }
    }
}
Copy the code

After controlling keyboard event handling, the render function also needs to be changed.

#[derive(Clone, Copy)]
enum ShipFrame {
    UpNorm = 0,
    UpFast = 1,
    UpSlow = 2,
    MidNorm = 3,
    MidFast = 4,
    MidSlow = 5,
    DownNorm = 6,
    DownFast = 7,
    DownSlow = 8,}impl View for ShipView {
    fn render(&mut self, context: &mut Phi) -> ViewAction {
        let (w, h) = context.output_size();
        let canvas = &mut context.canvas;
        let events = &mut context.events;

        if events.now.quit || events.now.key_escape == Some(true) {
            return ViewAction::Quit;
        }

        let diagonal: bool =
            (events.key_up ^ events.key_down) &&
            (events.key_left ^ events.key_right);
        let moved = if diagonal { 1.0 / 2.0 f64.sqrt() } else { 1.0 } *
        PLAYER_SPEED;
        let dx: f64 = match (events.key_left, events.key_right) {
            (true.true) | (false.false) = >0.0,
            (true.false) => -moved,
            (false.true) => moved,
        };

        let dy: f64 = match (events.key_up, events.key_down) {
            (true.true) | (false.false) = >0.0,
            (true.false) => -moved,
            (false.true) => moved,
        };

        self.player.rect.x += dx;
        self.player.rect.y += dy;

        canvas.set_draw_color(Color::RGB(0.30.0));
        canvas.clear();

        canvas.set_draw_color(Color::RGB(200.200.50));

        let movable_region: Rectangle = Rectangle::new(0.0.0.0, w * 0.7, h);
        self.player.rect = self.player.rect
            .move_inside(movable_region)
            .unwrap();

        self.player.current =
            if dx == 0.0 && dy < 0.0 { ShipFrame::UpNorm }
            else if dx > 0.0 && dy < 0.0 { ShipFrame::UpFast }
            else if dx < 0.0 && dy < 0.0 { ShipFrame::UpSlow }
            else if dx == 0.0 && dy == 0.0 { ShipFrame::MidNorm }
            else if dx > 0.0 && dy == 0.0 { ShipFrame::MidFast }
            else if dx < 0.0 && dy == 0.0 { ShipFrame::MidSlow }
            else if dx == 0.0 && dy > 0.0 { ShipFrame::DownNorm }
            else if dx > 0.0 && dy > 0.0 { ShipFrame::DownFast }
            else if dx < 0.0 && dy > 0.0 { ShipFrame::DownSlow }
            else { unreachable!() };
        
        self.player.sprites[self.player.current as usize]
            .render(canvas, self.player.rect);

        ViewAction::None}}impl Rectangle {
    pub fn move_inside(self, parent: Rectangle) -> Option<Rectangle> {
        if self.w > parent.w || self.h > parent.h {
            return None;
        }
        Some(Rectangle {
            w: self.w,
            h: self.h,
            x: if self.x < parent.x {
                parent.x
            } else if self.x + self.w >= parent.x + parent.w {
                parent.x + parent.w - self.w
            } else {
                self.x
            },
            y: if self.y < parent.y {
                parent.y
            } else if self.y + self.h >= parent.y + parent.h {
                parent.y + parent.h - self.h
            } else {
                self.y
            },
        })
    }

    pub fn contains(&self, rect: Rectangle) -> bool {
        let x_min = rect.x;
        let x_max = x_min + rect.w;
        let y_min = rect.y;
        let y_max = y_min + rect.h;

        x_min >= self.x && x_min <= self.x + self.w && x_max >= self.x &&
            x_max <= self.x + self.w && y_min >= self.y
            && y_min <= self.y + self.h &&
            y_max >= self.y && y_max <= self.y + self.h
    }
}

impl Sprite {
    pub fn region(&self, rect: Rectangle) -> Option<Sprite> {
        let src: Rectangle = Rectangle {
            x: rect.x + self.src.x,
            y: rect.y + self.src.y, .. rect };if self.src.contains(src) {
            Some(Sprite {
                tex: self.tex.clone(),
                src,
            })
        } else {
            None}}pub fn render(&self, canvas: &mut Renderer, dest: Rectangle) {
        canvas.copy(&mut self.tex.borrow_mut(),
            self.src.to_sdl(), dest.to_sdl())
            .expect("failed to copy texture"); }}Copy the code

Now when you do that, you can see a spaceship and then you can see different patterns based on the arrow keys on the keyboard. We’re done here, but we could have written more intuitive code to have the canvas render sprites, not the sprites themselves.

self.player.sprites[self.player.current as usize] .render(canvas, self.player.rect);

pub trait CopySprite {
    fn copy_sprite(&mut self, sprite: &Sprite, dest: Rectangle);
}

impl<'window> CopySprite for Renderer<'window> {
    fn copy_sprite(&mut self, sprite: &Sprite, dest: Rectangle) {
        sprite.render(self, dest); }}impl View for ShipView {
    fn render(&mut self, context: &mut Phi) -> ViewAction {
        // ...
        
        canvas.copy_sprite(
            &self.player.sprites[self.player.current as usize].self.player.rect);
        
        // ...}}Copy the code

Use traits to abstract behavior.


That’s it for now. Let’s see what else we have to deal with.