본문 바로가기
GameMaker강좌[GMS2]/네트워크강좌

[게임메이커 강좌-네트워크][GMS2] 멀티플레이어 게임 만들기-4-클라이언트

by 타락카얀 2024. 12. 25.
728x90

 

 

GAME MAKER 강좌

 

KAYAN

 

 

 

 

 

 

◈ 클라이언트

 

이번에는 클라이언트의 게임 설정을 해봅시다.

 

클라이언트 오브젝트에 [Create 이벤트]를 추가하고, 필요한 기능을 추가합니다.

 

 

서버와 마찬가지로 서버와의 통신을 위한 버퍼와, 다른 클라이언트의 정보를 저장할 맵 데이터 구조체를 생성하도록 합니다.

그리고 서버에 연결을 시도합니다.

 
obj_client_system 오브젝트- Create 이벤트
 
 
if !( variable_global_exists( "global.main_server_IP" ) ){ global.connect_IP = "127.0.0.1"; }
if ( global.connect_IP == "" ){ global.connect_IP = "127.0.0.1"; }
 
global.net_buffer = buffer_create( 256, buffer_grow, 1 );
global.player_map = ds_map_create( ); //클라이언트 정보
 
//로컬 IP는 "127.0.0.1" 입니다.
 
global.client_socket = network_create_socket( network_socket_tcp ); //클라이언트 소켓 생성
network_set_config( network_config_connect_timeout, 1000 ); //서버 연결 초과 시간
 
//서버에 접속
var server = network_connect( global.client_socket , global.connect_IP, 6510 );
if server < 0
{
//연결 실패했을 때
}
else
{
//연결 되었을 때
}
 

 

서버에 연결 실패하면 클라이언트 오브젝트는 파기하고, "서버 연결에 실패했습니다." 메시지를 띄웁니다.

그리고 메인 화면으로 돌아가도록 합니다.

 
obj_client_system 오브젝트 - Create 이벤트
 
 
if !( variable_global_exists( "global.main_server_IP" ) ){ global.connect_IP = "127.0.0.1"; }
if ( global.connect_IP == "" ){ global.connect_IP = "127.0.0.1"; }
 
global.net_buffer = buffer_create( 256, buffer_grow, 1 );
global.player_map = ds_map_create( ); //클라이언트 정보
 
//로컬 IP는 "127.0.0.1" 입니다.
 
global.client_socket = network_create_socket( network_socket_tcp ); //클라이언트 소켓 생성
network_set_config( network_config_connect_timeout, 1000 ); //서버 연결 초과 시간
 
//---------- ▼ 서버에 접속
var server = network_connect( global.client_socket , global.connect_IP, 6510 );
if server < 0
{
//연결 실패했을 때
instance_destroy( );
show_message_async( "서버 연결에 실패했습니다." );
room_goto( Room_open );
}
else
{
//연결 되었을 때
buffer_seek( global.net_buffer, buffer_seek_start, 0 );
buffer_write( global.net_buffer , buffer_u16, 1 ); //이벤트 번호
buffer_write( global.net_buffer , buffer_string, global.playername ); //유저 이름
network_send_packet( global.client_socket, global.net_buffer, buffer_tell( global.net_buffer ) );
}
//---------- ▲ 서버에 접속
 

 

서버에 연결 되면 클라이언트 유저 이름을 보냅니다.

마지막으로 필요한 변수를 추가해줍니다.

 
obj_client_system 오브젝트- Create 이벤트
 
 
global.user_id = -1; //현재 클라이언트 유저 ID
user_count = 0;
global.server_name = ""; //서버 플레이어
 

 

클라이언트가 유저 이름을 서버에 보냈으니 처리해봅시다.

서버에 [네트워크 비동기 이벤트(Network Async Event)]를 추가하고, 이벤트를 구성합니다.

먼저 클라이언트가 서버에 접속 했을 때 설정입니다.

