Featured image of post 第6回-Observerを動的に追加・削除しよう - Perlでローグライク通知システムを作ろう

第6回-Observerを動的に追加・削除しよう - Perlでローグライク通知システムを作ろう

ゲーム中にサウンドをON/OFFしたい!Observerを実行時に追加・削除できる仕組みを実装。動的な設定変更に対応するコードを書きます。

@nqounetです。

「Perlでローグライク通知システムを作ろう」シリーズの第6回です。前回は、GameEventEmitterでObserverを一元管理する仕組みを作りました。今回は、ゲーム実行中にObserverを動的に追加・削除する方法を学びます。

前回の振り返り

前回は、GameEventEmitterクラスを作成し、attach/detach/notifyのメソッドを実装しました。

なぜ動的な追加・削除が必要なのか

ゲームでは、設定によって機能をON/OFFしたいことがあります。

  • サウンドをOFFにしたい(夜中にプレイするから)
  • デバッグログを一時的にONにしたい
  • 特定の実績を達成したら新しいObserverを有効化したい

これらを実現するには、ゲーム実行中にObserverを追加・削除できる必要があります。

サウンドのON/OFFを実装する

サウンドをON/OFFできる仕組みを作ってみましょう。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#!/usr/bin/env perl
# 言語: perl
# バージョン: 5.36以上
# 依存: Moo(cpanmでインストール)

use v5.36;

package GameEvent {
    use Moo;
    use v5.36;

    has 'type' => (
        is       => 'ro',
        required => 1,
    );

    has 'message' => (
        is       => 'ro',
        required => 1,
    );

    has 'timestamp' => (
        is      => 'ro',
        default => sub { time() },
    );

    has 'data' => (
        is      => 'ro',
        default => sub { {} },
    );
}

package GameEventObserver {
    use Moo::Role;
    use v5.36;

    requires 'update';
}

package LogObserver {
    use Moo;
    use v5.36;

    with 'GameEventObserver';

    has 'prefix' => (
        is      => 'ro',
        default => '[LOG]',
    );

    sub update ($self, $event) {
        say $self->prefix . " " . $event->message;
    }
}

package SoundObserver {
    use Moo;
    use v5.36;

    with 'GameEventObserver';

    has 'sound_map' => (
        is      => 'ro',
        default => sub {{
            enemy_defeated => 'victory.wav',
            item_acquired  => 'pickup.wav',
            level_up       => 'levelup.wav',
        }},
    );

    sub update ($self, $event) {
        my $type = $event->type;
        my $sound = $self->sound_map->{$type};

        if ($sound) {
            say "[SOUND] $sound を再生";
        }
    }
}

package GameEventEmitter {
    use Moo;
    use v5.36;

    has 'observers' => (
        is      => 'ro',
        default => sub { [] },
    );

    sub attach ($self, $observer) {
        push @{$self->observers}, $observer;
    }

    sub detach ($self, $observer) {
        @{$self->observers} = grep { $_ != $observer } @{$self->observers};
    }

    sub notify ($self, $event) {
        for my $observer (@{$self->observers}) {
            $observer->update($event);
        }
    }

    # 現在登録されているObserverの数を返す
    sub observer_count ($self) {
        return scalar @{$self->observers};
    }
}

package main {
    use v5.36;

    my $emitter = GameEventEmitter->new();

    my $log_observer = LogObserver->new();
    my $sound_observer = SoundObserver->new();

    # 最初はログとサウンド両方を登録
    $emitter->attach($log_observer);
    $emitter->attach($sound_observer);

    say "=== サウンドON状態(Observers: " . $emitter->observer_count . ") ===";
    say "";

    my $event1 = GameEvent->new(
        type    => 'enemy_defeated',
        message => 'スライムを倒した!',
    );
    $emitter->notify($event1);

    say "";
    say "--- サウンドをOFFにする ---";
    say "";

    # サウンドObserverを削除
    $emitter->detach($sound_observer);

    say "=== サウンドOFF状態(Observers: " . $emitter->observer_count . ") ===";
    say "";

    my $event2 = GameEvent->new(
        type    => 'enemy_defeated',
        message => 'ゴブリンを倒した!',
    );
    $emitter->notify($event2);

    say "";
    say "--- サウンドを再びONにする ---";
    say "";

    # サウンドObserverを再登録
    $emitter->attach($sound_observer);

    say "=== サウンドON状態(Observers: " . $emitter->observer_count . ") ===";
    say "";

    my $event3 = GameEvent->new(
        type    => 'enemy_defeated',
        message => 'オークを倒した!',
    );
    $emitter->notify($event3);
}

実行結果は以下のようになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
=== サウンドON状態(Observers: 2) ===

[LOG] スライムを倒した!
[SOUND] victory.wav を再生

--- サウンドをOFFにする ---

=== サウンドOFF状態(Observers: 1) ===

[LOG] ゴブリンを倒した!

--- サウンドを再びONにする ---

=== サウンドON状態(Observers: 2) ===

[LOG] オークを倒した!
[SOUND] victory.wav を再生

