Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ルーム付きサーバーを作ろう

例えばRoom(String)というコンポーネントを作ればサクッとルーム機能が実装できます。

コンポーネントを作ろう

preludeComponentが含まれています。

use ecson::prelude::*;

#[derive(Component)]
struct Room(String);

システムを定義しよう

一旦、ロジックは書かずに関数を作りましょう

1つめにチャットサーバーシステム本体です。

fn chat_server_system(
    mut commands: Commands,
    mut ev_received: MessageReader<MessageReceived>,
    mut ev_send: MessageWriter<SendMessage>,
    clients: Query<(Entity, &Room)>
) {

}

2つめにクリーンアップシステムです。接続が切れたときにエンティティを消してみましょう。

fn cleanup_system(
    mut commands: Commands,
    mut ev_disconnect: MessageReader<UserDisconnected>
) {

}

UserDisconnectedはEcsonが提供するものです。切断されたクライアントに対応するECSエンティティや、切断されたクライアントのネットワークIDが入っています。

Commandsbevy_ecsが提供するものです。ECSは「世界」を持ちます。その中で変更処理をする際は基本的にCommandsを介します。借用規則や並列処理、不整合をいい感じにやってくれます。

  • エンティティの生成
  • エンティティの削除
  • コンポーネントの追加・削除
  • リソースの挿入

といった操作には必ず使用してください。

ロジックを書こう

chat_server_system

{
    for msg in ev_received.read() {
        let NetworkPayload::Text(text) = &msg.payload else { continue };
        let text = text.trim();

        if let Some(room_name) = text.strip_prefix("/join") {
            // --- 【入出処理】 ---
            let new_room = room_name.trim().to_string();
            commands.entity(msg.entity).insert(Room(new_room.clone()));

            ev_send.write(SendMessage {
                target: msg.entity,
                payload: NetworkPayload::Text(format!("[System] Joined: {}", new_room)),
            });
        } else if let Ok((_, room)) = clients.get(msg.entity) {
            // --- 【発言処理】 ---
            // 同じルームにいる全員を検索して送信
            for (target_entity, target_room) in clients.iter() {
                if target_room.0 == room.0 {
                    ev_send.write(SendMessage {
                        target: target_entity,
                        payload: NetworkPayload::Text(format!("[{}]: {}", room.0, text)),
                    });
                }
            } 
        }
    }
}

ブロック分けして解説します。

if let Some(room_name) = text.strip_prefix("/join ") {
    // --- 【入室処理】 ---
    let new_room = room_name.trim().to_string();
    commands.entity(msg.entity).insert(Room(new_room.clone()));
    
    ev_send.write(SendMessage {
        target: msg.entity,
        payload: NetworkPayload::Text(format!("[System] Joined: {}", new_room)),
    });
} 

例えば「/join rust」というメッセージが来た時に/join を取り除き、rustという文字に加工します。それをルーム名にして、commands.entity(msg,entity).insert(Room(new_room.clone()))でエンティティ(すなわち接続)にRoomコンポーネントを付与しています。

else if let Ok((_, room)) = clients.get(msg.entity) {

}

/joinから始まるメッセージ以外は、if let Ok((_, room)) = clients.get(msg.entity)で送ってきた接続者(msg.entity)がRoomコンポーネントを持っているかを篩います。Query.get()メソッドを使うことで、エンティティを指定してその中身をピンポイントで覗き見することができます。

// --- 【発言処理】 ---
// 同じルームにいる全員を検索して送信
for (target_entity, target_room) in clients.iter() {
    if target_room.0 == room.0 {
        ev_send.write(SendMessage {
            target: target_entity,
            payload: NetworkPayload::Text(format!("[{}]: {}", room.0, text)),
        });
    }
}

Roomコンポーネントを持っているすべてのエンティティをイテレート(clients.iter())し、ルーム名が一致(if target_room.0 == room.0)する場合に、ev_send.writeしています。

cleanup_system

for event in ev_disconnect.read() {
    if let Ok(mut ent) = commands.get_entity(event.entity) {
        ent.despawn();
    }
}

あとで追加するEcsonWebSocketPluginはネットワークを監視しており、通信が途切れたりした瞬間にUserDisconnectedというイベントを発行します。 これを活用し、不要なエンティティをデスポーンさせています。メモリリークを防ぐためです。

システムを登録しよう

fn main() {
    EcsonApp::new()
        .add_plugins(EcsonWebSocketPlugin::new("127.0.0.1:8080"))
        .add_systems(Update, (chat_server_system, cleanup_system))
        .run()
}

(chat_server_system, cleanup_system)のようにタプルで複数一気に渡すことができます。

フロントエンドからテストをしよう

これも同じHTMLで構いません。

alt text

お疲れさまでした。 これであなたはEcsonにおけるチャットアプリ開発方法を身に付けました。 次の章では、チャットアプリを爆速で開発するためのプラグインを紹介します。