클라이언트가 서버에 접속하면 async_load 맵의 "type"은 network_type_connect 를 반환합니다.

obj_server_system 오브젝트 - Async - Networking 이벤트
 
 
var n_id = ds_map_find_value( async_load, "id" );
if n_id == global.server_socket
{
var type = ds_map_find_value( async_load, "type" );
switch( type )
{
 
//--------- 클라이언트 접속
case network_type_connect://현재 소켓에 새로운 클라이언트 접속
var sock = ds_map_find_value( async_load, "socket" );
ds_list_add( global.socketlist, sock ); //클라이언트 소켓 저장
 
buffer_seek( global.net_buffer, buffer_seek_start, 0 );
buffer_write( global.net_buffer , buffer_u16, 2 ); //패킷 이벤트 번호
buffer_write( global.net_buffer , buffer_u8, sock ); //접속자에게 유저 id 할당
network_send_packet( sock, global.net_buffer, buffer_tell( global.net_buffer ) );
 
break;
//--------- 클라이언트 접속
 
}
}
 

 

클라이언트가 서버에 접속하면, 클라이언트 소켓을 저장해둡니다.

그리고, 해당 유저에게 유저 ID를 할당합니다.

 

클라이언트가 서버에 접속하면서 자신의 이름을 서버의 1번 수신 이벤트로 보냈죠.

이제 그 이벤트를 처리해봅시다.

 
obj_server_system 오브젝트 - Async - Networking 이벤트
 
 
var n_id = ds_map_find_value( async_load, "id" );
if n_id == global.server_socket
{
var type = ds_map_find_value( async_load, "type" );
switch( type )
{
//--------- 클라이언트 접속
case network_type_connect://현재 소켓에 새로운 클라이언트 접속
var sock = ds_map_find_value( async_load, "socket" );
ds_list_add( global.socketlist, sock ); //클라이언트 소켓 저장
 
buffer_seek( global.net_buffer, buffer_seek_start, 0 );
buffer_write( global.net_buffer , buffer_u16, 2 ); //패킷 이벤트 번호
buffer_write( global.net_buffer , buffer_u8, sock ); //접속자에게 유저 id 할당
network_send_packet( sock, global.net_buffer, buffer_tell( global.net_buffer ) );
 
break;
//--------- 클라이언트 접속
}
}
 
// == == == == == == == == == == == ▼ 클라이언트 데이터 수신
else{
 
var sock = ds_map_find_value( async_load, "id" );
var t_buffer = ds_map_find_value( async_load, "buffer" );
var cmd_type = buffer_read( t_buffer, buffer_u16 ); //이벤트 번호
 
switch ( cmd_type )
{
//----------- 새로운 클라이언트 접속시 세부 정보 수신
case 1:
var _name;
_name = buffer_read( t_buffer, buffer_string ); //플레이어 이름
 
ds_map_set( global.player_map, "player_"+string( sock ), string( _name ) );
 
break;
//----------- 새로운 클라이언트 접속시 세부 정보 수신
}
}

// == == == == == == == == == == == ▲ 클라이언트 데이터 수신

 

신규 유저가 보낸 이름을 서버에 저장합니다.

현재 신규 유저의 정보를 알고 있는 것은 서버와 신규 유저 밖에 알지 못하죠.

기존의 클라이언트들에게 신규 유저의 정보를 보내 클라이언트의 정보를 갱신하도록 합니다.

obj_server_system 오브젝트 - Async - Networking 이벤트
 
 
//----------- 새로운 클라이언트 접속시 세부 정보 수신
case 1:
var _name;
_name = buffer_read( t_buffer, buffer_string ); //플레이어 이름
ds_map_set( global.player_map, "player_"+string( sock ), string( _name ) );
//----------- ▼
 
//클라이언트들에게 보낼 접속자 정보
if !( ds_map_exists( global.player_map, "player_order" ) ){ ds_map_add_list( global.player_map, "player_order", global.socketlist ); }
else{ ds_map_replace_list( global.player_map, "player_order", global.socketlist ); }
var _map = json_encode( global.player_map );
 
