🎮
RustでGodot Engineを使ったテスト可能な設計: 依存性逆転の原則を活用する
はじめに
Godot EngineをRustで利用する際、Camera3D
やInputEventMouseMotion
などのGodotクラスを直接操作するコードをそのまま書くと、テストが非常に難しくなります。
特に、new_alloc
といったGodot特有のメソッドは、Godot Engineのランタイム環境が必要なため、テスト環境ではエラーとなります。
そこで、依存性逆転の原則を活用して、Godot Engineに依存しない形で設計する方法を紹介します。これにより、テスト可能で拡張性の高いコードを実現できます。
依存性逆転の原則とは
依存性逆転の原則は、SOLID原則の一つで、以下の2つのルールに基づいています。
- 高レベルモジュール(ビジネスロジック)は、低レベルモジュール(具体的な実装)に依存してはならない。両者は抽象化に依存すべきである。
- 抽象化は、具体的な詳細に依存してはならない。具体的な詳細が抽象化に依存すべきである。
これをGodot Engineに適用すると、Camera3D
やInputEventMouseMotion
といった具体的なクラスに直接依存するのではなく、それらを抽象化したインターフェース(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. 抽象化の導入
まず、Camera3D
とInputEventMouseMotion
を抽象化します。
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に依存しない設計:
Camera
やInputEvent
を抽象化することで、テスト可能な設計を実現。 - テストの容易性: モックを利用して、Godotのランタイム環境がなくてもロジックをテスト可能。
- 柔軟性と拡張性: 他のエンジンやシステムに移行する場合でも、ロジックを再利用可能。
Discussion