7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

矩形の当たり判定処理を実装する

Posted at

はじめに

前回の記事の続きです。
今回は2つの矩形の当たり判定処理を実装します。
当たり判定はダメージを与えたり、オブジェクトの消去等のイベントを発生させる条件になります。
ゲームプログラミングにおいては重要なロジックです。

screen.png

矩形の構造体と当たり判定処理

矩形は左上の座標と幅、高さを持つデータ構造とします。

pub struct Rect {
    pub x: i32,
    pub y: i32,
    pub width: u32,
    pub height: u32,
}

2つの矩形が当たっているかを判定する関数は以下の通りです。
判定条件は「x軸とy軸の両方が重なっている場合」と言えます。

impl Rect {
    ...
    pub fn is_hit(&self, rect: &Rect) -> bool {
        return (self.x <= rect.x + rect.width as i32 && rect.x <= self.x + self.width as i32)
            && (self.y <= rect.y + rect.height as i32 && rect.y <= self.y + rect.height as i32);
    }
}

2つの矩形の位置関係は大きく分けて以下の4パターンがあります。
下図を見ると④の場合のみが当たっている状態であると分かります。

① 全く重なっていない場合

pattern1.png

② x軸のみ重なっている場合

pattern2.png

③ y軸のみ重なっている場合

pattern3.png

④ 両方重なっている場合

pattern4.png

また、Rect.is_hit関数の判定条件をよく見るとx軸もy軸も同じロジックで判定処理を行っていることが分かります。
どちらも始点の座標と長さで重なっているかを判定しています。
そこで共通のロジックを別関数に切り出して以下の様にリファクタします。

impl Rect {
    ...
    pub fn is_hit(&self, rect: &Rect) -> bool {
        return is_hit(self.x, self.width, rect.x, rect.width)
            && is_hit(self.y, self.height, rect.y, rect.height);
    }
}

fn is_hit(s1: i32, l1: u32, s2: i32, l2: u32) -> bool {
    let e1 = s1 + l1 as i32;
    let e2 = s2 + l2 as i32;
    return s1 <= e2 && s2 <= e1;
}

テストの実装

次にテストコードを実装します。
線分の当たり判定処理を行うis_hit関数と、矩形の当たり判定処理を行うRect.is_hit関数が正しく動作することを確認します。

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_line_hit() {
        assert_eq!(is_hit(50, 50, 19, 30), false);
        assert_eq!(is_hit(50, 50, 101, 20), false);
        assert_eq!(is_hit(50, 50, 40, 11), true);
        assert_eq!(is_hit(50, 50, 60, 15), true);
        assert_eq!(is_hit(50, 50, 90, 30), true);
    }

    #[test]
    fn test_rect_hit() {
        let rect = Rect::new(50, 50, 50, 50);
        assert_eq!(rect.is_hit(&Rect::new(0, 0, 30, 30)), false);
        assert_eq!(rect.is_hit(&Rect::new(0, 70, 40, 40)), false);
        assert_eq!(rect.is_hit(&Rect::new(40, 0, 20, 20)), false);
        assert_eq!(rect.is_hit(&Rect::new(40, 40, 15, 18)), true);
        assert_eq!(rect.is_hit(&Rect::new(60, 60, 10, 10)), true);
    }
}

cargo testでテストを実行します。

> cargo test
    Finished test [unoptimized + debuginfo] target(s) in 0.06s
     Running unittests src\main.rs (target\debug\deps\sample_sdl2-0e5b02a7694a67db.exe)

running 2 tests
test rect::tests::test_line_hit ... ok
test rect::tests::test_rect_hit ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

問題ないようです。
複雑な処理がある場合は、共通部分を抽出したり小さな処理に分割してテストすれば
不具合が発生した場合に問題箇所の特定が容易になります。

確認

実際に動かして確認します。
視覚的に分かりやすくするため、2つの矩形が当たっている場合に色を変えます。

screen.gif

ソースコード

今回実装したソースコードを載せます。