//클라이언트에 패킷을 보내 적용
buffer_seek( global.net_buffer, buffer_seek_start, 0 );
buffer_write( global.net_buffer , buffer_u16, 1 ); //패킷 이벤트 번호
buffer_write( global.net_buffer , buffer_string, global.playername ); //서버 관리자 닉네임
buffer_write( global.net_buffer , buffer_string, _map ); //클라이언트 정보
 
for ( var i = 0; i < ds_list_size( global.socketlist ); i+ = 1; ){
network_send_packet( ds_list_find_value( global.socketlist, i ), global.net_buffer, buffer_tell( global.net_buffer ) );
}
 
//서버에 플레이어 생성
inst = instance_create_depth( 0, 0, 0, obj_game_user );
inst.user_id = sock;
inst.user_name = _name;
 
//----------- ▲
break;
 

 

그리고 신규 유저를 제어할 게임 유저 오브젝트를 생성합니다.

게임 유저 오브젝트, 서버 오브젝트, 클라이언트 오브젝트는 네트워크가 종료 될 때까지 계속 유지되어야 하기 때문에 Persistent 가 체크되었나 확인합시다.

 

신규 유저가 서버에 접속했으니, 신규 유저보고 게임 룸으로 이동하도록 합니다.

 

※ [참고] 패킷을 보낼 때, 어떤 이벤트 번호로 보냈는지 확인하고, 보낸 번호와 동일한 이벤트 번호에서 버퍼를 수신해야 합니다.

※또한, 패킷을 수신할 때, 버퍼의 유형과 값 순서대로 수신해야 합니다. 그렇지 않으면 읽기 오류가 발생합니다.

 
obj_server_system 오브젝트 - Async - Networking 이벤트
 
 
//----------- 새로운 클라이언트 접속시 세부 정보 수신
case 1:
var _name;
_name = buffer_read( t_buffer, buffer_string ); //플레이어 이름
 
ds_map_set( global.player_map, "player_"+string( sock ), string( _name ) );
 
 
//클라이언트들에게 보낼 접속자 정보
if !( ds_map_exists( global.player_map, "player_order" ) ){ ds_map_add_list( global.player_map, "player_order", global.socketlist ); }
else{ ds_map_replace_list( global.player_map, "player_order", global.socketlist ); }
var _map = json_encode( global.player_map );
 
//클라이언트에 패킷을 보내 적용
buffer_seek( global.net_buffer, buffer_seek_start, 0 );
buffer_write( global.net_buffer , buffer_u16, 1 ); //패킷 이벤트 번호
buffer_write( global.net_buffer , buffer_string, global.playername ); //서버 관리자 닉네임
buffer_write( global.net_buffer , buffer_string, _map ); //클라이언트 정보
 
for ( var i = 0; i < ds_list_size( global.socketlist ); i+ = 1; ){
network_send_packet( ds_list_find_value( global.socketlist, i ), global.net_buffer, buffer_tell( global.net_buffer ) );
}
 
 
//서버에 플레이어 생성
inst = instance_create_depth( 0, 0, 0, obj_game_user );
inst.user_id = sock;
inst.user_name = _name;
 
//----------- ▼ 서버 접속시 게임 중일 때 게임 룸으로 이동

if !( global.game_room<0 ){
buffer_seek( global.net_buffer, buffer_seek_start, 0 );
buffer_write( global.net_buffer , buffer_u16, 51 ); //패킷 이벤트 번호
buffer_write( global.net_buffer , buffer_string, room_get_name( global.game_room ) );
network_send_packet( sock, global.net_buffer, buffer_tell( global.net_buffer ) );
}

//----------- ▲ 서버 접속시 게임 중일 때 게임 룸으로 이동
break;
 

 

이동할 룸의 정보를 신규 유저에게 보냅니다.

 

 

