🎮

RustでGodot Engineを使ったテスト可能な設計: 依存性逆転の原則を活用する

に公開

はじめに

Godot EngineをRustで利用する際、Camera3DInputEventMouseMotionなどのGodotクラスを直接操作するコードをそのまま書くと、テストが非常に難しくなります。
特に、new_allocといったGodot特有のメソッドは、Godot Engineのランタイム環境が必要なため、テスト環境ではエラーとなります。

そこで、依存性逆転の原則を活用して、Godot Engineに依存しない形で設計する方法を紹介します。これにより、テスト可能で拡張性の高いコードを実現できます。


依存性逆転の原則とは

依存性逆転の原則は、SOLID原則の一つで、以下の2つのルールに基づいています。

  1. 高レベルモジュール(ビジネスロジック)は、低レベルモジュール(具体的な実装)に依存してはならない。両者は抽象化に依存すべきである。
  2. 抽象化は、具体的な詳細に依存してはならない。具体的な詳細が抽象化に依存すべきである。

これをGodot Engineに適用すると、Camera3DInputEventMouseMotionといった具体的なクラスに直接依存するのではなく、それらを抽象化したインターフェース(trait)を利用する形になります。


具体例: マウスでカメラを回転させる動き

問題点: ベタ書き実装

以下のように、Godotのクラスを直接操作するコードは、テストが非常に難しいです。

fn unhandled_input(&mut self, event: Gd<InputEvent>) {
    if let Ok(input_event) = event.clone().try_cast::<InputEventMouseMotion>() {
        let relative = input_event.bind().get_relative();

        if let Some(camera) = self.base().get_node_or_null("Camera3D") {
            let mut camera = camera.cast::<Camera3D>();
            let rotation_y = -relative.x * 0.005;
            self.base_mut().rotate_y(rotation_y);

            let current_rotation = camera.bind().get_rotation();
            camera.bind_mut().set_rotation(Vector3::new(
                -std::f32::consts::FRAC_PI_2 + std::f32::consts::FRAC_PI_2 / 8.0,
                current_rotation.y,
                current_rotation.z,
            ));
        } else {
            godot_error!("Camera3D node not found");
        }
    }
}

このコードは、以下の問題を抱えています。

  • Godot Engineに強く依存しているため、テストが困難。
  • ロジックとエンジンコードが混在しており、責任が分離されていない。

解決策: 抽象化による設計

1. 抽象化の導入

まず、Camera3DInputEventMouseMotionを抽象化します。

pub trait Camera {
    fn rotate_y(&mut self, rotation_y: f32);
    fn set_rotation(&mut self, rotation: Vector3);
    fn get_rotation(&self) -> Vector3;
}

pub trait InputEvent {
    fn get_relative(&self) -> Vector2;
}

2. Godotクラスのラップ

次に、Godotの具体的なクラスをラップして、抽象化されたインターフェースを実装します。

pub struct GodotCamera {
    camera: Gd<Camera3D>,
}

impl Camera for GodotCamera {
    fn rotate_y(&mut self, rotation_y: f32) {
        self.camera.bind_mut().rotate_y(rotation_y);
    }

    fn set_rotation(&mut self, rotation: Vector3) {
        self.camera.bind_mut().set_rotation(rotation);
    }

    fn get_rotation(&self) -> Vector3 {
        self.camera.bind().get_rotation()
    }
}

pub struct GodotInputEvent {
    event: Gd<InputEventMouseMotion>,
}

impl InputEvent for GodotInputEvent {
    fn get_relative(&self) -> Vector2 {
        self.event.bind().get_relative()
    }
}

3. プレイヤーコントローラーのロジック

プレイヤーのロジックは、抽象化されたインターフェースを利用して実装します。

pub struct PlayerController {
    mouse_sensitivity: f64,
}

impl PlayerController {
    pub fn new(mouse_sensitivity: f64) -> Self {
        Self { mouse_sensitivity }
    }

    pub fn handle_mouse_motion(
        &self,
        input_event: &dyn InputEvent,
        camera: &mut dyn Camera,
    ) -> Result<(), String> {
        let relative = input_event.get_relative();

        // Y軸周りの回転を適用
        let rotation_y = -relative.x * self.mouse_sensitivity as f32;
        camera.rotate_y(rotation_y);

        // X軸周りの回転を適用(上下方向の制限を考慮)
        let current_rotation = camera.get_rotation();
        camera.set_rotation(Vector3::new(
            -std::f32::consts::FRAC_PI_2 + std::f32::consts::FRAC_PI_2 / 8.0,
            current_rotation.y,
            current_rotation.z,
        ));

        Ok(())
    }
}

テスト: Mockを使ってテストする

テスト時には、Godotに依存しないモックを利用します。

struct MockCamera {
    rotation_y: f32,
    rotation: Vector3,
}

impl Camera for MockCamera {
    fn rotate_y(&mut self, rotation_y: f32) {
        self.rotation_y = rotation_y;
    }

    fn set_rotation(&mut self, rotation: Vector3) {
        self.rotation = rotation;
    }

    fn get_rotation(&self) -> Vector3 {
        self.rotation
    }
}

struct MockInputEvent {
    relative: Vector2,
}

impl InputEvent for MockInputEvent {
    fn get_relative(&self) -> Vector2 {
        self.relative
    }
}

#[test]
fn test_handle_mouse_motion() {
    let mut camera = MockCamera {
        rotation_y: 0.0,
        rotation: Vector3::new(0.0, 0.0, 0.0),
    };
    let input_event = MockInputEvent {
        relative: Vector2::new(10.0, 0.0),
    };

    let controller = PlayerController::new(0.005);
    controller
        .handle_mouse_motion(&input_event, &mut camera)
        .unwrap();

    assert!((camera.rotation_y - -0.05).abs() < 1e-6);
    assert!(
        (camera.rotation.x - (-std::f32::consts::FRAC_PI_2 + std::f32::consts::FRAC_PI_2 / 8.0)).abs() < 1e-6
    );
}

まとめ

  • Godot Engineに依存しない設計: CameraInputEventを抽象化することで、テスト可能な設計を実現。
  • テストの容易性: モックを利用して、Godotのランタイム環境がなくてもロジックをテスト可能。
  • 柔軟性と拡張性: 他のエンジンやシステムに移行する場合でも、ロジックを再利用可能。

Discussion