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

## はじめに

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

![screen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2693350/6ec54bde-8811-8e99-aabc-db67e1b7eafa.png)

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

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

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

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

```rust
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2693350/ebae8135-79ba-8f75-f362-bac851e8b860.png)

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

![pattern2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2693350/434bf860-8a40-eaec-61fe-01c87d56b75b.png)

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

![pattern3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2693350/569d8a1a-e7d2-cc7d-6592-e27713005892.png)

**④ 両方重なっている場合**

![pattern4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2693350/ec10c6d7-256e-1306-462a-999323319be0.png)

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

```rust
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`関数が正しく動作することを確認します。

```rust
#[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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2693350/049b5f96-df57-80d0-2521-49391f8690ae.gif)

## ソースコード

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

<details><summary>rect.rs</summary>

```rust
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);
    }
}
```

</details>

<details><summary>main.rs</summary>

```rust
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));
    }
}
```

</details>

## 参考文献

https://doc.rust-jp.rs/book-ja/ch11-01-writing-tests.html