서버가 클라이언트 연결/해제시 클라이언트에게 보낸 패킷을 차례로 처리해봅시다.

 

 

먼저 클라이언트가 서버에 접속했을 때 보낸 패킷입니다.

2번 수신 이벤트로 보냈죠.

서버가 보낸 버퍼값을 클라이언트 유저 ID에 할당합니다.

 
obj_client_system 오브젝트 - Async - Networking 이벤트
 
 
var n_id = ds_map_find_value( async_load, "id" );
if n_id == global.client_socket
{
var type = ds_map_find_value( async_load, "type" );
if ( type == network_type_data )
{
// == == == == == == == == == 서버로 부터 받은 패킷 데이터
var t_buffer = ds_map_find_value( async_load, "buffer" );
var cmd_type = buffer_read( t_buffer, buffer_u16 );
switch ( cmd_type )
{
 
//--------- 서버에 처음 접속했을 때
case 2:
var sock = buffer_read( t_buffer, buffer_u8 );
global.user_id = sock; //서버로부터 유저 ID 할당 받음
break;
//--------- 서버에 처음 접속했을 때
 
}
// == == == == == == == == == 서버로 부터 받은 패킷 데이터
}
}
 

 

그리고 게임 룸으로 이동하라고 했죠.

51번 수신 이벤트로 보냈으니, 서버에서 보내온 룸으로 이동하라고 처리합니다.

 

 
obj_client_system 오브젝트 - Async - Networking 이벤트
 
 
var n_id = ds_map_find_value( async_load, "id" );
if n_id == global.client_socket
{
var type = ds_map_find_value( async_load, "type" );
if ( type == network_type_data )
{
// == == == == == == == == == 서버로 부터 받은 패킷 데이터
var t_buffer = ds_map_find_value( async_load, "buffer" );
var cmd_type = buffer_read( t_buffer, buffer_u16 );
switch ( cmd_type )
{
 
//--------- 서버에 처음 접속했을 때
case 2:
var sock = buffer_read( t_buffer, buffer_u8 );
global.user_id = sock; //서버로부터 유저 ID 할당 받음
break;
//--------- 서버에 처음 접속했을 때
 
//--------- ▼ 룸으로 이동( 서버 접속시 게임 중일 때 게임 룸으로 이동 )

case 51:
var _room = buffer_read( t_buffer, buffer_string );
room_goto( asset_get_index( _room ) );
break;

//--------- ▲ 룸으로 이동
 
}
// == == == == == == == == == 서버로 부터 받은 패킷 데이터
}
}
 

 

패킷 수신 이벤트 번호는 딱히 정해진 순서가 없습니다.

이것은 강좌에서 사용하기 위해 임의로 구성한 것이기 때문에, 여러분이 편집하기 쉬운 순서로 구성하시면 됩니다.

 

 

다음은 클라이언트가 연결을 해제 했을 경우입니다.

