Featured image of post 第5回-イベント発生元を管理しよう - Perlでローグライク通知システムを作ろう

第5回-イベント発生元を管理しよう - Perlでローグライク通知システムを作ろう

Observerリストを保持し、イベント発生時に全Observerへ通知するGameEventEmitterを作成。attach/detach/notifyの実装方法を解説。

@nqounetです。

「Perlでローグライク通知システムを作ろう」シリーズの第5回です。前回は、Observerが持つべきメソッドをMoo::Roleで定義しました。今回は、複数のObserverへの通知を一元管理するGameEventEmitterを作成します。

前回の振り返り

前回は、GameEventObserverというRoleを作成し、Observerは必ずupdateメソッドを持つという約束を定義しました。

現在の問題点

前回のコードでは、イベントを各Observerに通知するとき、このように書いていました。

1
2
3
4
5
for my $event (@events) {
    $log_observer->update($event);
    $achievement_observer->update($event);
    $sound_observer->update($event);
}

新しいObserverを追加するたびに、この部分を修正する必要があります。Observerの数が増えるほど、管理が大変になりますね。

解決策:イベント発生元(EventEmitter)を作る

この問題を解決するために、Observerのリストを管理し、イベント発生時に全Observerへ通知する仕組みを作ります。

	graph LR
    A[GameEventEmitter] -->|attach| B[LogObserver]
    A -->|attach| C[AchievementObserver]
    A -->|attach| D[SoundObserver]
    A -->|notify| B
    A -->|notify| C
    A -->|notify| D

GameEventEmitterクラスを作成する

イベント発生元を表すGameEventEmitterクラスを作成しましょう。

 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
#!/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 GameEventEmitter {
    use Moo;
    use v5.36;

    # Observerのリストを保持
    has 'observers' => (
        is      => 'ro',
        default => sub { [] },
    );

    # Observerを登録する
    sub attach ($self, $observer) {
        push @{$self->observers}, $observer;
        say "[EMITTER] Observerを登録しました";
    }

    # Observerを解除する
    sub detach ($self, $observer) {
        @{$self->observers} = grep { $_ != $observer } @{$self->observers};
        say "[EMITTER] Observerを解除しました";
    }

    # 全てのObserverに通知する
    sub notify ($self, $event) {
        for my $observer (@{$self->observers}) {
            $observer->update($event);
        }
    }
}

package main {
    use v5.36;

    # EventEmitterを作成
    my $emitter = GameEventEmitter->new();

    # Observerがない状態でイベントを発生させてみる
    my $event = GameEvent->new(
        type    => 'enemy_defeated',
        message => 'スライムを倒した!',
    );

    say "=== Observerなしで通知 ===";
    $emitter->notify($event);
    say "(何も起きない)";
    say "";
}

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

1
2
=== Observerなしで通知 ===
(何も起きない)

Observerがいないので、通知しても何も起きません。

Observerを登録して通知する

では、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
#!/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 AchievementObserver {
    use Moo;
    use v5.36;

    with 'GameEventObserver';

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

    has 'defeated_count' => (
        is      => 'rw',
        default => 0,
    );

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

        if ($type eq 'enemy_defeated') {
            my $count = $self->defeated_count + 1;
            $self->defeated_count($count);

            if ($count == 1) {
                $self->_unlock('はじめての勝利');
            }
        }
        elsif ($type eq 'level_up') {
            my $level = $event->data->{level} // 0;
            if ($level == 5) {
                $self->_unlock('成長');
            }
        }
    }

    sub _unlock ($self, $name) {
        push @{$self->unlocked}, $name;
        say "[ACHIEVEMENT] 実績解除: $name";
    }
}

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 main {
    use v5.36;

    # EventEmitterを作成
    my $emitter = GameEventEmitter->new();

    # Observerを作成
    my $log_observer = LogObserver->new();
    my $achievement_observer = AchievementObserver->new();

    # Observerを登録
    $emitter->attach($log_observer);
    $emitter->attach($achievement_observer);

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

    # イベントを発生させて通知
    my $event1 = GameEvent->new(
        type    => 'enemy_defeated',
        message => 'スライムを倒した!',
    );
    $emitter->notify($event1);

    say "";

    my $event2 = GameEvent->new(
        type    => 'item_acquired',
        message => '薬草を手に入れた!',
    );
    $emitter->notify($event2);
}

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

1
2
3
4
5
6
=== ダンジョン探索開始 ===

[LOG] スライムを倒した!
[ACHIEVEMENT] 実績解除: はじめての勝利

[LOG] 薬草を手に入れた!

emitter->notify($event)を1回呼ぶだけで、登録されている全てのObserverに通知されます。

attach/detach/notifyの役割

GameEventEmitterが持つ3つのメソッドの役割をまとめます。

メソッド役割引数
attachObserverを登録するObserver オブジェクト
detachObserverを解除するObserver オブジェクト
notify全Observerに通知するGameEvent オブジェクト

改善された点

GameEventEmitterを導入したことで、以下の点が改善されました。

改善点説明
一元管理Observerのリストをemitterが管理
通知の簡略化notifyを1回呼ぶだけで全Observerに通知
追加が容易新しいObserverはattachで登録するだけ

今回のまとめ

今回は、イベント発生元を管理するGameEventEmitterクラスを作成しました。

  • Observerのリストを保持
  • attach: Observerを登録
  • detach: Observerを解除
  • notify: 全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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
#!/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 AchievementObserver {
    use Moo;
    use v5.36;

    with 'GameEventObserver';

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

    has 'defeated_count' => (
        is      => 'rw',
        default => 0,
    );

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

        if ($type eq 'enemy_defeated') {
            my $count = $self->defeated_count + 1;
            $self->defeated_count($count);

            if ($count == 1) {
                $self->_unlock('はじめての勝利');
            }
            if ($count == 10) {
                $self->_unlock('ハンター');
            }
        }
        elsif ($type eq 'level_up') {
            my $level = $event->data->{level} // 0;
            if ($level == 5) {
                $self->_unlock('成長');
            }
        }
    }

    sub _unlock ($self, $name) {
        push @{$self->unlocked}, $name;
        say "[ACHIEVEMENT] 実績解除: $name";
    }
}

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

package main {
    use v5.36;

    my $emitter = GameEventEmitter->new();

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

    $emitter->attach($log_observer);
    $emitter->attach($achievement_observer);
    $emitter->attach($sound_observer);

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

    my @events = (
        GameEvent->new(
            type    => 'enemy_defeated',
            message => 'スライムを倒した!',
        ),
        GameEvent->new(
            type    => 'item_acquired',
            message => '薬草を手に入れた!',
        ),
        GameEvent->new(
            type    => 'level_up',
            message => 'レベルが5になった!',
            data    => { level => 5 },
        ),
    );

    for my $event (@events) {
        $emitter->notify($event);
        say "";
    }

    say "=== 解除した実績 ===";
    say "- $_" for @{$achievement_observer->unlocked};
}

次回予告

次回は「Observerを動的に追加・削除しよう」です。ゲーム中にサウンドをON/OFFしたい場合など、実行時にObserverを追加・削除する方法を学びます。

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