rect.rs
pub struct Rect {
    pub x: i32,
    pub y: i32,
    pub width: u32,
    pub height: u32,
}

impl Rect {
    pub fn new(x: i32, y: i32, width: u32, height: u32) -> Self {
        return Rect {
            x: x,
            y: y,
            width: width,
            height: height,
        };
    }

    pub fn is_hit(&self, rect: &Rect) -> bool {
        return is_hit(self.x, self.width, rect.x, rect.width)
            && is_hit(self.y, self.height, rect.y, rect.height);
    }
}

fn is_hit(s1: i32, l1: u32, s2: i32, l2: u32) -> bool {
    let e1 = s1 + l1 as i32;
    let e2 = s2 + l2 as i32;
    return s1 <= e2 && s2 <= e1;
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_line_hit() {
        assert_eq!(is_hit(50, 50, 19, 30), false);
        assert_eq!(is_hit(50, 50, 101, 20), false);
        assert_eq!(is_hit(50, 50, 40, 11), true);
        assert_eq!(is_hit(50, 50, 60, 15), true);
        assert_eq!(is_hit(50, 50, 90, 30), true);
    }

    #[test]
    fn test_rect_hit() {
        let rect = Rect::new(50, 50, 50, 50);
        assert_eq!(rect.is_hit(&Rect::new(0, 0, 30, 30)), false);
        assert_eq!(rect.is_hit(&Rect::new(0, 70, 40, 40)), false);
        assert_eq!(rect.is_hit(&Rect::new(40, 0, 20, 20)), false);
        assert_eq!(rect.is_hit(&Rect::new(40, 40, 15, 18)), true);
        assert_eq!(rect.is_hit(&Rect::new(60, 60, 10, 10)), true);
    }
}
main.rs
use sdl2::event::Event;
use sdl2::keyboard::Scancode;
use sdl2::pixels::Color;
use sdl2::rect::Rect;
use sdl2::render::Canvas;
use sdl2::video::Window;
use std::time::Duration;

mod rect;

fn draw_rect(canvas: &mut Canvas<Window>, rect: &rect::Rect, color: Color) {
    canvas.set_draw_color(color);
    // 画面描画用のRectを生成して渡す
    canvas.fill_rect(Rect::new(rect.x, rect.y, rect.width, rect.height));
}

pub fn main() {
    let sdl_context = sdl2::init().unwrap();
    let video_subsystem = sdl_context.video().unwrap();
    let window = video_subsystem
        .window("Rust-SDL2", 800, 600)
        .position_centered()
        .build()
        .unwrap();
    let mut rect = rect::Rect::new(50, 100, 100, 100);
    let rect2 = rect::Rect::new(350, 250, 100, 100);
    let white = Color::RGB(255, 255, 255);
    let green = Color::RGB(0, 255, 0);
    let mut canvas = window.into_canvas().build().unwrap();
    let mut event_pump = sdl_context.event_pump().unwrap();
    'running: loop {
        for event in event_pump.poll_iter() {
            match event {
                Event::Quit { .. } => break 'running,
                _ => {}
            }
        }
        let state = event_pump.keyboard_state();
        if state.is_scancode_pressed(Scancode::Up) {
            rect.y -= 5;
        }
        if state.is_scancode_pressed(Scancode::Down) {
            rect.y += 5;
        }
        if state.is_scancode_pressed(Scancode::Left) {
            rect.x -= 5;
        }
        if state.is_scancode_pressed(Scancode::Right) {
            rect.x += 5;
        }
        canvas.set_draw_color(Color::RGB(0, 0, 0));
        canvas.clear();

        let is_hit = rect.is_hit(&rect2);
        draw_rect(&mut canvas, &rect2, if is_hit { green } else { white });
        draw_rect(&mut canvas, &rect, if is_hit { green } else { white });

        canvas.present();
        std::thread::sleep(Duration::new(0, 1_000_000_000u32 / 60));
    }
}

参考文献

7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?