클라이언트가 연결을 정상적으로 해제 했을 때 async_load 맵의 "type"은 network_type_disconnect 를 반환합니다.

 
obj_server_system 오브젝트 - Async - Networking 이벤트
 
 
var n_id = ds_map_find_value( async_load, "id" );
if n_id == global.server_socket
{
var type = ds_map_find_value( async_load, "type" );
 
switch( type )
{
 
//--------- 클라이언트 연결
case network_type_connect://현재 소켓에 새로운 클라이언트 접속
var sock = ds_map_find_value( async_load, "socket" );
ds_list_add( global.socketlist, sock );
 
buffer_seek( global.net_buffer, buffer_seek_start, 0 );
buffer_write( global.net_buffer , buffer_u16, 2 ); //패킷 이벤트 번호
buffer_write( global.net_buffer , buffer_u8, sock ); //접속자에게 유저 id 할당
network_send_packet( sock, global.net_buffer, buffer_tell( global.net_buffer ) );
 
break;
//--------- 클라이언트 연결
 
 
 
//--------- ▼ 클라이언트 연결 해제

case network_type_disconnect://현재 소켓에 클라이언트 연결 종료
var sock = ds_map_find_value( async_load, "socket" );
var pos = ds_list_find_index( global.socketlist, sock );
if !( pos<0 ){
//---------
ds_list_delete( global.socketlist, pos ); //클라이언트 소켓
if ds_map_exists( global.player_map, "player_"+string( sock ) ){
ds_map_delete( global.player_map, "player_"+string( sock ) );
}
 
//클라이언트들에게 보낼 접속자 정보
var _map = json_encode( global.player_map );
//
buffer_seek( global.net_buffer, buffer_seek_start, 0 );
buffer_write( global.net_buffer , buffer_u16, 1 ); //패킷 이벤트 번호
buffer_write( global.net_buffer , buffer_string, global.playername ); //서버 관리자 닉네임
buffer_write( global.net_buffer , buffer_string, _map ); //클라이언트 정보
 
 
for ( var i = 0; i < ds_list_size( global.socketlist ); i+ = 1; ){
network_send_packet( ds_list_find_value( global.socketlist, i ), global.net_buffer, buffer_tell( global.net_buffer ) );
}
 
 
//--------- 연결 종료된 유저 삭제
with( obj_game_user ){ if user_id == sock{ instance_destroy( ); }}
buffer_seek( global.net_buffer, buffer_seek_start, 0 );
buffer_write( global.net_buffer , buffer_u16, 21 ); //패킷 이벤트 번호
buffer_write( global.net_buffer , buffer_u8, sock );
for ( var i = 0; i<ds_list_size( global.socketlist ); i+ = 1; ){
if !( ds_list_find_value( global.socketlist, i ) == sock ){
network_send_packet( ds_list_find_value( global.socketlist, i ), global.net_buffer, buffer_tell( global.net_buffer ) );
}}
//--------- 연결 종료된 유저 삭제
//----------
}
 
break;

//--------- ▲ 클라이언트 연결 해제
}
 
}
 
// == == == == == == == == == == == 클라이언트 데이터 수신
else{
//클라이언트 수신 이벤트 어쩌고... 저쩌고...있음.
}
// == == == == == == == == == == == 클라이언트 데이터 수신
 

 

연결이 종료되면 해당유저의 소켓과 정보를 파기하고, 갱신된 정보를 다른 클라이언트들에게 보냅니다.

그리고 해당 유저의 게임 유저 오브젝트(obj_game_user)를 파기하고, 다른 클라이언트들에게 플레이어 캐릭터를 파기하라고 요청합니다.

 

 

다음은 클라이언트가 서버에 연결 해제 했을 때 보낸 패킷입니다.

1번 수신 이벤트에서 플레이어 정보를 갱신합니다.

 
obj_client_system 오브젝트 - Async - Networking 이벤트
 
 
var n_id = ds_map_find_value( async_load, "id" );
if n_id == global.client_socket
{
var type = ds_map_find_value( async_load, "type" );
if ( type == network_type_data )
{
// == == == == == == == == == 서버로 부터 받은 패킷 데이터
var t_buffer = ds_map_find_value( async_load, "buffer" );
var cmd_type = buffer_read( t_buffer, buffer_u16 );
switch ( cmd_type )
{
 
//--------- ▼ 클라이언트 세부 정보 갱신
case 1:
global.server_name = buffer_read( t_buffer, buffer_string );
ds_map_destroy( global.player_map );
global.player_map = json_decode( buffer_read( t_buffer, buffer_string ) ); //접속자들 정보
break;
//--------- ▲ 클라이언트 세부 정보 갱신
 
//--------- 서버에 처음 접속했을 때
case 2:
var sock = buffer_read( t_buffer, buffer_u8 );
global.user_id = sock; //서버로부터 유저 ID 할당 받음
break;
//--------- 서버에 처음 접속했을 때
 
//--------- 룸으로 이동( 서버 접속시 게임 중일 때 게임 룸으로 이동 )
case 51:
var _room = buffer_read( t_buffer, buffer_string );
room_goto( asset_get_index( _room ) );
break;
//--------- 룸으로 이동
 
}
// == == == == == == == == == 서버로 부터 받은 패킷 데이터
}
}
 

 

