bevy ecs를 활용한 서버 및 클라이언트 개발
fn client_move_event_system(mut client_move_event: EventReader<ClientMoveEvent>, mut query: Query<(&mut Transform, &ClientSink), With<Client>>, tokio_handle: Res<TokioRuntime>,) {
for event in client_move_event.read() {
// 1. client Entity의 transform component 값을 변경시킨다.
// 2. 변경된 위치 좌표를 ClientSink Component의 sink 값을 통해서 보내준다.
let (mut transform, client_sink) = query.single_mut().unwrap();
match event.0 {
MoveDirection::Up => transform.translation.y += 10.0,
MoveDirection::Down => transform.translation.y -= 10.0,
MoveDirection::Left => transform.translation.x -= 10.0,
MoveDirection::Right => transform.translation.x += 10.0,
MoveDirection::None => {},
}
println!("move event occur!");
let cloned_transform = transform.clone();
let cloned_sink = client_sink.clone();
tokio_handle.0.spawn(async move {
let server_msg = ServerMessage::PlayerUpdate { transform: cloned_transform };
let json_str = serde_json::to_string(&server_msg).unwrap();
let msg = Message::text(json_str);
cloned_sink.0.send(msg).await;
});
}
}
위 코드는 정상적으로 컴파일되지 않는다. 여기서 문제는 query 매개변수를 통해서 transform과 ClientSink라는 컴포넌트를 빌려온다.
clone을 하더라도 ClientSink의 필드인 sink는 Clone trait가 구현되어있지 않다. tokio의 비동기 task를 생성해줄 때 내부적으로 소유권을 가지고 있거나 'static 라이프타임을 가지고 있어야한다. 새로 생성되는 task의 내부 sink의 라이프타임을 보장할 수 없어서 실패한다. 이 방식으로는 힘들 것 같아서 다른 방식으로 구현하였다.
fn client_move_event_system(mut client_move_event: EventReader<ClientMoveEvent>, mut query: Query<(&mut Transform, &ClientSender, &Client)>, tokio_handle: Res<TokioRuntime>, uuid_map: Res<UuidMap>) {
for event in client_move_event.read() {
// 1. client Entity의 transform component 값을 변경시킨다.
// 2. 변경된 위치 좌표를 ClientSink Component의 sink 값을 통해서 보내준다.
let entity = uuid_map.0.get(&event.uuid).unwrap();
let (mut transform, sender, _) = query.get_mut(*entity).unwrap();
match event.move_direction {
MoveDirection::Up => transform.translation.y += 10.0,
MoveDirection::Down => transform.translation.y -= 10.0,
MoveDirection::Left => transform.translation.x -= 10.0,
MoveDirection::Right => transform.translation.x += 10.0,
MoveDirection::None => {},
}
println!("move event occur!");
let cloned_transform = transform.translation.clone();
let cloned_tx = sender.0.clone();
tokio_handle.0.spawn(async move {
let server_msg = ServerMessage::PlayerUpdate { translation: cloned_transform };
let json_str = serde_json::to_string(&server_msg).unwrap();
println!("json: {}", json_str);
let msg = Message::text(json_str);
match cloned_tx.send(msg).await {
Ok(_) => {},
Err(e) => {
eprintln!("error: {}", e);
},
}
});
}
}
먼저 UuidMap이라는 리소스를 하나 생성해주어 Uuid를 키로 가지고 Entity를 값으로 가지도록 해주었다. get_mut를 호출하여 entity를 넘겨주어 event로 받은 클라이언트의 위치정보와 sender 컴포넌트를 가져올 수 있다.
let cloned_transform = transform.translation.clone();
let cloned_tx = sender.0.clone();
clone을 사용하여 생성하는 비동기 task에서 문제없이 사용할 수 있도록 해주었다.
TokioRuntime Resource를 활용해 task를 만들어주면서 내부적으로 보낼 메시지를 생성하도록 하였다.
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
pub enum ServerMessage {
PlayerUpdate {
translation: Vec3,
},
}
enum으로 만들었고 serde매크로를 활용해 tag = "type"을 지정해주었다. 자동으로 json: {"type":"PlayerUpdate","translation":[-10.0,20.0,0.0]} type필드가 Variant이름이 설정되게 할 수 있다.
/// sink handler
///
async fn sink_handler(mut recv: Receiver<Message>, mut sink: SplitSink<WebSocketStream<TcpStream>, Message>) {
println!("wait for recv sink message");
while let Some(msg) = recv.recv().await {
sink.send(msg).await.unwrap();
}
}
최종으로 sink_handler에서 메시지가 수신되면 클라이언트에게 보내도록 설정하였다.
소스는 아래에서 확인하면 된다.
https://github.com/wangki-kyu/bevy_ecs_authoritative
클라이언트에서 생성한 빨간색 공을 움직이기 위해서 방향 키를 움직이면
클라이언트에서 서버로 방향 키에 대한 정보를 보내준다. 서버 쪽에서 방향 키를 읽어서 클라이언트 엔티티의 위치정보를 조정하고 해당 값을 클라이언트로 보내준다. 클라이언트 쪽에서는 서버에서 보내준 위치 정보로 현재 엔티티의 값을 동기화시켜준다.
처음 ecs로 개발하는 거라서 완벽하지는 않지만 어느 정도 개념을 잡은 것 같다. 추가적으로 여러 클라이언트가 접속을 하여 움직일 때, 모든 클라이언트가 동기화되도록 처리하는 기능을 만들어봐야겠다.