はじめに
Rustの勉強とエミュレータ作ってみたくて、入門編のChip8を以下のサイト(pdf)を写経して作ったときによく分からなかった記法を備忘録がてら書いておきます。
An Introduction to Chip-8 Emulation using the Rust Programming
Language
ほぼほぼ写経しただけですけど、一応コードはこちらです。動かないゲームとかあるのでバグあるかもしれないです。。
記法、メソッド
copy_from_slice
こんな感じで出てきました。
new_emu.ram[..FONTSET_SIZE].copy_from_slice(&FONTSET);
// ram: [u8; RAM_SIZE]
以下は標準ライブラリをまとめた公式ドキュメントより抜粋です
pub fn copy_from_slice(&mut self, src: &[T])
where
T: Copy,
// Copies all elements from src into self, using a memcpy.
// The length of src must be the same as self.
// If T does not implement Copy, use clone_from_slice.
// Panics
// This function will panic if the two slices have different lengths.
// Examples
// Copying two elements from a slice into another:
let src = [1, 2, 3, 4];
let mut dst = [0, 0];
// Because the slices have to be the same length,
// we slice the source slice from four elements
// to two. It will panic if we don't do this.
dst.copy_from_slice(&src[2..]);
assert_eq!(src, [1, 2, 3, 4]);
assert_eq!(dst, [3, 4]);
スライスから任意の指定箇所をコピーするメソッドです。
match
match (digit1, digit2, digit3, digit4) {
(0, 0, 0, 0) => return,
(0, 0, 0xE, 0) => {
self.screen = [false; SCREEN_WIDTH * SCREEN_HEIGHT];
} // {...} ブロックの後は","を省略可能
(1, _, _, _) => { // "_"はなんでも良いという意味
self.pc = op & 0x0FFF;
}
(5, _, _, _) => {
let x = digit2 as usize; // indexはusize型にする必要がある
let y = digit3 as usize;
if self.v_reg[x] == self.v_reg[y] {
self.pc += 2;
}
}
// ...省略
(_, _, _, _) => unimplemented!("Unimplemented opcode: {:X}", op),
}
match evt {
Event::Quit { .. }
| Event::KeyDown { // | で複数条件を指定できる
keycode: Some(Keycode::Escape),
..
} => {
break 'gameloop;
}
_ => {}
}
wrapping_add, overflowing_add
こちらからいくつか引用させて頂きます。
Rustでの整数オーバーフローまとめ
オーバーフローした時の挙動が違うっぽいです。
// wrapping_add: オーバーフローした桁を無視
// docs: https://doc.rust-lang.org/std/intrinsics/fn.wrapping_add.html
assert_eq!(250u8.wrapping_add(10), 4);
assert_eq!(4u8.wrapping_sub(10), 250);
// overflowing_add: オーバーフローが起こった場合にboolのタプルを返します。計算結果はwrappingと同じ
// docs: https://doc.rust-lang.org/std/primitive.u64.html#method.overflowing_add
assert_eq!(200u8.overflowing_add(50), (250, false));
assert_eq!(200u8.overflowing_add(60), (4, true));
env::args().collect();
cargo run
時のコマンドライン引数の読み込み
let args: Vec<_> = env::args().collect();
// Vec<_>
// "_"には型を入れても良いが、こう書くことで型推論が働く(多分)
// コマンドライン引数はstd::env::argsで取れる
// collect()はイテレータをコレクションにする関数
コレクションはVecとか連想配列とかヒープに保存される型で、イテレータはループで使うときにこの型にする必要がある。
生成したVec型をループしたい時はargs.iter().mapみたいな感じに書く必要ありといった感じです。
Option
matchから抜粋したのが以下のコード
match evt {
Event::KeyDown {
// Event::KeyDownというstructのkeycodeだけ指定して、".."は他のプロパティは指定しないという意味
// Option型なので、keycodeにOption型の何か値が入っていたらSomeの中のkeyを取り出して使える
keycode: Some(key), ..
} => {
// if letの場合、左辺と右辺が同じであればSomeの中の変数kを取り出して使える
if let Some(k) = key2btn(key) {
chip8.keypress(k, true);
}
}
}
Option型は慣れが必要ですね。
`loop
ループの文頭にアポストロフィをつけた名称でループにラベルをつけることができるっぽいです。
'gameloop: loop {
for evt in event_pump.poll_iter() {
match evt {
Event::Quit { .. }
| Event::KeyDown {
keycode: Some(Keycode::Escape),
..
} => {
break 'gameloop; // ここで'gameloopという名前のループを終了しています。
}
...
ロジック
CPUのクロック処理
CPUがどういう処理をしているかですが、実際にロジックを担ってる箇所が以下です。
'gameloop: loop {
// 入力に対する(KeyUp等)の処理を行う
for evt in event_pump.poll_iter() {
// ...省略
}
// TICKS_PER_FRAME = 10
// 1ループで10回処理を呼んでる
for _ in 0..TICKS_PER_FRAME {
chip8.tick(); // ここで様々な処理を行なっている。
}
chip8.tick_timers();
draw_screen(&chip8, &mut canvas); // ディスプレイ表示の処理
}
// tick()の中身
pub fn tick(&mut self) {
let op = self.fetch(); // opコードを呼び出して、pcを進める
self.execute(op); // opコードを実行する
}
// tick_timers()の中身
pub fn tick_timers(&mut self) {
if self.dt > 0 {
self.dt -= 1;
}
if self.st > 0 {
if self.st == 1 {
// BEEP
}
self.st -= 1;
}
}
ここは完全に理解できてないんですが、ドキュメントを読む限り、ループの速度はシステム依存でスクリーンの描画速度に合わせているっぽいです。
1回スクリーンが切り替わる度に処理が10進むくらいがちょうど良いということで、TICKS_PER_FRAME = 10
としているようです。