그리고 게임을 나간 유저의 ID가 일치하는 플레이어 캐릭터를 파기합니다.

 
obj_client_system 오브젝트 - Async - Networking 이벤트
 
 
var n_id = ds_map_find_value( async_load, "id" );
if n_id == global.client_socket
{
var type = ds_map_find_value( async_load, "type" );
if ( type == network_type_data )
{
// == == == == == == == == == 서버로 부터 받은 패킷 데이터
var t_buffer = ds_map_find_value( async_load, "buffer" );
var cmd_type = buffer_read( t_buffer, buffer_u16 );
switch ( cmd_type )
{
 
//--------- 클라이언트 세부 정보 갱신
case 1:
global.server_name = buffer_read( t_buffer, buffer_string );
ds_map_destroy( global.player_map );
global.player_map = json_decode( buffer_read( t_buffer, buffer_string ) ); //접속자들 정보
break;
//--------- 클라이언트 세부 정보 갱신
 
//--------- 서버에 처음 접속했을 때
case 2:
var sock = buffer_read( t_buffer, buffer_u8 );
global.user_id = sock; //서버로부터 유저 ID 할당 받음
break;
//--------- 서버에 처음 접속했을 때
 
 
//--------- ▼ 플레이어 파기
case 21:
var _user_id = buffer_read( t_buffer, buffer_u8 );
with( obj_player_parent ){ if ( _user_id == user_id ){ instance_destroy( ); }}
break;
//--------- ▲ 플레이어 파기
 
//--------- 룸으로 이동( 서버 접속시 게임 중일 때 게임 룸으로 이동 )
case 51:
var _room = buffer_read( t_buffer, buffer_string );
room_goto( asset_get_index( _room ) );
break;
//--------- 룸으로 이동
}
// == == == == == == == == == 서버로 부터 받은 패킷 데이터
}
}
 

 

클라이언트가 서버에 연결/해제 했을 때의 이벤트를 처리했습니다.

 

 

 

 

◈ 클라이언트 종료했을 때

 

클라이언트를 종료했을 때 네트워크에 사용된 기능은 파기하여 메모리에서 제거하도록 합니다.

[Destroy 이벤트]를 추가하고, 소켓, 버퍼와 유저들의 정보가 담긴 DS 맵을 파기합니다.

 
obj_client_system 오브젝트 - Destroy 이벤트
 
 
if global.client_socket<0{ network_destroy( global.client_socket ); }
buffer_delete( global.net_buffer );
 
ds_map_destroy( global.player_map );
 

 

마찬가지로 [Game End 이벤트]를 추가하고, 파기해야하는 기능은 파기하도록 합니다.

 
obj_client_system 오브젝트 - Game End 이벤트
 
 
if global.client_socket<0{ network_destroy( global.client_socket ); }
buffer_delete( global.net_buffer );
 
ds_map_destroy( global.player_map );
 

 

클라이언트가 연결을 정상적으로 해제 했을 때, 연결했던 서버 async_load 맵의 "type"은 network_type_disconnect 를 트리거합니다.

 

[Room Start 이벤트]를 추가하고, 룸으로 이동할 때마다 카메라를 자동으로 설정하도록 합니다.

 
obj_client_system 오브젝트 - Room Start 이벤트
 
 
set_carmera_auto( ); //카메라 자동 설정
 
//메인 화면으로 갔을 때는 클라이언트 종료
if ( room == Room_open ){ instance_destroy( ); }
 

 

 

 

 

 

 

 

 

 

300x250

댓글