サウンドのON/OFFが動的に切り替わっているのがわかります。

設定オブジェクトを使った管理

より実践的な実装として、設定オブジェクトを使ってObserverを管理する方法を見てみましょう。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#!/usr/bin/env perl
# 言語: perl
# バージョン: 5.36以上
# 依存: Moo(cpanmでインストール)

use v5.36;

package GameEvent {
    use Moo;
    use v5.36;

    has 'type' => (
        is       => 'ro',
        required => 1,
    );

    has 'message' => (
        is       => 'ro',
        required => 1,
    );

    has 'data' => (
        is      => 'ro',
        default => sub { {} },
    );
}

package GameEventObserver {
    use Moo::Role;
    use v5.36;

    requires 'update';
}

package LogObserver {
    use Moo;
    use v5.36;

    with 'GameEventObserver';

    sub update ($self, $event) {
        say "[LOG] " . $event->message;
    }
}

package SoundObserver {
    use Moo;
    use v5.36;

    with 'GameEventObserver';

    sub update ($self, $event) {
        say "[SOUND] ♪ サウンドエフェクト再生";
    }
}

package GameEventEmitter {
    use Moo;
    use v5.36;

    has 'observers' => (
        is      => 'ro',
        default => sub { [] },
    );

    sub attach ($self, $observer) {
        push @{$self->observers}, $observer;
    }

    sub detach ($self, $observer) {
        @{$self->observers} = grep { $_ != $observer } @{$self->observers};
    }

    sub notify ($self, $event) {
        for my $observer (@{$self->observers}) {
            $observer->update($event);
        }
    }
}

# 設定を管理するクラス
package GameSettings {
    use Moo;
    use v5.36;

    # イベントエミッター
    has 'emitter' => (
        is       => 'ro',
        required => 1,
    );

    # サウンドObserver(内部で保持)
    has 'sound_observer' => (
        is      => 'ro',
        default => sub { SoundObserver->new() },
    );

    # サウンドがONかどうか
    has 'sound_enabled' => (
        is      => 'rw',
        default => 0,
    );

    # サウンドをON/OFFする
    sub toggle_sound ($self) {
        if ($self->sound_enabled) {
            $self->emitter->detach($self->sound_observer);
            $self->sound_enabled(0);
            say "[SETTINGS] サウンドをOFFにしました";
        } else {
            $self->emitter->attach($self->sound_observer);
            $self->sound_enabled(1);
            say "[SETTINGS] サウンドをONにしました";
        }
    }
}

package main {
    use v5.36;

    my $emitter = GameEventEmitter->new();

    # ログは常にON
    my $log_observer = LogObserver->new();
    $emitter->attach($log_observer);

    # 設定オブジェクトを作成
    my $settings = GameSettings->new(emitter => $emitter);

    say "=== ダンジョン探索開始 ===";
    say "";

    my $event1 = GameEvent->new(
        type    => 'enemy_defeated',
        message => 'スライムを倒した!',
    );
    $emitter->notify($event1);

    say "";
    $settings->toggle_sound();  # サウンドON
    say "";

    my $event2 = GameEvent->new(
        type    => 'enemy_defeated',
        message => 'ゴブリンを倒した!',
    );
    $emitter->notify($event2);

    say "";
    $settings->toggle_sound();  # サウンドOFF
    say "";

    my $event3 = GameEvent->new(
        type    => 'enemy_defeated',
        message => 'オークを倒した!',
    );
    $emitter->notify($event3);
}

実行結果は以下のようになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
=== ダンジョン探索開始 ===

[LOG] スライムを倒した!

[SETTINGS] サウンドをONにしました

[LOG] ゴブリンを倒した!
[SOUND] ♪ サウンドエフェクト再生

[SETTINGS] サウンドをOFFにしました

[LOG] オークを倒した!

動的管理のメリット

Observerを動的に追加・削除できることで、以下のメリットがあります。

メリット説明
ユーザー設定の反映サウンドON/OFF等をリアルタイムに切り替え
リソースの節約不要なObserverを削除してパフォーマンス向上
デバッグの容易さデバッグ用Observerを一時的に追加
段階的な機能解放ゲームの進行に応じて新しいObserverを追加

注意点

動的なObserver管理には注意点もあります。

  • 同じObserverを複数回登録しない: attachを呼びすぎると同じObserverが複数登録される
  • 存在しないObserverをdetachしない: detach済みのObserverをまたdetachしてもエラーにはならないが、意図しない動作になる可能性

今回のまとめ

今回は、Observerを動的に追加・削除する方法を学びました。

  • attach: 実行時にObserverを追加
  • detach: 実行時にObserverを削除
  • 設定オブジェクトと組み合わせて、ユーザー設定を反映

完成コード

今回の完成コードは前述の「設定オブジェクトを使った管理」のコードを参照してください。

次回予告

次回は「型チェックでバグを防ごう」です。間違ったオブジェクトがObserverとして登録されないよう、does制約を使った型チェックを学びます。

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。