タイルベース描画とアニメーションの実装
ぼっちgamedev Advent Calendar 2018の11日目。
・タイルの描画
・タイルベースアニメーション
について書く。
タイルの描画
4日目に書いた通り、ゲーム画面が複数のタイルの組み合わせで表現される場合がある。以下に関連するコードを引用する。
struct Renderable<T> {
position: Point,
scale: f32,
rotate: f32,
opacity: f32,
prop: T,
}
trait Drawing {
fn draw(&self, ctx: &RenderContext) {
unimplemented!();
}
}
trait Rendering {
fn render(&self, ctx: &RenderContext) {
unimplemented!();
}
}
impl<T: Drawing> Rendering for Renderable<T> {
fn render(&self, ctx: &RenderContext) {
ctx.set_coordinate(self.position);
ctx.set_transform(self.scale, self.rotate);
ctx.set_opacity(self.opacity);
self.prop.draw(ctx);
}
}
struct TileProperty<'a> {
tiles: &'a TileManager,
tile_id: usize,
}
type Tile<'a> = Renderable<TileProperty<'a>>
impl<'a> Drawing for TileProperty<'a> {/* 略 */}
TileManagerとタイル描画の実装を省略していたため、ここから実装していくことにする。まずタイル描画の仕様を確認し、それに基づいてTileManagerを実装していく。TileManagerには、タイルマップの指定IDのタイルをRenderContextの描画基準位置に描画するメソッドを想定し、以下のようにタイル描画を行う。
impl<'a> Drawing for TileProperty<'a> {
fn draw(&self, ctx: &mut RenderContext) {
self.tiles.copy(ctx, self.tile_id);
}
}
よって、TileManagerの実装は以下のようになる。
struct TileManager<'a> {
tilemap: &'a Image<'a>,
tile_width: i32,
tile_height: i32,
}
impl<'a> TileManager<'a> {
fn copy(&self, ctx: &mut RenderContext, tile_id: usize) {
let ix = self.tilemap.width / self.tile_width;
let dx: i32 = (tile_id as i32) % ix;
let dy: i32 = ((tile_id as i32)-dx) / ix;
let x = dx * self.tile_width;
let y = dy * self.tile_height;
ctx.draw_image(self.tilemap, x, y, self.tile_width, self.tile_height);
}
}
描画の仕組みはRenderContextの実装に依存することになるため、ここまででひとまずコンパイルが通ることを確認したい場合は、以下のようにImageとRenderContextを実装する。Imageのdataフィールドの型をStringのエイリアスにしているが、あくまでコンパイルを通すためであるため、正しい実装では無い。
#[derive(Debug, Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
type RawImageData = String;
struct Image<'a> {
path: String,
data: &'a RawImageData,
width: i32,
height: i32,
}
struct Renderable<T> {
position: Point,
scale: f32,
rotate: f32,
opacity: f32,
prop: T,
}
struct RenderContext {
coordinate: Point,
opacity: f32,
scale: f32,
rotate: f32,
}
impl RenderContext {
fn set_coordinate(&mut self, pos: Point) {
self.coordinate = pos;
}
fn set_transform(&mut self, scale: f32, rotate: f32) {
self.scale = scale;
self.rotate = rotate;
}
fn set_opacity(&mut self, opacity: f32) {
self.opacity = opacity;
}
fn draw_image(&mut self, image: &Image, dx: i32, dy: i32, dw: i32, dh: i32) {
println!("Copy {} from {:?} to {:?}, to {:?}", image.path, Point{x: dx, y: dy}, Point{x: dx+dw, y: dy+dh}, self.coordinate);
}
}
trait Drawing {
fn draw(&self, ctx: &mut RenderContext) {
unimplemented!();
}
}
trait Rendering {
fn render(&self, ctx: &mut RenderContext) {
unimplemented!();
}
}
impl<T: Drawing> Rendering for Renderable<T> {
fn render(&self, ctx: &mut RenderContext) {
ctx.set_coordinate(self.position);
ctx.set_transform(self.scale, self.rotate);
ctx.set_opacity(self.opacity);
self.prop.draw(ctx);
}
}
struct TileProperty<'a> {
tiles: &'a TileManager<'a>,
tile_id: usize,
}
type Tile<'a> = Renderable<TileProperty<'a>>;
impl<'a> Drawing for TileProperty<'a> {
fn draw(&self, ctx: &mut RenderContext) {
self.tiles.copy(ctx, self.tile_id);
}
}
struct TileManager<'a> {
tilemap: &'a Image<'a>,
tile_width: i32,
tile_height: i32,
}
impl<'a> TileManager<'a> {
fn copy(&self, ctx: &mut RenderContext, tile_id: usize) {
let ix = self.tilemap.width / self.tile_width;
let iy = self.tilemap.height / self.tile_height;
let dx: i32 = (tile_id as i32) % ix;
let dy: i32 = ((tile_id as i32)-dx) / ix;
ctx.draw_image(self.tilemap, dx*self.tile_width, dy*self.tile_height as i32, self.tile_width, self.tile_height);
}
}
fn main() {
let data = String::from("bar");
let image = Image {
path: String::from("images/foo.png"),
data: &data,
width: 128,
height: 128,
};
let tile_manager = TileManager {
tilemap: &image,
tile_width: 32,
tile_height: 32,
};
let mut ctx = RenderContext {
coordinate: Point {x: 0, y: 0},
opacity: 1.,
scale: 1.,
rotate: 0.,
};
let tile = Tile {
position: Point {x: 32, y: 0},
scale: 1.,
rotate: 0.,
opacity: 1.,
prop: TileProperty {
tiles: &tile_manager,
tile_id: 3,
},
};
tile.render(&mut ctx);
}
// Copy images/foo.png from Point { x: 96, y: 0 } to Point { x: 128, y: 32 }, to Point { x: 32, y: 0 }
タイルベースアニメーション
コンピュータゲームでは、パラパラ漫画のようにタイルを切り替えることでアニメーションを表現する。ここまででタイル描画を実装したため、アニメーションの実装は難しく無い。
TileProperty構造体にはtile_idフィールドを持たせたが、これを切り替えることでコピー対象のタイルも切り替わる。これを念頭に、アニメーションに必要な要素を考えてみる。
まず、アニメーションを構成する全てのコマ(タイル)の参照が必要となる。これはtile_idを集めた配列として表現すれば良い。そして各コマ毎の表示時間が必要だ。一定間隔で表示しても良いが、アニメーションの自由度が低下するため避けたい。これはコマ(タイル)と紐付いた情報となるため、tile_idと時間を持つ構造体を作ることにする。
struct AnimationFrame {
tile_id: usize,
time: u32,
}
type Animation = Vec<AnimationFrame>;
これをもとにアニメーションのコマを管理するAnimationManagerを実装する。AnimationManagerのupdateでは、前回の更新からの経過時間(ms)を計算し、該当タイルのtile_idを現在のフレーム番号のtile_idに変更する。すなわちAnimationManagerにアニメーション対象のタイルの変更可能な参照を渡す必要がある。以下に単純な実装を示す。
use std::time::{SystemTime, Duration};
struct AnimationState<'a, 'b> {
target: &'a mut TileProperty<'a>,
frame_number: usize,
elapsed: i32,
animation: &'b Animation,
is_loop: bool,
finished: bool,
}
struct AnimationManager<'a, 'b> {
states: Vec<AnimationState<'a, 'b>>,
last_updated: SystemTime,
}
fn elapsed_millis(st: &mut SystemTime) -> u64 {
let d = st.elapsed().unwrap();
let millis = d.as_secs() * 1000 + d.subsec_millis() as u64;
*st = SystemTime::now();
millis
}
impl<'a, 'b> AnimationManager<'a, 'b> {
fn push(&mut self, target: &'a mut TileProperty<'a>, animation: &'b Animation, is_loop: bool) {
let state = AnimationState {
target: target,
frame_number: 0,
elapsed: 0,
animation: animation,
is_loop: is_loop,
finished: false,
};
self.states.push(state);
}
fn update(&mut self) {
let delta = elapsed_millis(&mut self.last_updated) as i32;
for state in self.states.iter_mut() {
state.elapsed += delta;
let mut fnum = state.frame_number;
let mut head = state.animation[fnum].time as i32 - state.elapsed;
while head <= 0 && !state.finished {
state.elapsed = -head;
state.frame_number += 1;
if state.frame_number >= state.animation.len() {
if state.is_loop {
state.frame_number = 0;
} else {
state.finished = true;
}
}
fnum = state.frame_number;
state.target.tile_id = state.animation[fnum].tile_id;
head = state.animation[fnum].time as i32 - state.elapsed;
}
}
self.states.retain(|state| !state.finished);
}
}
コンパイルも通り、この実装でも一見問題無いように思える。が、AnimationManagerにアニメーション対象としてタイルを登録した後にrender関数を呼ぼうとするとコンパイル不可能となる。これはAnimationManagerにタイルのmutableな参照を渡した後に、タイルのimmutableな参照を要するrender関数を呼び出そうとするためである。これはRustにおける以下のルールに抵触する。
最初に、借用は全て所有者のスコープより長く存続してはなりません。 次に、次の2種類の借用のどちらか1つを持つことはありますが、両方を同時に持つことはありません。
・リソースに対する1つ以上の参照( &T )
・ただ1つのミュータブルな参照( &mut T )
The Rust Programming Language, First Edition - 参照と借用 より
これを回避するため、Renderableの定義を少し変更する必要がある。AnimationManagerの例のように、Renderableのpropは外部から変更を加えられる可能性を考慮しなければならない。参照先の値を変更可能なポインタを複数存在させるにはRc<RefCell<T>>というパターンを使う。
use std::cell::RefCell;
use std::rc::Rc;
struct Renderable<T> {
position: Point,
scale: f32,
rotate: f32,
opacity: f32,
prop: Rc<RefCell<T>>,
}
impl<'a, T: Drawing> Rendering for Renderable<T> {
fn render(&self, ctx: &mut RenderContext) {
ctx.set_coordinate(self.position);
ctx.set_transform(self.scale, self.rotate);
ctx.set_opacity(self.opacity);
self.prop.borrow().draw(ctx);
}
}
これに付随し、AnimationStateのtargetもRc<RefCell<T>>となるように変更する。
struct AnimationState<'a, 'b> {
target: Rc<RefCell<TileProperty<'a>>>,
frame_number: usize,
elapsed: i32,
animation: &'b Animation,
is_loop: bool,
finished: bool,
}
impl<'a, 'b> AnimationManager<'a, 'b> {
fn push(&mut self, target: Rc<RefCell<TileProperty<'a>>>, animation: &'b Animation, is_loop: bool) {
let state = AnimationState {
target: target,
frame_number: 0,
elapsed: 0,
animation: animation,
is_loop: is_loop,
finished: false,
};
self.states.push(state);
}
fn update(&mut self) {
let delta = elapsed_millis(&mut self.last_updated);
for state in self.states.iter_mut() {
state.elapsed += delta as i32;
let mut fnum = state.frame_number;
let mut head = state.animation[fnum].time as i32 - state.elapsed;
while head <= 0 && !state.finished {
state.elapsed = -head;
state.frame_number += 1;
if state.frame_number >= state.animation.len() {
if state.is_loop {
state.frame_number = 0;
} else {
state.finished = true;
}
}
fnum = state.frame_number;
let mut target = (*state.target).borrow_mut();
target.tile_id = state.animation[fnum].tile_id;
head = state.animation[fnum].time as i32 - state.elapsed;
}
}
self.states.retain(|state| !state.finished);
}
}
これで問題なくアニメーションに従ってtile_idを更新することが可能となる。例えば以下のようなテストコードが動作する。
fn main() {
let data = String::from("bar");
let image = Image {
path: String::from("images/foo.png"),
data: &data,
width: 128,
height: 128,
};
let tile_manager = TileManager {
tilemap: &image,
tile_width: 32,
tile_height: 32,
};
let mut ctx = RenderContext {
coordinate: Point {x: 0, y: 0},
opacity: 1.,
scale: 1.,
rotate: 0.,
};
let prop = TileProperty {
tiles: &tile_manager,
tile_id: 0,
};
let rcrefprop1 = Rc::new(RefCell::new(prop));
let rcrefprop2 = rcrefprop1.clone();
let tile = Tile {
position: Point {x: 32, y: 0},
scale: 1.,
rotate: 0.,
opacity: 1.,
prop: rcrefprop1,
};
let anime = vec![
AnimationFrame {
tile_id: 0,
time: 10,
},
AnimationFrame {
tile_id: 1,
time: 20,
}
];
let mut am = AnimationManager {
states: Vec::new(),
last_updated: SystemTime::now(),
};
am.push(rcrefprop2, &anime, true);
for _i in 0..10 {
sleep(Duration::new(0, 5_000_000));
am.update();
tile.render(&mut ctx);
}
}
/*
Copy images/foo.png from Point { x: 0, y: 0 } to Point { x: 32, y: 32 }, to Point { x: 32, y: 0 }
Copy images/foo.png from Point { x: 32, y: 0 } to Point { x: 64, y: 32 }, to Point { x: 32, y: 0 }
Copy images/foo.png from Point { x: 32, y: 0 } to Point { x: 64, y: 32 }, to Point { x: 32, y: 0 }
Copy images/foo.png from Point { x: 32, y: 0 } to Point { x: 64, y: 32 }, to Point { x: 32, y: 0 }
Copy images/foo.png from Point { x: 32, y: 0 } to Point { x: 64, y: 32 }, to Point { x: 32, y: 0 }
Copy images/foo.png from Point { x: 0, y: 0 } to Point { x: 32, y: 32 }, to Point { x: 32, y: 0 }
Copy images/foo.png from Point { x: 0, y: 0 } to Point { x: 32, y: 32 }, to Point { x: 32, y: 0 }
Copy images/foo.png from Point { x: 32, y: 0 } to Point { x: 64, y: 32 }, to Point { x: 32, y: 0 }
Copy images/foo.png from Point { x: 32, y: 0 } to Point { x: 64, y: 32 }, to Point { x: 32, y: 0 }
Copy images/foo.png from Point { x: 32, y: 0 } to Point { x: 64, y: 32 }, to Point { x: 32, y: 0 }
*/
まとめ
・タイルを用いたアニメーションはパラパラ漫画と同じ仕組み
・構造体のフィールドの管理を外部に委託するにはRc<RefCell<T>>を使う
明日はメニュー画面のUI/UXについて書きたい。