经过以前的代码, 已经搞出了个根据方向键移动的矩形, 咱们要是把一个飞船纹理加载到矩形上,就是个可移动的飞船了, 纹理加载若是本身实现,很是繁琐,幸亏 SDL 帮咱们处理了细节,只要作一个调参侠,我也能够快速加载纹理。canvas
不过标题说是处理精力,个么如今先讲一下精灵(sprite
)是什么玩意,若是从事过游戏开发,应该能理解精灵是什么玩意,可是废话仍是要讲的。假如你要模拟一条火龙喷火的效果,要是作个 3D 游戏,为了效果逼真咱们固然能够写个粒子动画模拟火焰,不过 2D 游戏能够不须要这么复杂,一条龙用一张图片显示,这张图片分几个区域,上面有闭嘴的状态图片,张嘴喷火的状态图片,而后显示不一样区域,就能够表现龙喷火的效果。ide
这里放一下图片素材,好理解上面的意思。函数
这张图包含九个小图,中间的是飞船普通状态,要是键盘点击上键,就让以前的矩形显示第一排第二个图,若是点击左就显示第二排第一个图,其余操做相似理解就好。布局
Cargo.toml 配置改一下,由于立刻要用到 image 相关到东西。动画
[dependencies.sdl2]
version = "0.29"
default-features = false
features = ["image"]
复制代码
可是饭要一口一口吃,先把加载纹理的事熟悉一下。如今要搞个飞船,先把名字改一下,RectView
不符合语义化,建议击毙,改为 ShipView
,这是个飞船视图,里面的内容稍微改一下,本质上其实没区别,只是名字变了。ui
use sdl2::render::{Texture, TextureQuery};
struct Ship {
rect: Rectangle,
tex: Texture,
}
pub struct ShipView {
player: Ship,
}
复制代码
接下来把 impl RectView
改一下,天然是改为 impl ShipView
。spa
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
},
}
}
}
复制代码
解构语法看着真爽,回到 ShipView
的 render
函数,把渲染操做修改一下指针
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());
}
}
复制代码
如今就很厉害了,把整张图都渲染出来了,接下来把九个小图,只显示其中一个,就拿中间那个开刀。
由于整图大小是 129 * 117,恰好九个,因此每一个小图大小是 43 * 39,既然小图大小肯定那就定义常量吧,反正这个东西固定了。code
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());
复制代码
这样就显示中间那个格子的小图了,显示效果应该还不错,就是丑了点,黄黄的背景还没处理,并且,要怎么实现上面说到的精灵图呢?orm
每次执行 copy 图片某区域操做,就能达到显示该区域的效果,其实咱们能够一次性 copy 全部的区域,而后根据键盘事件切换到对应区域就解决问题了。
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)
}
}
复制代码
Sprite
结构体有个奇怪的东西,Rc<RefCell<Texture>>
。这里用到了 Rc
这个智能指针,那就解释一下这玩意。Rc
其实全称就是引用计数器,若是有过 iOS
开发经验很好理解,Objective-C
的内存是经过引用计数来管理的,Rc
道理上相似。一般状况下,一个变量能够明确地知晓本身有某个值,而若是一个值存在多个全部者,就是 Rc
派上用场的时候了。某个值有一个全部者,计数器就是 1,若是有两个,计数器就是 2,若是有 0 个,值就能够被清理。如今的状况是 Ship
结构体包含多个精灵实例,可是咱们只用一张图片,因此用上 Rc
来引用同一个资源。同时,由于 phi.canvas.copy
函数参数须要 self
可变,虽然咱们不会改变图片,可是须要绕过借用检查器,这时候 RefCell
派上用场。如今把小图区域布局到 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,
},
}
}
}
复制代码
以后控制键盘事件处理,因此 render
函数也要改一下。
#[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.0f64.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");
}
}
复制代码
如今执行以后,能够看到一个飞船而后根据键盘方向键就能够看到不一样的图案。其实到这里已经完成了,其实还能够把代码写得更符合直觉一些,就是让 canvas
来渲染精灵,而不是精灵渲染其自己。
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);
// ...
}
}
复制代码
利用 trait
来抽象行为。
目前就这样吧,接下来看看还有什么要处理到。